@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.
- package/dist/types/DepositWidget.d.ts +0 -1
- package/dist/types/components/ViewTransition.d.ts +0 -1
- package/dist/types/components/shared/buttons/button.d.ts +0 -1
- package/dist/types/components/shared/icons/types.d.ts +0 -1
- package/dist/types/components/shared/icons/user-round.d.ts +0 -1
- package/dist/types/components/shared/navigation/base-navbar.d.ts +0 -1
- package/dist/types/components/shared/navigation/sub-navbar.d.ts +0 -1
- package/dist/types/components/token-badge-icon.d.ts +0 -1
- package/dist/types/components/token-list-item.d.ts +0 -1
- package/dist/types/components/view-container.d.ts +0 -1
- package/dist/types/constants.d.ts +0 -1
- package/dist/types/hooks/ui/useMainCTAButtonState.d.ts +0 -1
- package/dist/types/hooks/use-auto-select-token.d.ts +0 -1
- package/dist/types/hooks/use-deposit-route.d.ts +0 -1
- package/dist/types/hooks/use-token-selection.d.ts +0 -1
- package/dist/types/hooks/use-transaction-history.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/services/assets-service.d.ts +0 -1
- package/dist/types/services/wallet-history/format.d.ts +0 -1
- package/dist/types/services/wallet-history/format.test.d.ts +0 -1
- package/dist/types/services/wallet-history/get-main-explorer-url.d.ts +0 -1
- package/dist/types/services/wallet-history/get-wallet-history.d.ts +0 -1
- package/dist/types/services/wallet-history/types.d.ts +0 -1
- package/dist/types/services/wallet-history/validation.d.ts +0 -1
- package/dist/types/store/use-deposit-store.d.ts +0 -1
- package/dist/types/store/use-input-mode.d.ts +0 -1
- package/dist/types/store/useRouter.d.ts +0 -1
- package/dist/types/types.d.ts +0 -1
- package/dist/types/utils/format-date.d.ts +0 -1
- package/dist/types/utils/format-date.test.d.ts +0 -1
- package/dist/types/utils/transaction.d.ts +0 -1
- package/dist/types/views/connect-wallet/connect-wallet-view.d.ts +0 -1
- package/dist/types/views/connect-wallet/wallet-list-item.d.ts +0 -1
- package/dist/types/views/main/amount-input.d.ts +0 -1
- package/dist/types/views/main/connect-prompt.d.ts +0 -1
- package/dist/types/views/main/deposit-amount-input.d.ts +0 -1
- package/dist/types/views/main/deposit-form.d.ts +0 -1
- package/dist/types/views/main/main-cta-button.d.ts +0 -1
- package/dist/types/views/main/main-view.d.ts +0 -1
- package/dist/types/views/main/navbar/actions.d.ts +0 -1
- package/dist/types/views/main/navbar/icon.d.ts +0 -1
- package/dist/types/views/main/navbar/navbar.d.ts +0 -1
- package/dist/types/views/main/navbar/title.d.ts +0 -1
- package/dist/types/views/main/recipient/account.d.ts +0 -1
- package/dist/types/views/main/recipient/recipient.d.ts +0 -1
- package/dist/types/views/main/token-selector.d.ts +0 -1
- package/dist/types/views/qr-code.d.ts +0 -1
- package/dist/types/views/render-view.d.ts +0 -1
- package/dist/types/views/select-chain/chain-type-meta.d.ts +0 -1
- package/dist/types/views/select-chain/select-chain-view.d.ts +0 -1
- package/dist/types/views/select-token.d.ts +0 -1
- package/dist/types/views/transaction-history/activity-list-item.d.ts +0 -1
- package/dist/types/views/transaction-history/transaction-history-view.d.ts +0 -1
- package/dist/types/views/transaction-progress/helpers.d.ts +0 -1
- package/dist/types/views/transaction-progress/transaction-progress-view.d.ts +0 -1
- package/dist/types/views/transaction-progress/use-transaction-progress.d.ts +0 -1
- package/package.json +7 -7
- package/src/DepositWidget.tsx +158 -0
- package/src/compiled-tailwind.css +6100 -0
- package/src/components/ViewTransition.tsx +81 -0
- package/src/components/shared/buttons/button.tsx +17 -0
- package/src/components/shared/icons/types.ts +3 -0
- package/src/components/shared/icons/user-round.tsx +21 -0
- package/src/components/shared/navigation/base-navbar.tsx +15 -0
- package/src/components/shared/navigation/sub-navbar.tsx +46 -0
- package/src/components/token-badge-icon.tsx +31 -0
- package/src/components/token-list-item.tsx +84 -0
- package/src/components/view-container.tsx +16 -0
- package/src/constants.ts +1 -0
- package/src/css.d.ts +4 -0
- package/src/fonts/DMSans-Variable.woff2 +0 -0
- package/src/hooks/ui/useMainCTAButtonState.ts +143 -0
- package/src/hooks/use-auto-select-token.ts +65 -0
- package/src/hooks/use-deposit-route.ts +58 -0
- package/src/hooks/use-token-selection.ts +17 -0
- package/src/hooks/use-transaction-history.ts +198 -0
- package/src/index.ts +3 -0
- package/src/services/assets-service.ts +21 -0
- package/src/services/wallet-history/format.test.ts +63 -0
- package/src/services/wallet-history/format.ts +128 -0
- package/src/services/wallet-history/get-main-explorer-url.ts +74 -0
- package/src/services/wallet-history/get-wallet-history.ts +24 -0
- package/src/services/wallet-history/types.ts +66 -0
- package/src/services/wallet-history/validation.ts +60 -0
- package/src/store/use-deposit-store.ts +20 -0
- package/src/store/use-input-mode.ts +10 -0
- package/src/store/useRouter.ts +49 -0
- package/src/tailwind.css +16 -0
- package/src/types.ts +39 -0
- package/src/utils/format-date.test.ts +32 -0
- package/src/utils/format-date.ts +25 -0
- package/src/utils/transaction.ts +39 -0
- package/src/views/connect-wallet/connect-wallet-view.tsx +147 -0
- package/src/views/connect-wallet/wallet-list-item.tsx +69 -0
- package/src/views/main/amount-input.tsx +272 -0
- package/src/views/main/connect-prompt.tsx +47 -0
- package/src/views/main/deposit-amount-input.tsx +42 -0
- package/src/views/main/deposit-form.tsx +13 -0
- package/src/views/main/main-cta-button.tsx +14 -0
- package/src/views/main/main-view.tsx +24 -0
- package/src/views/main/navbar/actions.tsx +25 -0
- package/src/views/main/navbar/icon.tsx +11 -0
- package/src/views/main/navbar/navbar.tsx +16 -0
- package/src/views/main/navbar/title.tsx +64 -0
- package/src/views/main/recipient/account.tsx +81 -0
- package/src/views/main/recipient/recipient.tsx +64 -0
- package/src/views/main/token-selector.tsx +77 -0
- package/src/views/qr-code.tsx +14 -0
- package/src/views/render-view.tsx +28 -0
- package/src/views/select-chain/chain-type-meta.ts +37 -0
- package/src/views/select-chain/select-chain-view.tsx +97 -0
- package/src/views/select-token.tsx +227 -0
- package/src/views/transaction-history/activity-list-item.tsx +87 -0
- package/src/views/transaction-history/transaction-history-view.tsx +58 -0
- package/src/views/transaction-progress/helpers.tsx +93 -0
- package/src/views/transaction-progress/transaction-progress-view.tsx +217 -0
- 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,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
|
+
}));
|