@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
@@ -144,25 +144,29 @@ const resolveAleoNetwork = (networkKey) => {
144
144
  return { aleoNetworkParam: 'testnet', safeNetworkId };
145
145
  return { aleoNetworkParam: undefined, safeNetworkId };
146
146
  };
147
- /**
148
- * Build a `(address, isNative) → price` lookup from the curated-prices
149
- * endpoint response. The key shape matches the multichain balance feed
150
- * (native ALEO at `'0x0' + isNative=true`, every other token at its
151
- * program id + `isNative=false`) so the join is a straight read.
152
- */
153
- const buildPriceLookup = (priceList) => {
154
- const priceByKey = new Map();
147
+ const buildCuratedTokenLookup = (priceList) => {
148
+ const byKey = new Map();
155
149
  for (const entry of priceList) {
156
- priceByKey.set(`${entry.address}|${entry.isNative ? 1 : 0}`, entry.price);
150
+ byKey.set(`${entry.address}|${entry.isNative ? 1 : 0}`, {
151
+ logoURI: entry.logoURI,
152
+ price: entry.price,
153
+ });
157
154
  }
158
- return (address, isNative) => { var _a; return (_a = priceByKey.get(`${address}|${isNative ? 1 : 0}`)) !== null && _a !== void 0 ? _a : null; };
155
+ return (address, isNative) => {
156
+ var _a;
157
+ return (_a = byKey.get(`${address}|${isNative ? 1 : 0}`)) !== null && _a !== void 0 ? _a : {
158
+ logoURI: undefined,
159
+ price: null,
160
+ };
161
+ };
159
162
  };
160
163
  /**
161
164
  * Aggregate `credits.aleo / credits` records into the native ALEO
162
165
  * `TokenBalance`. Returns `undefined` when the wallet owns no credits
163
166
  * records so the caller can omit the row entirely.
164
167
  */
165
- const buildCreditsBalance = (records, networkId, priceFor) => {
168
+ const buildCreditsBalance = (records, networkId, lookupCurated) => {
169
+ var _a;
166
170
  const creditsRecords = records.filter((r) => (r === null || r === void 0 ? void 0 : r.program_name) === 'credits.aleo' &&
167
171
  (r === null || r === void 0 ? void 0 : r.record_name) === 'credits' &&
168
172
  typeof (r === null || r === void 0 ? void 0 : r.microcredits) === 'string');
@@ -173,19 +177,25 @@ const buildCreditsBalance = (records, networkId, priceFor) => {
173
177
  // below Number.MAX_SAFE_INTEGER for a single wallet (max u64 supply is
174
178
  // 1.5B credits = 1.5e15 microcredits; Number can hold up to ~9e15).
175
179
  const rawBalance = Number(totalMicrocredits);
180
+ const rawBalanceString = totalMicrocredits.toString();
176
181
  const balance = rawBalance / MICROCREDITS_PER_CREDIT;
177
- const price = priceFor('0x0', true);
182
+ const curated = lookupCurated('0x0', true);
178
183
  return {
179
184
  address: '0x0',
180
185
  balance,
181
186
  decimals: 6,
182
187
  isNative: true,
183
- logoURI: ALEO_CREDITS_LOGO,
184
- marketValue: price !== null ? balance * price : undefined,
188
+ // Prefer the server-curated logo (single source of truth for both
189
+ // the unshielded multichain feed and the shielded tab); fall back
190
+ // to the bundled ALEO glyph for older redcoast deployments that
191
+ // don't yet send `logoURI` on the prices response.
192
+ logoURI: (_a = curated.logoURI) !== null && _a !== void 0 ? _a : ALEO_CREDITS_LOGO,
193
+ marketValue: curated.price !== null ? balance * curated.price : undefined,
185
194
  name: 'Aleo Credits',
186
195
  networkId,
187
- price: price !== null ? price : undefined,
196
+ price: curated.price !== null ? curated.price : undefined,
188
197
  rawBalance,
198
+ rawBalanceString,
189
199
  symbol: 'ALEO',
190
200
  };
191
201
  };
@@ -195,8 +205,8 @@ const buildCreditsBalance = (records, networkId, priceFor) => {
195
205
  * match a curated spec, lack an `amount`, or have a malformed amount
196
206
  * string are silently skipped.
197
207
  */
198
- const buildTokenBalances = (records, networkId, priceFor) => {
199
- var _a, _b;
208
+ const buildTokenBalances = (records, networkId, lookupCurated) => {
209
+ var _a, _b, _c;
200
210
  const sumsByContract = new Map();
201
211
  const specsByContract = new Map();
202
212
  for (const r of records) {
@@ -208,7 +218,7 @@ const buildTokenBalances = (records, networkId, priceFor) => {
208
218
  sumsByContract.set(spec.contractAddress, prev + BigInt(r.amount));
209
219
  specsByContract.set(spec.contractAddress, spec);
210
220
  }
211
- catch (_c) {
221
+ catch (_d) {
212
222
  /* ignore — malformed amount string */
213
223
  }
214
224
  }
@@ -218,22 +228,26 @@ const buildTokenBalances = (records, networkId, priceFor) => {
218
228
  if (!spec)
219
229
  continue;
220
230
  const rawBalance = Number(total);
231
+ const rawBalanceString = total.toString();
221
232
  const balance = rawBalance / Math.pow(10, spec.decimals);
222
- const price = priceFor(spec.contractAddress, false);
233
+ const curated = lookupCurated(spec.contractAddress, false);
223
234
  out.push({
224
235
  address: spec.contractAddress,
225
236
  balance,
226
237
  decimals: spec.decimals,
227
- // Use redcoast's `DEFAULT_TOKEN_LOGO_URI` for stablecoins + ARC-21
228
- // so they render the same dark "?" icon the Unshielded tab shows
229
- // for these tokens (the multichain endpoint applies that fallback
230
- // itself; we mirror it here for visual parity).
231
- logoURI: (_b = spec.logoURI) !== null && _b !== void 0 ? _b : UNKNOWN_TOKEN_LOGO,
232
- marketValue: price !== null ? balance * price : undefined,
238
+ // Prefer the server-curated logo so the Shielded tab renders the
239
+ // same brand glyph the Unshielded tab gets from the multichain
240
+ // feed. Fall back to the local spec override (kept for tests +
241
+ // any future curated-but-not-yet-on-server tokens), then to the
242
+ // generic dark `?` placeholder for older redcoast deployments
243
+ // that haven't rolled out the logo response field yet.
244
+ logoURI: (_c = (_b = curated.logoURI) !== null && _b !== void 0 ? _b : spec.logoURI) !== null && _c !== void 0 ? _c : UNKNOWN_TOKEN_LOGO,
245
+ marketValue: curated.price !== null ? balance * curated.price : undefined,
233
246
  name: spec.name,
234
247
  networkId,
235
- price: price !== null ? price : undefined,
248
+ price: curated.price !== null ? curated.price : undefined,
236
249
  rawBalance,
250
+ rawBalanceString,
237
251
  symbol: spec.symbol,
238
252
  });
239
253
  }
@@ -304,12 +318,12 @@ const useAleoShieldedBalances = () => {
304
318
  : Promise.resolve([]),
305
319
  ]);
306
320
  const records = (_a = result === null || result === void 0 ? void 0 : result.records) !== null && _a !== void 0 ? _a : [];
307
- const priceFor = buildPriceLookup(priceList);
321
+ const lookupCurated = buildCuratedTokenLookup(priceList);
308
322
  const balances = [];
309
- const credits = buildCreditsBalance(records, safeNetworkId, priceFor);
323
+ const credits = buildCreditsBalance(records, safeNetworkId, lookupCurated);
310
324
  if (credits)
311
325
  balances.push(credits);
312
- balances.push(...buildTokenBalances(records, safeNetworkId, priceFor));
326
+ balances.push(...buildTokenBalances(records, safeNetworkId, lookupCurated));
313
327
  setTokenBalances(balances);
314
328
  }
315
329
  catch (err) {
@@ -140,25 +140,29 @@ const resolveAleoNetwork = (networkKey) => {
140
140
  return { aleoNetworkParam: 'testnet', safeNetworkId };
141
141
  return { aleoNetworkParam: undefined, safeNetworkId };
142
142
  };
143
- /**
144
- * Build a `(address, isNative) → price` lookup from the curated-prices
145
- * endpoint response. The key shape matches the multichain balance feed
146
- * (native ALEO at `'0x0' + isNative=true`, every other token at its
147
- * program id + `isNative=false`) so the join is a straight read.
148
- */
149
- const buildPriceLookup = (priceList) => {
150
- const priceByKey = new Map();
143
+ const buildCuratedTokenLookup = (priceList) => {
144
+ const byKey = new Map();
151
145
  for (const entry of priceList) {
152
- priceByKey.set(`${entry.address}|${entry.isNative ? 1 : 0}`, entry.price);
146
+ byKey.set(`${entry.address}|${entry.isNative ? 1 : 0}`, {
147
+ logoURI: entry.logoURI,
148
+ price: entry.price,
149
+ });
153
150
  }
154
- return (address, isNative) => { var _a; return (_a = priceByKey.get(`${address}|${isNative ? 1 : 0}`)) !== null && _a !== void 0 ? _a : null; };
151
+ return (address, isNative) => {
152
+ var _a;
153
+ return (_a = byKey.get(`${address}|${isNative ? 1 : 0}`)) !== null && _a !== void 0 ? _a : {
154
+ logoURI: undefined,
155
+ price: null,
156
+ };
157
+ };
155
158
  };
156
159
  /**
157
160
  * Aggregate `credits.aleo / credits` records into the native ALEO
158
161
  * `TokenBalance`. Returns `undefined` when the wallet owns no credits
159
162
  * records so the caller can omit the row entirely.
160
163
  */
161
- const buildCreditsBalance = (records, networkId, priceFor) => {
164
+ const buildCreditsBalance = (records, networkId, lookupCurated) => {
165
+ var _a;
162
166
  const creditsRecords = records.filter((r) => (r === null || r === void 0 ? void 0 : r.program_name) === 'credits.aleo' &&
163
167
  (r === null || r === void 0 ? void 0 : r.record_name) === 'credits' &&
164
168
  typeof (r === null || r === void 0 ? void 0 : r.microcredits) === 'string');
@@ -169,19 +173,25 @@ const buildCreditsBalance = (records, networkId, priceFor) => {
169
173
  // below Number.MAX_SAFE_INTEGER for a single wallet (max u64 supply is
170
174
  // 1.5B credits = 1.5e15 microcredits; Number can hold up to ~9e15).
171
175
  const rawBalance = Number(totalMicrocredits);
176
+ const rawBalanceString = totalMicrocredits.toString();
172
177
  const balance = rawBalance / MICROCREDITS_PER_CREDIT;
173
- const price = priceFor('0x0', true);
178
+ const curated = lookupCurated('0x0', true);
174
179
  return {
175
180
  address: '0x0',
176
181
  balance,
177
182
  decimals: 6,
178
183
  isNative: true,
179
- logoURI: ALEO_CREDITS_LOGO,
180
- marketValue: price !== null ? balance * price : undefined,
184
+ // Prefer the server-curated logo (single source of truth for both
185
+ // the unshielded multichain feed and the shielded tab); fall back
186
+ // to the bundled ALEO glyph for older redcoast deployments that
187
+ // don't yet send `logoURI` on the prices response.
188
+ logoURI: (_a = curated.logoURI) !== null && _a !== void 0 ? _a : ALEO_CREDITS_LOGO,
189
+ marketValue: curated.price !== null ? balance * curated.price : undefined,
181
190
  name: 'Aleo Credits',
182
191
  networkId,
183
- price: price !== null ? price : undefined,
192
+ price: curated.price !== null ? curated.price : undefined,
184
193
  rawBalance,
194
+ rawBalanceString,
185
195
  symbol: 'ALEO',
186
196
  };
187
197
  };
@@ -191,8 +201,8 @@ const buildCreditsBalance = (records, networkId, priceFor) => {
191
201
  * match a curated spec, lack an `amount`, or have a malformed amount
192
202
  * string are silently skipped.
193
203
  */
194
- const buildTokenBalances = (records, networkId, priceFor) => {
195
- var _a, _b;
204
+ const buildTokenBalances = (records, networkId, lookupCurated) => {
205
+ var _a, _b, _c;
196
206
  const sumsByContract = new Map();
197
207
  const specsByContract = new Map();
198
208
  for (const r of records) {
@@ -204,7 +214,7 @@ const buildTokenBalances = (records, networkId, priceFor) => {
204
214
  sumsByContract.set(spec.contractAddress, prev + BigInt(r.amount));
205
215
  specsByContract.set(spec.contractAddress, spec);
206
216
  }
207
- catch (_c) {
217
+ catch (_d) {
208
218
  /* ignore — malformed amount string */
209
219
  }
210
220
  }
@@ -214,22 +224,26 @@ const buildTokenBalances = (records, networkId, priceFor) => {
214
224
  if (!spec)
215
225
  continue;
216
226
  const rawBalance = Number(total);
227
+ const rawBalanceString = total.toString();
217
228
  const balance = rawBalance / Math.pow(10, spec.decimals);
218
- const price = priceFor(spec.contractAddress, false);
229
+ const curated = lookupCurated(spec.contractAddress, false);
219
230
  out.push({
220
231
  address: spec.contractAddress,
221
232
  balance,
222
233
  decimals: spec.decimals,
223
- // Use redcoast's `DEFAULT_TOKEN_LOGO_URI` for stablecoins + ARC-21
224
- // so they render the same dark "?" icon the Unshielded tab shows
225
- // for these tokens (the multichain endpoint applies that fallback
226
- // itself; we mirror it here for visual parity).
227
- logoURI: (_b = spec.logoURI) !== null && _b !== void 0 ? _b : UNKNOWN_TOKEN_LOGO,
228
- marketValue: price !== null ? balance * price : undefined,
234
+ // Prefer the server-curated logo so the Shielded tab renders the
235
+ // same brand glyph the Unshielded tab gets from the multichain
236
+ // feed. Fall back to the local spec override (kept for tests +
237
+ // any future curated-but-not-yet-on-server tokens), then to the
238
+ // generic dark `?` placeholder for older redcoast deployments
239
+ // that haven't rolled out the logo response field yet.
240
+ logoURI: (_c = (_b = curated.logoURI) !== null && _b !== void 0 ? _b : spec.logoURI) !== null && _c !== void 0 ? _c : UNKNOWN_TOKEN_LOGO,
241
+ marketValue: curated.price !== null ? balance * curated.price : undefined,
229
242
  name: spec.name,
230
243
  networkId,
231
- price: price !== null ? price : undefined,
244
+ price: curated.price !== null ? curated.price : undefined,
232
245
  rawBalance,
246
+ rawBalanceString,
233
247
  symbol: spec.symbol,
234
248
  });
235
249
  }
@@ -300,12 +314,12 @@ const useAleoShieldedBalances = () => {
300
314
  : Promise.resolve([]),
301
315
  ]);
302
316
  const records = (_a = result === null || result === void 0 ? void 0 : result.records) !== null && _a !== void 0 ? _a : [];
303
- const priceFor = buildPriceLookup(priceList);
317
+ const lookupCurated = buildCuratedTokenLookup(priceList);
304
318
  const balances = [];
305
- const credits = buildCreditsBalance(records, safeNetworkId, priceFor);
319
+ const credits = buildCreditsBalance(records, safeNetworkId, lookupCurated);
306
320
  if (credits)
307
321
  balances.push(credits);
308
- balances.push(...buildTokenBalances(records, safeNetworkId, priceFor));
322
+ balances.push(...buildTokenBalances(records, safeNetworkId, lookupCurated));
309
323
  setTokenBalances(balances);
310
324
  }
311
325
  catch (err) {
@@ -12,7 +12,9 @@ var useInternalDynamicContext = require('../../../../context/DynamicContext/useD
12
12
  var useTokenBalances = require('../../../../utils/hooks/useTokenBalances/useTokenBalances.cjs');
13
13
  var useAleoShieldedBalances = require('../../../../utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.cjs');
14
14
  var useAleoAutoMergeRecords = require('../../../../utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.cjs');
15
+ var buildTokenKey = require('../../../../utils/hooks/useAleoAutoShieldSponsoredTokens/buildTokenKey.cjs');
15
16
  var useAleoAutoShieldSponsoredTokens = require('../../../../utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.cjs');
17
+ var pollOnShielded = require('../../../../utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.cjs');
16
18
  var AccordionItem = require('../../../../components/Accordion/components/AccordionItem/AccordionItem.cjs');
17
19
  require('@dynamic-labs/iconic');
18
20
  require('@dynamic-labs/wallet-connector-core');
@@ -193,7 +195,7 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
193
195
  // `shieldToken` for tokens whose `transfer_public_to_private` is
194
196
  // Feemaster-sponsored. Unsponsored tokens stay on the manual Shield
195
197
  // Manually CTA, which prompts the user-paid confirmation modal.
196
- const { isShielding: isAutoShielding } = useAleoAutoShieldSponsoredTokens.useAleoAutoShieldSponsoredTokens({
198
+ const { isShielding: isAutoShielding, currentlyShieldingTokenKeys: autoShieldingTokenKeys, } = useAleoAutoShieldSponsoredTokens.useAleoAutoShieldSponsoredTokens({
197
199
  accountAddress: primaryWallet === null || primaryWallet === void 0 ? void 0 : primaryWallet.address,
198
200
  onShielded: React.useCallback(() => _tslib.__awaiter(void 0, void 0, void 0, function* () {
199
201
  yield fetchAccountBalances(true);
@@ -202,6 +204,16 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
202
204
  shieldHandle: aleoShieldHandle,
203
205
  unshieldedTokenBalances: unshieldedTokenBalances,
204
206
  });
207
+ // Liveness handle for the manual post-shield poll — set to true when
208
+ // the component unmounts so `pollOnShielded` exits its backoff loop
209
+ // instead of firing `fetchAccountBalances` on a torn-down tree.
210
+ const isUnmountedRef = React.useRef(false);
211
+ React.useEffect(() => {
212
+ isUnmountedRef.current = false;
213
+ return () => {
214
+ isUnmountedRef.current = true;
215
+ };
216
+ }, []);
205
217
  // Token currently awaiting user-paid fee confirmation. Set when
206
218
  // Shield Manually is clicked on a token Feemaster doesn't sponsor;
207
219
  // cleared on Cancel or after the modal's Shield button dispatches.
@@ -218,12 +230,14 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
218
230
  if (atomic <= BigInt(0))
219
231
  return;
220
232
  setShieldingAddress(token.address);
233
+ let didBroadcast = false;
221
234
  try {
222
235
  yield aleoShieldHandle.shieldToken({
223
236
  amount: atomic,
224
237
  isNative: token.isNative,
225
238
  tokenAddress: token.address,
226
239
  });
240
+ didBroadcast = true;
227
241
  // Refresh the unshielded list so the just-shielded balance drops to
228
242
  // 0 in the redcoast feed. The shielded side will pick the new
229
243
  // record up on its own polling once the RecordScanner indexes.
@@ -236,6 +250,20 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
236
250
  finally {
237
251
  setShieldingAddress(undefined);
238
252
  }
253
+ // The relay-accepted broadcast resolves *before* the
254
+ // public→private transition finalizes on-chain, so the immediate
255
+ // refresh above sees the pre-confirmation balance. Re-poll on the
256
+ // shared backoff so the Unshielded row visibly drops to 0 and the
257
+ // Shielded row picks the new record up without a manual reload.
258
+ // Polling is best-effort — errors are swallowed inside
259
+ // `pollOnShielded`. Skip when the broadcast itself failed; there's
260
+ // nothing new to converge on.
261
+ if (!didBroadcast)
262
+ return;
263
+ yield pollOnShielded.pollOnShielded(() => _tslib.__awaiter(void 0, void 0, void 0, function* () {
264
+ yield fetchAccountBalances(true);
265
+ yield refetchShielded();
266
+ }), () => isUnmountedRef.current);
239
267
  }), [aleoShieldHandle, fetchAccountBalances, refetchShielded, shieldingAddress]);
240
268
  const getSecondaryAction = React.useCallback((token) => {
241
269
  // Only on the Unshielded tab, only when the connector exposes shield
@@ -252,6 +280,15 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
252
280
  return undefined;
253
281
  if (!token.rawBalance || token.rawBalance <= 0)
254
282
  return undefined;
283
+ // Hide the CTA while auto-shield is mid-broadcast for this token.
284
+ // The connector dedupes regardless, but suppressing the button
285
+ // prevents two click-then-spinner cycles for a single underlying
286
+ // shield. Once auto-shield resolves, the row re-renders with the
287
+ // freshly-zero balance, the `rawBalance <= 0` guard above kicks in,
288
+ // and the CTA stays hidden until a new unshielded balance arrives.
289
+ const tokenKey = buildTokenKey.buildTokenKey(token);
290
+ if (tokenKey && autoShieldingTokenKeys.has(tokenKey))
291
+ return undefined;
255
292
  return {
256
293
  dataTestId: `shield-manually-${token.symbol}`,
257
294
  isLoading: shieldingAddress === token.address,
@@ -282,7 +319,13 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
282
319
  });
283
320
  },
284
321
  };
285
- }, [activeShieldTab, aleoShieldHandle, handleShieldToken, shieldingAddress]);
322
+ }, [
323
+ activeShieldTab,
324
+ aleoShieldHandle,
325
+ autoShieldingTokenKeys,
326
+ handleShieldToken,
327
+ shieldingAddress,
328
+ ]);
286
329
  const tokenBalances = React.useMemo(() => supportsShielded && activeShieldTab === 'shielded'
287
330
  ? shieldedTokenBalances
288
331
  : unshieldedTokenBalances, [
@@ -1,14 +1,16 @@
1
1
  'use client'
2
2
  import { __awaiter } from '../../../../../../_virtual/_tslib.js';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
- import { useState, useRef, useMemo, useCallback } from 'react';
4
+ import { useState, useRef, useMemo, useCallback, useEffect } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import '../../../../context/DynamicContext/useDynamicContext/useDynamicContext.js';
7
7
  import { useInternalDynamicContext } from '../../../../context/DynamicContext/useDynamicContext/useInternalDynamicContext/useInternalDynamicContext.js';
8
8
  import { useTokenBalances } from '../../../../utils/hooks/useTokenBalances/useTokenBalances.js';
9
9
  import { useAleoShieldedBalances } from '../../../../utils/hooks/useAleoShieldedBalances/useAleoShieldedBalances.js';
10
10
  import { useAleoAutoMergeRecords } from '../../../../utils/hooks/useAleoAutoMergeRecords/useAleoAutoMergeRecords.js';
11
+ import { buildTokenKey } from '../../../../utils/hooks/useAleoAutoShieldSponsoredTokens/buildTokenKey.js';
11
12
  import { useAleoAutoShieldSponsoredTokens } from '../../../../utils/hooks/useAleoAutoShieldSponsoredTokens/useAleoAutoShieldSponsoredTokens.js';
13
+ import { pollOnShielded } from '../../../../utils/hooks/useAleoAutoShieldSponsoredTokens/pollOnShielded.js';
12
14
  import { AccordionItem } from '../../../../components/Accordion/components/AccordionItem/AccordionItem.js';
13
15
  import '@dynamic-labs/iconic';
14
16
  import '@dynamic-labs/wallet-connector-core';
@@ -189,7 +191,7 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
189
191
  // `shieldToken` for tokens whose `transfer_public_to_private` is
190
192
  // Feemaster-sponsored. Unsponsored tokens stay on the manual Shield
191
193
  // Manually CTA, which prompts the user-paid confirmation modal.
192
- const { isShielding: isAutoShielding } = useAleoAutoShieldSponsoredTokens({
194
+ const { isShielding: isAutoShielding, currentlyShieldingTokenKeys: autoShieldingTokenKeys, } = useAleoAutoShieldSponsoredTokens({
193
195
  accountAddress: primaryWallet === null || primaryWallet === void 0 ? void 0 : primaryWallet.address,
194
196
  onShielded: useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
195
197
  yield fetchAccountBalances(true);
@@ -198,6 +200,16 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
198
200
  shieldHandle: aleoShieldHandle,
199
201
  unshieldedTokenBalances: unshieldedTokenBalances,
200
202
  });
203
+ // Liveness handle for the manual post-shield poll — set to true when
204
+ // the component unmounts so `pollOnShielded` exits its backoff loop
205
+ // instead of firing `fetchAccountBalances` on a torn-down tree.
206
+ const isUnmountedRef = useRef(false);
207
+ useEffect(() => {
208
+ isUnmountedRef.current = false;
209
+ return () => {
210
+ isUnmountedRef.current = true;
211
+ };
212
+ }, []);
201
213
  // Token currently awaiting user-paid fee confirmation. Set when
202
214
  // Shield Manually is clicked on a token Feemaster doesn't sponsor;
203
215
  // cleared on Cancel or after the modal's Shield button dispatches.
@@ -214,12 +226,14 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
214
226
  if (atomic <= BigInt(0))
215
227
  return;
216
228
  setShieldingAddress(token.address);
229
+ let didBroadcast = false;
217
230
  try {
218
231
  yield aleoShieldHandle.shieldToken({
219
232
  amount: atomic,
220
233
  isNative: token.isNative,
221
234
  tokenAddress: token.address,
222
235
  });
236
+ didBroadcast = true;
223
237
  // Refresh the unshielded list so the just-shielded balance drops to
224
238
  // 0 in the redcoast feed. The shielded side will pick the new
225
239
  // record up on its own polling once the RecordScanner indexes.
@@ -232,6 +246,20 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
232
246
  finally {
233
247
  setShieldingAddress(undefined);
234
248
  }
249
+ // The relay-accepted broadcast resolves *before* the
250
+ // public→private transition finalizes on-chain, so the immediate
251
+ // refresh above sees the pre-confirmation balance. Re-poll on the
252
+ // shared backoff so the Unshielded row visibly drops to 0 and the
253
+ // Shielded row picks the new record up without a manual reload.
254
+ // Polling is best-effort — errors are swallowed inside
255
+ // `pollOnShielded`. Skip when the broadcast itself failed; there's
256
+ // nothing new to converge on.
257
+ if (!didBroadcast)
258
+ return;
259
+ yield pollOnShielded(() => __awaiter(void 0, void 0, void 0, function* () {
260
+ yield fetchAccountBalances(true);
261
+ yield refetchShielded();
262
+ }), () => isUnmountedRef.current);
235
263
  }), [aleoShieldHandle, fetchAccountBalances, refetchShielded, shieldingAddress]);
236
264
  const getSecondaryAction = useCallback((token) => {
237
265
  // Only on the Unshielded tab, only when the connector exposes shield
@@ -248,6 +276,15 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
248
276
  return undefined;
249
277
  if (!token.rawBalance || token.rawBalance <= 0)
250
278
  return undefined;
279
+ // Hide the CTA while auto-shield is mid-broadcast for this token.
280
+ // The connector dedupes regardless, but suppressing the button
281
+ // prevents two click-then-spinner cycles for a single underlying
282
+ // shield. Once auto-shield resolves, the row re-renders with the
283
+ // freshly-zero balance, the `rawBalance <= 0` guard above kicks in,
284
+ // and the CTA stays hidden until a new unshielded balance arrives.
285
+ const tokenKey = buildTokenKey(token);
286
+ if (tokenKey && autoShieldingTokenKeys.has(tokenKey))
287
+ return undefined;
251
288
  return {
252
289
  dataTestId: `shield-manually-${token.symbol}`,
253
290
  isLoading: shieldingAddress === token.address,
@@ -278,7 +315,13 @@ const ActiveWalletBalance = ({ isLoading = false, }) => {
278
315
  });
279
316
  },
280
317
  };
281
- }, [activeShieldTab, aleoShieldHandle, handleShieldToken, shieldingAddress]);
318
+ }, [
319
+ activeShieldTab,
320
+ aleoShieldHandle,
321
+ autoShieldingTokenKeys,
322
+ handleShieldToken,
323
+ shieldingAddress,
324
+ ]);
282
325
  const tokenBalances = useMemo(() => supportsShielded && activeShieldTab === 'shielded'
283
326
  ? shieldedTokenBalances
284
327
  : unshieldedTokenBalances, [