@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
625 lines • 30.6 kB
JavaScript
import { ProtocolType, assert, convertDecimalsToIntegerString, convertToProtocolAddress, convertToScaledAmount, isValidAddress, isZeroishAddress, rootLogger, } from '@hyperlane-xyz/utils';
import { estimateTransactionFeeEthersV5ForGasUnits, } from '../providers/transactionFeeEstimators.js';
import { Token } from '../token/Token.js';
import { TokenAmount } from '../token/TokenAmount.js';
import { parseTokenConnectionId } from '../token/TokenConnection.js';
import { LOCKBOX_STANDARDS, MINT_LIMITED_STANDARDS, TOKEN_COLLATERALIZED_STANDARDS, TOKEN_STANDARD_TO_PROVIDER_TYPE, TokenStandard, } from '../token/TokenStandard.js';
import { EVM_TRANSFER_REMOTE_GAS_ESTIMATE, } from '../token/adapters/EvmTokenAdapter.js';
import { WarpCoreConfigSchema, WarpTxCategory, } from './types.js';
export class WarpCore {
multiProvider;
tokens;
localFeeConstants;
interchainFeeConstants;
routeBlacklist;
logger;
constructor(multiProvider, tokens, options) {
this.multiProvider = multiProvider;
this.tokens = tokens;
this.localFeeConstants = options?.localFeeConstants || [];
this.interchainFeeConstants = options?.interchainFeeConstants || [];
this.routeBlacklist = options?.routeBlacklist || [];
this.logger =
options?.logger ||
rootLogger.child({
module: 'WarpCore',
});
}
/**
* Takes the serialized representation of a warp config and returns a WarpCore instance
* @param multiProvider the MultiProtocolProvider containing chain metadata
* @param config the config object of type WarpCoreConfig
*/
static FromConfig(multiProvider, config) {
// Validate and parse config data
const parsedConfig = WarpCoreConfigSchema.parse(config);
// Instantiate all tokens
const tokens = parsedConfig.tokens.map((t) => new Token({
...t,
addressOrDenom: t.addressOrDenom || '',
connections: undefined,
}));
// Connect tokens together
parsedConfig.tokens.forEach((config, i) => {
for (const connection of config.connections || []) {
const token1 = tokens[i];
const { chainName, addressOrDenom } = parseTokenConnectionId(connection.token);
const token2 = tokens.find((t) => t.chainName === chainName && t.addressOrDenom === addressOrDenom);
assert(token2, `Connected token not found: ${chainName} ${addressOrDenom}`);
token1.addConnection({
...connection,
token: token2,
});
}
});
// Create new Warp
return new WarpCore(multiProvider, tokens, parsedConfig.options);
}
/**
* Queries the token router for an interchain gas quote (i.e. IGP fee).
* Sender is only required for Sealevel origins.
*/
async getInterchainTransferFee({ originToken, destination, sender, }) {
this.logger.debug(`Fetching interchain transfer quote to ${destination}`);
const { chainName: originName } = originToken;
const destinationName = this.multiProvider.getChainName(destination);
let gasAmount;
let gasAddressOrDenom;
// Check constant quotes first
const defaultQuote = this.interchainFeeConstants.find((q) => q.origin === originName && q.destination === destinationName);
if (defaultQuote) {
gasAmount = BigInt(defaultQuote.amount.toString());
gasAddressOrDenom = defaultQuote.addressOrDenom;
}
else {
// Otherwise, compute IGP quote via the adapter
const hypAdapter = originToken.getHypAdapter(this.multiProvider, destinationName);
const destinationDomainId = this.multiProvider.getDomainId(destination);
const quote = await hypAdapter.quoteTransferRemoteGas(destinationDomainId, sender, originToken.igpTokenAddressOrDenom);
gasAmount = BigInt(quote.amount);
gasAddressOrDenom = quote.addressOrDenom;
}
let igpToken;
if (!gasAddressOrDenom) {
// An empty/undefined addressOrDenom indicates the native token
igpToken = Token.FromChainMetadataNativeToken(this.multiProvider.getChainMetadata(originName));
}
else {
const searchResult = this.findToken(originName, gasAddressOrDenom);
assert(searchResult, `Fee token ${gasAddressOrDenom} is unknown`);
igpToken = searchResult;
}
this.logger.debug(`Quoted interchain transfer fee: ${gasAmount} ${igpToken.symbol}`);
return new TokenAmount(gasAmount, igpToken);
}
/**
* Simulates a transfer to estimate 'local' gas fees on the origin chain
*/
async getLocalTransferFee({ originToken, destination, sender, senderPubKey, interchainFee, }) {
this.logger.debug(`Estimating local transfer gas to ${destination}`);
const originMetadata = this.multiProvider.getChainMetadata(originToken.chainName);
const destinationMetadata = this.multiProvider.getChainMetadata(destination);
// Check constant quotes first
const defaultQuote = this.localFeeConstants.find((q) => q.origin === originMetadata.name &&
q.destination === destinationMetadata.name);
if (defaultQuote) {
return { gasUnits: 0, gasPrice: 0, fee: Number(defaultQuote.amount) };
}
// Form transactions to estimate local gas with
const recipient = convertToProtocolAddress(sender, destinationMetadata.protocol, destinationMetadata.bech32Prefix);
const txs = await this.getTransferRemoteTxs({
originTokenAmount: originToken.amount(1),
destination,
sender,
recipient,
interchainFee,
});
// Starknet does not support gas estimation without starknet account
if (originToken.protocol === ProtocolType.Starknet) {
return { gasUnits: 0n, gasPrice: 0n, fee: 0n };
}
// Typically the transfers require a single transaction
if (txs.length === 1) {
try {
return this.multiProvider.estimateTransactionFee({
chainNameOrId: originMetadata.name,
transaction: txs[0],
sender,
senderPubKey,
});
}
catch (error) {
this.logger.error(`Failed to estimate local gas fee for ${originToken.symbol} transfer`, error);
throw new Error('Gas estimation failed, balance may be insufficient', {
cause: error,
});
}
}
// On ethereum, sometimes 2 txs are required (one approve, one transferRemote)
else if (txs.length === 2 &&
originToken.protocol === ProtocolType.Ethereum) {
const provider = this.multiProvider.getEthersV5Provider(originMetadata.name);
// We use a hard-coded const as an estimate for the transferRemote because we
// cannot reliably simulate the tx when an approval tx is required first
return estimateTransactionFeeEthersV5ForGasUnits({
provider,
gasUnits: EVM_TRANSFER_REMOTE_GAS_ESTIMATE,
});
}
else {
throw new Error('Cannot estimate local gas for multiple transactions');
}
}
/**
* Similar to getLocalTransferFee in that it estimates local gas fees
* but it also resolves the native token and returns a TokenAmount
* @todo: rename to getLocalTransferFee for consistency (requires breaking change)
*/
async getLocalTransferFeeAmount({ originToken, destination, sender, senderPubKey, interchainFee, }) {
const originMetadata = this.multiProvider.getChainMetadata(originToken.chainName);
// If there's no native token, we can't represent local gas
if (!originMetadata.nativeToken)
throw new Error(`No native token found for ${originMetadata.name}`);
this.logger.debug(`Using native token ${originMetadata.nativeToken.symbol} for local gas fee`);
const localFee = await this.getLocalTransferFee({
originToken,
destination,
sender,
senderPubKey,
interchainFee,
});
// Get the local gas token. This assumes the chain's native token will pay for local gas
// This will need to be smarter if more complex scenarios on Cosmos are supported
const localGasToken = Token.FromChainMetadataNativeToken(originMetadata);
return localGasToken.amount(localFee.fee);
}
/**
* Gets a list of populated transactions required to transfer a token to a remote chain
* Typically just 1 transaction but sometimes more, like when an approval is required first
*/
async getTransferRemoteTxs({ originTokenAmount, destination, sender, recipient, interchainFee, }) {
const transactions = [];
const { token, amount } = originTokenAmount;
const destinationName = this.multiProvider.getChainName(destination);
const destinationDomainId = this.multiProvider.getDomainId(destination);
const providerType = TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard];
const hypAdapter = token.getHypAdapter(this.multiProvider, destinationName);
const [isApproveRequired, isRevokeApprovalRequired] = await Promise.all([
this.isApproveRequired({
originTokenAmount,
owner: sender,
}),
hypAdapter.isRevokeApprovalRequired(sender, originTokenAmount.token.addressOrDenom),
]);
const preTransferRemoteTxs = [];
// if the approval is required and the current allowance is not 0 we reset
// the allowance before setting the right approval as some tokens don't allow
// to override an already existing allowance. USDT is one of these tokens
// see: https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L205
if (isApproveRequired && isRevokeApprovalRequired) {
preTransferRemoteTxs.push([0, WarpTxCategory.Revoke]);
}
if (isApproveRequired) {
preTransferRemoteTxs.push([amount.toString(), WarpTxCategory.Approval]);
}
for (const [approveAmount, txCategory] of preTransferRemoteTxs) {
this.logger.info(`${txCategory} required for transfer of ${token.symbol}`);
const approveTxReq = await hypAdapter.populateApproveTx({
weiAmountOrId: approveAmount,
recipient: token.addressOrDenom,
});
this.logger.debug(`${txCategory} tx for ${token.symbol} populated`);
const approveTx = {
category: txCategory,
type: providerType,
transaction: approveTxReq,
};
transactions.push(approveTx);
}
if (!interchainFee) {
interchainFee = await this.getInterchainTransferFee({
originToken: token,
destination,
sender,
});
}
// if the interchain fee is of protocol starknet we also have
// to approve the transfer of this fee token
if (interchainFee.token.protocol === ProtocolType.Starknet) {
const interchainFeeAdapter = interchainFee.token.getAdapter(this.multiProvider);
const isRequired = await interchainFeeAdapter.isApproveRequired(sender, token.addressOrDenom, interchainFee.amount);
this.logger.debug(`Approval is${isRequired ? '' : ' not'} required for interchain fee of ${interchainFee.token.symbol}`);
if (isRequired) {
const txCategory = WarpTxCategory.Approval;
this.logger.info(`${txCategory} required for transfer of ${interchainFee.token.symbol}`);
const approveTxReq = await interchainFeeAdapter.populateApproveTx({
weiAmountOrId: interchainFee.amount,
recipient: token.addressOrDenom,
});
this.logger.debug(`${txCategory} tx for ${interchainFee.token.symbol} populated`);
const approveTx = {
category: txCategory,
type: providerType,
transaction: approveTxReq,
};
transactions.push(approveTx);
}
}
const transferTxReq = await hypAdapter.populateTransferRemoteTx({
weiAmountOrId: amount.toString(),
destination: destinationDomainId,
fromAccountOwner: sender,
recipient,
interchainGas: {
amount: interchainFee.amount,
addressOrDenom: interchainFee.token.addressOrDenom,
},
customHook: token.igpTokenAddressOrDenom,
});
this.logger.debug(`Remote transfer tx for ${token.symbol} populated`);
const transferTx = {
category: WarpTxCategory.Transfer,
type: providerType,
transaction: transferTxReq,
};
transactions.push(transferTx);
return transactions;
}
/**
* Fetch local and interchain fee estimates for a remote transfer
*/
async estimateTransferRemoteFees({ originToken, destination, sender, senderPubKey, }) {
this.logger.debug('Fetching remote transfer fee estimates');
// First get interchain gas quote (aka IGP quote)
// Start with this because it's used in the local fee estimation
const interchainQuote = await this.getInterchainTransferFee({
originToken,
destination,
sender,
});
// Next, get the local gas quote
const localQuote = await this.getLocalTransferFeeAmount({
originToken,
destination,
sender,
senderPubKey,
interchainFee: interchainQuote,
});
return {
interchainQuote,
localQuote,
};
}
/**
* Computes the max transferrable amount of the from the given
* token balance, accounting for local and interchain gas fees
*/
async getMaxTransferAmount({ balance, destination, sender, senderPubKey, feeEstimate, }) {
const originToken = balance.token;
if (!feeEstimate) {
feeEstimate = await this.estimateTransferRemoteFees({
originToken,
destination,
sender,
senderPubKey,
});
}
const { localQuote, interchainQuote } = feeEstimate;
let maxAmount = balance;
if (originToken.isFungibleWith(localQuote.token)) {
maxAmount = maxAmount.minus(localQuote.amount);
}
if (originToken.isFungibleWith(interchainQuote.token)) {
maxAmount = maxAmount.minus(interchainQuote.amount);
}
if (maxAmount.amount > 0)
return maxAmount;
else
return originToken.amount(0);
}
async getTokenCollateral(token) {
if (LOCKBOX_STANDARDS.includes(token.standard)) {
const adapter = token.getAdapter(this.multiProvider);
const tokenCollateral = await adapter.getBridgedSupply();
return tokenCollateral;
}
else {
const adapter = token.getAdapter(this.multiProvider);
const tokenCollateral = await adapter.getBalance(token.addressOrDenom);
return tokenCollateral;
}
}
/**
* Checks if destination chain's collateral is sufficient to cover the transfer
*/
async isDestinationCollateralSufficient({ originTokenAmount, destination, }) {
const { token: originToken, amount } = originTokenAmount;
const destinationName = this.multiProvider.getChainName(destination);
this.logger.debug(`Checking collateral for ${originToken.symbol} to ${destination}`);
const destinationToken = originToken.getConnectionForChain(destinationName)?.token;
assert(destinationToken, `No connection found for ${destinationName}`);
if (!TOKEN_COLLATERALIZED_STANDARDS.includes(destinationToken.standard)) {
this.logger.debug(`${destinationToken.symbol} is not collateralized, skipping`);
return true;
}
const destinationBalance = await this.getTokenCollateral(destinationToken);
const destinationBalanceInOriginDecimals = convertDecimalsToIntegerString(destinationToken.decimals, originToken.decimals, destinationBalance.toString());
// check for scaling factor
if (originToken.scale &&
destinationToken.scale &&
originToken.scale !== destinationToken.scale) {
const precisionFactor = 100000;
const scaledAmount = convertToScaledAmount({
fromScale: originToken.scale,
toScale: destinationToken.scale,
amount,
precisionFactor,
});
return (BigInt(destinationBalanceInOriginDecimals) * BigInt(precisionFactor) >=
scaledAmount);
}
const isSufficient = BigInt(destinationBalanceInOriginDecimals) >= amount;
this.logger.debug(`${originTokenAmount.token.symbol} to ${destination} has ${isSufficient ? 'sufficient' : 'INSUFFICIENT'} collateral`);
return isSufficient;
}
/**
* Checks if a token transfer requires an approval tx first
*/
async isApproveRequired({ originTokenAmount, owner, }) {
const { token, amount } = originTokenAmount;
const adapter = token.getAdapter(this.multiProvider);
const isRequired = await adapter.isApproveRequired(owner, token.addressOrDenom, amount);
this.logger.debug(`Approval is${isRequired ? '' : ' not'} required for transfer of ${token.symbol}`);
return isRequired;
}
/**
* Ensure the remote token transfer would be valid for the given chains, amount, sender, and recipient
*/
async validateTransfer({ originTokenAmount, destination, recipient, sender, senderPubKey, }) {
const chainError = this.validateChains(originTokenAmount.token.chainName, destination);
if (chainError)
return chainError;
const recipientError = this.validateRecipient(recipient, destination);
if (recipientError)
return recipientError;
const amountError = await this.validateAmount(originTokenAmount, destination, recipient);
if (amountError)
return amountError;
const destinationRateLimitError = await this.validateDestinationRateLimit(originTokenAmount, destination);
if (destinationRateLimitError)
return destinationRateLimitError;
const destinationCollateralError = await this.validateDestinationCollateral(originTokenAmount, destination);
if (destinationCollateralError)
return destinationCollateralError;
const originCollateralError = await this.validateOriginCollateral(originTokenAmount);
if (originCollateralError)
return originCollateralError;
const balancesError = await this.validateTokenBalances(originTokenAmount, destination, sender, senderPubKey);
if (balancesError)
return balancesError;
return null;
}
/**
* Ensure the origin and destination chains are valid and known by this WarpCore
*/
validateChains(origin, destination) {
if (!origin)
return { origin: 'Origin chain required' };
if (!destination)
return { destination: 'Destination chain required' };
const originMetadata = this.multiProvider.tryGetChainMetadata(origin);
const destinationMetadata = this.multiProvider.tryGetChainMetadata(destination);
if (!originMetadata)
return { origin: 'Origin chain metadata missing' };
if (!destinationMetadata)
return { destination: 'Destination chain metadata missing' };
if (this.routeBlacklist.some((bl) => bl.origin === originMetadata.name &&
bl.destination === destinationMetadata.name)) {
return { destination: 'Route is not currently allowed' };
}
return null;
}
/**
* Ensure recipient address is valid for the destination chain
*/
validateRecipient(recipient, destination) {
const destinationMetadata = this.multiProvider.getChainMetadata(destination);
const { protocol, bech32Prefix } = destinationMetadata;
// Ensure recip address is valid for the destination chain's protocol
if (!isValidAddress(recipient, protocol) || isZeroishAddress(recipient))
return { recipient: 'Invalid recipient' };
// Also ensure the address denom is correct if the dest protocol is Cosmos
if (protocol === ProtocolType.Cosmos ||
protocol === ProtocolType.CosmosNative) {
if (!bech32Prefix) {
this.logger.error(`No bech32 prefix found for chain ${destination}`);
return { destination: 'Invalid chain data' };
}
else if (!recipient.startsWith(bech32Prefix)) {
this.logger.error(`Recipient prefix should be ${bech32Prefix}`);
return { recipient: 'Invalid recipient prefix' };
}
}
return null;
}
/**
* Ensure token amount is valid
*/
async validateAmount(originTokenAmount, destination, recipient) {
if (!originTokenAmount.amount || originTokenAmount.amount < 0n) {
const isNft = originTokenAmount.token.isNft();
return { amount: isNft ? 'Invalid Token Id' : 'Invalid amount' };
}
// Check the transfer amount is sufficient on the destination side
const originToken = originTokenAmount.token;
const destinationName = this.multiProvider.getChainName(destination);
const destinationToken = originToken.getConnectionForChain(destinationName)?.token;
assert(destinationToken, `No connection found for ${destinationName}`);
const destinationAdapter = destinationToken.getAdapter(this.multiProvider);
// Get the min required destination amount
const minDestinationTransferAmount = await destinationAdapter.getMinimumTransferAmount(recipient);
// Convert the minDestinationTransferAmount to an origin amount
const minOriginTransferAmount = destinationToken.amount(convertDecimalsToIntegerString(originToken.decimals, destinationToken.decimals, minDestinationTransferAmount.toString()));
if (minOriginTransferAmount.amount > originTokenAmount.amount) {
return {
amount: `Minimum transfer amount is ${minOriginTransferAmount.getDecimalFormattedAmount()} ${originToken.symbol}`,
};
}
return null;
}
/**
* Ensure the sender has sufficient balances for transfer and interchain gas
*/
async validateTokenBalances(originTokenAmount, destination, sender, senderPubKey) {
const { token: originToken, amount } = originTokenAmount;
const { amount: senderBalance } = await originToken.getBalance(this.multiProvider, sender);
const senderBalanceAmount = originTokenAmount.token.amount(senderBalance);
// Check 1: Check basic token balance
if (amount > senderBalance)
return { amount: 'Insufficient balance' };
// Check 2: Ensure the balance can cover interchain fee
// Slightly redundant with Check 4 but gives more specific error messages
const interchainQuote = await this.getInterchainTransferFee({
originToken,
destination,
sender,
});
// Get balance of the IGP fee token, which may be different from the transfer token
const interchainQuoteTokenBalance = originToken.isFungibleWith(interchainQuote.token)
? senderBalanceAmount
: await interchainQuote.token.getBalance(this.multiProvider, sender);
if (interchainQuoteTokenBalance.amount < interchainQuote.amount) {
return {
amount: `Insufficient ${interchainQuote.token.symbol} for interchain gas`,
};
}
// Check 3: Simulates the transfer by getting the local gas fee
const localQuote = await this.getLocalTransferFeeAmount({
originToken,
destination,
sender,
senderPubKey,
interchainFee: interchainQuote,
});
const feeEstimate = { interchainQuote, localQuote };
// Check 4: Ensure balances can cover the COMBINED amount and fees
const maxTransfer = await this.getMaxTransferAmount({
balance: senderBalanceAmount,
destination,
sender,
senderPubKey,
feeEstimate,
});
if (amount > maxTransfer.amount) {
return { amount: 'Insufficient balance for gas and transfer' };
}
return null;
}
/**
* Ensure the sender has sufficient balances for transfer and interchain gas
*/
async validateDestinationCollateral(originTokenAmount, destination) {
const valid = await this.isDestinationCollateralSufficient({
originTokenAmount,
destination,
});
if (!valid) {
return { amount: 'Insufficient collateral on destination' };
}
return null;
}
/**
* Ensure the sender has sufficient balances for minting
*/
async validateDestinationRateLimit(originTokenAmount, destination) {
const { token: originToken, amount } = originTokenAmount;
const destinationName = this.multiProvider.getChainName(destination);
const destinationToken = originToken.getConnectionForChain(destinationName)?.token;
assert(destinationToken, `No connection found for ${destinationName}`);
if (!MINT_LIMITED_STANDARDS.includes(destinationToken.standard)) {
this.logger.debug(`${destinationToken.symbol} does not have rate limit constraint, skipping`);
return null;
}
let destinationMintLimit = 0n;
if (destinationToken.standard === TokenStandard.EvmHypVSXERC20 ||
destinationToken.standard === TokenStandard.EvmHypVSXERC20Lockbox ||
destinationToken.standard === TokenStandard.EvmHypXERC20 ||
destinationToken.standard === TokenStandard.EvmHypXERC20Lockbox) {
const adapter = destinationToken.getAdapter(this.multiProvider);
destinationMintLimit = await adapter.getMintLimit();
if (destinationToken.standard === TokenStandard.EvmHypVSXERC20 ||
destinationToken.standard === TokenStandard.EvmHypVSXERC20Lockbox) {
const bufferCap = await adapter.getMintMaxLimit();
const max = bufferCap / 2n;
if (destinationMintLimit > max) {
this.logger.debug(`Mint limit ${destinationMintLimit} exceeds max ${max}, using max`);
destinationMintLimit = max;
}
}
}
else if (destinationToken.standard === TokenStandard.EvmHypCollateralFiat) {
const adapter = destinationToken.getAdapter(this.multiProvider);
destinationMintLimit = await adapter.getMintLimit();
}
const destinationMintLimitInOriginDecimals = convertDecimalsToIntegerString(destinationToken.decimals, originToken.decimals, destinationMintLimit.toString());
const isSufficient = BigInt(destinationMintLimitInOriginDecimals) >= amount;
this.logger.debug(`${originTokenAmount.token.symbol} to ${destination} has ${isSufficient ? 'sufficient' : 'INSUFFICIENT'} rate limits`);
if (!isSufficient)
return { amount: 'Rate limit exceeded on destination' };
return null;
}
/**
* Ensure the sender has sufficient balances for transfer and interchain gas
*/
async validateOriginCollateral(originTokenAmount) {
const adapter = originTokenAmount.token.getAdapter(this.multiProvider);
if (originTokenAmount.token.standard === TokenStandard.EvmHypXERC20 ||
originTokenAmount.token.standard === TokenStandard.EvmHypXERC20Lockbox) {
const burnLimit = await adapter.getBurnLimit();
if (burnLimit < BigInt(originTokenAmount.amount)) {
return { amount: 'Insufficient burn limit on origin' };
}
}
return null;
}
/**
* Search through token list to find token with matching chain and address
*/
findToken(chainName, addressOrDenom) {
if (!addressOrDenom)
return null;
const results = this.tokens.filter((token) => token.chainName === chainName &&
token.addressOrDenom.toLowerCase() === addressOrDenom.toLowerCase());
if (results.length === 1)
return results[0];
if (results.length > 1)
throw new Error(`Ambiguous token search results for ${addressOrDenom}`);
// If the token is not found, check to see if it matches the denom of chain's native token
// This is a convenience so WarpConfigs don't need to include definitions for native tokens
const chainMetadata = this.multiProvider.getChainMetadata(chainName);
if (chainMetadata.nativeToken?.denom === addressOrDenom) {
return Token.FromChainMetadataNativeToken(chainMetadata);
}
return null;
}
/**
* Get the list of chains referenced by the tokens in this WarpCore
*/
getTokenChains() {
return [...new Set(this.tokens.map((t) => t.chainName)).values()];
}
/**
* Get the subset of tokens whose chain matches the given chainName
*/
getTokensForChain(chainName) {
return this.tokens.filter((t) => t.chainName === chainName);
}
/**
* Get the subset of tokens whose chain matches the given chainName
* and which are connected to a token on the given destination chain
*/
getTokensForRoute(origin, destination) {
return this.tokens.filter((t) => t.chainName === origin && t.getConnectionForChain(destination));
}
}
//# sourceMappingURL=WarpCore.js.map