From ed65556aa4fdc0a4695c92c4cf6e739661602b35 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 22 Jul 2024 17:46:23 +0100 Subject: [PATCH] feat: Improve WarpCore validation errors (#4116) ### Description Improve WarpCore validation error message for gas fee checks ### Drive-by changes Enable ES2022 lib types in TS config ### Related issues Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3768 ### Backward compatibility Yes ### Testing Tested in Warp UI --- .changeset/kind-coins-heal.md | 5 ++ tsconfig.json | 2 +- typescript/sdk/src/warp/WarpCore.ts | 130 +++++++++++++++++++--------- 3 files changed, 96 insertions(+), 41 deletions(-) create mode 100644 .changeset/kind-coins-heal.md diff --git a/.changeset/kind-coins-heal.md b/.changeset/kind-coins-heal.md new file mode 100644 index 0000000000..e5539217b8 --- /dev/null +++ b/.changeset/kind-coins-heal.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Improve WarpCore validation error message for IGP fee checks diff --git a/tsconfig.json b/tsconfig.json index 4acc444314..61731967d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "incremental": false, "lib": [ "ES2015", "ES2016", "ES2017", "ES2018", - "ES2019", "ES2020","ES2021", "DOM" + "ES2019", "ES2020","ES2021", "ES2022", "DOM" ], "module": "nodenext", "moduleResolution": "nodenext", diff --git a/typescript/sdk/src/warp/WarpCore.ts b/typescript/sdk/src/warp/WarpCore.ts index f68201d73f..753045908c 100644 --- a/typescript/sdk/src/warp/WarpCore.ts +++ b/typescript/sdk/src/warp/WarpCore.ts @@ -188,6 +188,7 @@ export class WarpCore { senderPubKey?: HexString; interchainFee?: TokenAmount; }): Promise { + this.logger.debug(`Estimating local transfer gas to ${destination}`); const originMetadata = this.multiProvider.getChainMetadata( originToken.chainName, ); @@ -220,12 +221,22 @@ export class WarpCore { // Typically the transfers require a single transaction if (txs.length === 1) { - return this.multiProvider.estimateTransactionFee({ - chainNameOrId: originMetadata.name, - transaction: txs[0], - sender, - senderPubKey, - }); + 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 ( @@ -246,6 +257,49 @@ export class WarpCore { } } + /** + * 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, + }: { + originToken: IToken; + destination: ChainNameOrId; + sender: Address; + senderPubKey?: HexString; + interchainFee?: TokenAmount; + }): Promise { + 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 @@ -339,15 +393,8 @@ export class WarpCore { destination, }); - 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}`); - // Next, get the local gas quote - const localFee = await this.getLocalTransferFee({ + const localQuote = await this.getLocalTransferFeeAmount({ originToken, destination, sender, @@ -355,11 +402,6 @@ export class WarpCore { interchainFee: interchainQuote, }); - // 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); - const localQuote = localGasToken.amount(localFee.fee); - return { interchainQuote, localQuote, @@ -620,26 +662,47 @@ export class WarpCore { sender: Address, senderPubKey?: HexString, ): Promise | null> { - const { token, amount } = originTokenAmount; + const { token: originToken, amount } = originTokenAmount; - const { amount: senderBalance } = await token.getBalance( + const { amount: senderBalance } = await originToken.getBalance( this.multiProvider, sender, ); const senderBalanceAmount = originTokenAmount.token.amount(senderBalance); - // First check basic token balance + // Check 1: Check basic token balance if (amount > senderBalance) return { amount: 'Insufficient balance' }; - // Next, ensure balances can cover the COMBINED amount and fees - // The combined will be more than originTokenAmount if the transfer - // fee token == the either of the fee tokens - const feeEstimate = await this.estimateTransferRemoteFees({ - originToken: token, + // 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, + }); + // 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, @@ -651,19 +714,6 @@ export class WarpCore { return { amount: 'Insufficient balance for gas and transfer' }; } - // Finally, if the IGP fee token differs from the transfer token, - // ensure there's sufficient balance for the IGP fee - const igpQuote = feeEstimate.interchainQuote; - if (!token.isFungibleWith(igpQuote.token)) { - const igpTokenBalance = await igpQuote.token.getBalance( - this.multiProvider, - sender, - ); - if (igpTokenBalance.amount < igpQuote.amount) { - return { amount: `Insufficient ${igpQuote.token.symbol} for gas` }; - } - } - return null; }