UNPKG

ethsub

Version:

A bridge between ethereum and substrate

247 lines (246 loc) 9.72 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.init = exports.Service = void 0; const ethers_1 = require("ethers"); const use_services_1 = require("use-services"); const utils_1 = require("./utils"); const types_1 = require("./types"); const services_1 = require("./services"); const EventSig = { Deposit: "Deposit(uint8,bytes32,uint64,address,bytes,bytes)", }; const { BigNumber } = ethers_1.ethers; const CONTRACT_PATH = "@ethsub/sol/build/contracts"; const ContractABIs = { Bridge: require(CONTRACT_PATH + "/Bridge.json"), Erc20Handler: require(CONTRACT_PATH + "/ERC20Handler.json"), }; 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]() { const { url, startBlock, privateKey, contracts } = this.args; this.provider = new ethers_1.ethers.providers.StaticJsonRpcProvider(url); this.wallet = new ethers_1.ethers.Wallet(privateKey, this.provider); this.bridge = new ethers_1.ethers.Contract(contracts.bridge, ContractABIs.Bridge.abi, this.wallet); this.erc20Handler = new ethers_1.ethers.Contract(contracts.erc20Handler, ContractABIs.Erc20Handler.abi, this.wallet); this.currentBlockNum = Math.max(startBlock, await this.store.loadEthBlockNum()); this.latestBlockNum = 0; } async [use_services_1.STOP_KEY]() { this.destoryed = true; } async pullBlocks() { this.subscribeLatestBlock(); while (true) { if (this.destoryed) break; const confirmBlockNum = this.latestBlockNum - this.args.confirmBlocks; if (this.currentBlockNum >= confirmBlockNum) { await (0, utils_1.sleep)(2500); continue; } await this.parseBlock(); await this.store.storeEthBlockNum(this.currentBlockNum); } } 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 } = msg; const logCtx = { source, nonce }; try { let proposalData; if (msg.type === types_1.ResourceType.ERC20) { proposalData = this.createErc20Data(msg); } else { throw new Error(`Write eth throw unknown msg type ${msg.type}`); } const { data, dataHash } = proposalData; let proposal = await this.getProposal(msg, dataHash); if (proposal._status === 3 || proposal._status === 4) { services_1.srvs.logger.info(`Proposal completed, skip voting`, logCtx); return true; } else if (proposal._status === 0) { await this.voteProposal(msg, data, dataHash); services_1.srvs.logger.info(`Proposal vote`, logCtx); } else if (proposal._status === 1) { const voted = await this.hasVoted(msg, dataHash); if (voted) { services_1.srvs.logger.info(`Proposal voted`, logCtx); return true; } await this.voteProposal(msg, data, dataHash); services_1.srvs.logger.info(`Proposal vote`, logCtx); } proposal = await this.getProposal(msg, dataHash); if (proposal._status === 2) { await this.executeProposal(msg, data); services_1.srvs.logger.info(`Proposal execute`, logCtx); } return true; } catch (err) { services_1.srvs.logger.error(err, logCtx); return false; } } async subscribeLatestBlock() { while (true) { if (this.destoryed) break; const latestBlockNum = await this.provider.getBlockNumber(); if (latestBlockNum > this.latestBlockNum) { this.latestBlockNum = latestBlockNum; } else { await (0, utils_1.sleep)(2999); } } } async parseBlock() { const blockNum = this.currentBlockNum; services_1.srvs.logger.debug(`Parse eth block ${blockNum}`); const logs = await this.provider.getLogs({ fromBlock: blockNum, toBlock: blockNum, address: this.bridge.address, topics: [ethers_1.ethers.utils.id(EventSig.Deposit)], }); for (const log of logs) { const logDesc = this.bridge.interface.parseLog(log); const resourceIdData = services_1.srvs.settings.resources.find((v) => v.eth.resourceId.toLowerCase() === logDesc.args.resourceID); if (!resourceIdData) continue; let msg; if (resourceIdData.type === types_1.ResourceType.ERC20) { msg = this.parseErc20(logDesc, resourceIdData); } else { continue; } await services_1.srvs.store.storeMsg(msg); } this.currentBlockNum += 1; } createErc20Data(msg) { 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.sub.decimals, resource.eth.decimals); const data = "0x" + ethers_1.ethers.utils .hexZeroPad(BigNumber.from(realAmount).toHexString(), 32) .substr(2) + // Deposit Amount (32 bytes) ethers_1.ethers.utils .hexZeroPad(ethers_1.ethers.utils.hexlify((recipient.length - 2) / 2), 32) .substr(2) + // len(recipientAddress) (32 bytes) recipient.substr(2); // recipientAddress (?? bytes) const dataHash = ethers_1.ethers.utils.solidityKeccak256(["address", "bytes"], [this.erc20Handler.address, data]); return { data, dataHash }; } parseErc20(log, resourceData) { const { data, destinationDomainID, depositNonce } = log.args; const amount = BigNumber.from(ethers_1.ethers.utils.stripZeros(data.slice(0, 66))).toString(); const recipient = data.slice(130); const msg = { source: this.args.chainId, destination: destinationDomainID, nonce: depositNonce.toNumber(), type: types_1.ResourceType.ERC20, resource: resourceData, payload: { amount, recipient }, }; return msg; } async waitTx(tx) { try { await tx.wait(this.args.confirmBlocks); } catch (err) { const reason = await this.revertReason(tx); if (/proposal already executed\/cancelled/.test(reason)) { return; } throw new Error(`Transaction ${tx.hash} throws ${reason}`); } } async hasVoted(msg, dataHash) { return this.bridge._hasVotedOnProposal(this.idAndNonce(msg), dataHash, this.wallet.address); } async voteProposal(msg, data, dataHash) { const { source, nonce, resource } = msg; const { eth: { resourceId }, } = resource; const tx = await this.bridge.voteProposal(source, nonce, resourceId, data, { gasLimit: this.args.gasLimit, ...(await this.getGasOpts()), }); await this.waitTx(tx); } async executeProposal(msg, data) { const { source, nonce, resource } = msg; const { eth: { resourceId }, } = resource; const tx = await this.bridge.executeProposal(source, nonce, data, resourceId, true, { gasLimit: this.args.gasLimit, ...(await this.getGasOpts()), }); await this.waitTx(tx); } getProposal(msg, dataHash) { return this.bridge.getProposal(msg.source, msg.nonce, dataHash); } idAndNonce(msg) { return (msg.nonce << 8) + msg.source; } async getGasOpts() { const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = await this.provider.getFeeData(); if (maxFeePerGas) { return { maxFeePerGas, maxPriorityFeePerGas }; } else { return { gasPrice }; } } async revertReason(tx) { var _a; try { if (tx.maxFeePerGas) delete tx.gasPrice; const code = await this.provider.call(tx, tx.blockNumber); return ethers_1.ethers.utils.toUtf8String("0x" + code.substr(138)); } catch (err) { let reason = (_a = err.error) === null || _a === void 0 ? void 0 : _a.message; if (reason) reason = reason.replace("execution reverted: ", ""); return reason; } } } exports.Service = Service; exports.init = (0, use_services_1.createInitFn)(Service);