ethsub
Version:
A bridge between ethereum and substrate
214 lines (213 loc) • 8.57 kB
JavaScript
"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);
}