UNPKG

@nomad-xyz/sdk-bridge

Version:
424 lines 18.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BridgeContext = void 0; const ethers_1 = require("ethers"); const multi_provider_1 = require("@nomad-xyz/multi-provider"); const bridge = __importStar(require("@nomad-xyz/contracts-bridge")); const sdk_1 = require("@nomad-xyz/sdk"); const bytes_1 = require("@ethersproject/bytes"); const BridgeContracts_1 = require("./BridgeContracts"); const BridgeMessage_1 = require("./BridgeMessage"); const backend_1 = require("./backend"); const DEFAULT_GAS_LIMIT = ethers_1.BigNumber.from(350000); const MOCK_GOERLI_ACCOUNTANT = '0x661afa47c0efeaee1414116b4fe7182dab37dc7b'; /** * The BridgeContext manages connections to Nomad Bridge contracts. * It inherits from the {@link MultiProvider} and {@link NomadContext} and * ensures that its contracts always use the latest registered providers and * signers. */ class BridgeContext extends sdk_1.NomadContext { constructor(environment = 'development', backend) { super(environment, backend); this._backend = backend; this.bridges = new Map(); for (const network of this.conf.networks) { const conf = this.conf.bridge[network]; if (!conf.bridgeRouter) throw new Error('substrate not yet supported'); const bridge = new BridgeContracts_1.BridgeContracts(this, network, conf); this.bridges.set(bridge.domain, bridge); } } /** * Create default backend for the context */ withDefaultBackend() { // TODO: What if backend doesn't exist for this environment? this._backend = backend_1.GoldSkyBridgeBackend.default(this.environment, this); return this; } static fromNomadContext(context) { const bridge = new BridgeContext(context.conf); for (const domain of context.domainNumbers) { const provider = context.getProvider(domain); if (provider) bridge.registerProvider(domain, provider); const signer = context.getSigner(domain); if (signer) bridge.registerSigner(domain, signer); } return bridge; } /** * Get the {@link BridgeContracts} for a given domain (or undefined) * * @param nameOrDomain A domain name or number. * @returns a {@link BridgeContracts} object (or undefined) */ getBridge(nameOrDomain) { const domain = this.resolveDomainName(nameOrDomain); return this.bridges.get(domain); } /** * Get the {@link BridgeContracts} for a given domain (or throw an error) * * @param nameOrDomain A domain name or number. * @returns a {@link BridgeContracts} object * @throws if no {@link BridgeContracts} object exists on that domain. */ mustGetBridge(nameOrDomain) { const bridge = this.getBridge(nameOrDomain); if (!bridge) { throw new Error(`Missing bridge for domain: ${nameOrDomain}`); } return bridge; } /** * Resolve the local representation of a token on some domain. E.g. find the * deployed Celo address of Ethereum's Sushi Token. * * WARNING: do not hold references to this contract, as it will not be * reconnected in the event the chain connection changes. * * @param nameOrDomain the target domain, which hosts the representation * @param token The token to locate on that domain * @returns An interface for that token (if it has been deployed on that * domain) */ async resolveRepresentation(nameOrDomain, token) { const domain = this.resolveDomain(nameOrDomain); const bridgeContracts = this.getBridge(domain); const tokenDomain = this.resolveDomain(token.domain); const tokenId = multi_provider_1.utils.canonizeId(token.id); const address = await bridgeContracts?.tokenRegistry['getLocalAddress(uint32,bytes32)'](tokenDomain, tokenId); if (!address || address == ethers_1.ethers.constants.AddressZero) { return; } const connection = this.getConnection(domain); if (!connection) { throw new Error(`No provider or signer for ${domain}. Register a connection first before calling resolveRepresentation.`); } return bridge.BridgeToken__factory.connect(multi_provider_1.utils.evmId(address), connection); } /** * Resolve the local representation of a token on ALL known domain. E.g. * find ALL deployed addresses of Ethereum's Sushi Token, on all registered * domains. * * WARNING: do not hold references to these contracts, as they will not be * reconnected in the event the chain connection changes. * * @param token The token to locate on ALL domains * @returns A {@link ResolvedTokenInfo} object with representation addresses */ async resolveRepresentations(token) { const tokens = new Map(); await Promise.all(this.domainNumbers.map(async (domain) => { const tok = await this.resolveRepresentation(domain, token); if (tok) { tokens.set(domain, tok); } })); return { domain: this.resolveDomain(token.domain), id: token.id, tokens, }; } /** * Resolve the canonical domain and identifier for a representation on some * domain. * * @param nameOrDomain The domain hosting the representation * @param representation The address of the representation on that domain * @returns The domain and ID for the canonical token * @throws If the token is unknown to the bridge router on its domain. */ async resolveCanonicalIdentifier(nameOrDomain, representation) { const domain = this.resolveDomain(nameOrDomain); const bridge = this.mustGetBridge(nameOrDomain); const repr = (0, bytes_1.hexlify)(multi_provider_1.utils.canonizeId(representation)); const canonical = await bridge.tokenRegistry.representationToCanonical(representation); if (canonical[0] !== 0) { return { domain: canonical[0], id: canonical[1], }; } // check if it's a local token const local = await bridge.tokenRegistry['getLocalAddress(uint32,bytes32)'](domain, repr); if (local !== ethers_1.ethers.constants.AddressZero) { return { domain, id: (0, bytes_1.hexlify)(multi_provider_1.utils.canonizeId(local)), }; } // throw throw new Error('Token not known to the bridge'); } /** * Resolve an interface for the canonical token corresponding to a * representation on some domain. * * @param nameOrDomain The domain hosting the representation * @param representation The address of the representation on that domain * @returns An interface for that token * @throws If the token is unknown to the bridge router on its domain. */ async resolveCanonicalToken(nameOrDomain, representation) { const canonicalId = await this.resolveCanonicalIdentifier(nameOrDomain, representation); if (!canonicalId) { throw new Error('Token seems to not exist'); } const token = await this.resolveRepresentation(canonicalId.domain, canonicalId); if (!token) { throw new Error('Cannot resolve canonical on its own domain. how did this happen?'); } return token; } /** * Send tokens from one domain to another. Approves the bridge if necessary. * * @param from The domain to send from * @param to The domain to send to * @param token The canonical token to send (details from originating chain) * @param amount The amount (in smallest unit) to send * @param recipient The identifier to send to on the `to` domain * @param enableFast TRUE to enable fast liquidity; FALSE to require no fast liquidity * @param overrides Any tx overrides (e.g. gas price) * @returns a {@link TransferMessage} object representing the in-flight * transfer * @throws On missing signers, missing tokens, tx issues, etc. */ async prepareSend(from, to, token, amount, recipient, enableFast = false, overrides = {}) { const fromDomain = this.resolveDomain(from); await this.checkHome(fromDomain); if (this.blacklist().has(fromDomain)) { throw new sdk_1.FailedHomeError(this, 'Attempted to send token to failed home!'); } const fromBridge = this.mustGetBridge(from); const bridgeAddress = fromBridge.bridgeRouter.address; const fromToken = await this.resolveRepresentation(from, token); if (!fromToken) { throw new Error(`Token not available on ${from}`); } const sender = this.getSigner(from); if (!sender) { throw new Error(`No signer for ${from}`); } const senderAddress = await sender.getAddress(); const approved = await fromToken.allowance(senderAddress, bridgeAddress); // Approve if necessary if (approved.lt(amount)) { const tx = await fromToken.approve(bridgeAddress, ethers_1.ethers.constants.MaxUint256, overrides); await tx.wait(); } if (!overrides.gasLimit) { overrides.gasLimit = DEFAULT_GAS_LIMIT; } // check if it will succeed/fail with callStatic await fromBridge.bridgeRouter.callStatic.send(fromToken.address, amount, this.resolveDomain(to), multi_provider_1.utils.canonizeId(recipient), enableFast, overrides); return fromBridge.bridgeRouter.populateTransaction.send(fromToken.address, amount, this.resolveDomain(to), multi_provider_1.utils.canonizeId(recipient), enableFast, overrides); } /** * Send tokens from one domain to another. Approves the bridge if necessary. * * @param from The domain to send from * @param to The domain to send to * @param token The canonical token to send (details from originating chain) * @param amount The amount (in smallest unit) to send * @param recipient The identifier to send to on the `to` domain * @param enableFast TRUE to enable fast liquidity; FALSE to require no fast liquidity * @param overrides Any tx overrides (e.g. gas price) * @returns a {@link TransferMessage} object representing the in-flight * transfer * @throws On missing signers, missing tokens, tx issues, etc. */ async send(from, to, token, amount, recipient, enableFast = false, overrides = {}) { const tx = await this.prepareSend(from, to, token, amount, recipient, enableFast, overrides); const dispatch = await this.mustGetSigner(from).sendTransaction(tx); const receipt = await dispatch.wait(); const message = await BridgeMessage_1.TransferMessage.singleFromReceipt(this, receipt); if (!message) { throw new Error(); } return message; } /** * Send a chain's native asset from one chain to another using the * `EthHelper` contract. * * @param from The domain to send from * @param to The domain to send to * @param amount The amount (in smallest unit) to send * @param recipient The identifier to send to on the `to` domain * @param enableFast TRUE to enable fast liquidity; FALSE to require no fast liquidity * @param overrides Any tx overrides (e.g. gas price) * @returns a {@link TransferMessage} object representing the in-flight * transfer * @throws On missing signers, tx issues, etc. */ async prepareSendNative(from, to, amount, recipient, enableFast = false, overrides = {}) { const fromDomain = this.resolveDomain(from); await this.checkHome(fromDomain); if (this.blacklist().has(fromDomain)) { throw new sdk_1.FailedHomeError(this, 'Attempted to send token to failed home!'); } const ethHelper = this.mustGetBridge(from).ethHelper; if (!ethHelper) { throw new Error(`No ethHelper for ${from}`); } const toDomain = this.resolveDomain(to); overrides.value = amount; if (!overrides.gasLimit) { overrides.gasLimit = DEFAULT_GAS_LIMIT; } // check if it will succeed/fail with callStatic await ethHelper.callStatic.sendToEVMLike(toDomain, recipient, enableFast, overrides); return ethHelper.populateTransaction.sendToEVMLike(toDomain, recipient, enableFast, overrides); } /** * Send a chain's native asset from one chain to another using the * `EthHelper` contract. * * @param from The domain to send from * @param to The domain to send to * @param amount The amount (in smallest unit) to send * @param recipient The identifier to send to on the `to` domain * @param enableFast TRUE to enable fast liquidity; FALSE to require no fast liquidity * @param overrides Any tx overrides (e.g. gas price) * @returns a {@link TransferMessage} object representing the in-flight * transfer * @throws On missing signers, tx issues, etc. */ async sendNative(from, to, amount, recipient, enableFast = false, overrides = {}) { const tx = await this.prepareSendNative(from, to, amount, recipient, enableFast, overrides); const dispatch = await this.mustGetSigner(from).sendTransaction(tx); const receipt = await dispatch.wait(); const message = await BridgeMessage_1.TransferMessage.singleFromReceipt(this, receipt); if (!message) { throw new Error(); } return message; } /** * Get the accountant associated with this environment, if any. Accountants * will be on Goerli for development, Ethereum for production. */ get accountant() { switch (this.environment) { case 'development': { return bridge.NftAccountant__factory.connect(MOCK_GOERLI_ACCOUNTANT, this.mustGetConnection('goerli')); } case 'production': { return this.mustGetBridge('ethereum').accountant; } default: { return; } } } /** * Get the info associated with the NFT ID, if any. * * @param id The numerical NFT ID * @returns The NFT info, or undefined if no NFT exists, or undefined if * this is not production (i.e. there is no network named "ethereum") * @throws If no signer is available, or if the transaction errors */ async nftInfo(id) { const info = await this.accountant?.info(id); if (!info || info._originalAmount.isZero()) { return; } return info; } /** * Prepare a transaction to recover from the NFT, if possible * * @param id The numerical NFT ID * @returns A populated transaction * @throws If no signer is available, or if the transaction errors */ async prepareRecover(id, overrides = {}) { // first check that the NFT exists const nftInfo = await this.nftInfo(id); if (!nftInfo) return; // mustGetBridge is safe here, as if ethereum doesn't exist, the NFT info // will be undefined if (!this.accountant) throw new multi_provider_1.UnreachableError('checked in nftInfo() call'); // set from so that estimation will succeed const callOverrides = overrides || {}; callOverrides.from = nftInfo._holder; // check if it will succeed/fail with callStatic await this.accountant.callStatic.recover(id, callOverrides); return this.accountant.populateTransaction.recover(id, callOverrides); } /** * Recover from the NFT, if possible * * @param id The numerical NFT ID * @returns A transaction receipt * @throws If no signer is available, or if the transaction errors */ async recover(id, overrides = {}) { const tx = await this.prepareRecover(id, overrides); if (!tx) return; // Netowrk must be either eth or goerli, as this is checked in // /s`prepareRecover` const network = this.environment === 'production' ? 'ethereum' : 'goerli'; const dispatch = await this.mustGetSigner(network).sendTransaction(tx); return await dispatch.wait(); } /** * Read the accountant's information on an asset * * @param id The token address on Ethereum * @returns The asset info (_totalAffected, _totalMinted, _totalCollected, _totalRecovered) */ async assetInfo(token) { return await this.accountant?.assetInfo(token); } /** * Checks if an address is on the allow list * * @param address A 20-byte Ethereum address * @returns Boolean, whether the address is on the allow list or not */ async isAllowed(address) { if (address.length !== 42) throw new Error('Address must be 20 bytes'); if (!this.accountant) throw new Error('Not able to fetch NFT Accountant contract'); return await this.accountant.allowList(address); } } exports.BridgeContext = BridgeContext; //# sourceMappingURL=BridgeContext.js.map