@dynamic-labs/sdk-react-core 4.84.0 → 4.84.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/package.cjs +1 -1
  3. package/package.js +1 -1
  4. package/package.json +12 -12
  5. package/src/index.cjs +2 -0
  6. package/src/index.d.ts +2 -2
  7. package/src/index.js +1 -0
  8. package/src/lib/components/SendBalanceForm/SendBalanceForm.cjs +26 -1
  9. package/src/lib/components/SendBalanceForm/SendBalanceForm.js +26 -1
  10. package/src/lib/components/SendBalancePageLayout/SendBalancePageLayout.cjs +6 -1
  11. package/src/lib/components/SendBalancePageLayout/SendBalancePageLayout.js +6 -1
  12. package/src/lib/utils/hooks/index.d.ts +2 -0
  13. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.cjs +24 -4
  14. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.d.ts +10 -2
  15. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.js +24 -4
  16. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs +14 -3
  17. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.d.ts +5 -1
  18. package/src/lib/utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js +14 -3
  19. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.cjs +14 -0
  20. package/src/lib/utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.js +14 -0
  21. package/src/lib/utils/hooks/usePrivateTokenBalances/index.d.ts +2 -0
  22. package/src/lib/utils/hooks/usePrivateTokenBalances/usePrivateTokenBalances.cjs +19 -0
  23. package/src/lib/utils/hooks/usePrivateTokenBalances/usePrivateTokenBalances.d.ts +9 -0
  24. package/src/lib/utils/hooks/usePrivateTokenBalances/usePrivateTokenBalances.js +15 -0
  25. package/src/lib/utils/hooks/useWalletDelegation/useWalletDelegation.cjs +19 -16
  26. package/src/lib/utils/hooks/useWalletDelegation/useWalletDelegation.d.ts +8 -0
  27. package/src/lib/utils/hooks/useWalletDelegation/useWalletDelegation.js +19 -17
  28. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.cjs +138 -21
  29. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/ActiveWalletBalance.js +139 -22
  30. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/optimisticShield.cjs +134 -0
  31. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/optimisticShield.d.ts +69 -0
  32. package/src/lib/widgets/DynamicWidget/components/ActiveWalletBalance/optimisticShield.js +127 -0
@@ -0,0 +1,15 @@
1
+ 'use client'
2
+ import { useAleoShieldedBalances } from '../useAleoShieldedBalances/useAleoShieldedBalances.js';
3
+
4
+ const usePrivateTokenBalances = () => {
5
+ const { tokenBalances, isLoading, error, refetch, supportsShielded } = useAleoShieldedBalances();
6
+ return {
7
+ error,
8
+ isLoading,
9
+ refetch,
10
+ supportsPrivateBalances: supportsShielded,
11
+ tokenBalances,
12
+ };
13
+ };
14
+
15
+ export { usePrivateTokenBalances };
@@ -41,6 +41,14 @@ var useDynamicWaas = require('../useDynamicWaas/useDynamicWaas.cjs');
41
41
  var useRefreshAuth = require('../useRefreshAuth/useRefreshAuth.cjs');
42
42
  var DelegationError = require('./DelegationError.cjs');
43
43
 
44
+ // Pre-share-set backends mark delegation via keyShares[].backupLocation='delegated';
45
+ // share-set backends use otherShareSets[].shareSetType='delegated' instead.
46
+ const isWalletDelegated = (walletProperties) => {
47
+ var _a, _b;
48
+ const hasDelegatedKeyShare = (_a = walletProperties === null || walletProperties === void 0 ? void 0 : walletProperties.keyShares) === null || _a === void 0 ? void 0 : _a.some((keyShare) => keyShare.backupLocation === 'delegated');
49
+ const hasDelegatedShareSet = (_b = walletProperties === null || walletProperties === void 0 ? void 0 : walletProperties.otherShareSets) === null || _b === void 0 ? void 0 : _b.some((shareSet) => shareSet.shareSetType === 'delegated');
50
+ return Boolean(hasDelegatedKeyShare || hasDelegatedShareSet);
51
+ };
44
52
  /**
45
53
  * Gets pending wallets that are eligible for delegation.
46
54
  * If wallets are provided and not empty, converts them to WalletWithStatus[] with pending status.
@@ -87,15 +95,11 @@ const useWalletDelegation = () => {
87
95
  const waasCredentials = user.verifiedCredentials.filter((vc) => vc.walletName === 'dynamicwaas' &&
88
96
  vc.format === sdkApiCore.JwtVerifiedCredentialFormatEnum.Blockchain);
89
97
  const hasWalletNeedingDelegation = waasCredentials.some((vc) => {
90
- var _a, _b, _c, _d, _e;
91
- // Check if already delegated (has delegated keyShare)
92
- const hasDelegatedKeyShare = (_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.keyShares) === null || _b === void 0 ? void 0 : _b.some((keyShare) => keyShare.backupLocation === 'delegated');
93
- // Check if denied
94
- const hasDeniedAccess = ((_d = (_c = vc.walletProperties) === null || _c === void 0 ? void 0 : _c.settings) === null || _d === void 0 ? void 0 : _d.hasDeniedDelegatedAccess) === true;
95
- // Check if dismissed this session
96
- const isDismissedThisSession = (_e = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _e === void 0 ? void 0 : _e.includes(vc.id);
97
- // Needs delegation if: not delegated, not denied, and not dismissed this session
98
- return (!hasDelegatedKeyShare && !hasDeniedAccess && !isDismissedThisSession);
98
+ var _a, _b, _c;
99
+ const isDelegated = isWalletDelegated(vc.walletProperties);
100
+ const hasDeniedAccess = ((_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.settings) === null || _b === void 0 ? void 0 : _b.hasDeniedDelegatedAccess) === true;
101
+ const isDismissedThisSession = (_c = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _c === void 0 ? void 0 : _c.includes(vc.id);
102
+ return !isDelegated && !hasDeniedAccess && !isDismissedThisSession;
99
103
  });
100
104
  return hasWalletNeedingDelegation;
101
105
  }, [user === null || user === void 0 ? void 0 : user.verifiedCredentials, delegatedAccessEnabled, promptUsersOnSignIn]);
@@ -129,20 +133,18 @@ const useWalletDelegation = () => {
129
133
  // Map credentials to wallets with status
130
134
  return waasCredentials
131
135
  .map((vc) => {
132
- var _a, _b, _c, _d, _e;
136
+ var _a, _b, _c;
133
137
  let status = 'pending';
134
- // Check if wallet has delegated keyShare
135
- const hasDelegatedKeyShare = (_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.keyShares) === null || _b === void 0 ? void 0 : _b.some((keyShare) => keyShare.backupLocation === 'delegated');
136
- // Check if user has denied delegation
137
- const hasDeniedAccess = ((_d = (_c = vc.walletProperties) === null || _c === void 0 ? void 0 : _c.settings) === null || _d === void 0 ? void 0 : _d.hasDeniedDelegatedAccess) === true;
138
- if (hasDelegatedKeyShare) {
138
+ const isDelegated = isWalletDelegated(vc.walletProperties);
139
+ const hasDeniedAccess = ((_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.settings) === null || _b === void 0 ? void 0 : _b.hasDeniedDelegatedAccess) === true;
140
+ if (isDelegated) {
139
141
  status = 'delegated';
140
142
  }
141
143
  else if (hasDeniedAccess) {
142
144
  status = 'denied';
143
145
  }
144
146
  // Check if dismissed this session (UI state only)
145
- const isDismissedThisSession = (_e = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _e === void 0 ? void 0 : _e.includes(vc.id);
147
+ const isDismissedThisSession = (_c = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _c === void 0 ? void 0 : _c.includes(vc.id);
146
148
  // Find corresponding wallet from userWallets
147
149
  const wallet = userWallets.find((w) => w.address === vc.address);
148
150
  if (!wallet) {
@@ -309,4 +311,5 @@ const useWalletDelegation = () => {
309
311
  };
310
312
 
311
313
  exports.getWalletsToDelegate = getWalletsToDelegate;
314
+ exports.isWalletDelegated = isWalletDelegated;
312
315
  exports.useWalletDelegation = useWalletDelegation;
@@ -5,6 +5,14 @@ export type WalletWithStatus = Wallet & {
5
5
  status: WalletDelegationStatus;
6
6
  isDismissedThisSession?: boolean;
7
7
  };
8
+ export declare const isWalletDelegated: (walletProperties: {
9
+ keyShares?: Array<{
10
+ backupLocation?: string;
11
+ }>;
12
+ otherShareSets?: Array<{
13
+ shareSetType?: string;
14
+ }>;
15
+ } | null | undefined) => boolean;
8
16
  /**
9
17
  * Gets pending wallets that are eligible for delegation.
10
18
  * If wallets are provided and not empty, converts them to WalletWithStatus[] with pending status.
@@ -37,6 +37,14 @@ import { useDynamicWaas } from '../useDynamicWaas/useDynamicWaas.js';
37
37
  import { useRefreshAuth } from '../useRefreshAuth/useRefreshAuth.js';
38
38
  import { DelegationError } from './DelegationError.js';
39
39
 
40
+ // Pre-share-set backends mark delegation via keyShares[].backupLocation='delegated';
41
+ // share-set backends use otherShareSets[].shareSetType='delegated' instead.
42
+ const isWalletDelegated = (walletProperties) => {
43
+ var _a, _b;
44
+ const hasDelegatedKeyShare = (_a = walletProperties === null || walletProperties === void 0 ? void 0 : walletProperties.keyShares) === null || _a === void 0 ? void 0 : _a.some((keyShare) => keyShare.backupLocation === 'delegated');
45
+ const hasDelegatedShareSet = (_b = walletProperties === null || walletProperties === void 0 ? void 0 : walletProperties.otherShareSets) === null || _b === void 0 ? void 0 : _b.some((shareSet) => shareSet.shareSetType === 'delegated');
46
+ return Boolean(hasDelegatedKeyShare || hasDelegatedShareSet);
47
+ };
40
48
  /**
41
49
  * Gets pending wallets that are eligible for delegation.
42
50
  * If wallets are provided and not empty, converts them to WalletWithStatus[] with pending status.
@@ -83,15 +91,11 @@ const useWalletDelegation = () => {
83
91
  const waasCredentials = user.verifiedCredentials.filter((vc) => vc.walletName === 'dynamicwaas' &&
84
92
  vc.format === JwtVerifiedCredentialFormatEnum.Blockchain);
85
93
  const hasWalletNeedingDelegation = waasCredentials.some((vc) => {
86
- var _a, _b, _c, _d, _e;
87
- // Check if already delegated (has delegated keyShare)
88
- const hasDelegatedKeyShare = (_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.keyShares) === null || _b === void 0 ? void 0 : _b.some((keyShare) => keyShare.backupLocation === 'delegated');
89
- // Check if denied
90
- const hasDeniedAccess = ((_d = (_c = vc.walletProperties) === null || _c === void 0 ? void 0 : _c.settings) === null || _d === void 0 ? void 0 : _d.hasDeniedDelegatedAccess) === true;
91
- // Check if dismissed this session
92
- const isDismissedThisSession = (_e = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _e === void 0 ? void 0 : _e.includes(vc.id);
93
- // Needs delegation if: not delegated, not denied, and not dismissed this session
94
- return (!hasDelegatedKeyShare && !hasDeniedAccess && !isDismissedThisSession);
94
+ var _a, _b, _c;
95
+ const isDelegated = isWalletDelegated(vc.walletProperties);
96
+ const hasDeniedAccess = ((_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.settings) === null || _b === void 0 ? void 0 : _b.hasDeniedDelegatedAccess) === true;
97
+ const isDismissedThisSession = (_c = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _c === void 0 ? void 0 : _c.includes(vc.id);
98
+ return !isDelegated && !hasDeniedAccess && !isDismissedThisSession;
95
99
  });
96
100
  return hasWalletNeedingDelegation;
97
101
  }, [user === null || user === void 0 ? void 0 : user.verifiedCredentials, delegatedAccessEnabled, promptUsersOnSignIn]);
@@ -125,20 +129,18 @@ const useWalletDelegation = () => {
125
129
  // Map credentials to wallets with status
126
130
  return waasCredentials
127
131
  .map((vc) => {
128
- var _a, _b, _c, _d, _e;
132
+ var _a, _b, _c;
129
133
  let status = 'pending';
130
- // Check if wallet has delegated keyShare
131
- const hasDelegatedKeyShare = (_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.keyShares) === null || _b === void 0 ? void 0 : _b.some((keyShare) => keyShare.backupLocation === 'delegated');
132
- // Check if user has denied delegation
133
- const hasDeniedAccess = ((_d = (_c = vc.walletProperties) === null || _c === void 0 ? void 0 : _c.settings) === null || _d === void 0 ? void 0 : _d.hasDeniedDelegatedAccess) === true;
134
- if (hasDelegatedKeyShare) {
134
+ const isDelegated = isWalletDelegated(vc.walletProperties);
135
+ const hasDeniedAccess = ((_b = (_a = vc.walletProperties) === null || _a === void 0 ? void 0 : _a.settings) === null || _b === void 0 ? void 0 : _b.hasDeniedDelegatedAccess) === true;
136
+ if (isDelegated) {
135
137
  status = 'delegated';
136
138
  }
137
139
  else if (hasDeniedAccess) {
138
140
  status = 'denied';
139
141
  }
140
142
  // Check if dismissed this session (UI state only)
141
- const isDismissedThisSession = (_e = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _e === void 0 ? void 0 : _e.includes(vc.id);
143
+ const isDismissedThisSession = (_c = sessionState === null || sessionState === void 0 ? void 0 : sessionState.dismissedWallets) === null || _c === void 0 ? void 0 : _c.includes(vc.id);
142
144
  // Find corresponding wallet from userWallets
143
145
  const wallet = userWallets.find((w) => w.address === vc.address);
144
146
  if (!wallet) {
@@ -304,4 +306,4 @@ const useWalletDelegation = () => {
304
306
  };
305
307
  };
306
308
 
307
- export { getWalletsToDelegate, useWalletDelegation };
309
+ export { getWalletsToDelegate, isWalletDelegated, useWalletDelegation };
@@ -127,6 +127,7 @@ require('../../views/ReceiveWalletFunds/ReceiveWalletFunds.cjs');
127
127
  require('../../../../store/state/multichainBalances.cjs');
128
128
  require('@dynamic-labs/store');
129
129
  require('../../../../shared/utils/functions/getInitialUrl/getInitialUrl.cjs');
130
+ var optimisticShield = require('./optimisticShield.cjs');
130
131
  var TokenBalanceList = require('./TokenBalanceList/TokenBalanceList.cjs');
131
132
 
132
133
  /** Component to display token balances for the primary wallet */
@@ -153,6 +154,57 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
153
154
  includeNativeBalance: true,
154
155
  });
155
156
  const { tokenBalances: shieldedTokenBalances, isLoading: isLoadingShielded, refetch: refetchShielded, supportsShielded, } = useAleoShieldedBalances.useAleoShieldedBalances();
157
+ // Optimistic shield ledger. A shield broadcast resolves before the
158
+ // public-balance mapping flips and well before the RecordScanner
159
+ // index reflects the new private record, so naively trusting the
160
+ // server feed shows a stale unshielded balance and an unchanged
161
+ // shielded balance for ~10–90s after the user clicks. Pushing an
162
+ // entry here deducts the burned amount from unshielded and prepends
163
+ // / bumps the matching shielded row, then `reconcileOptimisticShields`
164
+ // drops the entry as soon as either server feed catches up.
165
+ const [optimisticShields, setOptimisticShields] = React.useState([]);
166
+ const effectiveUnshieldedTokenBalances = React.useMemo(() => optimisticShield.applyOptimisticUnshieldedDeductions(unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [], optimisticShields), [unshieldedTokenBalances, optimisticShields]);
167
+ const effectiveShieldedTokenBalances = React.useMemo(() => optimisticShield.applyOptimisticShieldedAdditions(shieldedTokenBalances, optimisticShields), [shieldedTokenBalances, optimisticShields]);
168
+ // Reconcile + GC optimistic entries whenever either server feed
169
+ // updates. The reconciler is pure; we short-circuit the state write
170
+ // when nothing changed so downstream memos don't churn on every
171
+ // refresh tick.
172
+ React.useEffect(() => {
173
+ setOptimisticShields((current) => {
174
+ if (current.length === 0)
175
+ return current;
176
+ const next = optimisticShield.reconcileOptimisticShields(current, unshieldedTokenBalances !== null && unshieldedTokenBalances !== void 0 ? unshieldedTokenBalances : [], shieldedTokenBalances, Date.now());
177
+ return next.length === current.length ? current : next;
178
+ });
179
+ }, [unshieldedTokenBalances, shieldedTokenBalances]);
180
+ // Push helper used by both the manual Shield Manually CTA and the
181
+ // auto-shield hook so the optimistic UI semantics are identical
182
+ // whichever flow dispatches the broadcast.
183
+ const recordOptimisticShield = React.useCallback((info) => {
184
+ const { token, amount } = info;
185
+ const matchingShielded = shieldedTokenBalances.find((t) => token.isNative
186
+ ? Boolean(t.isNative)
187
+ : !t.isNative && t.address === token.address);
188
+ setOptimisticShields((current) => {
189
+ var _a, _b, _c, _d;
190
+ return [
191
+ ...current,
192
+ {
193
+ amount,
194
+ createdAtMs: Date.now(),
195
+ decimals: (_a = token.decimals) !== null && _a !== void 0 ? _a : 0,
196
+ isNative: Boolean(token.isNative),
197
+ logoURI: (_b = token.logoURI) !== null && _b !== void 0 ? _b : '',
198
+ name: token.name,
199
+ preShieldShieldedRaw: BigInt(Math.round((_c = matchingShielded === null || matchingShielded === void 0 ? void 0 : matchingShielded.rawBalance) !== null && _c !== void 0 ? _c : 0)),
200
+ preShieldUnshieldedRaw: BigInt(Math.round((_d = token.rawBalance) !== null && _d !== void 0 ? _d : 0)),
201
+ price: token.price,
202
+ symbol: token.symbol,
203
+ tokenAddress: token.address,
204
+ },
205
+ ];
206
+ });
207
+ }, [shieldedTokenBalances]);
156
208
  /**
157
209
  * Tab state for chains that have a shielded/unshielded split. Defaults to
158
210
  * shielded so Aleo wallets land on their primary balance type. Other
@@ -197,11 +249,22 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
197
249
  // Manually CTA, which prompts the user-paid confirmation modal.
198
250
  const { isShielding: isAutoShielding, currentlyShieldingTokenKeys: autoShieldingTokenKeys, } = useAleoAutoShieldSponsoredTokens.useAleoAutoShieldSponsoredTokens({
199
251
  accountAddress: primaryWallet === null || primaryWallet === void 0 ? void 0 : primaryWallet.address,
200
- onShielded: React.useCallback(() => _tslib.__awaiter(void 0, void 0, void 0, function* () {
201
- yield fetchAccountBalances(true);
202
- yield refetchShielded();
252
+ onShieldDispatched: recordOptimisticShield,
253
+ onShielded: React.useCallback(
254
+ // Parallel refresh — same reasoning as the manual shield path:
255
+ // shield ops move value across both tabs, and serializing the
256
+ // two refreshes doubled the visible latency for the Shielded
257
+ // tab in the common case.
258
+ () => _tslib.__awaiter(void 0, void 0, void 0, function* () {
259
+ return Promise.all([fetchAccountBalances(true), refetchShielded()]).then(() => undefined);
203
260
  }), [fetchAccountBalances, refetchShielded]),
204
261
  shieldHandle: aleoShieldHandle,
262
+ // Auto-shield reads the raw server feed (not the optimistic one):
263
+ // once we deduct optimistically the to-be-shielded token drops to
264
+ // zero in the effective list, which would suppress the next
265
+ // auto-shield run before this one finishes. The auto-shield hook
266
+ // has its own per-session idempotency (`seenKeys`) so it won't
267
+ // re-fire for the same token anyway.
205
268
  unshieldedTokenBalances: unshieldedTokenBalances,
206
269
  });
207
270
  // Liveness handle for the manual post-shield poll — set to true when
@@ -214,12 +277,25 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
214
277
  isUnmountedRef.current = true;
215
278
  };
216
279
  }, []);
280
+ // Latest server-side unshielded list mirrored into a ref so the
281
+ // post-shield convergence check can read the freshest value after
282
+ // each polled refresh. `unshieldedTokenBalances` is closure-captured
283
+ // by `handleShieldToken` at callback construction time, so without
284
+ // this ref the predicate would read the pre-shield list forever and
285
+ // the poll would never exit early. Reads from the raw server feed,
286
+ // NOT the optimistic one — convergence means "on-chain effect
287
+ // observed", which is only meaningful against the actual server
288
+ // state.
289
+ const unshieldedTokenBalancesRef = React.useRef(unshieldedTokenBalances);
290
+ React.useEffect(() => {
291
+ unshieldedTokenBalancesRef.current = unshieldedTokenBalances;
292
+ }, [unshieldedTokenBalances]);
217
293
  // Token currently awaiting user-paid fee confirmation. Set when
218
294
  // Shield Manually is clicked on a token Feemaster doesn't sponsor;
219
295
  // cleared on Cancel or after the modal's Shield button dispatches.
220
296
  const [pendingShieldToken, setPendingShieldToken] = React.useState(null);
221
297
  const handleShieldToken = React.useCallback((token) => _tslib.__awaiter(void 0, void 0, void 0, function* () {
222
- var _f;
298
+ var _f, _g;
223
299
  if (!aleoShieldHandle || shieldingAddress)
224
300
  return;
225
301
  // `rawBalance` is the atomic-units count redcoast surfaces (number).
@@ -229,6 +305,14 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
229
305
  const atomic = BigInt(Math.round((_f = token.rawBalance) !== null && _f !== void 0 ? _f : 0));
230
306
  if (atomic <= BigInt(0))
231
307
  return;
308
+ // Snapshot the pre-shield unshielded balance for the convergence
309
+ // predicate below. Aleo's `transfer_public_to_private` burns the
310
+ // public mapping value, so a successful shield must show up as a
311
+ // drop in this token's `rawBalance`. Polling exits early once
312
+ // that drop materializes — no need to keep refreshing on the
313
+ // long-tail schedule when we've already observed the on-chain
314
+ // effect.
315
+ const preShieldRawBalance = (_g = token.rawBalance) !== null && _g !== void 0 ? _g : 0;
232
316
  setShieldingAddress(token.address);
233
317
  let didBroadcast = false;
234
318
  try {
@@ -238,11 +322,22 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
238
322
  tokenAddress: token.address,
239
323
  });
240
324
  didBroadcast = true;
241
- // Refresh the unshielded list so the just-shielded balance drops to
242
- // 0 in the redcoast feed. The shielded side will pick the new
243
- // record up on its own polling once the RecordScanner indexes.
244
- yield fetchAccountBalances(true);
245
- yield refetchShielded();
325
+ // Optimistic balance update: deduct the burned amount from
326
+ // unshielded and bump (or synthesise) the matching shielded
327
+ // row before the server-side feeds catch up. Without this the
328
+ // Shield Manually CTA flickers back into view during the
329
+ // indexing tail and a fast user can issue a second shield
330
+ // that fails server-side (validator rejects the now-zero
331
+ // public balance) and looks to them like the first one
332
+ // failed.
333
+ recordOptimisticShield({ amount: atomic, token });
334
+ // Refresh both tabs in parallel. Shield moves value across the
335
+ // shielded/unshielded boundary, so both lists need to
336
+ // converge. Running them in parallel (vs. sequential) halves
337
+ // the visible refresh latency and shortens the window where
338
+ // `useTokenBalances`' `isLoading` guard could drop a
339
+ // concurrent fetch.
340
+ yield Promise.all([fetchAccountBalances(true), refetchShielded()]);
246
341
  }
247
342
  catch (err) {
248
343
  logger.logger.debug('[ActiveWalletBalance] shieldToken failed', err);
@@ -253,7 +348,7 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
253
348
  // The relay-accepted broadcast resolves *before* the
254
349
  // public→private transition finalizes on-chain, so the immediate
255
350
  // refresh above sees the pre-confirmation balance. Re-poll on the
256
- // shared backoff so the Unshielded row visibly drops to 0 and the
351
+ // shared backoff so the Unshielded row visibly drops and the
257
352
  // Shielded row picks the new record up without a manual reload.
258
353
  // Polling is best-effort — errors are swallowed inside
259
354
  // `pollOnShielded`. Skip when the broadcast itself failed; there's
@@ -261,10 +356,27 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
261
356
  if (!didBroadcast)
262
357
  return;
263
358
  yield pollOnShielded.pollOnShielded(() => _tslib.__awaiter(void 0, void 0, void 0, function* () {
264
- yield fetchAccountBalances(true);
265
- yield refetchShielded();
266
- }), () => isUnmountedRef.current);
267
- }), [aleoShieldHandle, fetchAccountBalances, refetchShielded, shieldingAddress]);
359
+ yield Promise.all([fetchAccountBalances(true), refetchShielded()]);
360
+ }), () => isUnmountedRef.current, () => {
361
+ var _a, _b;
362
+ // Convergence: the just-shielded token's unshielded raw
363
+ // balance has dropped below the pre-shield value. We read
364
+ // via the ref so we see whatever the store published after
365
+ // the most recent poll, not the closure-captured list from
366
+ // callback construction time. We don't require an exact 0
367
+ // — additional unshielded credits could land between scans;
368
+ // any decrease is sufficient evidence the burn took effect.
369
+ const current = (_a = unshieldedTokenBalancesRef.current) === null || _a === void 0 ? void 0 : _a.find((t) => t.address === token.address && t.isNative === token.isNative);
370
+ const currentRaw = (_b = current === null || current === void 0 ? void 0 : current.rawBalance) !== null && _b !== void 0 ? _b : 0;
371
+ return currentRaw < preShieldRawBalance;
372
+ });
373
+ }), [
374
+ aleoShieldHandle,
375
+ fetchAccountBalances,
376
+ recordOptimisticShield,
377
+ refetchShielded,
378
+ shieldingAddress,
379
+ ]);
268
380
  const getSecondaryAction = React.useCallback((token) => {
269
381
  // Only on the Unshielded tab, only when the connector exposes shield
270
382
  // helpers, only for tokens registered as shieldable, only for tokens
@@ -327,12 +439,12 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
327
439
  shieldingAddress,
328
440
  ]);
329
441
  const tokenBalances = React.useMemo(() => supportsShielded && activeShieldTab === 'shielded'
330
- ? shieldedTokenBalances
331
- : unshieldedTokenBalances, [
442
+ ? effectiveShieldedTokenBalances
443
+ : effectiveUnshieldedTokenBalances, [
332
444
  activeShieldTab,
333
- shieldedTokenBalances,
445
+ effectiveShieldedTokenBalances,
446
+ effectiveUnshieldedTokenBalances,
334
447
  supportsShielded,
335
- unshieldedTokenBalances,
336
448
  ]);
337
449
  const filteredTokenBalances = React.useMemo(() => (tokenBalances === null || tokenBalances === void 0 ? void 0 : tokenBalances.filter((token) => token.name)) || [], [tokenBalances]);
338
450
  const totalValue = React.useMemo(() => filteredTokenBalances.reduce((acc, token) => acc + ((token === null || token === void 0 ? void 0 : token.marketValue) || 0), 0), [filteredTokenBalances]);
@@ -375,8 +487,14 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
375
487
  setIsRefreshing(true);
376
488
  setIsSuccess(false);
377
489
  try {
378
- if (supportsShielded && activeShieldTab === 'shielded') {
379
- yield refetchShielded();
490
+ // Chains with a shielded/unshielded split (Aleo) refresh BOTH
491
+ // tabs on a single click. A shield op moves value across the
492
+ // boundary, so refreshing only the active tab leaves the other
493
+ // visibly stale — the original complaint that drove this fix.
494
+ // Other chains never had `refetchShielded` (no-op fallback) so
495
+ // they still just hit `fetchAccountBalances` here.
496
+ if (supportsShielded) {
497
+ yield Promise.all([refetchShielded(), fetchAccountBalances(true)]);
380
498
  }
381
499
  else {
382
500
  yield fetchAccountBalances(true);
@@ -400,7 +518,6 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
400
518
  fetchAccountBalances,
401
519
  refetchShielded,
402
520
  supportsShielded,
403
- activeShieldTab,
404
521
  ]);
405
522
  const primaryWalletNativeBalance = () => {
406
523
  if (!primaryWallet ||