UNPKG

ethsub

Version:

A bridge between ethereum and substrate

214 lines (213 loc) 8.57 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.init = exports.Service = void 0; const api_1 = require("@polkadot/api"); const keyring_1 = require("@polkadot/keyring"); const util_crypto_1 = require("@polkadot/util-crypto"); const lodash_1 = __importDefault(require("lodash")); const use_services_1 = require("use-services"); const utils_1 = require("./utils"); const types_1 = require("./types"); const services_1 = require("./services"); class Service { constructor(option) { this.destoryed = false; if (option.deps.length !== 1) { throw new Error("miss deps [store]"); } this.store = option.deps[0]; this.args = option.args; this.chainId = this.args.chainId; } async [use_services_1.INIT_KEY]() { this.api = new api_1.ApiPromise({ provider: new api_1.WsProvider(this.args.url), }); await this.api.isReady; this.wallet = await createWallet(this.args.secret); this.currentBlock = Math.max(this.args.startBlock, await this.store.loadSubBlockNum()); } async [use_services_1.STOP_KEY]() { this.destoryed = true; } async pullBlocks() { while (true) { if (this.destoryed) break; const finalizedHash = await this.api.rpc.chain.getFinalizedHead(); const header = await this.api.rpc.chain.getHeader(finalizedHash); const latestBlock = header.number.toNumber(); if (latestBlock <= this.currentBlock) { await (0, utils_1.sleep)(3000); continue; } await this.parseBlock(); await this.store.storeSubBlockNum(this.currentBlock); } } async pullMsgs(source) { while (true) { if (this.destoryed) break; const msg = await this.store.nextMsg(source, this.chainId); if (!msg) { await (0, utils_1.sleep)(3000); continue; } const ok = await this.writeChain(msg); if (ok) { await services_1.srvs.store.storeMsgStatus(msg, types_1.BridgeMsgStatus.Success); } else { await services_1.srvs.store.storeMsgStatus(msg, types_1.BridgeMsgStatus.Fail); } } } async writeChain(msg) { const { source, nonce, resource } = msg; const logCtx = { source, nonce }; try { const { sub: { resourceId }, } = resource; const maybeMethod = (await this.api.query.bridge.resources(resourceId)); if (maybeMethod.isNone) { throw new Error(`Write sub throw invalid resource ${resource.name}`); } const method = maybeMethod.unwrap().toUtf8(); let callData; if (msg.type === types_1.ResourceType.ERC20) { callData = this.createErc20Call(msg, method); } else { throw new Error(`Write sub throw unknown msg type ${msg.type}`); } const should = await this.shouldProposal(msg, callData); if (!should) { return true; } await this.sendTx(this.api.tx.bridge.acknowledgeProposal(nonce, source, resourceId, callData)); services_1.srvs.logger.info(`Proposal acknowledge`, logCtx); return true; } catch (err) { if (/bridge\.ProposalAlreadyComplete/.test(err.message)) { return true; } services_1.srvs.logger.error(err, logCtx); return false; } } async parseBlock() { const blockNum = this.currentBlock; const blockHash = await this.api.rpc.chain.getBlockHash(blockNum); services_1.srvs.logger.debug(`Parse sub block ${blockNum}`); const events = await this.api.query.system.events.at(blockHash); for (const evt of events) { let msg; const { event } = evt; if (event.section !== "bridge") continue; if (event.method === "FungibleTransfer") { const resourceId = event.data[2].toString(); const resourceIdData = services_1.srvs.settings.resources.find((v) => v.sub.resourceId === resourceId); if (!resourceIdData) continue; msg = this.parseErc20(event, resourceIdData); } else { continue; } await services_1.srvs.store.storeMsg(msg); } this.currentBlock += 1; } createErc20Call(msg, method) { const { source, nonce, resource, payload: { recipient, amount }, } = msg; services_1.srvs.logger.info(`Write erc20 propsoal`, { source, nonce }); const realAmount = (0, utils_1.exchangeAmount)(amount, resource.eth.decimals, resource.sub.decimals); const fn = lodash_1.default.get(this.api.tx, method); if (!fn || fn.meta.args.length !== 3) { `Resource ${resource.name} with invalid method ${method}`; } return fn("0x" + recipient, realAmount, resource.sub.resourceId); } parseErc20(event, resourceData) { const destination = parseInt(event.data[0].toString()); const nonce = parseInt(event.data[1].toString()); const amount = event.data[3].toString(); const recipient = event.data[4].toString(); const msg = { source: this.args.chainId, destination, nonce, type: types_1.ResourceType.ERC20, resource: resourceData, payload: { amount, recipient }, }; return msg; } async shouldProposal(msg, call) { const { source, nonce } = msg; const maybeVote = (await this.api.query.bridge.votes(msg.source, [ msg.nonce, call, ])); if (maybeVote.isNone) { return true; } const vote = maybeVote.unwrap(); if (vote.status.isInitiated) { if (vote.votesFor.find((v) => v.eq(this.wallet.address)) || vote.votesAgainst.find((v) => v.eq(this.wallet.address))) { services_1.srvs.logger.info("Proposal voted", { source, nonce }); return false; } return true; } else { services_1.srvs.logger.info("Proposal completed", { source, nonce }); return false; } } async sendTx(tx) { if (process.env.DRY_RUN) return; return new Promise((resolve, reject) => { tx.signAndSend(this.wallet, ({ events = [], status }) => { if (status.isInvalid || status.isDropped || status.isUsurped) { reject(new Error(`${status.type} transaction.`)); return; } if (status.isInBlock) { events.forEach(({ event: { data, method, section } }) => { if (section === "system" && method === "ExtrinsicFailed") { const [dispatchError] = data; if (dispatchError.isModule) { const mod = dispatchError.asModule; const error = this.api.registry.findMetaError(new Uint8Array([mod.index.toNumber(), mod.error.toNumber()])); throw new Error(`Transaction throw ${error.section}.${error.name}, ${error.docs.join("")}`); } else { throw new Error(`Transaction throw ${dispatchError.type}`); } } else if (method === "ExtrinsicSuccess") { resolve(); } }); } }).catch((e) => { reject(e); }); }); } } exports.Service = Service; exports.init = (0, use_services_1.createInitFn)(Service); async function createWallet(privateKey) { await (0, util_crypto_1.cryptoWaitReady)(); const keyring = new keyring_1.Keyring({ type: "sr25519" }); return keyring.addFromUri(privateKey); }