0xtrails 0.2.0 → 0.2.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 (113) hide show
  1. package/dist/analytics.d.ts +1 -0
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/{ccip-D6ToCrWc.js → ccip-BbfANth7.js} +1 -1
  4. package/dist/chains.d.ts.map +1 -1
  5. package/dist/config.d.ts +1 -2
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/constants.d.ts +2 -2
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/gasless.d.ts +19 -7
  10. package/dist/gasless.d.ts.map +1 -1
  11. package/dist/{index-BqgeTLL8.js → index-WpIVoh3X.js} +27626 -26572
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +68 -68
  15. package/dist/indexerClient.d.ts +10 -0
  16. package/dist/indexerClient.d.ts.map +1 -1
  17. package/dist/intentEntrypoint.d.ts +40 -14
  18. package/dist/intentEntrypoint.d.ts.map +1 -1
  19. package/dist/intents.d.ts.map +1 -1
  20. package/dist/prepareSend.d.ts +11 -8
  21. package/dist/prepareSend.d.ts.map +1 -1
  22. package/dist/relayer.d.ts.map +1 -1
  23. package/dist/trails.d.ts.map +1 -1
  24. package/dist/trailsClient.d.ts.map +1 -1
  25. package/dist/trailsRouter.d.ts +22 -0
  26. package/dist/trailsRouter.d.ts.map +1 -0
  27. package/dist/transactions.d.ts +0 -1
  28. package/dist/transactions.d.ts.map +1 -1
  29. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  30. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  31. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  32. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  33. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  34. package/dist/widget/components/Earn.d.ts.map +1 -1
  35. package/dist/widget/components/FeeOption.d.ts +22 -0
  36. package/dist/widget/components/FeeOption.d.ts.map +1 -0
  37. package/dist/widget/components/FeeOptions.d.ts +13 -17
  38. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  39. package/dist/widget/components/Fund.d.ts.map +1 -1
  40. package/dist/widget/components/FundMethods.d.ts +1 -1
  41. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  42. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  43. package/dist/widget/components/MeshConnectExchanges.d.ts +5 -2
  44. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  45. package/dist/widget/components/MeshConnectFlow.d.ts +2 -0
  46. package/dist/widget/components/MeshConnectFlow.d.ts.map +1 -1
  47. package/dist/widget/components/NativeGasOption.d.ts +12 -0
  48. package/dist/widget/components/NativeGasOption.d.ts.map +1 -0
  49. package/dist/widget/components/Pay.d.ts.map +1 -1
  50. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  51. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  52. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  53. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  54. package/dist/widget/hooks/useBack.d.ts +2 -0
  55. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  56. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  57. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  58. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  59. package/dist/widget/hooks/useSelectedFeeToken.d.ts +32 -0
  60. package/dist/widget/hooks/useSelectedFeeToken.d.ts.map +1 -0
  61. package/dist/widget/hooks/useSelectedMeshExchange.d.ts +14 -0
  62. package/dist/widget/hooks/useSelectedMeshExchange.d.ts.map +1 -0
  63. package/dist/widget/hooks/useSendForm.d.ts +8 -13
  64. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  65. package/dist/widget/index.js +1 -1
  66. package/dist/widget/widget.d.ts.map +1 -1
  67. package/package.json +29 -28
  68. package/src/analytics.ts +6 -0
  69. package/src/chains.ts +10 -0
  70. package/src/config.ts +25 -10
  71. package/src/constants.ts +11 -10
  72. package/src/gasless.ts +162 -109
  73. package/src/index.ts +1 -1
  74. package/src/indexerClient.ts +73 -1
  75. package/src/intentEntrypoint.ts +66 -101
  76. package/src/intents.ts +0 -2
  77. package/src/prepareSend.ts +1409 -887
  78. package/src/relayer.ts +4 -3
  79. package/src/trails.ts +1 -3
  80. package/src/trailsClient.ts +4 -1
  81. package/src/{balanceInjector.ts → trailsRouter.ts} +14 -14
  82. package/src/transactions.ts +4 -54
  83. package/src/widget/compiled.css +1 -1
  84. package/src/widget/components/AccountSettings.tsx +7 -1
  85. package/src/widget/components/ClassicSwap.tsx +173 -175
  86. package/src/widget/components/ConfigDisplay.tsx +34 -1
  87. package/src/widget/components/ConnectWallet.tsx +168 -11
  88. package/src/widget/components/ConnectedWallets.tsx +184 -102
  89. package/src/widget/components/DebugToast.tsx +3 -3
  90. package/src/widget/components/Earn.tsx +4 -27
  91. package/src/widget/components/FeeOption.tsx +78 -0
  92. package/src/widget/components/FeeOptions.tsx +192 -127
  93. package/src/widget/components/Fund.tsx +18 -27
  94. package/src/widget/components/FundMethods.tsx +3 -3
  95. package/src/widget/components/FundSendForm.tsx +0 -33
  96. package/src/widget/components/MeshConnectExchanges.tsx +32 -3
  97. package/src/widget/components/MeshConnectFlow.tsx +23 -4
  98. package/src/widget/components/NativeGasOption.tsx +99 -0
  99. package/src/widget/components/Pay.tsx +36 -32
  100. package/src/widget/components/PaySendForm.tsx +0 -37
  101. package/src/widget/components/QuoteDetails.tsx +0 -29
  102. package/src/widget/components/TokenSelector.tsx +11 -0
  103. package/src/widget/components/TransferPendingVertical.tsx +1 -1
  104. package/src/widget/components/UserPreferences.tsx +3 -4
  105. package/src/widget/hooks/useBack.tsx +4 -0
  106. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  107. package/src/widget/hooks/useDefaultTokenSelection.tsx +3 -7
  108. package/src/widget/hooks/useSelectedFeeToken.tsx +299 -0
  109. package/src/widget/hooks/useSelectedMeshExchange.tsx +46 -0
  110. package/src/widget/hooks/useSendForm.ts +78 -23
  111. package/src/widget/widget.tsx +173 -111
  112. package/dist/balanceInjector.d.ts +0 -22
  113. package/dist/balanceInjector.d.ts.map +0 -1
@@ -0,0 +1,299 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ type ReactNode,
9
+ } from "react"
10
+ import { logger } from "../../logger.js"
11
+ import { zeroAddress } from "viem"
12
+
13
+ // Define the FeeOption interface with balance check properties
14
+ export interface FeeOption {
15
+ tokenAddress: string
16
+ tokenSymbol: string
17
+ tokenDecimals: number
18
+ amount: string
19
+ amountUSD: number
20
+ notEnoughBalance?: boolean
21
+ tokenImageUrl?: string
22
+ chainId?: number
23
+ }
24
+
25
+ // Token type with balance information
26
+ export interface TokenWithBalance {
27
+ chainId: number
28
+ contractAddress?: string
29
+ balance?: string
30
+ imageUrl?: string
31
+ }
32
+
33
+ interface SelectedFeeTokenContextType {
34
+ selectedFeeToken: FeeOption | null
35
+ setSelectedFeeToken: (token: FeeOption | null) => void
36
+ clearSelectedFeeToken: () => void
37
+ processedFeeOptions: FeeOption[]
38
+ setRawFeeOptions: (
39
+ feeOptions: any[] | null | undefined,
40
+ originChainId: number | undefined,
41
+ availableTokens: TokenWithBalance[],
42
+ ) => void
43
+ }
44
+
45
+ const SelectedFeeTokenContext = createContext<
46
+ SelectedFeeTokenContextType | undefined
47
+ >(undefined)
48
+
49
+ interface SelectedFeeTokenProviderProps {
50
+ children: ReactNode
51
+ initialToken?: FeeOption | null
52
+ }
53
+
54
+ export const SelectedFeeTokenProvider: React.FC<
55
+ SelectedFeeTokenProviderProps
56
+ > = ({ children, initialToken = null }) => {
57
+ const [selectedFeeToken, setSelectedFeeTokenInternalRaw] =
58
+ useState<FeeOption | null>(initialToken)
59
+ const [hasUserSelectedFeeOption, setHasUserSelectedFeeOption] =
60
+ useState(false)
61
+ const [rawFeeOptions, setRawFeeOptionsInternal] = useState<any[]>([])
62
+ const [originTokenChainId, setOriginTokenChainId] = useState<
63
+ number | undefined
64
+ >(undefined)
65
+ const [availableTokens, setAvailableTokens] = useState<TokenWithBalance[]>([])
66
+
67
+ // Wrapper to log all state changes to selectedFeeToken
68
+ const setSelectedFeeTokenInternal = useCallback(
69
+ (token: FeeOption | null) => {
70
+ logger.console.log(
71
+ "[trails-sdk] [FEE-SELECT] selectedFeeToken state changing:",
72
+ {
73
+ from: selectedFeeToken,
74
+ to: token,
75
+ fromNull: selectedFeeToken === null,
76
+ toNull: token === null,
77
+ stackTrace: new Error().stack?.split("\n").slice(2, 5).join("\n"), // Get call stack
78
+ },
79
+ )
80
+ setSelectedFeeTokenInternalRaw(token)
81
+ },
82
+ [selectedFeeToken],
83
+ )
84
+
85
+ // Wrapper to track when user makes an explicit selection
86
+ const setSelectedFeeToken = useCallback(
87
+ (token: FeeOption | null) => {
88
+ logger.console.log(
89
+ "[trails-sdk] [FEE-SELECT] setSelectedFeeToken called (user selection):",
90
+ {
91
+ token,
92
+ isNull: token === null,
93
+ isUndefined: token === undefined,
94
+ },
95
+ )
96
+ setSelectedFeeTokenInternal(token)
97
+ setHasUserSelectedFeeOption(true)
98
+ },
99
+ [setSelectedFeeTokenInternal],
100
+ )
101
+
102
+ const clearSelectedFeeToken = useCallback(() => {
103
+ logger.console.log("[trails-sdk] [FEE-SELECT] Clearing selected fee token")
104
+ setSelectedFeeTokenInternal(null)
105
+ setHasUserSelectedFeeOption(false)
106
+ }, [setSelectedFeeTokenInternal])
107
+
108
+ // Process raw fee options with balance checks
109
+ const processedFeeOptions = useMemo(() => {
110
+ if (!rawFeeOptions || rawFeeOptions.length === 0) {
111
+ return []
112
+ }
113
+
114
+ logger.console.log("[trails-sdk] [FEE-SELECT] Processing fee options:", {
115
+ rawFeeOptions,
116
+ originTokenChainId,
117
+ availableTokensCount: availableTokens.length,
118
+ })
119
+
120
+ // Add balance check and additional properties to each fee option
121
+ const feeOptionsWithBalanceCheck = rawFeeOptions.map((option: any) => {
122
+ // For native token (zero address), check if user has enough native balance
123
+ if (option.tokenAddress === zeroAddress) {
124
+ // Find the native token balance for the origin chain
125
+ const nativeTokenInfo = availableTokens.find(
126
+ (token) =>
127
+ token.contractAddress?.toLowerCase() ===
128
+ zeroAddress.toLowerCase() && token.chainId === originTokenChainId,
129
+ )
130
+
131
+ const nativeBalance = nativeTokenInfo?.balance || "0"
132
+ const requiredAmount = option.amount
133
+ const notEnoughBalance = BigInt(nativeBalance) < BigInt(requiredAmount)
134
+
135
+ return {
136
+ ...option,
137
+ notEnoughBalance,
138
+ tokenImageUrl: nativeTokenInfo?.imageUrl,
139
+ chainId: originTokenChainId, // Native token is on the same chain as origin
140
+ }
141
+ }
142
+
143
+ // For ERC-20 tokens, check user's token balances
144
+ const userTokenBalance = availableTokens.find(
145
+ (token) =>
146
+ token.contractAddress?.toLowerCase() ===
147
+ option.tokenAddress.toLowerCase() &&
148
+ token.chainId === originTokenChainId,
149
+ )
150
+
151
+ if (userTokenBalance) {
152
+ const userBalance = userTokenBalance.balance || "0"
153
+ const requiredAmount = option.amount
154
+ const notEnoughBalance = BigInt(userBalance) < BigInt(requiredAmount)
155
+
156
+ return {
157
+ ...option,
158
+ notEnoughBalance,
159
+ tokenImageUrl: userTokenBalance.imageUrl,
160
+ chainId: userTokenBalance.chainId, // Use the actual token's chain ID
161
+ }
162
+ }
163
+
164
+ // If token balance not found, assume insufficient balance
165
+ return {
166
+ ...option,
167
+ notEnoughBalance: true,
168
+ tokenImageUrl: undefined,
169
+ chainId: originTokenChainId, // Fallback to origin chain
170
+ }
171
+ })
172
+
173
+ // Sort fee options: available options first, then disabled options at the bottom
174
+ const sortedFeeOptions = feeOptionsWithBalanceCheck.sort(
175
+ (a: FeeOption, b: FeeOption) => {
176
+ // If both have the same balance status, maintain original order
177
+ if (a.notEnoughBalance === b.notEnoughBalance) {
178
+ return 0
179
+ }
180
+ // Put available options (notEnoughBalance: false) before disabled options (notEnoughBalance: true)
181
+ return a.notEnoughBalance ? 1 : -1
182
+ },
183
+ )
184
+
185
+ logger.console.log(
186
+ "[trails-sdk] [FEE-SELECT] Processed and sorted fee options:",
187
+ sortedFeeOptions,
188
+ )
189
+
190
+ return sortedFeeOptions
191
+ }, [rawFeeOptions, originTokenChainId, availableTokens])
192
+
193
+ // Auto-select first fee option with sufficient balance (non-native token)
194
+ // Only auto-select on initial load, not when user explicitly chooses native gas
195
+ // If no ERC20 fee options are available, selectedFeeToken stays null (native gas)
196
+ useEffect(() => {
197
+ logger.console.log(
198
+ "[trails-sdk] [FEE-SELECT] Auto-selection useEffect triggered:",
199
+ {
200
+ processedFeeOptionsLength: processedFeeOptions.length,
201
+ hasUserSelectedFeeOption,
202
+ selectedFeeToken,
203
+ willAutoSelect:
204
+ processedFeeOptions.length > 0 &&
205
+ !hasUserSelectedFeeOption &&
206
+ !selectedFeeToken,
207
+ },
208
+ )
209
+
210
+ // Only auto-select if:
211
+ // 1. We have fee options
212
+ // 2. User hasn't made an explicit selection yet
213
+ // 3. No fee token is currently selected
214
+ if (
215
+ processedFeeOptions.length > 0 &&
216
+ !hasUserSelectedFeeOption &&
217
+ !selectedFeeToken
218
+ ) {
219
+ // Find first non-native token option with sufficient balance
220
+ const firstValidOption = processedFeeOptions.find(
221
+ (option: FeeOption) =>
222
+ option.tokenAddress !== zeroAddress && !option.notEnoughBalance,
223
+ )
224
+
225
+ if (firstValidOption) {
226
+ logger.console.log(
227
+ "[trails-sdk] [FEE-SELECT] Auto-selecting first valid fee option:",
228
+ firstValidOption,
229
+ )
230
+ // Strip extra fields to match the format expected by prepareSend
231
+ const cleanOption: FeeOption = {
232
+ tokenAddress: firstValidOption.tokenAddress,
233
+ tokenSymbol: firstValidOption.tokenSymbol,
234
+ tokenDecimals: firstValidOption.tokenDecimals,
235
+ amount: firstValidOption.amount,
236
+ amountUSD: firstValidOption.amountUSD,
237
+ }
238
+ // Use internal setter to avoid marking as user selection
239
+ setSelectedFeeTokenInternal(cleanOption)
240
+ } else {
241
+ logger.console.log(
242
+ "[trails-sdk] [FEE-SELECT] No valid ERC20 fee options available, will use native gas",
243
+ )
244
+ }
245
+ }
246
+ }, [
247
+ processedFeeOptions,
248
+ selectedFeeToken,
249
+ hasUserSelectedFeeOption,
250
+ setSelectedFeeTokenInternal,
251
+ ])
252
+
253
+ // Function to set raw fee options and process them
254
+ const setRawFeeOptions = useCallback(
255
+ (
256
+ feeOptions: any[] | null | undefined,
257
+ originChainId: number | undefined,
258
+ tokens: TokenWithBalance[],
259
+ ) => {
260
+ const apiFeeOptions = feeOptions ?? []
261
+ setRawFeeOptionsInternal(apiFeeOptions)
262
+ setOriginTokenChainId(originChainId)
263
+ setAvailableTokens(tokens)
264
+
265
+ logger.console.log("[trails-sdk] [FEE-SELECT] Raw fee options set:", {
266
+ feeOptionsCount: apiFeeOptions.length,
267
+ originChainId: originChainId,
268
+ tokensCount: tokens.length,
269
+ })
270
+ },
271
+ [],
272
+ )
273
+
274
+ const value: SelectedFeeTokenContextType = {
275
+ selectedFeeToken,
276
+ setSelectedFeeToken,
277
+ clearSelectedFeeToken,
278
+ processedFeeOptions,
279
+ setRawFeeOptions,
280
+ }
281
+
282
+ return (
283
+ <SelectedFeeTokenContext.Provider value={value}>
284
+ {children}
285
+ </SelectedFeeTokenContext.Provider>
286
+ )
287
+ }
288
+
289
+ export const useSelectedFeeToken = (): SelectedFeeTokenContextType => {
290
+ const context = useContext(SelectedFeeTokenContext)
291
+
292
+ if (context === undefined) {
293
+ throw new Error(
294
+ "useSelectedFeeToken must be used within a SelectedFeeTokenProvider",
295
+ )
296
+ }
297
+
298
+ return context
299
+ }
@@ -0,0 +1,46 @@
1
+ import { createContext, useContext, useState, useMemo } from "react"
2
+
3
+ export interface SelectedMeshExchange {
4
+ integrationId: string
5
+ exchangeName: string
6
+ }
7
+
8
+ interface SelectedMeshExchangeContextType {
9
+ selectedExchange: SelectedMeshExchange | null
10
+ setSelectedExchange: (exchange: SelectedMeshExchange | null) => void
11
+ }
12
+
13
+ const SelectedMeshExchangeContext = createContext<
14
+ SelectedMeshExchangeContextType | undefined
15
+ >(undefined)
16
+
17
+ export const SelectedMeshExchangeProvider: React.FC<{
18
+ children: React.ReactNode
19
+ }> = ({ children }) => {
20
+ const [selectedExchange, setSelectedExchange] =
21
+ useState<SelectedMeshExchange | null>(null)
22
+
23
+ const value = useMemo(
24
+ () => ({
25
+ selectedExchange,
26
+ setSelectedExchange,
27
+ }),
28
+ [selectedExchange],
29
+ )
30
+
31
+ return (
32
+ <SelectedMeshExchangeContext.Provider value={value}>
33
+ {children}
34
+ </SelectedMeshExchangeContext.Provider>
35
+ )
36
+ }
37
+
38
+ export const useSelectedMeshExchange = () => {
39
+ const context = useContext(SelectedMeshExchangeContext)
40
+ if (context === undefined) {
41
+ throw new Error(
42
+ "useSelectedMeshExchange must be used within a SelectedMeshExchangeProvider",
43
+ )
44
+ }
45
+ return context
46
+ }
@@ -40,6 +40,8 @@ import { etherlink } from "viem/chains"
40
40
  import { logger } from "../../logger.js"
41
41
  import { getIsContract } from "../../contractUtils.js"
42
42
  import { useTrailsClient } from "../../trailsClient.js"
43
+ import { useTokenList } from "./useTokenList.js"
44
+ import { useSelectedFeeToken } from "./useSelectedFeeToken.js"
43
45
 
44
46
  export interface Token {
45
47
  id: number
@@ -117,19 +119,14 @@ export type UseSendProps = {
117
119
  }
118
120
 
119
121
  export type FeeOption = {
120
- token: {
121
- chainId: number
122
- name: string
123
- symbol: string
124
- type: string
125
- decimals: number
126
- logoURL: string
127
- contractAddress: string | null
128
- tokenID: string | null
129
- }
130
- to: string
131
- value: string
132
- gasLimit: number
122
+ tokenAddress: string
123
+ tokenSymbol: string
124
+ tokenDecimals: number
125
+ amount: string
126
+ amountUSD: number
127
+ notEnoughBalance?: boolean
128
+ tokenImageUrl?: string
129
+ chainId?: number
133
130
  }
134
131
 
135
132
  export type UseSendReturn = {
@@ -436,6 +433,14 @@ export function useSendForm({
436
433
  const apiClient = useAPIClient()
437
434
  const trailsClient = useTrailsClient()
438
435
 
436
+ // Get user's token balances for balance checking
437
+ const { filteredTokensFormatted } = useTokenList({
438
+ onContinue: () => {}, // Not used for balance checking
439
+ onError: () => {}, // Not used for balance checking
440
+ fundMethod: undefined,
441
+ allSupportedTokens: true, // Get all tokens for balance checking
442
+ })
443
+
439
444
  const destTokenAddress = useTokenAddress({
440
445
  chainId: selectedDestinationChain?.id,
441
446
  tokenSymbol: selectedDestToken?.symbol,
@@ -569,9 +574,14 @@ export function useSendForm({
569
574
  return formatUsdAmountDisplay(amountUsd)
570
575
  }, [amount, destTokenPrices, sourceTokenPrices, tradeType])
571
576
 
572
- const [selectedFeeToken, setSelectedFeeToken] = useState<FeeOption | null>(
573
- null,
574
- )
577
+ // Use the fee token selection hook
578
+ const {
579
+ selectedFeeToken,
580
+ setSelectedFeeToken,
581
+ processedFeeOptions: feeOptions,
582
+ setRawFeeOptions,
583
+ } = useSelectedFeeToken()
584
+
575
585
  const [isRecipientContract, setIsRecipientContract] = useState(false)
576
586
  const [isSenderContractOnOrigin, setIsSenderContractOnOrigin] =
577
587
  useState(false)
@@ -794,7 +804,10 @@ export function useSendForm({
794
804
  originChainId: selectedToken.chainId,
795
805
  originTokenBalance:
796
806
  fundMethod === "qr-code" || fundMethod === "exchange"
797
- ? "1"
807
+ ? parseUnits(
808
+ "100",
809
+ selectedToken.contractInfo?.decimals ?? 18,
810
+ ).toString() // needs to be an amount that is greater than the minimum amount for the swap
798
811
  : selectedToken.balance,
799
812
  destinationChainId: selectedDestinationChain.id,
800
813
  recipient,
@@ -827,8 +840,16 @@ export function useSendForm({
827
840
  mode,
828
841
  fundMethod,
829
842
  checkoutOnHandlers,
843
+ selectedFeeToken,
830
844
  }
831
845
 
846
+ logger.console.log(
847
+ "[trails-sdk] [FEE-SELECT] getQuote using selectedFeeToken:",
848
+ {
849
+ selectedFeeToken,
850
+ },
851
+ )
852
+
832
853
  const result = await prepareSend(options)
833
854
 
834
855
  logger.console.log("[trails-sdk] prepareSend quote:", result.quote)
@@ -873,6 +894,7 @@ export function useSendForm({
873
894
  amountRaw,
874
895
  checkoutOnHandlers,
875
896
  mode,
897
+ selectedFeeToken,
876
898
  ])
877
899
 
878
900
  // Auto-fetch quotes when inputs change (debounced)
@@ -907,6 +929,7 @@ export function useSendForm({
907
929
  selectedToken?.chainId,
908
930
  selectedToken?.balance,
909
931
  selectedToken?.tokenPriceUsd,
932
+ // selectedFeeToken is passed to send() at execution time, not needed here
910
933
  ])
911
934
 
912
935
  // Calculate destination amount from quote if available
@@ -922,9 +945,31 @@ export function useSendForm({
922
945
  return formatAmountDisplay(quotedDestinationAmount || "0")
923
946
  }, [quotedDestinationAmount])
924
947
 
925
- const feeOptions = useMemo(() => {
926
- return prepareSendResult?.feeOptions?.options ?? []
927
- }, [prepareSendResult])
948
+ // Set raw fee options in the hook whenever prepareSendResult or tokens change
949
+ useEffect(() => {
950
+ const apiFeeOptions = prepareSendResult?.feeOptions?.feeOptions ?? []
951
+ logger.console.log(
952
+ "[trails-sdk] [FEE-SELECT] useSendForm setting raw fee options:",
953
+ {
954
+ prepareSendResult,
955
+ feeOptions: prepareSendResult?.feeOptions,
956
+ apiFeeOptions,
957
+ length: apiFeeOptions.length,
958
+ },
959
+ )
960
+
961
+ setRawFeeOptions(
962
+ apiFeeOptions,
963
+ selectedToken?.chainId,
964
+ filteredTokensFormatted,
965
+ )
966
+ // eslint-disable-next-line react-hooks/exhaustive-deps
967
+ }, [
968
+ prepareSendResult,
969
+ selectedToken?.chainId,
970
+ filteredTokensFormatted,
971
+ setRawFeeOptions,
972
+ ])
928
973
 
929
974
  const processSend = useCallback(async () => {
930
975
  try {
@@ -1008,7 +1053,17 @@ export function useSendForm({
1008
1053
 
1009
1054
  async function handleSend() {
1010
1055
  logger.console.log(
1011
- "[trails-sdk] handleRetry called, about to call send()",
1056
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] handleSend called, about to call send()",
1057
+ )
1058
+ logger.console.log(
1059
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] selectedFeeToken value at send() call time:",
1060
+ {
1061
+ selectedFeeToken,
1062
+ isNull: selectedFeeToken === null,
1063
+ isUndefined: selectedFeeToken === undefined,
1064
+ type: typeof selectedFeeToken,
1065
+ stringified: JSON.stringify(selectedFeeToken),
1066
+ },
1012
1067
  )
1013
1068
  // Wait for full send to complete
1014
1069
  const {
@@ -1017,7 +1072,7 @@ export function useSendForm({
1017
1072
  destinationMetaTxnReceipt,
1018
1073
  } = await send({
1019
1074
  onOriginSend,
1020
- feeTokenAddress: selectedFeeToken?.token.contractAddress,
1075
+ selectedFeeToken, // Pass current value at execution time
1021
1076
  })
1022
1077
  logger.console.log("[trails-sdk] send() completed, receipts:", {
1023
1078
  originUserTxReceipt,
@@ -1078,7 +1133,7 @@ export function useSendForm({
1078
1133
  onError,
1079
1134
  fundMethod,
1080
1135
  onNavigateToMeshConnect,
1081
- selectedFeeToken?.token.contractAddress,
1136
+ selectedFeeToken, // Include so handleSend captures latest value
1082
1137
  ])
1083
1138
 
1084
1139
  const handleSubmit = async (e: React.FormEvent) => {