@dynamic-labs/sdk-react-core 4.80.0 → 4.82.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.cjs +1 -1
  3. package/package.js +1 -1
  4. package/package.json +12 -12
  5. package/src/lib/components/SendBalanceForm/SendBalanceForm.cjs +63 -3
  6. package/src/lib/components/SendBalanceForm/SendBalanceForm.js +63 -3
  7. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/TransactionModeSegmentedControl.cjs +40 -0
  8. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/TransactionModeSegmentedControl.d.ts +16 -0
  9. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/TransactionModeSegmentedControl.js +36 -0
  10. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/icons.cjs +17 -0
  11. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/icons.d.ts +8 -0
  12. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/icons.js +12 -0
  13. package/src/lib/components/SendBalanceForm/TransactionModeSegmentedControl/index.d.ts +1 -0
  14. package/src/lib/data/api/aleo/getAleoCuratedPrices.cjs +73 -0
  15. package/src/lib/data/api/aleo/getAleoCuratedPrices.d.ts +38 -0
  16. package/src/lib/data/api/aleo/getAleoCuratedPrices.js +69 -0
  17. package/src/lib/shared/assets/index.d.ts +2 -0
  18. package/src/lib/shared/assets/midnight-shielded.cjs +54 -0
  19. package/src/lib/shared/assets/midnight-shielded.js +30 -0
  20. package/src/lib/shared/assets/midnight-unshielded.cjs +54 -0
  21. package/src/lib/shared/assets/midnight-unshielded.js +30 -0
  22. package/src/lib/styles/index.shadow.cjs +1 -1
  23. package/src/lib/styles/index.shadow.js +1 -1
  24. package/src/lib/utils/functions/compareChains/compareChains.cjs +1 -0
  25. package/src/lib/utils/functions/compareChains/compareChains.js +1 -0
  26. package/src/lib/utils/functions/getTransactionLink/blockExplorerPatterns.cjs +12 -0
  27. package/src/lib/utils/functions/getTransactionLink/blockExplorerPatterns.js +12 -0
  28. package/src/lib/utils/hooks/useAleoAutoMergeRecords/index.d.ts +1 -0
  29. package/src/lib/utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.cjs +246 -0
  30. package/src/lib/utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.d.ts +17 -0
  31. package/src/lib/utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.js +242 -0
  32. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/index.d.ts +1 -0
  33. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs +263 -0
  34. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.d.ts +59 -0
  35. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js +259 -0
  36. package/src/lib/utils/hooks/useAleoShieldedBalances/index.d.ts +1 -0
  37. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.cjs +443 -0
  38. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.d.ts +24 -0
  39. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.js +439 -0
  40. package/src/lib/utils/hooks/useEmbeddedWallet/useEmbeddedWallet.cjs +1 -0
  41. package/src/lib/utils/hooks/useEmbeddedWallet/useEmbeddedWallet.d.ts +1 -0
  42. package/src/lib/utils/hooks/useEmbeddedWallet/useEmbeddedWallet.js +1 -0
  43. package/src/lib/views/BackupUnsuccessfulView/BackupUnsuccessfulView.cjs +12 -1
  44. package/src/lib/views/BackupUnsuccessfulView/BackupUnsuccessfulView.js +12 -1
  45. package/src/lib/views/SendBalanceView/SendBalanceView.cjs +53 -0
  46. package/src/lib/views/SendBalanceView/SendBalanceView.js +53 -0
  47. package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/ActiveMidnightWalletBalance.cjs +193 -0
  48. package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/ActiveMidnightWalletBalance.d.ts +7 -0
  49. package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/ActiveMidnightWalletBalance.js +189 -0
  50. package/src/lib/widgets/DynamicWidget/components/ActiveMidnightWalletBalance/index.d.ts +1 -0
  51. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.cjs +216 -11
  52. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.js +216 -11
  53. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceItem/TokenBalanceItem.cjs +5 -2
  54. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceItem/TokenBalanceItem.d.ts +10 -1
  55. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceItem/TokenBalanceItem.js +5 -2
  56. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceItem/index.d.ts +1 -0
  57. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceList/TokenBalanceList.cjs +2 -2
  58. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceList/TokenBalanceList.d.ts +3 -1
  59. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/TokenBalanceList/TokenBalanceList.js +2 -2
  60. package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/ActiveMidnightWalletAddresses.cjs +124 -0
  61. package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/ActiveMidnightWalletAddresses.d.ts +9 -0
  62. package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/ActiveMidnightWalletAddresses.js +120 -0
  63. package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveMidnightWalletAddresses/index.d.ts +1 -0
  64. package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveWalletInformation.cjs +21 -10
  65. package/src/lib/widgets/DynamicWidget/components/ActiveWalletInformation/ActiveWalletInformation.js +22 -11
  66. package/src/lib/widgets/DynamicWidget/components/WalletDetailsCard/WalletDetailsCard.cjs +22 -2
  67. package/src/lib/widgets/DynamicWidget/components/WalletDetailsCard/WalletDetailsCard.d.ts +8 -1
  68. package/src/lib/widgets/DynamicWidget/components/WalletDetailsCard/WalletDetailsCard.js +23 -3
@@ -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;
@@ -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 {};
@@ -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 };
@@ -0,0 +1 @@
1
+ export { useAleoShieldedBalances } from './useAleoShieldedBalances';