@nomad-xyz/sdk-bridge
Version:
424 lines • 18.6 kB
JavaScript
"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