@chainlink/ccip-sdk 1.2.1 → 1.3.1

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 (108) 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 +19 -0
  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 +8 -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 +193 -7
  47. package/dist/evm/index.js.map +1 -1
  48. package/dist/index.d.ts +3 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/offchain.d.ts +27 -0
  53. package/dist/offchain.d.ts.map +1 -1
  54. package/dist/offchain.js +44 -2
  55. package/dist/offchain.js.map +1 -1
  56. package/dist/requests.d.ts +1 -25
  57. package/dist/requests.d.ts.map +1 -1
  58. package/dist/requests.js +0 -57
  59. package/dist/requests.js.map +1 -1
  60. package/dist/solana/index.d.ts +2 -2
  61. package/dist/solana/index.d.ts.map +1 -1
  62. package/dist/solana/index.js +6 -10
  63. package/dist/solana/index.js.map +1 -1
  64. package/dist/solana/utils.d.ts +1 -1
  65. package/dist/solana/utils.d.ts.map +1 -1
  66. package/dist/solana/utils.js +12 -14
  67. package/dist/solana/utils.js.map +1 -1
  68. package/dist/sui/index.d.ts +2 -2
  69. package/dist/sui/index.d.ts.map +1 -1
  70. package/dist/sui/index.js +1 -1
  71. package/dist/sui/index.js.map +1 -1
  72. package/dist/ton/index.d.ts +2 -2
  73. package/dist/ton/index.d.ts.map +1 -1
  74. package/dist/ton/index.js +28 -49
  75. package/dist/ton/index.js.map +1 -1
  76. package/dist/ton/send.d.ts +13 -1
  77. package/dist/ton/send.d.ts.map +1 -1
  78. package/dist/ton/send.js +16 -16
  79. package/dist/ton/send.js.map +1 -1
  80. package/dist/types.d.ts +1 -1
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/utils.d.ts +16 -0
  83. package/dist/utils.d.ts.map +1 -1
  84. package/dist/utils.js +38 -4
  85. package/dist/utils.js.map +1 -1
  86. package/package.json +4 -4
  87. package/src/api/index.ts +9 -23
  88. package/src/aptos/index.ts +5 -1
  89. package/src/chain.ts +85 -2
  90. package/src/errors/codes.ts +1 -0
  91. package/src/errors/index.ts +1 -0
  92. package/src/errors/recovery.ts +2 -0
  93. package/src/errors/specialized.ts +15 -0
  94. package/src/evm/abi/CCTPVerifier_2_0.ts +1146 -0
  95. package/src/evm/abi/USDCTokenPoolProxy_2_0.ts +872 -0
  96. package/src/evm/abi/VersionedVerifierResolver_2_0.ts +369 -0
  97. package/src/evm/const.ts +8 -0
  98. package/src/evm/index.ts +262 -8
  99. package/src/index.ts +6 -2
  100. package/src/offchain.ts +53 -1
  101. package/src/requests.ts +1 -59
  102. package/src/solana/index.ts +11 -11
  103. package/src/solana/utils.ts +24 -17
  104. package/src/sui/index.ts +2 -1
  105. package/src/ton/index.ts +41 -56
  106. package/src/ton/send.ts +20 -21
  107. package/src/types.ts +1 -1
  108. package/src/utils.ts +52 -4
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
  /**
@@ -1018,6 +1027,210 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
1018
1027
  })
1019
1028
  }
1020
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
+
1021
1234
  /**
1022
1235
  * Generates unsigned EVM transactions for sending a CCIP message.
1023
1236
  *
@@ -1426,44 +1639,84 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
1426
1639
  * Fetches the token pool configuration for an EVM token pool contract.
1427
1640
  *
1428
1641
  * @param tokenPool - Token pool contract address.
1429
- * @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.
1430
1645
  *
1431
1646
  * @remarks
1432
1647
  * For pools with version \>= 2.0, also returns `minBlockConfirmations` for
1433
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.
1434
1650
  */
1435
- async getTokenPoolConfig(tokenPool: string): Promise<{
1651
+ async getTokenPoolConfig(
1652
+ tokenPool: string,
1653
+ feeOpts?: TokenTransferFeeOpts,
1654
+ ): Promise<{
1436
1655
  token: string
1437
1656
  router: string
1438
1657
  typeAndVersion: string
1439
1658
  minBlockConfirmations?: number
1659
+ tokenTransferFeeConfig?: TokenTransferFeeConfig
1440
1660
  }> {
1441
1661
  const [_, version, typeAndVersion] = await this.typeAndVersion(tokenPool)
1442
1662
 
1443
- let contract, router, minBlockConfirmations
1663
+ let token, router, minBlockConfirmations, tokenTransferFeeConfig
1444
1664
  if (version < CCIPVersion.V2_0) {
1445
- contract = new Contract(
1665
+ const contract = new Contract(
1446
1666
  tokenPool,
1447
1667
  interfaces.TokenPool_v1_6,
1448
1668
  this.provider,
1449
1669
  ) as unknown as TypedContract<typeof TokenPool_ABI>
1670
+ token = contract.getToken()
1450
1671
  router = contract.getRouter()
1451
1672
  } else {
1452
- contract = new Contract(
1673
+ const contract = new Contract(
1453
1674
  tokenPool,
1454
1675
  interfaces.TokenPool_v2_0,
1455
1676
  this.provider,
1456
1677
  ) as unknown as TypedContract<typeof TokenPool_2_0_ABI>
1678
+ token = contract.getToken()
1457
1679
  router = contract.getDynamicConfig().then(([router]) => router)
1458
1680
  minBlockConfirmations = contract.getMinBlockConfirmations().catch((err) => {
1459
1681
  if (isError(err, 'CALL_EXCEPTION')) return 0
1460
1682
  throw CCIPError.from(err)
1461
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
+ }
1462
1716
  }
1463
- const token = contract.getToken()
1464
1717
 
1465
- return Promise.all([token, router, minBlockConfirmations]).then(
1466
- ([token, router, minBlockConfirmations]) => {
1718
+ return Promise.all([token, router, minBlockConfirmations, tokenTransferFeeConfig]).then(
1719
+ ([token, router, minBlockConfirmations, tokenTransferFeeConfig]) => {
1467
1720
  return {
1468
1721
  token: token as CleanAddressable<typeof token>,
1469
1722
  router: router as CleanAddressable<typeof router>,
@@ -1471,6 +1724,7 @@ export class EVMChain extends Chain<typeof ChainFamily.EVM> {
1471
1724
  ...(minBlockConfirmations != null && {
1472
1725
  minBlockConfirmations: Number(minBlockConfirmations),
1473
1726
  }),
1727
+ ...(tokenTransferFeeConfig != null && { tokenTransferFeeConfig }),
1474
1728
  }
1475
1729
  },
1476
1730
  )
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
@@ -5,10 +5,11 @@ 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 { getDataBytes, 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',
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,
@@ -393,64 +393,6 @@ export async function getMessagesInBatch<
393
393
  return messages
394
394
  }
395
395
 
396
- /**
397
- * Fetches CCIP requests originated by a specific sender.
398
- * @param source - Source chain instance.
399
- * @param sender - Sender address.
400
- * @param filter - Log filter options.
401
- * @returns Async generator of CCIP requests.
402
- * @throws {@link CCIPChainFamilyUnsupportedError} if chain family not supported for legacy messages
403
- *
404
- * @example
405
- * ```typescript
406
- * import { getMessagesForSender, EVMChain } from '@chainlink/ccip-sdk'
407
- *
408
- * const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')
409
- *
410
- * for await (const request of getMessagesForSender(chain, '0xSenderAddress', {})) {
411
- * console.log('Message ID:', request.message.messageId)
412
- * console.log('Destination:', request.lane.destChainSelector)
413
- * }
414
- * ```
415
- *
416
- * @see {@link getMessagesInTx} - Fetch from specific transaction
417
- * @see {@link getMessageById} - Search by messageId
418
- */
419
- export async function* getMessagesForSender(
420
- source: Chain,
421
- sender: string,
422
- filter: Pick<LogFilter, 'address' | 'startBlock' | 'startTime' | 'endBlock'>,
423
- ): AsyncGenerator<Omit<CCIPRequest, 'tx' | 'timestamp'>, void, unknown> {
424
- const filterWithSender = {
425
- ...filter,
426
- sender, // some chain families may use this to look for account lookup/narrow down the search
427
- topics: ['CCIPSendRequested', 'CCIPMessageSent'],
428
- }
429
- for await (const log of source.getLogs(filterWithSender)) {
430
- const message = (source.constructor as ChainStatic).decodeMessage(log)
431
- if (message?.sender !== sender) continue
432
- let destChainSelector, version
433
- if ('destChainSelector' in message) {
434
- destChainSelector = message.destChainSelector
435
- ;[, version] = await source.typeAndVersion(log.address)
436
- } else if (source.network.family === ChainFamily.EVM) {
437
- ;({ destChainSelector, version } = await (source as EVMChain).getLaneForOnRamp(log.address))
438
- } else {
439
- throw new CCIPChainFamilyUnsupportedError(source.network.family)
440
- }
441
- yield {
442
- lane: {
443
- sourceChainSelector: source.network.chainSelector,
444
- destChainSelector,
445
- onRamp: log.address,
446
- version: version as CCIPVersion,
447
- },
448
- message,
449
- log,
450
- }
451
- }
452
- }
453
-
454
396
  /**
455
397
  * Map source token to its pool address and destination token address.
456
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
@@ -1641,15 +1645,12 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1641
1645
  message.extraArgs.allowOutOfOrderExecution != null
1642
1646
  ? message.extraArgs.allowOutOfOrderExecution
1643
1647
  : true
1644
- const tokenReceiver =
1645
- message.extraArgs &&
1646
- 'tokenReceiver' in message.extraArgs &&
1647
- message.extraArgs.tokenReceiver != null &&
1648
- typeof message.extraArgs.tokenReceiver === 'string'
1649
- ? message.extraArgs.tokenReceiver
1648
+ const [tokenReceiver, receiver] =
1649
+ message.extraArgs && 'tokenReceiver' in message.extraArgs && !!message.extraArgs.tokenReceiver
1650
+ ? [message.extraArgs.tokenReceiver, message.receiver] // explicit tokenReceiver, keep both
1650
1651
  : message.tokenAmounts?.length
1651
- ? this.getAddress(message.receiver)
1652
- : PublicKey.default.toBase58()
1652
+ ? [this.getAddress(message.receiver), PublicKey.default.toBase58()] // if sending tokens without tokenReceiver, set receiver to default and tokenReceiver to message.receiver
1653
+ : [PublicKey.default.toBase58(), message.receiver] // otherwise, tokenReceiver is default and receiver is message.receiver
1653
1654
  const accounts =
1654
1655
  message.extraArgs && 'accounts' in message.extraArgs && message.extraArgs.accounts != null
1655
1656
  ? message.extraArgs.accounts
@@ -1671,9 +1672,8 @@ export class SolanaChain extends Chain<typeof ChainFamily.Solana> {
1671
1672
 
1672
1673
  return {
1673
1674
  ...message,
1675
+ receiver,
1674
1676
  extraArgs,
1675
- // if tokenReceiver, then message.receiver can (must?) be default
1676
- ...(!!message.tokenAmounts?.length && { receiver: PublicKey.default.toBase58() }),
1677
1677
  }
1678
1678
  }
1679
1679
  }
@@ -27,7 +27,7 @@ import {
27
27
  CCIPTransactionNotFinalizedError,
28
28
  } from '../errors/index.ts'
29
29
  import type { ChainLog, WithLogger } from '../types.ts'
30
- import { bigIntReplacer, getDataBytes, sleep } from '../utils.ts'
30
+ import { bigIntReplacer, getDataBytes, isBase64, sleep } from '../utils.ts'
31
31
  import type { IDL as BASE_TOKEN_POOL_IDL } from './idl/1.6.0/BASE_TOKEN_POOL.ts'
32
32
  import type { UnsignedSolanaTx, Wallet } from './types.ts'
33
33
  import type { RateLimiterState } from '../chain.ts'
@@ -217,7 +217,7 @@ export function parseSolanaLogs(logs: readonly string[]): ParsedLog[] {
217
217
  export function getErrorFromLogs(
218
218
  logs_:
219
219
  | readonly string[]
220
- | readonly Pick<ChainLog, 'address' | 'index' | 'data' | 'topics'>[]
220
+ | readonly Pick<ChainLog, 'address' | 'index' | 'data' | 'topics' | 'tx'>[]
221
221
  | null,
222
222
  ): { program: string; [k: string]: string } | undefined {
223
223
  if (!logs_?.length) return
@@ -234,12 +234,13 @@ export function getErrorFromLogs(
234
234
  !acc.length || (l.address === acc[0]!.address && !l.topics.length) ? [l, ...acc] : acc,
235
235
  [] as Pick<ChainLog, 'address' | 'index' | 'data'>[],
236
236
  )
237
+ .filter(({ data }) => !isBase64(data))
237
238
  .map(({ data }) => data as string)
238
239
  .reduceRight(
239
240
  (acc, l) =>
240
241
  l.endsWith(':') && acc.length
241
242
  ? [`${l} ${acc[0]}`, ...acc.slice(1)]
242
- : l.split(': ').length > 1 && l.split('. ').length > 1
243
+ : l.indexOf(': ') >= 0 && l.indexOf('. ') >= 0
243
244
  ? [...l.replace(/\.$/, '').split('. '), ...acc]
244
245
  : [l, ...acc],
245
246
  [] as string[],
@@ -261,22 +262,28 @@ export function getErrorFromLogs(
261
262
  return l
262
263
  }
263
264
  })
264
- if (lastProgramLogs.every((l) => l.indexOf(': ') >= 0)) {
265
- return {
266
- program: lastLog.address,
267
- ...Object.fromEntries(
268
- lastProgramLogs.map((l) => [
269
- l.substring(0, l.indexOf(': ')),
270
- l.substring(l.indexOf(': ') + 2),
271
- ]),
272
- ),
273
- }
265
+
266
+ const res: { program: string; [k: string]: string } = {
267
+ program: lastLog.address,
268
+ }
269
+ if (lastProgramLogs.every((l) => l.indexOf(': ') >= 0 || l.indexOf(' in ') >= 0)) {
270
+ Object.assign(
271
+ res,
272
+ Object.fromEntries(lastProgramLogs.map((l) => l.split(/: | in /, 2) as [string, string])),
273
+ )
274
274
  } else {
275
- return {
276
- program: lastLog.address,
277
- error: lastProgramLogs.join('\n'),
278
- }
275
+ res['error'] = lastProgramLogs.join('\n')
279
276
  }
277
+ if (!!logs[0] && 'tx' in logs[0] && !!logs[0].tx?.error)
278
+ Object.assign(
279
+ res,
280
+ Object.fromEntries(
281
+ Object.entries(logs[0].tx.error as Record<string, [number, string]>).map(
282
+ ([k, [i, e]]) => [`${k}[${i}]`, e] as const,
283
+ ),
284
+ ),
285
+ )
286
+ return res
280
287
  }
281
288
 
282
289
  /**
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