@renproject/ren
Version:
Official Ren JavaScript SDK for bridging crypto assets cross-chain.
407 lines • 20.5 kB
JavaScript
"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