Skip to content

Commit

Permalink
feat: Improve WarpCore validation errors (hyperlane-xyz#4116)
Browse files Browse the repository at this point in the history
### Description

Improve WarpCore validation error message for gas fee checks

### Drive-by changes

Enable ES2022 lib types in TS config

### Related issues

Fixes hyperlane-xyz#3768

### Backward compatibility

Yes

### Testing

Tested in Warp UI
  • Loading branch information
jmrossy authored Jul 22, 2024
1 parent 00333a5 commit ed65556
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-coins-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

Improve WarpCore validation error message for IGP fee checks
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"incremental": false,
"lib": [
"ES2015", "ES2016", "ES2017", "ES2018",
"ES2019", "ES2020","ES2021", "DOM"
"ES2019", "ES2020","ES2021", "ES2022", "DOM"
],
"module": "nodenext",
"moduleResolution": "nodenext",
Expand Down
130 changes: 90 additions & 40 deletions typescript/sdk/src/warp/WarpCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export class WarpCore {
senderPubKey?: HexString;
interchainFee?: TokenAmount;
}): Promise<TransactionFeeEstimate> {
this.logger.debug(`Estimating local transfer gas to ${destination}`);
const originMetadata = this.multiProvider.getChainMetadata(
originToken.chainName,
);
Expand Down Expand Up @@ -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 (
Expand All @@ -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<TokenAmount> {
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
Expand Down Expand Up @@ -339,27 +393,15 @@ 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,
senderPubKey,
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,
Expand Down Expand Up @@ -620,26 +662,47 @@ export class WarpCore {
sender: Address,
senderPubKey?: HexString,
): Promise<Record<string, string> | 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,
Expand All @@ -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;
}

Expand Down

0 comments on commit ed65556

Please sign in to comment.