Skip to content

chore: upgrade to stellar-sdk v16 (Protocol 27) and support CAP-71 auth entries - #2838

Merged
aristidesstaffieri merged 7 commits into
masterfrom
chore/p27
Jun 15, 2026
Merged

chore: upgrade to stellar-sdk v16 (Protocol 27) and support CAP-71 auth entries#2838
aristidesstaffieri merged 7 commits into
masterfrom
chore/p27

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Upgrades @stellar/stellar-sdk to 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, fetch default, @noble signing, stellar-base bundled 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-sdk and stellar-sdk-next) are pinned to the same version, per repo convention. 16.0.0-rc.1 is a release candidate — the final P27 release will be a follow-up version bump.

What's in this PR

  • */package.json, yarn.lock — bump stellar-sdk + stellar-sdk-next to 16.0.0-rc.1 in all 4 workspaces
  • extension/src/popup/helpers/soroban.tsparseAuthEntryPreimage returns the SDK preimage union (handles the CAP-71 WITH_ADDRESS arm); add getAddressCredentials / getAuthEntryBoundAddress; OperationOperationRecord and named-enum narrowing
  • extension/src/popup/views/SignAuthEntry/index.tsx (+ styles.scss) — show the bound address (identicon row) and block signing when it isn't the active account
  • extension/src/popup/components/AuthEntry/index.tsx — surface the per-entry bound address in transaction auth-entry review
  • extension/src/popup/components/signTransaction/Operations/* — migrate operation rendering to OperationRecord; drop the stale revoke-sponsorship casts; SignerKeyOptionsSigner
  • Asset issuer is now optional in v16 — handle it at the type boundaries (isContractId, issuerKey prop, AssetTile, balance helpers) instead of per call site
  • jest.config.js, config/jest/setupTests.tsx — transform the SDK's ESM deps and restore Node's Uint8Array so @noble/hashes v2 accepts Buffers under jsdom
  • tests — CAP-71 preimage/credential unit fixtures; e2e "bound to a different account" mismatch block

Test plan

Automated (run locally):

  • tsc --noEmit clean (extension + shared workspaces)
  • eslint --max-warnings 0 clean on changed files
  • jest --ci — 742 passed / 0 failed
  • yarn build (extension webpack) clean
  • CI green

Manual — auth entries (P0; use the signAuthEntry playground):

  • Legacy (pre-P27) auth entry → parses, shows invocation, signs
  • V2 entry bound to the active account → shows "Authorized address", signs
  • V2 entry bound to a different account → blocked ("Freighter is set to a different account")
  • Network mismatch and invalid/non-Soroban XDR still warn
  • Hardware wallet (Ledger) signs a legacy auth entry (note V2 behavior — device app may predate CAP-71)

Manual — transaction signing / operation rendering (P1):

  • setOptions with a signer (ed25519 / preAuthTx / sha256Hash / signedPayload)
  • All 6 revoke-sponsorship ops render correctly
  • invokeHostFunction (Soroban) — invocation + embedded auth entries + bound address
  • Spot-check common ops (payment, createAccount, pathPayment, manage offer, changeTrust, claimClaimableBalance, LP deposit/withdraw)
  • Operation-type labels render

Manual — asset issuer / icons (P1):

  • Send with an issued asset AND native XLM (issuer/icon display)
  • Swap tiles for issued assets and XLM
  • Add/remove a trustline; asset icons resolve; token-vs-classic detection

Manual — SDK runtime smoke (P0):

  • Submit a real payment on Testnet and confirm it lands (validates @noble signing + fetch Horizon submit)
  • Do a real swap (Soroban RPC simulate/assemble)
  • Account load / balances / history
  • Large/max amount → clean error, no crash (stricter v16 validation)

Manual — shared flows / both networks (P2):

  • signMessage and signBlob
  • Sign from a non-default account; repeat key flows on Mainnet

Notes:

  • The dApp-playground e2e suite can't run locally (docs.freighter.app playground 404s); the auth-entry e2e tests validate in CI.
  • 16.0.0-rc.1 is a release candidate; final P27 release is a follow-up bump.

V2 preimage example(shows the bound address for the auth entry)
Screenshot 2026-06-11 at 2 08 37 PM

  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
@aristidesstaffieri aristidesstaffieri self-assigned this Jun 10, 2026
@aristidesstaffieri
aristidesstaffieri marked this pull request as ready for review June 11, 2026 16:14
Copilot AI review requested due to automatic review settings June 11, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-next across workspaces to 16.0.0-rc.1 and 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 (invocationsentries).
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.

Comment thread extension/src/popup/views/SignTransaction/index.tsx
"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",
Comment thread extension/src/popup/locales/pt/translation.json Outdated
Comment thread extension/src/popup/locales/pt/translation.json Outdated
aristidesstaffieri and others added 2 commits June 11, 2026 10:26
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@socket-security

socket-security Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​stellar-sdk@​15.0.1 ⏵ 16.0.0-rc.100000
Updatednpm/​stellar-sdk-next@​15.0.1 ⏵ 16.0.0-rc.100000

View full report

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread extension/src/popup/locales/pt/translation.json Outdated
Comment thread extension/src/popup/views/SignAuthEntry/index.tsx Outdated
  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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice - this looks like an improvement rather than something needed to be done. This is much more scalabale/clear 🎉

@piyalbasu

Copy link
Copy Markdown
Contributor

getInvocationArgs doesn't validate the contract-creation preimage the way buildInvocationTree does

Nit / suggestion — not a merge blocker. Reachable only with malformed/adversarial auth XDR, and it fails closed (throws), so there's no signing-safety impact. Flagging only because both functions got the same .value.name refactor in this PR but only one kept the guard.

TL;DR: Two sibling helpers decode the same Soroban "create contract" authorization data. One rejects malformed input with a clear error; the other doesn't, and instead surfaces a cryptic low-level error. Worth making them consistent so a bad/hostile auth entry produces the same friendly message either way.

Detailed explanation (for agents)

Root cause: buildInvocationTree guards that the contract-ID preimage arm matches the executable type (wasm ⇒ contractIdPreimageFromAddress, SAC ⇒ contractIdPreimageFromAsset) before calling the accessor, throwing a descriptive "creation function appears invalid":

case "contractExecutableWasm": {
// A WASM executable must be paired with an address preimage.
if (preimage.switch().name !== "contractIdPreimageFromAddress") {
throw new Error(
`creation function appears invalid: ${JSON.stringify(
inner,
)} (should be wasm+address or token+asset)`,
);
}
const details = preimage.fromAddress();
output.args.type = "wasm";
output.args.wasm = {
salt: details.salt().toString("hex"),
hash: exec.wasmHash().toString("hex"),
address: Address.fromScAddress(details.address()).toString(),
};
if (isCreateV2) {
const v2Args = _inner as xdr.CreateContractArgsV2;
output.args.constructorArgs = v2Args.constructorArgs();
}
break;
}
case "contractExecutableStellarAsset": {
// A SAC executable must be paired with an asset preimage.
if (preimage.switch().name !== "contractIdPreimageFromAsset") {
throw new Error(
`creation function appears invalid: ${JSON.stringify(
inner,
)} (should be wasm+address or token+asset)`,
);
}
output.args.type = "sac";
output.args.asset = Asset.fromOperation(
preimage.fromAsset(),

getInvocationArgs got the same numeric-case → name-case refactor but no equivalent guard — it calls preimage.fromAddress() / preimage.fromAsset() directly:

case "contractExecutableWasm": {
const details = preimage.fromAddress();
const contractDetails = {
type: "wasm",
salt: details.salt().toString("hex"),
hash: exec.wasmHash().toString("hex"),
address: Address.fromScAddress(details.address()).toString(),
} as FnArgsCreateWasm;
if (isCreateV2) {
contractDetails.args = (
_invocation as xdr.CreateContractArgsV2
).constructorArgs();
}
return contractDetails;
}
case "contractExecutableStellarAsset": {
const sacDetails = {
type: "sac",
asset: Asset.fromOperation(preimage.fromAsset()).toString(),

For a create-contract auth invocation whose executable is contractExecutableWasm but whose preimage arm is contractIdPreimageFromAsset (or vice-versa), getInvocationArgs throws an opaque js-xdr "fromAddress not set" / "fromAsset not set" TypeError instead of the descriptive error. Since getInvocationArgs feeds the auth-entry display (getInvocationDetailsgetInvocationArgs), the signing view errors out cryptically rather than cleanly.

This is pre-existinggetInvocationArgs never had the cross-check (the old !!exec.switch().value !== !!preimage.switch().value guard lived only in buildInvocationTree). The PR doesn't regress it; it just leaves the two siblings asymmetric now that they otherwise mirror each other.

Suggested fixes (in increasing order of depth):

  1. Cheap guard: mirror the preimage.switch().name !== "contractIdPreimageFromAddress" / !== "contractIdPreimageFromAsset" checks from buildInvocationTree into the two getInvocationArgs branches.
  2. Root cause: factor the executable/preimage pairing + accessor into one shared helper both functions call, so the validation can't drift again.

Entirely fine to punt to a follow-up — just noting it while the .name refactor is fresh.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +701 to +705
if (op.type === "revokeSignerSponsorship") {
const { account, signer } = op;
return (
<>
<KeyValueSignerKeyOptions signer={signer} />
<KeyValueSignerKeyOptions signer={signer as unknown as Signer} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@aristidesstaffieri
aristidesstaffieri merged commit 50be167 into master Jun 15, 2026
11 checks passed
@aristidesstaffieri
aristidesstaffieri deleted the chore/p27 branch June 15, 2026 16:06
@github-actions github-actions Bot mentioned this pull request Jun 15, 2026
aristidesstaffieri added a commit that referenced this pull request Aug 26, 2026
  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.
aristidesstaffieri added a commit that referenced this pull request Aug 26, 2026
  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.
aristidesstaffieri added a commit that referenced this pull request Aug 26, 2026
* 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.
aristidesstaffieri added a commit that referenced this pull request Aug 26, 2026
* 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>
aristidesstaffieri added a commit that referenced this pull request Aug 26, 2026
  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.
aristidesstaffieri added a commit that referenced this pull request Aug 27, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants