@dynamic-labs/sdk-react-core 4.84.0 → 4.84.1
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/CHANGELOG.md +18 -1
- package/package.cjs +1 -1
- package/package.js +1 -1
- package/package.json +12 -12
- package/src/index.cjs +2 -0
- package/src/index.d.ts +2 -2
- package/src/index.js +1 -0
- package/src/lib/components/SendBalanceForm/SendBalanceForm.cjs +26 -1
- package/src/lib/components/SendBalanceForm/SendBalanceForm.js +26 -1
- package/src/lib/components/SendBalancePageLayout/SendBalancePageLayout.cjs +6 -1
- package/src/lib/components/SendBalancePageLayout/SendBalancePageLayout.js +6 -1
- package/src/lib/utils/hooks/index.d.ts +2 -0
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.cjs +24 -4
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.d.ts +10 -2
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.js +24 -4
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs +14 -3
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.d.ts +5 -1
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js +14 -3
- package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.cjs +14 -0
- package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.js +14 -0
- package/src/lib/utils/hooks/usePrivateTokenBalances/index.d.ts +2 -0
- package/src/lib/utils/hooks/usePrivateTokenBalances/usePrivateTokenBalances.cjs +19 -0
- package/src/lib/utils/hooks/usePrivateTokenBalances/usePrivateTokenBalances.d.ts +9 -0
- package/src/lib/utils/hooks/usePrivateTokenBalances/usePrivateTokenBalances.js +15 -0
- package/src/lib/utils/hooks/useWalletDelegation/useWalletDelegation.cjs +19 -16
- package/src/lib/utils/hooks/useWalletDelegation/useWalletDelegation.d.ts +8 -0
- package/src/lib/utils/hooks/useWalletDelegation/useWalletDelegation.js +19 -17
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.cjs +138 -21
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.js +139 -22
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/optimisticShield.cjs +134 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/optimisticShield.d.ts +69 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/optimisticShield.js +127 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import { __awaiter } from '../../../../../../_virtual/_tslib.js';
|
|
3
3
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
-
import { useState, useRef, useMemo,
|
|
4
|
+
import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import '../../../../context/DynamicContext/useDynamicContext/useDynamicContext.js';
|
|
7
7
|
import { useInternalDynamicContext } from '../../../../context/DynamicContext/useDynamicContext/useInternalDynamicContext/useInternalDynamicContext.js';
|
|
@@ -123,6 +123,7 @@ import '../../views/ReceiveWalletFunds/ReceiveWalletFunds.js';
|
|
|
123
123
|
import '../../../../store/state/multichainBalances.js';
|
|
124
124
|
import '@dynamic-labs/store';
|
|
125
125
|
import '../../../../shared/utils/functions/getInitialUrl/getInitialUrl.js';
|
|
126
|
+
import { applyOptimisticUnshieldedDeductions, applyOptimisticShieldedAdditions, reconcileOptimisticShields } from './optimisticShield.js';
|
|
126
127
|
import { TokenBalanceList } from './TokenBalanceList/TokenBalanceList.js';
|
|
127
128
|
|
|
128
129
|
/** Component to display token balances for the primary wallet */
|
|
@@ -149,6 +150,57 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
149
150
|
includeNativeBalance: true,
|
|
150
151
|
});
|
|
151
152
|
const { tokenBalances: shieldedTokenBalances, isLoading: isLoadingShielded, refetch: refetchShielded, supportsShielded, } = useAleoShieldedBalances();
|
|
153
|
+
// Optimistic shield ledger. A shield broadcast resolves before the
|
|
154
|
+
// public-balance mapping flips and well before the RecordScanner
|
|
155
|
+
// index reflects the new private record, so naively trusting the
|
|
156
|
+
// server feed shows a stale unshielded balance and an unchanged
|
|
157
|
+
// shielded balance for ~10–90s after the user clicks. Pushing an
|
|
158
|
+
// entry here deducts the burned amount from unshielded and prepends
|
|
159
|
+
// / bumps the matching shielded row, then `reconcileOptimisticShields`
|
|
160
|
+
// drops the entry as soon as either server feed catches up.
|
|
161
|
+
const [optimisticShields, setOptimisticShields] = useState([]);
|
|
162
|
+
const effectiveUnshieldedTokenBalances = useMemo(() => applyOptimisticUnshieldedDeductions(unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [], optimisticShields), [unshieldedTokenBalances, optimisticShields]);
|
|
163
|
+
const effectiveShieldedTokenBalances = useMemo(() => applyOptimisticShieldedAdditions(shieldedTokenBalances, optimisticShields), [shieldedTokenBalances, optimisticShields]);
|
|
164
|
+
// Reconcile + GC optimistic entries whenever either server feed
|
|
165
|
+
// updates. The reconciler is pure; we short-circuit the state write
|
|
166
|
+
// when nothing changed so downstream memos don't churn on every
|
|
167
|
+
// refresh tick.
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
setOptimisticShields((current) => {
|
|
170
|
+
if (current.length === 0)
|
|
171
|
+
return current;
|
|
172
|
+
const next = reconcileOptimisticShields(current, unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [], shieldedTokenBalances, Date.now());
|
|
173
|
+
return next.length === current.length ? current : next;
|
|
174
|
+
});
|
|
175
|
+
}, [unshieldedTokenBalances, shieldedTokenBalances]);
|
|
176
|
+
// Push helper used by both the manual Shield Manually CTA and the
|
|
177
|
+
// auto-shield hook so the optimistic UI semantics are identical
|
|
178
|
+
// whichever flow dispatches the broadcast.
|
|
179
|
+
const recordOptimisticShield = useCallback((info) => {
|
|
180
|
+
const { token, amount } = info;
|
|
181
|
+
const matchingShielded = shieldedTokenBalances.find((t) => token.isNative
|
|
182
|
+
? Boolean(t.isNative)
|
|
183
|
+
: !t.isNative && t.address === token.address);
|
|
184
|
+
setOptimisticShields((current) => {
|
|
185
|
+
var _a, _b, _c, _d;
|
|
186
|
+
return [
|
|
187
|
+
...current,
|
|
188
|
+
{
|
|
189
|
+
amount,
|
|
190
|
+
createdAtMs: Date.now(),
|
|
191
|
+
decimals: (_a = token.decimals) !== null && _a !== void 0 ? _a : 0,
|
|
192
|
+
isNative: Boolean(token.isNative),
|
|
193
|
+
logoURI: (_b = token.logoURI) !== null && _b !== void 0 ? _b : '',
|
|
194
|
+
name: token.name,
|
|
195
|
+
preShieldShieldedRaw: BigInt(Math.round((_c = matchingShielded === null || matchingShielded === void 0 ? void 0 : matchingShielded.rawBalance) !== null && _c !== void 0 ? _c : 0)),
|
|
196
|
+
preShieldUnshieldedRaw: BigInt(Math.round((_d = token.rawBalance) !== null && _d !== void 0 ? _d : 0)),
|
|
197
|
+
price: token.price,
|
|
198
|
+
symbol: token.symbol,
|
|
199
|
+
tokenAddress: token.address,
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
});
|
|
203
|
+
}, [shieldedTokenBalances]);
|
|
152
204
|
/**
|
|
153
205
|
* Tab state for chains that have a shielded/unshielded split. Defaults to
|
|
154
206
|
* shielded so Aleo wallets land on their primary balance type. Other
|
|
@@ -193,11 +245,22 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
193
245
|
// Manually CTA, which prompts the user-paid confirmation modal.
|
|
194
246
|
const { isShielding: isAutoShielding, currentlyShieldingTokenKeys: autoShieldingTokenKeys, } = useAleoAutoShieldSponsoredTokens({
|
|
195
247
|
accountAddress: primaryWallet === null || primaryWallet === void 0 ? void 0 : primaryWallet.address,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
248
|
+
onShieldDispatched: recordOptimisticShield,
|
|
249
|
+
onShielded: useCallback(
|
|
250
|
+
// Parallel refresh — same reasoning as the manual shield path:
|
|
251
|
+
// shield ops move value across both tabs, and serializing the
|
|
252
|
+
// two refreshes doubled the visible latency for the Shielded
|
|
253
|
+
// tab in the common case.
|
|
254
|
+
() => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
return Promise.all([fetchAccountBalances(true), refetchShielded()]).then(() => undefined);
|
|
199
256
|
}), [fetchAccountBalances, refetchShielded]),
|
|
200
257
|
shieldHandle: aleoShieldHandle,
|
|
258
|
+
// Auto-shield reads the raw server feed (not the optimistic one):
|
|
259
|
+
// once we deduct optimistically the to-be-shielded token drops to
|
|
260
|
+
// zero in the effective list, which would suppress the next
|
|
261
|
+
// auto-shield run before this one finishes. The auto-shield hook
|
|
262
|
+
// has its own per-session idempotency (`seenKeys`) so it won't
|
|
263
|
+
// re-fire for the same token anyway.
|
|
201
264
|
unshieldedTokenBalances: unshieldedTokenBalances,
|
|
202
265
|
});
|
|
203
266
|
// Liveness handle for the manual post-shield poll — set to true when
|
|
@@ -210,12 +273,25 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
210
273
|
isUnmountedRef.current = true;
|
|
211
274
|
};
|
|
212
275
|
}, []);
|
|
276
|
+
// Latest server-side unshielded list mirrored into a ref so the
|
|
277
|
+
// post-shield convergence check can read the freshest value after
|
|
278
|
+
// each polled refresh. `unshieldedTokenBalances` is closure-captured
|
|
279
|
+
// by `handleShieldToken` at callback construction time, so without
|
|
280
|
+
// this ref the predicate would read the pre-shield list forever and
|
|
281
|
+
// the poll would never exit early. Reads from the raw server feed,
|
|
282
|
+
// NOT the optimistic one — convergence means "on-chain effect
|
|
283
|
+
// observed", which is only meaningful against the actual server
|
|
284
|
+
// state.
|
|
285
|
+
const unshieldedTokenBalancesRef = useRef(unshieldedTokenBalances);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
unshieldedTokenBalancesRef.current = unshieldedTokenBalances;
|
|
288
|
+
}, [unshieldedTokenBalances]);
|
|
213
289
|
// Token currently awaiting user-paid fee confirmation. Set when
|
|
214
290
|
// Shield Manually is clicked on a token Feemaster doesn't sponsor;
|
|
215
291
|
// cleared on Cancel or after the modal's Shield button dispatches.
|
|
216
292
|
const [pendingShieldToken, setPendingShieldToken] = useState(null);
|
|
217
293
|
const handleShieldToken = useCallback((token) => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
-
var _f;
|
|
294
|
+
var _f, _g;
|
|
219
295
|
if (!aleoShieldHandle || shieldingAddress)
|
|
220
296
|
return;
|
|
221
297
|
// `rawBalance` is the atomic-units count redcoast surfaces (number).
|
|
@@ -225,6 +301,14 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
225
301
|
const atomic = BigInt(Math.round((_f = token.rawBalance) !== null && _f !== void 0 ? _f : 0));
|
|
226
302
|
if (atomic <= BigInt(0))
|
|
227
303
|
return;
|
|
304
|
+
// Snapshot the pre-shield unshielded balance for the convergence
|
|
305
|
+
// predicate below. Aleo's `transfer_public_to_private` burns the
|
|
306
|
+
// public mapping value, so a successful shield must show up as a
|
|
307
|
+
// drop in this token's `rawBalance`. Polling exits early once
|
|
308
|
+
// that drop materializes — no need to keep refreshing on the
|
|
309
|
+
// long-tail schedule when we've already observed the on-chain
|
|
310
|
+
// effect.
|
|
311
|
+
const preShieldRawBalance = (_g = token.rawBalance) !== null && _g !== void 0 ? _g : 0;
|
|
228
312
|
setShieldingAddress(token.address);
|
|
229
313
|
let didBroadcast = false;
|
|
230
314
|
try {
|
|
@@ -234,11 +318,22 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
234
318
|
tokenAddress: token.address,
|
|
235
319
|
});
|
|
236
320
|
didBroadcast = true;
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
321
|
+
// Optimistic balance update: deduct the burned amount from
|
|
322
|
+
// unshielded and bump (or synthesise) the matching shielded
|
|
323
|
+
// row before the server-side feeds catch up. Without this the
|
|
324
|
+
// Shield Manually CTA flickers back into view during the
|
|
325
|
+
// indexing tail and a fast user can issue a second shield
|
|
326
|
+
// that fails server-side (validator rejects the now-zero
|
|
327
|
+
// public balance) and looks to them like the first one
|
|
328
|
+
// failed.
|
|
329
|
+
recordOptimisticShield({ amount: atomic, token });
|
|
330
|
+
// Refresh both tabs in parallel. Shield moves value across the
|
|
331
|
+
// shielded/unshielded boundary, so both lists need to
|
|
332
|
+
// converge. Running them in parallel (vs. sequential) halves
|
|
333
|
+
// the visible refresh latency and shortens the window where
|
|
334
|
+
// `useTokenBalances`' `isLoading` guard could drop a
|
|
335
|
+
// concurrent fetch.
|
|
336
|
+
yield Promise.all([fetchAccountBalances(true), refetchShielded()]);
|
|
242
337
|
}
|
|
243
338
|
catch (err) {
|
|
244
339
|
logger.debug('[ActiveWalletBalance] shieldToken failed', err);
|
|
@@ -249,7 +344,7 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
249
344
|
// The relay-accepted broadcast resolves *before* the
|
|
250
345
|
// public→private transition finalizes on-chain, so the immediate
|
|
251
346
|
// refresh above sees the pre-confirmation balance. Re-poll on the
|
|
252
|
-
// shared backoff so the Unshielded row visibly drops
|
|
347
|
+
// shared backoff so the Unshielded row visibly drops and the
|
|
253
348
|
// Shielded row picks the new record up without a manual reload.
|
|
254
349
|
// Polling is best-effort — errors are swallowed inside
|
|
255
350
|
// `pollOnShielded`. Skip when the broadcast itself failed; there's
|
|
@@ -257,10 +352,27 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
257
352
|
if (!didBroadcast)
|
|
258
353
|
return;
|
|
259
354
|
yield pollOnShielded(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
260
|
-
yield fetchAccountBalances(true);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
355
|
+
yield Promise.all([fetchAccountBalances(true), refetchShielded()]);
|
|
356
|
+
}), () => isUnmountedRef.current, () => {
|
|
357
|
+
var _a, _b;
|
|
358
|
+
// Convergence: the just-shielded token's unshielded raw
|
|
359
|
+
// balance has dropped below the pre-shield value. We read
|
|
360
|
+
// via the ref so we see whatever the store published after
|
|
361
|
+
// the most recent poll, not the closure-captured list from
|
|
362
|
+
// callback construction time. We don't require an exact 0
|
|
363
|
+
// — additional unshielded credits could land between scans;
|
|
364
|
+
// any decrease is sufficient evidence the burn took effect.
|
|
365
|
+
const current = (_a = unshieldedTokenBalancesRef.current) === null || _a === void 0 ? void 0 : _a.find((t) => t.address === token.address && t.isNative === token.isNative);
|
|
366
|
+
const currentRaw = (_b = current === null || current === void 0 ? void 0 : current.rawBalance) !== null && _b !== void 0 ? _b : 0;
|
|
367
|
+
return currentRaw < preShieldRawBalance;
|
|
368
|
+
});
|
|
369
|
+
}), [
|
|
370
|
+
aleoShieldHandle,
|
|
371
|
+
fetchAccountBalances,
|
|
372
|
+
recordOptimisticShield,
|
|
373
|
+
refetchShielded,
|
|
374
|
+
shieldingAddress,
|
|
375
|
+
]);
|
|
264
376
|
const getSecondaryAction = useCallback((token) => {
|
|
265
377
|
// Only on the Unshielded tab, only when the connector exposes shield
|
|
266
378
|
// helpers, only for tokens registered as shieldable, only for tokens
|
|
@@ -323,12 +435,12 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
323
435
|
shieldingAddress,
|
|
324
436
|
]);
|
|
325
437
|
const tokenBalances = useMemo(() => supportsShielded && activeShieldTab === 'shielded'
|
|
326
|
-
?
|
|
327
|
-
:
|
|
438
|
+
? effectiveShieldedTokenBalances
|
|
439
|
+
: effectiveUnshieldedTokenBalances, [
|
|
328
440
|
activeShieldTab,
|
|
329
|
-
|
|
441
|
+
effectiveShieldedTokenBalances,
|
|
442
|
+
effectiveUnshieldedTokenBalances,
|
|
330
443
|
supportsShielded,
|
|
331
|
-
unshieldedTokenBalances,
|
|
332
444
|
]);
|
|
333
445
|
const filteredTokenBalances = useMemo(() => (tokenBalances === null || tokenBalances === void 0 ? void 0 : tokenBalances.filter((token) => token.name)) || [], [tokenBalances]);
|
|
334
446
|
const totalValue = useMemo(() => filteredTokenBalances.reduce((acc, token) => acc + ((token === null || token === void 0 ? void 0 : token.marketValue) || 0), 0), [filteredTokenBalances]);
|
|
@@ -371,8 +483,14 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
371
483
|
setIsRefreshing(true);
|
|
372
484
|
setIsSuccess(false);
|
|
373
485
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
486
|
+
// Chains with a shielded/unshielded split (Aleo) refresh BOTH
|
|
487
|
+
// tabs on a single click. A shield op moves value across the
|
|
488
|
+
// boundary, so refreshing only the active tab leaves the other
|
|
489
|
+
// visibly stale — the original complaint that drove this fix.
|
|
490
|
+
// Other chains never had `refetchShielded` (no-op fallback) so
|
|
491
|
+
// they still just hit `fetchAccountBalances` here.
|
|
492
|
+
if (supportsShielded) {
|
|
493
|
+
yield Promise.all([refetchShielded(), fetchAccountBalances(true)]);
|
|
376
494
|
}
|
|
377
495
|
else {
|
|
378
496
|
yield fetchAccountBalances(true);
|
|
@@ -396,7 +514,6 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
|
|
|
396
514
|
fetchAccountBalances,
|
|
397
515
|
refetchShielded,
|
|
398
516
|
supportsShielded,
|
|
399
|
-
activeShieldTab,
|
|
400
517
|
]);
|
|
401
518
|
const primaryWalletNativeBalance = () => {
|
|
402
519
|
if (!primaryWallet ||
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
// Default safety-net stale window for optimistic entries. Tuned to
|
|
7
|
+
// comfortably outlast the post-shield polling schedule (~106s) so
|
|
8
|
+
// reconciliation has a chance to fire on the real server-side state
|
|
9
|
+
// before the safety net trips.
|
|
10
|
+
const OPTIMISTIC_SHIELD_STALE_MS = 180000;
|
|
11
|
+
const matches = (entry, token) => {
|
|
12
|
+
if (entry.isNative)
|
|
13
|
+
return Boolean(token.isNative);
|
|
14
|
+
return !token.isNative && entry.tokenAddress === token.address;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Reduce each unshielded token's `rawBalance` by the sum of pending
|
|
18
|
+
* optimistic shields for the same `(address, isNative)`, clamped to 0.
|
|
19
|
+
* `balance` and `marketValue` scale by the same ratio so the fiat
|
|
20
|
+
* column doesn't disagree with the optimistic raw balance.
|
|
21
|
+
*
|
|
22
|
+
* Tokens with no pending optimistic shield pass through by reference
|
|
23
|
+
* (no copy) so downstream memos / identity comparisons don't churn on
|
|
24
|
+
* every render.
|
|
25
|
+
*/
|
|
26
|
+
const applyOptimisticUnshieldedDeductions = (balances, optimisticShields) => {
|
|
27
|
+
if (optimisticShields.length === 0)
|
|
28
|
+
return balances;
|
|
29
|
+
return balances.map((token) => {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
const adjustments = optimisticShields.filter((o) => matches(o, token));
|
|
32
|
+
if (adjustments.length === 0)
|
|
33
|
+
return token;
|
|
34
|
+
const currentRaw = BigInt(Math.round((_a = token.rawBalance) !== null && _a !== void 0 ? _a : 0));
|
|
35
|
+
const totalAdjustment = adjustments.reduce((sum, o) => sum + o.amount, BigInt(0));
|
|
36
|
+
const newRaw = currentRaw > totalAdjustment ? currentRaw - totalAdjustment : BigInt(0);
|
|
37
|
+
// Ratio-scale display-units balance + fiat so they stay consistent
|
|
38
|
+
// with the optimistic raw value. `currentRaw === 0n` happens when
|
|
39
|
+
// the server feed has already caught up to zero but reconciliation
|
|
40
|
+
// hasn't fired yet; treat ratio as 0 to keep display at 0.
|
|
41
|
+
const ratio = currentRaw > BigInt(0) ? Number(newRaw) / Number(currentRaw) : 0;
|
|
42
|
+
return Object.assign(Object.assign({}, token), { balance: ((_b = token.balance) !== null && _b !== void 0 ? _b : 0) * ratio, marketValue: token.marketValue !== undefined ? token.marketValue * ratio : undefined, rawBalance: Number(newRaw), rawBalanceString: newRaw.toString() });
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Apply optimistic shield additions to the shielded balance list. For
|
|
47
|
+
* each optimistic shield with a matching `(address, isNative)` entry
|
|
48
|
+
* already in `shielded`, bump that entry; otherwise prepend a fresh
|
|
49
|
+
* synthesized row built from the entry's captured metadata.
|
|
50
|
+
*
|
|
51
|
+
* Returns a fresh array only when at least one optimistic shield
|
|
52
|
+
* applies. When `optimisticShields` is empty we pass through the
|
|
53
|
+
* input array by reference so consumers that compare by identity
|
|
54
|
+
* don't churn.
|
|
55
|
+
*/
|
|
56
|
+
const applyOptimisticShieldedAdditions = (shielded, optimisticShields) => {
|
|
57
|
+
if (optimisticShields.length === 0)
|
|
58
|
+
return shielded;
|
|
59
|
+
const consumedIndices = new Set();
|
|
60
|
+
const merged = shielded.map((token) => {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
const adjustments = optimisticShields.filter((o, idx) => {
|
|
63
|
+
if (!matches(o, token))
|
|
64
|
+
return false;
|
|
65
|
+
consumedIndices.add(idx);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
if (adjustments.length === 0)
|
|
69
|
+
return token;
|
|
70
|
+
const totalAtomic = adjustments.reduce((sum, o) => sum + o.amount, BigInt(0));
|
|
71
|
+
const newRaw = BigInt(Math.round((_a = token.rawBalance) !== null && _a !== void 0 ? _a : 0)) + totalAtomic;
|
|
72
|
+
const decimals = (_b = token.decimals) !== null && _b !== void 0 ? _b : 0;
|
|
73
|
+
const denominator = Math.pow(10, decimals);
|
|
74
|
+
const newDisplay = Number(newRaw) / denominator;
|
|
75
|
+
return Object.assign(Object.assign({}, token), { balance: newDisplay, marketValue: token.price !== undefined
|
|
76
|
+
? newDisplay * token.price
|
|
77
|
+
: token.marketValue, rawBalance: Number(newRaw), rawBalanceString: newRaw.toString() });
|
|
78
|
+
});
|
|
79
|
+
// Prepend synthesized rows for any optimistic shield that didn't
|
|
80
|
+
// fold into an existing shielded row above. New rows go to the top
|
|
81
|
+
// of the list so the just-shielded token is immediately visible.
|
|
82
|
+
const synthesized = [];
|
|
83
|
+
optimisticShields.forEach((entry, idx) => {
|
|
84
|
+
if (consumedIndices.has(idx))
|
|
85
|
+
return;
|
|
86
|
+
const denominator = Math.pow(10, entry.decimals);
|
|
87
|
+
const display = Number(entry.amount) / denominator;
|
|
88
|
+
const marketValue = entry.price !== undefined ? display * entry.price : undefined;
|
|
89
|
+
synthesized.push(Object.assign(Object.assign({ address: entry.tokenAddress, balance: display, decimals: entry.decimals, isNative: entry.isNative, logoURI: entry.logoURI, name: entry.name, rawBalance: Number(entry.amount), rawBalanceString: entry.amount.toString(), symbol: entry.symbol }, (entry.price !== undefined ? { price: entry.price } : {})), (marketValue !== undefined ? { marketValue } : {})));
|
|
90
|
+
});
|
|
91
|
+
return [...synthesized, ...merged];
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Filter the optimistic shield list down to entries that are still
|
|
95
|
+
* relevant. An entry is dropped when any of:
|
|
96
|
+
*
|
|
97
|
+
* - the unshielded server feed reports a `rawBalance` strictly less
|
|
98
|
+
* than `preShieldUnshieldedRaw` for that token (the burn was
|
|
99
|
+
* observed on-chain),
|
|
100
|
+
* - the shielded server feed reports a `rawBalance` strictly greater
|
|
101
|
+
* than `preShieldShieldedRaw` for that token (the new record was
|
|
102
|
+
* indexed),
|
|
103
|
+
* - the unshielded server no longer lists the token at all (rare,
|
|
104
|
+
* but means the public balance is effectively zero — our
|
|
105
|
+
* optimistic deduct is moot),
|
|
106
|
+
* - the entry is older than `staleMs` (safety net for shields whose
|
|
107
|
+
* on-chain effect is masked by parallel inbound credits).
|
|
108
|
+
*
|
|
109
|
+
* Pure / module-scope so it's trivially unit-testable; the widget
|
|
110
|
+
* just calls it on every server-side balance update.
|
|
111
|
+
*/
|
|
112
|
+
const reconcileOptimisticShields = (optimisticShields, unshieldedTokenBalances, shieldedTokenBalances, nowMs, staleMs = OPTIMISTIC_SHIELD_STALE_MS) => optimisticShields.filter((entry) => {
|
|
113
|
+
var _a, _b;
|
|
114
|
+
if (nowMs - entry.createdAtMs > staleMs)
|
|
115
|
+
return false;
|
|
116
|
+
const unshielded = unshieldedTokenBalances.find((t) => matches(entry, t));
|
|
117
|
+
if (!unshielded)
|
|
118
|
+
return false;
|
|
119
|
+
const serverUnshielded = BigInt(Math.round((_a = unshielded.rawBalance) !== null && _a !== void 0 ? _a : 0));
|
|
120
|
+
if (serverUnshielded < entry.preShieldUnshieldedRaw)
|
|
121
|
+
return false;
|
|
122
|
+
const shielded = shieldedTokenBalances.find((t) => matches(entry, t));
|
|
123
|
+
const serverShielded = shielded
|
|
124
|
+
? BigInt(Math.round((_b = shielded.rawBalance) !== null && _b !== void 0 ? _b : 0))
|
|
125
|
+
: BigInt(0);
|
|
126
|
+
if (serverShielded > entry.preShieldShieldedRaw)
|
|
127
|
+
return false;
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
exports.OPTIMISTIC_SHIELD_STALE_MS = OPTIMISTIC_SHIELD_STALE_MS;
|
|
132
|
+
exports.applyOptimisticShieldedAdditions = applyOptimisticShieldedAdditions;
|
|
133
|
+
exports.applyOptimisticUnshieldedDeductions = applyOptimisticUnshieldedDeductions;
|
|
134
|
+
exports.reconcileOptimisticShields = reconcileOptimisticShields;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TokenBalance } from '@dynamic-labs/sdk-api-core';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory record of a shield operation that has been accepted by the
|
|
4
|
+
* prover/relay but not yet observed on-chain by either the public
|
|
5
|
+
* balance mapping (Provable's `/mapping/balances/...` endpoint) or
|
|
6
|
+
* the RecordScanner index. Until reconciliation drops it, the entry
|
|
7
|
+
* forces the widget to display the post-shield state — unshielded
|
|
8
|
+
* rawBalance reduced by `amount`, shielded rawBalance increased by
|
|
9
|
+
* the same — so the Shield Manually CTA disappears the moment the
|
|
10
|
+
* shield is broadcast instead of flickering back into view during
|
|
11
|
+
* the indexing tail and tempting the user into a second shield.
|
|
12
|
+
*/
|
|
13
|
+
export type OptimisticShieldEntry = {
|
|
14
|
+
tokenAddress: string;
|
|
15
|
+
isNative: boolean;
|
|
16
|
+
amount: bigint;
|
|
17
|
+
preShieldUnshieldedRaw: bigint;
|
|
18
|
+
preShieldShieldedRaw: bigint;
|
|
19
|
+
symbol: string;
|
|
20
|
+
name: string;
|
|
21
|
+
decimals: number;
|
|
22
|
+
logoURI: string;
|
|
23
|
+
price?: number;
|
|
24
|
+
createdAtMs: number;
|
|
25
|
+
};
|
|
26
|
+
export declare const OPTIMISTIC_SHIELD_STALE_MS = 180000;
|
|
27
|
+
/**
|
|
28
|
+
* Reduce each unshielded token's `rawBalance` by the sum of pending
|
|
29
|
+
* optimistic shields for the same `(address, isNative)`, clamped to 0.
|
|
30
|
+
* `balance` and `marketValue` scale by the same ratio so the fiat
|
|
31
|
+
* column doesn't disagree with the optimistic raw balance.
|
|
32
|
+
*
|
|
33
|
+
* Tokens with no pending optimistic shield pass through by reference
|
|
34
|
+
* (no copy) so downstream memos / identity comparisons don't churn on
|
|
35
|
+
* every render.
|
|
36
|
+
*/
|
|
37
|
+
export declare const applyOptimisticUnshieldedDeductions: (balances: TokenBalance[], optimisticShields: ReadonlyArray<OptimisticShieldEntry>) => TokenBalance[];
|
|
38
|
+
/**
|
|
39
|
+
* Apply optimistic shield additions to the shielded balance list. For
|
|
40
|
+
* each optimistic shield with a matching `(address, isNative)` entry
|
|
41
|
+
* already in `shielded`, bump that entry; otherwise prepend a fresh
|
|
42
|
+
* synthesized row built from the entry's captured metadata.
|
|
43
|
+
*
|
|
44
|
+
* Returns a fresh array only when at least one optimistic shield
|
|
45
|
+
* applies. When `optimisticShields` is empty we pass through the
|
|
46
|
+
* input array by reference so consumers that compare by identity
|
|
47
|
+
* don't churn.
|
|
48
|
+
*/
|
|
49
|
+
export declare const applyOptimisticShieldedAdditions: (shielded: TokenBalance[], optimisticShields: ReadonlyArray<OptimisticShieldEntry>) => TokenBalance[];
|
|
50
|
+
/**
|
|
51
|
+
* Filter the optimistic shield list down to entries that are still
|
|
52
|
+
* relevant. An entry is dropped when any of:
|
|
53
|
+
*
|
|
54
|
+
* - the unshielded server feed reports a `rawBalance` strictly less
|
|
55
|
+
* than `preShieldUnshieldedRaw` for that token (the burn was
|
|
56
|
+
* observed on-chain),
|
|
57
|
+
* - the shielded server feed reports a `rawBalance` strictly greater
|
|
58
|
+
* than `preShieldShieldedRaw` for that token (the new record was
|
|
59
|
+
* indexed),
|
|
60
|
+
* - the unshielded server no longer lists the token at all (rare,
|
|
61
|
+
* but means the public balance is effectively zero — our
|
|
62
|
+
* optimistic deduct is moot),
|
|
63
|
+
* - the entry is older than `staleMs` (safety net for shields whose
|
|
64
|
+
* on-chain effect is masked by parallel inbound credits).
|
|
65
|
+
*
|
|
66
|
+
* Pure / module-scope so it's trivially unit-testable; the widget
|
|
67
|
+
* just calls it on every server-side balance update.
|
|
68
|
+
*/
|
|
69
|
+
export declare const reconcileOptimisticShields: (optimisticShields: ReadonlyArray<OptimisticShieldEntry>, unshieldedTokenBalances: TokenBalance[], shieldedTokenBalances: TokenBalance[], nowMs: number, staleMs?: number) => OptimisticShieldEntry[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
// Default safety-net stale window for optimistic entries. Tuned to
|
|
3
|
+
// comfortably outlast the post-shield polling schedule (~106s) so
|
|
4
|
+
// reconciliation has a chance to fire on the real server-side state
|
|
5
|
+
// before the safety net trips.
|
|
6
|
+
const OPTIMISTIC_SHIELD_STALE_MS = 180000;
|
|
7
|
+
const matches = (entry, token) => {
|
|
8
|
+
if (entry.isNative)
|
|
9
|
+
return Boolean(token.isNative);
|
|
10
|
+
return !token.isNative && entry.tokenAddress === token.address;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Reduce each unshielded token's `rawBalance` by the sum of pending
|
|
14
|
+
* optimistic shields for the same `(address, isNative)`, clamped to 0.
|
|
15
|
+
* `balance` and `marketValue` scale by the same ratio so the fiat
|
|
16
|
+
* column doesn't disagree with the optimistic raw balance.
|
|
17
|
+
*
|
|
18
|
+
* Tokens with no pending optimistic shield pass through by reference
|
|
19
|
+
* (no copy) so downstream memos / identity comparisons don't churn on
|
|
20
|
+
* every render.
|
|
21
|
+
*/
|
|
22
|
+
const applyOptimisticUnshieldedDeductions = (balances, optimisticShields) => {
|
|
23
|
+
if (optimisticShields.length === 0)
|
|
24
|
+
return balances;
|
|
25
|
+
return balances.map((token) => {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
const adjustments = optimisticShields.filter((o) => matches(o, token));
|
|
28
|
+
if (adjustments.length === 0)
|
|
29
|
+
return token;
|
|
30
|
+
const currentRaw = BigInt(Math.round((_a = token.rawBalance) !== null && _a !== void 0 ? _a : 0));
|
|
31
|
+
const totalAdjustment = adjustments.reduce((sum, o) => sum + o.amount, BigInt(0));
|
|
32
|
+
const newRaw = currentRaw > totalAdjustment ? currentRaw - totalAdjustment : BigInt(0);
|
|
33
|
+
// Ratio-scale display-units balance + fiat so they stay consistent
|
|
34
|
+
// with the optimistic raw value. `currentRaw === 0n` happens when
|
|
35
|
+
// the server feed has already caught up to zero but reconciliation
|
|
36
|
+
// hasn't fired yet; treat ratio as 0 to keep display at 0.
|
|
37
|
+
const ratio = currentRaw > BigInt(0) ? Number(newRaw) / Number(currentRaw) : 0;
|
|
38
|
+
return Object.assign(Object.assign({}, token), { balance: ((_b = token.balance) !== null && _b !== void 0 ? _b : 0) * ratio, marketValue: token.marketValue !== undefined ? token.marketValue * ratio : undefined, rawBalance: Number(newRaw), rawBalanceString: newRaw.toString() });
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Apply optimistic shield additions to the shielded balance list. For
|
|
43
|
+
* each optimistic shield with a matching `(address, isNative)` entry
|
|
44
|
+
* already in `shielded`, bump that entry; otherwise prepend a fresh
|
|
45
|
+
* synthesized row built from the entry's captured metadata.
|
|
46
|
+
*
|
|
47
|
+
* Returns a fresh array only when at least one optimistic shield
|
|
48
|
+
* applies. When `optimisticShields` is empty we pass through the
|
|
49
|
+
* input array by reference so consumers that compare by identity
|
|
50
|
+
* don't churn.
|
|
51
|
+
*/
|
|
52
|
+
const applyOptimisticShieldedAdditions = (shielded, optimisticShields) => {
|
|
53
|
+
if (optimisticShields.length === 0)
|
|
54
|
+
return shielded;
|
|
55
|
+
const consumedIndices = new Set();
|
|
56
|
+
const merged = shielded.map((token) => {
|
|
57
|
+
var _a, _b;
|
|
58
|
+
const adjustments = optimisticShields.filter((o, idx) => {
|
|
59
|
+
if (!matches(o, token))
|
|
60
|
+
return false;
|
|
61
|
+
consumedIndices.add(idx);
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
if (adjustments.length === 0)
|
|
65
|
+
return token;
|
|
66
|
+
const totalAtomic = adjustments.reduce((sum, o) => sum + o.amount, BigInt(0));
|
|
67
|
+
const newRaw = BigInt(Math.round((_a = token.rawBalance) !== null && _a !== void 0 ? _a : 0)) + totalAtomic;
|
|
68
|
+
const decimals = (_b = token.decimals) !== null && _b !== void 0 ? _b : 0;
|
|
69
|
+
const denominator = Math.pow(10, decimals);
|
|
70
|
+
const newDisplay = Number(newRaw) / denominator;
|
|
71
|
+
return Object.assign(Object.assign({}, token), { balance: newDisplay, marketValue: token.price !== undefined
|
|
72
|
+
? newDisplay * token.price
|
|
73
|
+
: token.marketValue, rawBalance: Number(newRaw), rawBalanceString: newRaw.toString() });
|
|
74
|
+
});
|
|
75
|
+
// Prepend synthesized rows for any optimistic shield that didn't
|
|
76
|
+
// fold into an existing shielded row above. New rows go to the top
|
|
77
|
+
// of the list so the just-shielded token is immediately visible.
|
|
78
|
+
const synthesized = [];
|
|
79
|
+
optimisticShields.forEach((entry, idx) => {
|
|
80
|
+
if (consumedIndices.has(idx))
|
|
81
|
+
return;
|
|
82
|
+
const denominator = Math.pow(10, entry.decimals);
|
|
83
|
+
const display = Number(entry.amount) / denominator;
|
|
84
|
+
const marketValue = entry.price !== undefined ? display * entry.price : undefined;
|
|
85
|
+
synthesized.push(Object.assign(Object.assign({ address: entry.tokenAddress, balance: display, decimals: entry.decimals, isNative: entry.isNative, logoURI: entry.logoURI, name: entry.name, rawBalance: Number(entry.amount), rawBalanceString: entry.amount.toString(), symbol: entry.symbol }, (entry.price !== undefined ? { price: entry.price } : {})), (marketValue !== undefined ? { marketValue } : {})));
|
|
86
|
+
});
|
|
87
|
+
return [...synthesized, ...merged];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Filter the optimistic shield list down to entries that are still
|
|
91
|
+
* relevant. An entry is dropped when any of:
|
|
92
|
+
*
|
|
93
|
+
* - the unshielded server feed reports a `rawBalance` strictly less
|
|
94
|
+
* than `preShieldUnshieldedRaw` for that token (the burn was
|
|
95
|
+
* observed on-chain),
|
|
96
|
+
* - the shielded server feed reports a `rawBalance` strictly greater
|
|
97
|
+
* than `preShieldShieldedRaw` for that token (the new record was
|
|
98
|
+
* indexed),
|
|
99
|
+
* - the unshielded server no longer lists the token at all (rare,
|
|
100
|
+
* but means the public balance is effectively zero — our
|
|
101
|
+
* optimistic deduct is moot),
|
|
102
|
+
* - the entry is older than `staleMs` (safety net for shields whose
|
|
103
|
+
* on-chain effect is masked by parallel inbound credits).
|
|
104
|
+
*
|
|
105
|
+
* Pure / module-scope so it's trivially unit-testable; the widget
|
|
106
|
+
* just calls it on every server-side balance update.
|
|
107
|
+
*/
|
|
108
|
+
const reconcileOptimisticShields = (optimisticShields, unshieldedTokenBalances, shieldedTokenBalances, nowMs, staleMs = OPTIMISTIC_SHIELD_STALE_MS) => optimisticShields.filter((entry) => {
|
|
109
|
+
var _a, _b;
|
|
110
|
+
if (nowMs - entry.createdAtMs > staleMs)
|
|
111
|
+
return false;
|
|
112
|
+
const unshielded = unshieldedTokenBalances.find((t) => matches(entry, t));
|
|
113
|
+
if (!unshielded)
|
|
114
|
+
return false;
|
|
115
|
+
const serverUnshielded = BigInt(Math.round((_a = unshielded.rawBalance) !== null && _a !== void 0 ? _a : 0));
|
|
116
|
+
if (serverUnshielded < entry.preShieldUnshieldedRaw)
|
|
117
|
+
return false;
|
|
118
|
+
const shielded = shieldedTokenBalances.find((t) => matches(entry, t));
|
|
119
|
+
const serverShielded = shielded
|
|
120
|
+
? BigInt(Math.round((_b = shielded.rawBalance) !== null && _b !== void 0 ? _b : 0))
|
|
121
|
+
: BigInt(0);
|
|
122
|
+
if (serverShielded > entry.preShieldShieldedRaw)
|
|
123
|
+
return false;
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export { OPTIMISTIC_SHIELD_STALE_MS, applyOptimisticShieldedAdditions, applyOptimisticUnshieldedDeductions, reconcileOptimisticShields };
|