@dynamic-labs/sdk-react-core 4.81.0 → 4.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/package.cjs +2 -2
- package/package.js +2 -2
- package/package.json +13 -13
- package/src/lib/data/api/aleo/getAleoCuratedPrices.cjs +73 -0
- package/src/lib/data/api/aleo/getAleoCuratedPrices.d.ts +38 -0
- package/src/lib/data/api/aleo/getAleoCuratedPrices.js +69 -0
- package/src/lib/shared/assets/index.d.ts +2 -0
- package/src/lib/shared/assets/midnight-shielded.cjs +54 -0
- package/src/lib/shared/assets/midnight-shielded.js +30 -0
- package/src/lib/shared/assets/midnight-unshielded.cjs +54 -0
- package/src/lib/shared/assets/midnight-unshielded.js +30 -0
- package/src/lib/styles/index.shadow.cjs +1 -1
- package/src/lib/styles/index.shadow.js +1 -1
- package/src/lib/utils/functions/compareChains/compareChains.cjs +1 -0
- package/src/lib/utils/functions/compareChains/compareChains.js +1 -0
- package/src/lib/utils/functions/getTransactionLink/blockExplorerPatterns.cjs +12 -0
- package/src/lib/utils/functions/getTransactionLink/blockExplorerPatterns.js +12 -0
- package/src/lib/utils/hooks/useAleoAutoMergeRecords/index.d.ts +1 -0
- package/src/lib/utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.cjs +246 -0
- package/src/lib/utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.d.ts +17 -0
- package/src/lib/utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.js +242 -0
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/index.d.ts +1 -0
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs +263 -0
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.d.ts +59 -0
- package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js +259 -0
- package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.cjs +139 -68
- package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.js +139 -68
- package/src/lib/views/BackupUnsuccessfulView/BackupUnsuccessfulView.cjs +12 -1
- package/src/lib/views/BackupUnsuccessfulView/BackupUnsuccessfulView.js +12 -1
- package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/ActiveMidnightWalletBalance.cjs +193 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/ActiveMidnightWalletBalance.d.ts +7 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/ActiveMidnightWalletBalance.js +189 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/index.d.ts +1 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.cjs +26 -1
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.js +26 -1
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/ActiveMidnightWalletAddresses.cjs +124 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/ActiveMidnightWalletAddresses.d.ts +9 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/ActiveMidnightWalletAddresses.js +120 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/index.d.ts +1 -0
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveWalletInformation.cjs +21 -10
- package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveWalletInformation.js +22 -11
- package/src/lib/widgets/DynamicWidget/components/WalletDetailsCard/WalletDetailsCard.cjs +22 -2
- package/src/lib/widgets/DynamicWidget/components/WalletDetailsCard/WalletDetailsCard.d.ts +8 -1
- package/src/lib/widgets/DynamicWidget/components/WalletDetailsCard/WalletDetailsCard.js +23 -3
package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
6
|
+
var _tslib = require('../../../../../_virtual/_tslib.cjs');
|
|
7
|
+
var React = require('react');
|
|
8
|
+
|
|
9
|
+
// Backoff schedule for the post-shield refresh. `shieldToken` resolves
|
|
10
|
+
// when the relay accepts the broadcast, not when the public→private
|
|
11
|
+
// transition confirms on-chain. Aleo block time + RecordScanner indexer
|
|
12
|
+
// lag means the immediate post-broadcast fetch usually returns the
|
|
13
|
+
// pre-confirmation balance, so we re-poll for ~45s before giving up.
|
|
14
|
+
const REFRESH_DELAYS_MS = [
|
|
15
|
+
3000, 5000, 8000, 12000, 18000,
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Selects which of `tokens` should attempt an auto-shield on this cycle.
|
|
19
|
+
* A token is a candidate when:
|
|
20
|
+
* - it has a positive raw balance,
|
|
21
|
+
* - the registry recognises it as shieldable,
|
|
22
|
+
* - it has a usable idempotency key (address OR `isNative: true`), and
|
|
23
|
+
* - we haven't already attempted it for this `accountAddress` this
|
|
24
|
+
* session (`seenKeys`).
|
|
25
|
+
*
|
|
26
|
+
* Pure / module-scope so the effect body stays small (the cognitive
|
|
27
|
+
* complexity counter doesn't have to walk into the predicate).
|
|
28
|
+
*/
|
|
29
|
+
const buildShieldCandidates = (args) => {
|
|
30
|
+
const { tokens, accountAddress, shieldHandle, seenKeys } = args;
|
|
31
|
+
return tokens.filter((token) => {
|
|
32
|
+
var _a;
|
|
33
|
+
if (!token.rawBalance || token.rawBalance <= 0)
|
|
34
|
+
return false;
|
|
35
|
+
if (!shieldHandle.canShieldToken({
|
|
36
|
+
address: token.address,
|
|
37
|
+
isNative: token.isNative,
|
|
38
|
+
})) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const tokenKey = (_a = token.address) !== null && _a !== void 0 ? _a : (token.isNative ? '__native__' : '');
|
|
42
|
+
if (!tokenKey)
|
|
43
|
+
return false;
|
|
44
|
+
return !seenKeys.has(`${accountAddress}:${tokenKey}`);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Attempt to auto-shield a single token. Returns `true` only if the shield
|
|
49
|
+
* ceremony broadcast successfully. On any short-circuit (sponsorship miss,
|
|
50
|
+
* zero atomic units, dispatch error) the seen-key is removed so the next
|
|
51
|
+
* refresh can retry. Cancellation is checked via the `isCancelled` thunk
|
|
52
|
+
* so the effect-cleanup `cancelled` flag stays in scope.
|
|
53
|
+
*/
|
|
54
|
+
const tryShieldOneToken = (token, deps) => _tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
var _a, _b, _c, _d;
|
|
56
|
+
const { accountAddress, shieldHandle, seenKeys, isCancelled } = deps;
|
|
57
|
+
const tokenKey = (_a = token.address) !== null && _a !== void 0 ? _a : '__native__';
|
|
58
|
+
const key = `${accountAddress}:${tokenKey}`;
|
|
59
|
+
seenKeys.add(key);
|
|
60
|
+
let sponsored = false;
|
|
61
|
+
try {
|
|
62
|
+
sponsored =
|
|
63
|
+
(_c = (yield ((_b = shieldHandle.isShieldSponsored) === null || _b === void 0 ? void 0 : _b.call(shieldHandle, {
|
|
64
|
+
address: token.address,
|
|
65
|
+
isNative: token.isNative,
|
|
66
|
+
})))) !== null && _c !== void 0 ? _c : false;
|
|
67
|
+
}
|
|
68
|
+
catch (_e) {
|
|
69
|
+
sponsored = false;
|
|
70
|
+
}
|
|
71
|
+
if (isCancelled())
|
|
72
|
+
return false;
|
|
73
|
+
if (!sponsored) {
|
|
74
|
+
seenKeys.delete(key);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const atomic = BigInt(Math.round((_d = token.rawBalance) !== null && _d !== void 0 ? _d : 0));
|
|
78
|
+
if (atomic <= BigInt(0)) {
|
|
79
|
+
seenKeys.delete(key);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
yield shieldHandle.shieldToken({
|
|
84
|
+
amount: atomic,
|
|
85
|
+
isNative: token.isNative,
|
|
86
|
+
tokenAddress: token.address,
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (_f) {
|
|
91
|
+
seenKeys.delete(key);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* After a successful shield broadcast, poll `onShielded` on a backoff
|
|
97
|
+
* schedule so the consumer sees both the shielded and unshielded balances
|
|
98
|
+
* update without a manual refresh click. Stops early on cancel; swallows
|
|
99
|
+
* per-iteration errors (refresh is best-effort).
|
|
100
|
+
*/
|
|
101
|
+
const pollOnShielded = (onShielded, isCancelled) => _tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
+
for (const delay of REFRESH_DELAYS_MS) {
|
|
103
|
+
if (isCancelled())
|
|
104
|
+
return;
|
|
105
|
+
yield new Promise((resolve) => setTimeout(resolve, delay));
|
|
106
|
+
if (isCancelled())
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
yield onShielded();
|
|
110
|
+
}
|
|
111
|
+
catch (_g) {
|
|
112
|
+
/* swallow — refresh is best-effort */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalances, shieldHandle, onShielded, }) => {
|
|
117
|
+
// Tracks `(accountAddress:tokenAddress)` we've already attempted in this
|
|
118
|
+
// session. Cleared implicitly on a full page reload.
|
|
119
|
+
const seenKeysRef = React.useRef(new Set());
|
|
120
|
+
const [isShielding, setIsShielding] = React.useState(false);
|
|
121
|
+
// Counter of in-flight `run()` promises. Each effect-run that has at
|
|
122
|
+
// least one candidate increments on entry and decrements in `.finally()`.
|
|
123
|
+
// Without this counter, a stale `run()` resolving after a re-fire could
|
|
124
|
+
// flip `isShielding` to `false` while a *new* `run()` is still active —
|
|
125
|
+
// the indicator would blink off mid-shield, and the empty-candidates
|
|
126
|
+
// path of a new effect-run would also wrongly clear the indicator
|
|
127
|
+
// belonging to a still-running ceremony. We only call `setIsShielding(false)`
|
|
128
|
+
// when the counter reaches `0`.
|
|
129
|
+
const activeRunsRef = React.useRef(0);
|
|
130
|
+
// Tracks component-lifetime mount state. The per-effect `cancelled`
|
|
131
|
+
// variable signals "this particular run is no longer relevant", but
|
|
132
|
+
// it gets flipped to `true` whenever deps change (e.g. when an
|
|
133
|
+
// `onShielded` balance refresh updates `unshieldedTokenBalances`).
|
|
134
|
+
// Using `cancelled` to gate state cleanup would leave `isShielding`
|
|
135
|
+
// stuck at `true` because the cancelled run's `.finally()` skips the
|
|
136
|
+
// setState. `mountedRef` only flips on actual unmount.
|
|
137
|
+
//
|
|
138
|
+
// Important: refs persist across React 18 Strict Mode's fake
|
|
139
|
+
// unmount/re-mount cycle in dev. We MUST set `mountedRef.current =
|
|
140
|
+
// true` at the start of the mount effect body (not just at
|
|
141
|
+
// `useRef(true)` init), or Strict Mode's first cleanup leaves the
|
|
142
|
+
// ref `false` for the entire second mount, and `setIsShielding(false)`
|
|
143
|
+
// is permanently skipped.
|
|
144
|
+
const mountedRef = React.useRef(true);
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
mountedRef.current = true;
|
|
147
|
+
return () => {
|
|
148
|
+
mountedRef.current = false;
|
|
149
|
+
};
|
|
150
|
+
}, []);
|
|
151
|
+
// Content fingerprint for the balance list. We re-run the effect only
|
|
152
|
+
// when balances *meaningfully* change (token added/removed/balance
|
|
153
|
+
// shifted), not on every render where the parent passes a new array
|
|
154
|
+
// reference. Without this, parent re-renders re-fire the effect, the
|
|
155
|
+
// cleanup sets `cancelled = true` on the in-flight run, and the
|
|
156
|
+
// shieldToken dispatch never happens after the slow `isShieldSponsored`
|
|
157
|
+
// network call resolves.
|
|
158
|
+
const balanceFingerprint = React.useMemo(() => (unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [])
|
|
159
|
+
.map((t) => {
|
|
160
|
+
var _a, _b;
|
|
161
|
+
return `${(_a = t.address) !== null && _a !== void 0 ? _a : '__native__'}|${t.isNative ? 1 : 0}|${(_b = t.rawBalance) !== null && _b !== void 0 ? _b : 0}`;
|
|
162
|
+
})
|
|
163
|
+
// Use `localeCompare` so the sort is locale-aware and stable across
|
|
164
|
+
// JS engines (default `Array.prototype.sort` on strings is
|
|
165
|
+
// implementation-defined for non-ASCII input — `localeCompare` is
|
|
166
|
+
// the spec-mandated comparator for human-meaningful ordering).
|
|
167
|
+
.sort((a, b) => a.localeCompare(b))
|
|
168
|
+
.join(','), [unshieldedTokenBalances]);
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
if (!accountAddress || !shieldHandle)
|
|
171
|
+
return;
|
|
172
|
+
if (!unshieldedTokenBalances || unshieldedTokenBalances.length === 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (typeof shieldHandle.isShieldSponsored !== 'function')
|
|
176
|
+
return;
|
|
177
|
+
let cancelled = false;
|
|
178
|
+
const isCancelled = () => cancelled;
|
|
179
|
+
const candidates = buildShieldCandidates({
|
|
180
|
+
accountAddress,
|
|
181
|
+
seenKeys: seenKeysRef.current,
|
|
182
|
+
shieldHandle,
|
|
183
|
+
tokens: unshieldedTokenBalances,
|
|
184
|
+
});
|
|
185
|
+
if (candidates.length === 0) {
|
|
186
|
+
// No candidates this cycle — only clear the indicator if no other
|
|
187
|
+
// run is still in flight. Without the counter check, a re-fire
|
|
188
|
+
// whose cleanup cancelled the previous run would clobber the
|
|
189
|
+
// still-active in-flight indicator owned by a different effect-run
|
|
190
|
+
// (which can happen when deps churn during a ceremony).
|
|
191
|
+
if (mountedRef.current && activeRunsRef.current === 0) {
|
|
192
|
+
setIsShielding(false);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Flip the indicator on synchronously inside the effect so React
|
|
197
|
+
// groups it with the initial render commit (the call site is
|
|
198
|
+
// inherently `act()`-wrapped by `renderHook` / the user-event flow
|
|
199
|
+
// that drove the props change). Setting it inside the async `run()`
|
|
200
|
+
// fires the state update in a microtask AFTER the effect returns,
|
|
201
|
+
// which trips React 18's "update not wrapped in act(...)" warning
|
|
202
|
+
// that the test setup converts to a failure.
|
|
203
|
+
if (mountedRef.current)
|
|
204
|
+
setIsShielding(true);
|
|
205
|
+
activeRunsRef.current += 1;
|
|
206
|
+
const run = () => _tslib.__awaiter(void 0, void 0, void 0, function* () {
|
|
207
|
+
let didShield = false;
|
|
208
|
+
for (const token of candidates) {
|
|
209
|
+
if (cancelled)
|
|
210
|
+
return;
|
|
211
|
+
const ok = yield tryShieldOneToken(token, {
|
|
212
|
+
accountAddress,
|
|
213
|
+
isCancelled,
|
|
214
|
+
seenKeys: seenKeysRef.current,
|
|
215
|
+
shieldHandle,
|
|
216
|
+
});
|
|
217
|
+
if (ok)
|
|
218
|
+
didShield = true;
|
|
219
|
+
}
|
|
220
|
+
if (didShield && !cancelled && onShielded) {
|
|
221
|
+
yield pollOnShielded(onShielded, isCancelled);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
run()
|
|
225
|
+
.catch(() => {
|
|
226
|
+
/* swallow — see method jsdoc */
|
|
227
|
+
})
|
|
228
|
+
.finally(() => {
|
|
229
|
+
// Decrement the in-flight counter. Only clear the indicator if
|
|
230
|
+
// this was the LAST active run — otherwise an overlapping run
|
|
231
|
+
// (started by a re-fire while this one was mid-shield) is still
|
|
232
|
+
// shielding and the UI should keep showing "Auto-shielding…".
|
|
233
|
+
activeRunsRef.current = Math.max(0, activeRunsRef.current - 1);
|
|
234
|
+
if (!mountedRef.current)
|
|
235
|
+
return;
|
|
236
|
+
if (activeRunsRef.current > 0)
|
|
237
|
+
return;
|
|
238
|
+
// Defer the setState by one macrotask via `setTimeout(0)`. In
|
|
239
|
+
// production the 0ms delay is imperceptible. In tests it lets the
|
|
240
|
+
// framework's afterEach `act()` drain wrap the trailing state
|
|
241
|
+
// update, instead of firing it inside the synchronous microtask
|
|
242
|
+
// drain of a `waitFor` poll where it sits OUTSIDE any act region
|
|
243
|
+
// and trips React 18's "update not wrapped in act" warning gate.
|
|
244
|
+
// We re-check the counter inside the timer callback because a
|
|
245
|
+
// fresh effect-run could have incremented it during the 0ms gap.
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
if (mountedRef.current && activeRunsRef.current === 0) {
|
|
248
|
+
setIsShielding(false);
|
|
249
|
+
}
|
|
250
|
+
}, 0);
|
|
251
|
+
});
|
|
252
|
+
return () => {
|
|
253
|
+
cancelled = true;
|
|
254
|
+
};
|
|
255
|
+
// `unshieldedTokenBalances` is read inside the effect but tracked via
|
|
256
|
+
// `balanceFingerprint` so reference-only changes don't re-fire and
|
|
257
|
+
// cancel an in-flight shield ceremony.
|
|
258
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
|
+
}, [accountAddress, balanceFingerprint, shieldHandle, onShielded]);
|
|
260
|
+
return { isShielding };
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
exports.useAleoAutoShieldSponsoredTokens = useAleoAutoShieldSponsoredTokens;
|
package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TokenBalance } from '@dynamic-labs/sdk-api-core';
|
|
2
|
+
/**
|
|
3
|
+
* Connector duck-type — matches the `aleoShieldHandle` shape that
|
|
4
|
+
* `ActiveWalletBalance` already uses for the Shield Manually CTA. Kept
|
|
5
|
+
* here as a minimal local type so we don't pull a hard dependency on
|
|
6
|
+
* `@dynamic-labs/aleo` into sdk-react-core.
|
|
7
|
+
*/
|
|
8
|
+
type AleoShieldHandle = {
|
|
9
|
+
canShieldToken: (token: {
|
|
10
|
+
address?: string;
|
|
11
|
+
isNative?: boolean;
|
|
12
|
+
}) => boolean;
|
|
13
|
+
shieldToken: (args: {
|
|
14
|
+
tokenAddress: string;
|
|
15
|
+
isNative?: boolean;
|
|
16
|
+
amount: bigint;
|
|
17
|
+
}) => Promise<string>;
|
|
18
|
+
isShieldSponsored?: (token: {
|
|
19
|
+
address?: string;
|
|
20
|
+
isNative?: boolean;
|
|
21
|
+
}) => Promise<boolean>;
|
|
22
|
+
};
|
|
23
|
+
type UseAleoAutoShieldSponsoredTokensArgs = {
|
|
24
|
+
accountAddress: string | undefined;
|
|
25
|
+
unshieldedTokenBalances: TokenBalance[];
|
|
26
|
+
shieldHandle: AleoShieldHandle | undefined;
|
|
27
|
+
onShielded?: () => Promise<void> | void;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* On Aleo wallet load (and on each subsequent unshielded-balance refresh),
|
|
31
|
+
* auto-shields tokens whose `transfer_public_to_private` transition the
|
|
32
|
+
* Aleo Feemaster currently sponsors. Tokens that aren't sponsored stay
|
|
33
|
+
* untouched — the user can still trigger them manually via the existing
|
|
34
|
+
* Shield Manually CTA, which dispatches with user-paid fees.
|
|
35
|
+
*
|
|
36
|
+
* Idempotency is per-session, keyed by `(accountAddress, tokenAddress)`.
|
|
37
|
+
* On error the key is removed so the next refresh can retry. Auto-shield
|
|
38
|
+
* runs sequentially so a single sponsored quota can't be drained by N
|
|
39
|
+
* concurrent dispatches; the caller's "single shield in flight" UX is
|
|
40
|
+
* preserved.
|
|
41
|
+
*
|
|
42
|
+
* Failures are swallowed by design — auto-shield is a background
|
|
43
|
+
* optimisation, never a user-facing flow.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Return value of `useAleoAutoShieldSponsoredTokens`. Consumers can read
|
|
47
|
+
* `isShielding` to render a small "Shielding…" indicator while a
|
|
48
|
+
* background ceremony is in flight.
|
|
49
|
+
*/
|
|
50
|
+
export type UseAleoAutoShieldSponsoredTokensResult = {
|
|
51
|
+
/**
|
|
52
|
+
* `true` while at least one auto-shield ceremony is broadcasting
|
|
53
|
+
* (between candidate selection and the post-success refresh callback).
|
|
54
|
+
* Goes back to `false` on cleanup or once the run resolves.
|
|
55
|
+
*/
|
|
56
|
+
isShielding: boolean;
|
|
57
|
+
};
|
|
58
|
+
export declare const useAleoAutoShieldSponsoredTokens: ({ accountAddress, unshieldedTokenBalances, shieldHandle, onShielded, }: UseAleoAutoShieldSponsoredTokensArgs) => UseAleoAutoShieldSponsoredTokensResult;
|
|
59
|
+
export {};
|
package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { __awaiter } from '../../../../../_virtual/_tslib.js';
|
|
3
|
+
import { useRef, useState, useEffect, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
// Backoff schedule for the post-shield refresh. `shieldToken` resolves
|
|
6
|
+
// when the relay accepts the broadcast, not when the public→private
|
|
7
|
+
// transition confirms on-chain. Aleo block time + RecordScanner indexer
|
|
8
|
+
// lag means the immediate post-broadcast fetch usually returns the
|
|
9
|
+
// pre-confirmation balance, so we re-poll for ~45s before giving up.
|
|
10
|
+
const REFRESH_DELAYS_MS = [
|
|
11
|
+
3000, 5000, 8000, 12000, 18000,
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Selects which of `tokens` should attempt an auto-shield on this cycle.
|
|
15
|
+
* A token is a candidate when:
|
|
16
|
+
* - it has a positive raw balance,
|
|
17
|
+
* - the registry recognises it as shieldable,
|
|
18
|
+
* - it has a usable idempotency key (address OR `isNative: true`), and
|
|
19
|
+
* - we haven't already attempted it for this `accountAddress` this
|
|
20
|
+
* session (`seenKeys`).
|
|
21
|
+
*
|
|
22
|
+
* Pure / module-scope so the effect body stays small (the cognitive
|
|
23
|
+
* complexity counter doesn't have to walk into the predicate).
|
|
24
|
+
*/
|
|
25
|
+
const buildShieldCandidates = (args) => {
|
|
26
|
+
const { tokens, accountAddress, shieldHandle, seenKeys } = args;
|
|
27
|
+
return tokens.filter((token) => {
|
|
28
|
+
var _a;
|
|
29
|
+
if (!token.rawBalance || token.rawBalance <= 0)
|
|
30
|
+
return false;
|
|
31
|
+
if (!shieldHandle.canShieldToken({
|
|
32
|
+
address: token.address,
|
|
33
|
+
isNative: token.isNative,
|
|
34
|
+
})) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const tokenKey = (_a = token.address) !== null && _a !== void 0 ? _a : (token.isNative ? '__native__' : '');
|
|
38
|
+
if (!tokenKey)
|
|
39
|
+
return false;
|
|
40
|
+
return !seenKeys.has(`${accountAddress}:${tokenKey}`);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Attempt to auto-shield a single token. Returns `true` only if the shield
|
|
45
|
+
* ceremony broadcast successfully. On any short-circuit (sponsorship miss,
|
|
46
|
+
* zero atomic units, dispatch error) the seen-key is removed so the next
|
|
47
|
+
* refresh can retry. Cancellation is checked via the `isCancelled` thunk
|
|
48
|
+
* so the effect-cleanup `cancelled` flag stays in scope.
|
|
49
|
+
*/
|
|
50
|
+
const tryShieldOneToken = (token, deps) => __awaiter(void 0, void 0, void 0, function* () {
|
|
51
|
+
var _a, _b, _c, _d;
|
|
52
|
+
const { accountAddress, shieldHandle, seenKeys, isCancelled } = deps;
|
|
53
|
+
const tokenKey = (_a = token.address) !== null && _a !== void 0 ? _a : '__native__';
|
|
54
|
+
const key = `${accountAddress}:${tokenKey}`;
|
|
55
|
+
seenKeys.add(key);
|
|
56
|
+
let sponsored = false;
|
|
57
|
+
try {
|
|
58
|
+
sponsored =
|
|
59
|
+
(_c = (yield ((_b = shieldHandle.isShieldSponsored) === null || _b === void 0 ? void 0 : _b.call(shieldHandle, {
|
|
60
|
+
address: token.address,
|
|
61
|
+
isNative: token.isNative,
|
|
62
|
+
})))) !== null && _c !== void 0 ? _c : false;
|
|
63
|
+
}
|
|
64
|
+
catch (_e) {
|
|
65
|
+
sponsored = false;
|
|
66
|
+
}
|
|
67
|
+
if (isCancelled())
|
|
68
|
+
return false;
|
|
69
|
+
if (!sponsored) {
|
|
70
|
+
seenKeys.delete(key);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const atomic = BigInt(Math.round((_d = token.rawBalance) !== null && _d !== void 0 ? _d : 0));
|
|
74
|
+
if (atomic <= BigInt(0)) {
|
|
75
|
+
seenKeys.delete(key);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
yield shieldHandle.shieldToken({
|
|
80
|
+
amount: atomic,
|
|
81
|
+
isNative: token.isNative,
|
|
82
|
+
tokenAddress: token.address,
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch (_f) {
|
|
87
|
+
seenKeys.delete(key);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* After a successful shield broadcast, poll `onShielded` on a backoff
|
|
93
|
+
* schedule so the consumer sees both the shielded and unshielded balances
|
|
94
|
+
* update without a manual refresh click. Stops early on cancel; swallows
|
|
95
|
+
* per-iteration errors (refresh is best-effort).
|
|
96
|
+
*/
|
|
97
|
+
const pollOnShielded = (onShielded, isCancelled) => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
for (const delay of REFRESH_DELAYS_MS) {
|
|
99
|
+
if (isCancelled())
|
|
100
|
+
return;
|
|
101
|
+
yield new Promise((resolve) => setTimeout(resolve, delay));
|
|
102
|
+
if (isCancelled())
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
yield onShielded();
|
|
106
|
+
}
|
|
107
|
+
catch (_g) {
|
|
108
|
+
/* swallow — refresh is best-effort */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalances, shieldHandle, onShielded, }) => {
|
|
113
|
+
// Tracks `(accountAddress:tokenAddress)` we've already attempted in this
|
|
114
|
+
// session. Cleared implicitly on a full page reload.
|
|
115
|
+
const seenKeysRef = useRef(new Set());
|
|
116
|
+
const [isShielding, setIsShielding] = useState(false);
|
|
117
|
+
// Counter of in-flight `run()` promises. Each effect-run that has at
|
|
118
|
+
// least one candidate increments on entry and decrements in `.finally()`.
|
|
119
|
+
// Without this counter, a stale `run()` resolving after a re-fire could
|
|
120
|
+
// flip `isShielding` to `false` while a *new* `run()` is still active —
|
|
121
|
+
// the indicator would blink off mid-shield, and the empty-candidates
|
|
122
|
+
// path of a new effect-run would also wrongly clear the indicator
|
|
123
|
+
// belonging to a still-running ceremony. We only call `setIsShielding(false)`
|
|
124
|
+
// when the counter reaches `0`.
|
|
125
|
+
const activeRunsRef = useRef(0);
|
|
126
|
+
// Tracks component-lifetime mount state. The per-effect `cancelled`
|
|
127
|
+
// variable signals "this particular run is no longer relevant", but
|
|
128
|
+
// it gets flipped to `true` whenever deps change (e.g. when an
|
|
129
|
+
// `onShielded` balance refresh updates `unshieldedTokenBalances`).
|
|
130
|
+
// Using `cancelled` to gate state cleanup would leave `isShielding`
|
|
131
|
+
// stuck at `true` because the cancelled run's `.finally()` skips the
|
|
132
|
+
// setState. `mountedRef` only flips on actual unmount.
|
|
133
|
+
//
|
|
134
|
+
// Important: refs persist across React 18 Strict Mode's fake
|
|
135
|
+
// unmount/re-mount cycle in dev. We MUST set `mountedRef.current =
|
|
136
|
+
// true` at the start of the mount effect body (not just at
|
|
137
|
+
// `useRef(true)` init), or Strict Mode's first cleanup leaves the
|
|
138
|
+
// ref `false` for the entire second mount, and `setIsShielding(false)`
|
|
139
|
+
// is permanently skipped.
|
|
140
|
+
const mountedRef = useRef(true);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
mountedRef.current = true;
|
|
143
|
+
return () => {
|
|
144
|
+
mountedRef.current = false;
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
// Content fingerprint for the balance list. We re-run the effect only
|
|
148
|
+
// when balances *meaningfully* change (token added/removed/balance
|
|
149
|
+
// shifted), not on every render where the parent passes a new array
|
|
150
|
+
// reference. Without this, parent re-renders re-fire the effect, the
|
|
151
|
+
// cleanup sets `cancelled = true` on the in-flight run, and the
|
|
152
|
+
// shieldToken dispatch never happens after the slow `isShieldSponsored`
|
|
153
|
+
// network call resolves.
|
|
154
|
+
const balanceFingerprint = useMemo(() => (unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [])
|
|
155
|
+
.map((t) => {
|
|
156
|
+
var _a, _b;
|
|
157
|
+
return `${(_a = t.address) !== null && _a !== void 0 ? _a : '__native__'}|${t.isNative ? 1 : 0}|${(_b = t.rawBalance) !== null && _b !== void 0 ? _b : 0}`;
|
|
158
|
+
})
|
|
159
|
+
// Use `localeCompare` so the sort is locale-aware and stable across
|
|
160
|
+
// JS engines (default `Array.prototype.sort` on strings is
|
|
161
|
+
// implementation-defined for non-ASCII input — `localeCompare` is
|
|
162
|
+
// the spec-mandated comparator for human-meaningful ordering).
|
|
163
|
+
.sort((a, b) => a.localeCompare(b))
|
|
164
|
+
.join(','), [unshieldedTokenBalances]);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!accountAddress || !shieldHandle)
|
|
167
|
+
return;
|
|
168
|
+
if (!unshieldedTokenBalances || unshieldedTokenBalances.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (typeof shieldHandle.isShieldSponsored !== 'function')
|
|
172
|
+
return;
|
|
173
|
+
let cancelled = false;
|
|
174
|
+
const isCancelled = () => cancelled;
|
|
175
|
+
const candidates = buildShieldCandidates({
|
|
176
|
+
accountAddress,
|
|
177
|
+
seenKeys: seenKeysRef.current,
|
|
178
|
+
shieldHandle,
|
|
179
|
+
tokens: unshieldedTokenBalances,
|
|
180
|
+
});
|
|
181
|
+
if (candidates.length === 0) {
|
|
182
|
+
// No candidates this cycle — only clear the indicator if no other
|
|
183
|
+
// run is still in flight. Without the counter check, a re-fire
|
|
184
|
+
// whose cleanup cancelled the previous run would clobber the
|
|
185
|
+
// still-active in-flight indicator owned by a different effect-run
|
|
186
|
+
// (which can happen when deps churn during a ceremony).
|
|
187
|
+
if (mountedRef.current && activeRunsRef.current === 0) {
|
|
188
|
+
setIsShielding(false);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Flip the indicator on synchronously inside the effect so React
|
|
193
|
+
// groups it with the initial render commit (the call site is
|
|
194
|
+
// inherently `act()`-wrapped by `renderHook` / the user-event flow
|
|
195
|
+
// that drove the props change). Setting it inside the async `run()`
|
|
196
|
+
// fires the state update in a microtask AFTER the effect returns,
|
|
197
|
+
// which trips React 18's "update not wrapped in act(...)" warning
|
|
198
|
+
// that the test setup converts to a failure.
|
|
199
|
+
if (mountedRef.current)
|
|
200
|
+
setIsShielding(true);
|
|
201
|
+
activeRunsRef.current += 1;
|
|
202
|
+
const run = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
let didShield = false;
|
|
204
|
+
for (const token of candidates) {
|
|
205
|
+
if (cancelled)
|
|
206
|
+
return;
|
|
207
|
+
const ok = yield tryShieldOneToken(token, {
|
|
208
|
+
accountAddress,
|
|
209
|
+
isCancelled,
|
|
210
|
+
seenKeys: seenKeysRef.current,
|
|
211
|
+
shieldHandle,
|
|
212
|
+
});
|
|
213
|
+
if (ok)
|
|
214
|
+
didShield = true;
|
|
215
|
+
}
|
|
216
|
+
if (didShield && !cancelled && onShielded) {
|
|
217
|
+
yield pollOnShielded(onShielded, isCancelled);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
run()
|
|
221
|
+
.catch(() => {
|
|
222
|
+
/* swallow — see method jsdoc */
|
|
223
|
+
})
|
|
224
|
+
.finally(() => {
|
|
225
|
+
// Decrement the in-flight counter. Only clear the indicator if
|
|
226
|
+
// this was the LAST active run — otherwise an overlapping run
|
|
227
|
+
// (started by a re-fire while this one was mid-shield) is still
|
|
228
|
+
// shielding and the UI should keep showing "Auto-shielding…".
|
|
229
|
+
activeRunsRef.current = Math.max(0, activeRunsRef.current - 1);
|
|
230
|
+
if (!mountedRef.current)
|
|
231
|
+
return;
|
|
232
|
+
if (activeRunsRef.current > 0)
|
|
233
|
+
return;
|
|
234
|
+
// Defer the setState by one macrotask via `setTimeout(0)`. In
|
|
235
|
+
// production the 0ms delay is imperceptible. In tests it lets the
|
|
236
|
+
// framework's afterEach `act()` drain wrap the trailing state
|
|
237
|
+
// update, instead of firing it inside the synchronous microtask
|
|
238
|
+
// drain of a `waitFor` poll where it sits OUTSIDE any act region
|
|
239
|
+
// and trips React 18's "update not wrapped in act" warning gate.
|
|
240
|
+
// We re-check the counter inside the timer callback because a
|
|
241
|
+
// fresh effect-run could have incremented it during the 0ms gap.
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
if (mountedRef.current && activeRunsRef.current === 0) {
|
|
244
|
+
setIsShielding(false);
|
|
245
|
+
}
|
|
246
|
+
}, 0);
|
|
247
|
+
});
|
|
248
|
+
return () => {
|
|
249
|
+
cancelled = true;
|
|
250
|
+
};
|
|
251
|
+
// `unshieldedTokenBalances` is read inside the effect but tracked via
|
|
252
|
+
// `balanceFingerprint` so reference-only changes don't re-fire and
|
|
253
|
+
// cancel an in-flight shield ceremony.
|
|
254
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
255
|
+
}, [accountAddress, balanceFingerprint, shieldHandle, onShielded]);
|
|
256
|
+
return { isShielding };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export { useAleoAutoShieldSponsoredTokens };
|