UNPKG

@renproject/ren

Version:

Official Ren JavaScript SDK for bridging crypto assets cross-chain.

407 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Gateway = void 0; const utils_1 = require("@renproject/utils"); const immutable_1 = require("immutable"); const gatewayTransaction_1 = require("./gatewayTransaction"); const config_1 = require("./utils/config"); const fees_1 = require("./utils/fees"); const inputAndOutputTypes_1 = require("./utils/inputAndOutputTypes"); const transactionEmitter_1 = require("./utils/transactionEmitter"); /** * A Gateway allows moving funds through RenVM. Its defined by the asset being * moved, an origin chain and a target chain. For each of these chains, a * payload can be specified, allowing for more complex bridgings involving * contract interactions. * * A Gateway will be of one of two types - a deposit gateway, requiring users * to send funds to a gateway address, or a contract gateway, requiring users to * submit a specific transaction. For example, when moving BTC from Bitcoin * to Ethereum, the user will have to send BTC to a Bitcoin address (the gateway * address). When moving DAI from Ethereum to Polygon, the user will have to * submit a transaction locking the DAI on Ethereum. * * When these deposits or transactions are initiated, a GatewayTransaction * instance is created. */ class Gateway { /** * @hidden - should be created using [[RenJS.lockAndMint]] instead. */ constructor(renVM, fromChain, toChain, params, config = {}) { /** * A map of transactions that are required to be submitted before * `gateway.in` can be called. */ this.inSetup = {}; /** * Deposits represents the lock deposits that have been detected so far. */ this.transactions = (0, immutable_1.OrderedMap)(); /** The event emitter that handles "transaction" events. */ this.eventEmitter = new transactionEmitter_1.TransactionEmitter(() => this.transactions // Check that the transaction isn't a promise. // The result of promises will be emitted when they resolve. // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter((tx) => tx.then === undefined) .map((tx) => tx) .valueSeq() .toArray()); /** * @hidden - Called automatically when calling [[RenJS.gateway]]. It has * been split from the constructor because it is asynchronous. */ this.initialize = async () => { const { to, asset, from } = this.params; const { inputType, outputType, selector } = await (0, inputAndOutputTypes_1.getInputAndOutputTypes)({ asset, fromChain: this.fromChain, toChain: this.toChain, }); this._inputType = inputType; this._outputType = outputType; this._selector = selector; if (this.outputType === utils_1.OutputType.Release && !(0, utils_1.isContractChain)(this.fromChain)) { throw utils_1.ErrorWithCode.updateError(new Error(`Cannot release from non-contract chain ${this.fromChain.chain}`), utils_1.RenJSError.PARAMETER_ERROR); } if (this.outputType === utils_1.OutputType.Mint && !(0, utils_1.isContractChain)(this.toChain)) { throw utils_1.ErrorWithCode.updateError(new Error(`Cannot mint ${asset} to non-contract chain ${this.toChain.chain}`), utils_1.RenJSError.PARAMETER_ERROR); } const [fees, confirmationTarget, shard, payload, isDepositAssetOnFromChain,] = await Promise.all([ (0, fees_1.estimateTransactionFee)(this.provider, asset, this.fromChain, this.toChain), this.provider.getConfirmationTarget(this.fromChain.chain), (async () => { if (utils_1.utils.isDefined(this.params.shard)) { return this.params.shard; } if (this.inputType === utils_1.InputType.Lock && (0, utils_1.isDepositChain)(this.fromChain) && (await this.fromChain.isDepositAsset(asset))) { return await this.provider.selectShard(this.params.asset); } else { return { gPubKey: "", }; } })(), this.toChain.getOutputPayload(asset, this.inputType, this.outputType, to), (0, utils_1.isDepositChain)(this.fromChain) && this.fromChain.isDepositAsset(this.params.asset), ]); this._fees = fees; // Check if the selector is whitelisted - ie currently enabled in RenVM. // Run this later because the underlying queryConfig is cached by // `selectShard`. if (!(await this.provider.selectorWhitelisted(selector))) { throw new Error(`Unable to bridge ${asset} from ${from.chain} to ${to.chain}: Selector ${selector} not whitelisted.`); } if (payload) { this._pHash = (0, utils_1.generatePHash)(payload.payload); } this.params.shard = shard; this._inConfirmationTarget = confirmationTarget; // const sHash = utils.Ox(generateSHash(this.selector)); const sHash = (0, utils_1.generateSHash)(`${this.params.asset}/to${this.params.to.chain}`); if ((0, utils_1.isDepositChain)(this.fromChain) && isDepositAssetOnFromChain) { try { if (!(0, utils_1.isContractChain)(this.toChain)) { throw new Error(`Cannot mint ${asset} to non-contract chain ${this.toChain.chain}.`); } if (!payload) { throw new Error(`No target payload set.`); } if (!this.params.shard) { throw new utils_1.ErrorWithCode(`RenVM shard not selected.`, utils_1.RenJSError.INTERNAL_ERROR); } // Convert nonce to Uint8Array (using `0` if no nonce is set.) const nonce = typeof this.params.nonce === "string" ? utils_1.utils.fromBase64(this.params.nonce) : utils_1.utils.toNBytes(this.params.nonce || 0, 32); const gHash = (0, utils_1.generateGHash)(this.pHash, sHash, payload.toBytes, nonce); this._gHash = gHash; const gPubKey = utils_1.utils.fromBase64(this.params.shard.gPubKey); this.config.logger.debug("gPubKey:", utils_1.utils.Ox(gPubKey)); if (!gPubKey || gPubKey.length === 0) { throw utils_1.ErrorWithCode.updateError(new Error("Unable to fetch RenVM shard public key."), utils_1.RenJSError.NETWORK_ERROR); } if (!gHash || gHash.length === 0) { throw new Error("Invalid gateway hash being passed to gateway address generation."); } if (!asset || asset.length === 0) { throw new Error("Invalid asset being passed to gateway address generation."); } const gatewayAddress = await this.fromChain.createGatewayAddress(this.params.asset, this.params.from, gPubKey, gHash); this.gatewayAddress = gatewayAddress; const gatewayDetails = { selector, payload: payload.payload, pHash: this.pHash, to: payload.to, nonce: nonce, nHash: utils_1.utils.fromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), gPubKey: gPubKey, gHash: gHash, }; // Submit the gateway details to the back-up submitGateway // endpoint. void utils_1.utils .POST("https://validate-mint.herokuapp.com/", JSON.stringify({ gateway: gatewayAddress, gatewayDetails, })) .catch(() => { /* Ignore error. */ }); try { // Submit the gateway details to the submitGateway endpoint. await utils_1.utils.tryNTimes(async () => { await this.provider.submitGateway(gatewayAddress, gatewayDetails); }, 5); } catch (error) { throw new utils_1.ErrorWithCode(`Error submitting gateway details: ${utils_1.utils.extractError(error)}`, utils_1.RenJSError.GATEWAY_SUBMISSION_FAILED); } this.config.logger.debug("gateway address:", this.gatewayAddress); } catch (error) { throw utils_1.ErrorWithCode.updateError(error, error.code || utils_1.RenJSError.INTERNAL_ERROR); } // Will fetch deposits as long as there's at least one subscription. this._watchForDeposits().catch(this.config.logger.error); } if ((0, utils_1.isContractChain)(this.fromChain)) { const processInput = (input) => { // TODO: Add to queue instead so that it can be retried on error. this.processDeposit(input).catch(this.config.logger.error); }; // TODO const removeInput = () => { }; let inSetup; [this.in, inSetup] = await Promise.all([ this.fromChain.getInputTx(this.inputType, this.outputType, asset, from, () => ({ toChain: to.chain, toPayload: payload, gatewayAddress: this.gatewayAddress, }), this.inConfirmationTarget, processInput, removeInput), this.fromChain.getInSetup && this.fromChain.getInSetup(asset, this.inputType, this.outputType, from, () => ({ toChain: to.chain, toPayload: payload, gatewayAddress: this.gatewayAddress, })), ]); if (inSetup) { this.inSetup = Object.assign(Object.assign({}, this.inSetup), inSetup); } } return this; }; this.addTransaction = async (identifier, tx) => { // FIXME: Temporary work-around for data race. await utils_1.utils.sleep(0); this.transactions = this.transactions.set(identifier, tx); }; this.removeTransaction = async (identifier) => { // FIXME: Temporary work-around for data race. await utils_1.utils.sleep(0); this.transactions = this.transactions.remove(identifier); }; /** * `processDeposit` allows you to manually provide the details of a deposit * and returns a [[GatewayTransaction]] object. * * @param inputTx The deposit details in the format defined by the * LockChain. This should be the same format as `deposit.depositDetails` for * a deposit returned from `.on("transaction", ...)`. * * ```ts * const gatewayTransaction = await gateway * .processDeposit({ * chain: "Ethereum", * txHash: "0xef90...", * txid: "752...", * txindex: "0", * amount: "1", * }) * ``` * @category Main */ this.processDeposit = async (inputTx) => { const depositIdentifier = `${String(inputTx.txid)}_${String(inputTx.txindex)}`; const existingTransaction = this.transactions.get(depositIdentifier); // If the transaction hasn't been seen before. // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!existingTransaction) { const createGatewayTransaction = async () => { if (!utils_1.utils.isDefined(this.inConfirmationTarget)) { throw new Error("Gateway address must be generated before calling 'processDeposit'."); } // Determine which nonce to use - converting it to a Uint8Array // to ensure it's in a standard format before calling // utils.toURLBase64 again. const nonce = utils_1.utils.toURLBase64( // Check if the deposit has an associated nonce. This will // be true for contract-based inputs. inputTx.nonce ? utils_1.utils.fromBase64(inputTx.nonce) : // Check if the params have a nonce - this can be // a base64 string or a number. If no nonce is set, // default to `0`. typeof this.params.nonce === "string" ? utils_1.utils.fromBase64(this.params.nonce) : utils_1.utils.toNBytes(this.params.nonce || 0, 32)); const params = { asset: this.params.asset, fromTx: inputTx, to: this.params.to, shard: this.params.shard, nonce, }; // Check if `this.in` can be re-used or if a new DefaultTxWaiter // should be created. let inTx = this.in; if ( // No inTx. !inTx || // inTx is for another transaction. (inTx.progress.transaction && (inTx.progress.transaction.txHash !== inputTx.txHash || inTx.progress.transaction.txindex !== inputTx.txindex))) { inTx = new utils_1.DefaultTxWaiter({ chainTransaction: inputTx, chain: this.fromChain, target: this.inConfirmationTarget, }); } const transaction = new gatewayTransaction_1.GatewayTransaction(this.provider, this.fromChain, this.toChain, params, inTx, this.config); await transaction.initialize(); this.eventEmitter.emit("transaction", transaction); // this.deposits.set(deposit); this.config.logger.debug("new deposit:", inputTx); return transaction; }; const promise = createGatewayTransaction(); await this.addTransaction(depositIdentifier, promise); try { await this.addTransaction(depositIdentifier, await promise); } catch (error) { await this.removeTransaction(depositIdentifier); const message = `Error processing deposit ${inputTx.txHash}: ${utils_1.utils.extractError(error)}`; if (error instanceof Error) { error.message = message; } else { error = new Error(message); } throw error; } } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return (await existingTransaction); }; /** PRIVATE METHODS */ this._defaultGetter = (name) => { if (this[name] === undefined) { throw new Error(`Must call 'initialize' before accessing '${name}'.`); } return this[name]; }; /** * Internal method that fetches deposits to the gateway address. If there * are no listeners to the "transaction" event, then it pauses fetching * deposits. */ this._watchForDeposits = async () => { if (!this.gatewayAddress || !(0, utils_1.isDepositChain)(this.fromChain) || !this.fromChain.watchForDeposits) { return; } while (true) { try { const listenerCancelled = () => this.eventEmitter.listenerCount("transaction") === 0; // Change the return type of `this.processDeposit` to `void`. const onDeposit = (deposit) => { try { // TODO: Handle error. this.processDeposit(deposit).catch(this.config.logger.error); } catch (error) { this.config.logger.error(error); } }; // TODO: Flag deposits that have been cancelled, updating their status. const cancelDeposit = () => { }; // If there are no listeners, continue. TODO: Exit loop entirely // until a lister is added again. if (listenerCancelled()) { await utils_1.utils.sleep(1 * utils_1.utils.sleep.SECONDS); continue; } await this.fromChain.watchForDeposits(this.params.asset, this.params.from, this.gatewayAddress, onDeposit, cancelDeposit, listenerCancelled); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) { this.config.logger.error(utils_1.utils.extractError(error)); } await utils_1.utils.sleep(this.config.networkDelay); } }; this.params = Object.assign({}, params); this.fromChain = fromChain; this.toChain = toChain; this.provider = renVM; this.config = Object.assign(Object.assign({}, config_1.defaultRenJSConfig), config); { // Debug log this.config.logger.debug("gateway created:", String(renVM.endpointOrProvider), fromChain.chain, toChain.chain, this.params, config); } } /** The RenVM contract selector. */ get selector() { return this._defaultGetter("_selector") || this._selector; } /** The gateway hash, a hash of the payload, selector, to and nonce. */ get gHash() { return this._defaultGetter("_gHash") || this._gHash; } /** The payload hash, a hash of the payload. */ get pHash() { return this._defaultGetter("_pHash") || this._pHash; } /** The RenVM and blockchain fees of the gateway. */ get fees() { return this._defaultGetter("_fees") || this._fees; } /** The input type of the transaction, either a lock or a burn. */ get inputType() { return this._defaultGetter("_inputType") || this._inputType; } /** The output type of the transaction, either a mint or a release. */ get outputType() { return this._defaultGetter("_outputType") || this._outputType; } /** The number of confirmations required for `gateway.in`. */ get inConfirmationTarget() { return (this._defaultGetter("_inConfirmationTarget") || this._inConfirmationTarget); } /** * Listen to "transaction" events, registering a callback that will be * called for all previous transactions and for any future transaction. * * To remove the listener, call `gateway.eventEmitter.removeListener` or * `gateway.eventEmitter.removeAllListeners`. */ on(event, callback) { this.eventEmitter.on(event, callback); return this; } } exports.Gateway = Gateway; //# sourceMappingURL=gateway.js.map