0xtrails 0.6.4 → 0.6.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 (32) hide show
  1. package/dist/{ccip-B4OF5VSU.js → ccip-CbJrlK-L.js} +1 -1
  2. package/dist/{index-Bja6TsJC.js → index-w7_dK4c5.js} +20068 -19732
  3. package/dist/index.js +2 -2
  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/sameChainSameToken.d.ts.map +1 -1
  8. package/dist/utils.d.ts +8 -0
  9. package/dist/utils.d.ts.map +1 -1
  10. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  11. package/dist/widget/components/FeeOption.d.ts.map +1 -1
  12. package/dist/widget/components/Fund.d.ts.map +1 -1
  13. package/dist/widget/components/Pay.d.ts.map +1 -1
  14. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  15. package/dist/widget/css/compiled.css +1 -1
  16. package/dist/widget/hooks/useSendForm.d.ts +0 -1
  17. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  18. package/dist/widget/index.js +1 -1
  19. package/package.json +2 -2
  20. package/src/prepareSend.ts +20 -5
  21. package/src/relaySdk.ts +2 -0
  22. package/src/tokens.ts +52 -4
  23. package/src/transactionIntent/deposits/gaslessDeposit.ts +2 -2
  24. package/src/transactionIntent/handlers/sameChainSameToken.ts +312 -1
  25. package/src/utils.ts +15 -0
  26. package/src/widget/compiled.css +1 -1
  27. package/src/widget/components/ClassicSwap.tsx +88 -49
  28. package/src/widget/components/FeeOption.tsx +3 -0
  29. package/src/widget/components/Fund.tsx +37 -38
  30. package/src/widget/components/Pay.tsx +45 -42
  31. package/src/widget/components/PoolDeposit.tsx +32 -29
  32. package/src/widget/hooks/useSendForm.ts +92 -39
@@ -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,
@@ -829,40 +828,43 @@ export const Fund: React.FC<FundProps> = ({
829
828
  </div>
830
829
 
831
830
  {/* Origin Token Balance and Percentage Buttons */}
832
- {originToken && balanceFormatted && (
833
- <div className="flex items-center space-x-2">
834
- <button
835
- type="button"
836
- className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none p-0"
837
- onClick={() => handlePercentageClick(100)}
838
- onKeyDown={(e) => {
839
- if (e.key === "Enter" || e.key === " ") {
840
- e.preventDefault()
841
- handlePercentageClick(100)
842
- }
843
- }}
844
- title="Click to use full balance"
845
- >
846
- Balance:{" "}
847
- {isBalanceVisible ? balanceFormatted || "0.00" : "••••••"}
848
- </button>
849
-
850
- {/* Percentage Buttons */}
851
- <PercentageMaxButtons
852
- userBalance={balanceFormatted}
853
- isNativeToken={originToken.contractAddress === zeroAddress}
854
- gasCostFormatted={prepareSendQuote?.gasCostFormatted}
855
- chainId={originToken.chainId}
856
- onAmountSelect={(amount) => {
857
- setTokenAmountForBackend(amount)
858
- setSendFormAmount(amount)
859
- setGlobalAmount(amount)
860
- setInputDisplayValue(amount)
861
- }}
862
- className="opacity-100"
863
- />
864
- </div>
865
- )}
831
+ {originToken &&
832
+ balanceFormatted &&
833
+ fundMethod !== "qr-code" &&
834
+ fundMethod !== "exchange" && (
835
+ <div className="flex items-center space-x-2">
836
+ <button
837
+ type="button"
838
+ className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none p-0"
839
+ onClick={() => handlePercentageClick(100)}
840
+ onKeyDown={(e) => {
841
+ if (e.key === "Enter" || e.key === " ") {
842
+ e.preventDefault()
843
+ handlePercentageClick(100)
844
+ }
845
+ }}
846
+ title="Click to use full balance"
847
+ >
848
+ Balance:{" "}
849
+ {isBalanceVisible ? balanceFormatted || "0.00" : "••••••"}
850
+ </button>
851
+
852
+ {/* Percentage Buttons */}
853
+ <PercentageMaxButtons
854
+ userBalance={balanceFormatted}
855
+ isNativeToken={originToken.contractAddress === zeroAddress}
856
+ gasCostFormatted={prepareSendQuote?.gasCostFormatted}
857
+ chainId={originToken.chainId}
858
+ onAmountSelect={(amount) => {
859
+ setTokenAmountForBackend(amount)
860
+ setSendFormAmount(amount)
861
+ setGlobalAmount(amount)
862
+ setInputDisplayValue(amount)
863
+ }}
864
+ className="opacity-100"
865
+ />
866
+ </div>
867
+ )}
866
868
  </div>
867
869
  </div>
868
870
 
@@ -1149,7 +1151,6 @@ export const Fund: React.FC<FundProps> = ({
1149
1151
  !destinationTokenAddress ||
1150
1152
  !isValidCustomToken ||
1151
1153
  prepareSendQuote?.noSufficientBalance ||
1152
- isSameTokenWithoutCustomCalldata ||
1153
1154
  !prepareSendQuote
1154
1155
  }
1155
1156
  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 +1160,6 @@ export const Fund: React.FC<FundProps> = ({
1159
1160
  <Loader2 className="w-5 h-5 animate-spin mr-2" />
1160
1161
  <span>{buttonText}</span>
1161
1162
  </div>
1162
- ) : isSameTokenWithoutCustomCalldata ? (
1163
- "Select Different Tokens"
1164
1163
  ) : prepareSendQuote?.noSufficientBalance ? (
1165
1164
  "Insufficient Balance"
1166
1165
  ) : !selectedRecipient ? (
@@ -736,24 +736,15 @@ export const Pay: React.FC<PayProps> = ({
736
736
  </div>
737
737
 
738
738
  {/* Origin Token Balance and Percentage Buttons */}
739
- {originToken && originTokenBalance?.balanceFormatted && (
740
- <div className="flex items-center space-x-2">
741
- <button
742
- type="button"
743
- className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none p-0"
744
- onClick={() => {
745
- if (originTokenBalance.balanceFormatted) {
746
- const balance = parseFloat(
747
- originTokenBalance.balanceFormatted,
748
- )
749
- if (!Number.isNaN(balance)) {
750
- setTokenAmountForBackend(balance.toFixed(6))
751
- }
752
- }
753
- }}
754
- onKeyDown={(e) => {
755
- if (e.key === "Enter" || e.key === " ") {
756
- e.preventDefault()
739
+ {originToken &&
740
+ originTokenBalance?.balanceFormatted &&
741
+ fundMethod !== "qr-code" &&
742
+ fundMethod !== "exchange" && (
743
+ <div className="flex items-center space-x-2">
744
+ <button
745
+ type="button"
746
+ className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none p-0"
747
+ onClick={() => {
757
748
  if (originTokenBalance.balanceFormatted) {
758
749
  const balance = parseFloat(
759
750
  originTokenBalance.balanceFormatted,
@@ -762,31 +753,43 @@ export const Pay: React.FC<PayProps> = ({
762
753
  setTokenAmountForBackend(balance.toFixed(6))
763
754
  }
764
755
  }
765
- }
766
- }}
767
- title="Click to use full balance"
768
- >
769
- Balance: {originTokenBalance.balanceFormatted}
770
- </button>
771
-
772
- {/* Percentage Buttons - Only show if toAmount is not set */}
773
- {!toAmount && (
774
- <PercentageMaxButtons
775
- userBalance={originTokenBalance.balanceFormatted}
776
- isNativeToken={
777
- originToken.contractAddress === zeroAddress ||
778
- originToken.contractAddress === undefined
779
- }
780
- gasCostFormatted={prepareSendQuote?.gasCostFormatted}
781
- chainId={originToken.chainId}
782
- onAmountSelect={(amount) => {
783
- setTokenAmountForBackend(amount)
784
756
  }}
785
- className="opacity-100"
786
- />
787
- )}
788
- </div>
789
- )}
757
+ onKeyDown={(e) => {
758
+ if (e.key === "Enter" || e.key === " ") {
759
+ e.preventDefault()
760
+ if (originTokenBalance.balanceFormatted) {
761
+ const balance = parseFloat(
762
+ originTokenBalance.balanceFormatted,
763
+ )
764
+ if (!Number.isNaN(balance)) {
765
+ setTokenAmountForBackend(balance.toFixed(6))
766
+ }
767
+ }
768
+ }
769
+ }}
770
+ title="Click to use full balance"
771
+ >
772
+ Balance: {originTokenBalance.balanceFormatted}
773
+ </button>
774
+
775
+ {/* Percentage Buttons - Only show if toAmount is not set */}
776
+ {!toAmount && (
777
+ <PercentageMaxButtons
778
+ userBalance={originTokenBalance.balanceFormatted}
779
+ isNativeToken={
780
+ originToken.contractAddress === zeroAddress ||
781
+ originToken.contractAddress === undefined
782
+ }
783
+ gasCostFormatted={prepareSendQuote?.gasCostFormatted}
784
+ chainId={originToken.chainId}
785
+ onAmountSelect={(amount) => {
786
+ setTokenAmountForBackend(amount)
787
+ }}
788
+ className="opacity-100"
789
+ />
790
+ )}
791
+ </div>
792
+ )}
790
793
  </div>
791
794
  </div>
792
795
  </div>
@@ -396,35 +396,38 @@ export const PoolDeposit: React.FC<PoolDepositProps> = ({
396
396
  )}
397
397
 
398
398
  {/* Origin Token Balance and Percentage Buttons */}
399
- {originToken && balanceFormatted && (
400
- <div className="flex items-center space-x-2">
401
- <button
402
- type="button"
403
- className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none p-0"
404
- onClick={() => handleAmountSelect(balanceFormatted || "0")}
405
- onKeyDown={(e) => {
406
- if (e.key === "Enter" || e.key === " ") {
407
- e.preventDefault()
408
- handleAmountSelect(balanceFormatted || "0")
409
- }
410
- }}
411
- title="Click to use full balance"
412
- >
413
- Balance:{" "}
414
- {isBalanceVisible ? balanceFormatted || "0.00" : "••••••"}
415
- </button>
416
-
417
- {/* Percentage Buttons */}
418
- <PercentageMaxButtons
419
- userBalance={balanceFormatted}
420
- isNativeToken={originToken.contractAddress === zeroAddress}
421
- gasCostFormatted={prepareSendQuote?.gasCostFormatted}
422
- chainId={originToken.chainId}
423
- onAmountSelect={handleAmountSelect}
424
- className="opacity-100"
425
- />
426
- </div>
427
- )}
399
+ {originToken &&
400
+ balanceFormatted &&
401
+ fundMethod !== "qr-code" &&
402
+ fundMethod !== "exchange" && (
403
+ <div className="flex items-center space-x-2">
404
+ <button
405
+ type="button"
406
+ className="text-xs text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200 transition-colors bg-transparent border-none p-0"
407
+ onClick={() => handleAmountSelect(balanceFormatted || "0")}
408
+ onKeyDown={(e) => {
409
+ if (e.key === "Enter" || e.key === " ") {
410
+ e.preventDefault()
411
+ handleAmountSelect(balanceFormatted || "0")
412
+ }
413
+ }}
414
+ title="Click to use full balance"
415
+ >
416
+ Balance:{" "}
417
+ {isBalanceVisible ? balanceFormatted || "0.00" : "••••••"}
418
+ </button>
419
+
420
+ {/* Percentage Buttons */}
421
+ <PercentageMaxButtons
422
+ userBalance={balanceFormatted}
423
+ isNativeToken={originToken.contractAddress === zeroAddress}
424
+ gasCostFormatted={prepareSendQuote?.gasCostFormatted}
425
+ chainId={originToken.chainId}
426
+ onAmountSelect={handleAmountSelect}
427
+ className="opacity-100"
428
+ />
429
+ </div>
430
+ )}
428
431
  </div>
429
432
  </div>
430
433
 
@@ -171,7 +171,6 @@ export type UseSendReturn = {
171
171
  toAmountDisplay: string
172
172
  quoteError: string | null
173
173
  quoteErrorPrettified: string | null
174
- isSameTokenWithoutCustomCalldata: boolean
175
174
  isRecipientContract: boolean
176
175
  isSenderContractOnOrigin: boolean
177
176
  isSenderContractOnDestination: boolean
@@ -746,7 +745,76 @@ export function useSendForm({
746
745
 
747
746
  // Get quote automatically when inputs change
748
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
+
749
809
  // Only get quote if all required inputs are present
810
+ // Skip quote if origin and destination tokens are the same
811
+ const isSameToken =
812
+ selectedToken?.chainId === selectedDestinationChain?.id &&
813
+ selectedToken?.contractAddress?.toLowerCase() ===
814
+ destinationTokenAddress?.toLowerCase()
815
+ const isSenderSameAsRecipient =
816
+ account?.address?.toLowerCase() === recipient?.toLowerCase()
817
+
750
818
  if (
751
819
  !account ||
752
820
  !amount ||
@@ -757,10 +825,13 @@ export function useSendForm({
757
825
  amount === "0" ||
758
826
  !amountRaw ||
759
827
  amountRaw === "0" ||
760
- !selectedToken
828
+ !selectedToken ||
829
+ (isSameToken &&
830
+ isSenderSameAsRecipient &&
831
+ (mode === "swap" || mode === "fund"))
761
832
  ) {
762
833
  logger.console.log(
763
- "[trails-sdk] Skipping quote because of missing inputs",
834
+ "[trails-sdk] Skipping quote because of missing inputs or same token",
764
835
  {
765
836
  amount,
766
837
  destinationTokenAddress,
@@ -769,6 +840,8 @@ export function useSendForm({
769
840
  selectedDestinationChain,
770
841
  amountRaw,
771
842
  selectedToken,
843
+ isSameToken,
844
+ debugState,
772
845
  },
773
846
  )
774
847
  setQuoteError(null)
@@ -987,6 +1060,9 @@ export function useSendForm({
987
1060
  selectedDestToken?.decimals,
988
1061
  recipient,
989
1062
  destinationTokenAddress,
1063
+ destinationTokenAddressFromTokenSymbol,
1064
+ isCustomToken,
1065
+ toToken,
990
1066
  sequenceIndexerUrl,
991
1067
  selectedDestToken?.symbol,
992
1068
  selectedDestinationChain?.id,
@@ -1022,6 +1098,15 @@ export function useSendForm({
1022
1098
  useEffect(() => {
1023
1099
  // Only trigger if we have the essential inputs
1024
1100
  // Block empty string or exactly "0"
1101
+ // Skip quote if origin and destination tokens are the same
1102
+ const isSameToken =
1103
+ selectedToken?.chainId === selectedDestinationChain?.id &&
1104
+ selectedToken?.contractAddress?.toLowerCase() ===
1105
+ destinationTokenAddress?.toLowerCase()
1106
+
1107
+ const isSenderSameAsRecipient =
1108
+ account?.address?.toLowerCase() === recipient?.toLowerCase()
1109
+
1025
1110
  if (
1026
1111
  !amount ||
1027
1112
  amount === "0" ||
@@ -1029,7 +1114,10 @@ export function useSendForm({
1029
1114
  !isValidRecipient ||
1030
1115
  !selectedDestToken?.symbol ||
1031
1116
  !selectedDestinationChain?.id ||
1032
- !selectedToken
1117
+ !selectedToken ||
1118
+ (isSameToken &&
1119
+ isSenderSameAsRecipient &&
1120
+ (mode === "swap" || mode === "fund"))
1033
1121
  ) {
1034
1122
  setPrepareSendResult(null)
1035
1123
  latestResolvedIntentIdRef.current = null
@@ -1438,40 +1526,6 @@ export function useSendForm({
1438
1526
  }
1439
1527
  }, [quoteError])
1440
1528
 
1441
- // Check if origin and destination tokens are the same (same contract address on same chain)
1442
- // Only block same-token transactions when there's no custom calldata AND recipient is the same as sender
1443
- const isSameTokenWithoutCustomCalldata = useMemo(() => {
1444
- if (
1445
- !selectedToken ||
1446
- !selectedToken.contractAddress ||
1447
- !destinationTokenAddress ||
1448
- !selectedToken?.chainId ||
1449
- !selectedDestinationChain?.id
1450
- ) {
1451
- return false
1452
- }
1453
- const isSameChainAndToken =
1454
- selectedToken.contractAddress.toLowerCase() ===
1455
- destinationTokenAddress.toLowerCase() &&
1456
- selectedToken.chainId === selectedDestinationChain?.id
1457
-
1458
- // Allow same-token transactions if:
1459
- // 1. There's custom calldata (e.g., NFT minting)
1460
- // 2. Recipient is different from sender (simple transfer to another address)
1461
- const recipientIsSameAsSender =
1462
- recipient?.toLowerCase() === account?.address?.toLowerCase()
1463
-
1464
- return isSameChainAndToken && !toCalldata && recipientIsSameAsSender
1465
- }, [
1466
- selectedToken,
1467
- destinationTokenAddress,
1468
- selectedDestinationChain,
1469
- toCalldata,
1470
- selectedToken?.chainId,
1471
- recipient,
1472
- account?.address,
1473
- ])
1474
-
1475
1529
  return {
1476
1530
  amount,
1477
1531
  amountRaw,
@@ -1517,7 +1571,6 @@ export function useSendForm({
1517
1571
  prepareSendQuote: prepareSendResult?.quote ?? null,
1518
1572
  quoteError,
1519
1573
  quoteErrorPrettified,
1520
- isSameTokenWithoutCustomCalldata,
1521
1574
  isRecipientContract,
1522
1575
  isSenderContractOnOrigin,
1523
1576
  isSenderContractOnDestination,