@renproject/ren
Version:
Official Ren JavaScript SDK for bridging crypto assets cross-chain.
412 lines • 21 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { DefaultTxWaiter, ErrorWithCode, generateGHash, generatePHash, generateSHash, InputType, isContractChain, isDepositChain, OutputType, RenJSError, utils, } from "@renproject/utils";
import { OrderedMap } from "immutable";
import { GatewayTransaction } from "./gatewayTransaction";
import { defaultRenJSConfig } from "./utils/config";
import { estimateTransactionFee } from "./utils/fees";
import { getInputAndOutputTypes } from "./utils/inputAndOutputTypes";
import { TransactionEmitter } from "./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.
*/
export 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 = OrderedMap();
/** The event emitter that handles "transaction" events. */
this.eventEmitter = new 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 = () => __awaiter(this, void 0, void 0, function* () {
const { to, asset, from } = this.params;
const { inputType, outputType, selector } = yield getInputAndOutputTypes({
asset,
fromChain: this.fromChain,
toChain: this.toChain,
});
this._inputType = inputType;
this._outputType = outputType;
this._selector = selector;
if (this.outputType === OutputType.Release &&
!isContractChain(this.fromChain)) {
throw ErrorWithCode.updateError(new Error(`Cannot release from non-contract chain ${this.fromChain.chain}`), RenJSError.PARAMETER_ERROR);
}
if (this.outputType === OutputType.Mint &&
!isContractChain(this.toChain)) {
throw ErrorWithCode.updateError(new Error(`Cannot mint ${asset} to non-contract chain ${this.toChain.chain}`), RenJSError.PARAMETER_ERROR);
}
const [fees, confirmationTarget, shard, payload, isDepositAssetOnFromChain,] = yield Promise.all([
estimateTransactionFee(this.provider, asset, this.fromChain, this.toChain),
this.provider.getConfirmationTarget(this.fromChain.chain),
(() => __awaiter(this, void 0, void 0, function* () {
if (utils.isDefined(this.params.shard)) {
return this.params.shard;
}
if (this.inputType === InputType.Lock &&
isDepositChain(this.fromChain) &&
(yield this.fromChain.isDepositAsset(asset))) {
return yield this.provider.selectShard(this.params.asset);
}
else {
return {
gPubKey: "",
};
}
}))(),
this.toChain.getOutputPayload(asset, this.inputType, this.outputType, to),
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 (!(yield 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 = generatePHash(payload.payload);
}
this.params.shard = shard;
this._inConfirmationTarget = confirmationTarget;
// const sHash = utils.Ox(generateSHash(this.selector));
const sHash = generateSHash(`${this.params.asset}/to${this.params.to.chain}`);
if (isDepositChain(this.fromChain) && isDepositAssetOnFromChain) {
try {
if (!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 ErrorWithCode(`RenVM shard not selected.`, RenJSError.INTERNAL_ERROR);
}
// Convert nonce to Uint8Array (using `0` if no nonce is set.)
const nonce = typeof this.params.nonce === "string"
? utils.fromBase64(this.params.nonce)
: utils.toNBytes(this.params.nonce || 0, 32);
const gHash = generateGHash(this.pHash, sHash, payload.toBytes, nonce);
this._gHash = gHash;
const gPubKey = utils.fromBase64(this.params.shard.gPubKey);
this.config.logger.debug("gPubKey:", utils.Ox(gPubKey));
if (!gPubKey || gPubKey.length === 0) {
throw ErrorWithCode.updateError(new Error("Unable to fetch RenVM shard public key."), 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 = yield 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.fromBase64("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
gPubKey: gPubKey,
gHash: gHash,
};
// Submit the gateway details to the back-up submitGateway
// endpoint.
void utils
.POST("https://validate-mint.herokuapp.com/", JSON.stringify({
gateway: gatewayAddress,
gatewayDetails,
}))
.catch(() => {
/* Ignore error. */
});
try {
// Submit the gateway details to the submitGateway endpoint.
yield utils.tryNTimes(() => __awaiter(this, void 0, void 0, function* () {
yield this.provider.submitGateway(gatewayAddress, gatewayDetails);
}), 5);
}
catch (error) {
throw new ErrorWithCode(`Error submitting gateway details: ${utils.extractError(error)}`, RenJSError.GATEWAY_SUBMISSION_FAILED);
}
this.config.logger.debug("gateway address:", this.gatewayAddress);
}
catch (error) {
throw ErrorWithCode.updateError(error, error.code || RenJSError.INTERNAL_ERROR);
}
// Will fetch deposits as long as there's at least one subscription.
this._watchForDeposits().catch(this.config.logger.error);
}
if (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] = yield 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 = (identifier, tx) => __awaiter(this, void 0, void 0, function* () {
// FIXME: Temporary work-around for data race.
yield utils.sleep(0);
this.transactions = this.transactions.set(identifier, tx);
});
this.removeTransaction = (identifier) => __awaiter(this, void 0, void 0, function* () {
// FIXME: Temporary work-around for data race.
yield 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 = (inputTx) => __awaiter(this, void 0, void 0, function* () {
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 = () => __awaiter(this, void 0, void 0, function* () {
if (!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.toURLBase64(
// Check if the deposit has an associated nonce. This will
// be true for contract-based inputs.
inputTx.nonce
? 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.fromBase64(this.params.nonce)
: 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 DefaultTxWaiter({
chainTransaction: inputTx,
chain: this.fromChain,
target: this.inConfirmationTarget,
});
}
const transaction = new GatewayTransaction(this.provider, this.fromChain, this.toChain, params, inTx, this.config);
yield transaction.initialize();
this.eventEmitter.emit("transaction", transaction);
// this.deposits.set(deposit);
this.config.logger.debug("new deposit:", inputTx);
return transaction;
});
const promise = createGatewayTransaction();
yield this.addTransaction(depositIdentifier, promise);
try {
yield this.addTransaction(depositIdentifier, yield promise);
}
catch (error) {
yield this.removeTransaction(depositIdentifier);
const message = `Error processing deposit ${inputTx.txHash}: ${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 (yield 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 = () => __awaiter(this, void 0, void 0, function* () {
if (!this.gatewayAddress ||
!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()) {
yield utils.sleep(1 * utils.sleep.SECONDS);
continue;
}
yield 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.extractError(error));
}
yield 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({}, 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;
}
}
//# sourceMappingURL=gateway.js.map