@stableflow/pay-widgets 0.0.1-beta-82ede460a5-20260923
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/token-select/index.d.ts +165 -0
- package/dist/token-select/index.js +1467 -0
- package/dist/token-select/index.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1467 @@
|
|
|
1
|
+
// src/token-select/context.tsx
|
|
2
|
+
import { createContext, useContext, useEffect as useEffect2, useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
// src/token-select/balances.ts
|
|
5
|
+
import { useSyncExternalStore } from "react";
|
|
6
|
+
|
|
7
|
+
// src/token-select/config.ts
|
|
8
|
+
var TOKEN_BALANCE_POLL_MS = 6e4;
|
|
9
|
+
var CONFIG_STALE_MS = 30 * 60 * 1e3;
|
|
10
|
+
var ALL_CHAIN_FILTER = "all";
|
|
11
|
+
var DEFAULT_API_HOST = "https://api.stableflow.ai";
|
|
12
|
+
var POPULAR_SECTION_TITLE = "Popular on your network";
|
|
13
|
+
var TOKEN_SELECT_PREFS_STORAGE_KEY = "stableflow-pay:token-select-prefs:v1";
|
|
14
|
+
var TOKEN_SELECT_PREFS_VERSION = 2;
|
|
15
|
+
var MAX_RECENT_BLOCKCHAINS = 16;
|
|
16
|
+
var MAX_RECENT_ASSETS = 3;
|
|
17
|
+
var DISCONNECTED_CHAIN_ORDER = [
|
|
18
|
+
"near",
|
|
19
|
+
"sol",
|
|
20
|
+
"tron",
|
|
21
|
+
"eth",
|
|
22
|
+
"bsc",
|
|
23
|
+
"arb",
|
|
24
|
+
"base",
|
|
25
|
+
"pol",
|
|
26
|
+
"avax",
|
|
27
|
+
"op",
|
|
28
|
+
"bera",
|
|
29
|
+
"gnosis",
|
|
30
|
+
"xlayer",
|
|
31
|
+
"scroll"
|
|
32
|
+
];
|
|
33
|
+
var DEFAULT_CHAIN_KINDS = {
|
|
34
|
+
eth: "evm",
|
|
35
|
+
base: "evm",
|
|
36
|
+
arb: "evm",
|
|
37
|
+
op: "evm",
|
|
38
|
+
pol: "evm",
|
|
39
|
+
bsc: "evm",
|
|
40
|
+
avax: "evm",
|
|
41
|
+
gnosis: "evm",
|
|
42
|
+
scroll: "evm",
|
|
43
|
+
xlayer: "evm",
|
|
44
|
+
bera: "evm",
|
|
45
|
+
near: "near",
|
|
46
|
+
sol: "solana",
|
|
47
|
+
tron: "tron",
|
|
48
|
+
zec: "zec"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/token-select/balances.ts
|
|
52
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
53
|
+
var balances = {};
|
|
54
|
+
var reader = null;
|
|
55
|
+
var chainInflight = /* @__PURE__ */ new Map();
|
|
56
|
+
function emit() {
|
|
57
|
+
balances = { ...balances };
|
|
58
|
+
for (const listener of listeners) listener();
|
|
59
|
+
}
|
|
60
|
+
function subscribe(listener) {
|
|
61
|
+
listeners.add(listener);
|
|
62
|
+
return () => listeners.delete(listener);
|
|
63
|
+
}
|
|
64
|
+
function subscribePayTokenBalances(listener) {
|
|
65
|
+
return subscribe(listener);
|
|
66
|
+
}
|
|
67
|
+
function getPayTokenBalances() {
|
|
68
|
+
return balances;
|
|
69
|
+
}
|
|
70
|
+
function ownerKey(owner, chainKind) {
|
|
71
|
+
return chainKind === "solana" || chainKind === "zec" ? owner : owner.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
function balanceKey(owner, assetId, chainKind) {
|
|
74
|
+
return `${ownerKey(owner, chainKind)}:${assetId}`;
|
|
75
|
+
}
|
|
76
|
+
function isFresh(entry, now) {
|
|
77
|
+
return Boolean(
|
|
78
|
+
entry && entry.status === "success" && entry.updatedAt != null && now - entry.updatedAt < TOKEN_BALANCE_POLL_MS
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function loadingEntry(prev) {
|
|
82
|
+
if (!prev) return { raw: null, formatted: null, status: "loading", error: null, updatedAt: null };
|
|
83
|
+
return { ...prev, status: "loading", error: null };
|
|
84
|
+
}
|
|
85
|
+
function withStaleOnError(prev, next) {
|
|
86
|
+
if (next.status !== "error") return next;
|
|
87
|
+
if (prev?.raw == null && prev?.formatted == null) return next;
|
|
88
|
+
return { ...next, raw: prev.raw, formatted: prev.formatted, updatedAt: prev.updatedAt };
|
|
89
|
+
}
|
|
90
|
+
function setBalanceReader(next) {
|
|
91
|
+
reader = next;
|
|
92
|
+
}
|
|
93
|
+
function clearPayTokenBalances() {
|
|
94
|
+
balances = {};
|
|
95
|
+
emit();
|
|
96
|
+
}
|
|
97
|
+
function getPayTokenBalance(owner, assetId) {
|
|
98
|
+
if (!owner || !assetId) return void 0;
|
|
99
|
+
return balances[`${owner}:${assetId}`] ?? balances[`${owner.toLowerCase()}:${assetId}`];
|
|
100
|
+
}
|
|
101
|
+
function usePayTokenBalance(owner, assetId) {
|
|
102
|
+
const current = useSyncExternalStore(subscribe, getPayTokenBalances, getPayTokenBalances);
|
|
103
|
+
if (!owner || !assetId) return void 0;
|
|
104
|
+
return current[`${owner}:${assetId}`] ?? current[`${owner.toLowerCase()}:${assetId}`];
|
|
105
|
+
}
|
|
106
|
+
function writeResult(owner, token, result) {
|
|
107
|
+
const key = balanceKey(owner, token.assetId, token.chain.chainKind);
|
|
108
|
+
const prev = balances[key];
|
|
109
|
+
const next = "error" in result ? { raw: null, formatted: null, status: "error", error: result.error, updatedAt: Date.now() } : { raw: result.raw ?? null, formatted: result.formatted, status: "success", error: null, updatedAt: Date.now() };
|
|
110
|
+
balances[key] = withStaleOnError(prev, next);
|
|
111
|
+
}
|
|
112
|
+
async function refreshPayTokenBalance(owner, token) {
|
|
113
|
+
const key = balanceKey(owner, token.assetId, token.chain.chainKind);
|
|
114
|
+
balances[key] = loadingEntry(balances[key]);
|
|
115
|
+
emit();
|
|
116
|
+
if (!reader) {
|
|
117
|
+
balances[key] = withStaleOnError(balances[key], {
|
|
118
|
+
raw: null,
|
|
119
|
+
formatted: null,
|
|
120
|
+
status: "error",
|
|
121
|
+
error: "Balance reader is not configured",
|
|
122
|
+
updatedAt: Date.now()
|
|
123
|
+
});
|
|
124
|
+
emit();
|
|
125
|
+
return balances[key] ?? null;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const results = await reader({
|
|
129
|
+
owner,
|
|
130
|
+
chainKind: token.chain.chainKind,
|
|
131
|
+
blockchain: token.blockchain,
|
|
132
|
+
tokens: [token]
|
|
133
|
+
});
|
|
134
|
+
const result = results.find((item) => item.assetId === token.assetId) ?? {
|
|
135
|
+
assetId: token.assetId,
|
|
136
|
+
error: "Balance reader returned no result"
|
|
137
|
+
};
|
|
138
|
+
writeResult(owner, token, result);
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
writeResult(owner, token, {
|
|
141
|
+
assetId: token.assetId,
|
|
142
|
+
error: cause instanceof Error ? cause.message : "Failed to read balance"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
emit();
|
|
146
|
+
return balances[key] ?? null;
|
|
147
|
+
}
|
|
148
|
+
function groupByChain(tokens) {
|
|
149
|
+
const groups = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const token of tokens) {
|
|
151
|
+
const list = groups.get(token.blockchain) ?? [];
|
|
152
|
+
list.push(token);
|
|
153
|
+
groups.set(token.blockchain, list);
|
|
154
|
+
}
|
|
155
|
+
return groups;
|
|
156
|
+
}
|
|
157
|
+
async function fetchChain(owner, tokens, force) {
|
|
158
|
+
const blockchain = tokens[0]?.blockchain;
|
|
159
|
+
const kind = tokens[0]?.chain.chainKind;
|
|
160
|
+
if (!blockchain || !kind) return;
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const stale = tokens.filter((token) => force || !isFresh(balances[balanceKey(owner, token.assetId, kind)], now));
|
|
163
|
+
if (stale.length === 0) return;
|
|
164
|
+
const flightKey = `${ownerKey(owner, kind)}:${blockchain}`;
|
|
165
|
+
const pending = chainInflight.get(flightKey);
|
|
166
|
+
if (pending) return pending;
|
|
167
|
+
for (const token of stale) {
|
|
168
|
+
const key = balanceKey(owner, token.assetId, kind);
|
|
169
|
+
balances[key] = loadingEntry(balances[key]);
|
|
170
|
+
}
|
|
171
|
+
emit();
|
|
172
|
+
const run = (async () => {
|
|
173
|
+
if (!reader) {
|
|
174
|
+
for (const token of stale) {
|
|
175
|
+
writeResult(owner, token, { assetId: token.assetId, error: "Balance reader is not configured" });
|
|
176
|
+
}
|
|
177
|
+
emit();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const results = await reader({ owner, chainKind: kind, blockchain, tokens: stale });
|
|
182
|
+
const byId = new Map(results.map((item) => [item.assetId, item]));
|
|
183
|
+
for (const token of stale) {
|
|
184
|
+
writeResult(owner, token, byId.get(token.assetId) ?? {
|
|
185
|
+
assetId: token.assetId,
|
|
186
|
+
error: "Balance reader returned no result"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
const message = cause instanceof Error ? cause.message : "Failed to read balance";
|
|
191
|
+
for (const token of stale) writeResult(owner, token, { assetId: token.assetId, error: message });
|
|
192
|
+
}
|
|
193
|
+
emit();
|
|
194
|
+
})();
|
|
195
|
+
chainInflight.set(flightKey, run);
|
|
196
|
+
return run.finally(() => {
|
|
197
|
+
if (chainInflight.get(flightKey) === run) chainInflight.delete(flightKey);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function ensurePayTokenBalances(owners, tokens, opts) {
|
|
201
|
+
const force = Boolean(opts?.force);
|
|
202
|
+
const groups = groupByChain(tokens);
|
|
203
|
+
await Promise.all(Array.from(groups.values()).map((group) => {
|
|
204
|
+
const kind = group[0]?.chain.chainKind;
|
|
205
|
+
const owner = kind ? owners[kind] : void 0;
|
|
206
|
+
if (!owner) return Promise.resolve();
|
|
207
|
+
return fetchChain(owner, group, force);
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/token-select/pay-config.ts
|
|
212
|
+
import { useCallback, useEffect, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
213
|
+
|
|
214
|
+
// src/token-select/logos.ts
|
|
215
|
+
var LOGO_HOST = "https://assets.dapdap.net";
|
|
216
|
+
var CHAIN_LOGO_ALIAS = {
|
|
217
|
+
eth: "ethereum",
|
|
218
|
+
ethereum: "ethereum",
|
|
219
|
+
base: "base",
|
|
220
|
+
arb: "arbitrum",
|
|
221
|
+
arbitrum: "arbitrum",
|
|
222
|
+
op: "optimism",
|
|
223
|
+
optimism: "optimism",
|
|
224
|
+
pol: "polygon",
|
|
225
|
+
polygon: "polygon",
|
|
226
|
+
bsc: "bsc",
|
|
227
|
+
avax: "avalanche",
|
|
228
|
+
avalanche: "avalanche",
|
|
229
|
+
gnosis: "gnosis",
|
|
230
|
+
scroll: "scroll",
|
|
231
|
+
xlayer: "xlayer",
|
|
232
|
+
bera: "berachain",
|
|
233
|
+
berachain: "berachain",
|
|
234
|
+
near: "near",
|
|
235
|
+
sol: "solana",
|
|
236
|
+
solana: "solana",
|
|
237
|
+
tron: "tron",
|
|
238
|
+
zec: "zcash",
|
|
239
|
+
zcash: "zcash"
|
|
240
|
+
};
|
|
241
|
+
function logoPath(path) {
|
|
242
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
243
|
+
return `${LOGO_HOST}${normalized}`;
|
|
244
|
+
}
|
|
245
|
+
function chainLogoUrl(blockchainOrNetwork) {
|
|
246
|
+
const key = String(blockchainOrNetwork || "").toLowerCase();
|
|
247
|
+
const file = CHAIN_LOGO_ALIAS[key] || key.replace(/\s+/g, "");
|
|
248
|
+
return logoPath(`/stableflow/networks/${file}.png`);
|
|
249
|
+
}
|
|
250
|
+
function tokenLogoUrl(symbol) {
|
|
251
|
+
const upper = String(symbol || "").toUpperCase();
|
|
252
|
+
const file = upper === "USDT0" ? "usdt" : upper.toLowerCase();
|
|
253
|
+
return logoPath(`/stableflow/tokens/${file}.png`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/token-select/pay-config.ts
|
|
257
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
258
|
+
var WRAP_NEAR_CONTRACT = "wrap.near";
|
|
259
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
260
|
+
var chainKindOverrides = {};
|
|
261
|
+
var snapshot = {
|
|
262
|
+
apiHost: DEFAULT_API_HOST,
|
|
263
|
+
chains: [],
|
|
264
|
+
tokens: [],
|
|
265
|
+
source: null,
|
|
266
|
+
status: "idle",
|
|
267
|
+
error: null,
|
|
268
|
+
fetchedAt: null
|
|
269
|
+
};
|
|
270
|
+
var inflight = null;
|
|
271
|
+
function emit2(next) {
|
|
272
|
+
snapshot = next;
|
|
273
|
+
for (const listener of listeners2) listener();
|
|
274
|
+
}
|
|
275
|
+
function subscribe2(listener) {
|
|
276
|
+
listeners2.add(listener);
|
|
277
|
+
return () => listeners2.delete(listener);
|
|
278
|
+
}
|
|
279
|
+
function getPayConfigSnapshot() {
|
|
280
|
+
return snapshot;
|
|
281
|
+
}
|
|
282
|
+
function setChainKindOverrides(overrides) {
|
|
283
|
+
chainKindOverrides = overrides ?? {};
|
|
284
|
+
if (snapshot.source) {
|
|
285
|
+
const normalized = normalizePayConfig(snapshot.source, chainKindOverrides);
|
|
286
|
+
emit2({ ...snapshot, chains: normalized.chains, tokens: normalized.tokens });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function asRecord(value) {
|
|
290
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
function apiText(value) {
|
|
294
|
+
if (typeof value === "string") return value.trim();
|
|
295
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
function apiNumber(value) {
|
|
299
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
300
|
+
if (typeof value === "string" && value.trim()) {
|
|
301
|
+
const parsed = Number(value);
|
|
302
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
function asBoolean(value, fallback = true) {
|
|
307
|
+
if (typeof value === "boolean") return value;
|
|
308
|
+
if (typeof value === "string") {
|
|
309
|
+
const lower = value.trim().toLowerCase();
|
|
310
|
+
if (lower === "true") return true;
|
|
311
|
+
if (lower === "false") return false;
|
|
312
|
+
}
|
|
313
|
+
return fallback;
|
|
314
|
+
}
|
|
315
|
+
function mapSourceChain(raw) {
|
|
316
|
+
const row = asRecord(raw) ?? {};
|
|
317
|
+
return {
|
|
318
|
+
network: apiText(row.network),
|
|
319
|
+
chainId: apiText(row.chain_id ?? row.chainId),
|
|
320
|
+
chainName: apiText(row.chain_name ?? row.chainName),
|
|
321
|
+
logo: apiText(row.logo),
|
|
322
|
+
explorer: apiText(row.explorer)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function mapSourceToken(raw) {
|
|
326
|
+
const row = asRecord(raw) ?? {};
|
|
327
|
+
return {
|
|
328
|
+
symbol: apiText(row.symbol),
|
|
329
|
+
network: apiText(row.network),
|
|
330
|
+
decimals: apiNumber(row.decimals) ?? 0,
|
|
331
|
+
contractAddress: apiText(row.contract_address ?? row.contractAddress),
|
|
332
|
+
price: apiText(row.price),
|
|
333
|
+
supportPayment: asBoolean(row.support_payment ?? row.supportPayment),
|
|
334
|
+
supportReceive: asBoolean(row.support_receive ?? row.supportReceive)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function configBody(raw) {
|
|
338
|
+
const row = asRecord(raw) ?? {};
|
|
339
|
+
const data = asRecord(row.data);
|
|
340
|
+
if (typeof row.code === "number" && data) return data;
|
|
341
|
+
return row;
|
|
342
|
+
}
|
|
343
|
+
function mapPayConfigSource(raw) {
|
|
344
|
+
const row = configBody(raw);
|
|
345
|
+
const chains = Array.isArray(row.chains) ? row.chains.map(mapSourceChain) : [];
|
|
346
|
+
const tokens = Array.isArray(row.tokens) ? row.tokens.map(mapSourceToken) : [];
|
|
347
|
+
return { chains, tokens };
|
|
348
|
+
}
|
|
349
|
+
function chainKindFor(network, overrides = chainKindOverrides) {
|
|
350
|
+
const key = network.trim().toLowerCase();
|
|
351
|
+
if (!key) return null;
|
|
352
|
+
return overrides[key] ?? DEFAULT_CHAIN_KINDS[key] ?? null;
|
|
353
|
+
}
|
|
354
|
+
function normalizeSymbol(symbol) {
|
|
355
|
+
const upper = symbol.trim().toUpperCase();
|
|
356
|
+
if (!upper) return null;
|
|
357
|
+
if (upper === "USDT0") return "USDT";
|
|
358
|
+
if (upper === "WNEAR") return "NEAR";
|
|
359
|
+
return upper;
|
|
360
|
+
}
|
|
361
|
+
function tokenAssetId(network, symbol, contractAddress) {
|
|
362
|
+
const addr = String(contractAddress || "").trim();
|
|
363
|
+
return `${network}:${symbol}:${addr || "native"}`;
|
|
364
|
+
}
|
|
365
|
+
function isNativeAddress(contractAddress) {
|
|
366
|
+
const addr = String(contractAddress || "").trim();
|
|
367
|
+
if (!addr) return true;
|
|
368
|
+
const lower = addr.toLowerCase();
|
|
369
|
+
return lower === "native" || lower === ZERO_ADDRESS;
|
|
370
|
+
}
|
|
371
|
+
function normalizePayConfig(source, overrides = chainKindOverrides) {
|
|
372
|
+
const chains = [];
|
|
373
|
+
for (const chain of source.chains) {
|
|
374
|
+
const chainKind = chainKindFor(chain.network, overrides);
|
|
375
|
+
if (!chainKind || !chain.network) continue;
|
|
376
|
+
chains.push({
|
|
377
|
+
blockchain: chain.network,
|
|
378
|
+
chainName: chain.chainName || chain.network,
|
|
379
|
+
chainKind,
|
|
380
|
+
chainId: chain.chainId || void 0,
|
|
381
|
+
logo: chain.logo || chainLogoUrl(chain.network),
|
|
382
|
+
explorer: chain.explorer
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const chainByCode = new Map(chains.map((chain) => [chain.blockchain, chain]));
|
|
386
|
+
const tokens = [];
|
|
387
|
+
for (const token of source.tokens) {
|
|
388
|
+
const chain = chainByCode.get(token.network);
|
|
389
|
+
if (!chain) continue;
|
|
390
|
+
const symbol = normalizeSymbol(token.symbol);
|
|
391
|
+
if (!symbol) continue;
|
|
392
|
+
if (!Number.isInteger(token.decimals) || token.decimals < 0) continue;
|
|
393
|
+
const contractAddress = token.contractAddress.trim() || null;
|
|
394
|
+
tokens.push({
|
|
395
|
+
assetId: tokenAssetId(token.network, symbol, contractAddress),
|
|
396
|
+
symbol,
|
|
397
|
+
providerSymbol: token.symbol,
|
|
398
|
+
blockchain: token.network,
|
|
399
|
+
decimals: token.decimals,
|
|
400
|
+
contractAddress,
|
|
401
|
+
price: Number(token.price || 1),
|
|
402
|
+
logo: tokenLogoUrl(symbol),
|
|
403
|
+
supportPayment: token.supportPayment,
|
|
404
|
+
supportReceive: token.supportReceive,
|
|
405
|
+
chain
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const hasNearWrap = tokens.some((token) => token.symbol === "NEAR" && token.blockchain === "near" && token.contractAddress?.toLowerCase() === WRAP_NEAR_CONTRACT);
|
|
409
|
+
const visible = hasNearWrap ? tokens.filter((token) => !(token.symbol === "NEAR" && token.blockchain === "near" && isNativeAddress(token.contractAddress))) : tokens;
|
|
410
|
+
return { chains, tokens: visible };
|
|
411
|
+
}
|
|
412
|
+
function configUrl(apiHost) {
|
|
413
|
+
return `${apiHost.replace(/\/$/, "")}/v1/pay/config`;
|
|
414
|
+
}
|
|
415
|
+
async function load(apiHost, force) {
|
|
416
|
+
const fresh = snapshot.apiHost === apiHost && snapshot.status === "success" && snapshot.fetchedAt != null && Date.now() - snapshot.fetchedAt < CONFIG_STALE_MS;
|
|
417
|
+
if (!force && fresh) return;
|
|
418
|
+
if (inflight && !force) return inflight;
|
|
419
|
+
emit2({ ...snapshot, apiHost, status: snapshot.tokens.length > 0 ? snapshot.status : "loading", error: null });
|
|
420
|
+
const run = (async () => {
|
|
421
|
+
try {
|
|
422
|
+
const response = await fetch(configUrl(apiHost));
|
|
423
|
+
if (!response.ok) throw new Error(`Config request failed (${response.status})`);
|
|
424
|
+
const source = mapPayConfigSource(await response.json());
|
|
425
|
+
const normalized = normalizePayConfig(source, chainKindOverrides);
|
|
426
|
+
emit2({
|
|
427
|
+
apiHost,
|
|
428
|
+
source,
|
|
429
|
+
chains: normalized.chains,
|
|
430
|
+
tokens: normalized.tokens,
|
|
431
|
+
status: "success",
|
|
432
|
+
error: null,
|
|
433
|
+
fetchedAt: Date.now()
|
|
434
|
+
});
|
|
435
|
+
} catch (cause) {
|
|
436
|
+
const message = cause instanceof Error ? cause.message : "Failed to load config";
|
|
437
|
+
emit2({
|
|
438
|
+
...snapshot,
|
|
439
|
+
apiHost,
|
|
440
|
+
status: snapshot.source ? "success" : "error",
|
|
441
|
+
error: message
|
|
442
|
+
});
|
|
443
|
+
} finally {
|
|
444
|
+
inflight = null;
|
|
445
|
+
}
|
|
446
|
+
})();
|
|
447
|
+
inflight = run;
|
|
448
|
+
return run;
|
|
449
|
+
}
|
|
450
|
+
function refetchPayConfig(apiHost = snapshot.apiHost) {
|
|
451
|
+
return load(apiHost, true);
|
|
452
|
+
}
|
|
453
|
+
function usePayConfigSnapshot() {
|
|
454
|
+
return useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
|
|
455
|
+
}
|
|
456
|
+
function usePayConfig(options) {
|
|
457
|
+
const apiHost = options?.apiHost?.trim() || DEFAULT_API_HOST;
|
|
458
|
+
const current = useSyncExternalStore2(subscribe2, getPayConfigSnapshot, getPayConfigSnapshot);
|
|
459
|
+
const refetch = useCallback(() => {
|
|
460
|
+
void refetchPayConfig(apiHost);
|
|
461
|
+
}, [apiHost]);
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
void load(apiHost, false);
|
|
464
|
+
}, [apiHost]);
|
|
465
|
+
return {
|
|
466
|
+
chains: current.apiHost === apiHost ? current.chains : [],
|
|
467
|
+
tokens: current.apiHost === apiHost ? current.tokens : [],
|
|
468
|
+
source: current.apiHost === apiHost ? current.source : null,
|
|
469
|
+
status: current.apiHost === apiHost ? current.status : "loading",
|
|
470
|
+
error: current.apiHost === apiHost ? current.error : null,
|
|
471
|
+
refetch
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/token-select/context.tsx
|
|
476
|
+
import { jsx } from "react/jsx-runtime";
|
|
477
|
+
var PayWidgetsContext = createContext(null);
|
|
478
|
+
function PayWidgetsProvider(props) {
|
|
479
|
+
const {
|
|
480
|
+
apiHost,
|
|
481
|
+
popularTokens = [],
|
|
482
|
+
wallet,
|
|
483
|
+
readBalances,
|
|
484
|
+
onConfig,
|
|
485
|
+
onCopyAddress,
|
|
486
|
+
chainKinds,
|
|
487
|
+
children
|
|
488
|
+
} = props;
|
|
489
|
+
const config = usePayConfig({ apiHost });
|
|
490
|
+
useEffect2(() => {
|
|
491
|
+
setChainKindOverrides(chainKinds);
|
|
492
|
+
}, [chainKinds]);
|
|
493
|
+
useEffect2(() => {
|
|
494
|
+
setBalanceReader(readBalances);
|
|
495
|
+
return () => setBalanceReader(null);
|
|
496
|
+
}, [readBalances]);
|
|
497
|
+
useEffect2(() => {
|
|
498
|
+
if (config.status === "success" && config.source) onConfig?.(config.source);
|
|
499
|
+
}, [config.source, config.status, onConfig]);
|
|
500
|
+
const connected = Object.values(wallet.accounts).some((account) => Boolean(account?.address?.trim()));
|
|
501
|
+
useEffect2(() => {
|
|
502
|
+
if (!connected) clearPayTokenBalances();
|
|
503
|
+
}, [connected]);
|
|
504
|
+
const value = useMemo(
|
|
505
|
+
() => ({ wallet, popularTokens, onCopyAddress }),
|
|
506
|
+
[wallet, popularTokens, onCopyAddress]
|
|
507
|
+
);
|
|
508
|
+
return /* @__PURE__ */ jsx(PayWidgetsContext.Provider, { value, children });
|
|
509
|
+
}
|
|
510
|
+
function usePayWidgets() {
|
|
511
|
+
const value = useContext(PayWidgetsContext);
|
|
512
|
+
if (!value) {
|
|
513
|
+
throw new Error("Token select must be rendered inside PayWidgetsProvider");
|
|
514
|
+
}
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/token-select/TokenSelectDialog.tsx
|
|
519
|
+
import { useEffect as useEffect3, useMemo as useMemo2, useRef, useState, useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
520
|
+
import { Dialog } from "@stableflow/pay-ui/dialog";
|
|
521
|
+
import { SearchInput as SearchInput2 } from "@stableflow/pay-ui/search-input";
|
|
522
|
+
|
|
523
|
+
// src/token-select/prefs.ts
|
|
524
|
+
import { useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
525
|
+
var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
|
|
526
|
+
function rememberRecentAsset(recent, assetId) {
|
|
527
|
+
const id = assetId.trim();
|
|
528
|
+
if (!id) return recent;
|
|
529
|
+
return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
|
|
530
|
+
}
|
|
531
|
+
function rememberRecentBlockchain(recent, blockchain) {
|
|
532
|
+
const code = blockchain.trim();
|
|
533
|
+
if (!code) return recent;
|
|
534
|
+
return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
|
|
535
|
+
}
|
|
536
|
+
function migrateTokenSelectPrefs(persisted, version) {
|
|
537
|
+
const state = persisted ?? {};
|
|
538
|
+
const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
|
|
539
|
+
const recentAssetIds = version >= TOKEN_SELECT_PREFS_VERSION && Array.isArray(state.recentAssetIds) ? state.recentAssetIds.filter((id) => typeof id === "string" && id.trim()).slice(0, MAX_RECENT_ASSETS) : typeof state.lastAssetId === "string" && state.lastAssetId ? [state.lastAssetId] : [];
|
|
540
|
+
return { recentAssetIds, recentBlockchains };
|
|
541
|
+
}
|
|
542
|
+
function readStorage() {
|
|
543
|
+
if (typeof localStorage === "undefined") return EMPTY_PREFS;
|
|
544
|
+
try {
|
|
545
|
+
const raw = localStorage.getItem(TOKEN_SELECT_PREFS_STORAGE_KEY);
|
|
546
|
+
if (!raw) return EMPTY_PREFS;
|
|
547
|
+
const parsed = JSON.parse(raw);
|
|
548
|
+
const version = typeof parsed.version === "number" ? parsed.version : 0;
|
|
549
|
+
const payload = parsed && typeof parsed === "object" && "state" in parsed ? parsed.state : parsed;
|
|
550
|
+
return migrateTokenSelectPrefs(payload, version);
|
|
551
|
+
} catch {
|
|
552
|
+
return EMPTY_PREFS;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function writeStorage(state) {
|
|
556
|
+
if (typeof localStorage === "undefined") return;
|
|
557
|
+
localStorage.setItem(TOKEN_SELECT_PREFS_STORAGE_KEY, JSON.stringify({
|
|
558
|
+
state,
|
|
559
|
+
version: TOKEN_SELECT_PREFS_VERSION
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
var prefs = readStorage();
|
|
563
|
+
var listeners3 = /* @__PURE__ */ new Set();
|
|
564
|
+
function emit3(next) {
|
|
565
|
+
prefs = next;
|
|
566
|
+
writeStorage(next);
|
|
567
|
+
for (const listener of listeners3) listener();
|
|
568
|
+
}
|
|
569
|
+
function subscribe3(listener) {
|
|
570
|
+
listeners3.add(listener);
|
|
571
|
+
return () => listeners3.delete(listener);
|
|
572
|
+
}
|
|
573
|
+
function getTokenSelectPrefs() {
|
|
574
|
+
return prefs;
|
|
575
|
+
}
|
|
576
|
+
function setLastToken(assetId, blockchain) {
|
|
577
|
+
emit3({
|
|
578
|
+
recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
|
|
579
|
+
recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function setLastBlockchain(blockchain) {
|
|
583
|
+
emit3({
|
|
584
|
+
...prefs,
|
|
585
|
+
recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function useTokenSelectPrefs() {
|
|
589
|
+
return useSyncExternalStore3(subscribe3, getTokenSelectPrefs, getTokenSelectPrefs);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/token-select/format.ts
|
|
593
|
+
import { clsx } from "clsx";
|
|
594
|
+
import { twMerge } from "tailwind-merge";
|
|
595
|
+
var fontSans = "[font-family:Montserrat,PingFang_SC,system-ui,sans-serif]";
|
|
596
|
+
function cx(...inputs) {
|
|
597
|
+
return twMerge(clsx(inputs));
|
|
598
|
+
}
|
|
599
|
+
function formatAddress(address, prefix = 4, suffix = 5) {
|
|
600
|
+
if (!address) return "";
|
|
601
|
+
if (!address.startsWith("0x") && address.length <= 32) return address;
|
|
602
|
+
if (address.length <= prefix + suffix) return address;
|
|
603
|
+
return `${address.slice(0, prefix)}...${address.slice(-suffix)}`;
|
|
604
|
+
}
|
|
605
|
+
function chainKindLabel(kind) {
|
|
606
|
+
if (kind === "evm") return "EVM";
|
|
607
|
+
if (kind === "near") return "Near";
|
|
608
|
+
if (kind === "solana") return "Solana";
|
|
609
|
+
if (kind === "tron") return "Tron";
|
|
610
|
+
if (kind === "zec") return "Zcash";
|
|
611
|
+
return kind;
|
|
612
|
+
}
|
|
613
|
+
function formatDisplayAmount(value, options = {}) {
|
|
614
|
+
const maxDecimals = options.maxDecimals ?? 2;
|
|
615
|
+
const prefix = options.prefix ?? "";
|
|
616
|
+
const text = String(value).trim();
|
|
617
|
+
const negative = text.startsWith("-");
|
|
618
|
+
const unsigned = negative ? text.slice(1) : text;
|
|
619
|
+
if (!/^\d+(\.\d+)?$/.test(unsigned)) return `${prefix}0`;
|
|
620
|
+
const numeric = Number(unsigned);
|
|
621
|
+
const minUnit = 10 ** -maxDecimals;
|
|
622
|
+
if (options.showDust && numeric > 0 && numeric < minUnit) {
|
|
623
|
+
const dust = `<${minUnit.toFixed(maxDecimals)}`;
|
|
624
|
+
return prefix ? `${prefix} ${dust}` : dust;
|
|
625
|
+
}
|
|
626
|
+
const [intPart, fracPart = ""] = unsigned.split(".");
|
|
627
|
+
const fraction = fracPart.slice(0, maxDecimals).replace(/0+$/, "");
|
|
628
|
+
const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
629
|
+
const body = fraction.length > 0 ? `${grouped}.${fraction}` : grouped;
|
|
630
|
+
return `${prefix}${negative ? "-" : ""}${body}`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/token-select/chain-pane.tsx
|
|
634
|
+
import { Tooltip } from "@stableflow/pay-ui/tooltip";
|
|
635
|
+
|
|
636
|
+
// src/token-select/chain-wallet-status.tsx
|
|
637
|
+
import { IconLogout } from "@stableflow/pay-ui/icons/logout";
|
|
638
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
639
|
+
function stop(event) {
|
|
640
|
+
event.preventDefault();
|
|
641
|
+
event.stopPropagation();
|
|
642
|
+
}
|
|
643
|
+
function ChainWalletStatus({ kind }) {
|
|
644
|
+
const { wallet, onCopyAddress } = usePayWidgets();
|
|
645
|
+
const account = wallet.accounts[kind];
|
|
646
|
+
const address = account?.address?.trim() || "";
|
|
647
|
+
const hideDisconnect = account?.canDisconnect === false;
|
|
648
|
+
if (!address) {
|
|
649
|
+
return /* @__PURE__ */ jsx2(
|
|
650
|
+
"button",
|
|
651
|
+
{
|
|
652
|
+
type: "button",
|
|
653
|
+
onClick: (event) => {
|
|
654
|
+
stop(event);
|
|
655
|
+
void wallet.connect(kind);
|
|
656
|
+
},
|
|
657
|
+
className: cx("shrink-0 cursor-pointer border-0 bg-transparent p-0 text-xs font-medium text-[#3F8AFB] hover:underline", fontSans),
|
|
658
|
+
children: account?.connecting ? "Connecting\u2026" : "Connect"
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", onClick: stop, children: [
|
|
663
|
+
/* @__PURE__ */ jsx2(
|
|
664
|
+
"button",
|
|
665
|
+
{
|
|
666
|
+
type: "button",
|
|
667
|
+
onClick: async (event) => {
|
|
668
|
+
stop(event);
|
|
669
|
+
if (!onCopyAddress) return;
|
|
670
|
+
onCopyAddress(address);
|
|
671
|
+
},
|
|
672
|
+
className: cx("truncate border-0 bg-transparent p-0 text-xs text-[#606060] hover:text-black", fontSans),
|
|
673
|
+
children: formatAddress(address)
|
|
674
|
+
}
|
|
675
|
+
),
|
|
676
|
+
hideDisconnect ? null : /* @__PURE__ */ jsx2(
|
|
677
|
+
"button",
|
|
678
|
+
{
|
|
679
|
+
type: "button",
|
|
680
|
+
"aria-label": "Disconnect",
|
|
681
|
+
onClick: (event) => {
|
|
682
|
+
stop(event);
|
|
683
|
+
void wallet.disconnect(kind);
|
|
684
|
+
},
|
|
685
|
+
className: "inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-[#ff5656]",
|
|
686
|
+
children: /* @__PURE__ */ jsx2(IconLogout, { className: "size-3" })
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
] });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/token-select/utils.ts
|
|
693
|
+
function tokenBalanceUsd(token, formatted) {
|
|
694
|
+
if (formatted == null || formatted === "") return -1;
|
|
695
|
+
const amount = Number(formatted);
|
|
696
|
+
if (!Number.isFinite(amount)) return -1;
|
|
697
|
+
const price = Number(token.price);
|
|
698
|
+
return amount * (Number.isFinite(price) ? price : 0);
|
|
699
|
+
}
|
|
700
|
+
function tokenMatchesSearch(token, query) {
|
|
701
|
+
const q = query.trim().toLowerCase();
|
|
702
|
+
if (!q) return true;
|
|
703
|
+
if (token.symbol.toLowerCase().includes(q)) return true;
|
|
704
|
+
if (token.providerSymbol.toLowerCase().includes(q)) return true;
|
|
705
|
+
return Boolean(token.contractAddress?.toLowerCase().includes(q));
|
|
706
|
+
}
|
|
707
|
+
function matchesChainFilter(token, chainFilter) {
|
|
708
|
+
if (chainFilter === ALL_CHAIN_FILTER) return true;
|
|
709
|
+
return token.blockchain === chainFilter;
|
|
710
|
+
}
|
|
711
|
+
function formattedBalancePositive(formatted) {
|
|
712
|
+
if (formatted == null || formatted === "") return false;
|
|
713
|
+
const amount = Number(formatted);
|
|
714
|
+
return Number.isFinite(amount) && amount > 0;
|
|
715
|
+
}
|
|
716
|
+
function chainHasBalance(blockchain, tokens, getBalance) {
|
|
717
|
+
return tokens.some(
|
|
718
|
+
(token) => token.blockchain === blockchain && formattedBalancePositive(getBalance(token))
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
function chainBalanceUsd(blockchain, tokens, getBalanceUsd) {
|
|
722
|
+
let total = 0;
|
|
723
|
+
for (const token of tokens) {
|
|
724
|
+
if (token.blockchain !== blockchain) continue;
|
|
725
|
+
const usd = getBalanceUsd(token);
|
|
726
|
+
if (usd > 0) total += usd;
|
|
727
|
+
}
|
|
728
|
+
return total;
|
|
729
|
+
}
|
|
730
|
+
var DISCONNECTED_CHAIN_RANK = new Map(
|
|
731
|
+
DISCONNECTED_CHAIN_ORDER.map((code, index) => [code, index])
|
|
732
|
+
);
|
|
733
|
+
function parsePopularTokens(raw) {
|
|
734
|
+
if (!raw?.trim()) return [];
|
|
735
|
+
const refs = [];
|
|
736
|
+
for (const part of raw.split(",")) {
|
|
737
|
+
const trimmed = part.trim();
|
|
738
|
+
const colon = trimmed.indexOf(":");
|
|
739
|
+
if (colon <= 0 || colon >= trimmed.length - 1) continue;
|
|
740
|
+
const blockchain = trimmed.slice(0, colon).trim().toLowerCase();
|
|
741
|
+
const symbol = trimmed.slice(colon + 1).trim();
|
|
742
|
+
if (!blockchain || !symbol || symbol.includes(":")) continue;
|
|
743
|
+
refs.push({ blockchain, symbol: symbol.toUpperCase() });
|
|
744
|
+
}
|
|
745
|
+
return refs;
|
|
746
|
+
}
|
|
747
|
+
function popularTokensForWallets(tokens, configured, isChainConnected) {
|
|
748
|
+
const result = [];
|
|
749
|
+
for (const ref of configured) {
|
|
750
|
+
const token = tokens.find((item) => item.blockchain === ref.blockchain && item.symbol.toUpperCase() === ref.symbol);
|
|
751
|
+
if (!token || !isChainConnected(token.chain.chainKind)) continue;
|
|
752
|
+
result.push(token);
|
|
753
|
+
}
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
function sortChainsForSidebar(chains, tokens, options) {
|
|
757
|
+
if (!options.walletConnected) {
|
|
758
|
+
const tail = DISCONNECTED_CHAIN_ORDER.length;
|
|
759
|
+
return chains.map((chain, index) => ({ chain, index })).sort((left, right) => {
|
|
760
|
+
const leftRank = DISCONNECTED_CHAIN_RANK.get(left.chain.blockchain) ?? tail;
|
|
761
|
+
const rightRank = DISCONNECTED_CHAIN_RANK.get(right.chain.blockchain) ?? tail;
|
|
762
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
763
|
+
return left.index - right.index;
|
|
764
|
+
}).map((item) => item.chain);
|
|
765
|
+
}
|
|
766
|
+
if (!options.showBalances) return chains.slice();
|
|
767
|
+
return chains.slice().sort((left, right) => {
|
|
768
|
+
const leftUsd = chainBalanceUsd(left.blockchain, tokens, options.getBalanceUsd);
|
|
769
|
+
const rightUsd = chainBalanceUsd(right.blockchain, tokens, options.getBalanceUsd);
|
|
770
|
+
if (rightUsd !== leftUsd) return rightUsd - leftUsd;
|
|
771
|
+
return left.chainName.localeCompare(right.chainName);
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function initialChainFilter(lockChainKind, recentBlockchains, chains) {
|
|
775
|
+
if (!lockChainKind) return ALL_CHAIN_FILTER;
|
|
776
|
+
const matching = chains.filter((chain) => chain.chainKind === lockChainKind);
|
|
777
|
+
if (matching.length === 0) return ALL_CHAIN_FILTER;
|
|
778
|
+
for (const code of recentBlockchains) {
|
|
779
|
+
if (matching.some((chain) => chain.blockchain === code)) return code;
|
|
780
|
+
}
|
|
781
|
+
return matching[0].blockchain;
|
|
782
|
+
}
|
|
783
|
+
function isBlockchainDisabled(blockchain, disabledBlockchains) {
|
|
784
|
+
if (!disabledBlockchains?.length) return false;
|
|
785
|
+
const code = blockchain.toLowerCase();
|
|
786
|
+
return disabledBlockchains.some((item) => item.toLowerCase() === code);
|
|
787
|
+
}
|
|
788
|
+
function isChainKindLocked(chainKind, lockChainKind) {
|
|
789
|
+
return Boolean(lockChainKind && chainKind !== lockChainKind);
|
|
790
|
+
}
|
|
791
|
+
var BALANCE_RANK_FUNDED = 0;
|
|
792
|
+
var BALANCE_RANK_LOADING = 1;
|
|
793
|
+
var BALANCE_RANK_EMPTY = 2;
|
|
794
|
+
function tokenBalanceRank(usd, loading) {
|
|
795
|
+
if (loading) return BALANCE_RANK_LOADING;
|
|
796
|
+
if (usd > 0) return BALANCE_RANK_FUNDED;
|
|
797
|
+
return BALANCE_RANK_EMPTY;
|
|
798
|
+
}
|
|
799
|
+
function compareTokensBySymbol(left, right) {
|
|
800
|
+
return left.symbol.localeCompare(right.symbol) || left.chain.chainName.localeCompare(right.chain.chainName);
|
|
801
|
+
}
|
|
802
|
+
function sortTokensByBalance(tokens, getBalanceUsd, isLoading) {
|
|
803
|
+
return tokens.slice().sort((left, right) => {
|
|
804
|
+
const leftRank = tokenBalanceRank(getBalanceUsd(left), isLoading(left));
|
|
805
|
+
const rightRank = tokenBalanceRank(getBalanceUsd(right), isLoading(right));
|
|
806
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
807
|
+
if (leftRank === BALANCE_RANK_FUNDED) {
|
|
808
|
+
const byUsd = getBalanceUsd(right) - getBalanceUsd(left);
|
|
809
|
+
if (byUsd !== 0) return byUsd;
|
|
810
|
+
}
|
|
811
|
+
return compareTokensBySymbol(left, right);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
function sortTokensBySymbol(tokens) {
|
|
815
|
+
return tokens.slice().sort(compareTokensBySymbol);
|
|
816
|
+
}
|
|
817
|
+
function positiveBalanceTokens(tokens, getBalanceUsd) {
|
|
818
|
+
return sortTokensByBalance(
|
|
819
|
+
tokens.filter((token) => getBalanceUsd(token) > 0),
|
|
820
|
+
getBalanceUsd,
|
|
821
|
+
() => false
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
function recentTokensInOrder(tokens, recentAssetIds) {
|
|
825
|
+
const byId = new Map(tokens.map((token) => [token.assetId, token]));
|
|
826
|
+
const result = [];
|
|
827
|
+
for (const assetId of recentAssetIds) {
|
|
828
|
+
const token = byId.get(assetId);
|
|
829
|
+
if (token) result.push(token);
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/token-select/chain-pane.tsx
|
|
835
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
836
|
+
function lockReason(lockChainKind) {
|
|
837
|
+
return `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain`;
|
|
838
|
+
}
|
|
839
|
+
function ChainPane(props) {
|
|
840
|
+
const {
|
|
841
|
+
chains,
|
|
842
|
+
fundedBlockchains,
|
|
843
|
+
showWalletStatus = true,
|
|
844
|
+
onSelectFilter,
|
|
845
|
+
lockChainKind = null,
|
|
846
|
+
disabledBlockchains = null,
|
|
847
|
+
disabledReason
|
|
848
|
+
} = props;
|
|
849
|
+
function wrapDisabled(disabled, reason, content) {
|
|
850
|
+
if (!disabled || !reason) return content;
|
|
851
|
+
return /* @__PURE__ */ jsx3(Tooltip, { side: "right", triggerClassName: "min-w-0 flex-1", content: reason, children: content });
|
|
852
|
+
}
|
|
853
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col", children: [
|
|
854
|
+
/* @__PURE__ */ jsx3("p", { className: cx("shrink-0 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
|
|
855
|
+
/* @__PURE__ */ jsx3("div", { className: "mt-3 flex min-h-0 flex-1 flex-col overflow-y-auto", children: chains.map((chain) => {
|
|
856
|
+
const locked = isChainKindLocked(chain.chainKind, lockChainKind);
|
|
857
|
+
const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
|
|
858
|
+
const disabled = locked || chainDisabled;
|
|
859
|
+
const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind) : void 0;
|
|
860
|
+
return /* @__PURE__ */ jsxs2(
|
|
861
|
+
"div",
|
|
862
|
+
{
|
|
863
|
+
className: cx(
|
|
864
|
+
"flex min-h-14 w-full items-center gap-2 rounded-[12px] px-2 md:min-h-[66px] md:gap-3 md:px-3",
|
|
865
|
+
disabled ? "opacity-40" : "hover:bg-[#F6F6F6]"
|
|
866
|
+
),
|
|
867
|
+
children: [
|
|
868
|
+
wrapDisabled(
|
|
869
|
+
disabled,
|
|
870
|
+
reason,
|
|
871
|
+
/* @__PURE__ */ jsxs2(
|
|
872
|
+
"button",
|
|
873
|
+
{
|
|
874
|
+
type: "button",
|
|
875
|
+
disabled,
|
|
876
|
+
onClick: () => {
|
|
877
|
+
if (disabled) return;
|
|
878
|
+
onSelectFilter(chain.blockchain);
|
|
879
|
+
},
|
|
880
|
+
className: cx(
|
|
881
|
+
"flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
|
|
882
|
+
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
|
883
|
+
),
|
|
884
|
+
children: [
|
|
885
|
+
/* @__PURE__ */ jsxs2("span", { className: "relative size-8 shrink-0", children: [
|
|
886
|
+
/* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
|
|
887
|
+
fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx3("span", { className: "absolute -top-0.5 -right-0.5 size-2 rounded-full border border-white bg-[#06f] md:size-2.5" }) : null
|
|
888
|
+
] }),
|
|
889
|
+
/* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black md:text-base", fontSans), children: chain.chainName })
|
|
890
|
+
]
|
|
891
|
+
}
|
|
892
|
+
)
|
|
893
|
+
),
|
|
894
|
+
showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : null
|
|
895
|
+
]
|
|
896
|
+
},
|
|
897
|
+
chain.blockchain
|
|
898
|
+
);
|
|
899
|
+
}) })
|
|
900
|
+
] });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/token-select/network-rail.tsx
|
|
904
|
+
import { IconAllNetworks } from "@stableflow/pay-ui/icons/all-networks";
|
|
905
|
+
import { IconWallet } from "@stableflow/pay-ui/icons/wallet";
|
|
906
|
+
import { Tooltip as Tooltip2 } from "@stableflow/pay-ui/tooltip";
|
|
907
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
908
|
+
function chipClass(selected, disabled) {
|
|
909
|
+
return cx(
|
|
910
|
+
"relative flex size-10 shrink-0 items-center justify-center rounded-[12px] border border-solid md:size-[50px]",
|
|
911
|
+
selected ? "border-[#06f] bg-[rgba(0,102,255,0.1)]" : "border-transparent bg-transparent",
|
|
912
|
+
disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer hover:bg-black/5"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
function NetworkRail(props) {
|
|
916
|
+
const {
|
|
917
|
+
view,
|
|
918
|
+
chainFilter,
|
|
919
|
+
chips,
|
|
920
|
+
fundedBlockchains,
|
|
921
|
+
walletConnected,
|
|
922
|
+
showAllLabel = false,
|
|
923
|
+
lockChainKind = null,
|
|
924
|
+
disabledBlockchains = null,
|
|
925
|
+
disabledReason,
|
|
926
|
+
onSelectFilter,
|
|
927
|
+
onOpenNetworks,
|
|
928
|
+
className
|
|
929
|
+
} = props;
|
|
930
|
+
function wrapDisabled(disabled, reason, content) {
|
|
931
|
+
if (!disabled || !reason) return content;
|
|
932
|
+
return /* @__PURE__ */ jsx4(Tooltip2, { side: "right", content: reason, children: content });
|
|
933
|
+
}
|
|
934
|
+
return /* @__PURE__ */ jsxs3("div", { className: cx("flex h-full w-[52px] shrink-0 flex-col items-center border-r border-[#e3e3e3] pr-2 md:w-[72px] md:pr-3", className), children: [
|
|
935
|
+
showAllLabel ? /* @__PURE__ */ jsx4(
|
|
936
|
+
"button",
|
|
937
|
+
{
|
|
938
|
+
type: "button",
|
|
939
|
+
"aria-label": "All",
|
|
940
|
+
onClick: () => onSelectFilter(ALL_CHAIN_FILTER),
|
|
941
|
+
className: chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false),
|
|
942
|
+
children: /* @__PURE__ */ jsx4("span", { className: cx("text-xs font-medium text-black md:text-sm", fontSans), children: "All" })
|
|
943
|
+
}
|
|
944
|
+
) : /* @__PURE__ */ jsx4(
|
|
945
|
+
"button",
|
|
946
|
+
{
|
|
947
|
+
type: "button",
|
|
948
|
+
"aria-label": "Wallet",
|
|
949
|
+
disabled: !walletConnected,
|
|
950
|
+
onClick: () => {
|
|
951
|
+
if (!walletConnected) return;
|
|
952
|
+
onSelectFilter(ALL_CHAIN_FILTER);
|
|
953
|
+
},
|
|
954
|
+
className: walletConnected ? chipClass(view === "token" && chainFilter === ALL_CHAIN_FILTER, false) : "relative flex size-10 shrink-0 cursor-not-allowed items-center justify-center rounded-[12px] border border-solid border-transparent md:size-[50px]",
|
|
955
|
+
children: walletConnected ? /* @__PURE__ */ jsx4(IconWallet, { className: "size-4 text-black" }) : /* @__PURE__ */ jsx4("span", { className: "flex size-8 items-center justify-center rounded-[8px] bg-[#EFEFEF]", children: /* @__PURE__ */ jsx4(IconWallet, { className: "size-4 text-[#9FA7BA]" }) })
|
|
956
|
+
}
|
|
957
|
+
),
|
|
958
|
+
/* @__PURE__ */ jsx4("div", { className: "mt-2 flex min-h-0 w-full flex-1 flex-col items-center gap-2 overflow-y-auto", children: chips.map((chain) => {
|
|
959
|
+
const locked = isChainKindLocked(chain.chainKind, lockChainKind);
|
|
960
|
+
const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
|
|
961
|
+
const disabled = locked || chainDisabled;
|
|
962
|
+
const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind)}; edit the recipient to change chain` : void 0;
|
|
963
|
+
const selected = view === "token" && chainFilter === chain.blockchain;
|
|
964
|
+
return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
|
|
965
|
+
disabled,
|
|
966
|
+
reason,
|
|
967
|
+
/* @__PURE__ */ jsxs3(
|
|
968
|
+
"button",
|
|
969
|
+
{
|
|
970
|
+
type: "button",
|
|
971
|
+
"aria-label": chain.chainName,
|
|
972
|
+
disabled,
|
|
973
|
+
onClick: () => {
|
|
974
|
+
if (disabled) return;
|
|
975
|
+
onSelectFilter(chain.blockchain);
|
|
976
|
+
},
|
|
977
|
+
className: chipClass(selected, disabled),
|
|
978
|
+
children: [
|
|
979
|
+
/* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-6 object-cover md:size-8" }),
|
|
980
|
+
fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx4("span", { className: "absolute top-1 right-1 size-2 rounded-full border border-white bg-[#06f] md:size-2.5" }) : null
|
|
981
|
+
]
|
|
982
|
+
}
|
|
983
|
+
)
|
|
984
|
+
) }, chain.blockchain);
|
|
985
|
+
}) }),
|
|
986
|
+
/* @__PURE__ */ jsx4("div", { className: "mt-2 flex w-full shrink-0 justify-center border-t border-[#e3e3e3] pt-2", children: /* @__PURE__ */ jsx4(
|
|
987
|
+
"button",
|
|
988
|
+
{
|
|
989
|
+
type: "button",
|
|
990
|
+
"aria-label": "All networks",
|
|
991
|
+
onClick: onOpenNetworks,
|
|
992
|
+
className: chipClass(view === "network", false),
|
|
993
|
+
children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-4 text-black" })
|
|
994
|
+
}
|
|
995
|
+
) })
|
|
996
|
+
] });
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/token-select/token-pane.tsx
|
|
1000
|
+
import { SearchInput } from "@stableflow/pay-ui/search-input";
|
|
1001
|
+
import { Skeleton } from "@stableflow/pay-ui/skeleton";
|
|
1002
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1003
|
+
function TokenPane(props) {
|
|
1004
|
+
const {
|
|
1005
|
+
sections,
|
|
1006
|
+
selectedAssetId,
|
|
1007
|
+
loading,
|
|
1008
|
+
showBalances = false,
|
|
1009
|
+
showWalletStatus = true,
|
|
1010
|
+
getBalance,
|
|
1011
|
+
isBalanceLoading,
|
|
1012
|
+
onSelectToken,
|
|
1013
|
+
isTokenDisabled,
|
|
1014
|
+
search = "",
|
|
1015
|
+
onSearchChange,
|
|
1016
|
+
showSearch = false,
|
|
1017
|
+
searchClassName,
|
|
1018
|
+
rowClassName
|
|
1019
|
+
} = props;
|
|
1020
|
+
const visibleSections = sections.filter((section) => section.title || section.tokens.length > 0);
|
|
1021
|
+
const hasTokens = sections.some((section) => section.tokens.length > 0);
|
|
1022
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col", children: [
|
|
1023
|
+
showSearch && onSearchChange ? /* @__PURE__ */ jsx5(
|
|
1024
|
+
SearchInput,
|
|
1025
|
+
{
|
|
1026
|
+
value: search,
|
|
1027
|
+
onChange: onSearchChange,
|
|
1028
|
+
placeholder: "search or paste address",
|
|
1029
|
+
className: "mb-3 shrink-0",
|
|
1030
|
+
inputClassName: cx(
|
|
1031
|
+
"h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30",
|
|
1032
|
+
searchClassName
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
) : null,
|
|
1036
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: [
|
|
1037
|
+
visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
|
|
1038
|
+
section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-2 md:px-3.5", children: [
|
|
1039
|
+
/* @__PURE__ */ jsx5("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
|
|
1040
|
+
showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx5(ChainWalletStatus, { kind: section.walletKind }) : null
|
|
1041
|
+
] }) : null,
|
|
1042
|
+
section.tokens.map((token) => /* @__PURE__ */ jsx5(
|
|
1043
|
+
TokenRow,
|
|
1044
|
+
{
|
|
1045
|
+
token,
|
|
1046
|
+
selected: token.assetId === selectedAssetId,
|
|
1047
|
+
recent: token.assetId === section.badgeAssetId,
|
|
1048
|
+
showBalances,
|
|
1049
|
+
formatted: showBalances ? getBalance(token) : null,
|
|
1050
|
+
loadingBalance: showBalances && isBalanceLoading(token),
|
|
1051
|
+
disabled: Boolean(isTokenDisabled?.(token)),
|
|
1052
|
+
onSelect: () => onSelectToken(token),
|
|
1053
|
+
className: rowClassName
|
|
1054
|
+
},
|
|
1055
|
+
`${section.id}-${token.assetId}`
|
|
1056
|
+
))
|
|
1057
|
+
] }, section.id)),
|
|
1058
|
+
loading && !hasTokens ? /* @__PURE__ */ jsx5(TokenListSkeleton, {}) : null,
|
|
1059
|
+
!loading && !hasTokens ? /* @__PURE__ */ jsx5("p", { className: cx("px-1 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
|
|
1060
|
+
] })
|
|
1061
|
+
] });
|
|
1062
|
+
}
|
|
1063
|
+
function TokenListSkeleton() {
|
|
1064
|
+
return /* @__PURE__ */ jsx5("div", { "aria-hidden": true, children: Array.from({ length: 6 }, (_, index) => /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2.5 px-2 py-3 md:px-3.5", children: [
|
|
1065
|
+
/* @__PURE__ */ jsx5(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
|
|
1066
|
+
/* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
|
|
1067
|
+
/* @__PURE__ */ jsx5(Skeleton, { className: "h-3.5 w-24" }),
|
|
1068
|
+
/* @__PURE__ */ jsx5(Skeleton, { className: "h-3 w-16" })
|
|
1069
|
+
] })
|
|
1070
|
+
] }, index)) });
|
|
1071
|
+
}
|
|
1072
|
+
function TokenRow(props) {
|
|
1073
|
+
const { token, selected, recent, showBalances, formatted, loadingBalance, disabled, onSelect, className } = props;
|
|
1074
|
+
const usd = showBalances && !loadingBalance && formatted != null ? tokenBalanceUsd(token, formatted) : -1;
|
|
1075
|
+
return /* @__PURE__ */ jsxs4(
|
|
1076
|
+
"button",
|
|
1077
|
+
{
|
|
1078
|
+
type: "button",
|
|
1079
|
+
disabled,
|
|
1080
|
+
onClick: onSelect,
|
|
1081
|
+
className: cx(
|
|
1082
|
+
"flex w-full items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-2 py-3 text-left hover:bg-[#F6F6F6] md:px-3.5",
|
|
1083
|
+
selected && "bg-[#F6F6F6]",
|
|
1084
|
+
disabled && "cursor-not-allowed opacity-40 hover:bg-transparent",
|
|
1085
|
+
className
|
|
1086
|
+
),
|
|
1087
|
+
children: [
|
|
1088
|
+
/* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-2.5", children: [
|
|
1089
|
+
/* @__PURE__ */ jsxs4("span", { className: "relative size-8 shrink-0", children: [
|
|
1090
|
+
/* @__PURE__ */ jsx5("img", { src: token.logo, alt: "", className: "size-8 rounded-full object-cover" }),
|
|
1091
|
+
/* @__PURE__ */ jsx5(
|
|
1092
|
+
"img",
|
|
1093
|
+
{
|
|
1094
|
+
src: token.chain.logo,
|
|
1095
|
+
alt: "",
|
|
1096
|
+
className: "absolute -right-0.5 -bottom-0.5 size-3.5 rounded-[4px] border border-white object-cover"
|
|
1097
|
+
}
|
|
1098
|
+
)
|
|
1099
|
+
] }),
|
|
1100
|
+
/* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
|
|
1101
|
+
/* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1.5", children: [
|
|
1102
|
+
/* @__PURE__ */ jsx5("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
|
|
1103
|
+
recent ? /* @__PURE__ */ jsx5("span", { className: cx("shrink-0 rounded-[9px] bg-[#06f]/10 px-1.5 text-[10px] font-medium leading-[18px] text-[#06f]", fontSans), children: "Recently Used" }) : null
|
|
1104
|
+
] }),
|
|
1105
|
+
/* @__PURE__ */ jsx5("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
|
|
1106
|
+
] })
|
|
1107
|
+
] }),
|
|
1108
|
+
showBalances ? /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx5(
|
|
1109
|
+
"span",
|
|
1110
|
+
{
|
|
1111
|
+
className: "inline-block size-3.5 animate-spin rounded-full border-2 border-[#606060] border-r-transparent",
|
|
1112
|
+
"aria-label": "Loading balance"
|
|
1113
|
+
}
|
|
1114
|
+
) : formatted != null ? /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
1115
|
+
/* @__PURE__ */ jsx5("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
|
|
1116
|
+
usd >= 0 ? /* @__PURE__ */ jsx5("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
|
|
1117
|
+
] }) : /* @__PURE__ */ jsx5("span", { className: cx("text-sm text-[#606060]", fontSans), children: "\u2014" }) }) : null
|
|
1118
|
+
]
|
|
1119
|
+
}
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/token-select/TokenSelectDialog.tsx
|
|
1124
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1125
|
+
var SEARCH_INPUT_CLASS = "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30";
|
|
1126
|
+
function chainsFromTokens(tokens) {
|
|
1127
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
1128
|
+
for (const token of tokens) {
|
|
1129
|
+
if (!byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
|
|
1130
|
+
}
|
|
1131
|
+
return Array.from(byCode.values());
|
|
1132
|
+
}
|
|
1133
|
+
function ownerForToken(owners, token) {
|
|
1134
|
+
return owners[token.chain.chainKind] ?? null;
|
|
1135
|
+
}
|
|
1136
|
+
function hasAnyOwner(owners) {
|
|
1137
|
+
return Object.values(owners).some((address) => Boolean(address));
|
|
1138
|
+
}
|
|
1139
|
+
function isNativeToken(token) {
|
|
1140
|
+
const addr = String(token.contractAddress || "").trim().toLowerCase();
|
|
1141
|
+
return !addr || addr === "native" || addr === "0x0000000000000000000000000000000000000000";
|
|
1142
|
+
}
|
|
1143
|
+
function TokenSelectDialog(props) {
|
|
1144
|
+
const {
|
|
1145
|
+
open,
|
|
1146
|
+
onClose,
|
|
1147
|
+
onSelect,
|
|
1148
|
+
role = "payer",
|
|
1149
|
+
title = "Select Token",
|
|
1150
|
+
selectedAssetId,
|
|
1151
|
+
balanceOwners,
|
|
1152
|
+
allowedBlockchains = null,
|
|
1153
|
+
lockChainKind = null,
|
|
1154
|
+
excludeNative = false,
|
|
1155
|
+
disabledBlockchains = null,
|
|
1156
|
+
disabledReason,
|
|
1157
|
+
className,
|
|
1158
|
+
titleClassName,
|
|
1159
|
+
closeClassName,
|
|
1160
|
+
contentClassName,
|
|
1161
|
+
searchClassName,
|
|
1162
|
+
railClassName,
|
|
1163
|
+
rowClassName
|
|
1164
|
+
} = props;
|
|
1165
|
+
const payer = role === "payer";
|
|
1166
|
+
const showBalances = payer;
|
|
1167
|
+
const { wallet, popularTokens: popularRefs } = usePayWidgets();
|
|
1168
|
+
const { tokens, status } = usePayConfigSnapshot();
|
|
1169
|
+
const prefs2 = useTokenSelectPrefs();
|
|
1170
|
+
const balanceEntries = useSyncExternalStore4(subscribePayTokenBalances, getPayTokenBalances, getPayTokenBalances);
|
|
1171
|
+
const connectedOwners = useMemo2(() => {
|
|
1172
|
+
const owners2 = {};
|
|
1173
|
+
for (const [kind, account] of Object.entries(wallet.accounts)) {
|
|
1174
|
+
const address = account?.address?.trim();
|
|
1175
|
+
if (address) owners2[kind] = address;
|
|
1176
|
+
}
|
|
1177
|
+
return owners2;
|
|
1178
|
+
}, [wallet.accounts]);
|
|
1179
|
+
const owners = showBalances ? { ...connectedOwners, ...balanceOwners } : {};
|
|
1180
|
+
const walletConnected = hasAnyOwner(connectedOwners);
|
|
1181
|
+
const walletWasConnected = useRef(null);
|
|
1182
|
+
const [search, setSearch] = useState("");
|
|
1183
|
+
const [chainFilter, setChainFilter] = useState(ALL_CHAIN_FILTER);
|
|
1184
|
+
const [view, setView] = useState("token");
|
|
1185
|
+
const loading = status === "loading" || status === "idle";
|
|
1186
|
+
useEffect3(() => {
|
|
1187
|
+
if (!open) return;
|
|
1188
|
+
setSearch("");
|
|
1189
|
+
if (lockChainKind) {
|
|
1190
|
+
setView("token");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (!payer) {
|
|
1194
|
+
setView("token");
|
|
1195
|
+
setChainFilter(ALL_CHAIN_FILTER);
|
|
1196
|
+
}
|
|
1197
|
+
}, [open, lockChainKind, payer]);
|
|
1198
|
+
useEffect3(() => {
|
|
1199
|
+
if (!open || !lockChainKind || tokens.length === 0) return;
|
|
1200
|
+
setChainFilter(initialChainFilter(
|
|
1201
|
+
lockChainKind,
|
|
1202
|
+
getTokenSelectPrefs().recentBlockchains,
|
|
1203
|
+
chainsFromTokens(tokens)
|
|
1204
|
+
));
|
|
1205
|
+
}, [open, lockChainKind, tokens]);
|
|
1206
|
+
useEffect3(() => {
|
|
1207
|
+
if (!open) {
|
|
1208
|
+
walletWasConnected.current = null;
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (lockChainKind || !payer) {
|
|
1212
|
+
walletWasConnected.current = walletConnected;
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const previous = walletWasConnected.current;
|
|
1216
|
+
walletWasConnected.current = walletConnected;
|
|
1217
|
+
if (previous !== null && previous === walletConnected) return;
|
|
1218
|
+
setView(walletConnected ? "token" : "network");
|
|
1219
|
+
setChainFilter(ALL_CHAIN_FILTER);
|
|
1220
|
+
}, [open, lockChainKind, payer, walletConnected]);
|
|
1221
|
+
const allowed = useMemo2(() => {
|
|
1222
|
+
if (!allowedBlockchains || allowedBlockchains.length === 0) return null;
|
|
1223
|
+
return new Set(allowedBlockchains.map((code) => code.toLowerCase()));
|
|
1224
|
+
}, [allowedBlockchains]);
|
|
1225
|
+
const scopedTokens = useMemo2(() => {
|
|
1226
|
+
return tokens.filter((token) => {
|
|
1227
|
+
if (allowed && !allowed.has(token.blockchain.toLowerCase())) return false;
|
|
1228
|
+
if (excludeNative && isNativeToken(token)) return false;
|
|
1229
|
+
if (payer && !token.supportPayment) return false;
|
|
1230
|
+
if (!payer && !token.supportReceive) return false;
|
|
1231
|
+
return true;
|
|
1232
|
+
});
|
|
1233
|
+
}, [tokens, allowed, excludeNative, payer]);
|
|
1234
|
+
const ownerKey2 = ["evm", "near", "solana", "tron", "zec"].map((kind) => `${kind}:${owners[kind] ?? ""}`).join("|");
|
|
1235
|
+
const tokenKey = useMemo2(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
|
|
1236
|
+
const ownersRef = useRef(owners);
|
|
1237
|
+
const tokensRef = useRef(scopedTokens);
|
|
1238
|
+
ownersRef.current = owners;
|
|
1239
|
+
tokensRef.current = scopedTokens;
|
|
1240
|
+
useEffect3(() => {
|
|
1241
|
+
if (!showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
|
|
1242
|
+
void ensurePayTokenBalances(ownersRef.current, tokensRef.current);
|
|
1243
|
+
const id = window.setInterval(() => {
|
|
1244
|
+
void ensurePayTokenBalances(ownersRef.current, tokensRef.current, { force: true });
|
|
1245
|
+
}, TOKEN_BALANCE_POLL_MS);
|
|
1246
|
+
return () => window.clearInterval(id);
|
|
1247
|
+
}, [showBalances, open, ownerKey2, tokenKey]);
|
|
1248
|
+
function balanceOf(token) {
|
|
1249
|
+
return getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted;
|
|
1250
|
+
}
|
|
1251
|
+
function usdOf(token) {
|
|
1252
|
+
if (!showBalances) return -1;
|
|
1253
|
+
return tokenBalanceUsd(token, balanceOf(token));
|
|
1254
|
+
}
|
|
1255
|
+
function loadingOf(token) {
|
|
1256
|
+
if (!showBalances) return false;
|
|
1257
|
+
const owner = ownerForToken(owners, token);
|
|
1258
|
+
if (!owner) return false;
|
|
1259
|
+
const entry = getPayTokenBalance(owner, token.assetId);
|
|
1260
|
+
return entry?.formatted == null && (!entry || entry.status === "loading");
|
|
1261
|
+
}
|
|
1262
|
+
const availableChains = useMemo2(() => {
|
|
1263
|
+
const codes = new Set(scopedTokens.map((token) => token.blockchain));
|
|
1264
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
1265
|
+
for (const token of scopedTokens) {
|
|
1266
|
+
if (codes.has(token.blockchain) && !byCode.has(token.blockchain)) byCode.set(token.blockchain, token.chain);
|
|
1267
|
+
}
|
|
1268
|
+
return Array.from(byCode.values());
|
|
1269
|
+
}, [scopedTokens]);
|
|
1270
|
+
const sortedChains = useMemo2(
|
|
1271
|
+
() => sortChainsForSidebar(availableChains, scopedTokens, {
|
|
1272
|
+
showBalances,
|
|
1273
|
+
walletConnected,
|
|
1274
|
+
getBalanceUsd: (token) => tokenBalanceUsd(token, getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)
|
|
1275
|
+
}),
|
|
1276
|
+
[availableChains, scopedTokens, showBalances, walletConnected, owners, balanceEntries]
|
|
1277
|
+
);
|
|
1278
|
+
const fundedBlockchains = useMemo2(() => {
|
|
1279
|
+
const funded = /* @__PURE__ */ new Set();
|
|
1280
|
+
if (!showBalances) return funded;
|
|
1281
|
+
for (const chain of availableChains) {
|
|
1282
|
+
if (chainHasBalance(chain.blockchain, scopedTokens, (token) => getPayTokenBalance(ownerForToken(owners, token), token.assetId)?.formatted)) {
|
|
1283
|
+
funded.add(chain.blockchain);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return funded;
|
|
1287
|
+
}, [availableChains, owners, scopedTokens, showBalances, balanceEntries]);
|
|
1288
|
+
const popularTokens = useMemo2(
|
|
1289
|
+
() => popularTokensForWallets(scopedTokens, popularRefs, (kind) => Boolean(connectedOwners[kind])),
|
|
1290
|
+
[scopedTokens, popularRefs, connectedOwners]
|
|
1291
|
+
);
|
|
1292
|
+
const newestAssetId = showBalances ? prefs2.recentAssetIds[0] ?? null : null;
|
|
1293
|
+
const searchingAll = chainFilter === ALL_CHAIN_FILTER && search.trim().length > 0;
|
|
1294
|
+
const walletKind = chainFilter === ALL_CHAIN_FILTER ? null : availableChains.find((chain) => chain.blockchain === chainFilter)?.chainKind ?? null;
|
|
1295
|
+
const sections = useMemo2(() => {
|
|
1296
|
+
if (showBalances && chainFilter === ALL_CHAIN_FILTER && searchingAll) {
|
|
1297
|
+
const matched2 = scopedTokens.filter((token) => tokenMatchesSearch(token, search));
|
|
1298
|
+
return [{
|
|
1299
|
+
id: "search",
|
|
1300
|
+
tokens: sortTokensByBalance(matched2, usdOf, loadingOf),
|
|
1301
|
+
badgeAssetId: newestAssetId
|
|
1302
|
+
}];
|
|
1303
|
+
}
|
|
1304
|
+
if (showBalances && chainFilter === ALL_CHAIN_FILTER) {
|
|
1305
|
+
const next = [];
|
|
1306
|
+
const recent = recentTokensInOrder(scopedTokens, prefs2.recentAssetIds);
|
|
1307
|
+
if (recent.length > 0) {
|
|
1308
|
+
next.push({
|
|
1309
|
+
id: "recent",
|
|
1310
|
+
title: "Recently Used",
|
|
1311
|
+
tokens: recent,
|
|
1312
|
+
badgeAssetId: newestAssetId
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
const yours = positiveBalanceTokens(scopedTokens, usdOf);
|
|
1316
|
+
if (yours.length > 0) {
|
|
1317
|
+
next.push({ id: "yours", title: "Your Tokens", tokens: yours });
|
|
1318
|
+
}
|
|
1319
|
+
if (popularTokens.length > 0) {
|
|
1320
|
+
next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
|
|
1321
|
+
}
|
|
1322
|
+
return next;
|
|
1323
|
+
}
|
|
1324
|
+
const matched = scopedTokens.filter((token) => matchesChainFilter(token, chainFilter) && tokenMatchesSearch(token, search));
|
|
1325
|
+
if (!showBalances && chainFilter === ALL_CHAIN_FILTER && !search.trim()) {
|
|
1326
|
+
const next = [];
|
|
1327
|
+
if (popularTokens.length > 0) {
|
|
1328
|
+
next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
|
|
1329
|
+
}
|
|
1330
|
+
next.push({ id: "all", tokens: sortTokensBySymbol(matched) });
|
|
1331
|
+
return next;
|
|
1332
|
+
}
|
|
1333
|
+
const chainName = availableChains.find((chain) => chain.blockchain === chainFilter)?.chainName ?? chainFilter;
|
|
1334
|
+
return [{
|
|
1335
|
+
id: chainFilter,
|
|
1336
|
+
title: chainName,
|
|
1337
|
+
walletKind,
|
|
1338
|
+
tokens: showBalances ? sortTokensByBalance(matched, usdOf, loadingOf) : sortTokensBySymbol(matched),
|
|
1339
|
+
badgeAssetId: showBalances ? newestAssetId : null
|
|
1340
|
+
}];
|
|
1341
|
+
}, [
|
|
1342
|
+
showBalances,
|
|
1343
|
+
chainFilter,
|
|
1344
|
+
searchingAll,
|
|
1345
|
+
search,
|
|
1346
|
+
scopedTokens,
|
|
1347
|
+
prefs2.recentAssetIds,
|
|
1348
|
+
newestAssetId,
|
|
1349
|
+
walletKind,
|
|
1350
|
+
balanceEntries,
|
|
1351
|
+
owners,
|
|
1352
|
+
popularTokens,
|
|
1353
|
+
availableChains
|
|
1354
|
+
]);
|
|
1355
|
+
function handleSelectFilter(filter) {
|
|
1356
|
+
setChainFilter(filter);
|
|
1357
|
+
if (filter !== ALL_CHAIN_FILTER) setLastBlockchain(filter);
|
|
1358
|
+
setView("token");
|
|
1359
|
+
}
|
|
1360
|
+
function handleSelectToken(token) {
|
|
1361
|
+
if (isBlockchainDisabled(token.blockchain, disabledBlockchains)) return;
|
|
1362
|
+
if (isChainKindLocked(token.chain.chainKind, lockChainKind)) return;
|
|
1363
|
+
if (payer) setLastToken(token.assetId, token.blockchain);
|
|
1364
|
+
else setLastBlockchain(token.blockchain);
|
|
1365
|
+
onSelect({ token });
|
|
1366
|
+
onClose();
|
|
1367
|
+
}
|
|
1368
|
+
const awaitingBalances = showBalances && chainFilter === ALL_CHAIN_FILTER && !searchingAll && scopedTokens.some((token) => loadingOf(token));
|
|
1369
|
+
return /* @__PURE__ */ jsx6(
|
|
1370
|
+
Dialog,
|
|
1371
|
+
{
|
|
1372
|
+
open,
|
|
1373
|
+
onClose,
|
|
1374
|
+
title,
|
|
1375
|
+
titleClassName: cx(
|
|
1376
|
+
"min-w-0 flex-1 truncate text-base font-medium text-[#606060] md:flex-none",
|
|
1377
|
+
fontSans,
|
|
1378
|
+
titleClassName
|
|
1379
|
+
),
|
|
1380
|
+
closeClassName: cx(view === "token" ? "ml-0" : void 0, closeClassName),
|
|
1381
|
+
contentClassName,
|
|
1382
|
+
headerAction: view === "token" ? /* @__PURE__ */ jsx6(
|
|
1383
|
+
SearchInput2,
|
|
1384
|
+
{
|
|
1385
|
+
value: search,
|
|
1386
|
+
onChange: setSearch,
|
|
1387
|
+
placeholder: "search or paste address",
|
|
1388
|
+
className: "ml-auto hidden w-[215px] shrink-0 md:block",
|
|
1389
|
+
inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
|
|
1390
|
+
}
|
|
1391
|
+
) : null,
|
|
1392
|
+
cardClassName: cx("w-full max-h-[90vh] md:w-[474px]", className),
|
|
1393
|
+
children: /* @__PURE__ */ jsxs5("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
|
|
1394
|
+
view === "token" ? /* @__PURE__ */ jsx6(
|
|
1395
|
+
SearchInput2,
|
|
1396
|
+
{
|
|
1397
|
+
value: search,
|
|
1398
|
+
onChange: setSearch,
|
|
1399
|
+
placeholder: "search or paste address",
|
|
1400
|
+
className: "mb-3 shrink-0 md:hidden",
|
|
1401
|
+
inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
|
|
1402
|
+
}
|
|
1403
|
+
) : null,
|
|
1404
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex min-h-0 flex-1", children: [
|
|
1405
|
+
/* @__PURE__ */ jsx6(
|
|
1406
|
+
NetworkRail,
|
|
1407
|
+
{
|
|
1408
|
+
view,
|
|
1409
|
+
chainFilter,
|
|
1410
|
+
chips: sortedChains,
|
|
1411
|
+
fundedBlockchains,
|
|
1412
|
+
walletConnected,
|
|
1413
|
+
showAllLabel: !payer,
|
|
1414
|
+
lockChainKind,
|
|
1415
|
+
disabledBlockchains,
|
|
1416
|
+
disabledReason,
|
|
1417
|
+
onSelectFilter: handleSelectFilter,
|
|
1418
|
+
onOpenNetworks: () => setView("network"),
|
|
1419
|
+
className: railClassName
|
|
1420
|
+
}
|
|
1421
|
+
),
|
|
1422
|
+
view === "network" ? /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
|
|
1423
|
+
ChainPane,
|
|
1424
|
+
{
|
|
1425
|
+
chains: sortedChains,
|
|
1426
|
+
fundedBlockchains,
|
|
1427
|
+
showWalletStatus: payer,
|
|
1428
|
+
onSelectFilter: handleSelectFilter,
|
|
1429
|
+
lockChainKind,
|
|
1430
|
+
disabledBlockchains,
|
|
1431
|
+
disabledReason
|
|
1432
|
+
}
|
|
1433
|
+
) }) : /* @__PURE__ */ jsx6("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col pl-3 md:pl-4", children: /* @__PURE__ */ jsx6(
|
|
1434
|
+
TokenPane,
|
|
1435
|
+
{
|
|
1436
|
+
sections,
|
|
1437
|
+
selectedAssetId,
|
|
1438
|
+
loading: loading || awaitingBalances,
|
|
1439
|
+
showBalances,
|
|
1440
|
+
showWalletStatus: payer,
|
|
1441
|
+
getBalance: balanceOf,
|
|
1442
|
+
isBalanceLoading: loadingOf,
|
|
1443
|
+
onSelectToken: handleSelectToken,
|
|
1444
|
+
isTokenDisabled: (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind),
|
|
1445
|
+
searchClassName,
|
|
1446
|
+
rowClassName
|
|
1447
|
+
}
|
|
1448
|
+
) })
|
|
1449
|
+
] })
|
|
1450
|
+
] })
|
|
1451
|
+
}
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
export {
|
|
1455
|
+
DEFAULT_API_HOST,
|
|
1456
|
+
PayWidgetsProvider,
|
|
1457
|
+
TokenSelectDialog,
|
|
1458
|
+
clearPayTokenBalances,
|
|
1459
|
+
mapPayConfigSource,
|
|
1460
|
+
normalizePayConfig,
|
|
1461
|
+
parsePopularTokens,
|
|
1462
|
+
refreshPayTokenBalance,
|
|
1463
|
+
usePayConfig,
|
|
1464
|
+
usePayConfigSnapshot,
|
|
1465
|
+
usePayTokenBalance
|
|
1466
|
+
};
|
|
1467
|
+
//# sourceMappingURL=index.js.map
|