@hyperlane-xyz/sdk 25.5.0 → 26.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/app/MultiProtocolApp.d.ts +5 -1
  2. package/dist/app/MultiProtocolApp.d.ts.map +1 -1
  3. package/dist/app/MultiProtocolApp.js +6 -0
  4. package/dist/app/MultiProtocolApp.js.map +1 -1
  5. package/dist/app/MultiProtocolApp.test.js +2 -2
  6. package/dist/app/MultiProtocolApp.test.js.map +1 -1
  7. package/dist/consts/igp.d.ts.map +1 -1
  8. package/dist/consts/igp.js +2 -0
  9. package/dist/consts/igp.js.map +1 -1
  10. package/dist/contracts/contracts.d.ts.map +1 -1
  11. package/dist/contracts/contracts.js +13 -6
  12. package/dist/contracts/contracts.js.map +1 -1
  13. package/dist/core/EvmCoreModule.d.ts +1 -0
  14. package/dist/core/EvmCoreModule.d.ts.map +1 -1
  15. package/dist/core/EvmCoreModule.js +2 -1
  16. package/dist/core/EvmCoreModule.js.map +1 -1
  17. package/dist/core/MultiProtocolCore.d.ts.map +1 -1
  18. package/dist/core/MultiProtocolCore.js +2 -2
  19. package/dist/core/MultiProtocolCore.js.map +1 -1
  20. package/dist/core/adapters/EvmCoreAdapter.d.ts.map +1 -1
  21. package/dist/core/adapters/EvmCoreAdapter.js +2 -1
  22. package/dist/core/adapters/EvmCoreAdapter.js.map +1 -1
  23. package/dist/deploy/HyperlaneDeployer.d.ts.map +1 -1
  24. package/dist/deploy/HyperlaneDeployer.js +3 -4
  25. package/dist/deploy/HyperlaneDeployer.js.map +1 -1
  26. package/dist/deploy/warp.d.ts.map +1 -1
  27. package/dist/deploy/warp.js +4 -0
  28. package/dist/deploy/warp.js.map +1 -1
  29. package/dist/fee/EvmTokenFeeModule.d.ts +1 -0
  30. package/dist/fee/EvmTokenFeeModule.d.ts.map +1 -1
  31. package/dist/fee/EvmTokenFeeModule.js +2 -1
  32. package/dist/fee/EvmTokenFeeModule.js.map +1 -1
  33. package/dist/gas/utils.d.ts.map +1 -1
  34. package/dist/gas/utils.js +1 -0
  35. package/dist/gas/utils.js.map +1 -1
  36. package/dist/hook/EvmHookModule.d.ts +1 -0
  37. package/dist/hook/EvmHookModule.d.ts.map +1 -1
  38. package/dist/hook/EvmHookModule.js +2 -1
  39. package/dist/hook/EvmHookModule.js.map +1 -1
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/ism/EvmIsmModule.d.ts +1 -0
  45. package/dist/ism/EvmIsmModule.d.ts.map +1 -1
  46. package/dist/ism/EvmIsmModule.js +2 -1
  47. package/dist/ism/EvmIsmModule.js.map +1 -1
  48. package/dist/metadata/ChainMetadataManager.d.ts.map +1 -1
  49. package/dist/metadata/ChainMetadataManager.js +3 -3
  50. package/dist/metadata/ChainMetadataManager.js.map +1 -1
  51. package/dist/metadata/agentConfig.d.ts.map +1 -1
  52. package/dist/metadata/agentConfig.js +1 -0
  53. package/dist/metadata/agentConfig.js.map +1 -1
  54. package/dist/metadata/blockExplorer.js +2 -2
  55. package/dist/metadata/blockExplorer.js.map +1 -1
  56. package/dist/metadata/chainMetadataTypes.d.ts +0 -1
  57. package/dist/metadata/chainMetadataTypes.d.ts.map +1 -1
  58. package/dist/metadata/chainMetadataTypes.js +0 -1
  59. package/dist/metadata/chainMetadataTypes.js.map +1 -1
  60. package/dist/metadata/warpRouteConfig.d.ts +10 -8
  61. package/dist/metadata/warpRouteConfig.d.ts.map +1 -1
  62. package/dist/providers/MultiProtocolProvider.d.ts +2 -1
  63. package/dist/providers/MultiProtocolProvider.d.ts.map +1 -1
  64. package/dist/providers/MultiProtocolProvider.js +3 -0
  65. package/dist/providers/MultiProtocolProvider.js.map +1 -1
  66. package/dist/providers/MultiProvider.d.ts.map +1 -1
  67. package/dist/providers/MultiProvider.js +5 -5
  68. package/dist/providers/MultiProvider.js.map +1 -1
  69. package/dist/providers/ProviderType.d.ts +25 -5
  70. package/dist/providers/ProviderType.d.ts.map +1 -1
  71. package/dist/providers/ProviderType.js +2 -0
  72. package/dist/providers/ProviderType.js.map +1 -1
  73. package/dist/providers/explorerHealthTest.d.ts.map +1 -1
  74. package/dist/providers/explorerHealthTest.js +1 -0
  75. package/dist/providers/explorerHealthTest.js.map +1 -1
  76. package/dist/providers/providerBuilders.d.ts +2 -1
  77. package/dist/providers/providerBuilders.d.ts.map +1 -1
  78. package/dist/providers/providerBuilders.js +9 -0
  79. package/dist/providers/providerBuilders.js.map +1 -1
  80. package/dist/providers/rpcHealthTest.d.ts.map +1 -1
  81. package/dist/providers/rpcHealthTest.js +2 -0
  82. package/dist/providers/rpcHealthTest.js.map +1 -1
  83. package/dist/providers/transactionFeeEstimators.d.ts +4 -3
  84. package/dist/providers/transactionFeeEstimators.d.ts.map +1 -1
  85. package/dist/providers/transactionFeeEstimators.js +19 -6
  86. package/dist/providers/transactionFeeEstimators.js.map +1 -1
  87. package/dist/providers/transactions/submitter/submitterBuilderGetter.d.ts.map +1 -1
  88. package/dist/providers/transactions/submitter/submitterBuilderGetter.js +1 -0
  89. package/dist/providers/transactions/submitter/submitterBuilderGetter.js.map +1 -1
  90. package/dist/router/MultiProtocolRouterApps.d.ts.map +1 -1
  91. package/dist/router/MultiProtocolRouterApps.js +3 -3
  92. package/dist/router/MultiProtocolRouterApps.js.map +1 -1
  93. package/dist/signers/cosmos/cosmjs.d.ts +2 -1
  94. package/dist/signers/cosmos/cosmjs.d.ts.map +1 -1
  95. package/dist/signers/cosmos/cosmjs.js +1 -1
  96. package/dist/signers/cosmos/cosmjs.js.map +1 -1
  97. package/dist/signers/evm/ethersv5.d.ts +2 -1
  98. package/dist/signers/evm/ethersv5.d.ts.map +1 -1
  99. package/dist/signers/evm/ethersv5.js +5 -5
  100. package/dist/signers/evm/ethersv5.js.map +1 -1
  101. package/dist/signers/radix/radix-toolkit.d.ts +2 -1
  102. package/dist/signers/radix/radix-toolkit.d.ts.map +1 -1
  103. package/dist/signers/radix/radix-toolkit.js +1 -1
  104. package/dist/signers/radix/radix-toolkit.js.map +1 -1
  105. package/dist/signers/signers.d.ts.map +1 -1
  106. package/dist/signers/signers.js +1 -0
  107. package/dist/signers/signers.js.map +1 -1
  108. package/dist/signers/starknet/starknetjs.d.ts +2 -1
  109. package/dist/signers/starknet/starknetjs.d.ts.map +1 -1
  110. package/dist/signers/starknet/starknetjs.js +1 -1
  111. package/dist/signers/starknet/starknetjs.js.map +1 -1
  112. package/dist/signers/svm/solana-web3js.d.ts +2 -1
  113. package/dist/signers/svm/solana-web3js.d.ts.map +1 -1
  114. package/dist/signers/svm/solana-web3js.js +1 -1
  115. package/dist/signers/svm/solana-web3js.js.map +1 -1
  116. package/dist/signers/types.d.ts +2 -1
  117. package/dist/signers/types.d.ts.map +1 -1
  118. package/dist/token/EvmWarpModule.d.ts +9 -0
  119. package/dist/token/EvmWarpModule.d.ts.map +1 -1
  120. package/dist/token/EvmWarpModule.hardhat-test.js +56 -1
  121. package/dist/token/EvmWarpModule.hardhat-test.js.map +1 -1
  122. package/dist/token/EvmWarpModule.js +92 -3
  123. package/dist/token/EvmWarpModule.js.map +1 -1
  124. package/dist/token/EvmWarpRouteReader.d.ts +4 -0
  125. package/dist/token/EvmWarpRouteReader.d.ts.map +1 -1
  126. package/dist/token/EvmWarpRouteReader.hardhat-test.js +56 -0
  127. package/dist/token/EvmWarpRouteReader.hardhat-test.js.map +1 -1
  128. package/dist/token/EvmWarpRouteReader.js +41 -0
  129. package/dist/token/EvmWarpRouteReader.js.map +1 -1
  130. package/dist/token/IToken.d.ts +1 -0
  131. package/dist/token/IToken.d.ts.map +1 -1
  132. package/dist/token/Token.d.ts +1 -0
  133. package/dist/token/Token.d.ts.map +1 -1
  134. package/dist/token/Token.js +36 -13
  135. package/dist/token/Token.js.map +1 -1
  136. package/dist/token/Token.test.js +19 -0
  137. package/dist/token/Token.test.js.map +1 -1
  138. package/dist/token/TokenStandard.d.ts +21 -1
  139. package/dist/token/TokenStandard.d.ts.map +1 -1
  140. package/dist/token/TokenStandard.js +98 -0
  141. package/dist/token/TokenStandard.js.map +1 -1
  142. package/dist/token/adapters/EvmMultiCollateralAdapter.d.ts +45 -0
  143. package/dist/token/adapters/EvmMultiCollateralAdapter.d.ts.map +1 -0
  144. package/dist/token/adapters/EvmMultiCollateralAdapter.js +66 -0
  145. package/dist/token/adapters/EvmMultiCollateralAdapter.js.map +1 -0
  146. package/dist/token/adapters/EvmMultiCollateralAdapter.test.d.ts +2 -0
  147. package/dist/token/adapters/EvmMultiCollateralAdapter.test.d.ts.map +1 -0
  148. package/dist/token/adapters/EvmMultiCollateralAdapter.test.js +147 -0
  149. package/dist/token/adapters/EvmMultiCollateralAdapter.test.js.map +1 -0
  150. package/dist/token/adapters/EvmTokenAdapter.d.ts.map +1 -1
  151. package/dist/token/adapters/EvmTokenAdapter.js +24 -22
  152. package/dist/token/adapters/EvmTokenAdapter.js.map +1 -1
  153. package/dist/token/config.d.ts +2 -0
  154. package/dist/token/config.d.ts.map +1 -1
  155. package/dist/token/config.js +3 -0
  156. package/dist/token/config.js.map +1 -1
  157. package/dist/token/configUtils.d.ts.map +1 -1
  158. package/dist/token/configUtils.js +5 -3
  159. package/dist/token/configUtils.js.map +1 -1
  160. package/dist/token/contracts.d.ts +3 -0
  161. package/dist/token/contracts.d.ts.map +1 -1
  162. package/dist/token/contracts.js +3 -0
  163. package/dist/token/contracts.js.map +1 -1
  164. package/dist/token/deploy.d.ts +1 -0
  165. package/dist/token/deploy.d.ts.map +1 -1
  166. package/dist/token/deploy.js +36 -5
  167. package/dist/token/deploy.js.map +1 -1
  168. package/dist/token/nativeTokenMetadata.d.ts.map +1 -1
  169. package/dist/token/nativeTokenMetadata.js +6 -0
  170. package/dist/token/nativeTokenMetadata.js.map +1 -1
  171. package/dist/token/tokenMetadataUtils.d.ts.map +1 -1
  172. package/dist/token/tokenMetadataUtils.js +4 -3
  173. package/dist/token/tokenMetadataUtils.js.map +1 -1
  174. package/dist/token/types.d.ts +1268 -94
  175. package/dist/token/types.d.ts.map +1 -1
  176. package/dist/token/types.js +18 -1
  177. package/dist/token/types.js.map +1 -1
  178. package/dist/warp/WarpCore.d.ts +53 -12
  179. package/dist/warp/WarpCore.d.ts.map +1 -1
  180. package/dist/warp/WarpCore.js +262 -57
  181. package/dist/warp/WarpCore.js.map +1 -1
  182. package/dist/warp/WarpCore.test.js +422 -0
  183. package/dist/warp/WarpCore.test.js.map +1 -1
  184. package/package.json +12 -11
@@ -1,4 +1,4 @@
1
- import { ProtocolType, assert, convertDecimalsToIntegerString, convertToProtocolAddress, convertToScaledAmount, isValidAddress, isZeroishAddress, rootLogger, } from '@hyperlane-xyz/utils';
1
+ import { ProtocolType, assert, convertDecimalsToIntegerString, convertToProtocolAddress, convertToScaledAmount, isEVMLike, isValidAddress, isZeroishAddress, rootLogger, } from '@hyperlane-xyz/utils';
2
2
  import { estimateTransactionFeeEthersV5ForGasUnits, } from '../providers/transactionFeeEstimators.js';
3
3
  import { Token } from '../token/Token.js';
4
4
  import { TokenAmount } from '../token/TokenAmount.js';
@@ -60,7 +60,7 @@ export class WarpCore {
60
60
  * and for token fee quote if it exists.
61
61
  * Sender is only required for Sealevel origins.
62
62
  */
63
- async getInterchainTransferFee({ originTokenAmount, destination, sender, recipient, }) {
63
+ async getInterchainTransferFee({ originTokenAmount, destination, sender, recipient, destinationToken, }) {
64
64
  this.logger.debug(`Fetching interchain transfer quote to ${destination}`);
65
65
  const { amount, token: originToken } = originTokenAmount;
66
66
  const originName = originToken.chainName;
@@ -77,15 +77,33 @@ export class WarpCore {
77
77
  }
78
78
  else {
79
79
  // Otherwise, compute IGP quote via the adapter
80
- const hypAdapter = originToken.getHypAdapter(this.multiProvider, destinationName);
80
+ let quote;
81
81
  const destinationDomainId = this.multiProvider.getDomainId(destination);
82
- const quote = await hypAdapter.quoteTransferRemoteGas({
83
- destination: destinationDomainId,
84
- sender,
85
- customHook: originToken.igpTokenAddressOrDenom,
86
- recipient,
87
- amount,
88
- });
82
+ if (this.isMultiCollateralTransfer(originToken, destinationToken)) {
83
+ const resolvedDestinationToken = this.resolveDestinationToken({
84
+ originToken,
85
+ destination,
86
+ destinationToken,
87
+ });
88
+ assert(resolvedDestinationToken.addressOrDenom, 'Destination token missing addressOrDenom');
89
+ const multiCollateralAdapter = originToken.getHypAdapter(this.multiProvider, destinationName);
90
+ quote = await multiCollateralAdapter.quoteTransferRemoteToGas({
91
+ destination: destinationDomainId,
92
+ recipient,
93
+ amount,
94
+ targetRouter: resolvedDestinationToken.addressOrDenom,
95
+ });
96
+ }
97
+ else {
98
+ const hypAdapter = originToken.getHypAdapter(this.multiProvider, destinationName);
99
+ quote = await hypAdapter.quoteTransferRemoteGas({
100
+ destination: destinationDomainId,
101
+ sender,
102
+ customHook: originToken.igpTokenAddressOrDenom,
103
+ recipient,
104
+ amount,
105
+ });
106
+ }
89
107
  gasAmount = BigInt(quote.igpQuote.amount);
90
108
  gasAddressOrDenom = quote.igpQuote.addressOrDenom;
91
109
  feeAmount = quote.tokenFeeQuote?.amount;
@@ -122,7 +140,7 @@ export class WarpCore {
122
140
  /**
123
141
  * Simulates a transfer to estimate 'local' gas fees on the origin chain
124
142
  */
125
- async getLocalTransferFee({ originToken, destination, sender, senderPubKey, interchainFee, tokenFeeQuote, }) {
143
+ async getLocalTransferFee({ originToken, destination, sender, senderPubKey, interchainFee, tokenFeeQuote, destinationToken, }) {
126
144
  this.logger.debug(`Estimating local transfer gas to ${destination}`);
127
145
  const originMetadata = this.multiProvider.getChainMetadata(originToken.chainName);
128
146
  const destinationMetadata = this.multiProvider.getChainMetadata(destination);
@@ -150,6 +168,7 @@ export class WarpCore {
150
168
  recipient,
151
169
  interchainFee,
152
170
  tokenFeeQuote,
171
+ destinationToken,
153
172
  });
154
173
  // Starknet does not support gas estimation without starknet account
155
174
  if (originToken.protocol === ProtocolType.Starknet) {
@@ -173,8 +192,7 @@ export class WarpCore {
173
192
  }
174
193
  }
175
194
  // On ethereum, sometimes 2 txs are required (one approve, one transferRemote)
176
- else if (txs.length === 2 &&
177
- originToken.protocol === ProtocolType.Ethereum) {
195
+ else if (txs.length === 2 && isEVMLike(originToken.protocol)) {
178
196
  const provider = this.multiProvider.getEthersV5Provider(originMetadata.name);
179
197
  // We use a hard-coded const as an estimate for the transferRemote because we
180
198
  // cannot reliably simulate the tx when an approval tx is required first
@@ -192,7 +210,7 @@ export class WarpCore {
192
210
  * but it also resolves the native token and returns a TokenAmount
193
211
  * @todo: rename to getLocalTransferFee for consistency (requires breaking change)
194
212
  */
195
- async getLocalTransferFeeAmount({ originToken, destination, sender, senderPubKey, interchainFee, tokenFeeQuote, }) {
213
+ async getLocalTransferFeeAmount({ originToken, destination, sender, senderPubKey, interchainFee, tokenFeeQuote, destinationToken, }) {
196
214
  const originMetadata = this.multiProvider.getChainMetadata(originToken.chainName);
197
215
  // If there's no native token, we can't represent local gas
198
216
  if (!originMetadata.nativeToken)
@@ -205,6 +223,7 @@ export class WarpCore {
205
223
  senderPubKey,
206
224
  interchainFee,
207
225
  tokenFeeQuote,
226
+ destinationToken,
208
227
  });
209
228
  // Get the local gas token. This assumes the chain's native token will pay for local gas
210
229
  // This will need to be smarter if more complex scenarios on Cosmos are supported
@@ -215,7 +234,19 @@ export class WarpCore {
215
234
  * Gets a list of populated transactions required to transfer a token to a remote chain
216
235
  * Typically just 1 transaction but sometimes more, like when an approval is required first
217
236
  */
218
- async getTransferRemoteTxs({ originTokenAmount, destination, sender, recipient, interchainFee, tokenFeeQuote, }) {
237
+ async getTransferRemoteTxs({ originTokenAmount, destination, sender, recipient, interchainFee, tokenFeeQuote, destinationToken, }) {
238
+ // Check if this is a MultiCollateral transfer
239
+ if (destinationToken &&
240
+ this.isMultiCollateralTransfer(originTokenAmount.token, destinationToken)) {
241
+ return this.getMultiCollateralTransferTxs({
242
+ originTokenAmount,
243
+ destination,
244
+ sender,
245
+ recipient,
246
+ destinationToken,
247
+ });
248
+ }
249
+ // Standard warp route transfer
219
250
  const transactions = [];
220
251
  const { token, amount } = originTokenAmount;
221
252
  const destinationName = this.multiProvider.getChainName(destination);
@@ -228,6 +259,7 @@ export class WarpCore {
228
259
  destination,
229
260
  sender,
230
261
  recipient,
262
+ destinationToken,
231
263
  });
232
264
  interchainFee = transferFee.igpQuote;
233
265
  tokenFeeQuote = transferFee.tokenFeeQuote;
@@ -320,11 +352,104 @@ export class WarpCore {
320
352
  transactions.push(transferTx);
321
353
  return transactions;
322
354
  }
355
+ /**
356
+ * Check if this is a MultiCollateral transfer.
357
+ * Returns true if both tokens are MultiCollateral tokens.
358
+ */
359
+ isMultiCollateralTransfer(originToken, destinationToken) {
360
+ if (!destinationToken)
361
+ return false;
362
+ return (originToken.isMultiCollateralToken() &&
363
+ destinationToken.isMultiCollateralToken());
364
+ }
365
+ /**
366
+ * Executes a MultiCollateral transfer between different collateral routers.
367
+ * Uses transferRemoteTo for both same-chain and cross-chain transfers.
368
+ * Same-chain: calls handle() directly on target router (atomic, no relay needed).
369
+ */
370
+ async getMultiCollateralTransferTxs({ originTokenAmount, destination, sender, recipient, destinationToken, }) {
371
+ const transactions = [];
372
+ const { token: originToken, amount } = originTokenAmount;
373
+ const destinationName = this.multiProvider.getChainName(destination);
374
+ const resolvedDestinationToken = this.resolveDestinationToken({
375
+ originToken,
376
+ destination,
377
+ destinationToken,
378
+ });
379
+ assert(originToken.collateralAddressOrDenom, 'Origin token missing collateralAddressOrDenom');
380
+ assert(resolvedDestinationToken.addressOrDenom, 'Destination token missing addressOrDenom');
381
+ const providerType = TOKEN_STANDARD_TO_PROVIDER_TYPE[originToken.standard];
382
+ const adapter = originToken.getHypAdapter(this.multiProvider, destinationName);
383
+ const transferQuote = await adapter.quoteTransferRemoteToGas({
384
+ destination: this.multiProvider.getDomainId(destination),
385
+ recipient,
386
+ amount,
387
+ targetRouter: resolvedDestinationToken.addressOrDenom,
388
+ });
389
+ assert(!transferQuote.igpQuote.addressOrDenom ||
390
+ isZeroishAddress(transferQuote.igpQuote.addressOrDenom), `MultiCollateral transferRemoteTo requires native IGP fee; got ${transferQuote.igpQuote.addressOrDenom}`);
391
+ const tokenFeeAmount = transferQuote.tokenFeeQuote?.amount ?? 0n;
392
+ const totalDebit = amount + tokenFeeAmount;
393
+ const [isApproveRequired, isRevokeApprovalRequired] = await Promise.all([
394
+ adapter.isApproveRequired(sender, originToken.addressOrDenom, totalDebit),
395
+ adapter.isRevokeApprovalRequired(sender, originToken.addressOrDenom),
396
+ ]);
397
+ if (isApproveRequired && isRevokeApprovalRequired) {
398
+ const revokeTxReq = await adapter.populateApproveTx({
399
+ weiAmountOrId: 0,
400
+ recipient: originToken.addressOrDenom,
401
+ });
402
+ transactions.push({
403
+ category: WarpTxCategory.Revoke,
404
+ type: providerType,
405
+ transaction: revokeTxReq,
406
+ });
407
+ }
408
+ if (isApproveRequired) {
409
+ const approveTxReq = await adapter.populateApproveTx({
410
+ weiAmountOrId: totalDebit,
411
+ recipient: originToken.addressOrDenom,
412
+ });
413
+ transactions.push({
414
+ category: WarpTxCategory.Approval,
415
+ type: providerType,
416
+ transaction: approveTxReq,
417
+ });
418
+ }
419
+ // transferRemoteTo works for both same-chain and cross-chain.
420
+ // Same-chain: calls handle() directly on target router (atomic, no relay needed).
421
+ const destinationDomainId = this.multiProvider.getDomainId(destination);
422
+ const txReq = await adapter.populateTransferRemoteToTx({
423
+ destination: destinationDomainId,
424
+ recipient,
425
+ amount,
426
+ targetRouter: resolvedDestinationToken.addressOrDenom,
427
+ interchainGas: transferQuote,
428
+ });
429
+ transactions.push({
430
+ category: WarpTxCategory.Transfer,
431
+ type: providerType,
432
+ transaction: txReq,
433
+ });
434
+ return transactions;
435
+ }
323
436
  /**
324
437
  * Fetch local and interchain fee estimates for a remote transfer
325
438
  */
326
- async estimateTransferRemoteFees({ originTokenAmount, destination, recipient, sender, senderPubKey, }) {
439
+ async estimateTransferRemoteFees({ originTokenAmount, destination, recipient, sender, senderPubKey, destinationToken, }) {
327
440
  this.logger.debug('Fetching remote transfer fee estimates');
441
+ const { token: originToken } = originTokenAmount;
442
+ // Handle MultiCollateral fee estimation
443
+ if (this.isMultiCollateralTransfer(originToken, destinationToken)) {
444
+ return this.estimateMultiCollateralFees({
445
+ originTokenAmount,
446
+ destination,
447
+ destinationToken,
448
+ recipient,
449
+ sender,
450
+ senderPubKey,
451
+ });
452
+ }
328
453
  // First get interchain gas quote (aka IGP quote)
329
454
  // Start with this because it's used in the local fee estimation
330
455
  const { igpQuote, tokenFeeQuote } = await this.getInterchainTransferFee({
@@ -348,11 +473,43 @@ export class WarpCore {
348
473
  tokenFeeQuote,
349
474
  };
350
475
  }
476
+ /**
477
+ * Estimate fees for a MultiCollateral transfer.
478
+ */
479
+ async estimateMultiCollateralFees({ originTokenAmount, destination, destinationToken, recipient, sender, senderPubKey, }) {
480
+ const { token: originToken } = originTokenAmount;
481
+ const resolvedDestinationToken = this.resolveDestinationToken({
482
+ originToken,
483
+ destination,
484
+ destinationToken,
485
+ });
486
+ const { igpQuote: interchainQuote, tokenFeeQuote } = await this.getInterchainTransferFee({
487
+ originTokenAmount,
488
+ destination,
489
+ sender,
490
+ recipient,
491
+ destinationToken: resolvedDestinationToken,
492
+ });
493
+ const localQuote = await this.getLocalTransferFeeAmount({
494
+ originToken,
495
+ destination,
496
+ sender,
497
+ senderPubKey,
498
+ interchainFee: interchainQuote,
499
+ tokenFeeQuote,
500
+ destinationToken,
501
+ });
502
+ return {
503
+ interchainQuote,
504
+ localQuote,
505
+ tokenFeeQuote,
506
+ };
507
+ }
351
508
  /**
352
509
  * Computes the max transferrable amount of the from the given
353
510
  * token balance, accounting for local and interchain gas fees
354
511
  */
355
- async getMaxTransferAmount({ balance, destination, recipient, sender, senderPubKey, feeEstimate, }) {
512
+ async getMaxTransferAmount({ balance, destination, recipient, sender, senderPubKey, feeEstimate, destinationToken, }) {
356
513
  const originToken = balance.token;
357
514
  if (!feeEstimate) {
358
515
  feeEstimate = await this.estimateTransferRemoteFees({
@@ -361,6 +518,7 @@ export class WarpCore {
361
518
  recipient,
362
519
  sender,
363
520
  senderPubKey,
521
+ destinationToken,
364
522
  });
365
523
  }
366
524
  const { localQuote, interchainQuote, tokenFeeQuote } = feeEstimate;
@@ -377,6 +535,7 @@ export class WarpCore {
377
535
  destination,
378
536
  recipient,
379
537
  sender,
538
+ destinationToken,
380
539
  });
381
540
  // Because tokenFeeQuote is calculated based on the amount, we need to recalculate
382
541
  // the tokenFeeQuote after subtracting the localQuote and IGP to get max transfer amount
@@ -403,26 +562,28 @@ export class WarpCore {
403
562
  /**
404
563
  * Checks if destination chain's collateral is sufficient to cover the transfer
405
564
  */
406
- async isDestinationCollateralSufficient({ originTokenAmount, destination, }) {
565
+ async isDestinationCollateralSufficient({ originTokenAmount, destination, destinationToken, }) {
407
566
  const { token: originToken, amount } = originTokenAmount;
408
- const destinationName = this.multiProvider.getChainName(destination);
409
567
  this.logger.debug(`Checking collateral for ${originToken.symbol} to ${destination}`);
410
- const destinationToken = originToken.getConnectionForChain(destinationName)?.token;
411
- assert(destinationToken, `No connection found for ${destinationName}`);
412
- if (!TOKEN_COLLATERALIZED_STANDARDS.includes(destinationToken.standard)) {
413
- this.logger.debug(`${destinationToken.symbol} is not collateralized, skipping`);
568
+ const resolvedDestinationToken = this.resolveDestinationToken({
569
+ originToken,
570
+ destination,
571
+ destinationToken,
572
+ });
573
+ if (!TOKEN_COLLATERALIZED_STANDARDS.includes(resolvedDestinationToken.standard)) {
574
+ this.logger.debug(`${resolvedDestinationToken.symbol} is not collateralized, skipping`);
414
575
  return true;
415
576
  }
416
- const destinationBalance = await this.getTokenCollateral(destinationToken);
417
- const destinationBalanceInOriginDecimals = convertDecimalsToIntegerString(destinationToken.decimals, originToken.decimals, destinationBalance.toString());
577
+ const destinationBalance = await this.getTokenCollateral(resolvedDestinationToken);
578
+ const destinationBalanceInOriginDecimals = convertDecimalsToIntegerString(resolvedDestinationToken.decimals, originToken.decimals, destinationBalance.toString());
418
579
  // check for scaling factor
419
580
  if (originToken.scale &&
420
- destinationToken.scale &&
421
- originToken.scale !== destinationToken.scale) {
581
+ resolvedDestinationToken.scale &&
582
+ originToken.scale !== resolvedDestinationToken.scale) {
422
583
  const precisionFactor = 100_000;
423
584
  const scaledAmount = convertToScaledAmount({
424
585
  fromScale: originToken.scale,
425
- toScale: destinationToken.scale,
586
+ toScale: resolvedDestinationToken.scale,
426
587
  amount,
427
588
  precisionFactor,
428
589
  });
@@ -446,26 +607,42 @@ export class WarpCore {
446
607
  /**
447
608
  * Ensure the remote token transfer would be valid for the given chains, amount, sender, and recipient
448
609
  */
449
- async validateTransfer({ originTokenAmount, destination, recipient, sender, senderPubKey, }) {
610
+ async validateTransfer({ originTokenAmount, destination, recipient, sender, senderPubKey, destinationToken, }) {
450
611
  const chainError = this.validateChains(originTokenAmount.token.chainName, destination);
451
612
  if (chainError)
452
613
  return chainError;
453
614
  const recipientError = this.validateRecipient(recipient, destination);
454
615
  if (recipientError)
455
616
  return recipientError;
456
- const amountError = await this.validateAmount(originTokenAmount, destination, recipient);
617
+ const resolvedDestinationToken = (() => {
618
+ try {
619
+ return this.resolveDestinationToken({
620
+ originToken: originTokenAmount.token,
621
+ destination,
622
+ destinationToken,
623
+ });
624
+ }
625
+ catch (error) {
626
+ const message = error instanceof Error ? error.message : 'Invalid destination token';
627
+ return { error: message };
628
+ }
629
+ })();
630
+ if ('error' in resolvedDestinationToken) {
631
+ return { destinationToken: resolvedDestinationToken.error };
632
+ }
633
+ const amountError = await this.validateAmount(originTokenAmount, destination, recipient, resolvedDestinationToken);
457
634
  if (amountError)
458
635
  return amountError;
459
- const destinationRateLimitError = await this.validateDestinationRateLimit(originTokenAmount, destination);
636
+ const destinationRateLimitError = await this.validateDestinationRateLimit(originTokenAmount, destination, resolvedDestinationToken);
460
637
  if (destinationRateLimitError)
461
638
  return destinationRateLimitError;
462
- const destinationCollateralError = await this.validateDestinationCollateral(originTokenAmount, destination);
639
+ const destinationCollateralError = await this.validateDestinationCollateral(originTokenAmount, destination, resolvedDestinationToken);
463
640
  if (destinationCollateralError)
464
641
  return destinationCollateralError;
465
642
  const originCollateralError = await this.validateOriginCollateral(originTokenAmount);
466
643
  if (originCollateralError)
467
644
  return originCollateralError;
468
- const balancesError = await this.validateTokenBalances(originTokenAmount, destination, sender, recipient, senderPubKey);
645
+ const balancesError = await this.validateTokenBalances(originTokenAmount, destination, sender, recipient, senderPubKey, resolvedDestinationToken);
469
646
  if (balancesError)
470
647
  return balancesError;
471
648
  return null;
@@ -516,21 +693,23 @@ export class WarpCore {
516
693
  /**
517
694
  * Ensure token amount is valid
518
695
  */
519
- async validateAmount(originTokenAmount, destination, recipient) {
696
+ async validateAmount(originTokenAmount, destination, recipient, destinationToken) {
520
697
  if (!originTokenAmount.amount || originTokenAmount.amount < 0n) {
521
698
  const isNft = originTokenAmount.token.isNft();
522
699
  return { amount: isNft ? 'Invalid Token Id' : 'Invalid amount' };
523
700
  }
524
701
  // Check the transfer amount is sufficient on the destination side
525
702
  const originToken = originTokenAmount.token;
526
- const destinationName = this.multiProvider.getChainName(destination);
527
- const destinationToken = originToken.getConnectionForChain(destinationName)?.token;
528
- assert(destinationToken, `No connection found for ${destinationName}`);
529
- const destinationAdapter = destinationToken.getAdapter(this.multiProvider);
703
+ const resolvedDestinationToken = this.resolveDestinationToken({
704
+ originToken,
705
+ destination,
706
+ destinationToken,
707
+ });
708
+ const destinationAdapter = resolvedDestinationToken.getAdapter(this.multiProvider);
530
709
  // Get the min required destination amount
531
710
  const minDestinationTransferAmount = await destinationAdapter.getMinimumTransferAmount(recipient);
532
711
  // Convert the minDestinationTransferAmount to an origin amount
533
- const minOriginTransferAmount = destinationToken.amount(convertDecimalsToIntegerString(originToken.decimals, destinationToken.decimals, minDestinationTransferAmount.toString()));
712
+ const minOriginTransferAmount = originToken.amount(convertDecimalsToIntegerString(resolvedDestinationToken.decimals, originToken.decimals, minDestinationTransferAmount.toString()));
534
713
  if (minOriginTransferAmount.amount > originTokenAmount.amount) {
535
714
  return {
536
715
  amount: `Minimum transfer amount is ${minOriginTransferAmount.getDecimalFormattedAmount()} ${originToken.symbol}`,
@@ -541,7 +720,7 @@ export class WarpCore {
541
720
  /**
542
721
  * Ensure the sender has sufficient balances for transfer and interchain gas
543
722
  */
544
- async validateTokenBalances(originTokenAmount, destination, sender, recipient, senderPubKey) {
723
+ async validateTokenBalances(originTokenAmount, destination, sender, recipient, senderPubKey, destinationToken) {
545
724
  const { token: originToken, amount } = originTokenAmount;
546
725
  const { amount: senderBalance } = await originToken.getBalance(this.multiProvider, sender);
547
726
  const senderBalanceAmount = originTokenAmount.token.amount(senderBalance);
@@ -555,6 +734,7 @@ export class WarpCore {
555
734
  destination,
556
735
  sender,
557
736
  recipient,
737
+ destinationToken,
558
738
  });
559
739
  // Get balance of the IGP fee token, which may be different from the transfer token
560
740
  const interchainQuoteTokenBalance = originToken.isFungibleWith(interchainQuote.token)
@@ -581,6 +761,7 @@ export class WarpCore {
581
761
  senderPubKey,
582
762
  interchainFee: interchainQuote,
583
763
  tokenFeeQuote,
764
+ destinationToken,
584
765
  });
585
766
  const feeEstimate = { interchainQuote, localQuote };
586
767
  // Check 5: Ensure balances can cover the COMBINED amount and fees
@@ -591,6 +772,7 @@ export class WarpCore {
591
772
  sender,
592
773
  senderPubKey,
593
774
  feeEstimate,
775
+ destinationToken,
594
776
  });
595
777
  if (amount > maxTransfer.amount) {
596
778
  return { amount: 'Insufficient balance for gas and transfer' };
@@ -600,10 +782,11 @@ export class WarpCore {
600
782
  /**
601
783
  * Ensure the sender has sufficient balances for transfer and interchain gas
602
784
  */
603
- async validateDestinationCollateral(originTokenAmount, destination) {
785
+ async validateDestinationCollateral(originTokenAmount, destination, destinationToken) {
604
786
  const valid = await this.isDestinationCollateralSufficient({
605
787
  originTokenAmount,
606
788
  destination,
789
+ destinationToken,
607
790
  });
608
791
  if (!valid) {
609
792
  return { amount: 'Insufficient collateral on destination' };
@@ -613,24 +796,28 @@ export class WarpCore {
613
796
  /**
614
797
  * Ensure the sender has sufficient balances for minting
615
798
  */
616
- async validateDestinationRateLimit(originTokenAmount, destination) {
799
+ async validateDestinationRateLimit(originTokenAmount, destination, destinationToken) {
617
800
  const { token: originToken, amount } = originTokenAmount;
618
- const destinationName = this.multiProvider.getChainName(destination);
619
- const destinationToken = originToken.getConnectionForChain(destinationName)?.token;
620
- assert(destinationToken, `No connection found for ${destinationName}`);
621
- if (!MINT_LIMITED_STANDARDS.includes(destinationToken.standard)) {
622
- this.logger.debug(`${destinationToken.symbol} does not have rate limit constraint, skipping`);
801
+ const resolvedDestinationToken = this.resolveDestinationToken({
802
+ originToken,
803
+ destination,
804
+ destinationToken,
805
+ });
806
+ if (!MINT_LIMITED_STANDARDS.includes(resolvedDestinationToken.standard)) {
807
+ this.logger.debug(`${resolvedDestinationToken.symbol} does not have rate limit constraint, skipping`);
623
808
  return null;
624
809
  }
625
810
  let destinationMintLimit = 0n;
626
- if (destinationToken.standard === TokenStandard.EvmHypVSXERC20 ||
627
- destinationToken.standard === TokenStandard.EvmHypVSXERC20Lockbox ||
628
- destinationToken.standard === TokenStandard.EvmHypXERC20 ||
629
- destinationToken.standard === TokenStandard.EvmHypXERC20Lockbox) {
630
- const adapter = destinationToken.getAdapter(this.multiProvider);
811
+ if (resolvedDestinationToken.standard === TokenStandard.EvmHypVSXERC20 ||
812
+ resolvedDestinationToken.standard ===
813
+ TokenStandard.EvmHypVSXERC20Lockbox ||
814
+ resolvedDestinationToken.standard === TokenStandard.EvmHypXERC20 ||
815
+ resolvedDestinationToken.standard === TokenStandard.EvmHypXERC20Lockbox) {
816
+ const adapter = resolvedDestinationToken.getAdapter(this.multiProvider);
631
817
  destinationMintLimit = await adapter.getMintLimit();
632
- if (destinationToken.standard === TokenStandard.EvmHypVSXERC20 ||
633
- destinationToken.standard === TokenStandard.EvmHypVSXERC20Lockbox) {
818
+ if (resolvedDestinationToken.standard === TokenStandard.EvmHypVSXERC20 ||
819
+ resolvedDestinationToken.standard ===
820
+ TokenStandard.EvmHypVSXERC20Lockbox) {
634
821
  const bufferCap = await adapter.getMintMaxLimit();
635
822
  const max = bufferCap / 2n;
636
823
  if (destinationMintLimit > max) {
@@ -639,11 +826,11 @@ export class WarpCore {
639
826
  }
640
827
  }
641
828
  }
642
- else if (destinationToken.standard === TokenStandard.EvmHypCollateralFiat) {
643
- const adapter = destinationToken.getAdapter(this.multiProvider);
829
+ else if (resolvedDestinationToken.standard === TokenStandard.EvmHypCollateralFiat) {
830
+ const adapter = resolvedDestinationToken.getAdapter(this.multiProvider);
644
831
  destinationMintLimit = await adapter.getMintLimit();
645
832
  }
646
- const destinationMintLimitInOriginDecimals = convertDecimalsToIntegerString(destinationToken.decimals, originToken.decimals, destinationMintLimit.toString());
833
+ const destinationMintLimitInOriginDecimals = convertDecimalsToIntegerString(resolvedDestinationToken.decimals, originToken.decimals, destinationMintLimit.toString());
647
834
  const isSufficient = BigInt(destinationMintLimitInOriginDecimals) >= amount;
648
835
  this.logger.debug(`${originTokenAmount.token.symbol} to ${destination} has ${isSufficient ? 'sufficient' : 'INSUFFICIENT'} rate limits`);
649
836
  if (!isSufficient)
@@ -664,6 +851,24 @@ export class WarpCore {
664
851
  }
665
852
  return null;
666
853
  }
854
+ resolveDestinationToken({ originToken, destination, destinationToken, }) {
855
+ const destinationName = this.multiProvider.getChainName(destination);
856
+ const destinationCandidates = originToken
857
+ .getConnections()
858
+ .filter((connection) => connection.token.chainName === destinationName)
859
+ .map((connection) => connection.token);
860
+ assert(destinationCandidates.length > 0, `No connection found for ${destinationName}`);
861
+ if (destinationToken) {
862
+ assert(destinationToken.chainName === destinationName, `Destination token chain mismatch for ${destinationName}`);
863
+ const matchedToken = destinationCandidates.find((candidate) => candidate.equals(destinationToken) ||
864
+ candidate.addressOrDenom.toLowerCase() ===
865
+ destinationToken.addressOrDenom.toLowerCase());
866
+ assert(matchedToken, `Destination token ${destinationToken.addressOrDenom} is not connected from ${originToken.chainName} to ${destinationName}`);
867
+ return matchedToken;
868
+ }
869
+ assert(destinationCandidates.length === 1, `Ambiguous route to ${destinationName}; specify destination token`);
870
+ return destinationCandidates[0];
871
+ }
667
872
  /**
668
873
  * Search through token list to find token with matching chain and address
669
874
  */