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
@@ -6,7 +6,10 @@ import { useSelectedFundMethod } from "../hooks/useSelectedFundMethod.js"
6
6
  import type { ProcessedFeeOption } from "../hooks/useSelectedFeeOption.js"
7
7
  import { logger } from "../../logger.js"
8
8
  import type { FeeOption } from "@0xsequence/trails-api"
9
- import { FeeOption as FeeOptionComponent } from "./FeeOption.js"
9
+ import {
10
+ FeeOption as FeeOptionComponent,
11
+ isFeeOptionDisabled,
12
+ } from "./FeeOption.js"
10
13
  import { isNativeToken, normalizeAddress } from "../../utils.js"
11
14
  import type { Token } from "../hooks/useOriginSelectedToken.js"
12
15
 
@@ -90,7 +93,11 @@ export const FeeOptions: React.FC<FeeOptionsProps> = ({
90
93
  const selectedOption = getSelectedOption()
91
94
 
92
95
  // Use nativeGasOption as fallback when selectedOption is null/undefined
93
- const displayOption = selectedOption || nativeGasOption
96
+ const displayOption = selectedOption
97
+ ? isFeeOptionDisabled(selectedOption as ProcessedFeeOption, originTokenInfo)
98
+ ? nativeGasOption
99
+ : selectedOption
100
+ : nativeGasOption
94
101
 
95
102
  const handleFeeOptionSelect = (option: FeeOption) => {
96
103
  // For native gas (zero/ETH address), set to null to trigger non-gasless flow
@@ -111,6 +118,35 @@ export const FeeOptions: React.FC<FeeOptionsProps> = ({
111
118
  setIsOpen(false)
112
119
  }
113
120
 
121
+ if (processedFeeOptions.length === 1 && selectedOption) {
122
+ return (
123
+ <div className="space-y-1" ref={accordionRef}>
124
+ <div className="p-2">
125
+ <div className="flex justify-between items-center mb-2">
126
+ <div className="text-xs font-medium trails-text-primary">
127
+ Gas Fee Option
128
+ </div>
129
+ </div>
130
+ <div className="mt-2 space-y-1">
131
+ <FeeOptionComponent
132
+ key={`${selectedOption?.tokenAddress}-0`}
133
+ option={selectedOption as ProcessedFeeOption}
134
+ isSelected={
135
+ isNativeToken(selectedOption?.tokenAddress)
136
+ ? selectedFeeOption === null ||
137
+ isNativeToken(selectedFeeOption?.tokenAddress)
138
+ : normalizeAddress(selectedFeeOption?.tokenAddress) ===
139
+ normalizeAddress(selectedOption?.tokenAddress)
140
+ }
141
+ chainId={selectedOption?.chainId || chainId}
142
+ originTokenInfo={originTokenInfo}
143
+ />
144
+ </div>
145
+ </div>
146
+ </div>
147
+ )
148
+ }
149
+
114
150
  return (
115
151
  <div className="space-y-1" ref={accordionRef}>
116
152
  <div className="p-2">
@@ -126,8 +162,17 @@ export const FeeOptions: React.FC<FeeOptionsProps> = ({
126
162
  {/* Accordion Header */}
127
163
  <button
128
164
  type="button"
129
- onClick={() => setIsOpen(!isOpen)}
130
- className="w-full flex items-center justify-between p-1.5 trails-border-radius-input border trails-border-primary hover:trails-hover-bg transition-colors cursor-pointer"
165
+ onClick={
166
+ processedFeeOptions.length === 1
167
+ ? undefined
168
+ : () => setIsOpen(!isOpen)
169
+ }
170
+ disabled={processedFeeOptions.length === 1}
171
+ className={`w-full flex items-center justify-between p-1.5 trails-border-radius-input border trails-border-primary transition-colors ${
172
+ processedFeeOptions.length === 1
173
+ ? "cursor-default"
174
+ : "hover:trails-hover-bg cursor-pointer"
175
+ }`}
131
176
  >
132
177
  <div className="flex items-center space-x-2">
133
178
  {displayOption && (
@@ -154,16 +199,18 @@ export const FeeOptions: React.FC<FeeOptionsProps> = ({
154
199
  </div>
155
200
  </div>
156
201
  )}
157
- <ChevronDown
158
- className={`w-3 h-3 trails-text-muted transition-transform ${
159
- isOpen ? "transform rotate-180" : ""
160
- }`}
161
- />
202
+ {processedFeeOptions.length > 1 && (
203
+ <ChevronDown
204
+ className={`w-3 h-3 trails-text-muted transition-transform ${
205
+ isOpen ? "transform rotate-180" : ""
206
+ }`}
207
+ />
208
+ )}
162
209
  </div>
163
210
  </button>
164
211
 
165
212
  {/* Accordion Content */}
166
- {isOpen && (
213
+ {isOpen && processedFeeOptions.length > 1 && (
167
214
  <div className="mt-2 space-y-1">
168
215
  {/* All Fee Options (including native gas) */}
169
216
  {processedFeeOptions.map((option, index) => (
@@ -142,7 +142,6 @@ export const Fund: React.FC<FundProps> = ({
142
142
  setSelectedDestinationChain,
143
143
  quoteError,
144
144
  quoteErrorPrettified,
145
- isSameTokenWithoutCustomCalldata,
146
145
  destinationTokenAddress,
147
146
  isValidCustomToken,
148
147
  isSenderContractOnOrigin,
@@ -1149,7 +1148,6 @@ export const Fund: React.FC<FundProps> = ({
1149
1148
  !destinationTokenAddress ||
1150
1149
  !isValidCustomToken ||
1151
1150
  prepareSendQuote?.noSufficientBalance ||
1152
- isSameTokenWithoutCustomCalldata ||
1153
1151
  !prepareSendQuote
1154
1152
  }
1155
1153
  className="w-full font-semibold py-4 px-4 trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white disabled:text-gray-500 disabled:cursor-not-allowed cursor-pointer relative"
@@ -1159,8 +1157,6 @@ export const Fund: React.FC<FundProps> = ({
1159
1157
  <Loader2 className="w-5 h-5 animate-spin mr-2" />
1160
1158
  <span>{buttonText}</span>
1161
1159
  </div>
1162
- ) : isSameTokenWithoutCustomCalldata ? (
1163
- "Select Different Tokens"
1164
1160
  ) : prepareSendQuote?.noSufficientBalance ? (
1165
1161
  "Insufficient Balance"
1166
1162
  ) : !selectedRecipient ? (
@@ -38,6 +38,8 @@ import { FundingMethodSelectorButton } from "./FundingMethodSelectorButton.js"
38
38
  import { PercentageMaxButtons } from "./PercentageMaxButtons.js"
39
39
  import { useDynamicInputStyles } from "./DynamicInputStyles.js"
40
40
  import { DynamicSizeInputField } from "./DynamicSizeInputField.js"
41
+ import { isFeeOptionDisabled } from "./FeeOption.js"
42
+ import type { ProcessedFeeOption } from "../hooks/useSelectedFeeOption.js"
41
43
 
42
44
  interface PayProps {
43
45
  selectedToken?: any // Origin token (optional - user can select)
@@ -1019,18 +1021,23 @@ export const Pay: React.FC<PayProps> = ({
1019
1021
  ) : null}
1020
1022
 
1021
1023
  {/* Fee Options */}
1022
- {fundMethod !== "qr-code" && fundMethod !== "exchange" && (
1024
+ {fundMethod !== "qr-code" && fundMethod !== "exchange" && originToken && (
1023
1025
  <FeeOptions
1024
1026
  processedFeeOptions={processedFeeOptions}
1025
1027
  selectedFeeOption={selectedFeeOption}
1026
1028
  setSelectedFeeOption={(feeOption) => setSelectedFeeOption(feeOption)}
1027
- chainId={originToken?.chainId}
1029
+ chainId={originToken.chainId}
1028
1030
  isRefetching={isLoadingQuote}
1029
- originTokenInfo={{
1030
- originToken: originToken!,
1031
- originTokenBalance: originToken?.balance ?? "0",
1032
- originTokenAmount: tokenAmountForBackend ?? "0",
1033
- }}
1031
+ originTokenInfo={
1032
+ originToken
1033
+ ? {
1034
+ originToken,
1035
+ originTokenBalance: originToken.balance ?? "0",
1036
+ originTokenAmount:
1037
+ prepareSendQuote?.originAmountFormatted ?? "0",
1038
+ }
1039
+ : undefined
1040
+ }
1034
1041
  />
1035
1042
  )}
1036
1043
 
@@ -1058,7 +1065,15 @@ export const Pay: React.FC<PayProps> = ({
1058
1065
  !destinationTokenAddress ||
1059
1066
  !isValidCustomToken ||
1060
1067
  prepareSendQuote?.noSufficientBalance ||
1061
- !prepareSendQuote
1068
+ !prepareSendQuote ||
1069
+ selectedFeeOption
1070
+ ? isFeeOptionDisabled(selectedFeeOption as ProcessedFeeOption, {
1071
+ originToken: originToken,
1072
+ originTokenBalance: originToken?.balance ?? "0",
1073
+ originTokenAmount:
1074
+ prepareSendQuote?.originAmountFormatted ?? "0",
1075
+ })
1076
+ : false
1062
1077
  }
1063
1078
  className="w-full font-semibold py-4 px-4 trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white disabled:text-gray-500 disabled:cursor-not-allowed cursor-pointer relative"
1064
1079
  >
@@ -40,6 +40,8 @@ import { generateAaveDepositCalldata } from "../../aave.js"
40
40
  import { generateMorphoDepositCalldata } from "../../morpho.js"
41
41
  import { TRAILS_ROUTER_PLACEHOLDER_AMOUNT } from "../../trailsRouter.js"
42
42
  import { ScreenHeader } from "./ScreenHeader.js"
43
+ import type { ProcessedFeeOption } from "../hooks/useSelectedFeeOption.js"
44
+ import { isFeeOptionDisabled } from "./FeeOption.js"
43
45
 
44
46
  interface PoolDepositProps {
45
47
  account?: Account
@@ -626,7 +628,14 @@ export const PoolDeposit: React.FC<PoolDepositProps> = ({
626
628
  !selectedPool ||
627
629
  isLoadingQuote ||
628
630
  !prepareSendQuote ||
629
- prepareSendQuote?.noSufficientBalance
631
+ prepareSendQuote?.noSufficientBalance ||
632
+ selectedFeeOption
633
+ ? isFeeOptionDisabled(selectedFeeOption as ProcessedFeeOption, {
634
+ originToken: originToken!,
635
+ originTokenBalance: originToken?.balance ?? "0",
636
+ originTokenAmount: amount ?? "0",
637
+ })
638
+ : false
630
639
  }
631
640
  className={`w-full font-semibold py-4 px-4 trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white disabled:text-gray-500 disabled:cursor-not-allowed cursor-pointer relative`}
632
641
  >
@@ -72,6 +72,7 @@ export type UseQuoteProps = {
72
72
  abortSignal?: AbortSignal
73
73
  apiKey?: string | null
74
74
  nodeGatewayEnv?: "prod" | "dev" | "local" | "cors-anywhere"
75
+ isSmartWallet?: boolean | null
75
76
  }
76
77
 
77
78
  export type SwapReturn = {
@@ -149,6 +150,7 @@ export function useQuote({
149
150
  nodeGatewayEnv,
150
151
  abortSignal: externalAbortSignal,
151
152
  apiKey,
153
+ isSmartWallet,
152
154
  }: Partial<UseQuoteProps> = {}): UseQuoteReturn {
153
155
  // Set node gateway environment override for this quote session
154
156
  if (nodeGatewayEnv) {
@@ -212,6 +214,7 @@ export function useQuote({
212
214
  slippageTolerance,
213
215
  quoteProvider,
214
216
  apiKey,
217
+ isSmartWallet,
215
218
  ],
216
219
  queryFn: async () => {
217
220
  try {
@@ -467,6 +470,7 @@ export function useQuote({
467
470
  sequenceProjectAccessKey,
468
471
  originPublicClient: originPublicClient ?? undefined,
469
472
  destinationPublicClient: destinationPublicClient ?? undefined,
473
+ isSmartWallet: isSmartWallet ?? undefined,
470
474
  }
471
475
 
472
476
  logger.console.log("[trails-sdk] options", options)
@@ -50,6 +50,7 @@ import {
50
50
  useSelectedFeeOption,
51
51
  type ProcessedFeeOption,
52
52
  } from "./useSelectedFeeOption.js"
53
+ import { useWidgetProps } from "./useWidgetProps.js"
53
54
 
54
55
  export interface Token {
55
56
  id: number
@@ -170,7 +171,6 @@ export type UseSendReturn = {
170
171
  toAmountDisplay: string
171
172
  quoteError: string | null
172
173
  quoteErrorPrettified: string | null
173
- isSameTokenWithoutCustomCalldata: boolean
174
174
  isRecipientContract: boolean
175
175
  isSenderContractOnOrigin: boolean
176
176
  isSenderContractOnDestination: boolean
@@ -206,6 +206,9 @@ export function useSendForm({
206
206
  const { connector } = useAccount()
207
207
  const walletId = connector?.id
208
208
 
209
+ // Read isSmartWallet from widget props
210
+ const { isSmartWallet } = useWidgetProps()
211
+
209
212
  // Auto-set quoteProvider to "lifi" if either from or to chain is etherlink
210
213
  const effectiveQuoteProvider = useMemo(() => {
211
214
  if (!quoteProvider || quoteProvider === "auto") {
@@ -742,6 +745,67 @@ export function useSendForm({
742
745
 
743
746
  // Get quote automatically when inputs change
744
747
  const getQuote = useCallback(async () => {
748
+ // Debug log: comprehensive state check
749
+ const debugState = {
750
+ account: account ? { address: account.address } : null,
751
+ amount,
752
+ amountRaw,
753
+ destinationTokenAddress,
754
+ destinationTokenAddressFromTokenSymbol,
755
+ isValidRecipient,
756
+ selectedDestToken: selectedDestToken
757
+ ? {
758
+ symbol: selectedDestToken.symbol,
759
+ name: selectedDestToken.name,
760
+ decimals: selectedDestToken.decimals,
761
+ }
762
+ : null,
763
+ selectedDestinationChain: selectedDestinationChain
764
+ ? {
765
+ id: selectedDestinationChain.id,
766
+ name: selectedDestinationChain.name,
767
+ }
768
+ : null,
769
+ selectedToken: selectedToken
770
+ ? {
771
+ symbol: selectedToken.symbol,
772
+ contractAddress: selectedToken.contractAddress,
773
+ chainId: selectedToken.chainId,
774
+ contractInfo: selectedToken.contractInfo,
775
+ }
776
+ : null,
777
+ isCustomToken,
778
+ toToken,
779
+ recipient,
780
+ conditions: {
781
+ hasAccount: !!account,
782
+ hasAmount: !!amount,
783
+ hasAmountRaw: !!amountRaw,
784
+ amountNotZero: amount !== "0",
785
+ amountRawNotZero: amountRaw !== "0",
786
+ hasDestinationTokenAddress: !!destinationTokenAddress,
787
+ hasDestinationTokenAddressFromSymbol:
788
+ !!destinationTokenAddressFromTokenSymbol,
789
+ isValidRecipientValue: isValidRecipient,
790
+ hasSelectedDestToken: !!selectedDestToken,
791
+ hasSelectedDestinationChain: !!selectedDestinationChain,
792
+ hasSelectedToken: !!selectedToken,
793
+ },
794
+ allConditionsMet:
795
+ !!account &&
796
+ !!amount &&
797
+ !!destinationTokenAddress &&
798
+ isValidRecipient &&
799
+ !!selectedDestToken &&
800
+ !!selectedDestinationChain &&
801
+ amount !== "0" &&
802
+ !!amountRaw &&
803
+ amountRaw !== "0" &&
804
+ !!selectedToken,
805
+ }
806
+
807
+ logger.console.log("[trails-sdk] [DEBUG] getQuote state check", debugState)
808
+
745
809
  // Only get quote if all required inputs are present
746
810
  if (
747
811
  !account ||
@@ -765,6 +829,7 @@ export function useSendForm({
765
829
  selectedDestinationChain,
766
830
  amountRaw,
767
831
  selectedToken,
832
+ debugState,
768
833
  },
769
834
  )
770
835
  setQuoteError(null)
@@ -918,6 +983,7 @@ export function useSendForm({
918
983
  sequenceProjectAccessKey: trailsApiKey,
919
984
  originPublicClient: originChainPublicClient ?? undefined,
920
985
  destinationPublicClient: destinationChainPublicClient ?? undefined,
986
+ isSmartWallet,
921
987
  }
922
988
 
923
989
  logger.console.log(
@@ -982,6 +1048,9 @@ export function useSendForm({
982
1048
  selectedDestToken?.decimals,
983
1049
  recipient,
984
1050
  destinationTokenAddress,
1051
+ destinationTokenAddressFromTokenSymbol,
1052
+ isCustomToken,
1053
+ toToken,
985
1054
  sequenceIndexerUrl,
986
1055
  selectedDestToken?.symbol,
987
1056
  selectedDestinationChain?.id,
@@ -1009,6 +1078,7 @@ export function useSendForm({
1009
1078
  buildQuoteInputsFingerprint,
1010
1079
  originChainPublicClient,
1011
1080
  destinationChainPublicClient,
1081
+ isSmartWallet,
1012
1082
  ])
1013
1083
 
1014
1084
  // Auto-fetch quotes when inputs change (debounced)
@@ -1432,40 +1502,6 @@ export function useSendForm({
1432
1502
  }
1433
1503
  }, [quoteError])
1434
1504
 
1435
- // Check if origin and destination tokens are the same (same contract address on same chain)
1436
- // Only block same-token transactions when there's no custom calldata AND recipient is the same as sender
1437
- const isSameTokenWithoutCustomCalldata = useMemo(() => {
1438
- if (
1439
- !selectedToken ||
1440
- !selectedToken.contractAddress ||
1441
- !destinationTokenAddress ||
1442
- !selectedToken?.chainId ||
1443
- !selectedDestinationChain?.id
1444
- ) {
1445
- return false
1446
- }
1447
- const isSameChainAndToken =
1448
- selectedToken.contractAddress.toLowerCase() ===
1449
- destinationTokenAddress.toLowerCase() &&
1450
- selectedToken.chainId === selectedDestinationChain?.id
1451
-
1452
- // Allow same-token transactions if:
1453
- // 1. There's custom calldata (e.g., NFT minting)
1454
- // 2. Recipient is different from sender (simple transfer to another address)
1455
- const recipientIsSameAsSender =
1456
- recipient?.toLowerCase() === account?.address?.toLowerCase()
1457
-
1458
- return isSameChainAndToken && !toCalldata && recipientIsSameAsSender
1459
- }, [
1460
- selectedToken,
1461
- destinationTokenAddress,
1462
- selectedDestinationChain,
1463
- toCalldata,
1464
- selectedToken?.chainId,
1465
- recipient,
1466
- account?.address,
1467
- ])
1468
-
1469
1505
  return {
1470
1506
  amount,
1471
1507
  amountRaw,
@@ -1511,7 +1547,6 @@ export function useSendForm({
1511
1547
  prepareSendQuote: prepareSendResult?.quote ?? null,
1512
1548
  quoteError,
1513
1549
  quoteErrorPrettified,
1514
- isSameTokenWithoutCustomCalldata,
1515
1550
  isRecipientContract,
1516
1551
  isSenderContractOnOrigin,
1517
1552
  isSenderContractOnDestination,
@@ -275,6 +275,7 @@ export type TrailsWidgetProps = {
275
275
  appImageUrl?: string
276
276
  appDescription?: string
277
277
  payMessage?: string
278
+ isSmartWallet?: boolean
278
279
  }
279
280
 
280
281
  export interface TrailsWidgetRef {