0xtrails 0.2.4 → 0.2.6

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 (212) hide show
  1. package/dist/aave.d.ts +8 -0
  2. package/dist/aave.d.ts.map +1 -1
  3. package/dist/abortController.d.ts +8 -0
  4. package/dist/abortController.d.ts.map +1 -0
  5. package/dist/{ccip-BlV1Mry3.js → ccip-Xjh9d1gb.js} +7 -7
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/constants.d.ts.map +1 -1
  10. package/dist/error.d.ts +1 -0
  11. package/dist/error.d.ts.map +1 -1
  12. package/dist/estimate.d.ts +52 -0
  13. package/dist/estimate.d.ts.map +1 -1
  14. package/dist/fees.d.ts +19 -0
  15. package/dist/fees.d.ts.map +1 -0
  16. package/dist/{index-BNWCIGfQ.js → index-BnhdZ8Ho.js} +76406 -75798
  17. package/dist/index.js +726 -520
  18. package/dist/intents.d.ts +40 -0
  19. package/dist/intents.d.ts.map +1 -1
  20. package/dist/metaTxnMonitor.d.ts +3 -3
  21. package/dist/metaTxnMonitor.d.ts.map +1 -1
  22. package/dist/metaTxns.d.ts +3 -3
  23. package/dist/metaTxns.d.ts.map +1 -1
  24. package/dist/morpho.d.ts +8 -0
  25. package/dist/morpho.d.ts.map +1 -1
  26. package/dist/prepareSend.d.ts +19 -75
  27. package/dist/prepareSend.d.ts.map +1 -1
  28. package/dist/queryParams.d.ts.map +1 -1
  29. package/dist/relayer.d.ts +6 -6
  30. package/dist/relayer.d.ts.map +1 -1
  31. package/dist/sequenceWallet.d.ts +2 -2
  32. package/dist/sequenceWallet.d.ts.map +1 -1
  33. package/dist/tokens.d.ts.map +1 -1
  34. package/dist/transactions.d.ts +4 -2
  35. package/dist/transactions.d.ts.map +1 -1
  36. package/dist/wallets.d.ts.map +1 -1
  37. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  38. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts +4 -0
  39. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts.map +1 -0
  40. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  41. package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
  42. package/dist/widget/components/ClassicSwap.d.ts +4 -2
  43. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  44. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  45. package/dist/widget/components/ConnectedWallets.d.ts +4 -0
  46. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  47. package/dist/widget/components/DynamicInputStyles.d.ts +18 -0
  48. package/dist/widget/components/DynamicInputStyles.d.ts.map +1 -0
  49. package/dist/widget/components/Earn.d.ts +2 -2
  50. package/dist/widget/components/Earn.d.ts.map +1 -1
  51. package/dist/widget/components/ErrorAnimationIcon.d.ts +2 -0
  52. package/dist/widget/components/ErrorAnimationIcon.d.ts.map +1 -0
  53. package/dist/widget/components/FeeBreakdown.d.ts +9 -0
  54. package/dist/widget/components/FeeBreakdown.d.ts.map +1 -0
  55. package/dist/widget/components/Fund.d.ts +2 -2
  56. package/dist/widget/components/Fund.d.ts.map +1 -1
  57. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  58. package/dist/widget/components/{FundSendForm.d.ts → FundSwap.d.ts} +13 -7
  59. package/dist/widget/components/FundSwap.d.ts.map +1 -0
  60. package/dist/widget/components/FundingMethodSelectorButton.d.ts +4 -0
  61. package/dist/widget/components/FundingMethodSelectorButton.d.ts.map +1 -0
  62. package/dist/widget/components/Identicon.d.ts.map +1 -1
  63. package/dist/widget/components/MeshConnectExchanges.d.ts +0 -3
  64. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  65. package/dist/widget/components/Modal.d.ts.map +1 -1
  66. package/dist/widget/components/Pay.d.ts +2 -2
  67. package/dist/widget/components/Pay.d.ts.map +1 -1
  68. package/dist/widget/components/PercentageMaxButtons.d.ts +12 -0
  69. package/dist/widget/components/PercentageMaxButtons.d.ts.map +1 -0
  70. package/dist/widget/components/{PaySendForm.d.ts → PoolDeposit.d.ts} +14 -36
  71. package/dist/widget/components/PoolDeposit.d.ts.map +1 -0
  72. package/dist/widget/components/{SimpleSwap.d.ts → PoolWithdraw.d.ts} +19 -10
  73. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -0
  74. package/dist/widget/components/QuoteDetails.d.ts +1 -0
  75. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  76. package/dist/widget/components/Receipt.d.ts.map +1 -1
  77. package/dist/widget/components/Receive.d.ts.map +1 -1
  78. package/dist/widget/components/RecipientSelectorButton.d.ts +4 -0
  79. package/dist/widget/components/RecipientSelectorButton.d.ts.map +1 -0
  80. package/dist/widget/components/Recipients.d.ts.map +1 -1
  81. package/dist/widget/components/RequiredPropsError.d.ts +8 -0
  82. package/dist/widget/components/RequiredPropsError.d.ts.map +1 -0
  83. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  84. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  85. package/dist/widget/components/Swap.d.ts +3 -2
  86. package/dist/widget/components/Swap.d.ts.map +1 -1
  87. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  88. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  89. package/dist/widget/components/TokenDisplayNonSelectable.d.ts +11 -0
  90. package/dist/widget/components/TokenDisplayNonSelectable.d.ts.map +1 -0
  91. package/dist/widget/components/TokenImage.d.ts +1 -0
  92. package/dist/widget/components/TokenImage.d.ts.map +1 -1
  93. package/dist/widget/components/TokenList.d.ts.map +1 -1
  94. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  95. package/dist/widget/components/TokenSelectorButton.d.ts +16 -0
  96. package/dist/widget/components/TokenSelectorButton.d.ts.map +1 -0
  97. package/dist/widget/components/Tooltip.d.ts +9 -0
  98. package/dist/widget/components/Tooltip.d.ts.map +1 -0
  99. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  100. package/dist/widget/components/WaasFeeOptions.d.ts +9 -0
  101. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -0
  102. package/dist/widget/components/WalletConfirmation.d.ts.map +1 -1
  103. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  104. package/dist/widget/components/WalletList.d.ts.map +1 -1
  105. package/dist/widget/css/compiled.css +2 -0
  106. package/dist/widget/css/index.css +554 -0
  107. package/dist/widget/hooks/useBack.d.ts +1 -0
  108. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  109. package/dist/widget/hooks/useCheckout.d.ts +1 -1
  110. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  111. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  112. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  113. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +3 -3
  114. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  115. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -1
  116. package/dist/widget/hooks/useQuote.d.ts +83 -0
  117. package/dist/widget/hooks/useQuote.d.ts.map +1 -0
  118. package/dist/widget/hooks/useSelectedFundMethod.d.ts +12 -0
  119. package/dist/widget/hooks/useSelectedFundMethod.d.ts.map +1 -0
  120. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -1
  121. package/dist/widget/hooks/useSendForm.d.ts +2 -2
  122. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  123. package/dist/widget/index.js +2 -2
  124. package/dist/widget/widget.d.ts +9 -4
  125. package/dist/widget/widget.d.ts.map +1 -1
  126. package/package.json +18 -12
  127. package/src/aave.ts +32 -0
  128. package/src/abortController.ts +35 -0
  129. package/src/config.ts +12 -4
  130. package/src/constants.ts +5 -0
  131. package/src/error.ts +19 -1
  132. package/src/estimate.ts +416 -5
  133. package/src/fees.ts +199 -0
  134. package/src/intents.ts +161 -11
  135. package/src/metaTxnMonitor.ts +3 -3
  136. package/src/metaTxns.ts +3 -5
  137. package/src/morpho.ts +32 -0
  138. package/src/prepareSend.ts +714 -550
  139. package/src/queryParams.ts +2 -1
  140. package/src/relayer.ts +11 -11
  141. package/src/sequenceWallet.ts +2 -2
  142. package/src/tokens.ts +7 -1
  143. package/src/trails.ts +3 -3
  144. package/src/transactions.ts +62 -18
  145. package/src/wallets.ts +8 -0
  146. package/src/widget/compiled.css +2 -2
  147. package/src/widget/components/AccountActionsDropdown.tsx +3 -13
  148. package/src/widget/components/AccountIntentTransactionHistoryButton.tsx +22 -0
  149. package/src/widget/components/AccountSettings.tsx +48 -54
  150. package/src/widget/components/ChainFilterDropdown.tsx +24 -3
  151. package/src/widget/components/ClassicSwap.tsx +131 -213
  152. package/src/widget/components/ConnectWallet.tsx +8 -38
  153. package/src/widget/components/ConnectedWallets.tsx +132 -77
  154. package/src/widget/components/DynamicInputStyles.tsx +76 -0
  155. package/src/widget/components/Earn.tsx +82 -593
  156. package/src/widget/components/ErrorAnimationIcon.tsx +130 -0
  157. package/src/widget/components/FeeBreakdown.tsx +155 -0
  158. package/src/widget/components/Fund.tsx +41 -108
  159. package/src/widget/components/FundMethods.tsx +82 -159
  160. package/src/widget/components/FundSwap.tsx +52 -0
  161. package/src/widget/components/FundingMethodSelectorButton.tsx +70 -0
  162. package/src/widget/components/Identicon.tsx +164 -95
  163. package/src/widget/components/MeshConnectExchanges.tsx +2 -15
  164. package/src/widget/components/Modal.tsx +0 -8
  165. package/src/widget/components/Pay.tsx +214 -237
  166. package/src/widget/components/PercentageMaxButtons.tsx +77 -0
  167. package/src/widget/components/PoolDeposit.tsx +569 -0
  168. package/src/widget/components/PoolWithdraw.tsx +884 -0
  169. package/src/widget/components/PriceImpactWarning.tsx +1 -1
  170. package/src/widget/components/QuoteDetails.tsx +43 -12
  171. package/src/widget/components/Receipt.tsx +16 -2
  172. package/src/widget/components/Receive.tsx +0 -2
  173. package/src/widget/components/RecipientSelectorButton.tsx +44 -0
  174. package/src/widget/components/Recipients.tsx +63 -157
  175. package/src/widget/components/RequiredPropsError.tsx +33 -0
  176. package/src/widget/components/ScreenHeader.tsx +62 -34
  177. package/src/widget/components/SlippageToleranceSettings.tsx +2 -1
  178. package/src/widget/components/Swap.tsx +4 -45
  179. package/src/widget/components/SwapSettings.tsx +2 -14
  180. package/src/widget/components/ThemeProvider.tsx +2 -1
  181. package/src/widget/components/TokenDisplayNonSelectable.tsx +40 -0
  182. package/src/widget/components/TokenImage.tsx +22 -5
  183. package/src/widget/components/TokenList.tsx +0 -1
  184. package/src/widget/components/TokenSelector.tsx +63 -53
  185. package/src/widget/components/TokenSelectorButton.tsx +98 -0
  186. package/src/widget/components/Tooltip.tsx +51 -0
  187. package/src/widget/components/TransferPendingVertical.tsx +1 -1
  188. package/src/widget/components/UserPreferences.tsx +6 -24
  189. package/src/widget/components/WaasFeeOptions.tsx +450 -0
  190. package/src/widget/components/WalletConfirmation.tsx +76 -14
  191. package/src/widget/components/WalletConnect.tsx +93 -29
  192. package/src/widget/components/WalletList.tsx +4 -2
  193. package/src/widget/hooks/useBack.tsx +2 -0
  194. package/src/widget/hooks/useCheckout.ts +36 -20
  195. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  196. package/src/widget/hooks/useDefaultTokenSelection.tsx +104 -28
  197. package/src/widget/hooks/usePayMessage.tsx +86 -11
  198. package/src/widget/hooks/useQuote.ts +413 -0
  199. package/src/widget/hooks/useSelectedFundMethod.tsx +41 -0
  200. package/src/widget/hooks/useSelectedRecipient.tsx +10 -0
  201. package/src/widget/hooks/useSendForm.ts +32 -6
  202. package/src/widget/index.css +27 -0
  203. package/src/widget/widget.tsx +326 -283
  204. package/dist/widget/components/FundSendForm.d.ts.map +0 -1
  205. package/dist/widget/components/PaySendForm.d.ts.map +0 -1
  206. package/dist/widget/components/SimpleSwap.d.ts.map +0 -1
  207. package/dist/widget/hooks/useSwapSettings.d.ts +0 -16
  208. package/dist/widget/hooks/useSwapSettings.d.ts.map +0 -1
  209. package/src/widget/components/FundSendForm.tsx +0 -903
  210. package/src/widget/components/PaySendForm.tsx +0 -869
  211. package/src/widget/components/SimpleSwap.tsx +0 -983
  212. package/src/widget/hooks/useSwapSettings.tsx +0 -100
package/src/estimate.ts CHANGED
@@ -1,7 +1,156 @@
1
- import { type PublicClient, formatUnits } from "viem"
1
+ import { type PublicClient, formatUnits, createPublicClient, http } from "viem"
2
+ import { getChainInfo } from "./chains.js"
3
+ import { logger } from "./logger.js"
4
+
5
+ // Default minimum gas limit for deposit transactions
6
+ // Used as fallback when gas estimation is not available
7
+ // Buffer is only added when dynamically estimating gas, not when using this default
8
+ export const DEFAULT_MIN_GASLIMIT = 250_000n
9
+
10
+ // Buffer added to estimated gas (only applied when gas is dynamically estimated)
11
+ export const GAS_BUFFER = 50_000n
12
+
13
+ // Fee multiplier for faster transaction processing (similar to MetaMask's "Aggressive" mode)
14
+ // 2x priority fee increases likelihood of quick inclusion
15
+ export const PRIORITY_FEE_MULTIPLIER = 2.0
16
+
17
+ /**
18
+ * Estimate gas limit for a transaction with consistent logic
19
+ * Buffer is only applied when gas is dynamically estimated (not when using DEFAULT_MIN_GASLIMIT)
20
+ * @param publicClient - Viem public client for the chain
21
+ * @param params - Transaction parameters for gas estimation
22
+ * @param logContext - Context string for logging (e.g., "quote", "deposit")
23
+ * @returns Gas limit as bigint, or undefined if estimation fails
24
+ */
25
+ export async function estimateGasLimit(
26
+ publicClient: PublicClient,
27
+ params: {
28
+ account: string
29
+ to: string | bigint | number
30
+ data: string | bigint | number
31
+ value: bigint | string | number
32
+ },
33
+ logContext: string = "transaction",
34
+ ): Promise<bigint | undefined> {
35
+ try {
36
+ const estimatedGasLimit = await publicClient.estimateGas({
37
+ account: params.account as `0x${string}`,
38
+ to: params.to as `0x${string}`,
39
+ data: params.data as `0x${string}`,
40
+ value:
41
+ typeof params.value === "bigint" ? params.value : BigInt(params.value),
42
+ })
43
+
44
+ // Only add buffer when we actually estimated gas (estimatedGasLimit > DEFAULT_MIN_GASLIMIT)
45
+ // If estimation is below default, just use default without buffer
46
+ let gasLimit: bigint
47
+ if (estimatedGasLimit > DEFAULT_MIN_GASLIMIT) {
48
+ // Dynamic estimation - add buffer for safety
49
+ gasLimit = estimatedGasLimit + GAS_BUFFER
50
+ logger.console.log(
51
+ `[trails-sdk][gas-estimation] estimated gasLimit for ${logContext}:`,
52
+ gasLimit,
53
+ "(estimated:",
54
+ estimatedGasLimit,
55
+ "buffer:",
56
+ GAS_BUFFER,
57
+ ")",
58
+ )
59
+ } else {
60
+ // Use default minimum without buffer
61
+ gasLimit = DEFAULT_MIN_GASLIMIT
62
+ logger.console.log(
63
+ `[trails-sdk][gas-estimation] using default gasLimit for ${logContext}:`,
64
+ gasLimit,
65
+ "(estimated was:",
66
+ estimatedGasLimit,
67
+ "which is below default)",
68
+ )
69
+ }
70
+
71
+ return gasLimit
72
+ } catch (error) {
73
+ logger.console.error(
74
+ `[trails-sdk][gas-estimation] Error estimating gas limit for ${logContext}:`,
75
+ error,
76
+ )
77
+ return undefined
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get fee parameters (EIP-1559 or legacy) from a public client
83
+ * Applies PRIORITY_FEE_MULTIPLIER for faster transaction processing
84
+ * @param publicClient - Viem public client for the chain
85
+ * @returns Promise<FeeData> - Fee data including maxFeePerGas and maxPriorityFeePerGas if available
86
+ */
87
+ export async function getFeeData(publicClient: PublicClient): Promise<{
88
+ maxFeePerGas?: bigint
89
+ maxPriorityFeePerGas?: bigint
90
+ gasPrice?: bigint
91
+ }> {
92
+ try {
93
+ // Try to get EIP-1559 fee data
94
+ const feeData = await publicClient.estimateFeesPerGas()
95
+
96
+ if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
97
+ // EIP-1559 chain - apply multiplier for faster processing
98
+ // Increase priority fee by multiplier (e.g., 2x)
99
+ const boostedPriorityFee = BigInt(
100
+ Math.floor(
101
+ Number(feeData.maxPriorityFeePerGas) * PRIORITY_FEE_MULTIPLIER,
102
+ ),
103
+ )
104
+
105
+ // Calculate additional fee to add to maxFeePerGas
106
+ const additionalFee = boostedPriorityFee - feeData.maxPriorityFeePerGas
107
+ const boostedMaxFee = feeData.maxFeePerGas + additionalFee
108
+
109
+ logger.console.log(
110
+ "[trails-sdk][gas-estimation] Boosting fees for faster processing:",
111
+ {
112
+ originalMaxFee: feeData.maxFeePerGas,
113
+ originalPriorityFee: feeData.maxPriorityFeePerGas,
114
+ boostedMaxFee,
115
+ boostedPriorityFee,
116
+ multiplier: PRIORITY_FEE_MULTIPLIER,
117
+ },
118
+ )
119
+
120
+ return {
121
+ maxFeePerGas: boostedMaxFee,
122
+ maxPriorityFeePerGas: boostedPriorityFee,
123
+ }
124
+ }
125
+ } catch (_error) {
126
+ // EIP-1559 not supported, fall through to legacy
127
+ }
128
+
129
+ // Fallback to legacy gas price - also apply multiplier
130
+ try {
131
+ const gasPrice = await publicClient.getGasPrice()
132
+ const boostedGasPrice = BigInt(
133
+ Math.floor(Number(gasPrice) * PRIORITY_FEE_MULTIPLIER),
134
+ )
135
+
136
+ logger.console.log(
137
+ "[trails-sdk][gas-estimation] Boosting legacy gas price for faster processing:",
138
+ {
139
+ originalGasPrice: gasPrice,
140
+ boostedGasPrice,
141
+ multiplier: PRIORITY_FEE_MULTIPLIER,
142
+ },
143
+ )
144
+
145
+ return { gasPrice: boostedGasPrice }
146
+ } catch (error) {
147
+ throw new Error(`Failed to get fee data: ${error}`)
148
+ }
149
+ }
2
150
 
3
151
  /**
4
152
  * Estimates gas cost for a transaction on a given chain
153
+ * Properly handles EIP-1559 fees (uses maxFeePerGas) on supported chains
5
154
  * @param publicClient - Viem public client for the chain
6
155
  * @param gasLimit - Gas limit in wei (defaults to 100,000)
7
156
  * @returns Promise<bigint> - Estimated gas cost in wei
@@ -11,11 +160,17 @@ export async function estimateGasCost(
11
160
  gasLimit: bigint = 100_000n,
12
161
  ): Promise<bigint> {
13
162
  try {
14
- // Get current gas price from the chain
15
- const gasPrice = await publicClient.getGasPrice()
163
+ const feeData = await getFeeData(publicClient)
164
+
165
+ // Use maxFeePerGas for EIP-1559, otherwise use legacy gas price
166
+ const effectiveGasPrice = feeData.maxFeePerGas || feeData.gasPrice
16
167
 
17
- // Calculate gas cost: gas limit * gas price
18
- const gasCost = gasLimit * gasPrice
168
+ if (!effectiveGasPrice) {
169
+ throw new Error("Could not determine gas price")
170
+ }
171
+
172
+ // Calculate gas cost: gas limit * effective gas price
173
+ const gasCost = gasLimit * effectiveGasPrice
19
174
 
20
175
  return gasCost
21
176
  } catch (error) {
@@ -55,3 +210,259 @@ export async function estimateGasCostUsd(
55
210
  const gasCostInEth = parseFloat(formatUnits(gasCost, 18))
56
211
  return gasCostInEth * nativeTokenPriceUsd
57
212
  }
213
+
214
+ /**
215
+ * Calculate maximum amount for native token considering gas costs
216
+ * @param userBalanceFormatted - User's balance as formatted string (e.g., "1.5")
217
+ * @param gasCostFormatted - Gas cost in native token from quote (e.g., "0.001")
218
+ * @param bufferPercentage - Safety buffer % (default 50% for gas price volatility)
219
+ * @returns Maximum usable amount as formatted string (never throws, returns "0" on error)
220
+ */
221
+ export function calculateMaxNativeAmount(
222
+ userBalanceFormatted: string,
223
+ gasCostFormatted?: string,
224
+ bufferPercentage: number = 50,
225
+ ): string {
226
+ logger.console.log(
227
+ "[trails-sdk][gas-estimation] calculateMaxNativeAmount called with:",
228
+ { userBalanceFormatted, gasCostFormatted, bufferPercentage },
229
+ )
230
+ try {
231
+ // Validate inputs
232
+ if (!userBalanceFormatted || typeof userBalanceFormatted !== "string") {
233
+ logger.console.log(
234
+ "[trails-sdk][gas-estimation] Invalid userBalanceFormatted, returning 0",
235
+ )
236
+ return "0"
237
+ }
238
+
239
+ const balance = parseFloat(userBalanceFormatted)
240
+
241
+ // Check for invalid balance
242
+ if (Number.isNaN(balance) || !Number.isFinite(balance) || balance <= 0) {
243
+ return "0"
244
+ }
245
+
246
+ // If no gas estimate, return full balance
247
+ if (!gasCostFormatted || typeof gasCostFormatted !== "string") {
248
+ return userBalanceFormatted
249
+ }
250
+
251
+ const gasCost = parseFloat(gasCostFormatted)
252
+
253
+ // Check for invalid gas cost
254
+ if (Number.isNaN(gasCost) || !Number.isFinite(gasCost) || gasCost < 0) {
255
+ return userBalanceFormatted
256
+ }
257
+
258
+ // If gas cost is zero, return full balance
259
+ if (gasCost === 0) {
260
+ return userBalanceFormatted
261
+ }
262
+
263
+ // Validate buffer percentage
264
+ const safeBufferPercentage =
265
+ typeof bufferPercentage === "number" &&
266
+ Number.isFinite(bufferPercentage) &&
267
+ bufferPercentage >= 0
268
+ ? bufferPercentage
269
+ : 50
270
+
271
+ // Add buffer to gas cost (default 50% to handle gas price spikes)
272
+ const gasWithBuffer = gasCost * (1 + safeBufferPercentage / 100)
273
+
274
+ // Check for overflow
275
+ if (!Number.isFinite(gasWithBuffer)) {
276
+ return "0"
277
+ }
278
+
279
+ // Calculate max amount
280
+ const maxAmount = balance - gasWithBuffer
281
+
282
+ // Check for invalid result
283
+ if (Number.isNaN(maxAmount) || !Number.isFinite(maxAmount)) {
284
+ return "0"
285
+ }
286
+
287
+ // If not enough for gas, return 0
288
+ if (maxAmount <= 0) {
289
+ return "0"
290
+ }
291
+
292
+ // Return with reasonable precision (6 decimals)
293
+ const result = maxAmount.toFixed(6)
294
+
295
+ // Final validation of result
296
+ if (!result || typeof result !== "string") {
297
+ return "0"
298
+ }
299
+
300
+ logger.console.log(
301
+ "[trails-sdk][gas-estimation] calculateMaxNativeAmount result:",
302
+ { balance, gasCost, gasWithBuffer, maxAmount, result },
303
+ )
304
+
305
+ return result
306
+ } catch (error) {
307
+ // Catch any unexpected errors and return safe default
308
+ logger.console.error(
309
+ "[trails-sdk][gas-estimation] Error in calculateMaxNativeAmount:",
310
+ error,
311
+ )
312
+ return "0"
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Calculate a conservative default gas cost using real gas price
318
+ * Buffer is only added when gas is dynamically estimated above DEFAULT_MIN_GASLIMIT
319
+ * Properly handles EIP-1559 fees (maxFeePerGas + maxPriorityFee) on supported chains
320
+ * Falls back to 1% of balance if gas price fetch fails
321
+ *
322
+ * @param balance - User's balance as formatted string
323
+ * @param chainId - Chain ID to fetch gas price from
324
+ * @param account - Account address for gas estimation (optional)
325
+ * @param to - Transaction recipient address (optional)
326
+ * @param data - Transaction data (optional)
327
+ * @param value - Transaction value (optional)
328
+ * @returns Promise<string> - Estimated gas cost in native token units
329
+ */
330
+ export async function getDefaultGasCostEstimate(
331
+ balance: string,
332
+ chainId?: number,
333
+ account?: string,
334
+ to?: string,
335
+ data?: string,
336
+ value?: string,
337
+ ): Promise<string> {
338
+ logger.console.log(
339
+ "[trails-sdk][gas-estimation] getDefaultGasCostEstimate called with:",
340
+ {
341
+ balance,
342
+ chainId,
343
+ hasAccount: !!account,
344
+ hasTo: !!to,
345
+ hasData: !!data,
346
+ hasValue: !!value,
347
+ },
348
+ )
349
+ try {
350
+ const balanceNum = parseFloat(balance)
351
+ if (Number.isNaN(balanceNum) || balanceNum <= 0) {
352
+ logger.console.log(
353
+ "[trails-sdk][gas-estimation] Invalid balance, returning 0",
354
+ )
355
+ return "0"
356
+ }
357
+
358
+ // Try to get real gas price if chainId is provided
359
+ if (chainId) {
360
+ try {
361
+ const chain = getChainInfo(chainId)
362
+ if (chain) {
363
+ const publicClient = createPublicClient({
364
+ chain,
365
+ transport: http(),
366
+ })
367
+
368
+ // Try to estimate gas if we have transaction details
369
+ let gasLimit = DEFAULT_MIN_GASLIMIT
370
+ if (account && to && data !== undefined && value !== undefined) {
371
+ try {
372
+ const estimatedGas = await publicClient.estimateGas({
373
+ account: account as `0x${string}`,
374
+ to: to as `0x${string}`,
375
+ data: data as `0x${string}`,
376
+ value: BigInt(value),
377
+ })
378
+ // Only add buffer when dynamically estimating gas above default
379
+ if (estimatedGas > DEFAULT_MIN_GASLIMIT) {
380
+ gasLimit = estimatedGas + GAS_BUFFER
381
+ logger.console.log(
382
+ "[trails-sdk][gas-estimation] Using estimated gas + buffer for max button:",
383
+ { estimatedGas, buffer: GAS_BUFFER, gasLimit },
384
+ )
385
+ } else {
386
+ gasLimit = DEFAULT_MIN_GASLIMIT
387
+ logger.console.log(
388
+ "[trails-sdk][gas-estimation] Using default gas limit for max button (estimated was below default):",
389
+ { estimatedGas, gasLimit },
390
+ )
391
+ }
392
+ } catch (error) {
393
+ logger.console.error(
394
+ "[trails-sdk][gas-estimation] Error estimating gas for max button, using default:",
395
+ error,
396
+ )
397
+ // Use default if estimation fails (no buffer)
398
+ }
399
+ }
400
+
401
+ // Get gas price - use getFeeData which applies the speed multiplier
402
+ let effectiveGasPrice: bigint
403
+ try {
404
+ const feeData = await getFeeData(publicClient)
405
+ effectiveGasPrice = feeData.maxFeePerGas || feeData.gasPrice || 0n
406
+
407
+ if (effectiveGasPrice === 0n) {
408
+ throw new Error("Could not determine gas price")
409
+ }
410
+
411
+ logger.console.log(
412
+ "[trails-sdk][gas-estimation] Using boosted fee data for max button:",
413
+ {
414
+ maxFeePerGas: feeData.maxFeePerGas,
415
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
416
+ gasPrice: feeData.gasPrice,
417
+ effectiveGasPrice,
418
+ },
419
+ )
420
+ } catch (_error) {
421
+ // If getFeeData fails, fall back to basic gas price with manual multiplier
422
+ logger.console.log(
423
+ "[trails-sdk][gas-estimation] getFeeData failed, using fallback with multiplier",
424
+ )
425
+ const basicGasPrice = await publicClient.getGasPrice()
426
+ effectiveGasPrice = BigInt(
427
+ Math.floor(Number(basicGasPrice) * PRIORITY_FEE_MULTIPLIER),
428
+ )
429
+ }
430
+
431
+ // Calculate gas cost: gasLimit × effectiveGasPrice
432
+ const gasCostWei = gasLimit * effectiveGasPrice
433
+
434
+ // Convert to native token units (always 18 decimals)
435
+ const gasCostFormatted = formatUnits(gasCostWei, 18)
436
+
437
+ logger.console.log(
438
+ "[trails-sdk][gas-estimation] Calculated gas cost for max button:",
439
+ { gasLimit, effectiveGasPrice, gasCostWei, gasCostFormatted },
440
+ )
441
+
442
+ return gasCostFormatted
443
+ }
444
+ } catch (error) {
445
+ logger.console.error(
446
+ "[trails-sdk][gas-estimation] Error fetching gas price for max button:",
447
+ error,
448
+ )
449
+ // Fall through to percentage-based fallback
450
+ }
451
+ }
452
+
453
+ // Fallback: Reserve 1% of balance for gas
454
+ // This works for any native token and any chain
455
+ const defaultGasCost = balanceNum * 0.01
456
+ logger.console.log(
457
+ "[trails-sdk][gas-estimation] Using fallback 1% gas cost estimate:",
458
+ { balance: balanceNum, defaultGasCost: defaultGasCost.toFixed(6) },
459
+ )
460
+ return defaultGasCost.toFixed(6)
461
+ } catch (error) {
462
+ logger.console.error(
463
+ "[trails-sdk][gas-estimation] Error in getDefaultGasCostEstimate:",
464
+ error,
465
+ )
466
+ return "0"
467
+ }
468
+ }
package/src/fees.ts ADDED
@@ -0,0 +1,199 @@
1
+ import type { GetIntentCallsPayloadsReturn } from "./intents.js"
2
+ import { getTokenInfo } from "./tokens.js"
3
+ import { formatRawAmount, formatUsdAmountDisplay } from "./tokenBalances.js"
4
+ import { zeroAddress } from "viem"
5
+
6
+ export interface FeeItem {
7
+ amount: string
8
+ tokenSymbol: string
9
+ tokenAddress: string
10
+ chainId: number
11
+ usdValue: string
12
+ }
13
+
14
+ export interface TrailsFeeBreakdown {
15
+ originRelayFee?: FeeItem
16
+ destinationRelayFee?: FeeItem
17
+ providerFee?: FeeItem
18
+ trailsFee?: FeeItem
19
+ totalUsdValue?: string
20
+ originChainId?: number
21
+ destinationChainId?: number
22
+ }
23
+
24
+ // Helper function to format token amounts with proper decimal conversion
25
+ async function formatTokenAmount(
26
+ amount: string | number | undefined,
27
+ tokenAddress: string,
28
+ chainId: number,
29
+ ): Promise<string> {
30
+ if (!amount) return "0"
31
+ const num = typeof amount === "string" ? parseFloat(amount) : amount
32
+ if (num === 0) return "0"
33
+
34
+ // Get token info to determine decimals
35
+ const tokenInfo = await getTokenInfo(chainId, tokenAddress)
36
+ if (!tokenInfo?.decimals) {
37
+ throw new Error(
38
+ `Token decimals not found for token ${tokenAddress} on chain ${chainId}`,
39
+ )
40
+ }
41
+
42
+ // Use existing formatRawAmount utility
43
+ const formatted = formatRawAmount(BigInt(Math.floor(num)), tokenInfo.decimals)
44
+
45
+ // Handle very small amounts
46
+ const numFormatted = parseFloat(formatted)
47
+ if (numFormatted < 0.000001 && numFormatted > 0) return "<0.000001"
48
+
49
+ return formatted
50
+ }
51
+
52
+ // Extract fee breakdown from intent data
53
+ export async function extractTrailsFeeBreakdown(
54
+ intent: GetIntentCallsPayloadsReturn,
55
+ ): Promise<TrailsFeeBreakdown | null> {
56
+ const trailsFee = intent.payloads?.trailsFee
57
+ if (!trailsFee?.executeQuote?.chainQuotes) {
58
+ return null
59
+ }
60
+
61
+ const chainQuotes = trailsFee.executeQuote.chainQuotes
62
+
63
+ // Get chain IDs from the calls array to determine origin and destination chains
64
+ const calls = intent.payloads?.calls || []
65
+ const originChainId = calls[0]?.chainId // First call is origin chain
66
+ const destinationChainId = calls[1]?.chainId // Second call is destination chain
67
+
68
+ // Find quotes by chainId
69
+ const originChainQuote = chainQuotes.find(
70
+ (quote: any) => quote.chainId === originChainId,
71
+ )
72
+ const destinationChainQuote = chainQuotes.find(
73
+ (quote: any) => quote.chainId === destinationChainId,
74
+ )
75
+
76
+ // If we can't find any chain quotes, return null
77
+ if (!originChainQuote && !destinationChainQuote) {
78
+ console.warn(
79
+ "[trails-sdk] Could not find any chain quotes for fee breakdown",
80
+ {
81
+ originChainId,
82
+ destinationChainId,
83
+ availableChainIds: chainQuotes.map((q: any) => q.chainId),
84
+ },
85
+ )
86
+ return null
87
+ }
88
+
89
+ const breakdown: TrailsFeeBreakdown = {}
90
+
91
+ // Extract origin relay fee if available
92
+ if (originChainQuote) {
93
+ const originTokenSymbol = originChainQuote.nativeTokenSymbol || "ETH"
94
+ const originChainIdNum = parseInt(originChainId || "0", 10)
95
+ const nativeTokenAddress = zeroAddress
96
+
97
+ breakdown.originRelayFee = {
98
+ amount: await formatTokenAmount(
99
+ originChainQuote.totalFeeAmount,
100
+ nativeTokenAddress,
101
+ originChainIdNum,
102
+ ),
103
+ tokenSymbol: originTokenSymbol,
104
+ tokenAddress: nativeTokenAddress,
105
+ chainId: originChainIdNum,
106
+ usdValue: formatUsdAmountDisplay(originChainQuote.totalFeeUSD),
107
+ }
108
+ breakdown.originChainId = originChainIdNum
109
+ }
110
+
111
+ // Extract destination relay fee if available
112
+ if (destinationChainQuote) {
113
+ const destinationTokenSymbol =
114
+ destinationChainQuote.nativeTokenSymbol || "ETH"
115
+ const destinationChainIdNum = parseInt(destinationChainId || "0", 10)
116
+ const nativeTokenAddress = zeroAddress
117
+
118
+ breakdown.destinationRelayFee = {
119
+ amount: await formatTokenAmount(
120
+ destinationChainQuote.totalFeeAmount,
121
+ nativeTokenAddress,
122
+ destinationChainIdNum,
123
+ ),
124
+ tokenSymbol: destinationTokenSymbol,
125
+ tokenAddress: nativeTokenAddress,
126
+ chainId: destinationChainIdNum,
127
+ usdValue: formatUsdAmountDisplay(destinationChainQuote.totalFeeUSD),
128
+ }
129
+ breakdown.destinationChainId = destinationChainIdNum
130
+ }
131
+
132
+ // Extract provider fee from crossChainFee if available
133
+ const providerFeeAmount = trailsFee.crossChainFee?.providerFeeUSD
134
+ const providerFeeRawAmount = trailsFee.crossChainFee?.providerFee
135
+ if (providerFeeAmount !== undefined || providerFeeRawAmount !== undefined) {
136
+ const feeTokenAddress = trailsFee.feeToken || zeroAddress
137
+ const originChainIdNum = parseInt(originChainId || "0", 10)
138
+
139
+ // Get token info to determine the actual token symbol
140
+ const feeTokenInfo = await getTokenInfo(originChainIdNum, feeTokenAddress)
141
+ if (!feeTokenInfo?.symbol) {
142
+ throw new Error(
143
+ `Token symbol not found for provider fee token ${feeTokenAddress} on chain ${originChainIdNum}`,
144
+ )
145
+ }
146
+
147
+ breakdown.providerFee = {
148
+ amount: await formatTokenAmount(
149
+ providerFeeRawAmount,
150
+ feeTokenAddress,
151
+ originChainIdNum,
152
+ ),
153
+ tokenSymbol: feeTokenInfo.symbol,
154
+ tokenAddress: feeTokenAddress,
155
+ chainId: originChainIdNum,
156
+ usdValue: formatUsdAmountDisplay(providerFeeAmount),
157
+ }
158
+ }
159
+
160
+ // Extract Trails platform fee from crossChainFee if available
161
+ const trailsFeeAmount = trailsFee.crossChainFee?.trailsSwapFeeUSD
162
+ const trailsFeeRawAmount = trailsFee.crossChainFee?.trailsSwapFee
163
+ if (trailsFeeAmount !== undefined || trailsFeeRawAmount !== undefined) {
164
+ const feeTokenAddress = trailsFee.feeToken || zeroAddress
165
+ const originChainIdNum = parseInt(originChainId || "0", 10)
166
+
167
+ // Get token info to determine the actual token symbol
168
+ const feeTokenInfo = await getTokenInfo(originChainIdNum, feeTokenAddress)
169
+ if (!feeTokenInfo?.symbol) {
170
+ throw new Error(
171
+ `Token symbol not found for trails fee token ${feeTokenAddress} on chain ${originChainIdNum}`,
172
+ )
173
+ }
174
+
175
+ breakdown.trailsFee = {
176
+ amount: await formatTokenAmount(
177
+ trailsFeeRawAmount,
178
+ feeTokenAddress,
179
+ originChainIdNum,
180
+ ),
181
+ tokenSymbol: feeTokenInfo.symbol,
182
+ tokenAddress: feeTokenAddress,
183
+ chainId: originChainIdNum,
184
+ usdValue: formatUsdAmountDisplay(trailsFeeAmount),
185
+ }
186
+ }
187
+
188
+ // Calculate total USD value from the actual data
189
+ const originUsd = parseFloat(originChainQuote?.totalFeeUSD || "0")
190
+ const destinationUsd = parseFloat(destinationChainQuote?.totalFeeUSD || "0")
191
+ const providerUsd =
192
+ typeof providerFeeAmount === "number" ? providerFeeAmount : 0
193
+ const trailsUsd = typeof trailsFeeAmount === "number" ? trailsFeeAmount : 0
194
+ const totalUsd = originUsd + destinationUsd + providerUsd + trailsUsd
195
+
196
+ breakdown.totalUsdValue = formatUsdAmountDisplay(totalUsd)
197
+
198
+ return breakdown
199
+ }