@xswap-link/sdk 0.0.13 → 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/.github/workflows/main.yml +1 -1
- package/.github/workflows/publish.yml +1 -1
- package/.prettierrc +7 -0
- package/CHANGELOG.md +12 -1
- package/README.md +23 -14
- package/dist/index.css +1012 -0
- package/dist/index.d.mts +209 -52
- package/dist/index.d.ts +209 -52
- package/dist/index.js +33963 -88
- package/dist/index.mjs +33949 -76
- package/package.json +8 -2
- package/postcss.config.js +3 -0
- package/src/components/Alert/index.tsx +9 -0
- package/src/components/Skeleton/index.tsx +10 -0
- package/src/components/TxConfigForm/Form.tsx +567 -0
- package/src/components/TxConfigForm/History.tsx +159 -0
- package/src/components/TxConfigForm/HistoryCard.tsx +209 -0
- package/src/components/TxConfigForm/TokenPicker.tsx +165 -0
- package/src/components/TxConfigForm/index.jsx +100 -0
- package/src/components/global.css +34 -0
- package/src/components/global.d.ts +6 -0
- package/src/components/icons/ArrowRightIcon.tsx +11 -0
- package/src/components/icons/ArrowUpRightIcon.tsx +11 -0
- package/src/components/icons/CheckIcon.tsx +11 -0
- package/src/components/icons/ChevronDownIcon.tsx +15 -0
- package/src/components/icons/ChevronUpIcon.tsx +15 -0
- package/src/components/icons/CircularProgressIcon.tsx +24 -0
- package/src/components/icons/CloseIcon.tsx +15 -0
- package/src/components/icons/CoinsIcon.tsx +15 -0
- package/src/components/icons/DownArrorIcon.tsx +17 -0
- package/src/components/icons/HistoryIcon.tsx +13 -0
- package/src/components/icons/HourGlassIcon.tsx +11 -0
- package/src/components/icons/PercentageIcon.tsx +29 -0
- package/src/components/icons/SearchIcon.tsx +15 -0
- package/src/components/icons/XMarkIcon.tsx +11 -0
- package/src/components/icons/index.ts +14 -0
- package/src/components/index.ts +3 -0
- package/src/constants/index.ts +6 -5
- package/src/contracts/abi/BatchQuery.json +52 -0
- package/src/contracts/abi/index.ts +2 -0
- package/src/contracts/addresses.ts +34 -0
- package/src/contracts/index.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useDebounce.tsx +21 -0
- package/src/index.ts +6 -0
- package/src/models/Addresses.ts +12 -0
- package/src/models/Route.ts +17 -5
- package/src/models/TokenData.ts +45 -0
- package/src/models/TransactionHistory.ts +48 -0
- package/src/models/XSwapConfig.ts +3 -0
- package/src/models/forms/TxConfigFormData.ts +8 -0
- package/src/models/forms/index.ts +1 -0
- package/src/models/index.ts +6 -6
- package/src/models/integrations/GenerateStakingCallsParams.ts +8 -0
- package/src/models/integrations/index.ts +1 -0
- package/src/models/payloads/GetPricesPayload.ts +4 -0
- package/src/models/payloads/GetRoutePayload.ts +1 -3
- package/src/models/payloads/GetSwapTxPayload.ts +8 -0
- package/src/models/payloads/index.ts +2 -0
- package/src/services/api.ts +46 -64
- package/src/services/index.ts +1 -0
- package/src/services/integrations/customCalls/index.ts +1 -0
- package/src/services/integrations/customCalls/staking.ts +83 -0
- package/src/services/integrations/index.ts +2 -0
- package/src/services/integrations/transactions.ts +30 -0
- package/src/utils/contracts.ts +153 -11
- package/src/utils/index.ts +49 -1
- package/src/utils/numbers.ts +47 -0
- package/src/utils/strings.ts +6 -0
- package/tailwind.config.js +14 -0
- package/test/api.test.ts +1 -1
- package/tsconfig.json +10 -1
- package/xswap.config.ts +18 -0
- package/index.ts +0 -5
- package/src/models/ApiOverrides.ts +0 -3
- package/src/models/Chain.ts +0 -20
- package/src/models/Prices.ts +0 -9
- package/src/models/Token.ts +0 -10
- package/src/models/XSwapFee.ts +0 -4
- package/src/models/XSwapFees.ts +0 -7
- package/src/utils/bigNumbers.ts +0 -7
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Transaction, TransactionStatus, Chain } from "@src/models";
|
|
2
|
+
import React, { FC, useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { getHistory } from "@src/services";
|
|
4
|
+
import { FALLBACK_CROSSCHAIN_ESTIMATION_TIME } from "@src/constants";
|
|
5
|
+
import BigNumberJS from "bignumber.js";
|
|
6
|
+
import { HistoryCard } from "./HistoryCard";
|
|
7
|
+
import { CircularProgressIcon } from "../icons";
|
|
8
|
+
|
|
9
|
+
const PENDING_TXS_LOCAL_STORAGE_KEY = "pending_transactions";
|
|
10
|
+
|
|
11
|
+
type LocalHistory = { [walletAddress: string]: Transaction[] };
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
signer: string;
|
|
15
|
+
supportedChains: Chain[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const History: FC<Props> = ({ signer, supportedChains }) => {
|
|
19
|
+
const [mappedHistory, setMappedHistory] = useState<Transaction[]>();
|
|
20
|
+
// const [latestHistory, setLatestHistory] = useState<Transaction[]>();
|
|
21
|
+
const [historyLoadedOnce, setHistoryLoadedOnce] = useState(false);
|
|
22
|
+
|
|
23
|
+
const getLocalHistory = () => {
|
|
24
|
+
return JSON.parse(
|
|
25
|
+
localStorage.getItem(PENDING_TXS_LOCAL_STORAGE_KEY) || "{}",
|
|
26
|
+
) as LocalHistory;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const setLocalHistory = (data: LocalHistory) => {
|
|
30
|
+
localStorage.setItem(PENDING_TXS_LOCAL_STORAGE_KEY, JSON.stringify(data));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const fetchHistory = useCallback(async () => {
|
|
34
|
+
const downloadedHistory = await getHistory({ walletAddress: signer });
|
|
35
|
+
const fetchedTransactions: Transaction[] = [];
|
|
36
|
+
if (downloadedHistory) {
|
|
37
|
+
for (const entry of downloadedHistory.history) {
|
|
38
|
+
if (!entry.source) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let status: TransactionStatus;
|
|
42
|
+
if (entry.failed) status = "REVERTED";
|
|
43
|
+
if (!!entry.target) {
|
|
44
|
+
status = "DONE";
|
|
45
|
+
} else {
|
|
46
|
+
status = "IN_PROGRESS";
|
|
47
|
+
}
|
|
48
|
+
const timestampMs = new Date(entry.source?.blockTime).getTime();
|
|
49
|
+
|
|
50
|
+
let estimatedDeliveryTimestamp = FALLBACK_CROSSCHAIN_ESTIMATION_TIME;
|
|
51
|
+
|
|
52
|
+
if (new BigNumberJS(entry.source.valueForInstantCcipRecieve).gt(0)) {
|
|
53
|
+
estimatedDeliveryTimestamp = timestampMs + 30 * 1000;
|
|
54
|
+
} else {
|
|
55
|
+
const blockchainEstimate = timestampMs + 30 * 1000 * 60;
|
|
56
|
+
|
|
57
|
+
if (blockchainEstimate) {
|
|
58
|
+
estimatedDeliveryTimestamp = blockchainEstimate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fetchedTransactions.push({
|
|
63
|
+
hash: entry.source.transactionHash,
|
|
64
|
+
timestamp: timestampMs / 1000,
|
|
65
|
+
sourceChainId: entry.source.blockchainId,
|
|
66
|
+
targetChainId: entry.source.targetBlockchainId,
|
|
67
|
+
amountWei: entry.source.tokenAmount,
|
|
68
|
+
tokenAddress: entry.source.tokenAddress,
|
|
69
|
+
tokenOutAddress:
|
|
70
|
+
entry.target?.tokenAddress || entry.source.tokenOutAddress,
|
|
71
|
+
tokenOutAmount:
|
|
72
|
+
entry.target?.tokenAmount || entry.source.estimatedAmountOut,
|
|
73
|
+
estimatedDeliveryTimestamp,
|
|
74
|
+
status,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const txHashToTxFromAPI: Record<string, Transaction> = {};
|
|
79
|
+
for (const transaction of fetchedTransactions) {
|
|
80
|
+
txHashToTxFromAPI[transaction.hash] = transaction;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const localHistory = getLocalHistory();
|
|
84
|
+
const newLocalHistory: Record<string, Transaction[]> = {};
|
|
85
|
+
|
|
86
|
+
for (const [walletAddress, transactions] of Object.entries(
|
|
87
|
+
localHistory,
|
|
88
|
+
)) {
|
|
89
|
+
if (walletAddress.toLowerCase() !== signer.toLowerCase()) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const newLocalHistoryForWallet: Transaction[] = [];
|
|
93
|
+
|
|
94
|
+
for (const transaction of transactions) {
|
|
95
|
+
if (!txHashToTxFromAPI[transaction.hash]) {
|
|
96
|
+
// localStorage has fresher data.
|
|
97
|
+
fetchedTransactions.push(transaction);
|
|
98
|
+
newLocalHistoryForWallet.push(transaction);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
txHashToTxFromAPI[transaction.hash] &&
|
|
104
|
+
transaction.status === "DONE" &&
|
|
105
|
+
txHashToTxFromAPI[transaction.hash]?.status === "IN_PROGRESS"
|
|
106
|
+
) {
|
|
107
|
+
// If we know on the frontend that the tx is done we should update backend result
|
|
108
|
+
txHashToTxFromAPI[transaction.hash]!.status = "DONE";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
txHashToTxFromAPI[transaction.hash]?.status === "DONE" ||
|
|
113
|
+
txHashToTxFromAPI[transaction.hash]?.status === "REVERTED"
|
|
114
|
+
) {
|
|
115
|
+
// API has the same or fresher data.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
newLocalHistory[walletAddress.toLowerCase()] = newLocalHistoryForWallet;
|
|
121
|
+
}
|
|
122
|
+
setLocalHistory(newLocalHistory);
|
|
123
|
+
setMappedHistory(
|
|
124
|
+
fetchedTransactions.sort((a, b) => b.timestamp - a.timestamp),
|
|
125
|
+
);
|
|
126
|
+
setHistoryLoadedOnce(true);
|
|
127
|
+
}
|
|
128
|
+
}, [signer, getHistory]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!historyLoadedOnce) fetchHistory();
|
|
132
|
+
const timer = setInterval(() => {
|
|
133
|
+
fetchHistory();
|
|
134
|
+
}, 60000);
|
|
135
|
+
return () => clearInterval(timer);
|
|
136
|
+
}, [signer, fetchHistory, getHistory, historyLoadedOnce]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="w-full h-[50vh] overflow-scroll overflow-x-hidden">
|
|
140
|
+
{mappedHistory?.length === 0 && historyLoadedOnce && (
|
|
141
|
+
<div className="w-full text-center py-4 px-0 text-[rgb(158,158,158)]">
|
|
142
|
+
Your history is empty...
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
{!mappedHistory && !historyLoadedOnce && (
|
|
146
|
+
<div className="flex w-full h-full items-center justify-center">
|
|
147
|
+
<CircularProgressIcon />
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
{mappedHistory?.map((transaction, index) => (
|
|
151
|
+
<HistoryCard
|
|
152
|
+
key={index}
|
|
153
|
+
transaction={transaction}
|
|
154
|
+
supportedChains={supportedChains}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { FC, useCallback, useMemo } from "react";
|
|
2
|
+
import { format } from "date-fns";
|
|
3
|
+
import { Transaction, Chain, Token } from "@src/models";
|
|
4
|
+
import { deepMergeObjects, weiToHumanReadable } from "@src/utils";
|
|
5
|
+
import {
|
|
6
|
+
ArrowRightIcon,
|
|
7
|
+
ArrowUpRightIcon,
|
|
8
|
+
CheckIcon,
|
|
9
|
+
HourGlassIcon,
|
|
10
|
+
XMarkIcon,
|
|
11
|
+
} from "@src/components/icons";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
transaction: Transaction;
|
|
15
|
+
supportedChains: Chain[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const HistoryCard: FC<Props> = ({ transaction, supportedChains }) => {
|
|
19
|
+
const getDate = useCallback((blockTimestamp: number) => {
|
|
20
|
+
const date = new Date(blockTimestamp * 1000);
|
|
21
|
+
return format(date, "d MMM yyyy HH:mm:ss");
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const date = getDate(transaction.timestamp);
|
|
25
|
+
|
|
26
|
+
const supportedTokens: Record<string, Record<string, Token>> = useMemo(() => {
|
|
27
|
+
const customTokens = JSON.parse(
|
|
28
|
+
localStorage.getItem("custom-tokens") || "{}",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const chainIdToTokenAddressToData: Record<
|
|
32
|
+
string,
|
|
33
|
+
Record<string, Token>
|
|
34
|
+
> = {};
|
|
35
|
+
|
|
36
|
+
supportedChains.forEach((chain) => {
|
|
37
|
+
chainIdToTokenAddressToData[chain.chainId] = {};
|
|
38
|
+
chain.tokens.forEach((token) => {
|
|
39
|
+
chainIdToTokenAddressToData[chain.chainId]![token.address] = token;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
return deepMergeObjects(chainIdToTokenAddressToData, customTokens);
|
|
43
|
+
}, [supportedChains]);
|
|
44
|
+
|
|
45
|
+
const findTokenDataByAddressAndChain = useCallback(
|
|
46
|
+
(tokenAddress: string, chainId: string): Token | undefined => {
|
|
47
|
+
return supportedTokens[chainId]?.[tokenAddress];
|
|
48
|
+
},
|
|
49
|
+
[supportedTokens],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const tokenData = findTokenDataByAddressAndChain(
|
|
53
|
+
transaction.tokenAddress.toLowerCase(),
|
|
54
|
+
transaction.sourceChainId,
|
|
55
|
+
);
|
|
56
|
+
const tokenOutData = transaction.tokenOutAddress
|
|
57
|
+
? findTokenDataByAddressAndChain(
|
|
58
|
+
transaction.tokenOutAddress?.toLowerCase(),
|
|
59
|
+
transaction.targetChainId,
|
|
60
|
+
)
|
|
61
|
+
: undefined;
|
|
62
|
+
const transferredAmount = weiToHumanReadable({
|
|
63
|
+
amount: transaction.amountWei,
|
|
64
|
+
decimals: tokenData?.decimals || 18,
|
|
65
|
+
precisionFractionalPlaces: 4,
|
|
66
|
+
});
|
|
67
|
+
const receivedAmount = weiToHumanReadable({
|
|
68
|
+
amount: transaction?.tokenOutAmount || "0",
|
|
69
|
+
decimals: tokenOutData?.decimals || 18,
|
|
70
|
+
precisionFractionalPlaces: 4,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const sourceChainData = supportedChains.find(
|
|
74
|
+
(chain) => chain.chainId === transaction.sourceChainId,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const targetChainData = supportedChains.find(
|
|
78
|
+
(chain) => chain.chainId === transaction.targetChainId,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col border border-solid border-[rgba(255,255,255,0.1)] rounded-xl bg-[rgb(15,15,15)] p-2 gap-4 mb-2">
|
|
83
|
+
<div className="flex items-center">
|
|
84
|
+
<div className="text-[rgba(255,255,255,0.6)] text-xs">{date}</div>
|
|
85
|
+
<div className="flex flex-grow justify-end items-center">
|
|
86
|
+
{transaction.status === "IN_PROGRESS" && (
|
|
87
|
+
<div className="flex items-center">
|
|
88
|
+
<div className="w-3.5 h-3.5 mr-1 text-[rgb(250,200,100)]">
|
|
89
|
+
<HourGlassIcon />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="text-xs text-[rgb(250,200,100)]">In progress</div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
{transaction.status === "DONE" && (
|
|
96
|
+
<div className="flex items-center">
|
|
97
|
+
<div className="w-3.5 h-3.5 mr-1 text-[rgb(100,200,100)]">
|
|
98
|
+
<CheckIcon />
|
|
99
|
+
</div>
|
|
100
|
+
<div className="text-xs text-[rgb(100,200,100)]">Done</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{transaction.status === "REVERTED" && (
|
|
105
|
+
<div className="flex items-center">
|
|
106
|
+
<div className="w-4 h-4 mr-1 text-[rgb(255,100,100)]">
|
|
107
|
+
<XMarkIcon />
|
|
108
|
+
</div>
|
|
109
|
+
<div className="text-xs text-[rgb(255,100,100)]">Reverted</div>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
<a
|
|
114
|
+
href={`${
|
|
115
|
+
supportedChains.find(
|
|
116
|
+
(chain) => chain.chainId === transaction.sourceChainId,
|
|
117
|
+
)?.transactionExplorer
|
|
118
|
+
}/${transaction.hash}`}
|
|
119
|
+
target="_blank"
|
|
120
|
+
rel="noreferrer"
|
|
121
|
+
className="ml-2 no-underline"
|
|
122
|
+
aria-label="Show the transaction in the chain explorer"
|
|
123
|
+
>
|
|
124
|
+
<div className="w-3.5 h-3.5 text-[rgba(255,255,255,0.6)]">
|
|
125
|
+
<ArrowUpRightIcon />
|
|
126
|
+
</div>
|
|
127
|
+
</a>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex flex-wrap gap-8">
|
|
131
|
+
<div className="flex flex-grow items-center">
|
|
132
|
+
<div className="flex items-center">
|
|
133
|
+
<img
|
|
134
|
+
src={sourceChainData?.image}
|
|
135
|
+
alt={`${sourceChainData?.name} logo`}
|
|
136
|
+
className="w-5 h-5 mr-1"
|
|
137
|
+
/>
|
|
138
|
+
<div className="text-xs text-[rgba(255,255,255,0.6)]">
|
|
139
|
+
{sourceChainData?.displayName}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="w-3.5 h-3.5 text-[rgba(255,255,255,0.6)] my-0 mx-1.5">
|
|
144
|
+
<ArrowRightIcon />
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div className="flex items-center">
|
|
148
|
+
<img
|
|
149
|
+
src={targetChainData?.image}
|
|
150
|
+
alt={`${targetChainData?.name} logo`}
|
|
151
|
+
className="w-5 h-5 mr-1"
|
|
152
|
+
/>
|
|
153
|
+
<div className="text-xs text-[rgba(255,255,255,0.6)]">
|
|
154
|
+
{targetChainData?.displayName}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="flex items-center mb-2 last:mb-0">
|
|
159
|
+
<div className="flex flex-1 items-center flex-wrap text-xs text-[rgba(255,255,255,0.6)]">
|
|
160
|
+
<div className="flex items-center">
|
|
161
|
+
{Number(transferredAmount) < 0.0001
|
|
162
|
+
? "<0.0001"
|
|
163
|
+
: transferredAmount}
|
|
164
|
+
<div className="text-xs ml-1 text-rgba(255,255,255,0.6)">
|
|
165
|
+
{tokenData?.symbol}
|
|
166
|
+
</div>
|
|
167
|
+
{tokenData?.image ? (
|
|
168
|
+
<img
|
|
169
|
+
src={tokenData?.image}
|
|
170
|
+
alt={tokenData?.name || ""}
|
|
171
|
+
className="w-5 h-5 ml-1"
|
|
172
|
+
/>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="flex items-center justify-center w-5 h-5 text-[10px] ml-1 rounded-full bg-[#272e40] text-[#e0e7fa]">
|
|
175
|
+
{tokenData?.symbol.substring(0, 1)}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
{transaction.tokenOutAddress && (
|
|
180
|
+
<>
|
|
181
|
+
<div className="w-3.5 h-3.5 text-[rgba(255,255,255,0.6)] my-0 mx-1.5">
|
|
182
|
+
<ArrowRightIcon />
|
|
183
|
+
</div>
|
|
184
|
+
<div className="flex items-center">
|
|
185
|
+
{Number(receivedAmount) < 0.0001 ? "<0.0001" : receivedAmount}
|
|
186
|
+
<div className="text-xs ml-1 text-[rgba(255,255,255,0.6)]">
|
|
187
|
+
{tokenOutData?.symbol}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{tokenOutData?.image ? (
|
|
191
|
+
<img
|
|
192
|
+
src={tokenOutData?.image}
|
|
193
|
+
alt={tokenOutData?.name || ""}
|
|
194
|
+
className="w-5 h-5 ml-1"
|
|
195
|
+
/>
|
|
196
|
+
) : (
|
|
197
|
+
<div className="flex items-center justify-center w-5 h-5 text-[10px] ml-1 rounded-full bg-[#272e40] text-[#e0e7fa]">
|
|
198
|
+
{tokenData?.symbol.substring(0, 1)}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { MouseEvent, FC, useState, useMemo } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { SearchIcon } from "../icons";
|
|
4
|
+
import { Token, TokenBalances } from "@src/models";
|
|
5
|
+
import { weiToHumanReadable } from "@src/utils";
|
|
6
|
+
import { Skeleton } from "@src/components";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
onCloseClick: () => void;
|
|
10
|
+
tokens: Token[] | undefined;
|
|
11
|
+
selectedToken: Token | undefined;
|
|
12
|
+
setSelectedToken: (token: Token) => void;
|
|
13
|
+
signer: string;
|
|
14
|
+
balances: TokenBalances | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const TokenPicker: FC<Props> = ({
|
|
18
|
+
onCloseClick,
|
|
19
|
+
tokens,
|
|
20
|
+
selectedToken,
|
|
21
|
+
setSelectedToken,
|
|
22
|
+
signer,
|
|
23
|
+
balances,
|
|
24
|
+
}) => {
|
|
25
|
+
const [searchValue, setSearchValue] = useState("");
|
|
26
|
+
|
|
27
|
+
const modalRoot = document.querySelector(".xswap");
|
|
28
|
+
|
|
29
|
+
const onBackdropClick = (e: MouseEvent<HTMLDivElement>) => {
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
e.nativeEvent.stopImmediatePropagation();
|
|
32
|
+
if (e.target === e.currentTarget) {
|
|
33
|
+
onCloseClick();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const filteredTokens = useMemo(() => {
|
|
38
|
+
const isMatch = (value: string, searchValue: string) =>
|
|
39
|
+
value.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
|
|
40
|
+
return tokens?.filter(
|
|
41
|
+
(token) =>
|
|
42
|
+
isMatch(token.symbol, searchValue) ||
|
|
43
|
+
isMatch(token.name, searchValue) ||
|
|
44
|
+
token.address === searchValue.toLowerCase(),
|
|
45
|
+
);
|
|
46
|
+
}, [searchValue, tokens]);
|
|
47
|
+
|
|
48
|
+
return modalRoot
|
|
49
|
+
? createPortal(
|
|
50
|
+
<div
|
|
51
|
+
onClick={onBackdropClick}
|
|
52
|
+
className="box-border fixed h-full w-full z-[999] top-0 left-0 bg-[rgba(0,0,0,0.8)] flex items-center justify-center p-5"
|
|
53
|
+
>
|
|
54
|
+
<div className="relative bg-black rounded-3xl p-6 max-h-[70%] flex flex-col text-base text-white w-[452px] h-[70%] border border-solid border-[rgba(255,255,255,0.2)]">
|
|
55
|
+
<p className="text-base mb-4">Pick a token</p>
|
|
56
|
+
<div className="flex items-center border border-solid border-[rgba(255,255,255,0.1)] rounded-lg py-0 px-3 gap-2 bg-[rgba(15,15,15,1)]">
|
|
57
|
+
<div className="w-6 h-6">
|
|
58
|
+
<SearchIcon />
|
|
59
|
+
</div>
|
|
60
|
+
<input
|
|
61
|
+
placeholder="Search name or paste address"
|
|
62
|
+
value={searchValue}
|
|
63
|
+
onChange={(e) => setSearchValue(e.target.value)}
|
|
64
|
+
className="relative h-[41px] leading-[41px] font-normal z-[1] w-full text-white bg-transparent border-none outline-none placeholder:text-sm"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="my-4 mx-0 flex flex-wrap gap-3">
|
|
68
|
+
{tokens
|
|
69
|
+
?.filter((token) => token?.quickPick)
|
|
70
|
+
.map((token) => (
|
|
71
|
+
<div
|
|
72
|
+
key={token.address}
|
|
73
|
+
className={`flex gap-2 py-1 px-2 items-center bg-[rgb(15,15,15)] rounded-2xl border border-solid border-[rgba(255,255,255,0.1)] hover:bg-[rgb(25,25,25)] hover:cursor-pointer ${
|
|
74
|
+
token.address === selectedToken?.address
|
|
75
|
+
? "bg-[rgb(35, 35, 35)]"
|
|
76
|
+
: ""
|
|
77
|
+
}`}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
setSelectedToken(token);
|
|
80
|
+
onCloseClick();
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<img src={token.image} alt={token.name} className="w-5" />
|
|
84
|
+
<div className="text-sm">{token.symbol}</div>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
<div className="h-[2px] my-0 mx-[-24px] w-[100%+48px] px-12 bg-[rgba(255,255,255,0.1)]" />
|
|
89
|
+
<div className="overflow-y-scroll h-full mx-[-24px] w-[100%+48px] flex flex-col">
|
|
90
|
+
{filteredTokens && filteredTokens.length > 0 ? (
|
|
91
|
+
filteredTokens
|
|
92
|
+
.sort((a, b) =>
|
|
93
|
+
balances &&
|
|
94
|
+
balances[a.address] &&
|
|
95
|
+
balances[b.address] &&
|
|
96
|
+
balances[a.address]?.lte(balances[b.address] || "0")
|
|
97
|
+
? 1
|
|
98
|
+
: -1,
|
|
99
|
+
)
|
|
100
|
+
.map((token, index) => (
|
|
101
|
+
<div
|
|
102
|
+
key={`${index}_${token.address}`}
|
|
103
|
+
className={`flex gap-3 py-2 px-[24px] w-full items-center hover:bg-[rgb(25,25,25)] hover:cursor-pointer ${
|
|
104
|
+
token.address === selectedToken?.address
|
|
105
|
+
? "bg-[rgb(35,35,35)]"
|
|
106
|
+
: ""
|
|
107
|
+
}`}
|
|
108
|
+
onClick={() => {
|
|
109
|
+
setSelectedToken(token);
|
|
110
|
+
onCloseClick();
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{token.image ? (
|
|
114
|
+
<img
|
|
115
|
+
src={token.image}
|
|
116
|
+
alt={token.name}
|
|
117
|
+
className="token-picker__all__logo"
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="flex items-center justify-center w-9 h-9 text-[15px] ml-1 rounded-full bg-[#272e40] text-[#e0e7fa]">
|
|
121
|
+
{token?.symbol.substring(0, 1)}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
<div className="flex justify-between items-center gap-1 overflow-hidden grow">
|
|
125
|
+
<div className="flex flex-col leading-[16px]">
|
|
126
|
+
<div className="overflow-hidden whitespace-nowrap text-ellipsis text-[15px]">
|
|
127
|
+
{token.name}
|
|
128
|
+
</div>
|
|
129
|
+
<div className="text-[#888] text-[11px]">
|
|
130
|
+
{token.symbol}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{signer && (
|
|
134
|
+
<>
|
|
135
|
+
{balances &&
|
|
136
|
+
balances[token.address] &&
|
|
137
|
+
token.decimals ? (
|
|
138
|
+
<div className="text-xs">
|
|
139
|
+
{weiToHumanReadable({
|
|
140
|
+
amount:
|
|
141
|
+
balances[token.address]?.toString() || "0",
|
|
142
|
+
decimals: token.decimals,
|
|
143
|
+
precisionFractionalPlaces: 4,
|
|
144
|
+
})}
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<Skeleton w="w-12" h="h-3" />
|
|
148
|
+
)}
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
))
|
|
154
|
+
) : (
|
|
155
|
+
<div className="mt-4 flex justify-center">
|
|
156
|
+
<p className="text-sm text-white">No tokens found.</p>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>,
|
|
162
|
+
modalRoot,
|
|
163
|
+
)
|
|
164
|
+
: null;
|
|
165
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import "@src/global.css";
|
|
2
|
+
import { Form } from "./Form";
|
|
3
|
+
import { CloseIcon } from "@src/components/icons/CloseIcon";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import ReactDOM from "react-dom";
|
|
6
|
+
|
|
7
|
+
const TxConfigForm = ({ isOpen, onClose, content }) => {
|
|
8
|
+
const onBackdropClick = (e) => {
|
|
9
|
+
e.stopPropagation();
|
|
10
|
+
e.nativeEvent.stopImmediatePropagation();
|
|
11
|
+
if (e.target === e.currentTarget) {
|
|
12
|
+
onClose();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (!isOpen) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className="top-0 left-0 right-0 bottom-0 bg-[rgba(0,0,0,0.8)] fixed flex items-center justify-center z-10"
|
|
21
|
+
onClick={onBackdropClick}
|
|
22
|
+
>
|
|
23
|
+
<div className="relative bg-black rounded-3xl w-lg overflow-auto text-white border-2 border-solid border-[rgba(255,255,255,0.1)]">
|
|
24
|
+
{content}
|
|
25
|
+
<div
|
|
26
|
+
className="absolute top-3.5 right-3 cursor-pointer text-white"
|
|
27
|
+
onClick={onClose}
|
|
28
|
+
>
|
|
29
|
+
<CloseIcon />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class TxConfigModalWrapper extends React.Component {
|
|
37
|
+
constructor(props) {
|
|
38
|
+
super(props);
|
|
39
|
+
this.state = { isOpen: false, content: "No wallet connected" };
|
|
40
|
+
this.closeModal = this.closeModal.bind(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
openModal(content) {
|
|
44
|
+
this.setState({ isOpen: true, content });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
closeModal() {
|
|
48
|
+
this.setState({ isOpen: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
return this.state.isOpen ? (
|
|
53
|
+
<TxConfigForm
|
|
54
|
+
isOpen={this.state.isOpen}
|
|
55
|
+
onClose={this.closeModal}
|
|
56
|
+
content={this.state.content}
|
|
57
|
+
/>
|
|
58
|
+
) : null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// skip on server-side integrations
|
|
63
|
+
let modalWrapperInstance;
|
|
64
|
+
if (typeof document !== "undefined") {
|
|
65
|
+
// Create a div element that will host the modal, and append it to the body
|
|
66
|
+
const modalDiv = document.createElement("div");
|
|
67
|
+
modalDiv.classList.add("xswap");
|
|
68
|
+
document.body.appendChild(modalDiv);
|
|
69
|
+
|
|
70
|
+
// // Render the ModalWrapper component into the modalDiv
|
|
71
|
+
modalWrapperInstance = ReactDOM.render(<TxConfigModalWrapper />, modalDiv);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const openTxConfigForm = async ({
|
|
75
|
+
dstChainId,
|
|
76
|
+
dstTokenAddr,
|
|
77
|
+
customContractCalls,
|
|
78
|
+
desc,
|
|
79
|
+
supportedChains,
|
|
80
|
+
}) => {
|
|
81
|
+
try {
|
|
82
|
+
return await new Promise((resolve) => {
|
|
83
|
+
modalWrapperInstance.openModal(
|
|
84
|
+
<Form
|
|
85
|
+
dstChainId={dstChainId}
|
|
86
|
+
dstTokenAddr={dstTokenAddr}
|
|
87
|
+
customContractCalls={customContractCalls}
|
|
88
|
+
desc={desc}
|
|
89
|
+
supportedChains={supportedChains}
|
|
90
|
+
onSubmit={(route) => {
|
|
91
|
+
resolve(route);
|
|
92
|
+
modalWrapperInstance.closeModal();
|
|
93
|
+
}}
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new Error(`User input failed, ${err}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
.xswap {
|
|
6
|
+
/* Works on Firefox */
|
|
7
|
+
* {
|
|
8
|
+
scrollbar-width: thin;
|
|
9
|
+
scrollbar-color: #252525 #000;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Works on Chrome, Edge, and Safari */
|
|
13
|
+
*::-webkit-scrollbar {
|
|
14
|
+
width: 4px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
*::-webkit-scrollbar-track {
|
|
18
|
+
background: #000;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
*::-webkit-scrollbar-thumb {
|
|
23
|
+
background-color: #252525;
|
|
24
|
+
border-radius: 4px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
button {
|
|
28
|
+
@apply text-white border-none rounded-2xl text-xl py-4 cursor-pointer w-full bg-gradient-to-r from-[#3681c6] to-[#2b4a9d] disabled:opacity-25;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.globalBorder {
|
|
32
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const ArrowRightIcon = () => {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
viewBox="0 0 448 512"
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
>
|
|
8
|
+
<path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z" />
|
|
9
|
+
</svg>
|
|
10
|
+
);
|
|
11
|
+
};
|