@dynamic-labs/sdk-react-core 4.83.1 → 4.83.2-alpha.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 (29) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.cjs +4 -4
  3. package/package.js +4 -4
  4. package/package.json +15 -15
  5. package/src/lib/client/extension/deprecated/mfa/verifyTotpMfaDevice/verifyTotpMfaDevice.d.ts +1 -1
  6. package/src/lib/components/SendBalancePageLayout/SendBalancePageLayout.cjs +2 -0
  7. package/src/lib/components/SendBalancePageLayout/SendBalancePageLayout.js +2 -0
  8. package/src/lib/components/SendBalancePageLayout/components/TokensBalanceDropdown/TokensBalanceDropdown.cjs +1 -0
  9. package/src/lib/components/SendBalancePageLayout/components/TokensBalanceDropdown/TokensBalanceDropdown.js +1 -0
  10. package/src/lib/data/api/aleo/getAleoCuratedPrices.d.ts +9 -0
  11. package/src/lib/styles/index.shadow.cjs +1 -1
  12. package/src/lib/styles/index.shadow.js +1 -1
  13. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/buildTokenKey.cjs +18 -0
  14. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/buildTokenKey.d.ts +18 -0
  15. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/buildTokenKey.js +14 -0
  16. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/index.d.ts +2 -0
  17. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/nativeTokenKey.cjs +15 -0
  18. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/nativeTokenKey.d.ts +8 -0
  19. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/nativeTokenKey.js +11 -0
  20. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.cjs +50 -0
  21. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.d.ts +12 -0
  22. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.js +45 -0
  23. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs +48 -41
  24. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.d.ts +11 -0
  25. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js +47 -40
  26. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.cjs +43 -29
  27. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.js +43 -29
  28. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.cjs +45 -2
  29. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.js +46 -3
@@ -0,0 +1,18 @@
1
+ 'use client'
2
+ 'use strict';
3
+
4
+ Object.defineProperty(exports, '__esModule', { value: true });
5
+
6
+ var nativeTokenKey = require('./nativeTokenKey.cjs');
7
+
8
+ /**
9
+ * Per-token idempotency key used by `useAleoAutoShieldSponsoredTokens`
10
+ * to dedup auto-shield candidates and by consumers (e.g.
11
+ * `ActiveWalletBalance`) to read back which tokens are currently
12
+ * mid-broadcast via `currentlyShieldingTokenKeys`. Returns an empty
13
+ * string when neither an address nor the native flag is set so callers
14
+ * can treat the token as "no usable key" and skip it.
15
+ */
16
+ const buildTokenKey = (token) => { var _a; return (_a = token.address) !== null && _a !== void 0 ? _a : (token.isNative ? nativeTokenKey.nativeTokenKey : ''); };
17
+
18
+ exports.buildTokenKey = buildTokenKey;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Argument shape — narrowed to just what the key derivation needs so the
3
+ * helper can be called with a partial `TokenBalance` or with a raw
4
+ * `{ address, isNative }` pair from any consumer.
5
+ */
6
+ export type BuildTokenKeyArgs = {
7
+ address?: string;
8
+ isNative?: boolean;
9
+ };
10
+ /**
11
+ * Per-token idempotency key used by `useAleoAutoShieldSponsoredTokens`
12
+ * to dedup auto-shield candidates and by consumers (e.g.
13
+ * `ActiveWalletBalance`) to read back which tokens are currently
14
+ * mid-broadcast via `currentlyShieldingTokenKeys`. Returns an empty
15
+ * string when neither an address nor the native flag is set so callers
16
+ * can treat the token as "no usable key" and skip it.
17
+ */
18
+ export declare const buildTokenKey: (token: BuildTokenKeyArgs) => string;
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+ import { nativeTokenKey } from './nativeTokenKey.js';
3
+
4
+ /**
5
+ * Per-token idempotency key used by `useAleoAutoShieldSponsoredTokens`
6
+ * to dedup auto-shield candidates and by consumers (e.g.
7
+ * `ActiveWalletBalance`) to read back which tokens are currently
8
+ * mid-broadcast via `currentlyShieldingTokenKeys`. Returns an empty
9
+ * string when neither an address nor the native flag is set so callers
10
+ * can treat the token as "no usable key" and skip it.
11
+ */
12
+ const buildTokenKey = (token) => { var _a; return (_a = token.address) !== null && _a !== void 0 ? _a : (token.isNative ? nativeTokenKey : ''); };
13
+
14
+ export { buildTokenKey };
@@ -1 +1,3 @@
1
1
  export { useAleoAutoShieldSponsoredTokens } from './useAleoAutoShieldSponsoredTokens';
2
+ export type { UseAleoAutoShieldSponsoredTokensResult } from './useAleoAutoShieldSponsoredTokens';
3
+ export { pollOnShielded, REFRESH_DELAYS_MS } from './pollOnShielded';
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+ 'use strict';
3
+
4
+ Object.defineProperty(exports, '__esModule', { value: true });
5
+
6
+ /**
7
+ * Stand-in key for `credits.aleo` — the Aleo native asset has no contract
8
+ * address from redcoast's perspective, so anywhere we'd otherwise reach
9
+ * for `token.address` as the idempotency / dedup key we use this
10
+ * sentinel instead. Centralised so the hook and any consumer (e.g. the
11
+ * `Shield Manually` CTA in `ActiveWalletBalance`) read the same value.
12
+ */
13
+ const nativeTokenKey = '__native__';
14
+
15
+ exports.nativeTokenKey = nativeTokenKey;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Stand-in key for `credits.aleo` — the Aleo native asset has no contract
3
+ * address from redcoast's perspective, so anywhere we'd otherwise reach
4
+ * for `token.address` as the idempotency / dedup key we use this
5
+ * sentinel instead. Centralised so the hook and any consumer (e.g. the
6
+ * `Shield Manually` CTA in `ActiveWalletBalance`) read the same value.
7
+ */
8
+ export declare const nativeTokenKey = "__native__";
@@ -0,0 +1,11 @@
1
+ 'use client'
2
+ /**
3
+ * Stand-in key for `credits.aleo` — the Aleo native asset has no contract
4
+ * address from redcoast's perspective, so anywhere we'd otherwise reach
5
+ * for `token.address` as the idempotency / dedup key we use this
6
+ * sentinel instead. Centralised so the hook and any consumer (e.g. the
7
+ * `Shield Manually` CTA in `ActiveWalletBalance`) read the same value.
8
+ */
9
+ const nativeTokenKey = '__native__';
10
+
11
+ export { nativeTokenKey };
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+ 'use strict';
3
+
4
+ Object.defineProperty(exports, '__esModule', { value: true });
5
+
6
+ var _tslib = require('../../../../../_virtual/_tslib.cjs');
7
+
8
+ // Backoff schedule for the post-shield refresh. `shieldToken` resolves
9
+ // when the relay accepts the broadcast, not when the public→private
10
+ // transition confirms on-chain. Aleo block time + RecordScanner indexer
11
+ // lag means the immediate post-broadcast fetch usually returns the
12
+ // pre-confirmation balance, so we re-poll for ~45s before giving up.
13
+ //
14
+ // Shared between the auto-shield hook (background) and the widget's
15
+ // manual Shield Manually CTA (foreground), so both flows update the
16
+ // unshielded and shielded balances on the same cadence — previously
17
+ // the manual flow refreshed exactly once and the unshielded row
18
+ // stayed at its pre-broadcast value for the full RecordScanner lag,
19
+ // making a successful shield look like a no-op.
20
+ const REFRESH_DELAYS_MS = [
21
+ 3000, 5000, 8000, 12000, 18000,
22
+ ];
23
+ /**
24
+ * After a successful shield broadcast, poll `onShielded` on a backoff
25
+ * schedule so the consumer sees both the shielded and unshielded balances
26
+ * update without a manual refresh click. Stops early on cancel; swallows
27
+ * per-iteration errors (refresh is best-effort).
28
+ *
29
+ * `isCancelled` is a thunk so callers can hook it up to whatever liveness
30
+ * signal they have (effect cleanup `cancelled` flag, abort signal, mount
31
+ * ref, etc.) without this module having to know about React lifecycle.
32
+ */
33
+ const pollOnShielded = (onShielded, isCancelled) => _tslib.__awaiter(void 0, void 0, void 0, function* () {
34
+ for (const delay of REFRESH_DELAYS_MS) {
35
+ if (isCancelled())
36
+ return;
37
+ yield new Promise((resolve) => setTimeout(resolve, delay));
38
+ if (isCancelled())
39
+ return;
40
+ try {
41
+ yield onShielded();
42
+ }
43
+ catch (_a) {
44
+ /* swallow — refresh is best-effort */
45
+ }
46
+ }
47
+ });
48
+
49
+ exports.REFRESH_DELAYS_MS = REFRESH_DELAYS_MS;
50
+ exports.pollOnShielded = pollOnShielded;
@@ -0,0 +1,12 @@
1
+ export declare const REFRESH_DELAYS_MS: ReadonlyArray<number>;
2
+ /**
3
+ * After a successful shield broadcast, poll `onShielded` on a backoff
4
+ * schedule so the consumer sees both the shielded and unshielded balances
5
+ * update without a manual refresh click. Stops early on cancel; swallows
6
+ * per-iteration errors (refresh is best-effort).
7
+ *
8
+ * `isCancelled` is a thunk so callers can hook it up to whatever liveness
9
+ * signal they have (effect cleanup `cancelled` flag, abort signal, mount
10
+ * ref, etc.) without this module having to know about React lifecycle.
11
+ */
12
+ export declare const pollOnShielded: (onShielded: () => Promise<void> | void, isCancelled: () => boolean) => Promise<void>;
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+ import { __awaiter } from '../../../../../_virtual/_tslib.js';
3
+
4
+ // Backoff schedule for the post-shield refresh. `shieldToken` resolves
5
+ // when the relay accepts the broadcast, not when the public→private
6
+ // transition confirms on-chain. Aleo block time + RecordScanner indexer
7
+ // lag means the immediate post-broadcast fetch usually returns the
8
+ // pre-confirmation balance, so we re-poll for ~45s before giving up.
9
+ //
10
+ // Shared between the auto-shield hook (background) and the widget's
11
+ // manual Shield Manually CTA (foreground), so both flows update the
12
+ // unshielded and shielded balances on the same cadence — previously
13
+ // the manual flow refreshed exactly once and the unshielded row
14
+ // stayed at its pre-broadcast value for the full RecordScanner lag,
15
+ // making a successful shield look like a no-op.
16
+ const REFRESH_DELAYS_MS = [
17
+ 3000, 5000, 8000, 12000, 18000,
18
+ ];
19
+ /**
20
+ * After a successful shield broadcast, poll `onShielded` on a backoff
21
+ * schedule so the consumer sees both the shielded and unshielded balances
22
+ * update without a manual refresh click. Stops early on cancel; swallows
23
+ * per-iteration errors (refresh is best-effort).
24
+ *
25
+ * `isCancelled` is a thunk so callers can hook it up to whatever liveness
26
+ * signal they have (effect cleanup `cancelled` flag, abort signal, mount
27
+ * ref, etc.) without this module having to know about React lifecycle.
28
+ */
29
+ const pollOnShielded = (onShielded, isCancelled) => __awaiter(void 0, void 0, void 0, function* () {
30
+ for (const delay of REFRESH_DELAYS_MS) {
31
+ if (isCancelled())
32
+ return;
33
+ yield new Promise((resolve) => setTimeout(resolve, delay));
34
+ if (isCancelled())
35
+ return;
36
+ try {
37
+ yield onShielded();
38
+ }
39
+ catch (_a) {
40
+ /* swallow — refresh is best-effort */
41
+ }
42
+ }
43
+ });
44
+
45
+ export { REFRESH_DELAYS_MS, pollOnShielded };
@@ -5,15 +5,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
 
6
6
  var _tslib = require('../../../../../_virtual/_tslib.cjs');
7
7
  var React = require('react');
8
+ var buildTokenKey = require('./buildTokenKey.cjs');
9
+ var nativeTokenKey = require('./nativeTokenKey.cjs');
10
+ var pollOnShielded = require('./pollOnShielded.cjs');
8
11
 
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
12
  /**
18
13
  * Selects which of `tokens` should attempt an auto-shield on this cycle.
19
14
  * A token is a candidate when:
@@ -29,7 +24,6 @@ const REFRESH_DELAYS_MS = [
29
24
  const buildShieldCandidates = (args) => {
30
25
  const { tokens, accountAddress, shieldHandle, seenKeys } = args;
31
26
  return tokens.filter((token) => {
32
- var _a;
33
27
  if (!token.rawBalance || token.rawBalance <= 0)
34
28
  return false;
35
29
  if (!shieldHandle.canShieldToken({
@@ -38,7 +32,7 @@ const buildShieldCandidates = (args) => {
38
32
  })) {
39
33
  return false;
40
34
  }
41
- const tokenKey = (_a = token.address) !== null && _a !== void 0 ? _a : (token.isNative ? '__native__' : '');
35
+ const tokenKey = buildTokenKey.buildTokenKey(token);
42
36
  if (!tokenKey)
43
37
  return false;
44
38
  return !seenKeys.has(`${accountAddress}:${tokenKey}`);
@@ -52,20 +46,20 @@ const buildShieldCandidates = (args) => {
52
46
  * so the effect-cleanup `cancelled` flag stays in scope.
53
47
  */
54
48
  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__';
49
+ var _a, _b, _c;
50
+ const { accountAddress, shieldHandle, seenKeys, isCancelled, inFlightTokenKeys, } = deps;
51
+ const tokenKey = buildTokenKey.buildTokenKey(token) || nativeTokenKey.nativeTokenKey;
58
52
  const key = `${accountAddress}:${tokenKey}`;
59
53
  seenKeys.add(key);
60
54
  let sponsored = false;
61
55
  try {
62
56
  sponsored =
63
- (_c = (yield ((_b = shieldHandle.isShieldSponsored) === null || _b === void 0 ? void 0 : _b.call(shieldHandle, {
57
+ (_b = (yield ((_a = shieldHandle.isShieldSponsored) === null || _a === void 0 ? void 0 : _a.call(shieldHandle, {
64
58
  address: token.address,
65
59
  isNative: token.isNative,
66
- })))) !== null && _c !== void 0 ? _c : false;
60
+ })))) !== null && _b !== void 0 ? _b : false;
67
61
  }
68
- catch (_e) {
62
+ catch (_d) {
69
63
  sponsored = false;
70
64
  }
71
65
  if (isCancelled())
@@ -74,11 +68,12 @@ const tryShieldOneToken = (token, deps) => _tslib.__awaiter(void 0, void 0, void
74
68
  seenKeys.delete(key);
75
69
  return false;
76
70
  }
77
- const atomic = BigInt(Math.round((_d = token.rawBalance) !== null && _d !== void 0 ? _d : 0));
71
+ const atomic = BigInt(Math.round((_c = token.rawBalance) !== null && _c !== void 0 ? _c : 0));
78
72
  if (atomic <= BigInt(0)) {
79
73
  seenKeys.delete(key);
80
74
  return false;
81
75
  }
76
+ inFlightTokenKeys === null || inFlightTokenKeys === void 0 ? void 0 : inFlightTokenKeys.add(tokenKey);
82
77
  try {
83
78
  yield shieldHandle.shieldToken({
84
79
  amount: atomic,
@@ -87,30 +82,12 @@ const tryShieldOneToken = (token, deps) => _tslib.__awaiter(void 0, void 0, void
87
82
  });
88
83
  return true;
89
84
  }
90
- catch (_f) {
85
+ catch (_e) {
91
86
  seenKeys.delete(key);
92
87
  return false;
93
88
  }
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
- }
89
+ finally {
90
+ inFlightTokenKeys === null || inFlightTokenKeys === void 0 ? void 0 : inFlightTokenKeys.delete(tokenKey);
114
91
  }
115
92
  });
116
93
  const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalances, shieldHandle, onShielded, }) => {
@@ -148,6 +125,35 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
148
125
  mountedRef.current = false;
149
126
  };
150
127
  }, []);
128
+ // Identity-stable set of token keys we're currently broadcasting for.
129
+ // Mutated in `tryShieldOneToken` (add on entry, delete in `finally`)
130
+ // via the `inFlightKeysAdapter` below. We rebuild the returned `Set`
131
+ // as a new instance on every mutation so React consumers re-render;
132
+ // the underlying ref-held Set is internal and never exposed.
133
+ const inFlightKeysRef = React.useRef(new Set());
134
+ const [currentlyShieldingTokenKeys, setCurrentlyShieldingTokenKeys] = React.useState(() => new Set());
135
+ // Stable adapter we hand to `tryShieldOneToken`. Each add/delete
136
+ // mutates the ref-held set and publishes a fresh ReadonlySet snapshot
137
+ // to state so subscribers re-render. We can't pass `inFlightKeysRef.current`
138
+ // directly because the consumer can't subscribe to mutation — the
139
+ // state copy is what makes this observable from React.
140
+ const inFlightKeysAdapter = React.useMemo(() => ({
141
+ add: (key) => {
142
+ inFlightKeysRef.current.add(key);
143
+ if (mountedRef.current) {
144
+ setCurrentlyShieldingTokenKeys(new Set(inFlightKeysRef.current));
145
+ }
146
+ },
147
+ delete: (key) => {
148
+ inFlightKeysRef.current.delete(key);
149
+ if (mountedRef.current) {
150
+ setCurrentlyShieldingTokenKeys(new Set(inFlightKeysRef.current));
151
+ }
152
+ },
153
+ }),
154
+ // mountedRef is stable; we read its `.current` inside the closures
155
+ // to avoid resubscribing consumers on every mount.
156
+ []);
151
157
  // Content fingerprint for the balance list. We re-run the effect only
152
158
  // when balances *meaningfully* change (token added/removed/balance
153
159
  // shifted), not on every render where the parent passes a new array
@@ -158,7 +164,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
158
164
  const balanceFingerprint = React.useMemo(() => (unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [])
159
165
  .map((t) => {
160
166
  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}`;
167
+ return `${(_a = t.address) !== null && _a !== void 0 ? _a : nativeTokenKey.nativeTokenKey}|${t.isNative ? 1 : 0}|${(_b = t.rawBalance) !== null && _b !== void 0 ? _b : 0}`;
162
168
  })
163
169
  // Use `localeCompare` so the sort is locale-aware and stable across
164
170
  // JS engines (default `Array.prototype.sort` on strings is
@@ -210,6 +216,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
210
216
  return;
211
217
  const ok = yield tryShieldOneToken(token, {
212
218
  accountAddress,
219
+ inFlightTokenKeys: inFlightKeysAdapter,
213
220
  isCancelled,
214
221
  seenKeys: seenKeysRef.current,
215
222
  shieldHandle,
@@ -218,7 +225,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
218
225
  didShield = true;
219
226
  }
220
227
  if (didShield && !cancelled && onShielded) {
221
- yield pollOnShielded(onShielded, isCancelled);
228
+ yield pollOnShielded.pollOnShielded(onShielded, isCancelled);
222
229
  }
223
230
  });
224
231
  run()
@@ -257,7 +264,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
257
264
  // cancel an in-flight shield ceremony.
258
265
  // eslint-disable-next-line react-hooks/exhaustive-deps
259
266
  }, [accountAddress, balanceFingerprint, shieldHandle, onShielded]);
260
- return { isShielding };
267
+ return { currentlyShieldingTokenKeys, isShielding };
261
268
  };
262
269
 
263
270
  exports.useAleoAutoShieldSponsoredTokens = useAleoAutoShieldSponsoredTokens;
@@ -54,6 +54,17 @@ export type UseAleoAutoShieldSponsoredTokensResult = {
54
54
  * Goes back to `false` on cleanup or once the run resolves.
55
55
  */
56
56
  isShielding: boolean;
57
+ /**
58
+ * Token keys (`token.address` or `nativeTokenKey`) for which a shield
59
+ * ceremony is currently in flight from this hook. Stable identity
60
+ * across renders — only changes when a ceremony starts or ends, not
61
+ * when other parts of the hook state churn. Consumers (e.g. the
62
+ * widget's Shield Manually CTA) read this set to suppress duplicate
63
+ * CTAs for tokens auto-shield is already broadcasting; the connector
64
+ * has its own dedup, but hiding the CTA also prevents the user from
65
+ * seeing a stale spinner snap to a second one.
66
+ */
67
+ currentlyShieldingTokenKeys: ReadonlySet<string>;
57
68
  };
58
69
  export declare const useAleoAutoShieldSponsoredTokens: ({ accountAddress, unshieldedTokenBalances, shieldHandle, onShielded, }: UseAleoAutoShieldSponsoredTokensArgs) => UseAleoAutoShieldSponsoredTokensResult;
59
70
  export {};
@@ -1,15 +1,10 @@
1
1
  'use client'
2
2
  import { __awaiter } from '../../../../../_virtual/_tslib.js';
3
3
  import { useRef, useState, useEffect, useMemo } from 'react';
4
+ import { buildTokenKey } from './buildTokenKey.js';
5
+ import { nativeTokenKey } from './nativeTokenKey.js';
6
+ import { pollOnShielded } from './pollOnShielded.js';
4
7
 
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
8
  /**
14
9
  * Selects which of `tokens` should attempt an auto-shield on this cycle.
15
10
  * A token is a candidate when:
@@ -25,7 +20,6 @@ const REFRESH_DELAYS_MS = [
25
20
  const buildShieldCandidates = (args) => {
26
21
  const { tokens, accountAddress, shieldHandle, seenKeys } = args;
27
22
  return tokens.filter((token) => {
28
- var _a;
29
23
  if (!token.rawBalance || token.rawBalance <= 0)
30
24
  return false;
31
25
  if (!shieldHandle.canShieldToken({
@@ -34,7 +28,7 @@ const buildShieldCandidates = (args) => {
34
28
  })) {
35
29
  return false;
36
30
  }
37
- const tokenKey = (_a = token.address) !== null && _a !== void 0 ? _a : (token.isNative ? '__native__' : '');
31
+ const tokenKey = buildTokenKey(token);
38
32
  if (!tokenKey)
39
33
  return false;
40
34
  return !seenKeys.has(`${accountAddress}:${tokenKey}`);
@@ -48,20 +42,20 @@ const buildShieldCandidates = (args) => {
48
42
  * so the effect-cleanup `cancelled` flag stays in scope.
49
43
  */
50
44
  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__';
45
+ var _a, _b, _c;
46
+ const { accountAddress, shieldHandle, seenKeys, isCancelled, inFlightTokenKeys, } = deps;
47
+ const tokenKey = buildTokenKey(token) || nativeTokenKey;
54
48
  const key = `${accountAddress}:${tokenKey}`;
55
49
  seenKeys.add(key);
56
50
  let sponsored = false;
57
51
  try {
58
52
  sponsored =
59
- (_c = (yield ((_b = shieldHandle.isShieldSponsored) === null || _b === void 0 ? void 0 : _b.call(shieldHandle, {
53
+ (_b = (yield ((_a = shieldHandle.isShieldSponsored) === null || _a === void 0 ? void 0 : _a.call(shieldHandle, {
60
54
  address: token.address,
61
55
  isNative: token.isNative,
62
- })))) !== null && _c !== void 0 ? _c : false;
56
+ })))) !== null && _b !== void 0 ? _b : false;
63
57
  }
64
- catch (_e) {
58
+ catch (_d) {
65
59
  sponsored = false;
66
60
  }
67
61
  if (isCancelled())
@@ -70,11 +64,12 @@ const tryShieldOneToken = (token, deps) => __awaiter(void 0, void 0, void 0, fun
70
64
  seenKeys.delete(key);
71
65
  return false;
72
66
  }
73
- const atomic = BigInt(Math.round((_d = token.rawBalance) !== null && _d !== void 0 ? _d : 0));
67
+ const atomic = BigInt(Math.round((_c = token.rawBalance) !== null && _c !== void 0 ? _c : 0));
74
68
  if (atomic <= BigInt(0)) {
75
69
  seenKeys.delete(key);
76
70
  return false;
77
71
  }
72
+ inFlightTokenKeys === null || inFlightTokenKeys === void 0 ? void 0 : inFlightTokenKeys.add(tokenKey);
78
73
  try {
79
74
  yield shieldHandle.shieldToken({
80
75
  amount: atomic,
@@ -83,30 +78,12 @@ const tryShieldOneToken = (token, deps) => __awaiter(void 0, void 0, void 0, fun
83
78
  });
84
79
  return true;
85
80
  }
86
- catch (_f) {
81
+ catch (_e) {
87
82
  seenKeys.delete(key);
88
83
  return false;
89
84
  }
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
- }
85
+ finally {
86
+ inFlightTokenKeys === null || inFlightTokenKeys === void 0 ? void 0 : inFlightTokenKeys.delete(tokenKey);
110
87
  }
111
88
  });
112
89
  const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalances, shieldHandle, onShielded, }) => {
@@ -144,6 +121,35 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
144
121
  mountedRef.current = false;
145
122
  };
146
123
  }, []);
124
+ // Identity-stable set of token keys we're currently broadcasting for.
125
+ // Mutated in `tryShieldOneToken` (add on entry, delete in `finally`)
126
+ // via the `inFlightKeysAdapter` below. We rebuild the returned `Set`
127
+ // as a new instance on every mutation so React consumers re-render;
128
+ // the underlying ref-held Set is internal and never exposed.
129
+ const inFlightKeysRef = useRef(new Set());
130
+ const [currentlyShieldingTokenKeys, setCurrentlyShieldingTokenKeys] = useState(() => new Set());
131
+ // Stable adapter we hand to `tryShieldOneToken`. Each add/delete
132
+ // mutates the ref-held set and publishes a fresh ReadonlySet snapshot
133
+ // to state so subscribers re-render. We can't pass `inFlightKeysRef.current`
134
+ // directly because the consumer can't subscribe to mutation — the
135
+ // state copy is what makes this observable from React.
136
+ const inFlightKeysAdapter = useMemo(() => ({
137
+ add: (key) => {
138
+ inFlightKeysRef.current.add(key);
139
+ if (mountedRef.current) {
140
+ setCurrentlyShieldingTokenKeys(new Set(inFlightKeysRef.current));
141
+ }
142
+ },
143
+ delete: (key) => {
144
+ inFlightKeysRef.current.delete(key);
145
+ if (mountedRef.current) {
146
+ setCurrentlyShieldingTokenKeys(new Set(inFlightKeysRef.current));
147
+ }
148
+ },
149
+ }),
150
+ // mountedRef is stable; we read its `.current` inside the closures
151
+ // to avoid resubscribing consumers on every mount.
152
+ []);
147
153
  // Content fingerprint for the balance list. We re-run the effect only
148
154
  // when balances *meaningfully* change (token added/removed/balance
149
155
  // shifted), not on every render where the parent passes a new array
@@ -154,7 +160,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
154
160
  const balanceFingerprint = useMemo(() => (unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [])
155
161
  .map((t) => {
156
162
  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}`;
163
+ return `${(_a = t.address) !== null && _a !== void 0 ? _a : nativeTokenKey}|${t.isNative ? 1 : 0}|${(_b = t.rawBalance) !== null && _b !== void 0 ? _b : 0}`;
158
164
  })
159
165
  // Use `localeCompare` so the sort is locale-aware and stable across
160
166
  // JS engines (default `Array.prototype.sort` on strings is
@@ -206,6 +212,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
206
212
  return;
207
213
  const ok = yield tryShieldOneToken(token, {
208
214
  accountAddress,
215
+ inFlightTokenKeys: inFlightKeysAdapter,
209
216
  isCancelled,
210
217
  seenKeys: seenKeysRef.current,
211
218
  shieldHandle,
@@ -253,7 +260,7 @@ const useAleoAutoShieldSponsoredTokens = ({ accountAddress, unshieldedTokenBalan
253
260
  // cancel an in-flight shield ceremony.
254
261
  // eslint-disable-next-line react-hooks/exhaustive-deps
255
262
  }, [accountAddress, balanceFingerprint, shieldHandle, onShielded]);
256
- return { isShielding };
263
+ return { currentlyShieldingTokenKeys, isShielding };
257
264
  };
258
265
 
259
266
  export { useAleoAutoShieldSponsoredTokens };