├── service
├── .gitignore
├── .eslintignore
├── .prettierignore
├── .ionide
│ └── symbolCache.db
├── nodemon.json
├── .prettierrc
├── src
│ ├── tests
│ │ ├── eth.js
│ │ ├── db.js
│ │ ├── rpc.js
│ │ ├── bsc.js
│ │ └── utils.js
│ ├── controllers
│ │ ├── index.js
│ │ ├── BalanceController.js
│ │ ├── IndexController.js
│ │ └── SubmitController.js
│ ├── models
│ │ ├── Data.js
│ │ ├── Transfer.js
│ │ ├── Transaction.js
│ │ └── Conversion.js
│ ├── modules
│ │ ├── eth.js
│ │ ├── index.js
│ │ ├── data.js
│ │ ├── db.js
│ │ ├── bsc.js
│ │ ├── rpc.js
│ │ ├── web3.js
│ │ └── chores.js
│ ├── server.js
│ ├── app.js
│ └── utils
│ │ ├── config.js
│ │ └── index.js
├── Dockerfile
├── package.json
└── abi
│ └── WBGL.json
├── app
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── assets
│ │ ├── images
│ │ │ └── logo.png
│ │ └── abi
│ │ │ └── WBGL.json
│ ├── utils
│ │ ├── config.js
│ │ ├── index.js
│ │ └── wallet.js
│ ├── reportWebVitals.js
│ ├── components
│ │ ├── CheckWalletConnection.js
│ │ ├── ConnectWallet.js
│ │ ├── Header.js
│ │ ├── App.js
│ │ ├── Footer.js
│ │ ├── BglToWbgl.js
│ │ └── WbglToBgl.js
│ └── index.js
├── .gitignore
├── Dockerfile
├── package.json
└── README.md
├── docker
├── BGL.conf
└── Dockerfile
├── .github
└── FUNDING.yml
├── docker-compose.example.yml
├── README.md
└── .gitignore
/service/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | .ionide
--------------------------------------------------------------------------------
/service/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
--------------------------------------------------------------------------------
/service/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
--------------------------------------------------------------------------------
/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BitgesellOfficial/wbgl-bridge/HEAD/app/public/favicon.ico
--------------------------------------------------------------------------------
/app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BitgesellOfficial/wbgl-bridge/HEAD/app/public/logo192.png
--------------------------------------------------------------------------------
/app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BitgesellOfficial/wbgl-bridge/HEAD/app/public/logo512.png
--------------------------------------------------------------------------------
/app/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BitgesellOfficial/wbgl-bridge/HEAD/app/src/assets/images/logo.png
--------------------------------------------------------------------------------
/service/.ionide/symbolCache.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BitgesellOfficial/wbgl-bridge/HEAD/service/.ionide/symbolCache.db
--------------------------------------------------------------------------------
/service/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": false,
3 | "watch": ["src"],
4 | "env": {
5 | "NODE_ENV": "development"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/service/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "printWidth": 80,
5 | "singleQuote": false,
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/docker/BGL.conf:
--------------------------------------------------------------------------------
1 | rpcbind=0.0.0.0
2 | rpcport=8455
3 | rpcallowip=0.0.0.0/0
4 | rpcuser=localuser
5 | rpcpassword=devpass
6 |
7 | txindex=1
8 | fallbackfee=0.0001
9 |
--------------------------------------------------------------------------------
/app/src/utils/config.js:
--------------------------------------------------------------------------------
1 | export const appTitle = process.env.REACT_APP_TITLE || 'Bitgesell-WBGL Bridge'
2 |
3 | export const serviceUrl = process.env.REACT_APP_SERVICE_URL
4 |
5 | export const isTest = process.env.NODE_ENV !== 'production'
6 |
--------------------------------------------------------------------------------
/service/src/tests/eth.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import { Eth } from "../modules/index.js";
3 |
4 | describe("instantiate ethereum via Web3Base", () => {
5 | it("should return handle to communicate ethereum", () => {
6 | assert.ok(Eth);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/service/src/controllers/index.js:
--------------------------------------------------------------------------------
1 | import * as IndexController from "./IndexController.js";
2 | import * as BalanceController from "./BalanceController.js";
3 | import * as SubmitController from "./SubmitController.js";
4 |
5 | export { IndexController, BalanceController, SubmitController };
6 |
--------------------------------------------------------------------------------
/service/src/models/Data.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const schema = new mongoose.Schema({
4 | name: { type: String, required: true, unique: true },
5 | value: { type: mongoose.Schema.Types.Mixed, required: true },
6 | });
7 |
8 | export default mongoose.model("Data", schema);
9 |
--------------------------------------------------------------------------------
/service/src/modules/eth.js:
--------------------------------------------------------------------------------
1 | import { confirmations, eth } from "../utils/config.js";
2 | import Web3Base from "./web3.js";
3 |
4 | export default new Web3Base(
5 | eth.endpoint,
6 | "eth",
7 | eth.contract,
8 | eth.account,
9 | eth.key,
10 | "nonce",
11 | confirmations.eth,
12 | );
13 |
--------------------------------------------------------------------------------
/service/src/modules/index.js:
--------------------------------------------------------------------------------
1 | import eth from "./eth.js";
2 | import bsc from "./bsc.js";
3 |
4 | export * as RPC from "./rpc.js";
5 | export * as Db from "./db.js";
6 | export * as Data from "./data.js";
7 | export * as Chores from "./chores.js";
8 |
9 | export const Eth = eth;
10 | export const Bsc = bsc;
11 |
--------------------------------------------------------------------------------
/service/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:buster-slim
2 | RUN apt-get update && apt-get install -y nano
3 | RUN npm install pm2 -g
4 |
5 | ARG NODE_ENV=production
6 | ENV NODE_ENV $NODE_ENV
7 |
8 | WORKDIR /service
9 |
10 | COPY . .
11 |
12 | RUN yarn install
13 |
14 | CMD ["sh", "-c", "yarn ; pm2-runtime src/server.js"]
15 |
--------------------------------------------------------------------------------
/service/src/controllers/BalanceController.js:
--------------------------------------------------------------------------------
1 | import { Bsc, Eth, RPC } from "../modules/index.js";
2 |
3 | export const bgl = async (_req, res) =>
4 | res.json(Math.floor(await RPC.getBalance()));
5 | export const eth = async (_req, res) =>
6 | res.json(parseInt(await Eth.getWBGLBalance()));
7 | export const bsc = async (_req, res) =>
8 | res.json(parseInt(await Bsc.getWBGLBalance()));
9 |
--------------------------------------------------------------------------------
/service/src/tests/db.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import { Db } from "../modules/index.js";
3 |
4 | describe("init()", () => {
5 | it("should return true for database init", async () => {
6 | if (!Db.isConnected()) {
7 | let isconnected = await Db.init();
8 | assert.equal(isconnected, true);
9 | } else {
10 | assert.equal(Db.isConnected(), true);
11 | }
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/app/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/service/src/modules/data.js:
--------------------------------------------------------------------------------
1 | import Data from "../models/Data.js";
2 |
3 | export async function get(name, defaultValue = null) {
4 | const record = await Data.findOne({ name }).exec();
5 | return record
6 | ? record["value"]
7 | : defaultValue instanceof Function
8 | ? await defaultValue()
9 | : defaultValue;
10 | }
11 |
12 | export async function set(name, value) {
13 | await Data.updateOne({ name }, { value }, { upsert: true });
14 | }
15 |
--------------------------------------------------------------------------------
/service/src/models/Transfer.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const schema = new mongoose.Schema(
4 | {
5 | id: { type: String, required: true },
6 | type: { type: String, required: true, enum: ["bgl", "wbgl"] },
7 | chain: { type: String, enum: ["eth", "bsc"], default: "eth" },
8 | from: { type: String, required: true },
9 | to: { type: String, required: true },
10 | },
11 | { timestamps: true },
12 | );
13 |
14 | export default mongoose.model("Transfer", schema);
15 |
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | # pull base image
2 | FROM node:buster-slim
3 |
4 | # set our node environment, either development or production
5 | # defaults to production, compose overrides this to development on build and run
6 | ARG NODE_ENV=production
7 | ENV NODE_ENV $NODE_ENV
8 |
9 | # default to port 19006 for node, and 19001 and 19002 (tests) for debug
10 | ARG PORT=19006
11 | ENV PORT $PORT
12 | EXPOSE $PORT 19001 19002
13 |
14 | WORKDIR /app
15 | ENV PATH /app/.bin:$PATH
16 | COPY ./package.json ./
17 | RUN yarn install
18 |
--------------------------------------------------------------------------------
/service/src/server.js:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import app from "./app.js";
3 | import { port } from "./utils/config.js";
4 | import { Db, Chores } from "./modules/index.js";
5 |
6 | const server = http.createServer(app);
7 | server.listen(parseInt(port), () => {
8 | console.log(`listening on *:${port}`);
9 | });
10 |
11 | process.on("uncaughtException", async (error) => {
12 | console.log("UNCAUGHT EXCEPTION:", error);
13 | await Db.close();
14 | process.exit(1);
15 | });
16 |
17 | await Db.init();
18 | await Chores.init();
19 |
--------------------------------------------------------------------------------
/app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/components/CheckWalletConnection.js:
--------------------------------------------------------------------------------
1 | import {useMetaMask} from 'metamask-react'
2 | import React from 'react'
3 | import {isChainValid} from '../utils'
4 | import {isTest} from '../utils/config'
5 |
6 | function CheckWalletConnection({children}) {
7 | const {status, chainId} = useMetaMask()
8 | if (status !== 'connected') {
9 | return 'Please connect wallet.'
10 | } else if (!isChainValid(chainId)) {
11 | return `Please connect your wallet to either Ethereum ${isTest ? '(Ropsten)' : ''} or Binance Smart Chain ${isTest ? 'Testnet' : 'Mainnet'}.`
12 | }
13 | return children
14 | }
15 |
16 | export default CheckWalletConnection
17 |
--------------------------------------------------------------------------------
/app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import {MetaMaskProvider} from 'metamask-react'
4 | import App from './components/App'
5 | import reportWebVitals from './reportWebVitals'
6 |
7 | ReactDOM.render(
8 |
9 |
10 |
11 |
12 | ,
13 | document.getElementById('root'),
14 | )
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals()
20 |
--------------------------------------------------------------------------------
/service/src/models/Transaction.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const schema = new mongoose.Schema(
4 | {
5 | type: { type: String, required: true },
6 | chain: { type: String, enum: ["eth", "bsc"] },
7 | id: { type: String, required: true },
8 | transfer: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | required: true,
11 | ref: "Transfer",
12 | },
13 | address: { type: String, required: true },
14 | amount: { type: Number, required: true },
15 | blockHash: { type: String, required: true },
16 | time: { type: Date, required: true },
17 | },
18 | { timestamps: true },
19 | );
20 |
21 | export default mongoose.model("Transaction", schema);
22 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bullseye-slim
2 |
3 | RUN apt-get update -y \
4 | && apt-get install curl ca-certificates apt-transport-https bash perl -y \
5 | && apt-get clean
6 |
7 | RUN curl -L "http://security.ubuntu.com/ubuntu/pool/main/p/perl/perl-modules-5.30_5.30.0-9ubuntu0.2_all.deb" -o "/var/tmp/perl-modules.deb" \
8 | && curl -L "https://github.com/BitgesellOfficial/bitgesell/releases/download/0.1.7/bitgesell_0.1.7_amd64.deb" -o "/var/tmp/bitgesell.deb" \
9 | && dpkg -i "/var/tmp/perl-modules.deb" \
10 | && dpkg -i "/var/tmp/bitgesell.deb" \
11 | && apt-get install -y -f \
12 | && rm -rf "/var/tmp/*"
13 |
14 | WORKDIR "/root/.BGL"
15 |
16 | COPY BGL.conf .
17 |
18 | EXPOSE 8455
19 |
20 | VOLUME "/root/.BGL"
21 |
22 | CMD ["BGLd"]
23 |
--------------------------------------------------------------------------------
/service/src/modules/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { mongo } from "../utils/config.js";
3 |
4 | let dbConnected = false;
5 |
6 | export const init = async () => {
7 | try {
8 | await mongoose.connect(mongo.url, {
9 | dbName: mongo.database,
10 | useNewUrlParser: true,
11 | useUnifiedTopology: true,
12 | useCreateIndex: true,
13 | });
14 | } catch (error) {
15 | setTimeout(() => {
16 | console.log("DB failed to start");
17 | init();
18 | }, 10000);
19 | return (dbConnected = false);
20 | }
21 | return (dbConnected = true);
22 | };
23 |
24 | export const isConnected = () => {
25 | return dbConnected;
26 | };
27 |
28 | export const close = async () => {
29 | await mongoose.disconnect();
30 | dbConnected = false;
31 | };
32 |
--------------------------------------------------------------------------------
/app/src/components/ConnectWallet.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {useMetaMask} from 'metamask-react'
3 | import {Button, Chip} from '@material-ui/core'
4 | import {chainLabel, isChainValid} from '../utils'
5 |
6 | function ConnectWallet() {
7 | const {status, chainId, connect} = useMetaMask()
8 |
9 | switch (status) {
10 | case 'initializing':
11 | return (
Synchronizing...
)
12 |
13 | case 'unavailable':
14 | return (Please install Metamask!
)
15 |
16 | case 'notConnected':
17 | return ()
18 |
19 | case 'connecting':
20 | return (Connecting...
)
21 |
22 | case 'connected':
23 | return ()
24 | }
25 | }
26 |
27 | export default ConnectWallet
28 |
--------------------------------------------------------------------------------
/service/src/models/Conversion.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const schema = new mongoose.Schema(
4 | {
5 | type: { type: String, required: true },
6 | chain: { type: String, enum: ["eth", "bsc"], default: "eth" },
7 | transfer: {
8 | type: mongoose.Schema.Types.ObjectId,
9 | required: true,
10 | ref: "Transfer",
11 | },
12 | transaction: {
13 | type: mongoose.Schema.Types.ObjectId,
14 | required: true,
15 | ref: "Transaction",
16 | },
17 | address: { type: String, required: true },
18 | amount: { type: Number, required: true },
19 | sendAmount: { type: Number },
20 | txid: String,
21 | nonce: Number,
22 | receipt: Object,
23 | returnTxid: String,
24 | status: { type: String, default: "pending" },
25 | txChecks: Number,
26 | },
27 | { timestamps: true },
28 | );
29 |
30 | export default mongoose.model("Conversion", schema);
31 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: vporton # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
--------------------------------------------------------------------------------
/service/src/modules/bsc.js:
--------------------------------------------------------------------------------
1 | import Common from "ethereumjs-common";
2 | import { bsc, confirmations } from "../utils/config.js";
3 | import Web3Base from "./web3.js";
4 |
5 | class Bsc extends Web3Base {
6 | async getNetworkId() {
7 | if (!this.networkId) {
8 | this.networkId = await this.web3.eth.net.getId();
9 | }
10 | return this.networkId;
11 | }
12 |
13 | async getChainId() {
14 | if (!this.chainId) {
15 | this.chainId = await this.web3.eth.getChainId();
16 | }
17 | return this.chainId;
18 | }
19 |
20 | async transactionOpts() {
21 | const params = {
22 | name: "bnb",
23 | networkId: await this.getNetworkId(),
24 | chainId: await this.getChainId(),
25 | };
26 | const common = Common.default.forCustomChain(
27 | "mainnet",
28 | params,
29 | "petersburg",
30 | );
31 | return { common };
32 | }
33 | }
34 |
35 | export default new Bsc(
36 | bsc.endpoint,
37 | "bsc",
38 | bsc.contract,
39 | bsc.account,
40 | bsc.key,
41 | "bscNonce",
42 | confirmations.bsc,
43 | );
44 |
--------------------------------------------------------------------------------
/service/src/app.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cors from "cors";
3 | import { port } from "./utils/config.js";
4 | import {
5 | BalanceController,
6 | IndexController,
7 | SubmitController,
8 | } from "./controllers/index.js";
9 |
10 | const app = express();
11 | app.set("port", port);
12 | app.use(cors());
13 | app.use(express.json());
14 |
15 | app.use(function(req, res, next) {
16 | res.header("Access-Control-Allow-Origin", "*");
17 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
18 | res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
19 | next();
20 | });
21 |
22 | app.get("/", IndexController.healthCheck);
23 | app.get("/state", IndexController.state);
24 | app.get("/contracts", IndexController.contracts);
25 |
26 | app.get("/balance/bgl", BalanceController.bgl);
27 | app.get("/balance/eth", BalanceController.eth);
28 | app.get("/balance/bsc", BalanceController.bsc);
29 |
30 | app.post("/submit/bgl", SubmitController.bglToWbgl);
31 | app.post("/submit/wbgl", SubmitController.wbglToBgl);
32 |
33 | export default app;
34 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bgl-wbgl-bridge",
3 | "version": "0.3.1",
4 | "license": "MIT",
5 | "dependencies": {
6 | "@fontsource/roboto": "^4.3.0",
7 | "@material-ui/core": "^4.11.4",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "metamask-react": "^2.4.0",
12 | "react": "^17.0.2",
13 | "react-dom": "^17.0.2",
14 | "react-hook-form": "^7.6.6",
15 | "react-scripts": "4.0.3",
16 | "swr": "^0.5.6",
17 | "web-vitals": "^1.0.1",
18 | "web3": "^1.7.5"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Box, Container, Link, makeStyles, Typography} from '@material-ui/core'
3 | import {blueGrey} from '@material-ui/core/colors'
4 |
5 | import logo from '../assets/images/logo.png'
6 | import {appTitle} from '../utils/config'
7 | import ConnectWallet from './ConnectWallet'
8 |
9 | function Header() {
10 | const classes = useStyles()
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
{appTitle}
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | const bgColor = blueGrey[700]
29 |
30 | const useStyles = makeStyles(theme => ({
31 | container: {},
32 | logo: {
33 | marginLeft: 10,
34 | marginRight: 5,
35 | height: 33,
36 | verticalAlign: 'middle',
37 | },
38 | }))
39 |
40 | export default Header
41 |
--------------------------------------------------------------------------------
/app/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import {isTest, serviceUrl} from './config'
2 |
3 | export const url = path => serviceUrl + path
4 |
5 | export const fetcher = url => fetch(url).then(res => res.json())
6 |
7 | export const post = async (url, data) => {
8 | const response = await fetch(url, {
9 | method: 'POST',
10 | mode: 'cors',
11 | cache: 'no-cache',
12 | headers: {
13 | 'Content-Type': 'application/json',
14 | },
15 | body: JSON.stringify(data),
16 | })
17 | return response.json()
18 | }
19 |
20 | export function chainLabel(chainId) {
21 | const names = {
22 | eth: 'Ethereum',
23 | bsc: 'Binance Smart Chain',
24 | }
25 | let chain = chainId
26 |
27 | if (!['eth', 'bsc'].includes(chainId)) {
28 | chain = isChainBsc(chainId) ? 'bsc' : 'eth'
29 | }
30 |
31 | return names[chain]
32 | }
33 |
34 | export function isChainValid(chainId) {
35 | return isTest ? [3, 97, '0x3', '0x61'].includes(chainId) : [1, 56, '0x1', '0x38'].includes(chainId)
36 | }
37 |
38 | export const isChainBsc = chainId => (typeof chainId == 'string' ? ['0x38', '0x61'] : [56, 97]).includes(chainId)
39 |
40 | let contracts
41 | export async function getTokenAddress(chainId) {
42 | if (!contracts) {
43 | contracts = await (await fetch(url('/contracts'))).json()
44 | }
45 | return contracts[isChainBsc(chainId) ? 'bsc' : 'eth']
46 | }
47 |
--------------------------------------------------------------------------------
/service/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bgl-wbgl-bridge-service",
3 | "version": "0.3.1",
4 | "description": "BGL-WBGL(ETH) bridge",
5 | "main": "src/server.js",
6 | "license": "MIT",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "nodemon -L --inspect=0.0.0.0:9229 src/server.js",
10 | "test": "mocha src/tests/**/*.js -r dotenv/config --timeout 10000",
11 | "lint:check": "eslint .",
12 | "lint:fix": "eslint --fix",
13 | "prettier:check": "prettier --check .",
14 | "prettier:fix": "prettier --write \"**/*.js\" .prettierrc --config ./.prettierrc",
15 | "format": "npm run lint:fix && npm run prettier:fix"
16 | },
17 | "dependencies": {
18 | "bitcoin-core": "^3.0.0",
19 | "cors": "^2.8.5",
20 | "dotenv": "^16.0.0",
21 | "ethereumjs-common": "^1.5.2",
22 | "ethereumjs-tx": "^2.1.2",
23 | "express": "^4.17.1",
24 | "mongoose": "^5.12.10",
25 | "web3": "^1.3.6"
26 | },
27 | "devDependencies": {
28 | "eslint": "^8.12.0",
29 | "eslint-config-prettier": "^8.5.0",
30 | "eslint-plugin-prettier": "^4.0.0",
31 | "mocha": "^9.2.2",
32 | "nodemon": "^2.0.7",
33 | "pre-commit": "^1.2.2",
34 | "prettier": "^2.6.2",
35 | "should": "^13.2.3"
36 | },
37 | "options": {
38 | "mocha": "--timeout 20000 --recursive --require should"
39 | },
40 | "pre-commit": [
41 | "lint"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/utils/wallet.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react'
2 | import {useMetaMask} from 'metamask-react'
3 | import Web3 from 'web3'
4 |
5 | import abi from '../assets/abi/WBGL.json'
6 | import {getTokenAddress, isChainValid} from './index'
7 |
8 | let web3
9 | let WBGL
10 | let interval
11 |
12 | export function getWeb3(ethereum) {
13 | if (!web3) {
14 | web3 = new Web3(ethereum)
15 | }
16 | return web3
17 | }
18 |
19 | export function useWbglBalance() {
20 | const [balance, setBalance] = useState('')
21 | const {ethereum, account, chainId} = useMetaMask()
22 |
23 | const fetch = async () => {
24 | const current = web3.utils.fromWei(await WBGL.methods['balanceOf'](account).call(), 'ether')
25 | setBalance(current)
26 | }
27 |
28 | useEffect(() => {
29 | if (isChainValid(chainId)) {
30 | getTokenAddress(chainId).then(async contractAddress => {
31 | const web3 = getWeb3(ethereum)
32 | WBGL = new web3.eth.Contract(abi, contractAddress)
33 |
34 | await fetch()
35 |
36 | interval = setInterval(fetch, 30000)
37 | })
38 |
39 | return () => clearInterval(interval)
40 | }
41 | }, [])
42 |
43 | return balance
44 | }
45 |
46 | export async function sendWbgl(chainId, from, to, amount, ethereum) {
47 | const web3 = getWeb3(ethereum)
48 | const value = web3.utils.toWei(amount, 'ether');
49 |
50 | WBGL = WBGL || new web3.eth.Contract(abi, await getTokenAddress(chainId))
51 |
52 | await WBGL.methods.transfer(to, value).send({from})
53 | }
54 |
--------------------------------------------------------------------------------
/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | %REACT_APP_TITLE%
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/service/src/modules/rpc.js:
--------------------------------------------------------------------------------
1 | import Client from "bitcoin-core";
2 | import { rpc, confirmations } from "../utils/config.js";
3 |
4 | let client;
5 |
6 | export const getClient = () => {
7 | if (!client) {
8 | client = new Client(rpc);
9 | }
10 | return client;
11 | };
12 |
13 | export const getBlockchainInfo = async () =>
14 | await getClient().command("getblockchaininfo");
15 |
16 | export const getBlockCount = async () =>
17 | await getClient().command("getblockcount");
18 |
19 | export const getBalance = async () => await getClient().command("getbalance");
20 |
21 | export const validateAddress = async (address) =>
22 | (await getClient().command("validateaddress", address)).isvalid;
23 |
24 | export const createAddress = async () =>
25 | await getClient().command("getnewaddress");
26 |
27 | export const tips = async () => await getClient().getChainTips();
28 | export const generate = async (tip) => await getClient().generate(tip);
29 |
30 | export const listSinceBlock = async (
31 | blockHash,
32 | confirmation = confirmations.bgl,
33 | ) => {
34 | return await getClient().command(
35 | "listsinceblock",
36 | blockHash ? blockHash : undefined,
37 | confirmation,
38 | );
39 | };
40 |
41 | export const getTransactionFromAddress = async (txid) => {
42 | const rawTx = await getClient().command("getrawtransaction", txid, true);
43 | const vin = rawTx["vin"][0];
44 | const txIn = await getClient().command("getrawtransaction", vin.txid, true);
45 | return txIn["vout"][vin["vout"]]["scriptPubKey"]["address"];
46 | };
47 |
48 | export const send = async (address, amount) =>
49 | await getClient().command("sendtoaddress", address, amount);
50 |
--------------------------------------------------------------------------------
/service/src/utils/config.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config();
3 |
4 | export const env = process.env.NODE_ENV || "development";
5 |
6 | export const port = process.env.PORT || "8080";
7 |
8 | const rpcConfig = {
9 | host: process.env.RPC_HOST || "localhost",
10 | port: process.env.RPC_PORT || "8332",
11 | };
12 | if (process.env.hasOwnProperty("RPC_USER") && process.env.RPC_USER) {
13 | rpcConfig.username = process.env.RPC_USER;
14 | }
15 | if (process.env.hasOwnProperty("RPC_PASSWORD") && process.env.RPC_PASSWORD) {
16 | rpcConfig.password = process.env.RPC_PASSWORD;
17 | }
18 | if (process.env.hasOwnProperty("RPC_WALLET") && process.env.RPC_WALLET) {
19 | rpcConfig.wallet = process.env.RPC_WALLET;
20 | }
21 | export const rpc = rpcConfig;
22 |
23 | export const eth = {
24 | endpoint: process.env.ETH_ENDPOINT,
25 | account: process.env.ETH_ACCOUNT,
26 | key: Buffer.from(process.env.ETH_PRIVKEY, "hex"),
27 | contract: process.env.ETH_CONTRACT_ADDRESS,
28 | };
29 |
30 | export const bsc = {
31 | endpoint: process.env.BSC_ENDPOINT,
32 | account: process.env.BSC_ACCOUNT,
33 | key: Buffer.from(process.env.BSC_PRIVKEY, "hex"),
34 | contract: process.env.BSC_CONTRACT_ADDRESS,
35 | };
36 |
37 | export const mongo = {
38 | url: process.env.DB_CONNECTION,
39 | database: process.env.DB_DATABASE || "wbgl_bridge",
40 | };
41 |
42 | export const confirmations = {
43 | bgl: parseInt(process.env.BGL_MIN_CONFIRMATIONS) || 3,
44 | eth: parseInt(process.env.ETH_MIN_CONFIRMATIONS) || 3,
45 | bsc: parseInt(process.env.BSC_MIN_CONFIRMATIONS) || 3,
46 | };
47 |
48 | export const feePercentage = process.env.FEE_PERCENTAGE || 1;
49 |
50 | export const nonces = {
51 | bsc: parseInt(process.env.BSC_NONCE) || 0,
52 | eth: parseInt(process.env.ETH__NONCE) || 0,
53 | };
54 |
--------------------------------------------------------------------------------
/app/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import {Box, Container, CssBaseline, makeStyles, Tab, Tabs} from '@material-ui/core'
3 | import Header from './Header'
4 | import Footer from './Footer'
5 | import BglToWbgl from './BglToWbgl'
6 | import WbglToBgl from './WbglToBgl'
7 |
8 | import '@fontsource/roboto'
9 |
10 | function TabPanel(props) {
11 | const {children, value, index, ...other} = props
12 |
13 | return (
14 |
20 | {value === index && (
21 |
22 | {children}
23 |
24 | )}
25 |
26 | )
27 | }
28 |
29 | function App() {
30 | const classes = useStyles()
31 | const [tab, setTab] = useState(0)
32 | const changeTab = (_event, newValue) => {
33 | setTab(newValue)
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | const useStyles = makeStyles(theme => ({
62 | container: {
63 | height: '100%',
64 | },
65 |
66 | }))
67 |
68 | export default App
69 |
--------------------------------------------------------------------------------
/service/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 | import { RPC } from "../modules/index.js";
3 |
4 | export const isValidEthAddress = (address) =>
5 | /^0x[a-fA-F0-9]{40}$/i.test(address);
6 |
7 | export const isValidBglAddress = async (address) =>
8 | RPC.validateAddress(address);
9 |
10 | export const sha3 = (value) => Web3.utils.sha3(value).substring(2);
11 |
12 | const bn = Web3.utils.toBN;
13 |
14 | export function isString(s) {
15 | return typeof s === "string" || s instanceof String;
16 | }
17 |
18 | export function toBaseUnit(value, decimals) {
19 | if (!isString(value)) {
20 | throw new Error("Pass strings to prevent floating point precision issues.");
21 | }
22 | const ten = bn(10);
23 | const base = ten.pow(bn(decimals));
24 |
25 | // Is it negative?
26 | let negative = value.substring(0, 1) === "-";
27 | if (negative) {
28 | value = value.substring(1);
29 | }
30 |
31 | if (value === ".") {
32 | throw new Error(
33 | `Invalid value ${value} cannot be converted to` +
34 | ` base unit with ${decimals} decimals.`,
35 | );
36 | }
37 |
38 | // Split it into a whole and fractional part
39 | let comps = value.split(".");
40 | if (comps.length > 2) {
41 | throw new Error("Too many decimal points");
42 | }
43 |
44 | let whole = comps[0],
45 | fraction = comps[1];
46 |
47 | if (!whole) {
48 | whole = "0";
49 | }
50 | if (!fraction) {
51 | fraction = "0";
52 | }
53 | if (fraction.length > decimals) {
54 | throw new Error("Too many decimal places");
55 | }
56 |
57 | while (fraction.length < decimals) {
58 | fraction += "0";
59 | }
60 |
61 | whole = bn(whole);
62 | fraction = bn(fraction);
63 | let wei = whole.mul(base).add(fraction);
64 |
65 | if (negative) {
66 | wei = wei.neg();
67 | }
68 |
69 | return bn(wei.toString(10));
70 | }
71 |
--------------------------------------------------------------------------------
/service/src/controllers/IndexController.js:
--------------------------------------------------------------------------------
1 | import { Db, RPC, Eth, Bsc } from "../modules/index.js";
2 | import { eth, bsc } from "../utils/config.js";
3 |
4 | export const healthCheck = async (_req, res) => {
5 | try {
6 | await RPC.getBalance();
7 | if (!Db.isConnected()) {
8 | res.json(500, {
9 | status: "error",
10 | message: "Database connection not available",
11 | });
12 | }
13 | res.json({
14 | status: "ok",
15 | });
16 | } catch (e) {
17 | console.log(e);
18 | res.status(500).json({ status: "error", message: "RPC not available" });
19 | }
20 | };
21 |
22 | export const state = async (_req, res) => {
23 | try {
24 | const blockchainInfo = await RPC.getBlockchainInfo();
25 | if (!Db.isConnected()) {
26 | res.json(500, {
27 | status: "error",
28 | message: "Database connection not available",
29 | });
30 | }
31 | res.json({
32 | status: "ok",
33 | BGL: {
34 | blockchainInfo,
35 | blockCount: await RPC.getBlockCount(),
36 | balance: await RPC.getBalance(),
37 | },
38 | ETH: {
39 | chain: Eth.getChain(),
40 | gasPrice: await Eth.getGasPrice(),
41 | wbglBalance: await Eth.getWBGLBalance(),
42 | transactionCount: await Eth.getTransactionCount(),
43 | },
44 | BSC: {
45 | chain: Bsc.getChain(),
46 | gasPrice: await Bsc.getGasPrice(),
47 | wbglBalance: await Bsc.getWBGLBalance(),
48 | transactionCount: await Bsc.getTransactionCount(),
49 | },
50 | });
51 | } catch (e) {
52 | console.log(e);
53 | res.status(500).json({ status: "error", message: "RPC not available" });
54 | }
55 | };
56 |
57 | export const contracts = async (_req, res) => {
58 | res.json({
59 | eth: eth.contract,
60 | bsc: bsc.contract,
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/service/src/tests/rpc.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import {
3 | tips,
4 | generate,
5 | getBlockchainInfo,
6 | getBlockCount,
7 | getBalance,
8 | createAddress,
9 | listSinceBlock,
10 | getTransactionFromAddress,
11 | validateAddress,
12 | } from "../modules/rpc.js";
13 |
14 | describe("REST", () => {
15 | before(async () => {
16 | const [tip] = await tips();
17 |
18 | if (tip.height >= 100000) {
19 | return null;
20 | }
21 |
22 | await generate(100000);
23 | });
24 |
25 | describe("getBlockchainInfo()", () => {
26 | it("should return blockchain information", async () => {
27 | let info = await getBlockchainInfo();
28 | assert.ok(info);
29 | });
30 | });
31 | describe("getBlockCount()", () => {
32 | it("Is block count valid", async () => {
33 | let block = await getBlockCount();
34 | assert.ok(block);
35 | });
36 | });
37 | describe("createAddress()", () => {
38 | it("should create new address", async () => {
39 | let address = await createAddress();
40 | assert.ok(validateAddress(address));
41 | });
42 | });
43 | describe("getBalance()", () => {
44 | it("should get wallet balance", async () => {
45 | let amount = await getBalance();
46 | assert.ok(amount >= 0);
47 | });
48 | });
49 | describe("listSinceBlock()", () => {
50 | it("list blocks", async () => {
51 | let blockHash =
52 | "000000000000028f3c217bfe1c873d8c2d9de6104f5add4299d43dd127564135";
53 | let result = await listSinceBlock(blockHash);
54 | assert.ok(result.lastblock);
55 | });
56 | });
57 | describe("#getTransactionFromAddress", () => {
58 | it("Get address from txid", async () => {
59 | let txid =
60 | "6e8027d688ec2bcc4dcf137475ee5611ad306f7a64b38f0a28a0fffe98fce8aa";
61 | let address = await getTransactionFromAddress(txid);
62 | assert.equal(validateAddress(address), true);
63 | assert.ok(address);
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/app/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Box, Container, makeStyles, Typography} from '@material-ui/core'
3 | import {grey} from '@material-ui/core/colors'
4 | import useSWR from 'swr'
5 | import {fetcher, url} from '../utils'
6 |
7 | const textColor = grey[700]
8 | const bgColor = grey[300]
9 |
10 | function useBalance(id) {
11 | const {data, error} = useSWR(url(`/balance/${id}`), fetcher, {refreshInterval: 60000})
12 | return {
13 | balance: data,
14 | isLoading: !error && typeof data !== 'number',
15 | isError: error,
16 | }
17 | }
18 |
19 | function Footer() {
20 | const {balance: bglBalance, isLoading: bglLoading} = useBalance('bgl')
21 | const {balance: ethBalance, isLoading: ethLoading} = useBalance('eth')
22 | const {balance: bscBalance, isLoading: bscLoading} = useBalance('bsc')
23 | const classes = useStyles()
24 | const balanceClass = isLoading => isLoading ? classes.pulsing : undefined
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | Balances:
32 |
33 |
34 |
35 | BGL: {bglLoading ? 'Loading...' : bglBalance}
36 |
37 |
38 |
39 |
40 | WBGL (Eth): {ethLoading ? 'Loading...' : ethBalance}
41 |
42 |
43 |
44 |
45 | WBGL (BSC): {bscLoading ? 'Loading...' : bscBalance}
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | const useStyles = makeStyles(theme => ({
55 | none: {},
56 | pulsing: {
57 | opacity: 0.3,
58 | animation: '$pulse 0.7s alternate infinite',
59 | },
60 | '@keyframes pulse': {
61 | '0%': {
62 | opacity: 1,
63 | },
64 | '100%': {
65 | opacity: 0.3,
66 | },
67 | },
68 | }))
69 |
70 | export default Footer
71 |
--------------------------------------------------------------------------------
/app/src/components/BglToWbgl.js:
--------------------------------------------------------------------------------
1 | import {useMetaMask} from 'metamask-react'
2 | import {useState, Fragment} from 'react'
3 | import {useForm} from 'react-hook-form'
4 | import {
5 | Box,
6 | Button,
7 | List, ListItemText,
8 | Typography,
9 | } from '@material-ui/core'
10 | import {post, url, chainLabel, isChainBsc} from '../utils'
11 | import CheckWalletConnection from './CheckWalletConnection'
12 |
13 | function BglToWbgl() {
14 | const {handleSubmit} = useForm()
15 | const [submitting, setSubmitting] = useState(false)
16 | const [sendAddress, setSendAddress] = useState(false)
17 | const [balance, setBalance] = useState(0)
18 | const [feePercentage, setFeePercentage] = useState(0)
19 | const {chainId, account} = useMetaMask()
20 |
21 | const onSubmit = () => {
22 | const data = {
23 | chain: isChainBsc(chainId) ? 'bsc' : 'eth',
24 | address: account,
25 | }
26 |
27 | setSubmitting(true)
28 |
29 | post(url('/submit/bgl'), data).then(response => {
30 | setSendAddress(response.bglAddress)
31 | setBalance(Math.floor(response.balance))
32 | setFeePercentage(response.feePercentage)
33 | }).catch(result => {
34 | console.error('Error submitting form:', result)
35 | }).finally(() => setSubmitting(false))
36 | }
37 |
38 | return !sendAddress ? (
39 |
40 |
49 |
50 | ) : (
51 |
52 | Send BGL to: {sendAddress}
53 |
54 | The currently available WBGL balance is {balance}. If you send more BGL than is available to complete the exchange, your BGL will be returned to your address.
55 |
56 |
57 | Please note, that a fee of {feePercentage}% will be automatically deducted from the transfer amount. This exchange pair is active for 7 days.
58 |
59 |
60 | )
61 | }
62 |
63 | export default BglToWbgl
64 |
--------------------------------------------------------------------------------
/docker-compose.example.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | app:
5 | build:
6 | context: ./app
7 | args:
8 | - NODE_ENV=development
9 | depends_on:
10 | - service
11 | environment:
12 | - NODE_ENV=development
13 | - NODE_OPTIONS=--openssl-legacy-provider
14 | - CHOKIDAR_USEPOLLING=true
15 | - SSL=false
16 | - REACT_APP_SERVICE_URL=http://localhost:8480
17 | - REACT_APP_TITLE=Bitgesell-WBGL Bridge
18 | tty: true
19 | ports:
20 | - "8406:19006"
21 | - "8401:19001"
22 | - "8402:19002"
23 | volumes:
24 | - ./app:/app:delegated
25 | - ./app/package.json:/app/package.json
26 | - /app/node_modules
27 | command: yarn start
28 | healthcheck:
29 | disable: true
30 |
31 | service:
32 | build:
33 | context: ./service
34 | args:
35 | - NODE_ENV=development
36 | depends_on:
37 | - core
38 | - db
39 | environment:
40 | NODE_ENV: development
41 | PORT: 8480
42 | RPC_HOST: core
43 | RPC_PORT: 8455
44 | RPC_USER: localuser
45 | RPC_PASSWORD: devpass
46 | BGL_MIN_CONFIRMATIONS: 3
47 | ETH_ENDPOINT: ""
48 | ETH_ACCOUNT: ""
49 | ETH_PRIVKEY: ""
50 | ETH_CONTRACT_ADDRESS: ""
51 | ETH_MIN_CONFIRMATIONS: 3
52 | BSC_ENDPOINT: ""
53 | BSC_ACCOUNT: ""
54 | BSC_PRIVKEY: ""
55 | BSC_CONTRACT_ADDRESS: ""
56 | BSC_MIN_CONFIRMATIONS: 3
57 | DB_CONNECTION: mongodb://root:dev@db:27017
58 | DB_DATABASE: wbgl_bridge
59 | FEE_PERCENTAGE: 1
60 | ports:
61 | - "8480:8480"
62 | volumes:
63 | - ./service:/service:delegated
64 | - ./service/package.json:/service/package.json
65 | - /service/node_modules
66 | command: yarn dev
67 |
68 | core:
69 | build:
70 | context: ./docker
71 | volumes:
72 | - /root/.BGL
73 | ports:
74 | - "8455"
75 |
76 | db:
77 | image: mongo
78 | environment:
79 | - MONGO_INITDB_DATABASE=wbgl_bridge
80 | - MONGO_INITDB_ROOT_USERNAME=root
81 | - MONGO_INITDB_ROOT_PASSWORD=dev
82 | volumes:
83 | - mongodb:/data/db
84 | - mongodb_config:/data/configdb
85 |
86 | mongo-express:
87 | image: mongo-express
88 | ports:
89 | - "8481:8081"
90 | depends_on:
91 | - db
92 | environment:
93 | - ME_CONFIG_MONGODB_SERVER=db
94 | - ME_CONFIG_MONGODB_ADMINUSERNAME=root
95 | - ME_CONFIG_MONGODB_ADMINPASSWORD=dev
96 |
97 | volumes:
98 | mongodb:
99 | mongodb_config:
100 |
--------------------------------------------------------------------------------
/service/src/tests/bsc.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import { Bsc, Db } from "../modules/index.js";
3 |
4 | describe("instantiate BSC via Web3Base", () => {
5 | it("should return handle to communicate BSC", () => {
6 | assert.ok(Bsc);
7 | });
8 | });
9 |
10 | describe("getNetworkId()", () => {
11 | it("should return BSC network id", async () => {
12 | let id = await Bsc.getNetworkId();
13 | const BSC_NETWORK_ID = 56;
14 | assert.equal(id, BSC_NETWORK_ID);
15 | });
16 | });
17 |
18 | describe("getChainId()", () => {
19 | it("should return BSC chain id", async () => {
20 | let id = await Bsc.getChainId();
21 | const BSC_CHAIN_ID = 56;
22 | assert.equal(id, BSC_CHAIN_ID);
23 | });
24 | });
25 |
26 | describe("transactionOpts()", () => {
27 | it("should return BSC transaction options", async () => {
28 | let opts = await Bsc.transactionOpts();
29 | assert.ok(opts);
30 | });
31 | });
32 |
33 | describe("getChain()", () => {
34 | it("should return BSC chain name", () => {
35 | let chain = Bsc.getChain();
36 | const MAIN_CHAIN = "mainnet";
37 | assert.equal(chain, MAIN_CHAIN);
38 | });
39 | });
40 |
41 | describe("getGasPrice()", () => {
42 | it("should return BSC gas price", async () => {
43 | let gasPrice = await Bsc.getGasPrice();
44 | assert.ok(gasPrice > 0);
45 | });
46 | });
47 |
48 | describe("getEstimateGas()", () => {
49 | it("should return BSC gas estimate", async () => {
50 | let amount = "0.130";
51 | let gasEstimate = await Bsc.getEstimateGas(amount);
52 | assert.ok(gasEstimate > 0);
53 | });
54 | });
55 |
56 | describe("getWBGLBalance()", () => {
57 | it("should return WGBL token balance", async () => {
58 | let balance = await Bsc.getWBGLBalance();
59 | let bal = await Bsc.getWBGLBalance1(
60 | "0x2ba64efb7a4ec8983e22a49c81fa216ac33f383a",
61 | );
62 | assert.ok(balance);
63 | });
64 | });
65 |
66 | describe("getTransactionCount()", () => {
67 | it("should return BSC transaction nounce", async () => {
68 | let nounce = await Bsc.getTransactionCount();
69 | assert.ok(nounce);
70 | });
71 | });
72 |
73 | describe("getTransactionReceipt(txid)", () => {
74 | it("should return BSC transaction receipt", async () => {
75 | let txid =
76 | "0x9cd5c91c7ecac2b57c77e80f802e62c8c243e6399c6d39d970c8d74f3f118c25";
77 | let receipt = await Bsc.getTransactionReceipt(txid);
78 | assert.ok(receipt.blockNumber > 1);
79 | });
80 | });
81 |
82 | describe("sendWBGL(address, amount)", () => {
83 | it("should return BSC send WBGL", async () => {
84 | if (!Db.isConnected()) {
85 | let isconnected = await Db.init();
86 | assert.equal(isconnected, true);
87 | }
88 | let address = "0x9192b4E1Fb761658f542d9941739aD434e3d45DF";
89 | let amount = "10";
90 | let rtn = await Bsc.sendWBGL(address, amount);
91 | console.log(rtn);
92 | assert.ok(rtn);
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/service/src/tests/utils.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import {
3 | isValidEthAddress,
4 | isValidBglAddress,
5 | sha3,
6 | toBaseUnit,
7 | } from "../utils/index.js";
8 |
9 | describe("Functions inside utils folder", function () {
10 | describe("#sha3", function () {
11 | it("Should be equal because same string is used in creating the hashes", function () {
12 | let hashOfbgl =
13 | "68f2ac5a446296401ad77284ceab4f70b1b3efe0a1b0c216125af9306c11c414";
14 | let name = "bgl";
15 | assert.equal(sha3(name), hashOfbgl);
16 | });
17 | it("Should not be equal because different strings are used in creating the hashes", function () {
18 | let hashOfbgl =
19 | "68f2ac5a446296401ad77284ceab4f70b1b3efe0a1b0c216125af9306c11c414";
20 | let name = "bgls";
21 | assert.notEqual(sha3(name), hashOfbgl);
22 | });
23 | });
24 | describe("#isValidEthAddress", function () {
25 | it("This is a valid Ethereum address", function () {
26 | let address = "0x50e507cA8B4B657e1483Ac6f50D285e23EBfBA7A";
27 | assert.equal(isValidEthAddress(address), true);
28 | });
29 | it("This is not a valid Ethereum address", function () {
30 | let address = "0x50e507cA8B4B657e1483Ac6f50D285e23EBfBA7";
31 | assert.equal(isValidEthAddress(address), false);
32 | });
33 | });
34 | describe("#isValidBglAddress", function () {
35 | it("This is a valid BGL address", function () {
36 | let address = "bgl1qapzlteru5p93c6exsqvjmy34pua8q0lws0p4kg";
37 | isValidBglAddress(address)
38 | .then(function (resolved) {
39 | assert.equal(resolved, true);
40 | })
41 | .catch(function (rejected) {
42 | console.log("error :", rejected);
43 | assert.notEqual(true, false);
44 | });
45 | });
46 | it("This is not a valid bgl address", function () {
47 | let address = "bgl1qapzlteru5p93c6exsqvjmy34pua8q0lws0p4k";
48 | isValidBglAddress(address)
49 | .then(function (resolved) {
50 | assert.equal(resolved, false);
51 | })
52 | .catch(function (rejected) {
53 | console.log("error :", rejected);
54 | assert.notEqual(true, false);
55 | });
56 | });
57 | });
58 | describe("#toBaseUnit", function () {
59 | it("Valid inputs", function () {
60 | let value = "299.78";
61 | let decimals = 5;
62 | let result = toBaseUnit(value, decimals);
63 | let expected = 29978000;
64 | assert.equal(result["words"][0], expected);
65 | });
66 | it("Invalid inputs, value is dot", function () {
67 | let value = ".";
68 | let decimals = 5;
69 | assert.throws(function () {
70 | toBaseUnit(value, decimals);
71 | });
72 | });
73 | it("Invalid inputs, value is multiple dots", function () {
74 | let value = "500.34.9";
75 | let decimals = 5;
76 | assert.throws(function () {
77 | toBaseUnit(value, decimals);
78 | });
79 | });
80 | it("Invalid inputs, fraction length greater than decimals value", function () {
81 | let value = "500.34989766";
82 | let decimals = 5;
83 | assert.throws(function () {
84 | toBaseUnit(value, decimals);
85 | });
86 | });
87 | it("Invalid inputs, value not a string", function () {
88 | let value = 500;
89 | let decimals = 5;
90 | assert.throws(function () {
91 | toBaseUnit(value, decimals);
92 | });
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `yarn build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WBGL Bridge
2 |
3 | This is the prototype (proof-of-concept) version of the Bitgesell-WBGL bridge application that allows users to exchange between BGL coins and WBGL ERC-20 tokens.
4 |
5 | Consists of frontend GUI that runs in the browser (React) and communicates with the backend service, which in turn is connected to the Bitgesell network (using RPC of a running node) and an Ethereum gateway (via a websocket endpoint).
6 |
7 | ## Setup
8 |
9 | ### Backend Service
10 |
11 | Backend service is a Node.js application that exposes a port for HTTP requests. In production, it is recommended to hide it behind an SSL-enabled proxy server (such as nginx). CORS needs to be configured to enable XHR requests from the domain the frontend application is being served from.
12 |
13 | MongoDB database is used for storing data used by the service.
14 |
15 | To set up the service, go to the `service` directory, and run either of the following commands (depending on whether you use npm or yarn as the package manager):
16 | ```shell
17 | yarn
18 | ```
19 | or
20 | ```shell
21 | npm install
22 | ```
23 |
24 | Backend service is configured using environment variables that need to be set before running the application with Node.js. The following variables are supported:
25 |
26 | - `NODE_ENV`: Application environment. Should be set to `production` on production.
27 | - `PORT`: Port to listen for HTTP requests on. Defaults to `8080`.
28 | - `RPC_HOST`: Hostname or IP address of the Bitgesell node running a JSON-RPC interface. This prototype only supports single-wallet nodes. The custodial wallet should have enough BGL reserve for incoming WBGL exchanges. Defaults to `localhost`.
29 | - `RPC_PORT`: Port the JSON-RPC API is running on. Defaults to `3445`.
30 | - `RPC_USER`: The RPC server user name.
31 | - `RPC_PASSWORD`: The RPC server user password.
32 | - `ETH_ENDPOINT`: The Ethereum API endpoint (currently, only Websocket is supported). For example, an Infura WSS endpoint URL like `wss://mainnet.infura.io/ws/v3/1d17658c92194f73a0143d18fa548a66`.
33 | - `ETH_ACCOUNT`: The Ethereum address of the custodial account WBGL tokens are sent to and from. Should have enough WBGL in reserve for incoming BGL exchanges, as well as enough ether for gas.
34 | - `ETH_PRIVKEY`: The private key for the custodial account as a hexadecimal string.
35 | - `ETH_CONTRACT_ADDRESS`: The ethereum address of the WBGL ERC-20 token contract.
36 | - `DB_CONNECTION`: MongoDB connection string in the following format: `mongodb://username:password@hostname:27017`.
37 | - `DB_DATABASE`: Name of the MongoDB database. Defaults to `wbgl_bridge`.
38 |
39 | The command for running the service is:
40 | ```shell
41 | node src/server.js
42 | ```
43 |
44 | ### Frontend Application
45 |
46 | Frontend application is a minimal GUI developed in React. To prepare the application build process, go to the `app` folder, and install the dependencies using one of the following commands:
47 | ```shell
48 | yarn
49 | ```
50 | or
51 | ```shell
52 | npm install
53 | ```
54 |
55 | Before building the application, you'll need to set the `REACT_APP_SERVICE_URL` environment variable, which should contain the URL the background service is running at. It should look something like this: `https://service.domain`.
56 |
57 | To build the application, run one of the following commands:
58 | ```shell
59 | yarn build
60 | ```
61 | or
62 | ```shell
63 | npm run build
64 | ```
65 |
66 | This will create the static application bundle in the `build` subdirectory, which can be directly served using any web server.
67 |
68 | ## Caveats
69 |
70 | As mentioned above, this is a proof-of-concept implementation of the Bitgesell-WBGL bridge application. As such, it comes with multiple shortcomings that should be overcome before launching a full scale service. Some of these shortcomings are:
71 |
72 | - Very minimal GUI. Should add a lot of features (trade details, status updates, dedicated trade url, etc).
73 | - No DDoS protection. Should implement at least CAPTCHA.
74 | - Limited error checking. All possible exception paths should be covered.
75 | - No set limits for transfer amounts. If custodial wallet has insufficient funds to fulfil a trade, transfers will simply fail.
76 | - No fee calculations. Transaction fees should be covered by the user instead of the service.
77 | - No wallet integration. Should support at least address autofill and message signing.
78 | - Not a typed language. Should be rewritten in e.g. Typescript.
79 | - No unit tests.
80 |
--------------------------------------------------------------------------------
/service/src/controllers/SubmitController.js:
--------------------------------------------------------------------------------
1 | import Transfer from "../models/Transfer.js";
2 | import { RPC, Eth, Bsc } from "../modules/index.js";
3 | import { bsc, eth, feePercentage } from "../utils/config.js";
4 | import { isValidBglAddress, isValidEthAddress, sha3 } from "../utils/index.js";
5 |
6 | export const bglToWbgl = async (req, res) => {
7 | const data = req.body;
8 | if (!data.hasOwnProperty("address") || !isValidEthAddress(data.address)) {
9 | res.status(400).json({
10 | status: "error",
11 | field: "address",
12 | message: "No address or invalid ethereum address provided.",
13 | });
14 | return;
15 | }
16 | const chain =
17 | data.hasOwnProperty("chain") && data.chain !== "eth" ? "bsc" : "eth";
18 | const Chain = chain === "eth" ? Eth : Bsc;
19 | try {
20 | let transfer = await Transfer.findOne({
21 | type: "bgl",
22 | chain,
23 | to: data.address,
24 | }).exec();
25 | if (!transfer) {
26 | const bglAddress = await RPC.createAddress();
27 | transfer = new Transfer({
28 | id: sha3("bgl" + chain + bglAddress + data.address),
29 | type: "bgl",
30 | chain,
31 | from: bglAddress,
32 | to: data.address,
33 | });
34 | }
35 | transfer.markModified("type");
36 | await transfer.save();
37 |
38 | res.json({
39 | status: "ok",
40 | id: transfer.id,
41 | bglAddress: transfer.from,
42 | balance: await Chain.getWBGLBalance(),
43 | feePercentage: feePercentage,
44 | });
45 | } catch (e) {
46 | console.error(`Error: couldn't reach either RPC server or mongodb `, e);
47 | res.status(400).json({
48 | status: "error",
49 | message: "Network is likely to be down.",
50 | });
51 | return;
52 | }
53 | };
54 |
55 | export const wbglToBgl = async (req, res) => {
56 | const data = req.body;
57 | if (
58 | !data.hasOwnProperty("ethAddress") ||
59 | !isValidEthAddress(data.ethAddress)
60 | ) {
61 | res.status(400).json({
62 | status: "error",
63 | field: "ethAddress",
64 | message: "No ethereum address or invalid address provided.",
65 | });
66 | return;
67 | }
68 | try {
69 | if (
70 | !data.hasOwnProperty("bglAddress") ||
71 | !(await isValidBglAddress(data.bglAddress))
72 | ) {
73 | res.status(400).json({
74 | status: "error",
75 | field: "bglAddress",
76 | message: "No Bitgesell address or invalid address provided.",
77 | });
78 | return;
79 | }
80 | if (
81 | !data.hasOwnProperty("signature") ||
82 | typeof data.signature !== "string"
83 | ) {
84 | res.status(400).json({
85 | status: "error",
86 | field: "signature",
87 | message: "No signature or malformed signature provided.",
88 | });
89 | return;
90 | }
91 | const chain =
92 | data.hasOwnProperty("chain") && data.chain !== "eth" ? "bsc" : "eth";
93 | console.log("data.chain :", data.chain);
94 | const Chain = chain === "eth" ? Eth : Bsc;
95 | if (
96 | data.ethAddress.toLowerCase() !==
97 | Chain.web3.eth.accounts.recover(data.bglAddress, data.signature).toLowerCase()
98 | ) {
99 | res.status(400).json({
100 | status: "error",
101 | field: "signature",
102 | message: "Signature does not match the address provided.",
103 | });
104 | return;
105 | }
106 |
107 | let transfer = await Transfer.findOne({
108 | type: "wbgl",
109 | chain,
110 | from: data.ethAddress,
111 | }).exec();
112 | if (!transfer) {
113 | transfer = new Transfer({
114 | id: sha3("wbgl" + chain + data.ethAddress + data.bglAddress),
115 | type: "wbgl",
116 | chain,
117 | from: data.ethAddress,
118 | to: data.bglAddress,
119 | });
120 | }
121 | transfer.markModified("type");
122 | await transfer.save();
123 |
124 | res.json({
125 | status: "ok",
126 | id: transfer.id,
127 | address: chain === "eth" ? eth.account : bsc.account,
128 | balance: await RPC.getBalance(),
129 | feePercentage: feePercentage,
130 | });
131 | } catch (e) {
132 | console.error(`Error: network related error `, e);
133 | res.status(400).json({
134 | status: "error",
135 | message: "Network is likely to be down.",
136 | });
137 | return;
138 | }
139 | };
140 |
--------------------------------------------------------------------------------
/service/src/modules/web3.js:
--------------------------------------------------------------------------------
1 | import Web3 from "web3";
2 | //import {Transaction} from 'ethereumjs-tx'
3 | import pkg from "ethereumjs-tx";
4 | const { Transaction } = pkg;
5 | import fs from "fs";
6 | import { Data } from "../modules/index.js";
7 | import { toBaseUnit } from "../utils/index.js";
8 |
9 | const bn = Web3.utils.toBN;
10 | const createProvider = (endpoint) => new Web3.providers.HttpProvider(endpoint);
11 |
12 | class Web3Base {
13 | decimals = 18;
14 |
15 | constructor(
16 | endpoint,
17 | id,
18 | contractAddress,
19 | custodialAccountAddress,
20 | custodialAccountKey,
21 | nonceDataName,
22 | confirmations,
23 | ) {
24 | this.id = id;
25 | this.contractAddress = contractAddress;
26 | this.custodialAccountAddress = custodialAccountAddress;
27 | this.custodialAccountKey = custodialAccountKey;
28 | this.nonceDataName = nonceDataName;
29 | this.confirmations = confirmations;
30 |
31 | this.web3 = new Web3(createProvider(endpoint));
32 | this.WBGL = new this.web3.eth.Contract(
33 | JSON.parse(fs.readFileSync("abi/WBGL.json", "utf8")),
34 | contractAddress,
35 | );
36 | this.WBGL.methods["decimals"]()
37 | .call()
38 | .then((decimals) => (this.decimals = decimals));
39 |
40 | this.init()
41 | .then(() => {})
42 | .catch((err) => console.log("error ", err));
43 | }
44 |
45 | async init() {
46 | const chain = await this.web3.eth.net.getNetworkType();
47 | this.chain = ["main", "private"].includes(chain) ? "mainnet" : chain;
48 | }
49 |
50 | getChain() {
51 | return this.chain;
52 | }
53 |
54 | async getGasPrice() {
55 | const gasPrice = await this.web3.eth.getGasPrice();
56 | return this.web3.utils.fromWei(gasPrice, "Gwei");
57 | }
58 |
59 | async getEstimateGas(amount) {
60 | return await this.WBGL.methods["transfer"](
61 | this.custodialAccountAddress,
62 | toBaseUnit(amount, this.decimals),
63 | ).estimateGas({ from: this.custodialAccountAddress });
64 | }
65 |
66 | async getWBGLBalance() {
67 | return this.convertWGBLBalance(
68 | await this.WBGL.methods["balanceOf"](this.custodialAccountAddress).call(),
69 | );
70 | }
71 |
72 | async getWBGLBalance1(address) {
73 | return this.convertWGBLBalance(
74 | await this.WBGL.methods["balanceOf"](address).call(),
75 | );
76 | }
77 |
78 | convertWGBLBalance(number, resultDecimals = this.decimals) {
79 | const balance = bn(number);
80 | const divisor = bn(10).pow(bn(this.decimals));
81 | const beforeDec = balance.div(divisor).toString();
82 | const afterDec = balance
83 | .mod(divisor)
84 | .toString()
85 | .padStart(this.decimals, "0")
86 | .substring(0, resultDecimals);
87 | return beforeDec + (afterDec !== "0" ? "." + afterDec : "");
88 | }
89 |
90 | async getTransactionCount() {
91 | return await this.web3.eth.getTransactionCount(
92 | this.custodialAccountAddress,
93 | "latest",
94 | );
95 | }
96 |
97 | async getTransactionReceipt(txid) {
98 | return await this.web3.eth.getTransactionReceipt(txid);
99 | }
100 |
101 | sendWBGL(address, amount, nonce) {
102 | return new Promise(async (resolve, reject) => {
103 | const data = this.WBGL.methods["transfer"](
104 | address,
105 | toBaseUnit(amount, this.decimals),
106 | ).encodeABI();
107 | const rawTx = {
108 | nonce: this.web3.utils.toHex(nonce),
109 | gasPrice: this.web3.utils.toHex(
110 | this.web3.utils.toWei(
111 | Math.ceil(1.25 * parseFloat(await this.getGasPrice())).toString(),
112 | "Gwei",
113 | ),
114 | ),
115 | gasLimit: this.web3.utils.toHex(
116 | (await this.getEstimateGas(amount)) * 2,
117 | ),
118 | from: this.custodialAccountAddress,
119 | to: this.contractAddress,
120 | value: "0x00",
121 | data,
122 | };
123 | console.log(rawTx);
124 |
125 | const tx = new Transaction(rawTx, await this.transactionOpts());
126 | tx.sign(this.custodialAccountKey);
127 |
128 | const serializedTx = "0x" + tx.serialize().toString("hex");
129 | console.log("Serialized tx: " + serializedTx);
130 | this.web3.eth
131 | .sendSignedTransaction(serializedTx)
132 | .on("transactionHash", resolve)
133 | .on('error', console.error);
134 | });
135 | }
136 |
137 | async transactionOpts() {
138 | return { chain: this.chain };
139 | }
140 | }
141 |
142 | export default Web3Base;
143 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Windows template
2 | # Windows thumbnail cache files
3 | Thumbs.db
4 | Thumbs.db:encryptable
5 | ehthumbs.db
6 | ehthumbs_vista.db
7 |
8 | # Dump file
9 | *.stackdump
10 |
11 | # Folder config file
12 | [Dd]esktop.ini
13 |
14 | # Recycle Bin used on file shares
15 | $RECYCLE.BIN/
16 |
17 | # Windows Installer files
18 | *.cab
19 | *.msi
20 | *.msix
21 | *.msm
22 | *.msp
23 |
24 | # Windows shortcuts
25 | *.lnk
26 |
27 | ### Node template
28 | # Logs
29 | logs
30 | *.log
31 | npm-debug.log*
32 | yarn-debug.log*
33 | yarn-error.log*
34 | lerna-debug.log*
35 |
36 | # Lockfiles
37 | yarn.lock
38 | package-lock.json
39 |
40 | # Diagnostic reports (https://nodejs.org/api/report.html)
41 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
42 |
43 | # Runtime data
44 | pids
45 | *.pid
46 | *.seed
47 | *.pid.lock
48 |
49 | # Directory for instrumented libs generated by jscoverage/JSCover
50 | lib-cov
51 |
52 | # Coverage directory used by tools like istanbul
53 | coverage
54 | *.lcov
55 |
56 | # nyc test coverage
57 | .nyc_output
58 |
59 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
60 | .grunt
61 |
62 | # Bower dependency directory (https://bower.io/)
63 | bower_components
64 |
65 | # node-waf configuration
66 | .lock-wscript
67 |
68 | # Compiled binary addons (https://nodejs.org/api/addons.html)
69 | build/Release
70 |
71 | # Dependency directories
72 | node_modules/
73 | jspm_packages/
74 |
75 | # Snowpack dependency directory (https://snowpack.dev/)
76 | web_modules/
77 |
78 | # TypeScript cache
79 | *.tsbuildinfo
80 |
81 | # Optional npm cache directory
82 | .npm
83 |
84 | # Optional eslint cache
85 | .eslintcache
86 |
87 | # Microbundle cache
88 | .rpt2_cache/
89 | .rts2_cache_cjs/
90 | .rts2_cache_es/
91 | .rts2_cache_umd/
92 |
93 | # Optional REPL history
94 | .node_repl_history
95 |
96 | # Output of 'npm pack'
97 | *.tgz
98 |
99 | # Yarn Integrity file
100 | .yarn-integrity
101 |
102 | # dotenv environment variables file
103 | .env
104 | .env.test
105 |
106 | # parcel-bundler cache (https://parceljs.org/)
107 | .cache
108 | .parcel-cache
109 |
110 | # Next.js build output
111 | .next
112 | out
113 |
114 | # Nuxt.js build / generate output
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 | .cache/
120 | # Comment in the public line in if your project uses Gatsby and not Next.js
121 | # https://nextjs.org/blog/next-9-1#public-directory-support
122 | # public
123 |
124 | # vuepress build output
125 | .vuepress/dist
126 |
127 | # Serverless directories
128 | .serverless/
129 |
130 | # FuseBox cache
131 | .fusebox/
132 |
133 | # DynamoDB Local files
134 | .dynamodb/
135 |
136 | # TernJS port file
137 | .tern-port
138 |
139 | # Stores VSCode versions used for testing VSCode extensions
140 | .vscode-test
141 |
142 | # yarn v2
143 | .yarn/cache
144 | .yarn/unplugged
145 | .yarn/build-state.yml
146 | .yarn/install-state.gz
147 | .pnp.*
148 |
149 | ### macOS template
150 | # General
151 | .DS_Store
152 | .AppleDouble
153 | .LSOverride
154 |
155 | # Icon must end with two \r
156 | Icon
157 |
158 | # Thumbnails
159 | ._*
160 |
161 | # Files that might appear in the root of a volume
162 | .DocumentRevisions-V100
163 | .fseventsd
164 | .Spotlight-V100
165 | .TemporaryItems
166 | .Trashes
167 | .VolumeIcon.icns
168 | .com.apple.timemachine.donotpresent
169 |
170 | # Directories potentially created on remote AFP share
171 | .AppleDB
172 | .AppleDesktop
173 | Network Trash Folder
174 | Temporary Items
175 | .apdisk
176 |
177 | ### Linux template
178 | *~
179 |
180 | # temporary files which can be created if a process still has a handle open of a deleted file
181 | .fuse_hidden*
182 |
183 | # KDE directory preferences
184 | .directory
185 |
186 | # Linux trash folder which might appear on any partition or disk
187 | .Trash-*
188 |
189 | # .nfs files are created when an open file is removed but is still being accessed
190 | .nfs*
191 |
192 | ### JetBrains
193 | .idea
194 |
195 | # Gradle and Maven with auto-import
196 | # When using Gradle or Maven with auto-import, you should exclude module files,
197 | # since they will be recreated, and may cause churn. Uncomment if using
198 | # auto-import.
199 | # .idea/artifacts
200 | # .idea/compiler.xml
201 | # .idea/jarRepositories.xml
202 | # .idea/modules.xml
203 | # .idea/*.iml
204 | # .idea/modules
205 | # *.iml
206 | # *.ipr
207 |
208 | # CMake
209 | cmake-build-*/
210 |
211 | # Mongo Explorer plugin
212 | .idea/**/mongoSettings.xml
213 |
214 | # File-based project format
215 | *.iws
216 |
217 | # IntelliJ
218 | out/
219 |
220 | # mpeltonen/sbt-idea plugin
221 | .idea_modules/
222 |
223 | # JIRA plugin
224 | atlassian-ide-plugin.xml
225 |
226 | # Cursive Clojure plugin
227 | .idea/replstate.xml
228 |
229 | # Crashlytics plugin (for Android Studio and IntelliJ)
230 | com_crashlytics_export_strings.xml
231 | crashlytics.properties
232 | crashlytics-build.properties
233 | fabric.properties
234 |
235 | # Editor-based Rest Client
236 | .idea/httpRequests
237 |
238 | # Android studio 3.1+ serialized cache file
239 | .idea/caches/build_file_checksums.ser
240 |
241 | # Docker
242 | docker-compose.yml
243 |
--------------------------------------------------------------------------------
/app/src/components/WbglToBgl.js:
--------------------------------------------------------------------------------
1 | import {Fragment, useState} from 'react'
2 | import {useMetaMask} from 'metamask-react'
3 | import {useForm} from 'react-hook-form'
4 | import {
5 | Box,
6 | Button,
7 | List, ListItemText,
8 | TextField,
9 | Typography,
10 | } from '@material-ui/core'
11 | import {chainLabel, isChainBsc, post, url} from '../utils'
12 | import {sendWbgl, useWbglBalance} from '../utils/wallet'
13 | import CheckWalletConnection from './CheckWalletConnection'
14 |
15 | function WbglToBgl() {
16 | const [submitting, setSubmitting] = useState(false)
17 | const [sendAddress, setSendAddress] = useState('')
18 | const [balance, setBalance] = useState(0)
19 | const [feePercentage, setFeePercentage] = useState(0)
20 | const wbglBalance = useWbglBalance()
21 | const {chainId, account, ethereum} = useMetaMask()
22 | const chain = isChainBsc(chainId) ? 'bsc' : 'eth'
23 |
24 | const AddressForm = () => {
25 | const {register, handleSubmit, setError, setFocus, formState: {errors}} = useForm()
26 |
27 | const onSubmit = async data => {
28 | setSubmitting(true)
29 |
30 | data.chain = chain
31 | data.ethAddress = account
32 | try {
33 | data.signature = await ethereum.request({
34 | method: 'personal_sign',
35 | params: [
36 | data.bglAddress,
37 | account,
38 | ],
39 | })
40 | } catch (e) {
41 | setSubmitting(false)
42 | return
43 | }
44 |
45 | post(url('/submit/wbgl'), data).then(response => {
46 | setSendAddress(response.address)
47 | setBalance(Math.floor(response.balance))
48 | setFeePercentage(response.feePercentage)
49 | }).catch(result => {
50 | if (result.hasOwnProperty('field')) {
51 | setError(result.field, {type: 'manual', message: result.message})
52 | setFocus(result.field)
53 | }
54 | }).finally(() => setSubmitting(false))
55 | }
56 |
57 | return (
58 |
73 | )
74 | }
75 |
76 | const SendForm = () => {
77 | const {register, handleSubmit, setError, formState: {errors}} = useForm()
78 |
79 | const onSubmit = async data => {
80 | const amount = parseFloat(data.amount)
81 | const balance = parseFloat(wbglBalance)
82 |
83 | if (!amount || !balance || amount > balance) {
84 | setError('amount', {type: 'manual', message: 'Not enough WBGL available!', shouldFocus: true})
85 | return
86 | }
87 |
88 | setSubmitting(true)
89 | await sendWbgl(chainId, account, sendAddress, data.amount, ethereum)
90 | setSubmitting(false)
91 | }
92 |
93 | return (
94 |
107 | )
108 | }
109 |
110 | return (
111 |
112 |
113 |
114 |
115 |
116 |
117 | {!sendAddress ? (
118 |
119 | ) : (
120 |
121 |
122 | The currently available BGL balance is {balance}. If you send more WBGL than is available to complete
123 | the exchange, your WBGL will be returned to your address.
124 |
125 |
126 | Please note, that a fee of {feePercentage}% will be automatically deducted from the transfer amount.
127 |
128 |
129 |
130 | )}
131 |
132 |
133 | )
134 | }
135 |
136 | export default WbglToBgl
137 |
--------------------------------------------------------------------------------
/service/src/modules/chores.js:
--------------------------------------------------------------------------------
1 | import { confirmations, feePercentage, bsc, nonces } from "../utils/config.js";
2 | import { Data, RPC, Eth, Bsc } from "./index.js";
3 | import Transaction from "../models/Transaction.js";
4 | import Transfer from "../models/Transfer.js";
5 | import Conversion from "../models/Conversion.js";
6 |
7 | let ethNonce = 0;
8 | let bscNonce = 0;
9 |
10 | function setupNonce(){
11 | if ((ethNonce == 0) && (nonces.eth == 0)) {
12 | ethNonce = Eth.getTransactionCount();
13 | } else if ((nonces.eth > 0) && (nonces.eth > ethNonce)) {
14 | ethNonce = nonces.eth;
15 | }
16 | if ((bscNonce == 0) && (nonces.bsc == 0)) {
17 | bscNonce = Bsc.getTransactionCount();
18 | } else if ((nonces.bsc > 0) && (nonces.bsc > bscNonce)) {
19 | bscNonce = nonces.bsc;
20 | }
21 | }
22 |
23 | const expireDate = () => {
24 | const expireDate = new Date();
25 | expireDate.setTime(expireDate.getTime() - 7 * 24 * 3600000);
26 | return expireDate.toISOString();
27 | };
28 |
29 | const deductFee = (amount) =>
30 | parseFloat((((100 - feePercentage) * amount) / 100).toFixed(3));
31 |
32 | async function returnBGL(conversion, address) {
33 | try {
34 | conversion.status = "returned";
35 | await conversion.save();
36 | conversion.returnTxid = await RPC.send(address, conversion.amount);
37 | await conversion.save();
38 | } catch (e) {
39 | console.error(
40 | `Error returning BGL to ${address}, conversion ID: ${conversion._id}.`,
41 | e,
42 | );
43 | conversion.status = "error";
44 | await conversion.save();
45 | }
46 | }
47 |
48 | async function returnWBGL(Chain, conversion, address) {
49 | try {
50 | conversion.status = "returned";
51 | await conversion.save();
52 | conversion.returnTxid = await Chain.sendWBGL(
53 | address,
54 | conversion.amount.toString(),
55 | );
56 | await conversion.save();
57 | } catch (e) {
58 | console.error(
59 | `Error returning WBGL (${Chain.id}) to ${address}, conversion ID: ${conversion._id}.`,
60 | e,
61 | );
62 | conversion.status = "error";
63 | await conversion.save();
64 | }
65 | }
66 |
67 | async function checkBglTransactions() {
68 | try {
69 | const blockHash = await Data.get("lastBglBlockHash");
70 | const result = await RPC.listSinceBlock(
71 | blockHash || undefined,
72 | confirmations.bgl,
73 | );
74 | setupNonce();
75 | result.transactions
76 | .filter(
77 | (tx) =>
78 | tx.confirmations >= confirmations.bgl && tx.category === "receive",
79 | )
80 | .forEach((tx) => {
81 | Transfer.findOne({
82 | type: "bgl",
83 | from: tx.address,
84 | updatedAt: { $gte: expireDate() },
85 | })
86 | .exec()
87 | .then(async (transfer) => {
88 | if (
89 | transfer &&
90 | !(await Transaction.findOne({ id: tx["txid"] }).exec())
91 | ) {
92 | const Chain = transfer.chain === "bsc" ? Bsc : Eth;
93 | const fromAddress = await RPC.getTransactionFromAddress(
94 | tx["txid"],
95 | );
96 | const transaction = await Transaction.create({
97 | type: "bgl",
98 | id: tx["txid"],
99 | transfer: transfer._id,
100 | address: fromAddress,
101 | amount: tx["amount"],
102 | blockHash: tx["blockhash"],
103 | time: new Date(tx["time"] * 1000),
104 | });
105 | const amount = deductFee(tx["amount"]);
106 | const conversion = await Conversion.create({
107 | type: "wbgl",
108 | chain: transfer.chain,
109 | transfer: transfer._id,
110 | transaction: transaction._id,
111 | address: transfer.to,
112 | amount: tx["amount"],
113 | sendAmount: amount,
114 | });
115 |
116 | if (amount > (await Chain.getWBGLBalance())) {
117 | console.log(
118 | `Insufficient WBGL balance, returning ${tx["amount"]} BGL to ${fromAddress}`,
119 | );
120 | await returnBGL(conversion, fromAddress);
121 | return;
122 | }
123 |
124 | try {
125 | const AssignedNonce = transfer.chain === "bsc" ? Bsc : Eth;
126 | if (transfer.chain === "bsc") {
127 | bscNonce += 1;
128 | conversion.txid = await Chain.sendWBGL(
129 | transfer.to,
130 | amount.toString(),
131 | bscNonce
132 | );
133 | } else {
134 | ethNonce += 1;
135 | conversion.txid = await Chain.sendWBGL(
136 | transfer.to,
137 | amount.toString(),
138 | ethNonce
139 | );
140 | }
141 | await conversion.save();
142 | } catch (e) {
143 | console.log(
144 | `Error sending ${amount} WBGL to ${transfer.to}`,
145 | e,
146 | );
147 | conversion.status = "error";
148 | await conversion.save();
149 |
150 | await returnBGL(conversion, fromAddress);
151 | }
152 | }
153 | });
154 | });
155 |
156 | await Data.set("lastBglBlockHash", result["lastblock"]);
157 | } catch (e) {
158 | console.error("Error: checkBglTransactions function failed. Check network");
159 | }
160 |
161 | setTimeout(checkBglTransactions, 60000);
162 | }
163 |
164 | export async function checkWbglTransfers(Chain = Eth, prefix = "Eth") {
165 | try {
166 | const currentBlock = await Chain.web3.eth.getBlockNumber();
167 | console.log("currentBlock: ", currentBlock);
168 | const blockNumber = Math.max(
169 | await Data.get(`last${prefix}BlockNumber`, currentBlock - 2000),
170 | currentBlock - 2000,
171 | );
172 | console.log("blockNumber: ", blockNumber);
173 | const events = await Chain.WBGL.getPastEvents("Transfer", {
174 | fromBlock: blockNumber + 1,
175 | toBlock: currentBlock,
176 | filter: { to: Chain.custodialAccountAddress },
177 | });
178 | console.log("event : ", events);
179 | events.forEach((event) => {
180 | const fromQuery = {
181 | $regex: new RegExp(`^${event.returnValues.from}$`, "i"),
182 | };
183 | console.log("fromQuery:", fromQuery);
184 | Transfer.findOne({
185 | type: "wbgl",
186 | chain: Chain.id,
187 | from: fromQuery,
188 | updatedAt: { $gte: expireDate() },
189 | })
190 | .exec()
191 | .then(async (transfer) => {
192 | if (
193 | transfer &&
194 | !(await Transaction.findOne({
195 | chain: Chain.id,
196 | id: event.transactionHash,
197 | }).exec())
198 | ) {
199 | const amount = Chain.convertWGBLBalance(event.returnValues.value);
200 | const sendAmount = deductFee(amount);
201 | const transaction = await Transaction.create({
202 | type: "wbgl",
203 | chain: Chain.id,
204 | id: event.transactionHash,
205 | transfer: transfer._id,
206 | address: event.returnValues.from,
207 | amount,
208 | blockHash: event.blockHash,
209 | time: Date.now(),
210 | });
211 | console.log("transaction:", transaction);
212 | const conversion = await Conversion.create({
213 | type: "bgl",
214 | chain: Chain.id,
215 | transfer: transfer._id,
216 | transaction: transaction._id,
217 | address: transfer.to,
218 | amount,
219 | sendAmount,
220 | });
221 | console.log("amount : ", amount);
222 | if (amount > (await RPC.getBalance())) {
223 | console.log(
224 | `Insufficient BGL balance, returning ${amount} WBGL to ${transfer.from}`,
225 | );
226 | await returnWBGL(Chain, conversion, transfer.from);
227 | return;
228 | }
229 |
230 | try {
231 | conversion.txid = await RPC.send(transfer.to, sendAmount);
232 | conversion.status = "sent";
233 | await conversion.save();
234 | } catch (e) {
235 | console.error(
236 | `Error sending ${sendAmount} BGL to ${transfer.to}`,
237 | e,
238 | );
239 | conversion.status = "error";
240 | await conversion.save();
241 |
242 | await returnWBGL(Chain, conversion, transfer.from);
243 | }
244 | }
245 | });
246 | Data.set(`last${prefix}BlockHash`, event.blockHash);
247 | Data.set(`last${prefix}BlockNumber`, event.blockNumber);
248 | });
249 | } catch (e) {
250 | console.error("Error: checkWbglTransfers function failed. Check network");
251 | }
252 |
253 | setTimeout(() => checkWbglTransfers(Chain, prefix), 60000);
254 | }
255 |
256 | async function checkPendingConversions(Chain) {
257 | const conversions = await Conversion.find({
258 | chain: Chain.id,
259 | type: "wbgl",
260 | status: "pending",
261 | txid: { $exists: true },
262 | }).exec();
263 | let blockNumber;
264 | console.log("checkPendingConversions:", conversions);
265 | try {
266 | for (const conversion of conversions) {
267 | const receipt = await Chain.getTransactionReceipt(conversion.txid);
268 | console.log("receipt", receipt);
269 | blockNumber = blockNumber || (await Chain.web3.eth.getBlockNumber());
270 |
271 | if (receipt && blockNumber - receipt.blockNumber >= Chain.confirmations) {
272 | conversion.status = "sent";
273 | conversion.receipt = receipt;
274 | conversion.markModified("receipt");
275 | } else {
276 | conversion.txChecks = (conversion.txChecks || 0) + 1;
277 | }
278 |
279 | await conversion.save();
280 | }
281 | } catch (e) {
282 | console.error(
283 | "Error: checkPendingConversions function failed. Check chain network or mongodb",
284 | );
285 | }
286 | setTimeout(() => checkPendingConversions(Chain), 60000);
287 | }
288 |
289 | export const init = async () => {
290 | await checkWbglTransfers(Eth, "Eth");
291 | await checkWbglTransfers(Bsc, "Bsc");
292 |
293 | await checkBglTransactions();
294 |
295 | await checkPendingConversions(Eth);
296 | await checkPendingConversions(Bsc);
297 | };
298 |
--------------------------------------------------------------------------------
/service/abi/WBGL.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "inputs": [],
4 | "stateMutability": "nonpayable",
5 | "type": "constructor"
6 | },
7 | {
8 | "anonymous": false,
9 | "inputs": [
10 | {
11 | "indexed": true,
12 | "internalType": "address",
13 | "name": "owner",
14 | "type": "address"
15 | },
16 | {
17 | "indexed": true,
18 | "internalType": "address",
19 | "name": "spender",
20 | "type": "address"
21 | },
22 | {
23 | "indexed": false,
24 | "internalType": "uint256",
25 | "name": "value",
26 | "type": "uint256"
27 | }
28 | ],
29 | "name": "Approval",
30 | "type": "event"
31 | },
32 | {
33 | "inputs": [
34 | {
35 | "internalType": "address",
36 | "name": "spender",
37 | "type": "address"
38 | },
39 | {
40 | "internalType": "uint256",
41 | "name": "amount",
42 | "type": "uint256"
43 | }
44 | ],
45 | "name": "approve",
46 | "outputs": [
47 | {
48 | "internalType": "bool",
49 | "name": "",
50 | "type": "bool"
51 | }
52 | ],
53 | "stateMutability": "nonpayable",
54 | "type": "function"
55 | },
56 | {
57 | "inputs": [
58 | {
59 | "internalType": "uint256",
60 | "name": "amount",
61 | "type": "uint256"
62 | }
63 | ],
64 | "name": "burn",
65 | "outputs": [],
66 | "stateMutability": "nonpayable",
67 | "type": "function"
68 | },
69 | {
70 | "inputs": [
71 | {
72 | "internalType": "address",
73 | "name": "account",
74 | "type": "address"
75 | },
76 | {
77 | "internalType": "uint256",
78 | "name": "amount",
79 | "type": "uint256"
80 | }
81 | ],
82 | "name": "burnFrom",
83 | "outputs": [],
84 | "stateMutability": "nonpayable",
85 | "type": "function"
86 | },
87 | {
88 | "inputs": [
89 | {
90 | "internalType": "address",
91 | "name": "spender",
92 | "type": "address"
93 | },
94 | {
95 | "internalType": "uint256",
96 | "name": "subtractedValue",
97 | "type": "uint256"
98 | }
99 | ],
100 | "name": "decreaseAllowance",
101 | "outputs": [
102 | {
103 | "internalType": "bool",
104 | "name": "",
105 | "type": "bool"
106 | }
107 | ],
108 | "stateMutability": "nonpayable",
109 | "type": "function"
110 | },
111 | {
112 | "inputs": [
113 | {
114 | "internalType": "bytes32",
115 | "name": "role",
116 | "type": "bytes32"
117 | },
118 | {
119 | "internalType": "address",
120 | "name": "account",
121 | "type": "address"
122 | }
123 | ],
124 | "name": "grantRole",
125 | "outputs": [],
126 | "stateMutability": "nonpayable",
127 | "type": "function"
128 | },
129 | {
130 | "inputs": [
131 | {
132 | "internalType": "address",
133 | "name": "spender",
134 | "type": "address"
135 | },
136 | {
137 | "internalType": "uint256",
138 | "name": "addedValue",
139 | "type": "uint256"
140 | }
141 | ],
142 | "name": "increaseAllowance",
143 | "outputs": [
144 | {
145 | "internalType": "bool",
146 | "name": "",
147 | "type": "bool"
148 | }
149 | ],
150 | "stateMutability": "nonpayable",
151 | "type": "function"
152 | },
153 | {
154 | "inputs": [
155 | {
156 | "internalType": "address",
157 | "name": "to",
158 | "type": "address"
159 | },
160 | {
161 | "internalType": "uint256",
162 | "name": "amount",
163 | "type": "uint256"
164 | }
165 | ],
166 | "name": "mint",
167 | "outputs": [],
168 | "stateMutability": "nonpayable",
169 | "type": "function"
170 | },
171 | {
172 | "inputs": [],
173 | "name": "pause",
174 | "outputs": [],
175 | "stateMutability": "nonpayable",
176 | "type": "function"
177 | },
178 | {
179 | "anonymous": false,
180 | "inputs": [
181 | {
182 | "indexed": false,
183 | "internalType": "address",
184 | "name": "account",
185 | "type": "address"
186 | }
187 | ],
188 | "name": "Paused",
189 | "type": "event"
190 | },
191 | {
192 | "inputs": [
193 | {
194 | "internalType": "bytes32",
195 | "name": "role",
196 | "type": "bytes32"
197 | },
198 | {
199 | "internalType": "address",
200 | "name": "account",
201 | "type": "address"
202 | }
203 | ],
204 | "name": "renounceRole",
205 | "outputs": [],
206 | "stateMutability": "nonpayable",
207 | "type": "function"
208 | },
209 | {
210 | "inputs": [
211 | {
212 | "internalType": "bytes32",
213 | "name": "role",
214 | "type": "bytes32"
215 | },
216 | {
217 | "internalType": "address",
218 | "name": "account",
219 | "type": "address"
220 | }
221 | ],
222 | "name": "revokeRole",
223 | "outputs": [],
224 | "stateMutability": "nonpayable",
225 | "type": "function"
226 | },
227 | {
228 | "anonymous": false,
229 | "inputs": [
230 | {
231 | "indexed": true,
232 | "internalType": "bytes32",
233 | "name": "role",
234 | "type": "bytes32"
235 | },
236 | {
237 | "indexed": true,
238 | "internalType": "bytes32",
239 | "name": "previousAdminRole",
240 | "type": "bytes32"
241 | },
242 | {
243 | "indexed": true,
244 | "internalType": "bytes32",
245 | "name": "newAdminRole",
246 | "type": "bytes32"
247 | }
248 | ],
249 | "name": "RoleAdminChanged",
250 | "type": "event"
251 | },
252 | {
253 | "anonymous": false,
254 | "inputs": [
255 | {
256 | "indexed": true,
257 | "internalType": "bytes32",
258 | "name": "role",
259 | "type": "bytes32"
260 | },
261 | {
262 | "indexed": true,
263 | "internalType": "address",
264 | "name": "account",
265 | "type": "address"
266 | },
267 | {
268 | "indexed": true,
269 | "internalType": "address",
270 | "name": "sender",
271 | "type": "address"
272 | }
273 | ],
274 | "name": "RoleGranted",
275 | "type": "event"
276 | },
277 | {
278 | "anonymous": false,
279 | "inputs": [
280 | {
281 | "indexed": true,
282 | "internalType": "bytes32",
283 | "name": "role",
284 | "type": "bytes32"
285 | },
286 | {
287 | "indexed": true,
288 | "internalType": "address",
289 | "name": "account",
290 | "type": "address"
291 | },
292 | {
293 | "indexed": true,
294 | "internalType": "address",
295 | "name": "sender",
296 | "type": "address"
297 | }
298 | ],
299 | "name": "RoleRevoked",
300 | "type": "event"
301 | },
302 | {
303 | "inputs": [
304 | {
305 | "internalType": "address",
306 | "name": "recipient",
307 | "type": "address"
308 | },
309 | {
310 | "internalType": "uint256",
311 | "name": "amount",
312 | "type": "uint256"
313 | }
314 | ],
315 | "name": "transfer",
316 | "outputs": [
317 | {
318 | "internalType": "bool",
319 | "name": "",
320 | "type": "bool"
321 | }
322 | ],
323 | "stateMutability": "nonpayable",
324 | "type": "function"
325 | },
326 | {
327 | "anonymous": false,
328 | "inputs": [
329 | {
330 | "indexed": true,
331 | "internalType": "address",
332 | "name": "from",
333 | "type": "address"
334 | },
335 | {
336 | "indexed": true,
337 | "internalType": "address",
338 | "name": "to",
339 | "type": "address"
340 | },
341 | {
342 | "indexed": false,
343 | "internalType": "uint256",
344 | "name": "value",
345 | "type": "uint256"
346 | }
347 | ],
348 | "name": "Transfer",
349 | "type": "event"
350 | },
351 | {
352 | "inputs": [
353 | {
354 | "internalType": "address",
355 | "name": "sender",
356 | "type": "address"
357 | },
358 | {
359 | "internalType": "address",
360 | "name": "recipient",
361 | "type": "address"
362 | },
363 | {
364 | "internalType": "uint256",
365 | "name": "amount",
366 | "type": "uint256"
367 | }
368 | ],
369 | "name": "transferFrom",
370 | "outputs": [
371 | {
372 | "internalType": "bool",
373 | "name": "",
374 | "type": "bool"
375 | }
376 | ],
377 | "stateMutability": "nonpayable",
378 | "type": "function"
379 | },
380 | {
381 | "inputs": [],
382 | "name": "unpause",
383 | "outputs": [],
384 | "stateMutability": "nonpayable",
385 | "type": "function"
386 | },
387 | {
388 | "anonymous": false,
389 | "inputs": [
390 | {
391 | "indexed": false,
392 | "internalType": "address",
393 | "name": "account",
394 | "type": "address"
395 | }
396 | ],
397 | "name": "Unpaused",
398 | "type": "event"
399 | },
400 | {
401 | "inputs": [
402 | {
403 | "internalType": "address",
404 | "name": "owner",
405 | "type": "address"
406 | },
407 | {
408 | "internalType": "address",
409 | "name": "spender",
410 | "type": "address"
411 | }
412 | ],
413 | "name": "allowance",
414 | "outputs": [
415 | {
416 | "internalType": "uint256",
417 | "name": "",
418 | "type": "uint256"
419 | }
420 | ],
421 | "stateMutability": "view",
422 | "type": "function"
423 | },
424 | {
425 | "inputs": [
426 | {
427 | "internalType": "address",
428 | "name": "account",
429 | "type": "address"
430 | }
431 | ],
432 | "name": "balanceOf",
433 | "outputs": [
434 | {
435 | "internalType": "uint256",
436 | "name": "",
437 | "type": "uint256"
438 | }
439 | ],
440 | "stateMutability": "view",
441 | "type": "function"
442 | },
443 | {
444 | "inputs": [],
445 | "name": "decimals",
446 | "outputs": [
447 | {
448 | "internalType": "uint8",
449 | "name": "",
450 | "type": "uint8"
451 | }
452 | ],
453 | "stateMutability": "view",
454 | "type": "function"
455 | },
456 | {
457 | "inputs": [],
458 | "name": "DEFAULT_ADMIN_ROLE",
459 | "outputs": [
460 | {
461 | "internalType": "bytes32",
462 | "name": "",
463 | "type": "bytes32"
464 | }
465 | ],
466 | "stateMutability": "view",
467 | "type": "function"
468 | },
469 | {
470 | "inputs": [
471 | {
472 | "internalType": "bytes32",
473 | "name": "role",
474 | "type": "bytes32"
475 | }
476 | ],
477 | "name": "getRoleAdmin",
478 | "outputs": [
479 | {
480 | "internalType": "bytes32",
481 | "name": "",
482 | "type": "bytes32"
483 | }
484 | ],
485 | "stateMutability": "view",
486 | "type": "function"
487 | },
488 | {
489 | "inputs": [
490 | {
491 | "internalType": "bytes32",
492 | "name": "role",
493 | "type": "bytes32"
494 | },
495 | {
496 | "internalType": "uint256",
497 | "name": "index",
498 | "type": "uint256"
499 | }
500 | ],
501 | "name": "getRoleMember",
502 | "outputs": [
503 | {
504 | "internalType": "address",
505 | "name": "",
506 | "type": "address"
507 | }
508 | ],
509 | "stateMutability": "view",
510 | "type": "function"
511 | },
512 | {
513 | "inputs": [
514 | {
515 | "internalType": "bytes32",
516 | "name": "role",
517 | "type": "bytes32"
518 | }
519 | ],
520 | "name": "getRoleMemberCount",
521 | "outputs": [
522 | {
523 | "internalType": "uint256",
524 | "name": "",
525 | "type": "uint256"
526 | }
527 | ],
528 | "stateMutability": "view",
529 | "type": "function"
530 | },
531 | {
532 | "inputs": [
533 | {
534 | "internalType": "bytes32",
535 | "name": "role",
536 | "type": "bytes32"
537 | },
538 | {
539 | "internalType": "address",
540 | "name": "account",
541 | "type": "address"
542 | }
543 | ],
544 | "name": "hasRole",
545 | "outputs": [
546 | {
547 | "internalType": "bool",
548 | "name": "",
549 | "type": "bool"
550 | }
551 | ],
552 | "stateMutability": "view",
553 | "type": "function"
554 | },
555 | {
556 | "inputs": [],
557 | "name": "MINTER_ROLE",
558 | "outputs": [
559 | {
560 | "internalType": "bytes32",
561 | "name": "",
562 | "type": "bytes32"
563 | }
564 | ],
565 | "stateMutability": "view",
566 | "type": "function"
567 | },
568 | {
569 | "inputs": [],
570 | "name": "name",
571 | "outputs": [
572 | {
573 | "internalType": "string",
574 | "name": "",
575 | "type": "string"
576 | }
577 | ],
578 | "stateMutability": "view",
579 | "type": "function"
580 | },
581 | {
582 | "inputs": [],
583 | "name": "paused",
584 | "outputs": [
585 | {
586 | "internalType": "bool",
587 | "name": "",
588 | "type": "bool"
589 | }
590 | ],
591 | "stateMutability": "view",
592 | "type": "function"
593 | },
594 | {
595 | "inputs": [],
596 | "name": "PAUSER_ROLE",
597 | "outputs": [
598 | {
599 | "internalType": "bytes32",
600 | "name": "",
601 | "type": "bytes32"
602 | }
603 | ],
604 | "stateMutability": "view",
605 | "type": "function"
606 | },
607 | {
608 | "inputs": [
609 | {
610 | "internalType": "bytes4",
611 | "name": "interfaceId",
612 | "type": "bytes4"
613 | }
614 | ],
615 | "name": "supportsInterface",
616 | "outputs": [
617 | {
618 | "internalType": "bool",
619 | "name": "",
620 | "type": "bool"
621 | }
622 | ],
623 | "stateMutability": "view",
624 | "type": "function"
625 | },
626 | {
627 | "inputs": [],
628 | "name": "symbol",
629 | "outputs": [
630 | {
631 | "internalType": "string",
632 | "name": "",
633 | "type": "string"
634 | }
635 | ],
636 | "stateMutability": "view",
637 | "type": "function"
638 | },
639 | {
640 | "inputs": [],
641 | "name": "totalSupply",
642 | "outputs": [
643 | {
644 | "internalType": "uint256",
645 | "name": "",
646 | "type": "uint256"
647 | }
648 | ],
649 | "stateMutability": "view",
650 | "type": "function"
651 | }
652 | ]
653 |
--------------------------------------------------------------------------------
/app/src/assets/abi/WBGL.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "inputs": [],
4 | "stateMutability": "nonpayable",
5 | "type": "constructor"
6 | },
7 | {
8 | "anonymous": false,
9 | "inputs": [
10 | {
11 | "indexed": true,
12 | "internalType": "address",
13 | "name": "owner",
14 | "type": "address"
15 | },
16 | {
17 | "indexed": true,
18 | "internalType": "address",
19 | "name": "spender",
20 | "type": "address"
21 | },
22 | {
23 | "indexed": false,
24 | "internalType": "uint256",
25 | "name": "value",
26 | "type": "uint256"
27 | }
28 | ],
29 | "name": "Approval",
30 | "type": "event"
31 | },
32 | {
33 | "inputs": [
34 | {
35 | "internalType": "address",
36 | "name": "spender",
37 | "type": "address"
38 | },
39 | {
40 | "internalType": "uint256",
41 | "name": "amount",
42 | "type": "uint256"
43 | }
44 | ],
45 | "name": "approve",
46 | "outputs": [
47 | {
48 | "internalType": "bool",
49 | "name": "",
50 | "type": "bool"
51 | }
52 | ],
53 | "stateMutability": "nonpayable",
54 | "type": "function"
55 | },
56 | {
57 | "inputs": [
58 | {
59 | "internalType": "uint256",
60 | "name": "amount",
61 | "type": "uint256"
62 | }
63 | ],
64 | "name": "burn",
65 | "outputs": [],
66 | "stateMutability": "nonpayable",
67 | "type": "function"
68 | },
69 | {
70 | "inputs": [
71 | {
72 | "internalType": "address",
73 | "name": "account",
74 | "type": "address"
75 | },
76 | {
77 | "internalType": "uint256",
78 | "name": "amount",
79 | "type": "uint256"
80 | }
81 | ],
82 | "name": "burnFrom",
83 | "outputs": [],
84 | "stateMutability": "nonpayable",
85 | "type": "function"
86 | },
87 | {
88 | "inputs": [
89 | {
90 | "internalType": "address",
91 | "name": "spender",
92 | "type": "address"
93 | },
94 | {
95 | "internalType": "uint256",
96 | "name": "subtractedValue",
97 | "type": "uint256"
98 | }
99 | ],
100 | "name": "decreaseAllowance",
101 | "outputs": [
102 | {
103 | "internalType": "bool",
104 | "name": "",
105 | "type": "bool"
106 | }
107 | ],
108 | "stateMutability": "nonpayable",
109 | "type": "function"
110 | },
111 | {
112 | "inputs": [
113 | {
114 | "internalType": "bytes32",
115 | "name": "role",
116 | "type": "bytes32"
117 | },
118 | {
119 | "internalType": "address",
120 | "name": "account",
121 | "type": "address"
122 | }
123 | ],
124 | "name": "grantRole",
125 | "outputs": [],
126 | "stateMutability": "nonpayable",
127 | "type": "function"
128 | },
129 | {
130 | "inputs": [
131 | {
132 | "internalType": "address",
133 | "name": "spender",
134 | "type": "address"
135 | },
136 | {
137 | "internalType": "uint256",
138 | "name": "addedValue",
139 | "type": "uint256"
140 | }
141 | ],
142 | "name": "increaseAllowance",
143 | "outputs": [
144 | {
145 | "internalType": "bool",
146 | "name": "",
147 | "type": "bool"
148 | }
149 | ],
150 | "stateMutability": "nonpayable",
151 | "type": "function"
152 | },
153 | {
154 | "inputs": [
155 | {
156 | "internalType": "address",
157 | "name": "to",
158 | "type": "address"
159 | },
160 | {
161 | "internalType": "uint256",
162 | "name": "amount",
163 | "type": "uint256"
164 | }
165 | ],
166 | "name": "mint",
167 | "outputs": [],
168 | "stateMutability": "nonpayable",
169 | "type": "function"
170 | },
171 | {
172 | "inputs": [],
173 | "name": "pause",
174 | "outputs": [],
175 | "stateMutability": "nonpayable",
176 | "type": "function"
177 | },
178 | {
179 | "anonymous": false,
180 | "inputs": [
181 | {
182 | "indexed": false,
183 | "internalType": "address",
184 | "name": "account",
185 | "type": "address"
186 | }
187 | ],
188 | "name": "Paused",
189 | "type": "event"
190 | },
191 | {
192 | "inputs": [
193 | {
194 | "internalType": "bytes32",
195 | "name": "role",
196 | "type": "bytes32"
197 | },
198 | {
199 | "internalType": "address",
200 | "name": "account",
201 | "type": "address"
202 | }
203 | ],
204 | "name": "renounceRole",
205 | "outputs": [],
206 | "stateMutability": "nonpayable",
207 | "type": "function"
208 | },
209 | {
210 | "inputs": [
211 | {
212 | "internalType": "bytes32",
213 | "name": "role",
214 | "type": "bytes32"
215 | },
216 | {
217 | "internalType": "address",
218 | "name": "account",
219 | "type": "address"
220 | }
221 | ],
222 | "name": "revokeRole",
223 | "outputs": [],
224 | "stateMutability": "nonpayable",
225 | "type": "function"
226 | },
227 | {
228 | "anonymous": false,
229 | "inputs": [
230 | {
231 | "indexed": true,
232 | "internalType": "bytes32",
233 | "name": "role",
234 | "type": "bytes32"
235 | },
236 | {
237 | "indexed": true,
238 | "internalType": "bytes32",
239 | "name": "previousAdminRole",
240 | "type": "bytes32"
241 | },
242 | {
243 | "indexed": true,
244 | "internalType": "bytes32",
245 | "name": "newAdminRole",
246 | "type": "bytes32"
247 | }
248 | ],
249 | "name": "RoleAdminChanged",
250 | "type": "event"
251 | },
252 | {
253 | "anonymous": false,
254 | "inputs": [
255 | {
256 | "indexed": true,
257 | "internalType": "bytes32",
258 | "name": "role",
259 | "type": "bytes32"
260 | },
261 | {
262 | "indexed": true,
263 | "internalType": "address",
264 | "name": "account",
265 | "type": "address"
266 | },
267 | {
268 | "indexed": true,
269 | "internalType": "address",
270 | "name": "sender",
271 | "type": "address"
272 | }
273 | ],
274 | "name": "RoleGranted",
275 | "type": "event"
276 | },
277 | {
278 | "anonymous": false,
279 | "inputs": [
280 | {
281 | "indexed": true,
282 | "internalType": "bytes32",
283 | "name": "role",
284 | "type": "bytes32"
285 | },
286 | {
287 | "indexed": true,
288 | "internalType": "address",
289 | "name": "account",
290 | "type": "address"
291 | },
292 | {
293 | "indexed": true,
294 | "internalType": "address",
295 | "name": "sender",
296 | "type": "address"
297 | }
298 | ],
299 | "name": "RoleRevoked",
300 | "type": "event"
301 | },
302 | {
303 | "inputs": [
304 | {
305 | "internalType": "address",
306 | "name": "recipient",
307 | "type": "address"
308 | },
309 | {
310 | "internalType": "uint256",
311 | "name": "amount",
312 | "type": "uint256"
313 | }
314 | ],
315 | "name": "transfer",
316 | "outputs": [
317 | {
318 | "internalType": "bool",
319 | "name": "",
320 | "type": "bool"
321 | }
322 | ],
323 | "stateMutability": "nonpayable",
324 | "type": "function"
325 | },
326 | {
327 | "anonymous": false,
328 | "inputs": [
329 | {
330 | "indexed": true,
331 | "internalType": "address",
332 | "name": "from",
333 | "type": "address"
334 | },
335 | {
336 | "indexed": true,
337 | "internalType": "address",
338 | "name": "to",
339 | "type": "address"
340 | },
341 | {
342 | "indexed": false,
343 | "internalType": "uint256",
344 | "name": "value",
345 | "type": "uint256"
346 | }
347 | ],
348 | "name": "Transfer",
349 | "type": "event"
350 | },
351 | {
352 | "inputs": [
353 | {
354 | "internalType": "address",
355 | "name": "sender",
356 | "type": "address"
357 | },
358 | {
359 | "internalType": "address",
360 | "name": "recipient",
361 | "type": "address"
362 | },
363 | {
364 | "internalType": "uint256",
365 | "name": "amount",
366 | "type": "uint256"
367 | }
368 | ],
369 | "name": "transferFrom",
370 | "outputs": [
371 | {
372 | "internalType": "bool",
373 | "name": "",
374 | "type": "bool"
375 | }
376 | ],
377 | "stateMutability": "nonpayable",
378 | "type": "function"
379 | },
380 | {
381 | "inputs": [],
382 | "name": "unpause",
383 | "outputs": [],
384 | "stateMutability": "nonpayable",
385 | "type": "function"
386 | },
387 | {
388 | "anonymous": false,
389 | "inputs": [
390 | {
391 | "indexed": false,
392 | "internalType": "address",
393 | "name": "account",
394 | "type": "address"
395 | }
396 | ],
397 | "name": "Unpaused",
398 | "type": "event"
399 | },
400 | {
401 | "inputs": [
402 | {
403 | "internalType": "address",
404 | "name": "owner",
405 | "type": "address"
406 | },
407 | {
408 | "internalType": "address",
409 | "name": "spender",
410 | "type": "address"
411 | }
412 | ],
413 | "name": "allowance",
414 | "outputs": [
415 | {
416 | "internalType": "uint256",
417 | "name": "",
418 | "type": "uint256"
419 | }
420 | ],
421 | "stateMutability": "view",
422 | "type": "function"
423 | },
424 | {
425 | "inputs": [
426 | {
427 | "internalType": "address",
428 | "name": "account",
429 | "type": "address"
430 | }
431 | ],
432 | "name": "balanceOf",
433 | "outputs": [
434 | {
435 | "internalType": "uint256",
436 | "name": "",
437 | "type": "uint256"
438 | }
439 | ],
440 | "stateMutability": "view",
441 | "type": "function"
442 | },
443 | {
444 | "inputs": [],
445 | "name": "decimals",
446 | "outputs": [
447 | {
448 | "internalType": "uint8",
449 | "name": "",
450 | "type": "uint8"
451 | }
452 | ],
453 | "stateMutability": "view",
454 | "type": "function"
455 | },
456 | {
457 | "inputs": [],
458 | "name": "DEFAULT_ADMIN_ROLE",
459 | "outputs": [
460 | {
461 | "internalType": "bytes32",
462 | "name": "",
463 | "type": "bytes32"
464 | }
465 | ],
466 | "stateMutability": "view",
467 | "type": "function"
468 | },
469 | {
470 | "inputs": [
471 | {
472 | "internalType": "bytes32",
473 | "name": "role",
474 | "type": "bytes32"
475 | }
476 | ],
477 | "name": "getRoleAdmin",
478 | "outputs": [
479 | {
480 | "internalType": "bytes32",
481 | "name": "",
482 | "type": "bytes32"
483 | }
484 | ],
485 | "stateMutability": "view",
486 | "type": "function"
487 | },
488 | {
489 | "inputs": [
490 | {
491 | "internalType": "bytes32",
492 | "name": "role",
493 | "type": "bytes32"
494 | },
495 | {
496 | "internalType": "uint256",
497 | "name": "index",
498 | "type": "uint256"
499 | }
500 | ],
501 | "name": "getRoleMember",
502 | "outputs": [
503 | {
504 | "internalType": "address",
505 | "name": "",
506 | "type": "address"
507 | }
508 | ],
509 | "stateMutability": "view",
510 | "type": "function"
511 | },
512 | {
513 | "inputs": [
514 | {
515 | "internalType": "bytes32",
516 | "name": "role",
517 | "type": "bytes32"
518 | }
519 | ],
520 | "name": "getRoleMemberCount",
521 | "outputs": [
522 | {
523 | "internalType": "uint256",
524 | "name": "",
525 | "type": "uint256"
526 | }
527 | ],
528 | "stateMutability": "view",
529 | "type": "function"
530 | },
531 | {
532 | "inputs": [
533 | {
534 | "internalType": "bytes32",
535 | "name": "role",
536 | "type": "bytes32"
537 | },
538 | {
539 | "internalType": "address",
540 | "name": "account",
541 | "type": "address"
542 | }
543 | ],
544 | "name": "hasRole",
545 | "outputs": [
546 | {
547 | "internalType": "bool",
548 | "name": "",
549 | "type": "bool"
550 | }
551 | ],
552 | "stateMutability": "view",
553 | "type": "function"
554 | },
555 | {
556 | "inputs": [],
557 | "name": "MINTER_ROLE",
558 | "outputs": [
559 | {
560 | "internalType": "bytes32",
561 | "name": "",
562 | "type": "bytes32"
563 | }
564 | ],
565 | "stateMutability": "view",
566 | "type": "function"
567 | },
568 | {
569 | "inputs": [],
570 | "name": "name",
571 | "outputs": [
572 | {
573 | "internalType": "string",
574 | "name": "",
575 | "type": "string"
576 | }
577 | ],
578 | "stateMutability": "view",
579 | "type": "function"
580 | },
581 | {
582 | "inputs": [],
583 | "name": "paused",
584 | "outputs": [
585 | {
586 | "internalType": "bool",
587 | "name": "",
588 | "type": "bool"
589 | }
590 | ],
591 | "stateMutability": "view",
592 | "type": "function"
593 | },
594 | {
595 | "inputs": [],
596 | "name": "PAUSER_ROLE",
597 | "outputs": [
598 | {
599 | "internalType": "bytes32",
600 | "name": "",
601 | "type": "bytes32"
602 | }
603 | ],
604 | "stateMutability": "view",
605 | "type": "function"
606 | },
607 | {
608 | "inputs": [
609 | {
610 | "internalType": "bytes4",
611 | "name": "interfaceId",
612 | "type": "bytes4"
613 | }
614 | ],
615 | "name": "supportsInterface",
616 | "outputs": [
617 | {
618 | "internalType": "bool",
619 | "name": "",
620 | "type": "bool"
621 | }
622 | ],
623 | "stateMutability": "view",
624 | "type": "function"
625 | },
626 | {
627 | "inputs": [],
628 | "name": "symbol",
629 | "outputs": [
630 | {
631 | "internalType": "string",
632 | "name": "",
633 | "type": "string"
634 | }
635 | ],
636 | "stateMutability": "view",
637 | "type": "function"
638 | },
639 | {
640 | "inputs": [],
641 | "name": "totalSupply",
642 | "outputs": [
643 | {
644 | "internalType": "uint256",
645 | "name": "",
646 | "type": "uint256"
647 | }
648 | ],
649 | "stateMutability": "view",
650 | "type": "function"
651 | }
652 | ]
653 |
--------------------------------------------------------------------------------