@0xsquid/deposit-widget 0.0.2-beta.0 → 0.1.0

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 (117) hide show
  1. package/dist/types/DepositWidget.d.ts +0 -1
  2. package/dist/types/components/ViewTransition.d.ts +0 -1
  3. package/dist/types/components/shared/buttons/button.d.ts +0 -1
  4. package/dist/types/components/shared/icons/types.d.ts +0 -1
  5. package/dist/types/components/shared/icons/user-round.d.ts +0 -1
  6. package/dist/types/components/shared/navigation/base-navbar.d.ts +0 -1
  7. package/dist/types/components/shared/navigation/sub-navbar.d.ts +0 -1
  8. package/dist/types/components/token-badge-icon.d.ts +0 -1
  9. package/dist/types/components/token-list-item.d.ts +0 -1
  10. package/dist/types/components/view-container.d.ts +0 -1
  11. package/dist/types/constants.d.ts +0 -1
  12. package/dist/types/hooks/ui/useMainCTAButtonState.d.ts +0 -1
  13. package/dist/types/hooks/use-auto-select-token.d.ts +0 -1
  14. package/dist/types/hooks/use-deposit-route.d.ts +0 -1
  15. package/dist/types/hooks/use-token-selection.d.ts +0 -1
  16. package/dist/types/hooks/use-transaction-history.d.ts +0 -1
  17. package/dist/types/index.d.ts +0 -1
  18. package/dist/types/services/assets-service.d.ts +0 -1
  19. package/dist/types/services/wallet-history/format.d.ts +0 -1
  20. package/dist/types/services/wallet-history/format.test.d.ts +0 -1
  21. package/dist/types/services/wallet-history/get-main-explorer-url.d.ts +0 -1
  22. package/dist/types/services/wallet-history/get-wallet-history.d.ts +0 -1
  23. package/dist/types/services/wallet-history/types.d.ts +0 -1
  24. package/dist/types/services/wallet-history/validation.d.ts +0 -1
  25. package/dist/types/store/use-deposit-store.d.ts +0 -1
  26. package/dist/types/store/use-input-mode.d.ts +0 -1
  27. package/dist/types/store/useRouter.d.ts +0 -1
  28. package/dist/types/types.d.ts +0 -1
  29. package/dist/types/utils/format-date.d.ts +0 -1
  30. package/dist/types/utils/format-date.test.d.ts +0 -1
  31. package/dist/types/utils/transaction.d.ts +0 -1
  32. package/dist/types/views/connect-wallet/connect-wallet-view.d.ts +0 -1
  33. package/dist/types/views/connect-wallet/wallet-list-item.d.ts +0 -1
  34. package/dist/types/views/main/amount-input.d.ts +0 -1
  35. package/dist/types/views/main/connect-prompt.d.ts +0 -1
  36. package/dist/types/views/main/deposit-amount-input.d.ts +0 -1
  37. package/dist/types/views/main/deposit-form.d.ts +0 -1
  38. package/dist/types/views/main/main-cta-button.d.ts +0 -1
  39. package/dist/types/views/main/main-view.d.ts +0 -1
  40. package/dist/types/views/main/navbar/actions.d.ts +0 -1
  41. package/dist/types/views/main/navbar/icon.d.ts +0 -1
  42. package/dist/types/views/main/navbar/navbar.d.ts +0 -1
  43. package/dist/types/views/main/navbar/title.d.ts +0 -1
  44. package/dist/types/views/main/recipient/account.d.ts +0 -1
  45. package/dist/types/views/main/recipient/recipient.d.ts +0 -1
  46. package/dist/types/views/main/token-selector.d.ts +0 -1
  47. package/dist/types/views/qr-code.d.ts +0 -1
  48. package/dist/types/views/render-view.d.ts +0 -1
  49. package/dist/types/views/select-chain/chain-type-meta.d.ts +0 -1
  50. package/dist/types/views/select-chain/select-chain-view.d.ts +0 -1
  51. package/dist/types/views/select-token.d.ts +0 -1
  52. package/dist/types/views/transaction-history/activity-list-item.d.ts +0 -1
  53. package/dist/types/views/transaction-history/transaction-history-view.d.ts +0 -1
  54. package/dist/types/views/transaction-progress/helpers.d.ts +0 -1
  55. package/dist/types/views/transaction-progress/transaction-progress-view.d.ts +0 -1
  56. package/dist/types/views/transaction-progress/use-transaction-progress.d.ts +0 -1
  57. package/package.json +7 -7
  58. package/src/DepositWidget.tsx +158 -0
  59. package/src/compiled-tailwind.css +6100 -0
  60. package/src/components/ViewTransition.tsx +81 -0
  61. package/src/components/shared/buttons/button.tsx +17 -0
  62. package/src/components/shared/icons/types.ts +3 -0
  63. package/src/components/shared/icons/user-round.tsx +21 -0
  64. package/src/components/shared/navigation/base-navbar.tsx +15 -0
  65. package/src/components/shared/navigation/sub-navbar.tsx +46 -0
  66. package/src/components/token-badge-icon.tsx +31 -0
  67. package/src/components/token-list-item.tsx +84 -0
  68. package/src/components/view-container.tsx +16 -0
  69. package/src/constants.ts +1 -0
  70. package/src/css.d.ts +4 -0
  71. package/src/fonts/DMSans-Variable.woff2 +0 -0
  72. package/src/hooks/ui/useMainCTAButtonState.ts +143 -0
  73. package/src/hooks/use-auto-select-token.ts +65 -0
  74. package/src/hooks/use-deposit-route.ts +58 -0
  75. package/src/hooks/use-token-selection.ts +17 -0
  76. package/src/hooks/use-transaction-history.ts +198 -0
  77. package/src/index.ts +3 -0
  78. package/src/services/assets-service.ts +21 -0
  79. package/src/services/wallet-history/format.test.ts +63 -0
  80. package/src/services/wallet-history/format.ts +128 -0
  81. package/src/services/wallet-history/get-main-explorer-url.ts +74 -0
  82. package/src/services/wallet-history/get-wallet-history.ts +24 -0
  83. package/src/services/wallet-history/types.ts +66 -0
  84. package/src/services/wallet-history/validation.ts +60 -0
  85. package/src/store/use-deposit-store.ts +20 -0
  86. package/src/store/use-input-mode.ts +10 -0
  87. package/src/store/useRouter.ts +49 -0
  88. package/src/tailwind.css +16 -0
  89. package/src/types.ts +39 -0
  90. package/src/utils/format-date.test.ts +32 -0
  91. package/src/utils/format-date.ts +25 -0
  92. package/src/utils/transaction.ts +39 -0
  93. package/src/views/connect-wallet/connect-wallet-view.tsx +147 -0
  94. package/src/views/connect-wallet/wallet-list-item.tsx +69 -0
  95. package/src/views/main/amount-input.tsx +272 -0
  96. package/src/views/main/connect-prompt.tsx +47 -0
  97. package/src/views/main/deposit-amount-input.tsx +42 -0
  98. package/src/views/main/deposit-form.tsx +13 -0
  99. package/src/views/main/main-cta-button.tsx +14 -0
  100. package/src/views/main/main-view.tsx +24 -0
  101. package/src/views/main/navbar/actions.tsx +25 -0
  102. package/src/views/main/navbar/icon.tsx +11 -0
  103. package/src/views/main/navbar/navbar.tsx +16 -0
  104. package/src/views/main/navbar/title.tsx +64 -0
  105. package/src/views/main/recipient/account.tsx +81 -0
  106. package/src/views/main/recipient/recipient.tsx +64 -0
  107. package/src/views/main/token-selector.tsx +77 -0
  108. package/src/views/qr-code.tsx +14 -0
  109. package/src/views/render-view.tsx +28 -0
  110. package/src/views/select-chain/chain-type-meta.ts +37 -0
  111. package/src/views/select-chain/select-chain-view.tsx +97 -0
  112. package/src/views/select-token.tsx +227 -0
  113. package/src/views/transaction-history/activity-list-item.tsx +87 -0
  114. package/src/views/transaction-history/transaction-history-view.tsx +58 -0
  115. package/src/views/transaction-progress/helpers.tsx +93 -0
  116. package/src/views/transaction-progress/transaction-progress-view.tsx +217 -0
  117. package/src/views/transaction-progress/use-transaction-progress.ts +112 -0
@@ -0,0 +1,198 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { useQueries } from "@tanstack/react-query";
3
+ import {
4
+ useWallet,
5
+ useHistoryStore,
6
+ useSquidTokens,
7
+ useSquidChains,
8
+ useConfigStore,
9
+ HistoryTxType,
10
+ parseToBigInt,
11
+ } from "@0xsquid/react-hooks";
12
+ import { getWalletHistory } from "../services/wallet-history/get-wallet-history";
13
+ import {
14
+ groupAndSortByDay,
15
+ pendingSwapToFormattedTx,
16
+ toFormattedTx,
17
+ type DayGroup,
18
+ } from "../services/wallet-history/format";
19
+ import type { SquidTransaction } from "../services/wallet-history/types";
20
+ import {
21
+ useDepositStore,
22
+ useIsPaymentMode,
23
+ usePaymentAmount,
24
+ } from "../store/use-deposit-store";
25
+ import {
26
+ isPaymentTx,
27
+ isSameAddress,
28
+ isSameTx,
29
+ matchesDestinationToken,
30
+ } from "../utils/transaction";
31
+
32
+ export function useTransactionHistory(): {
33
+ isLoading: boolean;
34
+ isError: boolean;
35
+ groups: DayGroup[];
36
+ } {
37
+ const { connectedAddresses } = useWallet();
38
+ const config = useDepositStore((s) => s.config);
39
+ const integratorId = config?.integrator.id;
40
+ const apiUrl = useConfigStore((s) => s.config.apiUrl);
41
+ const pendingFromStore = useHistoryStore((s) => s.transactions);
42
+ const { findToken, tokens } = useSquidTokens();
43
+ const { findChain } = useSquidChains();
44
+
45
+ const destinationToken = config?.destinationToken;
46
+ const isPaymentMode = useIsPaymentMode();
47
+ const paymentAmount = usePaymentAmount();
48
+
49
+ const destinationTokenDecimals = destinationToken
50
+ ? tokens.find(
51
+ (t) =>
52
+ t.chainId === destinationToken.chainId &&
53
+ isSameAddress(t.address, destinationToken.address),
54
+ )?.decimals
55
+ : null;
56
+
57
+ const addresses = useMemo(
58
+ () =>
59
+ Array.from(
60
+ new Set(
61
+ Object.values(connectedAddresses).filter(
62
+ (a): a is string => a != null && a.length > 0,
63
+ ),
64
+ ),
65
+ ),
66
+ [connectedAddresses],
67
+ );
68
+
69
+ // Memoized so TanStack Query doesn't re-run the filter on every render.
70
+ const select = useCallback(
71
+ (data: SquidTransaction[]) => {
72
+ if (!destinationToken) return [];
73
+
74
+ if (
75
+ isPaymentMode &&
76
+ (!paymentAmount || destinationTokenDecimals == null)
77
+ ) {
78
+ return [];
79
+ }
80
+
81
+ return data.filter((tx) => {
82
+ // validate destination token
83
+ if (
84
+ !matchesDestinationToken(
85
+ tx.quote.route.estimate.toToken,
86
+ destinationToken,
87
+ )
88
+ ) {
89
+ return false;
90
+ }
91
+
92
+ if (isPaymentMode) {
93
+ return isPaymentTx(tx, paymentAmount!, destinationTokenDecimals!);
94
+ }
95
+
96
+ return true;
97
+ });
98
+ },
99
+ [destinationToken, isPaymentMode, paymentAmount, destinationTokenDecimals],
100
+ );
101
+
102
+ const queries = useQueries({
103
+ queries: addresses.map((address) => ({
104
+ queryKey: ["deposit-widget", "tx-history", address, integratorId, apiUrl],
105
+ queryFn: () => {
106
+ if (!integratorId || !apiUrl) {
107
+ throw new Error(
108
+ "integratorId and apiUrl are required to fetch wallet history",
109
+ );
110
+ }
111
+ return getWalletHistory(address, integratorId, apiUrl);
112
+ },
113
+ select,
114
+ enabled: !!address && !!integratorId && !!apiUrl,
115
+ staleTime: 30_000,
116
+ })),
117
+ });
118
+
119
+ const isLoading = queries.some((q) => q.isLoading);
120
+ const isError = queries.length > 0 && queries.every((q) => q.isError);
121
+
122
+ return useMemo(() => {
123
+ if (!destinationToken) {
124
+ return { isLoading, isError, groups: [] };
125
+ }
126
+
127
+ const backendTxs: SquidTransaction[] = queries
128
+ .map((q) => q.data)
129
+ .flatMap((data) => data ?? []);
130
+
131
+ // Overlay local pendings for the brief window where a tx resolved locally
132
+ // but Squid's backend hasn't indexed it yet.
133
+ const formatted = backendTxs.map((tx) =>
134
+ toFormattedTx(tx, findChain(tx.fromChainId)),
135
+ );
136
+
137
+ const connectedAddressSet = new Set(addresses.map((a) => a.toLowerCase()));
138
+ const expectedPayment =
139
+ paymentAmount && destinationTokenDecimals != null
140
+ ? parseToBigInt(paymentAmount, destinationTokenDecimals).toString()
141
+ : undefined;
142
+
143
+ for (const { data: pendingTx, txType } of pendingFromStore) {
144
+ if (txType !== HistoryTxType.SWAP) continue;
145
+
146
+ // validate source address
147
+ if (!connectedAddressSet.has(pendingTx.fromAddress.toLowerCase())) {
148
+ continue;
149
+ }
150
+
151
+ // validate destination token
152
+ if (
153
+ !matchesDestinationToken(
154
+ { address: pendingTx.toToken, chainId: pendingTx.toChain },
155
+ destinationToken,
156
+ )
157
+ ) {
158
+ continue;
159
+ }
160
+
161
+ // skip if already indexed
162
+ if (backendTxs.some((tx) => isSameTx(tx, pendingTx))) continue;
163
+
164
+ const isPayment =
165
+ expectedPayment != null && pendingTx.toAmount === expectedPayment;
166
+
167
+ if (isPaymentMode !== isPayment) continue;
168
+
169
+ formatted.unshift(
170
+ pendingSwapToFormattedTx(pendingTx, findToken, findChain),
171
+ );
172
+ }
173
+
174
+ return {
175
+ isLoading,
176
+ isError,
177
+ groups: groupAndSortByDay(formatted),
178
+ };
179
+ // `useQueries` returns a fresh outer array every render, so depending on
180
+ // `queries` directly would bust this memo on every render. Spreading the
181
+ // individual `q.data` refs sidesteps that — they're kept stable by
182
+ // React Query's structural sharing across renders when the data is equal.
183
+ // eslint-disable-next-line react-hooks/exhaustive-deps
184
+ }, [
185
+ destinationToken,
186
+ addresses,
187
+ isPaymentMode,
188
+ paymentAmount,
189
+ destinationTokenDecimals,
190
+ findChain,
191
+ findToken,
192
+ pendingFromStore,
193
+ isLoading,
194
+ isError,
195
+ // eslint-disable-next-line react-hooks/exhaustive-deps
196
+ ...queries.map((q) => q.data),
197
+ ]);
198
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { DepositWidget } from "./DepositWidget";
2
+ export type { DepositConfig, WidgetView, WidgetViewId } from "./types";
3
+ export type { Theme, ThemeType } from "@0xsquid/ui";
@@ -0,0 +1,21 @@
1
+ import type { Token } from "@0xsquid/squid-types";
2
+
3
+ export type TokenWithBalance = Token & { balance: string };
4
+
5
+ export function sortTokensByBalance(
6
+ tokens: TokenWithBalance[],
7
+ ): TokenWithBalance[] {
8
+ return tokens.toSorted((a, b) => {
9
+ const aUsd = +a.balance * +(a.usdPrice ?? 0);
10
+ const bUsd = +b.balance * +(b.usdPrice ?? 0);
11
+ if (aUsd !== bUsd) return bUsd - aUsd;
12
+ return +(b.balance ?? 0) - +(a.balance ?? 0);
13
+ });
14
+ }
15
+
16
+ export function filterTokensByChain(
17
+ tokens: TokenWithBalance[],
18
+ chainId: string | null,
19
+ ): TokenWithBalance[] {
20
+ return chainId ? tokens.filter((token) => token.chainId === chainId) : tokens;
21
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { groupAndSortByDay } from "./format";
3
+ import type { FormattedTx } from "./types";
4
+
5
+ function makeTx(id: string, timestamp: string): FormattedTx {
6
+ return {
7
+ id,
8
+ timestamp,
9
+ status: "success",
10
+ fromAmount: "1",
11
+ toAmount: "1",
12
+ fromToken: { symbol: "USDC", logo: "" },
13
+ fromChain: { logo: "" },
14
+ toToken: { symbol: "USDC" },
15
+ };
16
+ }
17
+
18
+ describe("groupAndSortByDay", () => {
19
+ it("returns an empty array when given no transactions", () => {
20
+ expect(groupAndSortByDay([])).toEqual([]);
21
+ });
22
+
23
+ it("groups transactions into buckets keyed by UTC YYYY-MM-DD", () => {
24
+ const groups = groupAndSortByDay([
25
+ makeTx("a", "2026-04-14T10:00:00.000Z"),
26
+ makeTx("b", "2026-04-14T23:59:00.000Z"),
27
+ makeTx("c", "2026-04-13T12:00:00.000Z"),
28
+ ]);
29
+
30
+ expect(groups.map((g) => g.day)).toEqual(["2026-04-14", "2026-04-13"]);
31
+ expect(groups[0]!.transactions.map((t) => t.id)).toEqual(["b", "a"]);
32
+ expect(groups[1]!.transactions.map((t) => t.id)).toEqual(["c"]);
33
+ });
34
+
35
+ it("sorts day groups newest-first", () => {
36
+ const groups = groupAndSortByDay([
37
+ makeTx("old", "2026-01-01T00:00:00.000Z"),
38
+ makeTx("new", "2026-04-14T00:00:00.000Z"),
39
+ makeTx("mid", "2026-03-10T00:00:00.000Z"),
40
+ ]);
41
+
42
+ expect(groups.map((g) => g.day)).toEqual([
43
+ "2026-04-14",
44
+ "2026-03-10",
45
+ "2026-01-01",
46
+ ]);
47
+ });
48
+
49
+ it("sorts transactions within a day newest-first", () => {
50
+ const groups = groupAndSortByDay([
51
+ makeTx("morning", "2026-04-14T08:00:00.000Z"),
52
+ makeTx("evening", "2026-04-14T20:00:00.000Z"),
53
+ makeTx("noon", "2026-04-14T12:00:00.000Z"),
54
+ ]);
55
+
56
+ expect(groups).toHaveLength(1);
57
+ expect(groups[0]!.transactions.map((t) => t.id)).toEqual([
58
+ "evening",
59
+ "noon",
60
+ "morning",
61
+ ]);
62
+ });
63
+ });
@@ -0,0 +1,128 @@
1
+ import {
2
+ formatTokenAmount,
3
+ formatBNToReadable,
4
+ getTokenImage,
5
+ type SwapTransactionHistory,
6
+ } from "@0xsquid/react-hooks";
7
+ import type { ChainData, Token } from "@0xsquid/squid-types";
8
+ import { getMainExplorerUrl } from "./get-main-explorer-url";
9
+ import type { FormattedTx, SquidTransaction, TransactionStatus } from "./types";
10
+
11
+ export interface DayGroup {
12
+ day: string; // YYYY-MM-DD
13
+ transactions: FormattedTx[];
14
+ }
15
+
16
+ function formatAmount(raw: string, decimals: number): string {
17
+ const readable = formatBNToReadable(raw, decimals);
18
+ return formatTokenAmount(readable, { compact: true });
19
+ }
20
+
21
+ // Maps react-hooks' TransactionStatus (+ error variants) to our 3-state display.
22
+ function mapLocalStatusToDisplayStatus(status: string): TransactionStatus {
23
+ if (status === "success" || status === "partial_success") return "success";
24
+ if (status === "refunded" || status === "error") return "refunded";
25
+ return "ongoing";
26
+ }
27
+
28
+ function fallbackId(tx: SquidTransaction): string {
29
+ const { estimate } = tx.quote.route;
30
+ return `${tx.createdAt}-${estimate.fromToken.address}-${estimate.toToken.address}-${estimate.fromAmount}`;
31
+ }
32
+
33
+ export function toFormattedTx(
34
+ tx: SquidTransaction,
35
+ sourceChain: ChainData | undefined,
36
+ ): FormattedTx {
37
+ const { estimate } = tx.quote.route;
38
+ const { fromToken, toToken } = estimate;
39
+
40
+ return {
41
+ id: tx.transactionId ?? fallbackId(tx),
42
+ timestamp: tx.createdAt,
43
+ status: tx.statusResponse.squidTransactionStatus,
44
+ fromAmount: formatAmount(estimate.fromAmount, fromToken.decimals),
45
+ toAmount: formatAmount(estimate.toAmount, toToken.decimals),
46
+ fromToken: {
47
+ symbol: fromToken.symbol,
48
+ logo: getTokenImage({
49
+ address: fromToken.address,
50
+ chainId: tx.fromChainId,
51
+ }),
52
+ },
53
+ fromChain: { logo: sourceChain?.chainIconURI ?? "" },
54
+ toToken: { symbol: toToken.symbol },
55
+ explorer: getMainExplorerUrl({
56
+ transactionId: tx.transactionId,
57
+ routeType: tx.routeType,
58
+ sourceTxExplorerUrl: tx.sourceTxExplorerUrl,
59
+ statusResponse: tx.statusResponse,
60
+ actions: tx.actions,
61
+ sourceChain,
62
+ }),
63
+ };
64
+ }
65
+
66
+ export function pendingSwapToFormattedTx(
67
+ pending: SwapTransactionHistory,
68
+ findToken: (address?: string, chainId?: string) => Token | undefined,
69
+ findChain: (chainId: string | undefined) => ChainData | undefined,
70
+ ): FormattedTx {
71
+ const fromTokenMeta = findToken(pending.fromToken, pending.fromChain);
72
+ const toTokenMeta = findToken(pending.toToken, pending.toChain);
73
+
74
+ const fromReadable = fromTokenMeta
75
+ ? formatBNToReadable(pending.fromAmount, fromTokenMeta.decimals)
76
+ : pending.fromAmount;
77
+ const toReadable = toTokenMeta
78
+ ? formatBNToReadable(pending.toAmount, toTokenMeta.decimals)
79
+ : pending.toAmount;
80
+
81
+ return {
82
+ id: pending.transactionId,
83
+ timestamp: new Date(pending.timestamp).toISOString(),
84
+ status: mapLocalStatusToDisplayStatus(pending.status),
85
+ fromAmount: formatTokenAmount(fromReadable, { compact: true }),
86
+ toAmount: formatTokenAmount(toReadable, { compact: true }),
87
+ fromToken: {
88
+ symbol: fromTokenMeta?.symbol ?? "",
89
+ logo: getTokenImage({
90
+ address: pending.fromToken,
91
+ chainId: pending.fromChain,
92
+ }),
93
+ },
94
+ fromChain: { logo: findChain(pending.fromChain)?.chainIconURI ?? "" },
95
+ toToken: { symbol: toTokenMeta?.symbol ?? "" },
96
+ explorer: getMainExplorerUrl({
97
+ transactionId: pending.transactionId,
98
+ routeType: pending.routeType,
99
+ sourceTxExplorerUrl: pending.sourceTxExplorerUrl,
100
+ statusResponse: pending.statusResponse,
101
+ actions: pending.actions,
102
+ sourceChain: findChain(pending.fromChain),
103
+ }),
104
+ };
105
+ }
106
+
107
+ export function groupAndSortByDay(txs: FormattedTx[]): DayGroup[] {
108
+ const byDay = new Map<string, FormattedTx[]>();
109
+
110
+ for (const tx of txs) {
111
+ const dayKey = new Date(tx.timestamp).toISOString().slice(0, 10);
112
+ const bucket = byDay.get(dayKey) ?? [];
113
+ bucket.push(tx);
114
+ byDay.set(dayKey, bucket);
115
+ }
116
+
117
+ const groups: DayGroup[] = [];
118
+ for (const [day, transactions] of byDay.entries()) {
119
+ transactions.sort(
120
+ (a, b) =>
121
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
122
+ );
123
+ groups.push({ day, transactions });
124
+ }
125
+
126
+ groups.sort((a, b) => new Date(b.day).getTime() - new Date(a.day).getTime());
127
+ return groups;
128
+ }
@@ -0,0 +1,74 @@
1
+ import { isChainflipBridgeTransaction } from "@0xsquid/react-hooks";
2
+ import type { ChainData } from "@0xsquid/squid-types";
3
+ import type { RouteAction } from "./types";
4
+
5
+ // Ported from squid-widget's getMainExplorerUrl (richer than the one in @0xsquid/react-hooks).
6
+ // Source: squid-widget repo — packages/widget/src/widget/services/internal/transactionService.ts
7
+ // TODO: consolidate into @0xsquid/react-hooks when that version picks up coral + chainflip support.
8
+
9
+ export interface ExplorerStatusResponse {
10
+ coralTransactionUrl?: string;
11
+ axelarTransactionUrl?: string;
12
+ transactionUrl?: string;
13
+ toChain?: { transactionUrl?: string };
14
+ }
15
+
16
+ export interface ExplorerInput {
17
+ transactionId?: string;
18
+ routeType?: string;
19
+ sourceTxExplorerUrl?: string;
20
+ statusResponse?: ExplorerStatusResponse;
21
+ actions?: RouteAction[];
22
+ sourceChain?: ChainData | undefined;
23
+ }
24
+
25
+ export interface MainExplorer {
26
+ url: string;
27
+ name: string;
28
+ }
29
+
30
+ export function getMainExplorerUrl(
31
+ transaction: ExplorerInput,
32
+ ): MainExplorer | undefined {
33
+ if (transaction.statusResponse?.coralTransactionUrl) {
34
+ return {
35
+ url: transaction.statusResponse.coralTransactionUrl,
36
+ name: "Squidscan",
37
+ };
38
+ }
39
+
40
+ if (transaction.statusResponse?.axelarTransactionUrl) {
41
+ return {
42
+ url: transaction.statusResponse.axelarTransactionUrl,
43
+ name: "Axelarscan",
44
+ };
45
+ }
46
+
47
+ const chainflipScanUrl = transaction.statusResponse?.toChain?.transactionUrl;
48
+ if (
49
+ chainflipScanUrl &&
50
+ isChainflipBridgeTransaction(transaction.actions ?? [])
51
+ ) {
52
+ return { url: chainflipScanUrl, name: "Chainflip" };
53
+ }
54
+
55
+ if (transaction.sourceTxExplorerUrl) {
56
+ return {
57
+ url: transaction.sourceTxExplorerUrl,
58
+ name: transaction.sourceChain?.networkName
59
+ ? `${transaction.sourceChain.networkName} explorer`
60
+ : "Source explorer",
61
+ };
62
+ }
63
+
64
+ if (transaction.statusResponse?.transactionUrl) {
65
+ return {
66
+ url: transaction.statusResponse.transactionUrl,
67
+ name: transaction.sourceChain?.networkName
68
+ ? `${transaction.sourceChain.networkName} explorer`
69
+ : "Source explorer",
70
+ };
71
+ }
72
+
73
+ return undefined;
74
+ }
@@ -0,0 +1,24 @@
1
+ import type { SquidTransaction } from "./types";
2
+ import { isSquidTransaction } from "./validation";
3
+
4
+ export async function getWalletHistory(
5
+ address: string,
6
+ integratorId: string,
7
+ apiUrl: string,
8
+ ): Promise<SquidTransaction[]> {
9
+ const url = new URL("/v2/history/wallet", apiUrl);
10
+ url.searchParams.append("address", address);
11
+
12
+ const response = await fetch(url, {
13
+ headers: { "X-Integrator-Id": integratorId },
14
+ });
15
+
16
+ if (!response.ok) {
17
+ throw new Error(`Failed to fetch wallet history: ${response.status}`);
18
+ }
19
+
20
+ const data: unknown = await response.json();
21
+ if (!Array.isArray(data)) return [];
22
+
23
+ return data.filter(isSquidTransaction);
24
+ }
@@ -0,0 +1,66 @@
1
+ import type { isChainflipBridgeTransaction } from "@0xsquid/react-hooks";
2
+
3
+ export type TransactionStatus = "ongoing" | "success" | "refunded";
4
+
5
+ export type RouteAction = NonNullable<
6
+ Parameters<typeof isChainflipBridgeTransaction>[0]
7
+ >[number];
8
+
9
+ export interface SquidTransactionToken {
10
+ address: string;
11
+ symbol: string;
12
+ decimals: number;
13
+ chainId: string;
14
+ }
15
+
16
+ export interface SquidTransaction {
17
+ createdAt: string;
18
+ fromChainId: string;
19
+ toChainId: string;
20
+ transactionId?: string;
21
+ routeType?: string;
22
+ sourceTxExplorerUrl?: string;
23
+ actions?: RouteAction[];
24
+ quote: {
25
+ route: {
26
+ params?: {
27
+ toAddress?: string;
28
+ fromAmount?: string;
29
+ toAmount?: string;
30
+ };
31
+ estimate: {
32
+ fromAmount: string;
33
+ fromToken: SquidTransactionToken;
34
+ toAmount: string;
35
+ toToken: SquidTransactionToken;
36
+ };
37
+ };
38
+ };
39
+ statusResponse: {
40
+ squidTransactionStatus: TransactionStatus;
41
+ coralTransactionUrl?: string;
42
+ axelarTransactionUrl?: string;
43
+ transactionUrl?: string;
44
+ fromChain?: { transactionUrl?: string };
45
+ toChain?: { transactionUrl?: string };
46
+ };
47
+ }
48
+
49
+ export interface FormattedTx {
50
+ fromToken: {
51
+ symbol: string;
52
+ logo: string;
53
+ };
54
+ fromChain: {
55
+ logo: string;
56
+ };
57
+ toToken: {
58
+ symbol: string;
59
+ };
60
+ fromAmount: string;
61
+ toAmount: string;
62
+ timestamp: string;
63
+ status: TransactionStatus;
64
+ explorer?: { url: string; name: string };
65
+ id: string;
66
+ }
@@ -0,0 +1,60 @@
1
+ import type { SquidTransaction, TransactionStatus } from "./types";
2
+
3
+ const VALID_STATUSES: Set<string> = new Set<TransactionStatus>([
4
+ "ongoing",
5
+ "success",
6
+ "refunded",
7
+ ]);
8
+
9
+ function isObject(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === "object" && value !== null;
11
+ }
12
+
13
+ function isSquidTransactionToken(value: unknown): boolean {
14
+ if (!isObject(value)) return false;
15
+ return (
16
+ typeof value.address === "string" &&
17
+ typeof value.symbol === "string" &&
18
+ typeof value.decimals === "number" &&
19
+ typeof value.chainId === "string"
20
+ );
21
+ }
22
+
23
+ export function isSquidTransaction(tx: unknown): tx is SquidTransaction {
24
+ if (!isObject(tx)) return false;
25
+
26
+ if (
27
+ typeof tx.createdAt !== "string" ||
28
+ typeof tx.fromChainId !== "string" ||
29
+ typeof tx.toChainId !== "string"
30
+ ) {
31
+ return false;
32
+ }
33
+
34
+ if (!isObject(tx.quote)) return false;
35
+ const quote = tx.quote;
36
+ if (!isObject(quote.route)) return false;
37
+ const route = quote.route;
38
+ if (!isObject(route.estimate)) return false;
39
+ const estimate = route.estimate;
40
+
41
+ if (
42
+ typeof estimate.fromAmount !== "string" ||
43
+ typeof estimate.toAmount !== "string" ||
44
+ !isSquidTransactionToken(estimate.fromToken) ||
45
+ !isSquidTransactionToken(estimate.toToken)
46
+ ) {
47
+ return false;
48
+ }
49
+
50
+ if (!isObject(tx.statusResponse)) return false;
51
+ const status = tx.statusResponse;
52
+ if (
53
+ typeof status.squidTransactionStatus !== "string" ||
54
+ !VALID_STATUSES.has(status.squidTransactionStatus)
55
+ ) {
56
+ return false;
57
+ }
58
+
59
+ return true;
60
+ }
@@ -0,0 +1,20 @@
1
+ import { create } from "zustand";
2
+ import type { DepositConfig } from "../types";
3
+
4
+ interface DepositState {
5
+ config: DepositConfig | null;
6
+ setConfig: (config: DepositConfig) => void;
7
+ }
8
+
9
+ export const useDepositStore = create<DepositState>((set) => ({
10
+ config: null,
11
+ setConfig: (config) => set({ config }),
12
+ }));
13
+
14
+ export const useIsPaymentMode = (): boolean =>
15
+ useDepositStore((s) => s.config?.mode === "payment");
16
+
17
+ export const usePaymentAmount = (): string | null =>
18
+ useDepositStore((s) =>
19
+ s.config?.mode === "payment" ? s.config.amount : null,
20
+ );
@@ -0,0 +1,10 @@
1
+ import { UserInputType } from "@0xsquid/ui";
2
+ import { create } from "zustand";
3
+
4
+ export const useInputMode = create<{
5
+ mode: UserInputType;
6
+ setMode: (_mode: UserInputType) => void;
7
+ }>((set) => ({
8
+ mode: UserInputType.USD,
9
+ setMode: (mode) => set({ mode }),
10
+ }));