fix(keychain): surface real broadcast errors and retry via a known-good node#823
Conversation
…od node Extension broadcast failures threw a hardcoded "Extension broadcast failed", discarding the extension's actual message/error. That hid the real cause and, because the SDK classifies auth errors by string-matching the message, defeated the auth-upgrade fallback (a generic string never matches "missing active authority"). - Extend TxResponse with the optional message/error fields the extensions actually return. - Surface them via a shared extensionErrorMessage() helper, used by the unified hive-extensions layer and the legacy keychain.ts call sites. - broadcastViaKeychain retries a failed broadcast once through the first node in the curated public-nodes.json, so a flaky/outaged default node (e.g. the common api.hive.blog default during maintenance) no longer hard-blocks the user. Gated via isUserCancellation() so a user-cancel never re-opens the popup, and only when the caller didn't pin a node. - Promote modal: catch the mutateAsync rejection so a failed/declined sign no longer surfaces as an "Uncaught (in promise)", and show the reason inline. - Add unit tests for extensionErrorMessage / isUserCancellation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6378c87687
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (fallbackNode && !isUserCancellation(response)) { | ||
| return attempt(fallbackNode).then((retry) => { |
There was a problem hiding this comment.
Retry fallback only for node/transport failures
The retry path is currently triggered for every non-cancellation broadcast failure, so deterministic chain errors (for example, missing required active authority or RC/validation failures) will re-open the extension approval popup and run the same transaction again even though changing RPC nodes cannot fix it. This can also hide the original actionable error if the user cancels the second prompt. Gate this retry to node/transport outage signatures and otherwise throw the first failure immediately.
Useful? React with 👍 / 👎.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (13)
📒 Files selected for processing (19)
📝 WalkthroughWalkthroughThis PR adds normalized extension error utilities and tests, applies them across Keychain helpers and Hive extension broadcasts (including a one-shot fallback RPC retry), surfaces mutation errors in Promote, introduces invalidateAfterBroadcast, and switches many SDK mutation post-broadcast invalidations to use it while changing broadcastMode default to 'async'. ChangesKeychain error handling and RPC failover
Sequence Diagram(s)sequenceDiagram
participant Client
participant HiveExtensions
participant KeychainRPC
participant FallbackRPC
Client->>HiveExtensions: broadcast(tx, rpc?)
HiveExtensions->>KeychainRPC: attempt broadcast on primary
alt success
KeychainRPC-->>Client: return result
else failure and rpc == null and not user-cancel and isRetryableNodeError
HiveExtensions->>FallbackRPC: retry on fallback RPC
alt success
FallbackRPC-->>Client: return result
else failure
HiveExtensions-->>Client: reject(extensionErrorMessage)
end
else user cancellation or rpc pinned
HiveExtensions-->>Client: reject(extensionErrorMessage)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/src/specs/utils/extension-error.spec.ts (1)
39-45: ⚡ Quick winAdd an object-shaped cancellation case to the positive matrix.
Please add a case like
{ error: { message: "user_cancel" } }so cancellation detection won’t regress for non-string extension payloads.Suggested test addition
it.each([ [{ error: "user_cancel" }], + [{ error: { message: "user_cancel" } }], [{ message: "Request was canceled by the user." }], [{ message: "User declined the transaction" }], ])("treats %o as a cancellation", (resp) => { expect(isUserCancellation(resp)).toBe(true); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/specs/utils/extension-error.spec.ts` around lines 39 - 45, Add an extra positive test case to the existing it.each matrix in extension-error.spec.ts to cover object-shaped payloads so isUserCancellation continues to detect cancellations for non-string extension payloads; specifically, add a case like { error: { message: "user_cancel" } } to the array passed to it.each so the test invoking isUserCancellation(resp) expects true for that shape as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/utils/extension-error.ts`:
- Around line 47-53: isUserCancellation currently only checks resp.error when
it's a string, so object-shaped errors (e.g., { message: "user_cancel" } or
other serialized payloads) are missed; update isUserCancellation to derive a
normalized string from resp.error even when it's an object by checking
resp.error.message, resp.error.type, or JSON.stringify(resp.error) and
lowercasing that value (e.g., compute errVal from typeof resp.error === "string"
? resp.error : resp.error?.message ?? resp.error?.type ??
JSON.stringify(resp.error) ?? ""), then use errVal alongside resp.message to
detect "cancel"/"user_cancel"/"declined" occurrences; keep the existing boolean
checks but replace use of resp.error with the normalized errVal to ensure
object-shaped cancellation errors are recognized.
---
Nitpick comments:
In `@apps/web/src/specs/utils/extension-error.spec.ts`:
- Around line 39-45: Add an extra positive test case to the existing it.each
matrix in extension-error.spec.ts to cover object-shaped payloads so
isUserCancellation continues to detect cancellations for non-string extension
payloads; specifically, add a case like { error: { message: "user_cancel" } } to
the array passed to it.each so the test invoking isUserCancellation(resp)
expects true for that shape as well.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bf43cf7a-b797-4c9a-b494-886ca5e34091
📒 Files selected for processing (6)
apps/web/src/features/shared/promote/index.tsxapps/web/src/specs/utils/extension-error.spec.tsapps/web/src/types/keychain-impl.tsapps/web/src/utils/extension-error.tsapps/web/src/utils/hive-extensions.tsapps/web/src/utils/keychain.ts
Greptile SummaryThis PR fixes two layered issues in extension-based broadcast flows: the real extension error was previously discarded in favour of a hardcoded string, which also blocked the SDK's
Confidence Score: 5/5Safe to merge. Error surfacing is additive, the retry is gated by two independent guards, and the promote modal now catches rejections instead of propagating them unhandled. All issues from previous review threads appear addressed: isRetryableNodeError prevents retrying on RC/authority failures, the first-attempt error is re-thrown on resolved-failure retries, resetPromote() clears stale errors before each submission, and object-shaped cancellation codes are handled through normalizeErrorText. The two remaining suggestions are minor defensive hardening, not present defects on the happy or common-failure paths. apps/web/src/utils/hive-extensions.ts — the retry rejection path (fallback node timeout) still propagates the fallback node's error instead of the first attempt's. packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts — the fire-and-forget setTimeout could silently swallow an invalidateQueries exception. Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as Promote Modal
participant EXT as Browser Extension
participant NODE1 as User Keychain Node
participant NODE2 as Fallback Node
UI->>EXT: "requestBroadcast(ops, rpc=null)"
EXT->>NODE1: broadcast_transaction
alt Node outage / transport failure
NODE1-->>EXT: 502 / ECONNREFUSED
EXT-->>UI: "success=false, message=status code 502"
Note over UI: isRetryableNodeError=true AND not cancel
UI->>EXT: "requestBroadcast(ops, rpc=fallbackNode)"
EXT->>NODE2: broadcast_transaction
NODE2-->>EXT: ok
EXT-->>UI: "success=true"
UI->>UI: setStep(2)
else User cancels
EXT-->>UI: "success=false, error=user_cancel"
Note over UI: isUserCancellation=true — no retry
UI->>UI: promoteError shown inline
else Deterministic chain error
NODE1-->>EXT: missing required active authority
EXT-->>UI: "success=false, message=missing required active authority"
Note over UI: isRetryableNodeError=false — no retry
UI->>UI: SDK auth-upgrade flow triggers
end
Reviews (5): Last reviewed commit: "chore: apply changeset versioning for PR..." | Re-trigger Greptile |
…iew)
Addresses PR review feedback:
- Only retry the broadcast through the fallback node on node/transport failures
(new isRetryableNodeError) — not on deterministic chain errors (missing
authority, RC, already-broadcast), which fail identically on any node and
would needlessly re-open the extension popup.
- On a failed retry, surface the FIRST attempt's error (more semantically
relevant — e.g. lets the SDK detect missing-authority) instead of the
fallback node's error.
- isUserCancellation now handles object-shaped errors (e.g. { code: 4001,
message: "User rejected request" }) and "reject"/"declined" wording.
- Promote modal: reset the mutation error before each attempt and don't render a
stale promote error alongside a live preCheck error.
- Extend tests for object cancellations and isRetryableNodeError.
Switch the default BroadcastMode from 'sync' to 'async' so the SDK uses broadcast_transaction instead of the deprecated broadcast_transaction_synchronous. Transport/RPC errors are still thrown immediately; only the block-inclusion wait is dropped. 'sync' remains available as an explicit opt-in and broadcastOperations is marked @deprecated. No callers currently pass an explicit broadcastMode, and block_num consumers already use optional chaining.
The sync→async default flip introduced a stale-read window: hooks invalidated chain-derived queries (balances, account, follow state) synchronously in onSuccess, but async broadcast resolves at mempool acceptance — before the tx is in a block — so the refetch returned pre-transaction state. - Add invalidateAfterBroadcast(adapter, mode, keys): refetch immediately for 'sync', defer ~1 block (4s) for 'async'/default. - Route all 28 balance/chain-state mutation hooks through it. This also fixes the 3 hooks whose existing async-delay branched on the hook's local broadcastMode (undefined when omitted), bypassing the delay while the broadcast ran async. - Fix use-claim-account invalidation key: ["accounts", creator] matched no query (account-full is keyed get-account-full); use QueryKeys.accounts.full(creator).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts`:
- Around line 22-26: The exported function invalidateAfterBroadcast currently
types the keys parameter as any[][]; change this to a non-any shape such as
unknown[][] (or Array<Array<unknown>>) so the public API is strictly typed while
remaining compatible with PlatformAdapter.invalidateQueries; update the function
signature in invalidateAfterBroadcast(..., keys: unknown[][]) and ensure any
other references/overloads use the same unknown[][] type before forwarding keys
to adapter.invalidateQueries(keys).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0279ba46-a14e-4247-b735-45b16097b23c
📒 Files selected for processing (36)
apps/web/src/features/shared/promote/index.tsxapps/web/src/specs/utils/extension-error.spec.tsapps/web/src/utils/extension-error.tsapps/web/src/utils/hive-extensions.tspackages/sdk/src/modules/accounts/mutations/use-account-update.tspackages/sdk/src/modules/accounts/mutations/use-claim-account.tspackages/sdk/src/modules/accounts/mutations/use-create-account.tspackages/sdk/src/modules/accounts/mutations/use-follow.tspackages/sdk/src/modules/accounts/mutations/use-grant-posting-permission.tspackages/sdk/src/modules/accounts/mutations/use-unfollow.tspackages/sdk/src/modules/core/hive-tx.tspackages/sdk/src/modules/core/mutations/index.tspackages/sdk/src/modules/core/mutations/invalidate-after-broadcast.tspackages/sdk/src/modules/core/mutations/use-broadcast-mutation.tspackages/sdk/src/modules/wallet/mutations/use-claim-engine-rewards.tspackages/sdk/src/modules/wallet/mutations/use-claim-interest.tspackages/sdk/src/modules/wallet/mutations/use-convert.tspackages/sdk/src/modules/wallet/mutations/use-delegate-engine-token.tspackages/sdk/src/modules/wallet/mutations/use-delegate-rc.tspackages/sdk/src/modules/wallet/mutations/use-delegate-vesting-shares.tspackages/sdk/src/modules/wallet/mutations/use-engine-market-order.tspackages/sdk/src/modules/wallet/mutations/use-lock-larynx.tspackages/sdk/src/modules/wallet/mutations/use-power-larynx.tspackages/sdk/src/modules/wallet/mutations/use-set-withdraw-vesting-route.tspackages/sdk/src/modules/wallet/mutations/use-stake-engine-token.tspackages/sdk/src/modules/wallet/mutations/use-transfer-engine-token.tspackages/sdk/src/modules/wallet/mutations/use-transfer-from-savings.tspackages/sdk/src/modules/wallet/mutations/use-transfer-larynx.tspackages/sdk/src/modules/wallet/mutations/use-transfer-point.tspackages/sdk/src/modules/wallet/mutations/use-transfer-spk.tspackages/sdk/src/modules/wallet/mutations/use-transfer-to-savings.tspackages/sdk/src/modules/wallet/mutations/use-transfer-to-vesting.tspackages/sdk/src/modules/wallet/mutations/use-transfer.tspackages/sdk/src/modules/wallet/mutations/use-undelegate-engine-token.tspackages/sdk/src/modules/wallet/mutations/use-unstake-engine-token.tspackages/sdk/src/modules/wallet/mutations/use-withdraw-vesting.ts
✅ Files skipped from review due to trivial changes (1)
- packages/sdk/src/modules/core/hive-tx.ts
| export function invalidateAfterBroadcast( | ||
| adapter: PlatformAdapter | null | undefined, | ||
| broadcastMode: BroadcastMode | undefined, | ||
| keys: any[][] | ||
| ): void | Promise<void> { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate PlatformAdapter.invalidateQueries signature
rg -n --type=ts -C3 'interface\s+PlatformAdapter|type\s+PlatformAdapter|invalidateQueries\s*\('
# Locate invalidateAfterBroadcast signature/usages
rg -n --type=ts -C2 '\binvalidateAfterBroadcast\s*\('
rg -n --type=ts -C2 '\bkeys:\s*any\[\]\[\]'Repository: ecency/vision-next
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Show the current signature in the reviewed file
sed -n '1,120p' packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts | nl -ba | sed -n '1,120p'
# 2) Show the relevant lines around keys type (if file is larger, this still should include it)
rg -n --no-heading 'export function invalidateAfterBroadcast|keys:' packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts -n -S
# 3) Print PlatformAdapter.invalidateQueries signature from source
rg -n --no-heading 'invalidateQueries\s*\(' packages/sdk/src/modules/core/types/platform-adapter.ts -n -S
sed -n '1,120p' packages/sdk/src/modules/core/types/platform-adapter.ts | nl -ba | sed -n '1,140p'
# 4) Find call sites of invalidateAfterBroadcast and inspect how keys are passed
rg -n --no-heading '\binvalidateAfterBroadcast\s*\(' packages/sdk/src -S
# 5) Find any usage that passes any[][]-shaped values (fallback: locate "keys" variable near calls)
rg -n --no-heading -C3 '\binvalidateAfterBroadcast\s*\(' packages/sdk/src | head -n 200Repository: ecency/vision-next
Length of output: 105
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts"
# 1) Print the exact function signature block (around the export) with line numbers via awk
awk 'NR>=1 && NR<=120 {print NR ":" $0}' "$FILE" | rg -n 'invalidateAfterBroadcast|keys:' -n -S || true
# Also print a wider window around where the signature is
SIG_LINE=$(rg -n --no-heading 'export function invalidateAfterBroadcast' "$FILE" | cut -d: -f1 | head -n1 || true)
if [[ -n "${SIG_LINE:-}" ]]; then
START=$((SIG_LINE-10)); END=$((SIG_LINE+20))
awk -v s="$START" -v e="$END" 'NR>=s && NR<=e {print NR ":" $0}' "$FILE"
fi
# 2) Print the invalidateQueries type from PlatformAdapter
PA="packages/sdk/src/modules/core/types/platform-adapter.ts"
rg -n --no-heading 'invalidateQueries\s*\(' "$PA" -S
awk 'NR>=1 && NR<=120 {print NR ":" $0}' "$PA" | rg -n 'interface PlatformAdapter|invalidateQueries|queryKey' -n -S || true
# 3) Locate call sites of invalidateAfterBroadcast (inspect how keys are passed)
rg -n --no-heading '\binvalidateAfterBroadcast\s*\(' packages/sdk/src -S
# 4) For each call site, print the surrounding lines (small windows)
rg -n --no-heading '\binvalidateAfterBroadcast\s*\(' packages/sdk/src -S | cut -d: -f1 | head -n 20 | while read -r line; do
# line numbers refer to file-local, so we need file too. We'll re-grep per file for that line.
:
done
# Better approach: print small contexts directly around matches
rg -n -C4 --no-heading '\binvalidateAfterBroadcast\s*\(' packages/sdk/src -S | head -n 200Repository: ecency/vision-next
Length of output: 1014
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show exact PlatformAdapter.invalidateQueries signature/type
PA="packages/sdk/src/modules/core/types/platform-adapter.ts"
rg -n --no-heading 'invalidateQueries\s*\(' "$PA" -S
awk 'NR>=1 && NR<=220 {print NR ":" $0}' "$PA" | rg -n 'invalidateQueries|queryKey|QueryKey|PartialMatchKey|prefix|interface PlatformAdapter' -n -S || true
# Show all call sites of invalidateAfterBroadcast (small windows)
rg -n -C4 --no-heading '\binvalidateAfterBroadcast\s*\(' packages/sdk/src -S
rg -n -C4 --no-heading '\binvalidateAfterBroadcast\s*\(' packages -SRepository: ecency/vision-next
Length of output: 44
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Show PlatformAdapter interface section including invalidateQueries type
PA="packages/sdk/src/modules/core/types/platform-adapter.ts"
IFACE_LINE=$(rg -n --no-heading 'interface PlatformAdapter' "$PA" | head -n1 | cut -d: -f1 || true)
if [[ -n "${IFACE_LINE:-}" ]]; then
START=$((IFACE_LINE-10)); END=$((IFACE_LINE+120))
awk -v s="$START" -v e="$END" 'NR>=s && NR<=e {print NR ":" $0}' "$PA"
fi
# 2) Locate any usages of invalidateAfterBroadcast
rg -n --no-heading '\binvalidateAfterBroadcast\s*\(' packages -S | head -n 50 || true
# 3) If there are usages, inspect those files' keys argument shapes
# (prints 8 lines before/after each match)
rg -n -C8 --no-heading '\binvalidateAfterBroadcast\s*\(' packages -S | head -n 200 || trueRepository: ecency/vision-next
Length of output: 28798
🏁 Script executed:
#!/bin/bash
set -euo pipefail
PA="packages/sdk/src/modules/core/types/platform-adapter.ts"
# Show the invalidateQueries member block (likely later in file)
line=$(rg -n --no-heading 'invalidateQueries\s*' "$PA" | head -n 1 | cut -d: -f1 || true)
if [[ -n "${line:-}" ]]; then
START=$((line-20)); END=$((line+60))
awk -v s="$START" -v e="$END" 'NR>=s && NR<=e {print NR ":" $0}' "$PA"
else
echo "No invalidateQueries found in $PA"
fi
# Also show entire type of QueryKey if present (optional)
rg -n --no-heading 'QueryKey|PartialMatchKey|queryKey\s*:' "$PA" -SRepository: ecency/vision-next
Length of output: 2904
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find adapter implementations / assignments of invalidateQueries
rg -n --no-heading 'invalidateQueries\s*:\s*\(' packages -S || true
rg -n --no-heading 'invalidateQueries\s*\?' packages/sdk/src -S || true
rg -n --no-heading '\binvalidateQueries\s*=' packages -S || true
# Also find any casts/usages of invalidateAfterBroadcast keys that might affect typing
rg -n --no-heading 'invalidateAfterBroadcast\s*\(' packages/sdk/src/modules -SRepository: ecency/vision-next
Length of output: 4171
Remove explicit any from invalidateAfterBroadcast keys parameter
packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts exports invalidateAfterBroadcast(..., keys: any[][]) and forwards keys to adapter.invalidateQueries(keys). Replace any[][] with a non-any key shape (e.g., unknown[][] / Array<Array<unknown>>) so the helper’s public API is strictly typed (while still matching the current PlatformAdapter.invalidateQueries parameter type).
🧰 Tools
🪛 ESLint
[error] 25-25: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/sdk/src/modules/core/mutations/invalidate-after-broadcast.ts` around
lines 22 - 26, The exported function invalidateAfterBroadcast currently types
the keys parameter as any[][]; change this to a non-any shape such as
unknown[][] (or Array<Array<unknown>>) so the public API is strictly typed while
remaining compatible with PlatformAdapter.invalidateQueries; update the function
signature in invalidateAfterBroadcast(..., keys: unknown[][]) and ensure any
other references/overloads use the same unknown[][] type before forwarding keys
to adapter.invalidateQueries(keys).
- isRetryableNodeError: drop the over-broad bare "network" and the 500/"internal server error" signals (they can wrap deterministic chain rejections); keep specific transport/gateway signatures (ECONN*, ETIMEDOUT, network error, failed to fetch, 502/503/504, bad gateway, origin servers unavailable, ...). - invalidateAfterBroadcast: call adapter.invalidateQueries as a method instead of extracting it, so `this` stays bound. - Tests for the tightened classification.
The SDK ships via tsup (no type-check), so type errors had accumulated. Fixed all of them; 366 tests still pass. Production (5): ByteBuffer Uint8Array->BufferSource casts; drop unused Int8/Int32 serializers; precise non-null array result in filterDmcaEntry. Tests (107): cast loosely-invoked queryFn in query-option specs; assert possibly-undefined array/profile access in parse-accounts; SMTAsset.amount is a string (matches the Hive API; parseAsset parseFloats it) so snapshots are unchanged; loosen profile-metadata token literals; drop unused imports.
Problem
Promoting a post (or any active-authority op) signed with a browser extension could fail in the console with:
Two layered issues:
success: false,broadcastViaKeychainthrew a hardcoded"Extension broadcast failed"and ignored the extension's actualmessage/error(node error, missing key, RC, or a plain user-cancel). TheTxResponsetype only declared{ success, result }, so those fields weren't even visible.parseChainError→/missing active authority/i). A generic string matches nothing, so a recoverable auth failure hard-failed instead of showing the upgrade dialog.awaitedmutateAsyncwith notry/catch.Fix
TxResponsewith the optionalmessage/errorfields the extensions actually return.extensionErrorMessage()helper and surface the real reason in the unifiedhive-extensionslayer and the legacykeychain.tscall sites.broadcastViaKeychainnow retries a failed broadcast once through the first (preferred) node in the curatedpublic-nodes.json, so a flaky/outaged default node (e.g. the commonapi.hive.blogdefault during maintenance) no longer hard-blocks the user. Preserves the user's own Keychain node on the first attempt; the retry is gated byisUserCancellation()so a cancel never re-opens the popup, and only runs when the caller didn't pin a node.Uncaught (in promise)) and show the reason inline.extensionErrorMessage/isUserCancellation.Tradeoff to note
The fallback re-calls
requestBroadcast, which re-opens the extension confirmation popup. So in the (rare) genuine node-failure case the user approves a second time. Cancels and the happy path are unaffected. Full node hangs still surface as the existing timeout without a retry.Summary by CodeRabbit
New Features
Bug Fixes
Tests