0xtrails 0.6.3 → 0.6.5

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 (51) hide show
  1. package/dist/{ccip-Ct6RMeeG.js → ccip-BlQRn0i5.js} +1 -1
  2. package/dist/{index-27ebsG0R.js → index-BsEaWwhF.js} +21694 -21333
  3. package/dist/index.js +118 -115
  4. package/dist/prepareSend.d.ts.map +1 -1
  5. package/dist/relaySdk.d.ts.map +1 -1
  6. package/dist/tokens.d.ts.map +1 -1
  7. package/dist/transactionIntent/handlers/crossChain.d.ts +2 -1
  8. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  9. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts +2 -1
  10. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  11. package/dist/transactionIntent/quote/quoteHelpers.d.ts +1 -1
  12. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  13. package/dist/transactionIntent/types.d.ts +1 -0
  14. package/dist/transactionIntent/types.d.ts.map +1 -1
  15. package/dist/utils.d.ts +8 -0
  16. package/dist/utils.d.ts.map +1 -1
  17. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  18. package/dist/widget/components/FeeOption.d.ts +6 -1
  19. package/dist/widget/components/FeeOption.d.ts.map +1 -1
  20. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  21. package/dist/widget/components/Fund.d.ts.map +1 -1
  22. package/dist/widget/components/Pay.d.ts.map +1 -1
  23. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  24. package/dist/widget/css/compiled.css +1 -1
  25. package/dist/widget/hooks/useQuote.d.ts +2 -1
  26. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  27. package/dist/widget/hooks/useSendForm.d.ts +0 -1
  28. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  29. package/dist/widget/index.js +1 -1
  30. package/dist/widget/widget.d.ts +1 -0
  31. package/dist/widget/widget.d.ts.map +1 -1
  32. package/package.json +2 -2
  33. package/src/prepareSend.ts +23 -5
  34. package/src/relaySdk.ts +2 -0
  35. package/src/tokens.ts +52 -4
  36. package/src/transactionIntent/deposits/gaslessDeposit.ts +2 -2
  37. package/src/transactionIntent/handlers/crossChain.ts +3 -0
  38. package/src/transactionIntent/handlers/sameChainSameToken.ts +315 -1
  39. package/src/transactionIntent/quote/quoteHelpers.ts +7 -2
  40. package/src/transactionIntent/types.ts +1 -0
  41. package/src/utils.ts +15 -0
  42. package/src/widget/compiled.css +1 -1
  43. package/src/widget/components/ClassicSwap.tsx +51 -6
  44. package/src/widget/components/FeeOption.tsx +55 -38
  45. package/src/widget/components/FeeOptions.tsx +57 -10
  46. package/src/widget/components/Fund.tsx +0 -4
  47. package/src/widget/components/Pay.tsx +23 -8
  48. package/src/widget/components/PoolDeposit.tsx +10 -1
  49. package/src/widget/hooks/useQuote.ts +4 -0
  50. package/src/widget/hooks/useSendForm.ts +71 -36
  51. package/src/widget/widget.tsx +1 -0
@@ -1,7 +1,7 @@
1
1
  import { abortControllerRegistry } from "./abortController.js"
2
2
  import { trackPaymentError, trackPaymentStarted } from "./analytics.js"
3
3
  import { getSlippageToleranceValue } from "./widget/components/SlippageToleranceSettings.js"
4
- import { isNativeToken } from "./utils.js"
4
+ import { isNativeToken, isZeroAccount } from "./utils.js"
5
5
  import { getERC20TransferData } from "./encoders.js"
6
6
  import { getTokenPrice } from "./prices.js"
7
7
  import {
@@ -85,6 +85,7 @@ export async function prepareSend(
85
85
  executeIntentFn,
86
86
  sequenceProjectAccessKey,
87
87
  sequenceIndexerUrl,
88
+ isSmartWallet,
88
89
  } = options
89
90
  let { sourceTokenPriceUsd, destinationTokenPriceUsd } = options
90
91
 
@@ -134,7 +135,7 @@ export async function prepareSend(
134
135
  const transactionStates: TransactionState[] = []
135
136
 
136
137
  // Validate recipient is not zero address
137
- if (isNativeToken(recipient)) {
138
+ if (isZeroAccount(recipient)) {
138
139
  throw new Error("Recipient address cannot be zero address")
139
140
  }
140
141
 
@@ -148,10 +149,16 @@ export async function prepareSend(
148
149
  let effectiveDestinationAddress = recipient
149
150
  let effectiveDestinationCalldata = destinationCalldata
150
151
 
152
+ // Check if this is a same-chain same-token transfer (before modifying effectiveDestinationAddress)
153
+ const isToSameChain = isSameChain(originChainId, destinationChainId)
154
+ const isToSameToken = isSameToken(originTokenAddress, destinationTokenAddress)
155
+ const isSameChainSameToken = isToSameChain && isToSameToken
156
+
151
157
  if (
152
158
  !hasCustomCalldata &&
153
159
  tradeType === TradeType.EXACT_INPUT &&
154
- !isNativeToken(destinationTokenAddress)
160
+ !isNativeToken(destinationTokenAddress) &&
161
+ !isSameChainSameToken // Don't override recipient for same-chain same-token transfers
155
162
  ) {
156
163
  // we need to set custom calldata for the cctp transfer in order to have destination intent adddress execution needed for metatxn tracking
157
164
  effectiveDestinationCalldata = getERC20TransferData({
@@ -291,8 +298,6 @@ export async function prepareSend(
291
298
  }
292
299
  const isToSelf =
293
300
  account.address.toLowerCase() === effectiveDestinationAddress.toLowerCase()
294
- const isToSameChain = isSameChain(originChainId, destinationChainId)
295
- const isToSameToken = isSameToken(originTokenAddress, destinationTokenAddress)
296
301
 
297
302
  logger.console.log("[trails-sdk] isToSameChain", isToSameChain)
298
303
  logger.console.log("[trails-sdk] isToSameToken", isToSameToken)
@@ -380,6 +385,17 @@ export async function prepareSend(
380
385
  // }
381
386
 
382
387
  if (isToSameToken && isToSameChain) {
388
+ logger.console.log(
389
+ "[trails-sdk] Same-chain same-token detected, using handleSameChainSameToken:",
390
+ {
391
+ recipient: effectiveDestinationAddress,
392
+ originalRecipient: recipient,
393
+ accountAddress: account.address,
394
+ originTokenAddress,
395
+ destinationTokenAddress,
396
+ },
397
+ )
398
+
383
399
  return await handleSameChainSameToken({
384
400
  mainSignerAddress,
385
401
  originTokenAddress,
@@ -412,6 +428,7 @@ export async function prepareSend(
412
428
  executeIntentFn,
413
429
  sequenceProjectAccessKey,
414
430
  sequenceIndexerUrl,
431
+ isSmartWallet,
415
432
  })
416
433
  }
417
434
 
@@ -455,6 +472,7 @@ export async function prepareSend(
455
472
  executeIntentFn,
456
473
  sequenceProjectAccessKey,
457
474
  sequenceIndexerUrl,
475
+ isSmartWallet,
458
476
  })
459
477
  }
460
478
 
package/src/relaySdk.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  berachain,
13
13
  blast,
14
14
  opBNB,
15
+ bsc,
15
16
  bob,
16
17
  boba,
17
18
  celo,
@@ -90,6 +91,7 @@ export const relaySupportedChains: Record<number, Chain> = {
90
91
  [base.id]: base,
91
92
  [berachain.id]: berachain,
92
93
  [blast.id]: blast,
94
+ [bsc.id]: bsc,
93
95
  [opBNB.id]: opBNB,
94
96
  [bob.id]: bob,
95
97
  [boba.id]: boba,
package/src/tokens.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  linea,
17
17
  unichain,
18
18
  worldchain,
19
+ bsc,
19
20
  } from "viem/chains"
20
21
  import { getRelaySupportedTokens, type Chain } from "./relaySdk.js"
21
22
  import { useReadContracts } from "wagmi"
@@ -49,13 +50,13 @@ export const commonTokenImages: Record<string, string> = {
49
50
  ARB: "https://assets.sequence.info/images/tokens/large/42161/0x912ce59144191c1204e64559fe8253a0e49e6548.webp",
50
51
  LINK: "https://assets.sequence.info/images/tokens/large/1/0x514910771af9ca656af840dff83e8264ecf986ca.webp",
51
52
  XTZ: "https://assets.sequence.info/images/tokens/large/42793/0x0000000000000000000000000000000000000000.webp",
53
+ BNB: "https://assets.sequence.info/images/tokens/large/56/0x0000000000000000000000000000000000000000.webp",
52
54
  WXTZ: "https://assets.coingecko.com/coins/images/976/standard/Tezos-logo.png?1696502091",
53
55
  SOMI: "https://assets.sequence.info/images/tokens/large/5031/0x0000000000000000000000000000000000000000.webp",
54
56
  XAI: "https://assets.coingecko.com/coins/images/34258/large/round_icon_2048_px.png?1719523838",
55
57
  APE: "https://assets.sequence.info/images/tokens/large/33139/0x0000000000000000000000000000000000000000.webp",
56
58
  ANIME:
57
59
  "https://assets.coingecko.com/coins/images/53575/large/anime.jpg?1736748703",
58
- BNB: "https://assets.sequence.info/images/tokens/large/56/0x0000000000000000000000000000000000000000.webp",
59
60
  AVAX: "https://assets.sequence.info/images/tokens/large/43114/0x0000000000000000000000000000000000000000.webp",
60
61
  }
61
62
 
@@ -546,16 +547,43 @@ export async function getTokenInfo(
546
547
  }
547
548
  }
548
549
 
550
+ // Mapping of hyphenated symbols to their canonical token symbols.
551
+ // The Sequence API returns BNB-USD, but we want to use USDT instead.
552
+ const HYPHENATED_SYMBOL_MAP: Record<string, string> = {
553
+ "BNB-USD": "USDT",
554
+ }
555
+
549
556
  export async function getTokenAddress(chainId: number, tokenSymbol: string) {
550
557
  const chainInfo = getChainInfo(chainId)
551
- if (tokenSymbol === chainInfo?.nativeCurrency.symbol) {
558
+ // Normalize token symbol: trim whitespace and convert to uppercase for comparison
559
+ const normalizedTokenSymbol = tokenSymbol.trim().toUpperCase()
560
+
561
+ const nativeSymbol = (chainInfo as any)?.nativeCurrency?.symbol
562
+ if (nativeSymbol && normalizedTokenSymbol === nativeSymbol.toUpperCase()) {
552
563
  return zeroAddress
553
564
  }
554
565
 
555
566
  const tokens = await getSupportedTokens()
556
- const token = tokens.find(
557
- (t) => t.symbol === tokenSymbol && t.chainId === chainId,
567
+
568
+ // First try exact match (normalized)
569
+ let token = tokens.find(
570
+ (t) =>
571
+ t.symbol.trim().toUpperCase() === normalizedTokenSymbol &&
572
+ t.chainId === chainId,
558
573
  )
574
+
575
+ // If not found, check if it's a hyphenated symbol that maps to another symbol
576
+ if (!token) {
577
+ const mappedSymbol = HYPHENATED_SYMBOL_MAP[normalizedTokenSymbol]
578
+ if (mappedSymbol) {
579
+ token = tokens.find(
580
+ (t) =>
581
+ t.symbol.trim().toUpperCase() === mappedSymbol.toUpperCase() &&
582
+ t.chainId === chainId,
583
+ )
584
+ }
585
+ }
586
+
559
587
  if (token?.contractAddress) {
560
588
  return token.contractAddress
561
589
  }
@@ -1159,6 +1187,26 @@ export const commonTokens: SupportedToken[] = [
1159
1187
  chainName: etherlink.name,
1160
1188
  imageUrl: commonTokenImages.USDT as string,
1161
1189
  },
1190
+ {
1191
+ id: "USDT-bsc",
1192
+ symbol: "USDT",
1193
+ name: "Tether USD",
1194
+ contractAddress: "0x55d398326f99059ff775485246999027b3197955",
1195
+ decimals: 18,
1196
+ chainId: bsc.id,
1197
+ chainName: bsc.name,
1198
+ imageUrl: commonTokenImages.USDT as string,
1199
+ },
1200
+ {
1201
+ id: "BNB-bsc",
1202
+ symbol: "BNB",
1203
+ name: "BNB",
1204
+ contractAddress: "0x0000000000000000000000000000000000000000",
1205
+ decimals: 18,
1206
+ chainId: bsc.id,
1207
+ chainName: bsc.name,
1208
+ imageUrl: commonTokenImages.BNB as string,
1209
+ },
1162
1210
 
1163
1211
  // Somnia
1164
1212
  {
@@ -10,7 +10,7 @@ import type {
10
10
  import type { CheckoutOnHandlers } from "../../widget/hooks/useCheckout.js"
11
11
  import type { TransactionState } from "../../transactions.js"
12
12
  import { logger } from "../../logger.js"
13
- import { isNativeToken } from "../../utils.js"
13
+ import { isNativeToken, isZeroAccount } from "../../utils.js"
14
14
  import { attemptSwitchChain } from "../../chainSwitch.js"
15
15
  import { getTransactionStateFromReceipt } from "../execution/transactionState.js"
16
16
  import {
@@ -386,7 +386,7 @@ export async function attemptGaslessDeposit({
386
386
  | undefined
387
387
 
388
388
  // Validate that we have a valid fee collector address
389
- if (!feeCollectorAddress || isNativeToken(feeCollectorAddress)) {
389
+ if (!feeCollectorAddress || isZeroAccount(feeCollectorAddress)) {
390
390
  throw new Error(
391
391
  "[trails-sdk] Fee collector address not provided by API. Cannot proceed with gasless deposit. " +
392
392
  "Please ensure the API is returning feeCollectorAddress in the gasFeeOptions response.",
@@ -130,6 +130,7 @@ export async function handleCrossChain({
130
130
  executeIntentFn,
131
131
  sequenceProjectAccessKey,
132
132
  sequenceIndexerUrl,
133
+ isSmartWallet,
133
134
  }: {
134
135
  mainSignerAddress: string
135
136
  originChainId: number
@@ -174,6 +175,7 @@ export async function handleCrossChain({
174
175
  }) => Promise<ExecuteIntentResponse>
175
176
  sequenceProjectAccessKey?: string
176
177
  sequenceIndexerUrl?: string
178
+ isSmartWallet?: boolean
177
179
  }): Promise<PrepareSendReturn> {
178
180
  const salt = Date.now().toString()
179
181
 
@@ -193,6 +195,7 @@ export async function handleCrossChain({
193
195
  quoteProvider,
194
196
  undefined, // connector - not available in this context
195
197
  walletId, // walletId - use this to check for Sequence wallets
198
+ isSmartWallet, // isSmartWallet - force onlyNativeGasFee when true
196
199
  )
197
200
 
198
201
  logger.console.log(
@@ -24,6 +24,7 @@ import {
24
24
  buildSameChainSameTokenTransactionParams,
25
25
  quoteIntent,
26
26
  commitIntent,
27
+ sendOriginTransaction,
27
28
  } from "../../intents.js"
28
29
  import { estimateGasLimit } from "../../estimate.js"
29
30
  import { getNormalizedQuoteObject } from "../quote/normalizeQuote.js"
@@ -31,7 +32,11 @@ import { attemptSwitchChain } from "../../chainSwitch.js"
31
32
  import { attemptUserDepositTx } from "../deposits/depositOrchestrator.js"
32
33
  import { getIntentArgs } from "../quote/quoteHelpers.js"
33
34
  import { TradeType } from "../types.js"
34
- import { trackPaymentCompleted, trackPaymentError } from "../../analytics.js"
35
+ import {
36
+ trackPaymentCompleted,
37
+ trackPaymentError,
38
+ trackTransactionConfirmed,
39
+ } from "../../analytics.js"
35
40
  import { updatePersistentToast } from "../../toast.js"
36
41
  import { getChainInfo } from "../../chains.js"
37
42
  import { getTransactionStateFromReceipt } from "../execution/transactionState.js"
@@ -72,6 +77,7 @@ export async function handleSameChainSameToken({
72
77
  executeIntentFn,
73
78
  sequenceProjectAccessKey,
74
79
  sequenceIndexerUrl,
80
+ isSmartWallet,
75
81
  }: {
76
82
  mainSignerAddress: string
77
83
  originTokenAddress: string
@@ -108,6 +114,7 @@ export async function handleSameChainSameToken({
108
114
  }) => Promise<ExecuteIntentResponse>
109
115
  sequenceProjectAccessKey?: string
110
116
  sequenceIndexerUrl?: string
117
+ isSmartWallet?: boolean
111
118
  }): Promise<PrepareSendReturn> {
112
119
  logger.console.log("[trails-sdk] isToSameToken && isToSameChain")
113
120
  const testnet = isTestnetDebugMode()
@@ -133,6 +140,301 @@ export async function handleSameChainSameToken({
133
140
 
134
141
  const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
135
142
 
143
+ // Check if this is a native token transfer - if so, skip intents and do direct transfer
144
+ const isNative = isNativeToken(effectiveOriginTokenAddress)
145
+
146
+ if (isNative) {
147
+ logger.console.log(
148
+ "[trails-sdk] Same-chain same-token native transfer detected, using direct transfer (no intents)",
149
+ {
150
+ originChainId: effectiveOriginChainId,
151
+ tokenAddress: effectiveOriginTokenAddress,
152
+ amount: swapAmount,
153
+ recipient,
154
+ hasCustomCalldata,
155
+ },
156
+ )
157
+
158
+ // Build transaction params for direct native transfer
159
+ const originCallParamsBase = buildSameChainSameTokenTransactionParams({
160
+ hasCustomCalldata,
161
+ recipient,
162
+ effectiveOriginTokenAddress,
163
+ destinationCalldata,
164
+ swapAmount,
165
+ effectiveOriginChainId,
166
+ effectiveOriginChain,
167
+ })
168
+
169
+ // Estimate gas limit
170
+ const estimatedGasLimitForQuote = await estimateGasLimit(
171
+ effectivePublicClient,
172
+ {
173
+ account: account.address,
174
+ to: originCallParamsBase.to,
175
+ data: originCallParamsBase.data,
176
+ value: BigInt(originCallParamsBase.value),
177
+ },
178
+ "quote",
179
+ )
180
+
181
+ logger.console.log(
182
+ "[trails-sdk][gas-estimation] Estimated gas limit for direct native transfer:",
183
+ estimatedGasLimitForQuote,
184
+ )
185
+
186
+ // Create minimal quote without intent addresses (using recipient as both addresses)
187
+ const quote = await getNormalizedQuoteObject({
188
+ originDepositAddress: recipient, // Direct transfer, no intent address
189
+ destinationDepositAddress: recipient, // Direct transfer, no intent address
190
+ destinationAddress: recipient,
191
+ destinationCalldata,
192
+ originAmount: swapAmount,
193
+ destinationAmount: swapAmount,
194
+ originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
195
+ destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
196
+ originTokenAddress: effectiveOriginTokenAddress,
197
+ destinationTokenAddress: effectiveOriginTokenAddress,
198
+ transactionStates,
199
+ originChainId: effectiveOriginChainId,
200
+ destinationChainId: effectiveOriginChainId,
201
+ originNativeTokenPriceUsd,
202
+ slippageTolerance,
203
+ quoteProvider: quoteProvider || "direct",
204
+ noSufficientBalance,
205
+ estimatedGasLimit: estimatedGasLimitForQuote,
206
+ // No intent for direct transfers
207
+ })
208
+
209
+ // Call onCheckoutQuote callback if provided
210
+ if (checkoutOnHandlers?.triggerCheckoutQuote) {
211
+ checkoutOnHandlers.triggerCheckoutQuote(quote)
212
+ }
213
+
214
+ // Return PrepareSendReturn with direct send function
215
+ return {
216
+ quote,
217
+ feeOptions: undefined, // No fee options for direct native transfers
218
+ send: async ({
219
+ onOriginSend,
220
+ }: {
221
+ onOriginSend?: () => void
222
+ selectedFeeOption?: FeeOption | null
223
+ }): Promise<SendReturn> => {
224
+ try {
225
+ // Check balance before sending
226
+ const { hasEnoughBalance, balanceError } = await checkAccountBalance({
227
+ account,
228
+ tokenAddress: effectiveOriginTokenAddress,
229
+ depositAmount: swapAmount,
230
+ publicClient: effectivePublicClient,
231
+ })
232
+
233
+ if (!hasEnoughBalance) {
234
+ throw balanceError
235
+ }
236
+
237
+ const depositAmountFormatted = Number(
238
+ formatUnits(BigInt(swapAmount), originTokenDecimals),
239
+ )
240
+ const depositAmountUsd = calcAmountUsdPrice({
241
+ amount: depositAmountFormatted,
242
+ usdPrice: sourceTokenPriceUsd,
243
+ })
244
+
245
+ // Build transaction params
246
+ logger.console.log(
247
+ "[trails-sdk] Building same-chain same-token transaction params:",
248
+ {
249
+ recipient,
250
+ hasCustomCalldata,
251
+ effectiveOriginTokenAddress,
252
+ swapAmount,
253
+ accountAddress: account.address,
254
+ },
255
+ )
256
+
257
+ const originCallParams = buildSameChainSameTokenTransactionParams({
258
+ hasCustomCalldata,
259
+ recipient,
260
+ effectiveOriginTokenAddress,
261
+ destinationCalldata,
262
+ swapAmount,
263
+ effectiveOriginChainId,
264
+ effectiveOriginChain,
265
+ })
266
+
267
+ logger.console.log(
268
+ "[trails-sdk] origin call params",
269
+ originCallParams,
270
+ )
271
+
272
+ let originUserTxReceipt: TransactionReceipt | null = null
273
+
274
+ if (!dryMode) {
275
+ // Update transaction state to pending
276
+ try {
277
+ onTransactionStateChange([
278
+ {
279
+ transactionHash: "",
280
+ explorerUrl: "",
281
+ chainId: effectiveOriginChainId,
282
+ state: "pending",
283
+ label: "Execute",
284
+ },
285
+ ])
286
+ } catch (error) {
287
+ logger.console.error(
288
+ "[trails-sdk] Error calling onTransactionStateChange:",
289
+ error,
290
+ )
291
+ }
292
+
293
+ // Show persistent toast for checkout flow
294
+ updatePersistentToast(
295
+ "Payment Started",
296
+ "Waiting for wallet confirmation...",
297
+ "info",
298
+ )
299
+
300
+ logger.console.log(
301
+ "[trails-sdk] origin call params",
302
+ originCallParams,
303
+ )
304
+
305
+ // Use sendOriginTransaction helper which handles gas estimation, fee boosting, and tracking
306
+ const txHash = await sendOriginTransaction(
307
+ account,
308
+ walletClient,
309
+ originCallParams as any,
310
+ {
311
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
312
+ },
313
+ )
314
+
315
+ logger.console.log("[trails-sdk] origin tx", txHash)
316
+
317
+ if (onOriginSend) {
318
+ onOriginSend()
319
+ }
320
+
321
+ // Wait for transaction receipt
322
+ const receipt =
323
+ await effectivePublicClient.waitForTransactionReceipt({
324
+ hash: txHash,
325
+ })
326
+ logger.console.log("[trails-sdk] receipt", receipt)
327
+ originUserTxReceipt = receipt
328
+
329
+ // Track transaction confirmation
330
+ trackTransactionConfirmed({
331
+ transactionHash: txHash,
332
+ chainId: effectiveOriginChainId,
333
+ userAddress: account.address,
334
+ blockNumber: Number(receipt.blockNumber),
335
+ originTokenAddress: effectiveOriginTokenAddress,
336
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
337
+ })
338
+
339
+ // Remove persistent toast and show success
340
+ const chainInfo = getChainInfo(effectiveOriginChainId)
341
+ updatePersistentToast(
342
+ "Transfer Confirmed",
343
+ `Your transaction on ${(chainInfo as any)?.name || "chain"} has been confirmed`,
344
+ "info",
345
+ )
346
+
347
+ // Update transaction state to completed
348
+ try {
349
+ onTransactionStateChange([
350
+ getTransactionStateFromReceipt(
351
+ originUserTxReceipt,
352
+ effectiveOriginChainId,
353
+ transactionStates[0]?.label || "Execute",
354
+ ),
355
+ ])
356
+ } catch (error) {
357
+ logger.console.error(
358
+ "[trails-sdk] Error calling onTransactionStateChange:",
359
+ error,
360
+ )
361
+ }
362
+
363
+ // Track payment completion for same-chain same-token transaction
364
+ if (
365
+ originUserTxReceipt &&
366
+ originUserTxReceipt.status === "success"
367
+ ) {
368
+ trackPaymentCompleted({
369
+ userAddress: account.address,
370
+ originTxHash: originUserTxReceipt.transactionHash,
371
+ originChainId: effectiveOriginChainId,
372
+ mode,
373
+ fundMethod,
374
+ originTokenAddress: effectiveOriginTokenAddress,
375
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
376
+ destinationTokenAmountUsd: depositAmountUsd?.toString(), // same as deposit amount
377
+ })
378
+
379
+ // Call onCheckoutComplete callback if provided
380
+ if (checkoutOnHandlers?.triggerCheckoutComplete) {
381
+ checkoutOnHandlers.triggerCheckoutComplete(
382
+ "success",
383
+ account.address,
384
+ )
385
+ }
386
+ } else if (originUserTxReceipt) {
387
+ trackPaymentError({
388
+ error: "Transaction failed",
389
+ userAddress: account.address,
390
+ mode,
391
+ fundMethod,
392
+ originTokenAddress: effectiveOriginTokenAddress,
393
+ })
394
+
395
+ // Call onCheckoutError callback if provided
396
+ if (checkoutOnHandlers?.triggerCheckoutError) {
397
+ checkoutOnHandlers.triggerCheckoutError("Transaction failed")
398
+ }
399
+ }
400
+ }
401
+
402
+ return {
403
+ depositUserTxnReceipt: originUserTxReceipt,
404
+ originIntentTransaction: null, // No intent for direct transfers
405
+ destinationIntentTransaction: null, // No intent for direct transfers
406
+ }
407
+ } catch (error) {
408
+ const errorMessage =
409
+ error instanceof Error
410
+ ? error.message
411
+ : "Unknown error occurred during transaction"
412
+ logger.console.error(
413
+ "[trails-sdk] Error in direct native transfer:",
414
+ error,
415
+ )
416
+
417
+ // Track payment error
418
+ trackPaymentError({
419
+ error: errorMessage,
420
+ userAddress: account.address,
421
+ mode,
422
+ fundMethod,
423
+ originTokenAddress: effectiveOriginTokenAddress,
424
+ })
425
+
426
+ // Call onCheckoutError callback if provided
427
+ if (checkoutOnHandlers?.triggerCheckoutError) {
428
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
429
+ }
430
+
431
+ // Re-throw the error so caller can handle if needed
432
+ throw error
433
+ }
434
+ },
435
+ }
436
+ }
437
+
136
438
  // For same-chain transactions, use Intent flow to support gasless deposits
137
439
  const salt = Date.now().toString()
138
440
  const intentArgs = await getIntentArgs(
@@ -151,6 +453,7 @@ export async function handleSameChainSameToken({
151
453
  quoteProvider || "",
152
454
  undefined, // connector - not available in this context
153
455
  walletId, // walletId - use this to check for Sequence wallets
456
+ isSmartWallet, // isSmartWallet - force onlyNativeGasFee when true
154
457
  )
155
458
 
156
459
  logger.console.log(
@@ -301,6 +604,17 @@ export async function handleSameChainSameToken({
301
604
  const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
302
605
 
303
606
  // Build origin call params (reusing the same logic as quote estimation)
607
+ logger.console.log(
608
+ "[trails-sdk] Building same-chain same-token transaction params (intent flow):",
609
+ {
610
+ recipient,
611
+ hasCustomCalldata,
612
+ effectiveOriginTokenAddress,
613
+ swapAmount,
614
+ accountAddress: account.address,
615
+ },
616
+ )
617
+
304
618
  const originCallParamsBase = buildSameChainSameTokenTransactionParams({
305
619
  hasCustomCalldata,
306
620
  recipient,
@@ -25,6 +25,7 @@ export async function getIntentArgs(
25
25
  provider?: string | null,
26
26
  connector?: Connector | undefined,
27
27
  walletId?: string | undefined,
28
+ isSmartWallet?: boolean,
28
29
  ): Promise<QuoteIntentRequest> {
29
30
  const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
30
31
 
@@ -44,12 +45,16 @@ export async function getIntentArgs(
44
45
  originChainId,
45
46
  )
46
47
 
47
- // Set onlyNativeGasFee to true if wallet is Sequence or smart contract
48
- const onlyNativeGasFee = isSequence || isSmartContract
48
+ // Set onlyNativeGasFee to true if:
49
+ // 1. isSmartWallet prop is explicitly set to true, OR
50
+ // 2. wallet is Sequence or smart contract
51
+ const onlyNativeGasFee =
52
+ isSmartWallet === true || isSequence || isSmartContract
49
53
 
50
54
  logger.console.log("[trails-sdk] Wallet check for onlyNativeGasFee:", {
51
55
  isSequence,
52
56
  isSmartContract,
57
+ isSmartWallet,
53
58
  onlyNativeGasFee,
54
59
  walletId,
55
60
  connectorName: connector?.name,
@@ -64,6 +64,7 @@ export type PrepareSendOptions = {
64
64
  selectedFeeOption?: FeeOption | null
65
65
  walletId?: string
66
66
  abortSignal?: AbortSignal
67
+ isSmartWallet?: boolean
67
68
  // Optional mutation callbacks for React Query integration
68
69
  commitIntentFn?: (intent: Intent) => Promise<CommitIntentResponse>
69
70
  executeIntentFn?: (params: {
package/src/utils.ts CHANGED
@@ -104,5 +104,20 @@ export function isNativeToken(
104
104
  return addresses.includes(normalizedAddress)
105
105
  }
106
106
 
107
+ /**
108
+ * Checks if an address is the zero address (0x0000...0000)
109
+ * Use this for recipient/account addresses, not for token addresses.
110
+ * For token addresses, use isNativeToken() instead.
111
+ * @param address - The address to check
112
+ * @returns True if the address is the zero address
113
+ */
114
+ export function isZeroAccount(address?: string | null): boolean {
115
+ if (!address) {
116
+ return false
117
+ }
118
+
119
+ return address.toLowerCase() === zeroAddress.toLowerCase()
120
+ }
121
+
107
122
  export const normalizeAddress = (address?: string | null): string =>
108
123
  (address ?? "").toLowerCase()