v3.6.0 Protocol 28 (CAP-85) support and ADDRESS_V2 defaults
This release prepares the SDK for Protocol 28, makes CAP-71 upgraded authorization the default, and hardens strkey, amount, and price handling. Several changes alter observable behavior and the CAP-85 tag APIs change type; see the linked migration notes at the end.
Breaking: ADDRESS_V2 authorization by default
SimulateTransactionRequest.useUpgradedAuth and MethodOptions.useUpgradedAuth default to true, and the flag is always sent, so an explicit false reaches the RPC as the legacy opt-out. SorobanCredentials.forAddress() and forAddressCredentials() build the ADDRESS_V2 arm; the new forAddressLegacy() and forAddressCredentialsLegacy() build legacy ADDRESS. Auth entries returned by simulation against a Protocol 27+ server carry ADDRESS_V2 under the default flag, and SorobanCredentials.addressCredentials is null on them; read either arm through SorobanCredentials.innerAddressCredentials. RPC servers without Protocol 27 support ignore the flag and return legacy entries. On a network below Protocol 27, set the flag to false, since ADDRESS_V2 entries invalidate the transaction there.
OpenZeppelin smart accounts follow the same defaults: the kit's simulations and the fundWallet source-account conversion use ADDRESS_V2 (OZSmartAccountConfig.useUpgradedAuth), and delegated external-wallet entries carry the address-bound V2 preimage (useUpgradedAuthForWalletSigners). Both flags default to true and each opt-out restores the legacy arm, for relayer services that cannot parse Protocol 27 XDR and wallet software that cannot sign the address-bound preimage type. OZSmartAccountAuth.buildSourceAccountAuthPayloadHash takes the credential address as its second argument and a named useUpgradedAuth.
Details: Soroban guide, Protocol 27 credentials and smart-accounts onboarding.
Protocol 28: CAP-85 external references
A contract created from a CAP-85 external reference names an owner contract and a tag instead of carrying its own wasm hash. The SDK covers the lifecycle:
- The contract loading paths (
SorobanServer.loadContractCodeForContractId()andloadContractInfoForContractId(),SorobanClient.forClientOptions()) resolve external references through the owner's persistent tag entry. The newSorobanServer.getExternalRefWasmHash()resolves a reference directly and returns null for every miss. Resolution is one extragetLedgerEntriesread - no transaction, no fee, and the owner contract's code never executes. - The new
CreateContractFromExternalRefHostFunctionandCreateContractFromExternalRefWithConstructorHostFunctionbuild and parse external-ref create operations. Both reject an executable owner that is not a contract address, and parsing applies the same check. Envelopes carrying either create form now parse; they previously threwUnimplementedError. SorobanClient.deployFromExternalRef()deploys from a reference, resolving it client-side before the transaction is built, and returns a ready client. The newAddress.deriveContractId()computes, before anything is deployed, theC...contract id a deployment will produce. The id depends only on the deployer address, the salt, and the network (not limited to external references), so the future address can be used in advance - for example in another contract's constructor arguments.- TxRep renders both create forms and round-trips them byte for byte.
Details: Soroban guide, external reference executables.
Breaking: the executable tag is raw bytes
An executable tag is an XDR string, which carries arbitrary bytes. XdrContractExecutableExternalRef.tag and its positional constructor parameter are now Uint8List, and XdrSCVal.executableTag is Uint8List?, so a tag whose bytes spell no text can be decoded and built. The tagString and executableTagString getters read the bytes as UTF-8; XdrSCVal.forExecutableTag(String) and XdrContractExecutable.forExternalRef(XdrSCAddress, String) keep their signatures, and the new external-ref classes (CreateContractFromExternalRefHostFunction, CreateContractFromExternalRefWithConstructorHostFunction, DeployFromExternalRefRequest) each carry a forTagString constructor that takes the tag as text.
Strkey hardening (SEP-23)
Strkey decoding and encoding follow SEP-23 strictly. Malformed input that previously slipped through now throws, every decode rejection is a FormatException, and every encoder emits only strkeys the decoder reads back. StrKey.isValidContractIdHex delegates to isValidContractId and now answers false for lengths it previously accepted. SignedPayloadSigner.fromAccountId throws ArgumentError for an M-address, and SignedPayloadSigner rejects an empty payload.
Details: SEP-23 strkey guide.
Key and id input validation
KeyPair.fromPublicKey and KeyPair.fromSecretSeedList throw ArgumentError for raw key material that is not 32 bytes. Util.stringIdToXdrHash, which padded or truncated wrong-width input, is removed; Util.hexIdToXdrHash and Util.liquidityPoolIdToXdrHash replace it. Hex contract and pool ids must render exactly 32 bytes. XdrSCAddress.forContractId no longer misreads a 64-character hex id that begins with an uppercase C as a strkey - C is itself a hex digit, and such ids were previously rejected; they now parse. Lowercase hex was never affected.
Claimable balance id spelling
Horizon serves a claimable balance id as 72 hex characters. The claimable-balance request builders accept every circulating spelling and send that form (RequestBuilder.claimableBalanceIdHorizonHex() exposes the conversion), StrKey.encodeClaimableBalanceId reads the 32, 33, and 36-byte forms and verifies discriminants, and ids read back from XDR (ClaimClaimableBalanceOperation, ClawbackClaimableBalanceOperation, Address.fromXdr) report the 72-character form. The new getter XdrClaimableBalanceID.paddedBalanceIdHex returns the id in that 72-character Horizon spelling.
Details: SEP-23 strkey guide, liquidity pool and claimable balance IDs.
Reading submit results
SubmitTransactionResponse.success answers false for a result XDR it cannot read instead of throwing. SubmitTransactionResponse.getClaimableBalanceIdIdFromResult(position) now returns the id created by the operation at position; it previously answered position 0's id whenever another CreateClaimableBalance sat at position 0, and threw otherwise. Both it and getOfferIdFromResult(position) now read a fee-bumped transaction's inner operations (previously a throw) and answer null for an out-of-range position.
TxRep parsing
If a TxRep (SEP-11) line spells a fixed-width value - a hash, a uint256, a signature hint - with too few or too many hex digits, parsing now throws an Exception naming the key and both widths. Such a line previously decoded to a value of the spelled width and re-encoded to malformed XDR.
Amount and price handling
Util.decimalStringToStroops and Price.fromString validate their input shape and refuse malformed values: "0x10" used to be read as 16 and "1.2.3" as 1; both now throw. A price of zero, one too small for any signed-int32 fraction, or one beyond the int32 boundaries throws during transaction building instead of reaching the network. SEP-6 and SEP-24 fee requests send plain decimal amounts; values below 1e-6 asset units no longer go out in scientific notation.
Compatibility matrices, documentation, and skill
All compatibility matrices regenerated at 100% coverage against Horizon v28.0.0 and Soroban RPC v28.0.0. New documentation for CAP-85 external references, the ADDRESS_V2 defaults, and the SEP-23 strkey rules; the agent skill is updated to 1.4.0 with a regenerated API reference.
Migrating to 3.6.0
Several changes in this release require action in existing code. See the 3.6.0 migration notes - each note tells you whether you are affected, how it shows, and what to do.