chore: upgrade to stellar-sdk v16 (Protocol 27) and support CAP-71 auth entries - #2838
Conversation
Bump @stellar/stellar-sdk to 16.0.0-rc.1 (both stellar-sdk and
stellar-sdk-next aliases, all 4 workspaces) and migrate the codebase to the
v16 type surface and Protocol 27 / CAP-71 Soroban authorization.
CAP-71 (address-bound Soroban auth):
- parseAuthEntryPreimage returns the SDK preimage union and handles the new
ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS arm
- add getAddressCredentials / getAuthEntryBoundAddress to read the bound
address across ADDRESS, ADDRESS_V2 and ADDRESS_WITH_DELEGATES credentials
- SignAuthEntry shows the bound address and blocks signing when the bound
address is not the active account (delegated/other-account auth is
unsupported for now), mirroring the network-mismatch warning
- show the per-entry bound address in transaction auth-entry review
v16 migration:
- Operation -> OperationRecord for parsed-operation rendering; drop the stale
revoke-sponsorship casts in favor of native narrowing
- handle Asset.issuer now being optional at the type boundaries
(isContractId, issuerKey prop, AssetTile, balance helpers)
- AccountResponse no longer annotated as Account; OPERATION_TYPES indexing;
SignerKeyOptions removed in favor of Signer
- drop removed Transaction<...> generic type parameters
Test infra:
- jest transformIgnorePatterns: transform stellar-sdk and its ESM deps
- restore Node's Uint8Array after jsdom-global so @noble/hashes v2 accepts
Buffers in tests
- add CAP-71 preimage/credential unit fixtures and an e2e mismatch-block test
There was a problem hiding this comment.
Pull request overview
This PR upgrades the codebase to @stellar/stellar-sdk v16.0.0-rc.1 (Protocol 27) and adds CAP-71 support by parsing/displaying address-bound Soroban authorization preimages and blocking signing when the bound address is not the active account.
Changes:
- Bump
stellar-sdk/stellar-sdk-nextacross workspaces to16.0.0-rc.1and update related types/usages (e.g.,OperationRecord, signer types, optional asset issuer). - Add CAP-71 parsing/helpers (
parseAuthEntryPreimage, bound-address extraction) and surface bound address in auth-entry signing/review UI (plus an e2e mismatch-block test). - Update Jest config/setup to handle SDK v16’s ESM-first dependency graph and
@noble/*hashing behavior under jsdom.
Reviewed changes
Copilot reviewed 31 out of 33 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Updates dependency graph for stellar-sdk v16 and new transitive deps. |
| jest.config.js | Expands ESM transform allowlist for Jest to include SDK v16 + ESM deps. |
| config/jest/setupTests.tsx | Restores Node’s Uint8Array in jsdom to satisfy @noble/hashes v2 input checks. |
| extension/package.json | Bumps stellar-sdk / stellar-sdk-next to 16.0.0-rc.1. |
| @shared/api/package.json | Bumps stellar-sdk / stellar-sdk-next to 16.0.0-rc.1. |
| @shared/constants/package.json | Bumps stellar-sdk / stellar-sdk-next to 16.0.0-rc.1. |
| @shared/helpers/package.json | Bumps stellar-sdk / stellar-sdk-next to 16.0.0-rc.1. |
| @shared/helpers/soroban/server.ts | Updates Soroban RPC simulate helper typing for SDK v16 Transaction types. |
| @shared/api/helpers/soroban.ts | Makes isContractId handle optional/undefined contract IDs. |
| extension/src/popup/helpers/soroban.ts | Adds CAP-71 auth preimage parsing and bound-address extraction; updates XDR enum narrowing for v16. |
| extension/src/popup/helpers/tests/parseAuthEntryPreimage.test.ts | Adds unit fixtures/tests for CAP-71 preimage + bound-address extraction helpers. |
| extension/src/popup/views/SignAuthEntry/index.tsx | Parses CAP-71 preimages, blocks signing when bound address ≠ active account, and displays “Authorized address”. |
| extension/src/popup/views/SignAuthEntry/styles.scss | Layout tweaks so “Authorized address” label/value truncate correctly with identicon. |
| extension/src/popup/components/AuthEntry/index.tsx | Extends auth-entry display model to include and render per-entry bound address. |
| extension/src/popup/components/tests/AuthEntry.test.tsx | Updates tests for AuthEntries prop shape change (invocations → entries). |
| extension/src/popup/views/SignTransaction/index.tsx | Adapts transaction signing UI to new auth-entry display shape (bound address) and SDK v16 typing. |
| extension/src/popup/views/SignTransaction/Preview/Details/index.tsx | Updates operations prop typing to OperationRecord[]. |
| extension/src/popup/components/signTransaction/Operations/index.tsx | Migrates operation rendering to OperationRecord and updates signer typing/casts. |
| extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx | Updates signer types and handles optional asset issuer in display. |
| extension/src/popup/views/SignTransaction/hooks/useGetSignTxData.tsx | Adjusts icon lookup for optional issuer typing boundaries. |
| extension/src/popup/views/tests/SignTransaction.test.tsx | Updates tests for SDK v16 Transaction typing changes. |
| extension/src/popup/helpers/hardwareConnect.ts | Updates hardware signing helper typings for SDK v16 Transaction type. |
| extension/src/popup/helpers/getManageAssetXDR.ts | Removes now-unnecessary explicit Account typing for loadAccount. |
| extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx | Removes now-unnecessary explicit Account typing for loadAccount. |
| extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx | Removes now-unnecessary explicit Account typing for loadAccount. |
| extension/src/popup/helpers/balance.ts | Makes issuer optional at type boundaries for balance matching helpers. |
| extension/src/helpers/stellar.ts | Updates isAsset helper typing for optional issuer. |
| extension/src/popup/components/AssetTile/index.tsx | Makes asset.issuer optional to match SDK v16 asset typing changes. |
| extension/src/popup/components/account/AccountAssets/index.tsx | Makes issuerKey optional and adjusts retry icon fetch call site. |
| extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx | Narrows operation type lookup for OPERATION_TYPES with typed keys. |
| extension/src/popup/locales/en/translation.json | Adds strings for “Authorized address” and CAP-71 bound-address warning. |
| extension/src/popup/locales/pt/translation.json | Adds Portuguese-locale entries for new CAP-71-related strings (currently English values). |
| extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts | Adds e2e coverage for blocking CAP-71 auth entry signing when bound to a different address. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "At the end of this process, Freighter will only display accounts related to the new backup phrase.": "No final deste processo, o Freighter exibirá apenas contas relacionadas à nova frase de backup.", | ||
| "Authorizations": "Autorizações", | ||
| "Authorize": "Autorizar", | ||
| "Authorized address": "Authorized address", |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-d5b5cf3c2d317654f162 (SDF collaborators only — install instructions in the release description) |
useChangeTrustline constructed the Horizon server at the hook's top level
(render time) from settingsNetworkDetailsSelector. The dApp-triggered
add-token popup mounts as a fresh window with that selector at its empty
default (networkUrl: "") until settings hydrate. Under stellar-sdk v16,
new Horizon.Server("") throws synchronously (v15 tolerated it), so the
render-phase construction crashed the popup to the error boundary and the
approve button never rendered — failing the add-token e2e test.
Move the server construction into changeTrustline, where it is actually
used (on submit, after hydration). No behavior change on the happy path.
Also repoint the auth-entry e2e test to play.freighter.app (the playground
moved from docs.freighter.app in the merge).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c59de78a44
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Review flagged untranslated English in the Portuguese locale and a
string-concatenation pattern in SignAuthEntry that translators cannot
reorder.
pt/translation.json:
- Translate 9 fully-untranslated strings (e.g. "Authorized address",
"Failed to load assets.", "This authorization is for").
- Fix partial English-word leaks to match terminology used elsewhere in
the file: Amount→Quantia, Price→Preço, Fee→Taxa, migration→migração.
- Leave intentional loanwords/proper nouns as-is (Soroban, Blockaid,
trustline, Memo, Status, Tokens, fullscreen, toolbar, Bump).
SignAuthEntry: replace fragment+value+period concatenation with single
interpolated t() strings so locales control word order and punctuation:
- "This authorization is for {{address}}."
- "The authorization entry is for {{network}}."
Add the new interpolated keys to en/pt and drop the orphaned old keys.
| switch (fn.switch().value) { | ||
| // sorobanAuthorizedFunctionTypeContractFn | ||
| case 0: { | ||
| switch (fn.switch().name) { |
There was a problem hiding this comment.
nice - this looks like an improvement rather than something needed to be done. This is much more scalabale/clear 🎉
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17af893cdf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (op.type === "revokeSignerSponsorship") { | ||
| const { account, signer } = op; | ||
| return ( | ||
| <> | ||
| <KeyValueSignerKeyOptions signer={signer} /> | ||
| <KeyValueSignerKeyOptions signer={signer as unknown as Signer} /> |
There was a problem hiding this comment.
Preserve hex strings for revoked signer keys
When this branch renders a revokeSignerSponsorship for a hashX or preAuthTx signer, the SDK parser has already converted op.signer.sha256Hash/preAuthTx to hex strings. Casting it to Signer sends those strings into KeyValueSignerKeyOptions, which now calls formattedBuffer and re-encodes the ASCII characters as hex, so the review shows a different truncated hash than the signer key actually being revoked.
Useful? React with 👍 / 👎.
SDK 17's revokeSponsorship signer arm returns sha256Hash and preAuthTx as already-hex strings (convertXdrSignerKeyToObject calls uint8ArrayToHex), while setOptions returns real Uint8Arrays. formattedBuffer asserted bytes, so reviewing a revokeSignerSponsorship op with a hash-based signer threw `TypeError: Expected Uint8Array, got string` from a render body and put the ErrorBoundary screen where the sign/reject decision belongs. formattedBuffer now accepts Uint8Array | string, passing a string through rather than re-encoding it -- encoding it again hexes the ASCII of the hex, which is what the old Buffer.from(<hex string>) path did. That silent corruption is the wrong-hex display reported in #2838, so this closes it too: the row now reads DEAD…BEEF where it used to read 6465…6566. The types were not the problem; the casts were. RevokeSignerOpts already declares these fields as `Uint8Array | string` -- correctly -- and it was `signer as unknown as Signer` at the call site that discarded the string, because setOptions' Signer type declares bare Uint8Array. Both signer components now derive their prop type from the operation unions: type RevokeSignerKey = Operation.RevokeSignerSponsorship["signer"] type SetOptionsSignerKey = NonNullable<Extract<OperationRecord, { type: "setOptions" }>["signer"]> so the compiler tracks reality and both `as unknown as Signer` casts are gone. Verified that dropping the cast without this change is a typecheck error, i.e. the compiler would have caught the original bug. The `in` checks became discriminant truthiness checks, which is how the SDK's `?: never` unions are meant to be narrowed. The setOptions path is fixed alongside the reported one. It returns real bytes today so it was never broken, but it carried the identical cast on the adjacent line and would have hidden the same defect on the next SDK shape change. Tests cover formattedBuffer for both input shapes (it had none) and assert the rendered hex for decoded revokeSignerSponsorship ops, driving the real TransactionBuilder decode path rather than a hand-built signer object.
SDK 17's revokeSponsorship signer arm returns sha256Hash and preAuthTx as already-hex strings (convertXdrSignerKeyToObject calls uint8ArrayToHex), while setOptions returns real Uint8Arrays. formattedBuffer asserted bytes, so reviewing a revokeSignerSponsorship op with a hash-based signer threw `TypeError: Expected Uint8Array, got string` from a render body and put the ErrorBoundary screen where the sign/reject decision belongs. formattedBuffer now accepts Uint8Array | string, passing a string through rather than re-encoding it -- encoding it again hexes the ASCII of the hex, which is what the old Buffer.from(<hex string>) path did. That silent corruption is the wrong-hex display reported in #2838, so this closes it too: the row now reads DEAD…BEEF where it used to read 6465…6566. The types were not the problem; the casts were. RevokeSignerOpts already declares these fields as `Uint8Array | string` -- correctly -- and it was `signer as unknown as Signer` at the call site that discarded the string, because setOptions' Signer type declares bare Uint8Array. Both signer components now derive their prop type from the operation unions: type RevokeSignerKey = Operation.RevokeSignerSponsorship["signer"] type SetOptionsSignerKey = NonNullable<Extract<OperationRecord, { type: "setOptions" }>["signer"]> so the compiler tracks reality and both `as unknown as Signer` casts are gone. Verified that dropping the cast without this change is a typecheck error, i.e. the compiler would have caught the original bug. The `in` checks became discriminant truthiness checks, which is how the SDK's `?: never` unions are meant to be narrowed. The setOptions path is fixed alongside the reported one. It returns real bytes today so it was never broken, but it carried the identical cast on the adjacent line and would have hidden the same defect on the next SDK shape change. Tests cover formattedBuffer for both input shapes (it had none) and assert the rendered hex for decoded revokeSignerSponsorship ops, driving the real TransactionBuilder decode path rather than a hand-built signer object.
* chore(deps): migrate to @stellar/stellar-sdk 17.0.0
Bumps stellar-sdk and stellar-sdk-next from 16.0.0-rc.1 to 17.0.0 across
extension, @shared/api, @shared/constants and @shared/helpers, and works
through both v17 breaking-change guides: the class-based xdr namespace and
the Buffer -> Uint8Array switch.
XDR namespace
- Rename toXDR/fromXDR/fromXDRObject/toXDRObject/validateXDR to their
PascalCase forms, including jest mock keys and jest.spyOn string args.
- Replace .switch() with .type, .value()/arm getters with properties, enum
factory calls with singletons, and new xdr.Int64(n) with BigInt(n).
- Use xdr.expectUnionVariant where an arm accessor used to throw on the
wrong arm, preserving the old contract.
- Replace the hand-rolled @stellar/js-xdr XdrReader in parseWasmXdr with
xdr.decodeStream, dropping the direct @stellar/js-xdr dependency and the
js-xdr.d.ts module shim.
Buffer -> Uint8Array
- Use xdr.encodeBytes for hex/base64 encoding and XDR wrapper equals() for
byte comparison.
- Keep SignAuthEntryResponse and SignBlobResponse as Buffer: dApps on
freighter-api <4.0.0/<4.2.0 receive the raw object over
runtime.sendMessage, and Buffer and Uint8Array do not JSON-serialize the
same way. The handlers wrap Keypair.sign() in Buffer.from() so the public
API stays byte-identical.
- Re-wrap tx.hash()/tx.signatureBase() for @ledgerhq/hw-app-str, which
still types its arguments as Buffer.
Fixes two latent corruption bugs the migration exposed
- Hardware-wallet SEP-53 message signing returned comma-joined decimals
instead of base64, producing a signature that looked valid as it crossed
runtime.sendMessage but failed verification.
- manageData operations rendered their value as comma-joined decimals on
the sign-transaction approval screen.
- Also replaces the never-true native.constructor === "Uint8Array" check
with an instanceof test, which now matters because scValToNative returns
Uint8Array for non-UTF-8 string/symbol payloads.
Build and test config
- Raise engines.node to >=22.12.0 (the SDK's own floor) in the root and
docs package.json.
- Add @exodus/bytes and smol-toml to the jest transformIgnorePatterns
allowlist. The v17 CJS build require()s ESM-only deps, which Node 22.12+
supports but Jest's CJS module registry does not; without this, 155 of
217 suites fail to run.
- Use xdr.AssetCode toJson() rather than toString() for display, since
XdrValue.toString() base64-encodes.
* refactor(soroban): read contract specs via contract.Spec.fromWasm
* feat(soroban): handle CAP-85 external contract executables
Protocol 28 adds two XDR arms a wallet can encounter:
ContractExecutableType::CONTRACT_EXECUTABLE_EXTERNAL_REF and
SCValType::SCV_EXECUTABLE_TAG. stellar-sdk 17 decodes both, which exposed
our own switches -- getInvocationArgs and buildInvocationTree each threw
"unknown creation type", and that throw reached AuthEntry's render body
and took the popup to the ErrorBoundary.
* fix(soroban): surface the CAP-85 note earlier and name invalid pairings
Follow-up polish on the auth-entry review UI plus clearer diagnostics for
malformed contract creations.
The externally-managed-executable note now leads the auth entry's info
block instead of trailing it, so the caveat is read before the details it
qualifies. Its copy drops the positional reference ("the owner contract
above") and names the row instead ("the executable owner"), which stays
correct in both placements -- the Operations view still renders the note
after the executable rows, where the existing Operations__warning spacing
pattern applies.
* fix(sign-transaction): render hex signer keys instead of throwing
SDK 17's revokeSponsorship signer arm returns sha256Hash and preAuthTx as
already-hex strings (convertXdrSignerKeyToObject calls uint8ArrayToHex),
while setOptions returns real Uint8Arrays. formattedBuffer asserted bytes,
so reviewing a revokeSignerSponsorship op with a hash-based signer threw
`TypeError: Expected Uint8Array, got string` from a render body and put the
ErrorBoundary screen where the sign/reject decision belongs.
formattedBuffer now accepts Uint8Array | string, passing a string through
rather than re-encoding it -- encoding it again hexes the ASCII of the hex,
which is what the old Buffer.from(<hex string>) path did. That silent
corruption is the wrong-hex display reported in #2838, so this closes it
too: the row now reads DEAD…BEEF where it used to read 6465…6566.
The types were not the problem; the casts were. RevokeSignerOpts already
declares these fields as `Uint8Array | string` -- correctly -- and it was
`signer as unknown as Signer` at the call site that discarded the string,
because setOptions' Signer type declares bare Uint8Array. Both signer
components now derive their prop type from the operation unions:
type RevokeSignerKey = Operation.RevokeSignerSponsorship["signer"]
type SetOptionsSignerKey =
NonNullable<Extract<OperationRecord, { type: "setOptions" }>["signer"]>
so the compiler tracks reality and both `as unknown as Signer` casts are
gone. Verified that dropping the cast without this change is a typecheck
error, i.e. the compiler would have caught the original bug. The `in`
checks became discriminant truthiness checks, which is how the SDK's
`?: never` unions are meant to be narrowed.
The setOptions path is fixed alongside the reported one. It returns real
bytes today so it was never broken, but it carried the identical cast on
the adjacent line and would have hidden the same defect on the next SDK
shape change.
Tests cover formattedBuffer for both input shapes (it had none) and assert
the rendered hex for decoded revokeSignerSponsorship ops, driving the real
TransactionBuilder decode path rather than a hand-built signer object.
* v5.46.0 * chore(deps): migrate to @stellar/stellar-sdk 17.0.0 (#2977) * chore(deps): migrate to @stellar/stellar-sdk 17.0.0 Bumps stellar-sdk and stellar-sdk-next from 16.0.0-rc.1 to 17.0.0 across extension, @shared/api, @shared/constants and @shared/helpers, and works through both v17 breaking-change guides: the class-based xdr namespace and the Buffer -> Uint8Array switch. XDR namespace - Rename toXDR/fromXDR/fromXDRObject/toXDRObject/validateXDR to their PascalCase forms, including jest mock keys and jest.spyOn string args. - Replace .switch() with .type, .value()/arm getters with properties, enum factory calls with singletons, and new xdr.Int64(n) with BigInt(n). - Use xdr.expectUnionVariant where an arm accessor used to throw on the wrong arm, preserving the old contract. - Replace the hand-rolled @stellar/js-xdr XdrReader in parseWasmXdr with xdr.decodeStream, dropping the direct @stellar/js-xdr dependency and the js-xdr.d.ts module shim. Buffer -> Uint8Array - Use xdr.encodeBytes for hex/base64 encoding and XDR wrapper equals() for byte comparison. - Keep SignAuthEntryResponse and SignBlobResponse as Buffer: dApps on freighter-api <4.0.0/<4.2.0 receive the raw object over runtime.sendMessage, and Buffer and Uint8Array do not JSON-serialize the same way. The handlers wrap Keypair.sign() in Buffer.from() so the public API stays byte-identical. - Re-wrap tx.hash()/tx.signatureBase() for @ledgerhq/hw-app-str, which still types its arguments as Buffer. Fixes two latent corruption bugs the migration exposed - Hardware-wallet SEP-53 message signing returned comma-joined decimals instead of base64, producing a signature that looked valid as it crossed runtime.sendMessage but failed verification. - manageData operations rendered their value as comma-joined decimals on the sign-transaction approval screen. - Also replaces the never-true native.constructor === "Uint8Array" check with an instanceof test, which now matters because scValToNative returns Uint8Array for non-UTF-8 string/symbol payloads. Build and test config - Raise engines.node to >=22.12.0 (the SDK's own floor) in the root and docs package.json. - Add @exodus/bytes and smol-toml to the jest transformIgnorePatterns allowlist. The v17 CJS build require()s ESM-only deps, which Node 22.12+ supports but Jest's CJS module registry does not; without this, 155 of 217 suites fail to run. - Use xdr.AssetCode toJson() rather than toString() for display, since XdrValue.toString() base64-encodes. * refactor(soroban): read contract specs via contract.Spec.fromWasm * feat(soroban): handle CAP-85 external contract executables Protocol 28 adds two XDR arms a wallet can encounter: ContractExecutableType::CONTRACT_EXECUTABLE_EXTERNAL_REF and SCValType::SCV_EXECUTABLE_TAG. stellar-sdk 17 decodes both, which exposed our own switches -- getInvocationArgs and buildInvocationTree each threw "unknown creation type", and that throw reached AuthEntry's render body and took the popup to the ErrorBoundary. * fix(soroban): surface the CAP-85 note earlier and name invalid pairings Follow-up polish on the auth-entry review UI plus clearer diagnostics for malformed contract creations. The externally-managed-executable note now leads the auth entry's info block instead of trailing it, so the caveat is read before the details it qualifies. Its copy drops the positional reference ("the owner contract above") and names the row instead ("the executable owner"), which stays correct in both placements -- the Operations view still renders the note after the executable rows, where the existing Operations__warning spacing pattern applies. * fix(sign-transaction): render hex signer keys instead of throwing SDK 17's revokeSponsorship signer arm returns sha256Hash and preAuthTx as already-hex strings (convertXdrSignerKeyToObject calls uint8ArrayToHex), while setOptions returns real Uint8Arrays. formattedBuffer asserted bytes, so reviewing a revokeSignerSponsorship op with a hash-based signer threw `TypeError: Expected Uint8Array, got string` from a render body and put the ErrorBoundary screen where the sign/reject decision belongs. formattedBuffer now accepts Uint8Array | string, passing a string through rather than re-encoding it -- encoding it again hexes the ASCII of the hex, which is what the old Buffer.from(<hex string>) path did. That silent corruption is the wrong-hex display reported in #2838, so this closes it too: the row now reads DEAD…BEEF where it used to read 6465…6566. The types were not the problem; the casts were. RevokeSignerOpts already declares these fields as `Uint8Array | string` -- correctly -- and it was `signer as unknown as Signer` at the call site that discarded the string, because setOptions' Signer type declares bare Uint8Array. Both signer components now derive their prop type from the operation unions: type RevokeSignerKey = Operation.RevokeSignerSponsorship["signer"] type SetOptionsSignerKey = NonNullable<Extract<OperationRecord, { type: "setOptions" }>["signer"]> so the compiler tracks reality and both `as unknown as Signer` casts are gone. Verified that dropping the cast without this change is a typecheck error, i.e. the compiler would have caught the original bug. The `in` checks became discriminant truthiness checks, which is how the SDK's `?: never` unions are meant to be narrowed. The setOptions path is fixed alongside the reported one. It returns real bytes today so it was never broken, but it carried the identical cast on the adjacent line and would have hidden the same defect on the next SDK shape change. Tests cover formattedBuffer for both input shapes (it had none) and assert the rendered hex for decoded revokeSignerSponsorship ops, driving the real TransactionBuilder decode path rather than a hand-built signer object. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: aristides <aristides.staffieri@stellar.org>
SDK 17's revokeSponsorship signer arm returns sha256Hash and preAuthTx as already-hex strings (convertXdrSignerKeyToObject calls uint8ArrayToHex), while setOptions returns real Uint8Arrays. formattedBuffer asserted bytes, so reviewing a revokeSignerSponsorship op with a hash-based signer threw `TypeError: Expected Uint8Array, got string` from a render body and put the ErrorBoundary screen where the sign/reject decision belongs. formattedBuffer now accepts Uint8Array | string, passing a string through rather than re-encoding it -- encoding it again hexes the ASCII of the hex, which is what the old Buffer.from(<hex string>) path did. That silent corruption is the wrong-hex display reported in #2838, so this closes it too: the row now reads DEAD…BEEF where it used to read 6465…6566. The types were not the problem; the casts were. RevokeSignerOpts already declares these fields as `Uint8Array | string` -- correctly -- and it was `signer as unknown as Signer` at the call site that discarded the string, because setOptions' Signer type declares bare Uint8Array. Both signer components now derive their prop type from the operation unions: type RevokeSignerKey = Operation.RevokeSignerSponsorship["signer"] type SetOptionsSignerKey = NonNullable<Extract<OperationRecord, { type: "setOptions" }>["signer"]> so the compiler tracks reality and both `as unknown as Signer` casts are gone. Verified that dropping the cast without this change is a typecheck error, i.e. the compiler would have caught the original bug. The `in` checks became discriminant truthiness checks, which is how the SDK's `?: never` unions are meant to be narrowed. The setOptions path is fixed alongside the reported one. It returns real bytes today so it was never broken, but it carried the identical cast on the adjacent line and would have hidden the same defect on the next SDK shape change. Tests cover formattedBuffer for both input shapes (it had none) and assert the rendered hex for decoded revokeSignerSponsorship ops, driving the real TransactionBuilder decode path rather than a hand-built signer object.
* chore(deps): migrate to @stellar/stellar-sdk 17.0.0
Bumps stellar-sdk and stellar-sdk-next from 16.0.0-rc.1 to 17.0.0 across
extension, @shared/api, @shared/constants and @shared/helpers, and works
through both v17 breaking-change guides: the class-based xdr namespace and
the Buffer -> Uint8Array switch.
XDR namespace
- Rename toXDR/fromXDR/fromXDRObject/toXDRObject/validateXDR to their
PascalCase forms, including jest mock keys and jest.spyOn string args.
- Replace .switch() with .type, .value()/arm getters with properties, enum
factory calls with singletons, and new xdr.Int64(n) with BigInt(n).
- Use xdr.expectUnionVariant where an arm accessor used to throw on the
wrong arm, preserving the old contract.
- Replace the hand-rolled @stellar/js-xdr XdrReader in parseWasmXdr with
xdr.decodeStream, dropping the direct @stellar/js-xdr dependency and the
js-xdr.d.ts module shim.
Buffer -> Uint8Array
- Use xdr.encodeBytes for hex/base64 encoding and XDR wrapper equals() for
byte comparison.
- Keep SignAuthEntryResponse and SignBlobResponse as Buffer: dApps on
freighter-api <4.0.0/<4.2.0 receive the raw object over
runtime.sendMessage, and Buffer and Uint8Array do not JSON-serialize the
same way. The handlers wrap Keypair.sign() in Buffer.from() so the public
API stays byte-identical.
- Re-wrap tx.hash()/tx.signatureBase() for @ledgerhq/hw-app-str, which
still types its arguments as Buffer.
Fixes two latent corruption bugs the migration exposed
- Hardware-wallet SEP-53 message signing returned comma-joined decimals
instead of base64, producing a signature that looked valid as it crossed
runtime.sendMessage but failed verification.
- manageData operations rendered their value as comma-joined decimals on
the sign-transaction approval screen.
- Also replaces the never-true native.constructor === "Uint8Array" check
with an instanceof test, which now matters because scValToNative returns
Uint8Array for non-UTF-8 string/symbol payloads.
Build and test config
- Raise engines.node to >=22.12.0 (the SDK's own floor) in the root and
docs package.json.
- Add @exodus/bytes and smol-toml to the jest transformIgnorePatterns
allowlist. The v17 CJS build require()s ESM-only deps, which Node 22.12+
supports but Jest's CJS module registry does not; without this, 155 of
217 suites fail to run.
- Use xdr.AssetCode toJson() rather than toString() for display, since
XdrValue.toString() base64-encodes.
* refactor(soroban): read contract specs via contract.Spec.fromWasm
* feat(soroban): handle CAP-85 external contract executables
Protocol 28 adds two XDR arms a wallet can encounter:
ContractExecutableType::CONTRACT_EXECUTABLE_EXTERNAL_REF and
SCValType::SCV_EXECUTABLE_TAG. stellar-sdk 17 decodes both, which exposed
our own switches -- getInvocationArgs and buildInvocationTree each threw
"unknown creation type", and that throw reached AuthEntry's render body
and took the popup to the ErrorBoundary.
* fix(soroban): surface the CAP-85 note earlier and name invalid pairings
Follow-up polish on the auth-entry review UI plus clearer diagnostics for
malformed contract creations.
The externally-managed-executable note now leads the auth entry's info
block instead of trailing it, so the caveat is read before the details it
qualifies. Its copy drops the positional reference ("the owner contract
above") and names the row instead ("the executable owner"), which stays
correct in both placements -- the Operations view still renders the note
after the executable rows, where the existing Operations__warning spacing
pattern applies.
* fix(sign-transaction): render hex signer keys instead of throwing
SDK 17's revokeSponsorship signer arm returns sha256Hash and preAuthTx as
already-hex strings (convertXdrSignerKeyToObject calls uint8ArrayToHex),
while setOptions returns real Uint8Arrays. formattedBuffer asserted bytes,
so reviewing a revokeSignerSponsorship op with a hash-based signer threw
`TypeError: Expected Uint8Array, got string` from a render body and put the
ErrorBoundary screen where the sign/reject decision belongs.
formattedBuffer now accepts Uint8Array | string, passing a string through
rather than re-encoding it -- encoding it again hexes the ASCII of the hex,
which is what the old Buffer.from(<hex string>) path did. That silent
corruption is the wrong-hex display reported in #2838, so this closes it
too: the row now reads DEAD…BEEF where it used to read 6465…6566.
The types were not the problem; the casts were. RevokeSignerOpts already
declares these fields as `Uint8Array | string` -- correctly -- and it was
`signer as unknown as Signer` at the call site that discarded the string,
because setOptions' Signer type declares bare Uint8Array. Both signer
components now derive their prop type from the operation unions:
type RevokeSignerKey = Operation.RevokeSignerSponsorship["signer"]
type SetOptionsSignerKey =
NonNullable<Extract<OperationRecord, { type: "setOptions" }>["signer"]>
so the compiler tracks reality and both `as unknown as Signer` casts are
gone. Verified that dropping the cast without this change is a typecheck
error, i.e. the compiler would have caught the original bug. The `in`
checks became discriminant truthiness checks, which is how the SDK's
`?: never` unions are meant to be narrowed.
The setOptions path is fixed alongside the reported one. It returns real
bytes today so it was never broken, but it carried the identical cast on
the adjacent line and would have hidden the same defect on the next SDK
shape change.
Tests cover formattedBuffer for both input shapes (it had none) and assert
the rendered hex for decoded revokeSignerSponsorship ops, driving the real
TransactionBuilder decode path rather than a hand-built signer object.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Upgrades
@stellar/stellar-sdkto 16.0.0-rc.1 (Protocol 27) across all workspaces and adds support for CAP-71 address-bound Soroban authorization. The SDK jump is a major version with a broad type/runtime surface (ESM-first,fetchdefault,@noblesigning,stellar-basebundled in), so most of the diff is migrating the codebase to v16; the user-facing feature work is CAP-71 auth-entry handling.For CAP-71, Freighter now parses and displays the new address-bound auth-entry preimage and shows the bound address when signing. For now we only support authorizing on behalf of the active account — an auth entry bound to a different account (delegated auth, or a contract/smart-wallet authorizer) is blocked with a warning, the same way a network mismatch is. The delegate-tree display is intentionally out of scope.
Both SDK aliases (
stellar-sdkandstellar-sdk-next) are pinned to the same version, per repo convention.16.0.0-rc.1is a release candidate — the final P27 release will be a follow-up version bump.What's in this PR
*/package.json,yarn.lock— bumpstellar-sdk+stellar-sdk-nextto16.0.0-rc.1in all 4 workspacesextension/src/popup/helpers/soroban.ts—parseAuthEntryPreimagereturns the SDK preimage union (handles the CAP-71WITH_ADDRESSarm); addgetAddressCredentials/getAuthEntryBoundAddress;Operation→OperationRecordand named-enum narrowingextension/src/popup/views/SignAuthEntry/index.tsx(+styles.scss) — show the bound address (identicon row) and block signing when it isn't the active accountextension/src/popup/components/AuthEntry/index.tsx— surface the per-entry bound address in transaction auth-entry reviewextension/src/popup/components/signTransaction/Operations/*— migrate operation rendering toOperationRecord; drop the stale revoke-sponsorship casts;SignerKeyOptions→Signerissueris now optional in v16 — handle it at the type boundaries (isContractId,issuerKeyprop,AssetTile,balancehelpers) instead of per call sitejest.config.js,config/jest/setupTests.tsx— transform the SDK's ESM deps and restore Node'sUint8Arrayso@noble/hashesv2 accepts Buffers under jsdomTest plan
Automated (run locally):
tsc --noEmitclean (extension + shared workspaces)eslint --max-warnings 0clean on changed filesjest --ci— 742 passed / 0 failedyarn build(extension webpack) cleanManual — auth entries (P0; use the signAuthEntry playground):
Manual — transaction signing / operation rendering (P1):
Manual — asset issuer / icons (P1):
Manual — SDK runtime smoke (P0):
Manual — shared flows / both networks (P2):
Notes:
16.0.0-rc.1is a release candidate; final P27 release is a follow-up bump.V2 preimage example(shows the bound address for the auth entry)
