Repository navigation
v3.8.0 SEP-29 memo checks and XDR decoding hardening
This release runs the SEP-29 memo-required check on every transaction submission by default, rejects malformed and unknown XDR instead of decoding it partially, renders and parses operation prices exactly, and moves SorobanServer logging off stdout. The SEP-29 check changes what a submit call does for memo-less payments, and a removed constant, a retyped getter and new interface members can break existing code at compile time; see Compatibility and the linked migration notes at the end.
Breaking: SEP-29 memo-required check on submission
StellarSDK.submitTransaction, submitFeeBumpTransaction, submitAsyncTransaction, submitAsyncFeeBumpTransaction, submitTransactionEnvelopeXdrBase64 and submitAsyncTransactionEnvelopeXdrBase64 run the SEP-29 check before submitting. A transaction without a memo that pays into, path-pays into or merges into an account whose config.memo_required data entry is 1 throws the new AccountRequiresMemoException, carrying accountId and the zero-based operationIndex, and nothing is submitted. The check makes no request for a transaction that carries a memo. Otherwise it loads each distinct non-muxed destination from Horizon, in operation order, and stops at the first account that requires a memo; multiplexed (M...) destinations are not looked up, a destination Horizon does not know (404) is skipped, and fee bump transactions are checked through their inner transaction. Another lookup failure (ErrorResponse, TooManyRequestsException, or http.ClientException for a transport failure) surfaces from the submit call before anything is posted. Every submit method takes skipMemoRequiredCheck: true to submit without the check, and the new StellarSDK.checkMemoRequired(transaction) runs the check on its own. The envelope-string methods decode the envelope to check it and submit an envelope they cannot decode unchanged. SEP-7 URIScheme.signAndSubmitTransaction() runs the check when it submits directly to the network, not when it posts to a callback URL, and has no opt-out parameter. Subclasses that override a submit method must add the skipMemoRequiredCheck parameter, and classes that implement StellarSDK must implement checkMemoRequired. NetworkConstants.HTTP_NOT_FOUND (404) joins the HTTP status constants, and a SEP-29 compatibility matrix is added. Contributed by @ngybnc (#190, #191).
Details: SEP-29: Account Memo Requirements, the Memo Requirements section of the SDK usage guide, and signing and submitting in the SEP-7 guide.
XDR decoding hardening
XDR union decoders throw Exception('Unknown <Type> discriminant: <value>') for a discriminant that has no arm, where they returned a partially decoded object and read the following bytes as the next field. This covers every union with an int discriminant (the ...Ext unions, XdrTransactionMeta, XdrLedgerCloseMeta, XdrTransactionPhase, XdrGeneralizedTransactionSet and others) and XdrAsset, XdrAllowTrustOpAsset, XdrMuxedAccount, XdrTransactionEnvelope, XdrFeeBumpTransactionInnerTx, XdrInnerTransactionResultResult, XdrHashIDPreimage and XdrTransactionSignaturePayloadTaggedTransaction for enum values without an arm. Every read is bounded against the remaining input before it moves: an array count is checked before any element is decoded with the new XdrDataInputStream.readArrayLength(), a string, a variable-length opaque or a fixed-length field such as XdrHash before any byte of the value is read, and a fixed-width read (readInt, readBigInt64, readDouble and the other 1-, 2-, 4- and 8-byte readers) before it runs past the input. Each case throws a RangeError naming the count or width (XDR array count 5 exceeds the maximum of 2 for the 8 remaining bytes, XDR byte count cannot be negative, got -1, XDR read of 4 bytes exceeds the 0 remaining bytes); a refused byte count or fixed-width read leaves the read position unchanged. A negative array count previously decoded as an empty list in the generated decoders; readIntArray, readFloatArray and readDoubleArray already rejected negative counts when sizing their result lists. A negative byte count moved the read position backwards, and truncated input surfaced as RangeError("Reached end of file") or the platform's own out-of-bounds error. XdrDataInputStream and DataInput also read a Uint8List that is a view on a larger buffer from the view's own offset; integer and floating-point reads previously started at the beginning of the underlying buffer. SEP-45 WebAuthForContracts.decodeAuthorizationEntries rejects a negative or over-long entry count with ContractChallengeValidationException, and SEP-7 URIScheme.isValidSep7Url reports an envelope with an unknown union arm as invalid (#195, #197).
Exact operation prices
Operations read from XDR render their price string exactly: ManageBuyOfferOperation, ManageSellOfferOperation and CreatePassiveSellOfferOperation built through builder(...), and the minPrice and maxPrice of LiquidityPoolDepositOperation. The string is the decimal expansion of the XDR fraction, up to 20 decimal places and never in exponent notation, computed by the new Price.toDecimalString(). A price below 1e-6, such as 1/10000000, was rendered as "1e-7", which Price.fromString refused when the operation was encoded again; it now reads "0.0000001". Price.fromString parses a decimal exactly, as an integer over a power of ten, and expands it with integer arithmetic instead of through a double, so every nonzero price whose fraction in lowest terms, with a positive denominator, fits int32 round-trips to that fraction: "0.000000004096" parses to 1/244140625 and "0.99999999" to 99999999/100000000, where the double expansion gave 1/244140627 and 1799999971/1799999989. The accepted syntax, the int32 bounds and the exceptions are unchanged. At the int32 boundaries the result follows the exact value: a decimal just above 1/2147483648 parses to 1/2147483647, and a value below -2147483648 is refused. removeTailZero returns a string without a decimal point unchanged; it previously turned "100" into "1" (#196).
Liquidity pool ids in Horizon request builders
RequestBuilder.liquidityPoolIdHorizonHex(String) resolves a liquidity pool id, given as an L... strkey or as hex in either letter case, to the 64 lowercase hex characters Horizon serves, and throws ArgumentError naming the id and the reason for anything else. AccountsRequestBuilder.forLiquidityPool, EffectsRequestBuilder.forLiquidityPool, LiquidityPoolsRequestBuilder.forPoolId, LiquidityPoolTradesRequestBuilder.forPoolId, OperationsRequestBuilder.forLiquidityPool, TradesRequestBuilder.liquidityPoolId and TransactionsRequestBuilder.forLiquidityPool resolve their id through it: a hex id must be exactly 64 hex characters and is sent lowercase, and any other hex id throws ArgumentError before a request is made. These methods previously sent a hex id as given. A malformed L... id still throws ArgumentError; its message now ends with the reason in parentheses (#197).
Breaking: removed constant, retyped getter, new interface members
NetworkConstants.TRANSACTION_TIME_BUFFER_SECONDS, deprecated since 3.5.0 and unused by the SDK, is removed; a reference fails to compile. Network.networkId returns Uint8List instead of Uint8List?; it never returned null. Callers compile unchanged, the analyzer reports networkId! as an unnecessary non-null assertion, and a subclass overriding the getter or a class implementing Network must return Uint8List. Concrete classes implementing Price, XdrDataInputStream or SorobanServer must add toDecimalString(), readArrayLength(), or the nullable logger getter and setter. StellarProtocolConstants.XDR_ALIGNMENT_BYTES (4) names the XDR item alignment used for string, opaque and signed payload padding (#197).
SorobanServer logging
SorobanServer no longer writes to stdout. The new logger constructor parameter and settable field, a void Function(String message)?, receive each RPC response line while enableLogging is set and the TLS warning when httpOverrides is enabled. Without a logger, both go to dart:developer log under the name stellar_flutter_sdk.SorobanServer (SorobanServer.logName); logger: print restores console output. The warning text is SorobanServer.tlsOverridesWarning (#197).
External reference deployment
SorobanClient.deployFromExternalRef reports an executableOwner that is not a contract address with its own Exception message, naming the owner and stating that the executable owner of an external reference must be a contract. Such an owner previously received the external reference does not resolve: owner ... holds no 32-byte wasm hash entry under tag ... message, which now covers only a contract owner whose reference does not resolve. With the SDK's default server, a non-contract owner causes no RPC request in either 3.7.0 or 3.8.0; resolving a contract owner's reference still performs a lookup (#197).
Documentation
The SEP-29 guide describes the built-in check, and the SDK usage guide and the SEP-7 guide note where it runs (#191, #193). Sample secret seeds and account ids in the guides, the skill references and two dartdoc examples that failed the strkey checksum are replaced with valid keys, so the snippets run as written (#194).
Compatibility matrices, skill, and CI
All 22 compatibility matrices regenerated at 3.8.0 with 100% coverage against Horizon v28.0.1 and Soroban RPC v28.0.1, the same baselines as 3.7.0. The agent skill is updated to 1.6.0 with a SEP-29 reference page, the SEP-7 note and a regenerated API reference; the reference generator now emits function-typed fields such as SorobanServer.logger. GitHub Actions pins: claude-code-action 1.0.235, codecov-action 7.1.1, deploy-pages 5.0.1, setup-ruby 1.327.0. The CI test matrix adds Flutter 3.47.x.
Compatibility
Behavior changes: a memo-less transaction with payment, path payment or account merge operations triggers Horizon account lookups before submission and can throw AccountRequiresMemoException or a lookup error where it was posted directly; malformed or unknown XDR throws Exception (unknown union arm) or RangeError (count, length or width the input cannot hold) where it decoded partially or with a different error, and RangeError is an Error that on Exception catch does not catch; URIScheme.isValidSep7Url answers false for an envelope with an unknown union arm and throws RangeError for an envelope with a negative array count, because it catches Exception only; price strings of operations read from XDR are exact decimals, and Price.fromString returns exact fractions where the double expansion gave nearby ones; the liquidity pool request builders refuse hex ids that are not 64 characters and lowercase the rest; SorobanServer output leaves stdout; and two error messages change text (deployFromExternalRef for a non-contract owner, a malformed L... pool id). Compile-time changes: the removed TRANSACTION_TIME_BUFFER_SECONDS, Network.networkId overrides returning Uint8List?, overrides of the six submit methods without skipMemoRequiredCheck, and external classes implementing StellarSDK, Price, XdrDataInputStream, SorobanServer or Network. Ordinary callers of every other API compile unchanged, and the wire encoding of valid XDR is unchanged.
Migrating to 3.8.0
Several changes in this release require action in existing code. See the 3.8.0 migration notes; each note tells you whether you are affected, how it shows, and what to do.