@chainlink/ccip-sdk 1.2.0 → 1.3.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 (103) hide show
  1. package/dist/api/index.d.ts +1 -1
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +10 -20
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/aptos/index.d.ts +2 -2
  6. package/dist/aptos/index.d.ts.map +1 -1
  7. package/dist/aptos/index.js +1 -1
  8. package/dist/aptos/index.js.map +1 -1
  9. package/dist/chain.d.ts +75 -2
  10. package/dist/chain.d.ts.map +1 -1
  11. package/dist/chain.js +20 -1
  12. package/dist/chain.js.map +1 -1
  13. package/dist/errors/codes.d.ts +1 -0
  14. package/dist/errors/codes.d.ts.map +1 -1
  15. package/dist/errors/codes.js +1 -0
  16. package/dist/errors/codes.js.map +1 -1
  17. package/dist/errors/index.d.ts +1 -1
  18. package/dist/errors/index.d.ts.map +1 -1
  19. package/dist/errors/index.js +1 -1
  20. package/dist/errors/index.js.map +1 -1
  21. package/dist/errors/recovery.d.ts.map +1 -1
  22. package/dist/errors/recovery.js +1 -0
  23. package/dist/errors/recovery.js.map +1 -1
  24. package/dist/errors/specialized.d.ts +8 -0
  25. package/dist/errors/specialized.d.ts.map +1 -1
  26. package/dist/errors/specialized.js +10 -0
  27. package/dist/errors/specialized.js.map +1 -1
  28. package/dist/evm/abi/CCTPVerifier_2_0.d.ts +1118 -0
  29. package/dist/evm/abi/CCTPVerifier_2_0.d.ts.map +1 -0
  30. package/dist/evm/abi/CCTPVerifier_2_0.js +1147 -0
  31. package/dist/evm/abi/CCTPVerifier_2_0.js.map +1 -0
  32. package/dist/evm/abi/USDCTokenPoolProxy_2_0.d.ts +825 -0
  33. package/dist/evm/abi/USDCTokenPoolProxy_2_0.d.ts.map +1 -0
  34. package/dist/evm/abi/USDCTokenPoolProxy_2_0.js +873 -0
  35. package/dist/evm/abi/USDCTokenPoolProxy_2_0.js.map +1 -0
  36. package/dist/evm/abi/VersionedVerifierResolver_2_0.d.ts +350 -0
  37. package/dist/evm/abi/VersionedVerifierResolver_2_0.d.ts.map +1 -0
  38. package/dist/evm/abi/VersionedVerifierResolver_2_0.js +370 -0
  39. package/dist/evm/abi/VersionedVerifierResolver_2_0.js.map +1 -0
  40. package/dist/evm/const.d.ts +3 -0
  41. package/dist/evm/const.d.ts.map +1 -1
  42. package/dist/evm/const.js +6 -0
  43. package/dist/evm/const.js.map +1 -1
  44. package/dist/evm/index.d.ts +24 -3
  45. package/dist/evm/index.d.ts.map +1 -1
  46. package/dist/evm/index.js +201 -8
  47. package/dist/evm/index.js.map +1 -1
  48. package/dist/evm/types.d.ts +1 -1
  49. package/dist/evm/types.d.ts.map +1 -1
  50. package/dist/index.d.ts +3 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/offchain.d.ts +27 -0
  55. package/dist/offchain.d.ts.map +1 -1
  56. package/dist/offchain.js +49 -7
  57. package/dist/offchain.js.map +1 -1
  58. package/dist/requests.d.ts +1 -25
  59. package/dist/requests.d.ts.map +1 -1
  60. package/dist/requests.js +2 -57
  61. package/dist/requests.js.map +1 -1
  62. package/dist/solana/index.d.ts +2 -2
  63. package/dist/solana/index.d.ts.map +1 -1
  64. package/dist/solana/index.js +2 -2
  65. package/dist/solana/index.js.map +1 -1
  66. package/dist/sui/index.d.ts +2 -2
  67. package/dist/sui/index.d.ts.map +1 -1
  68. package/dist/sui/index.js +2 -2
  69. package/dist/sui/index.js.map +1 -1
  70. package/dist/ton/index.d.ts +2 -2
  71. package/dist/ton/index.d.ts.map +1 -1
  72. package/dist/ton/index.js +28 -49
  73. package/dist/ton/index.js.map +1 -1
  74. package/dist/ton/send.d.ts +13 -1
  75. package/dist/ton/send.d.ts.map +1 -1
  76. package/dist/ton/send.js +16 -16
  77. package/dist/ton/send.js.map +1 -1
  78. package/dist/utils.d.ts +16 -0
  79. package/dist/utils.d.ts.map +1 -1
  80. package/dist/utils.js +31 -1
  81. package/dist/utils.js.map +1 -1
  82. package/package.json +7 -7
  83. package/src/api/index.ts +9 -23
  84. package/src/aptos/index.ts +5 -1
  85. package/src/chain.ts +86 -3
  86. package/src/errors/codes.ts +1 -0
  87. package/src/errors/index.ts +1 -0
  88. package/src/errors/recovery.ts +2 -0
  89. package/src/errors/specialized.ts +15 -0
  90. package/src/evm/abi/CCTPVerifier_2_0.ts +1146 -0
  91. package/src/evm/abi/USDCTokenPoolProxy_2_0.ts +872 -0
  92. package/src/evm/abi/VersionedVerifierResolver_2_0.ts +369 -0
  93. package/src/evm/const.ts +6 -0
  94. package/src/evm/index.ts +277 -10
  95. package/src/evm/types.ts +1 -1
  96. package/src/index.ts +6 -2
  97. package/src/offchain.ts +58 -8
  98. package/src/requests.ts +2 -59
  99. package/src/solana/index.ts +8 -2
  100. package/src/sui/index.ts +5 -2
  101. package/src/ton/index.ts +41 -56
  102. package/src/ton/send.ts +20 -21
  103. package/src/utils.ts +42 -0
package/src/evm/index.ts CHANGED
@@ -34,6 +34,9 @@ import {
34
34
  type LogFilter,
35
35
  type RateLimiterState,
36
36
  type TokenPoolRemote,
37
+ type TokenTransferFeeConfig,
38
+ type TokenTransferFeeOpts,
39
+ type TotalFeesEstimate,
37
40
  Chain,
38
41
  LaneFeature,
39
42
  } from '../chain.ts'
@@ -60,6 +63,7 @@ import {
60
63
  } from '../errors/index.ts'
61
64
  import type { ExtraArgs } from '../extra-args.ts'
62
65
  import type { LeafHasher } from '../hasher/common.ts'
66
+ import { CCTP_FINALITY_FAST, getUsdcBurnFees } from '../offchain.ts'
63
67
  import { supportedChains } from '../supported-chains.ts'
64
68
  import {
65
69
  type CCIPExecution,
@@ -87,6 +91,7 @@ import {
87
91
  parseTypeAndVersion,
88
92
  } from '../utils.ts'
89
93
  import type Token_ABI from './abi/BurnMintERC677Token.ts'
94
+ import type CCTPVerifier_2_0_ABI from './abi/CCTPVerifier_2_0.ts'
90
95
  import type FeeQuoter_ABI from './abi/FeeQuoter_1_6.ts'
91
96
  import type TokenPool_1_5_ABI from './abi/LockReleaseTokenPool_1_5.ts'
92
97
  import type TokenPool_ABI from './abi/LockReleaseTokenPool_1_6_1.ts'
@@ -101,6 +106,8 @@ import type OnRamp_2_0_ABI from './abi/OnRamp_2_0.ts'
101
106
  import type Router_ABI from './abi/Router.ts'
102
107
  import type TokenAdminRegistry_1_5_ABI from './abi/TokenAdminRegistry_1_5.ts'
103
108
  import type TokenPool_2_0_ABI from './abi/TokenPool_2_0.ts'
109
+ import type USDCTokenPoolProxy_2_0_ABI from './abi/USDCTokenPoolProxy_2_0.ts'
110
+ import type VersionedVerifierResolver_2_0_ABI from './abi/VersionedVerifierResolver_2_0.ts'
104
111
  import {
105
112
  CCV_INDEXER_URL,
106
113
  VersionedContractABI,
@@ -241,6 +248,8 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
241
248
  maxArgs: 1,
242
249
  })
243
250
  this.getFeeTokens = memoize(this.getFeeTokens.bind(this), { async: true, maxArgs: 1 })
251
+ this.detectUsdcDomains = memoize(this.detectUsdcDomains.bind(this))
252
+ this.resolveVerifier = memoize(this.resolveVerifier.bind(this))
244
253
  }
245
254
 
246
255
  /**
@@ -973,11 +982,24 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
973
982
  if (version < CCIPVersion.V1_6)
974
983
  throw new CCIPVersionFeatureUnavailableError('feeQuoter', version, 'v1.6')
975
984
 
985
+ const isOnRamp = type.includes('OnRamp')
976
986
  const contract = new Contract(
977
987
  address,
978
- type.includes('OnRamp') ? interfaces.OnRamp_v1_6 : interfaces.OffRamp_v1_6,
988
+ version < CCIPVersion.V2_0
989
+ ? isOnRamp
990
+ ? interfaces.OnRamp_v1_6
991
+ : interfaces.OffRamp_v1_6
992
+ : isOnRamp
993
+ ? interfaces.OnRamp_v2_0
994
+ : interfaces.OffRamp_v2_0,
979
995
  this.provider,
980
- ) as unknown as TypedContract<typeof OnRamp_1_6_ABI | typeof OffRamp_1_6_ABI>
996
+ ) as unknown as TypedContract<
997
+ | typeof OnRamp_1_6_ABI
998
+ | typeof OffRamp_1_6_ABI
999
+ | typeof OnRamp_2_0_ABI
1000
+ | typeof OffRamp_2_0_ABI
1001
+ >
1002
+
981
1003
  const { feeQuoter } = await contract.getDynamicConfig()
982
1004
  return feeQuoter as string
983
1005
  }
@@ -1005,6 +1027,210 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
1005
1027
  })
1006
1028
  }
1007
1029
 
1030
+ /**
1031
+ * Detect whether a token pool is a USDC/CCTP pool via typeAndVersion, then resolve
1032
+ * the CCTPVerifier address and fetch source/dest CCTP domain IDs.
1033
+ *
1034
+ * @param tokenPool - The token pool address to check.
1035
+ * @param destChainSelector - Destination chain selector for getDomain().
1036
+ * @param ccvs - Cross-chain verifier addresses from extraArgs (fallback for verifier discovery).
1037
+ * @returns Source and dest CCTP domain IDs, or undefined if not a USDC pool.
1038
+ */
1039
+ private async detectUsdcDomains(
1040
+ tokenPool: string,
1041
+ destChainSelector: bigint,
1042
+ ccvs: string[] = [],
1043
+ ): Promise<{ sourceDomain: number; destDomain: number } | undefined> {
1044
+ // 1. Check if pool is USDCTokenPoolProxy
1045
+ let poolType: string
1046
+ try {
1047
+ ;[poolType] = await this.typeAndVersion(tokenPool)
1048
+ } catch {
1049
+ return undefined
1050
+ }
1051
+ if (poolType !== 'USDCTokenPoolProxy') return undefined
1052
+
1053
+ // 2. Find CCTPVerifier address
1054
+ let verifierAddress: string | undefined
1055
+
1056
+ // 2a. Try pool's getStaticConfig (returns resolver/verifier address)
1057
+ try {
1058
+ const proxy = new Contract(
1059
+ tokenPool,
1060
+ interfaces.USDCTokenPoolProxy_v2_0,
1061
+ this.provider,
1062
+ ) as unknown as TypedContract<typeof USDCTokenPoolProxy_2_0_ABI>
1063
+ const [, , cctpVerifier] = await proxy.getStaticConfig()
1064
+ const candidate = cctpVerifier as string
1065
+ if (candidate && candidate !== ZeroAddress) {
1066
+ verifierAddress = await this.resolveVerifier(candidate, destChainSelector)
1067
+ }
1068
+ } catch {
1069
+ /* proxy may not be initialized */
1070
+ }
1071
+
1072
+ // 2b. Fall back to scanning ccvs from extraArgs
1073
+ if (!verifierAddress) {
1074
+ for (const ccv of ccvs) {
1075
+ if (!ccv) continue
1076
+ try {
1077
+ const resolved = await this.resolveVerifier(ccv, destChainSelector)
1078
+ if (resolved) {
1079
+ verifierAddress = resolved
1080
+ break
1081
+ }
1082
+ } catch {
1083
+ /* not a valid contract */
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ if (!verifierAddress) return undefined
1089
+
1090
+ // 3. Fetch source and dest CCTP domain IDs from verifier
1091
+ try {
1092
+ const verifier = new Contract(
1093
+ verifierAddress,
1094
+ interfaces.CCTPVerifier_v2_0,
1095
+ this.provider,
1096
+ ) as unknown as TypedContract<typeof CCTPVerifier_2_0_ABI>
1097
+ const [staticConfig, domainResult] = await Promise.all([
1098
+ verifier.getStaticConfig(),
1099
+ verifier.getDomain(destChainSelector),
1100
+ ])
1101
+ return {
1102
+ sourceDomain: Number(staticConfig[3]), // localDomainIdentifier
1103
+ destDomain: Number(domainResult.domainIdentifier),
1104
+ }
1105
+ } catch (err) {
1106
+ if (isError(err, 'CALL_EXCEPTION')) return undefined
1107
+ throw CCIPError.from(err)
1108
+ }
1109
+ }
1110
+
1111
+ /**
1112
+ * Given a candidate address, check if it's a CCTPVerifier or VersionedVerifierResolver
1113
+ * and return the actual verifier address (resolving through the resolver if needed).
1114
+ */
1115
+ private async resolveVerifier(
1116
+ candidate: string,
1117
+ destChainSelector: bigint,
1118
+ ): Promise<string | undefined> {
1119
+ try {
1120
+ const [candidateType] = await this.typeAndVersion(candidate)
1121
+ if (candidateType === 'VersionedVerifierResolver') {
1122
+ const resolver = new Contract(
1123
+ candidate,
1124
+ interfaces.VersionedVerifierResolver_v2_0,
1125
+ this.provider,
1126
+ ) as unknown as TypedContract<typeof VersionedVerifierResolver_2_0_ABI>
1127
+ return (await resolver.getOutboundImplementation(destChainSelector, '0x')) as string
1128
+ }
1129
+ if (candidateType === 'CCTPVerifier') return candidate
1130
+ } catch {
1131
+ /* not a valid versioned contract */
1132
+ }
1133
+ return undefined
1134
+ }
1135
+
1136
+ /** {@inheritDoc Chain.getTotalFeesEstimate} */
1137
+ override async getTotalFeesEstimate(
1138
+ opts: Parameters<Chain['getTotalFeesEstimate']>[0],
1139
+ ): Promise<TotalFeesEstimate> {
1140
+ const tokenAmounts = opts.message.tokenAmounts
1141
+ const ccipFee$ = this.getFee(opts)
1142
+
1143
+ if (!tokenAmounts?.length) {
1144
+ return { ccipFee: await ccipFee$ }
1145
+ }
1146
+
1147
+ const { token, amount } = tokenAmounts[0]!
1148
+
1149
+ // Determine blockConfirmations and tokenArgs from extraArgs
1150
+ const extraArgs = opts.message.extraArgs
1151
+ let blockConfirmations = 0
1152
+ let tokenArgs: string = '0x'
1153
+ if (extraArgs && 'blockConfirmations' in extraArgs) {
1154
+ blockConfirmations = extraArgs.blockConfirmations as number
1155
+ tokenArgs = hexlify(extraArgs.tokenArgs as BytesLike)
1156
+ }
1157
+
1158
+ // Skip pool-level fee lookup for pre-v2.0 lanes
1159
+ const onRamp = await this.getOnRampForRouter(opts.router, opts.destChainSelector)
1160
+ const [, version] = await this.typeAndVersion(onRamp)
1161
+ if (version < CCIPVersion.V2_0) {
1162
+ return { ccipFee: await ccipFee$ }
1163
+ }
1164
+
1165
+ const onRampContract = new Contract(
1166
+ onRamp,
1167
+ interfaces.OnRamp_v2_0,
1168
+ this.provider,
1169
+ ) as unknown as TypedContract<typeof OnRamp_2_0_ABI>
1170
+
1171
+ const poolAddress = (await onRampContract.getPoolBySourceToken(
1172
+ opts.destChainSelector,
1173
+ token,
1174
+ )) as string
1175
+
1176
+ const [ccipFee, { tokenTransferFeeConfig }, usdcDomains] = await Promise.all([
1177
+ ccipFee$,
1178
+ this.getTokenPoolConfig(poolAddress, {
1179
+ destChainSelector: opts.destChainSelector,
1180
+ blockConfirmationsRequested: blockConfirmations,
1181
+ tokenArgs,
1182
+ }),
1183
+ this.detectUsdcDomains(
1184
+ poolAddress,
1185
+ opts.destChainSelector,
1186
+ extraArgs && 'ccvs' in extraArgs ? extraArgs.ccvs : [],
1187
+ ),
1188
+ ])
1189
+
1190
+ // USDC path: use Circle CCTP burn fees
1191
+ if (usdcDomains) {
1192
+ const burnFees = await getUsdcBurnFees(
1193
+ usdcDomains.sourceDomain,
1194
+ usdcDomains.destDomain,
1195
+ this.network.networkType,
1196
+ )
1197
+ const fast = blockConfirmations > 0
1198
+ // Tiers are sorted ascending by finalityThreshold; findLast for fast ensures
1199
+ // we pick the highest tier still within the fast threshold.
1200
+ const tier = fast
1201
+ ? burnFees.findLast((t) => t.finalityThreshold <= CCTP_FINALITY_FAST)
1202
+ : burnFees.find((t) => t.finalityThreshold > CCTP_FINALITY_FAST)
1203
+ if (tier && tier.minimumFee > 0) {
1204
+ return {
1205
+ ccipFee,
1206
+ tokenTransferFee: {
1207
+ feeDeducted: (BigInt(amount) * BigInt(tier.minimumFee)) / 10_000n,
1208
+ bps: tier.minimumFee,
1209
+ },
1210
+ }
1211
+ }
1212
+ return { ccipFee }
1213
+ }
1214
+
1215
+ // Non-USDC path: use on-chain tokenTransferFeeConfig
1216
+ if (!tokenTransferFeeConfig || !tokenTransferFeeConfig.isEnabled) {
1217
+ return { ccipFee }
1218
+ }
1219
+
1220
+ const useCustom = blockConfirmations > 0
1221
+ const bps = useCustom
1222
+ ? tokenTransferFeeConfig.customBlockConfirmationsTransferFeeBps
1223
+ : tokenTransferFeeConfig.defaultBlockConfirmationsTransferFeeBps
1224
+
1225
+ return {
1226
+ ccipFee,
1227
+ tokenTransferFee: {
1228
+ feeDeducted: (BigInt(amount) * BigInt(bps)) / 10_000n,
1229
+ bps,
1230
+ },
1231
+ }
1232
+ }
1233
+
1008
1234
  /**
1009
1235
  * Generates unsigned EVM transactions for sending a CCIP message.
1010
1236
  *
@@ -1413,44 +1639,84 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
1413
1639
  * Fetches the token pool configuration for an EVM token pool contract.
1414
1640
  *
1415
1641
  * @param tokenPool - Token pool contract address.
1416
- * @returns Token pool config containing token, router, typeAndVersion, and optionally minBlockConfirmations.
1642
+ * @param feeOpts - Optional parameters to also fetch token transfer fee config.
1643
+ * @returns Token pool config containing token, router, typeAndVersion, and optionally
1644
+ * minBlockConfirmations and tokenTransferFeeConfig.
1417
1645
  *
1418
1646
  * @remarks
1419
1647
  * For pools with version \>= 2.0, also returns `minBlockConfirmations` for
1420
1648
  * Faster-Than-Finality (FTF) support. Pre-2.0 pools omit this field.
1649
+ * When `feeOpts` is provided and the pool is v2.0+, also fetches token transfer fee config.
1421
1650
  */
1422
- async getTokenPoolConfig(tokenPool: string): Promise<{
1651
+ async getTokenPoolConfig(
1652
+ tokenPool: string,
1653
+ feeOpts?: TokenTransferFeeOpts,
1654
+ ): Promise<{
1423
1655
  token: string
1424
1656
  router: string
1425
1657
  typeAndVersion: string
1426
1658
  minBlockConfirmations?: number
1659
+ tokenTransferFeeConfig?: TokenTransferFeeConfig
1427
1660
  }> {
1428
1661
  const [_, version, typeAndVersion] = await this.typeAndVersion(tokenPool)
1429
1662
 
1430
- let contract, router, minBlockConfirmations
1663
+ let token, router, minBlockConfirmations, tokenTransferFeeConfig
1431
1664
  if (version < CCIPVersion.V2_0) {
1432
- contract = new Contract(
1665
+ const contract = new Contract(
1433
1666
  tokenPool,
1434
1667
  interfaces.TokenPool_v1_6,
1435
1668
  this.provider,
1436
1669
  ) as unknown as TypedContract<typeof TokenPool_ABI>
1670
+ token = contract.getToken()
1437
1671
  router = contract.getRouter()
1438
1672
  } else {
1439
- contract = new Contract(
1673
+ const contract = new Contract(
1440
1674
  tokenPool,
1441
1675
  interfaces.TokenPool_v2_0,
1442
1676
  this.provider,
1443
1677
  ) as unknown as TypedContract<typeof TokenPool_2_0_ABI>
1678
+ token = contract.getToken()
1444
1679
  router = contract.getDynamicConfig().then(([router]) => router)
1445
1680
  minBlockConfirmations = contract.getMinBlockConfirmations().catch((err) => {
1446
1681
  if (isError(err, 'CALL_EXCEPTION')) return 0
1447
1682
  throw CCIPError.from(err)
1448
1683
  })
1684
+ if (feeOpts) {
1685
+ tokenTransferFeeConfig = token.then((tokenAddr) =>
1686
+ contract
1687
+ .getTokenTransferFeeConfig(
1688
+ tokenAddr as string,
1689
+ feeOpts.destChainSelector,
1690
+ BigInt(feeOpts.blockConfirmationsRequested),
1691
+ feeOpts.tokenArgs,
1692
+ )
1693
+ .then((result) => ({
1694
+ destGasOverhead: Number(result.destGasOverhead),
1695
+ destBytesOverhead: Number(result.destBytesOverhead),
1696
+ defaultBlockConfirmationsFeeUSDCents: Number(
1697
+ result.defaultBlockConfirmationsFeeUSDCents,
1698
+ ),
1699
+ customBlockConfirmationsFeeUSDCents: Number(
1700
+ result.customBlockConfirmationsFeeUSDCents,
1701
+ ),
1702
+ defaultBlockConfirmationsTransferFeeBps: Number(
1703
+ result.defaultBlockConfirmationsTransferFeeBps,
1704
+ ),
1705
+ customBlockConfirmationsTransferFeeBps: Number(
1706
+ result.customBlockConfirmationsTransferFeeBps,
1707
+ ),
1708
+ isEnabled: result.isEnabled,
1709
+ }))
1710
+ .catch((err) => {
1711
+ if (isError(err, 'CALL_EXCEPTION')) return undefined
1712
+ throw CCIPError.from(err, 'UNKNOWN')
1713
+ }),
1714
+ )
1715
+ }
1449
1716
  }
1450
- const token = contract.getToken()
1451
1717
 
1452
- return Promise.all([token, router, minBlockConfirmations]).then(
1453
- ([token, router, minBlockConfirmations]) => {
1718
+ return Promise.all([token, router, minBlockConfirmations, tokenTransferFeeConfig]).then(
1719
+ ([token, router, minBlockConfirmations, tokenTransferFeeConfig]) => {
1454
1720
  return {
1455
1721
  token: token as CleanAddressable<typeof token>,
1456
1722
  router: router as CleanAddressable<typeof router>,
@@ -1458,6 +1724,7 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
1458
1724
  ...(minBlockConfirmations != null && {
1459
1725
  minBlockConfirmations: Number(minBlockConfirmations),
1460
1726
  }),
1727
+ ...(tokenTransferFeeConfig != null && { tokenTransferFeeConfig }),
1461
1728
  }
1462
1729
  },
1463
1730
  )
package/src/evm/types.ts CHANGED
@@ -7,7 +7,7 @@ import type { ChainFamily } from '../types.ts'
7
7
  */
8
8
  export type UnsignedEVMTx = {
9
9
  family: typeof ChainFamily.EVM
10
- transactions: Pick<TransactionRequest, 'from' | 'to' | 'data' | 'gasLimit'>[]
10
+ transactions: Pick<TransactionRequest, 'from' | 'to' | 'data' | 'gasLimit' | 'value'>[]
11
11
  }
12
12
 
13
13
  /**
package/src/index.ts CHANGED
@@ -37,6 +37,10 @@ export type {
37
37
  TokenInfo,
38
38
  TokenPoolConfig,
39
39
  TokenPoolRemote,
40
+ TokenTransferFee,
41
+ TokenTransferFeeConfig,
42
+ TokenTransferFeeOpts,
43
+ TotalFeesEstimate,
40
44
  } from './chain.ts'
41
45
  export { DEFAULT_API_RETRY_CONFIG, LaneFeature } from './chain.ts'
42
46
  export { calculateManualExecProof, discoverOffRamp } from './execution.ts'
@@ -51,8 +55,8 @@ export {
51
55
  encodeExtraArgs,
52
56
  } from './extra-args.ts'
53
57
  export { estimateReceiveExecution } from './gas.ts'
54
- export { getOffchainTokenData } from './offchain.ts'
55
- export { decodeMessage, getMessagesForSender, sourceToDestTokenAddresses } from './requests.ts'
58
+ export { CCTP_FINALITY_FAST, CCTP_FINALITY_STANDARD, getOffchainTokenData } from './offchain.ts'
59
+ export { decodeMessage, sourceToDestTokenAddresses } from './requests.ts'
56
60
  export {
57
61
  type CCIPExecution,
58
62
  type CCIPMessage,
package/src/offchain.ts CHANGED
@@ -1,14 +1,15 @@
1
- import { type BytesLike, dataLength, dataSlice, getBytes, toNumber } from 'ethers'
1
+ import { type BytesLike, dataLength, dataSlice, toNumber } from 'ethers'
2
2
  import type { PickDeep } from 'type-fest'
3
3
 
4
4
  import {
5
5
  CCIPLbtcAttestationNotApprovedError,
6
6
  CCIPLbtcAttestationNotFoundError,
7
7
  CCIPUsdcAttestationError,
8
+ CCIPUsdcBurnFeesError,
8
9
  } from './errors/index.ts'
9
10
  import { parseSourceTokenData } from './evm/messages.ts'
10
11
  import { type CCIPRequest, type OffchainTokenData, type WithLogger, NetworkType } from './types.ts'
11
- import { networkInfo } from './utils.ts'
12
+ import { fetchWithTimeout, getDataBytes, networkInfo } from './utils.ts'
12
13
 
13
14
  const CIRCLE_API_URL = {
14
15
  mainnet: 'https://iris-api.circle.com',
@@ -60,6 +61,57 @@ export async function getUsdcAttestation(
60
61
  return att
61
62
  }
62
63
 
64
+ /**
65
+ * CCTP V2 finality tier identifiers returned by Circle's burn-fees API.
66
+ *
67
+ * These are **opaque tier IDs**, not block counts or durations.
68
+ * The CCTP V2 whitepaper (Section 8, Table 2) defines exactly two tiers today;
69
+ * additional tiers may be added in the future (the wide spacing between values
70
+ * is intentional to leave room).
71
+ *
72
+ * @see https://developers.circle.com/cctp/concepts/fees
73
+ * @see CCTP V2 Whitepaper, Section 8 — "Finality Levels"
74
+ */
75
+
76
+ /** Fast / pre-finality tier: attested seconds after soft confirmation. */
77
+ export const CCTP_FINALITY_FAST = 1000
78
+
79
+ /** Standard / finalized tier: attested after full on-chain finality. */
80
+ export const CCTP_FINALITY_STANDARD = 2000
81
+
82
+ /**
83
+ * Fetches USDC burn fee tiers from Circle's CCTP API.
84
+ *
85
+ * @param sourceDomain - CCTP source domain identifier
86
+ * @param destDomain - CCTP destination domain identifier
87
+ * @param networkType - network type (mainnet or testnet)
88
+ * @returns Array of fee tiers with finality thresholds and BPS fees
89
+ */
90
+ export async function getUsdcBurnFees(
91
+ sourceDomain: number,
92
+ destDomain: number,
93
+ networkType: NetworkType,
94
+ ): Promise<{ finalityThreshold: number; minimumFee: number }[]> {
95
+ const baseUrl =
96
+ networkType === NetworkType.Mainnet ? CIRCLE_API_URL.mainnet : CIRCLE_API_URL.testnet
97
+ const url = `${baseUrl}/v2/burn/USDC/fees/${sourceDomain}/${destDomain}`
98
+ const res = await fetchWithTimeout(url, 'getUsdcBurnFees')
99
+ if (!res.ok) {
100
+ throw new CCIPUsdcBurnFeesError(sourceDomain, destDomain, res.status)
101
+ }
102
+ const json: unknown = await res.json()
103
+ if (!Array.isArray(json)) {
104
+ throw new CCIPUsdcBurnFeesError(sourceDomain, destDomain, res.status)
105
+ }
106
+ for (const tier of json) {
107
+ const t = tier as Record<string, unknown>
108
+ if (typeof t.finalityThreshold !== 'number' || typeof t.minimumFee !== 'number') {
109
+ throw new CCIPUsdcBurnFeesError(sourceDomain, destDomain, res.status)
110
+ }
111
+ }
112
+ return json as { finalityThreshold: number; minimumFee: number }[]
113
+ }
114
+
63
115
  const LOMBARD_API_URL = {
64
116
  mainnet: 'https://mainnet.prod.lombard.finance',
65
117
  testnet: 'https://gastald-testnet.prod.lombard.finance',
@@ -122,7 +174,7 @@ export async function getOffchainTokenData(
122
174
  const { networkType } = networkInfo(request.message.sourceChainSelector)
123
175
 
124
176
  function looksUsdcData(extraData: BytesLike) {
125
- if (dataLength(extraData) !== 64) return
177
+ if (getDataBytes(extraData).length !== 64) return
126
178
  // USDCTokenPool's extraData is a packed `SourceTokenDataPayloadV1{uint64 nonce, uint32 sourceDomain}`,
127
179
  // which we need to query CCTPv2 (by sourceDomain and txHash) and to filter by nonce among messages,
128
180
  // if more than one in tx
@@ -139,11 +191,9 @@ export async function getOffchainTokenData(
139
191
 
140
192
  function looksLbtcData(extraData: BytesLike) {
141
193
  // LBTC returns `message_hash`/`payloadHash` directly as `bytes32 extraData`
142
- if (
143
- dataLength(extraData) === 32 &&
144
- getBytes(extraData, 'extraData').filter(Boolean).length > 20 // looks like a hash
145
- )
146
- return true
194
+ const bytes = getDataBytes(extraData)
195
+ // looks like a hash
196
+ if (bytes.length === 32 && bytes.filter(Boolean).length > 20) return true
147
197
  }
148
198
 
149
199
  return Promise.all(
package/src/requests.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type BytesLike, hexlify, isBytesLike, toBigInt } from 'ethers'
2
2
  import type { PickDeep } from 'type-fest'
3
3
 
4
- import { type ChainStatic, type LogFilter, Chain } from './chain.ts'
4
+ import { type ChainStatic, Chain } from './chain.ts'
5
5
  import {
6
6
  CCIPChainFamilyUnsupportedError,
7
7
  CCIPMessageBatchIncompleteError,
@@ -197,6 +197,7 @@ export function decodeMessage(data: string | Uint8Array | Record<string, unknown
197
197
  */
198
198
  export function buildMessageForDest(message: MessageInput, dest: ChainFamily): AnyMessage {
199
199
  const chain = supportedChains[dest] ?? Chain
200
+ if (message.extraArgs && '_tag' in message.extraArgs) delete message.extraArgs._tag
200
201
  return chain.buildMessageForDest(message)
201
202
  }
202
203
 
@@ -392,64 +393,6 @@ export async function getMessagesInBatch<
392
393
  return messages
393
394
  }
394
395
 
395
- /**
396
- * Fetches CCIP requests originated by a specific sender.
397
- * @param source - Source chain instance.
398
- * @param sender - Sender address.
399
- * @param filter - Log filter options.
400
- * @returns Async generator of CCIP requests.
401
- * @throws {@link CCIPChainFamilyUnsupportedError} if chain family not supported for legacy messages
402
- *
403
- * @example
404
- * ```typescript
405
- * import { getMessagesForSender, EVMChain } from '@chainlink/ccip-sdk'
406
- *
407
- * const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')
408
- *
409
- * for await (const request of getMessagesForSender(chain, '0xSenderAddress', {})) {
410
- * console.log('Message ID:', request.message.messageId)
411
- * console.log('Destination:', request.lane.destChainSelector)
412
- * }
413
- * ```
414
- *
415
- * @see {@link getMessagesInTx} - Fetch from specific transaction
416
- * @see {@link getMessageById} - Search by messageId
417
- */
418
- export async function* getMessagesForSender(
419
- source: Chain,
420
- sender: string,
421
- filter: Pick<LogFilter, 'address' | 'startBlock' | 'startTime' | 'endBlock'>,
422
- ): AsyncGenerator<Omit<CCIPRequest, 'tx' | 'timestamp'>, void, unknown> {
423
- const filterWithSender = {
424
- ...filter,
425
- sender, // some chain families may use this to look for account lookup/narrow down the search
426
- topics: ['CCIPSendRequested', 'CCIPMessageSent'],
427
- }
428
- for await (const log of source.getLogs(filterWithSender)) {
429
- const message = (source.constructor as ChainStatic).decodeMessage(log)
430
- if (message?.sender !== sender) continue
431
- let destChainSelector, version
432
- if ('destChainSelector' in message) {
433
- destChainSelector = message.destChainSelector
434
- ;[, version] = await source.typeAndVersion(log.address)
435
- } else if (source.network.family === ChainFamily.EVM) {
436
- ;({ destChainSelector, version } = await (source as EVMChain).getLaneForOnRamp(log.address))
437
- } else {
438
- throw new CCIPChainFamilyUnsupportedError(source.network.family)
439
- }
440
- yield {
441
- lane: {
442
- sourceChainSelector: source.network.chainSelector,
443
- destChainSelector,
444
- onRamp: log.address,
445
- version: version as CCIPVersion,
446
- },
447
- message,
448
- log,
449
- }
450
- }
451
- }
452
-
453
396
  /**
454
397
  * Map source token to its pool address and destination token address.
455
398
  *
@@ -35,6 +35,7 @@ import {
35
35
  type LogFilter,
36
36
  type TokenInfo,
37
37
  type TokenPoolRemote,
38
+ type TokenTransferFeeOpts,
38
39
  Chain,
39
40
  } from '../chain.ts'
40
41
  import {
@@ -1366,7 +1367,10 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1366
1367
  * {@inheritDoc Chain.getTokenPoolConfig}
1367
1368
  * @throws {@link CCIPTokenPoolStateNotFoundError} if token pool state not found
1368
1369
  */
1369
- async getTokenPoolConfig(tokenPool: string): Promise<{
1370
+ async getTokenPoolConfig(
1371
+ tokenPool: string,
1372
+ _feeOpts?: TokenTransferFeeOpts,
1373
+ ): Promise<{
1370
1374
  token: string
1371
1375
  router: string
1372
1376
  tokenPoolProgram: string
@@ -1601,7 +1605,9 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1601
1605
  'accountIsWritableBitmap',
1602
1606
  ])
1603
1607
  if (message.extraArgs) {
1604
- const unknown = Object.keys(message.extraArgs).filter((k) => !SVM_EXTRA_ARGS_FIELDS.has(k))
1608
+ const unknown = Object.keys(message.extraArgs).filter(
1609
+ (k) => k !== '_tag' && !SVM_EXTRA_ARGS_FIELDS.has(k),
1610
+ )
1605
1611
  if (unknown.length)
1606
1612
  throw new CCIPArgumentInvalidError(
1607
1613
  'extraArgs',
package/src/sui/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  type ChainStatic,
13
13
  type GetBalanceOpts,
14
14
  type LogFilter,
15
+ type TokenTransferFeeOpts,
15
16
  Chain,
16
17
  } from '../chain.ts'
17
18
  import { getCcipStateAddress, getOffRampForCcip } from './discovery.ts'
@@ -793,7 +794,7 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
793
794
  }
794
795
 
795
796
  /** {@inheritDoc Chain.getTokenPoolConfig} */
796
- async getTokenPoolConfig(_tokenPool: string): Promise<never> {
797
+ async getTokenPoolConfig(_tokenPool: string, _feeOpts?: TokenTransferFeeOpts): Promise<never> {
797
798
  return Promise.reject(new CCIPNotImplementedError('SuiChain.getTokenPoolConfig'))
798
799
  }
799
800
 
@@ -826,7 +827,9 @@ export class SuiChain extends Chain<typeof ChainFamily.Sui> {
826
827
  'accounts', // alias for receiverObjectIds
827
828
  ])
828
829
  if (message.extraArgs) {
829
- const unknown = Object.keys(message.extraArgs).filter((k) => !SUI_EXTRA_ARGS_FIELDS.has(k))
830
+ const unknown = Object.keys(message.extraArgs).filter(
831
+ (k) => k !== '_tag' && !SUI_EXTRA_ARGS_FIELDS.has(k),
832
+ )
830
833
  if (unknown.length)
831
834
  throw new CCIPArgumentInvalidError(
832
835
  'extraArgs',