0xtrails 0.6.0 → 0.6.2
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.
- package/dist/{ccip-Dw5AN7oU.js → ccip-CZfykYU7.js} +4 -4
- package/dist/chains.d.ts +9 -2
- package/dist/chains.d.ts.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/contractUtils.d.ts +2 -1
- package/dist/contractUtils.d.ts.map +1 -1
- package/dist/{index-BtVUTbEZ.js → index-S9pphnT9.js} +29732 -36767
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +300 -242
- package/dist/prepareSend.d.ts.map +1 -1
- package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
- package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
- package/dist/transactionIntent/types.d.ts +3 -1
- package/dist/transactionIntent/types.d.ts.map +1 -1
- package/dist/widget/components/ChainImage.d.ts.map +1 -1
- package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
- package/dist/widget/components/DepositTracker.d.ts +12 -0
- package/dist/widget/components/DepositTracker.d.ts.map +1 -0
- package/dist/widget/components/Disconnect.d.ts.map +1 -1
- package/dist/widget/components/FeeBreakdown.d.ts.map +1 -1
- package/dist/widget/components/QRCodeDeposit.d.ts.map +1 -1
- package/dist/widget/components/QRCodeOptions.d.ts +9 -0
- package/dist/widget/components/QRCodeOptions.d.ts.map +1 -0
- package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
- package/dist/widget/components/Receipt.d.ts.map +1 -1
- package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
- package/dist/widget/components/Toast.d.ts.map +1 -1
- package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
- package/dist/widget/components/WalletConnectionPending.d.ts.map +1 -1
- package/dist/widget/css/compiled.css +1 -1
- package/dist/widget/css/index.css +103 -38
- package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
- package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
- package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
- package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
- package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
- package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
- package/dist/widget/hooks/useQuote.d.ts.map +1 -1
- package/dist/widget/hooks/useSelectedFeeOption.d.ts.map +1 -1
- package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
- package/dist/widget/index.js +1 -1
- package/dist/widget/widget.d.ts.map +1 -1
- package/package.json +6 -9
- package/src/chains.ts +37 -4
- package/src/constants.ts +1 -0
- package/src/contractUtils.ts +8 -7
- package/src/estimate.ts +2 -2
- package/src/index.ts +2 -0
- package/src/intents.ts +2 -2
- package/src/paymasterSend.ts +2 -2
- package/src/prepareSend.ts +34 -3
- package/src/sendUserOp.ts +2 -2
- package/src/tokens.ts +2 -2
- package/src/transactionIntent/deposits/gaslessDeposit.ts +2 -2
- package/src/transactionIntent/handlers/crossChain.ts +51 -2
- package/src/transactionIntent/handlers/sameChainSameToken.ts +52 -2
- package/src/transactionIntent/quote/normalizeQuote.ts +2 -2
- package/src/transactionIntent/types.ts +9 -1
- package/src/widget/compiled.css +1 -1
- package/src/widget/components/ChainImage.tsx +10 -7
- package/src/widget/components/ChainList.tsx +1 -1
- package/src/widget/components/ClassicSwap.tsx +8 -4
- package/src/widget/components/ConnectedWallets.tsx +1 -1
- package/src/widget/components/DepositTracker.tsx +298 -0
- package/src/widget/components/Disconnect.tsx +24 -3
- package/src/widget/components/FeeBreakdown.tsx +3 -3
- package/src/widget/components/QRCodeDeposit.tsx +29 -19
- package/src/widget/components/QRCodeOptions.tsx +65 -0
- package/src/widget/components/QuoteDetails.tsx +694 -803
- package/src/widget/components/Receipt.tsx +76 -40
- package/src/widget/components/ThemeProvider.tsx +7 -12
- package/src/widget/components/Toast.tsx +3 -2
- package/src/widget/components/TokenSelector.tsx +1 -1
- package/src/widget/components/Tooltip.tsx +1 -1
- package/src/widget/components/TransferPendingVertical.tsx +11 -2
- package/src/widget/components/WalletConnectionPending.tsx +28 -5
- package/src/widget/hooks/useCheckout.ts +10 -2
- package/src/widget/hooks/useCurrentScreen.tsx +1 -0
- package/src/widget/hooks/useDebugScreens.ts +1 -0
- package/src/widget/hooks/useIntentTransactionHistory.ts +114 -143
- package/src/widget/hooks/useQuote.ts +92 -6
- package/src/widget/hooks/useSelectedFeeOption.tsx +86 -29
- package/src/widget/hooks/useSendForm.ts +43 -7
- package/src/widget/index.css +103 -38
- package/src/widget/widget.tsx +48 -5
- package/dist/0xtrails.css +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useCallback, useState } from "react"
|
|
2
|
+
import { useQuery } from "@tanstack/react-query"
|
|
2
3
|
import {
|
|
3
4
|
getAccountTransactionHistory,
|
|
4
5
|
type IntentTransaction,
|
|
@@ -9,12 +10,12 @@ import { logger } from "../../logger.js"
|
|
|
9
10
|
import { useTrails } from "../providers/TrailsProvider.js"
|
|
10
11
|
import { commonTokenImages } from "../../tokens.js"
|
|
11
12
|
import { getTrailsClient } from "../../trailsClient.js"
|
|
12
|
-
import {
|
|
13
|
+
import type { IntentSummary } from "@0xsequence/trails-api"
|
|
13
14
|
import {
|
|
14
15
|
decodeTrailsTokenSweeperEvents,
|
|
15
16
|
decodeGuestModuleEvents,
|
|
16
17
|
} from "../../decoders.js"
|
|
17
|
-
import {
|
|
18
|
+
import { getChainRpcClient } from "../../chains.js"
|
|
18
19
|
import { getChainInfo } from "../../chains.js"
|
|
19
20
|
import { isNativeToken } from "../../utils.js"
|
|
20
21
|
|
|
@@ -443,7 +444,7 @@ async function decodeTransactionEvents(
|
|
|
443
444
|
continue
|
|
444
445
|
}
|
|
445
446
|
|
|
446
|
-
const publicClient =
|
|
447
|
+
const publicClient = getChainRpcClient(chainInfo.id)
|
|
447
448
|
const receipt = await publicClient.getTransactionReceipt({
|
|
448
449
|
hash: hash as `0x${string}`,
|
|
449
450
|
})
|
|
@@ -724,46 +725,57 @@ export type UseIntentTransactionHistoryReturn = {
|
|
|
724
725
|
totalPages?: number
|
|
725
726
|
}
|
|
726
727
|
|
|
728
|
+
// ============================================================================
|
|
729
|
+
// Main Hook
|
|
730
|
+
// ============================================================================
|
|
731
|
+
|
|
727
732
|
export function useIntentTransactionHistory({
|
|
728
733
|
accountAddress,
|
|
729
734
|
pageSize = 10,
|
|
730
735
|
enabled = true,
|
|
731
736
|
}: UseIntentTransactionHistoryParams): UseIntentTransactionHistoryReturn {
|
|
732
737
|
const trailsConfig = useTrails()
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const [error, setError] = useState<string | null>(null)
|
|
738
|
+
|
|
739
|
+
// Page state - store the full page object from API
|
|
736
740
|
const [page, setPage] = useState(0)
|
|
737
|
-
const [
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
741
|
+
const [pageHistory, setPageHistory] = useState<any[]>([undefined]) // undefined = first page (no page param needed)
|
|
742
|
+
|
|
743
|
+
// Get current page object
|
|
744
|
+
const currentPageObject = pageHistory[page]
|
|
745
|
+
|
|
746
|
+
// TanStack Query for fetching paginated data
|
|
747
|
+
const { data, isLoading, error, refetch, isFetching } = useQuery({
|
|
748
|
+
queryKey: [
|
|
749
|
+
"intentTransactionHistory",
|
|
750
|
+
accountAddress,
|
|
751
|
+
pageSize,
|
|
752
|
+
page, // Include page index in key for proper caching
|
|
753
|
+
enabled,
|
|
754
|
+
],
|
|
755
|
+
queryFn: async () => {
|
|
756
|
+
if (!accountAddress || !trailsConfig.trailsApiKey) {
|
|
757
|
+
throw new Error("Account address and API key required")
|
|
752
758
|
}
|
|
753
759
|
|
|
754
|
-
const trailsClient = getTrailsClient({
|
|
760
|
+
const trailsClient = getTrailsClient({
|
|
761
|
+
apiKey: trailsConfig.trailsApiKey,
|
|
762
|
+
hostname: trailsConfig.trailsApiUrl!,
|
|
763
|
+
})
|
|
755
764
|
|
|
756
|
-
const requestParams = {
|
|
765
|
+
const requestParams: any = {
|
|
757
766
|
byOwnerAddress: accountAddress,
|
|
758
|
-
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Build page object with pageSize for first request
|
|
770
|
+
// For subsequent requests, pass the opaque nextPage object which includes pageSize
|
|
771
|
+
if (currentPageObject) {
|
|
772
|
+
// Subsequent pages - use the opaque nextPage object as-is
|
|
773
|
+
requestParams.page = currentPageObject
|
|
774
|
+
} else {
|
|
775
|
+
// First page - set initial pageSize
|
|
776
|
+
requestParams.page = {
|
|
759
777
|
pageSize,
|
|
760
|
-
|
|
761
|
-
{
|
|
762
|
-
column: "created_at",
|
|
763
|
-
order: SortOrder.DESC,
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
},
|
|
778
|
+
}
|
|
767
779
|
}
|
|
768
780
|
|
|
769
781
|
logger.console.log(
|
|
@@ -776,142 +788,101 @@ export function useIntentTransactionHistory({
|
|
|
776
788
|
|
|
777
789
|
logger.console.log("[trails-sdk] getIntentTransactionHistory response:", {
|
|
778
790
|
intentCount: apiResult.intents?.length || 0,
|
|
779
|
-
hasNextPage:
|
|
780
|
-
|
|
791
|
+
hasNextPage: apiResult.nextPage?.more ?? false,
|
|
792
|
+
pageObject: apiResult.nextPage,
|
|
781
793
|
})
|
|
782
794
|
|
|
783
|
-
// Map
|
|
795
|
+
// Map and enrich transactions
|
|
784
796
|
const mappedTransactions: IntentTransaction[] = (
|
|
785
797
|
apiResult.intents || []
|
|
786
798
|
).map(mapIntentSummaryToTransaction)
|
|
787
799
|
|
|
800
|
+
const supportedTokens = await getSupportedTokens()
|
|
801
|
+
|
|
802
|
+
const enrichedTransactions = await Promise.all(
|
|
803
|
+
mappedTransactions.map(async (transaction) =>
|
|
804
|
+
enrichTransaction(transaction, supportedTokens, trailsConfig),
|
|
805
|
+
),
|
|
806
|
+
)
|
|
807
|
+
|
|
788
808
|
return {
|
|
789
|
-
transactions:
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
page: pageNum,
|
|
793
|
-
pageSize,
|
|
794
|
-
more: apiResult.nextPage.more || false,
|
|
795
|
-
}
|
|
796
|
-
: {
|
|
797
|
-
page: pageNum,
|
|
798
|
-
pageSize,
|
|
799
|
-
more: false,
|
|
800
|
-
},
|
|
809
|
+
transactions: enrichedTransactions,
|
|
810
|
+
nextPage: apiResult.nextPage,
|
|
811
|
+
hasMore: apiResult.nextPage?.more ?? false,
|
|
801
812
|
}
|
|
802
813
|
},
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
async (pageNum: number = 0, append: boolean = false) => {
|
|
808
|
-
if (!accountAddress || !enabled) return
|
|
809
|
-
|
|
810
|
-
setLoading(true)
|
|
811
|
-
setError(null)
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
const apiKey = trailsConfig.trailsApiKey || ""
|
|
815
|
-
const apiUrl = trailsConfig.trailsApiUrl!
|
|
814
|
+
enabled: enabled && !!accountAddress && !!trailsConfig.trailsApiKey,
|
|
815
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
816
|
+
placeholderData: (previousData) => previousData, // Keep previous data while fetching
|
|
817
|
+
})
|
|
816
818
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
pageSize,
|
|
821
|
-
pageNum,
|
|
822
|
-
apiKey,
|
|
823
|
-
apiUrl,
|
|
824
|
-
)
|
|
819
|
+
const transactions = data?.transactions ?? []
|
|
820
|
+
const hasMore = data?.hasMore ?? false
|
|
821
|
+
const nextPageObject = data?.nextPage
|
|
825
822
|
|
|
826
|
-
|
|
827
|
-
|
|
823
|
+
const refetchPage = useCallback(() => {
|
|
824
|
+
refetch()
|
|
825
|
+
}, [refetch])
|
|
828
826
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
(result.transactions || []).map(async (transaction) =>
|
|
832
|
-
enrichTransaction(transaction, supportedTokens, trailsConfig),
|
|
833
|
-
),
|
|
834
|
-
)
|
|
827
|
+
const nextPageHandler = useCallback(() => {
|
|
828
|
+
if (!hasMore || isLoading) return
|
|
835
829
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
830
|
+
// Check if next page is already cached
|
|
831
|
+
if (pageHistory[page + 1]) {
|
|
832
|
+
logger.console.log("[trails-sdk] Using cached page", page + 1)
|
|
833
|
+
setPage((p) => p + 1)
|
|
834
|
+
return
|
|
835
|
+
}
|
|
841
836
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
err,
|
|
853
|
-
)
|
|
854
|
-
} finally {
|
|
855
|
-
setLoading(false)
|
|
856
|
-
}
|
|
857
|
-
},
|
|
858
|
-
[
|
|
859
|
-
accountAddress,
|
|
860
|
-
enabled,
|
|
861
|
-
pageSize,
|
|
862
|
-
trailsConfig,
|
|
863
|
-
fetchIntentTransactionHistoryFromApi,
|
|
864
|
-
],
|
|
865
|
-
)
|
|
837
|
+
// Fetch next page by storing the nextPage object and incrementing page index
|
|
838
|
+
if (nextPageObject) {
|
|
839
|
+
setPageHistory((prev) => {
|
|
840
|
+
const newHistory = [...prev]
|
|
841
|
+
newHistory[page + 1] = nextPageObject
|
|
842
|
+
return newHistory
|
|
843
|
+
})
|
|
844
|
+
setPage((p) => p + 1)
|
|
845
|
+
}
|
|
846
|
+
}, [hasMore, isLoading, nextPageObject, page, pageHistory])
|
|
866
847
|
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
}
|
|
848
|
+
const prevPageHandler = useCallback(() => {
|
|
849
|
+
if (page <= 0 || isLoading) return
|
|
870
850
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
}
|
|
851
|
+
// Go back to previous page
|
|
852
|
+
logger.console.log("[trails-sdk] Going to previous page", page - 1)
|
|
853
|
+
setPage((p) => p - 1)
|
|
854
|
+
}, [page, isLoading])
|
|
877
855
|
|
|
878
|
-
const
|
|
879
|
-
if (hasMore
|
|
880
|
-
const next = page + 1
|
|
881
|
-
fetchTransactions(next, false)
|
|
882
|
-
}
|
|
883
|
-
}
|
|
856
|
+
const loadMoreHandler = useCallback(() => {
|
|
857
|
+
if (!hasMore || isFetching) return
|
|
884
858
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
859
|
+
if (nextPageObject) {
|
|
860
|
+
setPageHistory((prev) => {
|
|
861
|
+
const newHistory = [...prev]
|
|
862
|
+
newHistory[page + 1] = nextPageObject
|
|
863
|
+
return newHistory
|
|
864
|
+
})
|
|
865
|
+
setPage((p) => p + 1)
|
|
889
866
|
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const hasPrev = page > 0
|
|
867
|
+
}, [hasMore, isFetching, nextPageObject, page])
|
|
893
868
|
|
|
869
|
+
// Reset on account change
|
|
870
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: accountAddress is an external dependency
|
|
894
871
|
useEffect(() => {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
setTransactions([])
|
|
899
|
-
setError(null)
|
|
900
|
-
setHasMore(false)
|
|
901
|
-
setPage(0)
|
|
902
|
-
}
|
|
903
|
-
}, [accountAddress, enabled, fetchTransactions])
|
|
872
|
+
setPage(0)
|
|
873
|
+
setPageHistory([undefined])
|
|
874
|
+
}, [accountAddress])
|
|
904
875
|
|
|
905
876
|
return {
|
|
906
877
|
transactions,
|
|
907
|
-
loading,
|
|
908
|
-
error,
|
|
909
|
-
refetch,
|
|
910
|
-
loadMore,
|
|
878
|
+
loading: isLoading || isFetching,
|
|
879
|
+
error: error instanceof Error ? error.message : null,
|
|
880
|
+
refetch: refetchPage,
|
|
881
|
+
loadMore: loadMoreHandler,
|
|
911
882
|
hasMore,
|
|
912
883
|
page,
|
|
913
|
-
nextPage,
|
|
914
|
-
prevPage,
|
|
915
|
-
hasPrev,
|
|
884
|
+
nextPage: nextPageHandler,
|
|
885
|
+
prevPage: prevPageHandler,
|
|
886
|
+
hasPrev: page > 0,
|
|
916
887
|
}
|
|
917
888
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query"
|
|
2
2
|
import { useRef } from "react"
|
|
3
3
|
import type { TransactionReceipt } from "viem"
|
|
4
|
-
import { zeroAddress } from "viem"
|
|
4
|
+
import { zeroAddress, erc20Abi } from "viem"
|
|
5
5
|
import { useIndexerGatewayClient } from "../../indexerClient.js"
|
|
6
6
|
import { getTokenBalancesWithPrices } from "../../tokenBalances.js"
|
|
7
7
|
import { logger } from "../../logger.js"
|
|
@@ -13,7 +13,7 @@ import { getFullErrorMessage, getPrettifiedErrorMessage } from "../../error.js"
|
|
|
13
13
|
import { prepareSend } from "../../prepareSend.js"
|
|
14
14
|
import { abortControllerRegistry } from "../../abortController.js"
|
|
15
15
|
import { TradeType } from "../../prepareSend.js"
|
|
16
|
-
import { getChainInfo } from "../../chains.js"
|
|
16
|
+
import { getChainInfo, useChainRpcClient } from "../../chains.js"
|
|
17
17
|
import type { IntentTransaction } from "@0xsequence/trails-api"
|
|
18
18
|
import type { PrepareSendOptions } from "../../transactionIntent/types.js"
|
|
19
19
|
import type { Chain } from "../../chains.js"
|
|
@@ -188,6 +188,10 @@ export function useQuote({
|
|
|
188
188
|
})
|
|
189
189
|
const indexerGatewayClient = useIndexerGatewayClient()
|
|
190
190
|
|
|
191
|
+
// Get RPC clients for known chainIds using hooks (reads from TrailsProvider context)
|
|
192
|
+
const originPublicClient = useChainRpcClient(fromChainId ?? undefined)
|
|
193
|
+
const destinationPublicClient = useChainRpcClient(toChainId ?? undefined)
|
|
194
|
+
|
|
191
195
|
const { supportedTokens } = useSupportedTokens()
|
|
192
196
|
|
|
193
197
|
// Get mutation hooks for passing to prepareSend
|
|
@@ -312,6 +316,52 @@ export function useQuote({
|
|
|
312
316
|
)
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
// Helper function to fetch decimals on-chain if not found in token list
|
|
320
|
+
const fetchDecimalsOnChain = async (
|
|
321
|
+
tokenAddress: string,
|
|
322
|
+
chainId: number,
|
|
323
|
+
): Promise<number | null> => {
|
|
324
|
+
try {
|
|
325
|
+
// Use the hook-based RPC clients that are already available
|
|
326
|
+
const publicClient =
|
|
327
|
+
chainId === fromChainId
|
|
328
|
+
? originPublicClient
|
|
329
|
+
: chainId === toChainId
|
|
330
|
+
? destinationPublicClient
|
|
331
|
+
: null
|
|
332
|
+
|
|
333
|
+
if (!publicClient) {
|
|
334
|
+
logger.console.warn(
|
|
335
|
+
`[trails-sdk] No RPC client available for chain ${chainId}`,
|
|
336
|
+
)
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// For native tokens, return 18 decimals
|
|
341
|
+
if (tokenAddress.toLowerCase() === zeroAddress.toLowerCase()) {
|
|
342
|
+
const chainInfo = getChainInfo(chainId)
|
|
343
|
+
return chainInfo?.nativeCurrency.decimals ?? 18
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const decimals = await publicClient.readContract({
|
|
347
|
+
address: tokenAddress as `0x${string}`,
|
|
348
|
+
abi: erc20Abi,
|
|
349
|
+
functionName: "decimals",
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
logger.console.log(
|
|
353
|
+
`[trails-sdk] Fetched decimals on-chain for token ${tokenAddress} on chain ${chainId}: ${decimals}`,
|
|
354
|
+
)
|
|
355
|
+
return decimals
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger.console.error(
|
|
358
|
+
`[trails-sdk] Error fetching decimals on-chain for token ${tokenAddress} on chain ${chainId}:`,
|
|
359
|
+
error,
|
|
360
|
+
)
|
|
361
|
+
return null
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
315
365
|
const originToken = supportedTokens?.find(
|
|
316
366
|
(token) =>
|
|
317
367
|
token.contractAddress?.toLowerCase() ===
|
|
@@ -323,10 +373,27 @@ export function useQuote({
|
|
|
323
373
|
toTokenAddress?.toLowerCase() && token.chainId === toChainId,
|
|
324
374
|
)
|
|
325
375
|
|
|
326
|
-
|
|
376
|
+
let sourceTokenDecimals = originToken?.decimals
|
|
377
|
+
if (!sourceTokenDecimals && fromTokenAddress && fromChainId) {
|
|
378
|
+
logger.console.warn(
|
|
379
|
+
"[trails-sdk] [useQuote] Source token decimals not found in token list, fetching on-chain:",
|
|
380
|
+
{
|
|
381
|
+
originToken,
|
|
382
|
+
fromTokenAddress,
|
|
383
|
+
fromChainId,
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
const onChainDecimals = await fetchDecimalsOnChain(
|
|
387
|
+
fromTokenAddress,
|
|
388
|
+
fromChainId,
|
|
389
|
+
)
|
|
390
|
+
if (onChainDecimals !== null) {
|
|
391
|
+
sourceTokenDecimals = onChainDecimals
|
|
392
|
+
}
|
|
393
|
+
}
|
|
327
394
|
if (!sourceTokenDecimals) {
|
|
328
395
|
logger.console.error(
|
|
329
|
-
"[trails-sdk] [useQuote]
|
|
396
|
+
"[trails-sdk] [useQuote] Source token decimals not found:",
|
|
330
397
|
{
|
|
331
398
|
originToken,
|
|
332
399
|
fromTokenAddress,
|
|
@@ -335,10 +402,27 @@ export function useQuote({
|
|
|
335
402
|
)
|
|
336
403
|
throw new Error("Source token decimals not found")
|
|
337
404
|
}
|
|
338
|
-
|
|
405
|
+
let destinationTokenDecimals = destinationToken?.decimals
|
|
406
|
+
if (!destinationTokenDecimals && toTokenAddress && toChainId) {
|
|
407
|
+
logger.console.warn(
|
|
408
|
+
"[trails-sdk] Destination token decimals not found in token list, fetching on-chain:",
|
|
409
|
+
{
|
|
410
|
+
destinationToken,
|
|
411
|
+
toTokenAddress,
|
|
412
|
+
toChainId,
|
|
413
|
+
},
|
|
414
|
+
)
|
|
415
|
+
const onChainDecimals = await fetchDecimalsOnChain(
|
|
416
|
+
toTokenAddress,
|
|
417
|
+
toChainId,
|
|
418
|
+
)
|
|
419
|
+
if (onChainDecimals !== null) {
|
|
420
|
+
destinationTokenDecimals = onChainDecimals
|
|
421
|
+
}
|
|
422
|
+
}
|
|
339
423
|
if (!destinationTokenDecimals) {
|
|
340
424
|
logger.console.error(
|
|
341
|
-
"[trails-sdk]
|
|
425
|
+
"[trails-sdk] Destination token decimals not found:",
|
|
342
426
|
{
|
|
343
427
|
destinationToken,
|
|
344
428
|
toTokenAddress,
|
|
@@ -381,6 +465,8 @@ export function useQuote({
|
|
|
381
465
|
checkoutOnHandlers,
|
|
382
466
|
sequenceIndexerUrl,
|
|
383
467
|
sequenceProjectAccessKey,
|
|
468
|
+
originPublicClient: originPublicClient ?? undefined,
|
|
469
|
+
destinationPublicClient: destinationPublicClient ?? undefined,
|
|
384
470
|
}
|
|
385
471
|
|
|
386
472
|
logger.console.log("[trails-sdk] options", options)
|
|
@@ -13,6 +13,8 @@ import { logger } from "../../index.js"
|
|
|
13
13
|
import { isNativeToken, normalizeAddress } from "../../utils.js"
|
|
14
14
|
import { isAddressEqual } from "viem"
|
|
15
15
|
|
|
16
|
+
const FEE_OPTION_PREFERENCE_KEY = "trails-fee-option-preference"
|
|
17
|
+
|
|
16
18
|
const createBalanceKey = (
|
|
17
19
|
chainId?: number,
|
|
18
20
|
tokenAddress?: string | null,
|
|
@@ -101,8 +103,7 @@ export const SelectedFeeOptionProvider: React.FC<
|
|
|
101
103
|
> = ({ children, initialToken = null }) => {
|
|
102
104
|
const [selectedFeeOption, setSelectedFeeOptionInternalRaw] =
|
|
103
105
|
useState<FeeOption | null>(initialToken)
|
|
104
|
-
const [
|
|
105
|
-
useState(false)
|
|
106
|
+
const [hasUserSelected, setHasUserSelected] = useState(false)
|
|
106
107
|
const [feeOptions, setFeeOptionsInternal] = useState<FeeOption[]>([])
|
|
107
108
|
const [originTokenChainId, setOriginTokenChainId] = useState<
|
|
108
109
|
number | undefined
|
|
@@ -123,12 +124,23 @@ export const SelectedFeeOptionProvider: React.FC<
|
|
|
123
124
|
// Wrapper to track when user makes an explicit selection
|
|
124
125
|
const setSelectedFeeOption = useCallback((token: FeeOption | null) => {
|
|
125
126
|
setSelectedFeeOptionInternalRaw(token)
|
|
126
|
-
|
|
127
|
+
setHasUserSelected(true)
|
|
128
|
+
|
|
129
|
+
// Save preference to localStorage: "native" or "erc20"
|
|
130
|
+
const preference = token === null ? "native" : "erc20"
|
|
131
|
+
try {
|
|
132
|
+
localStorage.setItem(FEE_OPTION_PREFERENCE_KEY, preference)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.console.warn(
|
|
135
|
+
"[trails-sdk] Failed to save fee option preference to localStorage",
|
|
136
|
+
error,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
127
139
|
}, [])
|
|
128
140
|
|
|
129
141
|
const clearSelectedFeeOption = useCallback(() => {
|
|
130
142
|
setSelectedFeeOptionInternalRaw(null)
|
|
131
|
-
|
|
143
|
+
setHasUserSelected(false)
|
|
132
144
|
}, [])
|
|
133
145
|
|
|
134
146
|
// Process raw fee options with balance checks
|
|
@@ -169,44 +181,89 @@ export const SelectedFeeOptionProvider: React.FC<
|
|
|
169
181
|
)
|
|
170
182
|
}, [feeOptions, originTokenChainId, tokenBalanceLookup])
|
|
171
183
|
|
|
172
|
-
// Auto-select
|
|
173
|
-
// NOTE: If sufficient balance, it will usually resolve to the native gas option because native gas is the first option in the list returned by the API
|
|
184
|
+
// Auto-select fee option: check preference, then fallback to first option
|
|
174
185
|
useEffect(() => {
|
|
186
|
+
// Clear selection if no options available
|
|
175
187
|
if (processedFeeOptions.length === 0) {
|
|
176
188
|
if (selectedFeeOption !== null) {
|
|
177
189
|
setSelectedFeeOptionInternalRaw(null)
|
|
178
|
-
|
|
190
|
+
setHasUserSelected(false)
|
|
179
191
|
}
|
|
180
192
|
return
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
// If user has made a selection, validate and update it
|
|
196
|
+
if (hasUserSelected) {
|
|
197
|
+
// Check if current selection is still valid
|
|
198
|
+
const isSelectionValid =
|
|
199
|
+
selectedFeeOption === null
|
|
200
|
+
? processedFeeOptions.some((option) =>
|
|
201
|
+
isNativeToken(option.tokenAddress),
|
|
202
|
+
)
|
|
203
|
+
: processedFeeOptions.some((option) =>
|
|
204
|
+
isAddressEqual(
|
|
205
|
+
option.tokenAddress as `0x${string}`,
|
|
206
|
+
selectedFeeOption.tokenAddress as `0x${string}`,
|
|
207
|
+
),
|
|
208
|
+
)
|
|
191
209
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
if (isSelectionValid) {
|
|
211
|
+
// Update to latest object if it's an ERC20 token
|
|
212
|
+
if (selectedFeeOption !== null) {
|
|
213
|
+
const updatedOption = processedFeeOptions.find((option) =>
|
|
214
|
+
isAddressEqual(
|
|
215
|
+
option.tokenAddress as `0x${string}`,
|
|
216
|
+
selectedFeeOption.tokenAddress as `0x${string}`,
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
if (updatedOption && updatedOption !== selectedFeeOption) {
|
|
220
|
+
setSelectedFeeOptionInternalRaw(updatedOption)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return // Keep user's selection
|
|
224
|
+
} else {
|
|
225
|
+
// Selection is invalid, clear it and allow auto-selection below
|
|
226
|
+
setSelectedFeeOptionInternalRaw(null)
|
|
227
|
+
setHasUserSelected(false)
|
|
228
|
+
}
|
|
196
229
|
}
|
|
197
230
|
|
|
198
|
-
|
|
199
|
-
const fallbackOption =
|
|
200
|
-
processedFeeOptions.find((option) => !option.notEnoughBalance) ??
|
|
201
|
-
processedFeeOptions[0] ??
|
|
202
|
-
null
|
|
231
|
+
// No user selection - auto-select based on preference or fallback
|
|
203
232
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
// Load preference from localStorage
|
|
234
|
+
let preferredType: "native" | "erc20" | null = null
|
|
235
|
+
try {
|
|
236
|
+
const stored = localStorage.getItem(FEE_OPTION_PREFERENCE_KEY)
|
|
237
|
+
if (stored === "native" || stored === "erc20") {
|
|
238
|
+
preferredType = stored
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Ignore localStorage errors
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try to select based on preference
|
|
245
|
+
if (preferredType) {
|
|
246
|
+
const isNative = preferredType === "native"
|
|
247
|
+
const preferredOption = processedFeeOptions.find(
|
|
248
|
+
(option) => isNativeToken(option.tokenAddress) === isNative,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if (preferredOption) {
|
|
252
|
+
setSelectedFeeOptionInternalRaw(isNative ? null : preferredOption)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Fallback: select first available option
|
|
258
|
+
if (processedFeeOptions.length > 0) {
|
|
259
|
+
const firstOption = processedFeeOptions[0]
|
|
260
|
+
if (firstOption) {
|
|
261
|
+
setSelectedFeeOptionInternalRaw(
|
|
262
|
+
isNativeToken(firstOption.tokenAddress) ? null : firstOption,
|
|
263
|
+
)
|
|
207
264
|
}
|
|
208
265
|
}
|
|
209
|
-
}, [processedFeeOptions, selectedFeeOption,
|
|
266
|
+
}, [processedFeeOptions, selectedFeeOption, hasUserSelected])
|
|
210
267
|
|
|
211
268
|
// Function to set raw fee options and process them
|
|
212
269
|
const setFeeOptions = useCallback(
|
|
@@ -221,9 +278,9 @@ export const SelectedFeeOptionProvider: React.FC<
|
|
|
221
278
|
setOriginTokenChainId(originChainId)
|
|
222
279
|
setAvailableTokens(tokens)
|
|
223
280
|
|
|
281
|
+
// Only clear selection if fee options are empty
|
|
224
282
|
if (normalized.length === 0) {
|
|
225
283
|
setSelectedFeeOptionInternalRaw(null)
|
|
226
|
-
setHasUserSelectedFeeOption(false)
|
|
227
284
|
}
|
|
228
285
|
},
|
|
229
286
|
[],
|