ethsub
Version:
A bridge between ethereum and substrate
247 lines (246 loc) • 9.72 kB
JavaScript
"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);