xrpl_flutter_sdk 0.2.3-dev
xrpl_flutter_sdk: ^0.2.3-dev copied to clipboard
The first native Flutter/Dart SDK for the XRP Ledger (XRPL). Pure Dart, no platform channels. Payments, DEX, tokenization and account security.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.3-dev #
Phase 3 in progress: real-time subscription streams, so the SDK can react to network events (new ledgers, transactions, validations, server status changes) as they happen, instead of only polling on demand.
Added #
XrplConnection.ledgerEvents,.transactionEvents,.validationEvents,.serverEvents: typed broadcast streams for XRPL subscription push messages, routed by the message'stypefield (ledgerClosed,transaction,validationReceived,serverStatus). Separate fromrequest()'sid-matched responses, which is a genuinely different kind of message per the official specificationsubscribeToLedger/unsubscribeFromLedger,subscribeToTransactions/unsubscribeFromTransactions(with anincludeProposedoption),subscribeToValidations/unsubscribeFromValidations,subscribeToServer/unsubscribeFromServerin a newlib/src/connection/xrpl_subscriptions.dart- 8 new tests (146 -> 154), including a live integration test that
subscribes to the
ledgerstream and waits for a realledgerClosedevent from the public Testnet server
Design Decisions #
- Chose separate, typed streams per event type over one generic
stream the caller filters by
typethemselves, removes a class of typo-prone, silently-ignored-on-mismatch string comparisons from calling code, and matches the pattern official client libraries in other languages already use (e.g. separate stream channels per event type) - Event stream controllers are created once at construction, not per
connect()call, so a caller's existing.listen()subscriptions keep working across a disconnect/reconnect - Recognized-but-unhandled event types (
consensusPhase,bookChanges,peerStatusChange,manifestReceived) are silently ignored rather than raising an error - each is documented with its official source directly in_routeEvent's doc comment, since receiving an unrecognized event isn't itself a failure, just a feature not built yet (bookChangesis relevant to Phase 5, the admin-only ones aren't relevant to a client SDK at all)
Status #
Phase 3 in progress: connection lifecycle, requests/queries, and
subscription streams complete and verified against the real public
Testnet server.
Not ready for production use.
Next: Phase 3 closing audit (0.3.0-dev).
0.2.2-dev #
Phase 3 in progress: sending/receiving JSON-RPC requests over the connection, with account_info and server_info as the first real queries.
Added #
XrplConnection.request(command, [params]): sends a generic XRPL request and returns its response, matching responses to requests byidso multiple requests can be in flight on the same connection at once. ThrowsXrplConnectionExceptionif not connected, on timeout (20s default), or if the server responds with"status": "error"serverInfo(connection, {counters}): returns the connected server's status (result.info), unwrapped from the response envelopeaccountInfo(connection, account, {ledgerHash, ledgerIndex, queue, signerLists}): returns an account's data (result.account_data); all parameters beyondaccountare optional and only included in the request if explicitly provided- New
lib/src/connection/xrpl_queries.dart, holding command-specific helpers built on top ofXrplConnection.request - 3 new unit/integration tests for
request(), plus integration tests forserverInfoandaccountInfoagainst the real public Testnet server (146 tests total)
Design Decisions #
- Both query helpers return only the useful inner part of the
response (
result.info,result.account_data), not the full envelope - consistent between the two, so callers never need to know XRPL's response wrapping shape accountInfo's optional parameters default tonull, not sensible defaults - each is added to the outgoing request only if explicitly provided, keeping the simplest call as close to the official minimal example as possibleaccountInfo's success case (a funded account with real data) is not yet covered by a test, since Testnet resets periodically and a hardcoded "known funded account" would be unreliable long-term; only the stable error case (a freshly generated, never-funded account) is tested for now - tracked for once this SDK can fund a Testnet account itself
Status #
Phase 3 in progress: connection lifecycle, generic requests, and the
first two real queries complete and verified against the real public
Testnet server.
Not ready for production use.
Next: subscribe streams (0.2.3-dev).
0.2.1-dev #
Phase 3 in progress: the connection layer's foundation - network
endpoints and the WebSocket connection lifecycle (connect/disconnect).
No requests can be sent yet; that's 0.2.2-dev.
Added #
XrplEndpoint: Mainnet/Testnet/Devnet, each with its official public WebSocket URL, verified againstxrpl.org/docs/tutorials/public-servers. Separate fromXrplNetwork(address/), which only covers Mainnet/Testnet since those are the only two networks with an X-address prefix defined by XLS-5dXrplConnectionException: a new exception type, separate fromXrplCryptoException, for connection/network failures - a bad checksum and an unreachable server are different categories of problem and are now distinguishable by exception typeXrplConnection: manages a WebSocket connection's lifecycle (connect,disconnect,isConnected) against a givenXrplEndpoint, usingpackage:web_socket_channelrather thandart:io'sWebSocket, for the same mobile/desktop/web compatibility reasonsdart:mathwas chosen overdart:ioin Phase 1- 13 new unit tests (127 -> 140... TODO: replace with the real count
from pre_commit.ps1). Tests touching the real public Testnet server
are kept in a separate
test/src/connection/xrpl_connection_integration_test.dart, mirroringlib/src/undertest/src/, apart from the pure, network-free unit tests intest/connection/xrpl_connection_test.dart
Design Decisions #
0.2.1-devintentionally covers only the connection lifecycle, not sending or receiving messages - that's grouped with its first real use (account_info,server_info) in0.2.2-dev, rather than shipped in isolation with nothing using it yetXrplConnection.disconnect()is a safe no-op when not currently connected, rather than throwing - "make sure we're disconnected" is a reasonable thing to want regardless of current state- This is the SDK's first genuinely stateful, network-dependent type; everything in Phases 1 and 2 was offline and deterministic
Status #
Phase 3 in progress: network endpoints and connection lifecycle
complete and verified against the real public Testnet server.
Not ready for production use.
Next: sending/receiving JSON-RPC requests over the connection, and
the first real queries (account_info, server_info) - 0.2.2-dev.
0.2.0-dev #
Phase 2 complete. This release consolidates Phase 2: address derivation, from a public key to both classic addresses and X-addresses, fully integrated into XrplWallet, closed with a full error-handling and test suite audit.
Added #
- (Carried from 0.1.1-dev through 0.1.3-dev)
XrplClassicAddress,XrplNetwork,XrplXAddress, and their integration intoXrplWallet(classicAddressfield,xAddress()method)
Changed #
XrplClassicAddress: expanded the class-level doc comment to explain why classic addresses exist (a public key alone is not a usable, self-identifying, self-correcting account identifier)XrplWallet.classicAddress: corrected a doc comment that still pointed callers toXrplXAddress.deriveFromdirectly, written before thexAddress()method existed on the same class; now references[xAddress]
Design Decisions #
- Audited every file under
lib/src/address/and the new sections oflib/src/wallet/xrpl_wallet.dartagainst the same three questions used to close Phase 1: missing edge cases, error message clarity, documentation accuracy. Two of four files needed no changes at all - Reviewed error propagation across every layer in the address pipeline (public key to Account ID to classic/X-address to XrplWallet) and found no coverage gaps this time - unlike the Phase 1 audit, which did find and close two - because each new method's tests already asserted propagation from the layer below it as it was built, not deferred to a later cleanup pass
- Reviewed
test/wallet/xrpl_wallet_classic_address_test.dartandxrpl_wallet_x_address_test.dartfor duplication against the underlyingXrplClassicAddress/XrplXAddresstest suites; confirmed they are intentional chain-verification tests (checking the integration matches the standalone derivation), not accidental duplication, and left them as-is - This audit closes directly into
0.2.0-dev
Phase 2 Summary #
XrplClassicAddress,XrplNetwork,XrplXAddress, and their integration intoXrplWallet- 122 tests total, verified against official specifications and
independently computed vectors (including two vectors taken
directly from the official
ripple-address-codecX-address pull request) throughout - A test-vector transcription bug (reusing an unrelated official
example's public key) was caught by an unexpected test failure
during
0.1.2-dev, investigated rather than "fixed" by adjusting the expected value, and confirmed to be a test-construction error, not an implementation bug - No network interaction yet - that begins in Phase 3
Status #
Phase 2 complete. Not ready for production use.
Next: Phase 3 - Connection Layer (0.2.1-dev).
0.1.3-dev #
Phase 2 in progress: address derivation integrated directly into XrplWallet, so a wallet's address is available without a separate call to XrplClassicAddress or XrplXAddress.
Added #
XrplWallet.classicAddress: derived once at wallet construction and cached as a field, since it never changes for a given walletXrplWallet.xAddress({required network, tag}): a method, not a cached field, since an X-address depends on parameters that can differ on every call- 9 new unit tests (113 -> 122), covering both new members for both
algorithms, and confirming they match calling
XrplClassicAddress/XrplXAddressdirectly
Design Decisions #
classicAddressis a field (computed once, immutable);xAddressis a method (computed per call) - the difference follows directly from whether the value depends on call-time parametersXrplClassicAddressandXrplXAddresswere not modified; this sub-version only wires existing, already-verified pieces together
Status #
Phase 2 in progress: classic address, X-address, and their
integration into XrplWallet complete and tested. No network
interaction yet (that begins in Phase 3).
Not ready for production use.
Next: Phase 2 closing audit (error handling review + test
consolidation), closing at 0.2.0-dev.
0.1.2-dev #
Phase 2 in progress: X-Address, encoding account, network, and an optional destination tag into a single address string.
Added #
XrplNetwork: enum for Mainnet/Testnet, used to select the correct X-Address prefixXrplXAddress.deriveFrom(publicKey, {required network, tag}): derives an X-Address ("X..." mainnet, "T..." testnet), reusingXrplClassicAddress.accountIdFromPublicKeyandXrplBase58.encodeWithChecksum- Tag validation: rejects negative tags or tags above the 32-bit
maximum (
4294967295) withXrplCryptoException - 11 new unit tests (102 -> 113), including 2 official test vectors
(mainnet with the maximum tag, testnet with a small tag) from the
original
ripple-address-codecX-Address PR, independently re-verified via Python before use
Fixed #
- A test-vector transcription error (reusing the wrong public key from an unrelated official example) was caught by an unexpected test failure, not by manual review - corrected by re-verifying every vector independently in Python before finalizing the test file, per this SDK's standing verification practice
Design Decisions #
- Discovered during research that the X-Address payload is 31 bytes
(2-byte network prefix + 20-byte Account ID + 1-byte flag + 4-byte
tag + 4 reserved bytes), not 30 as initially assumed - confirmed
against the official
ripple-address-codecX-Address PR before implementing - The destination tag is encoded little-endian, the only place in this SDK where byte order is reversed from the big-endian convention used everywhere else (seeds, keys)
tagis an optional parameter (int?), not a required one like the signing algorithm elsewhere in the SDK - omitting a tag is a valid, common state, unlike omitting the algorithm
Status #
Phase 2 in progress: classic address and X-Address derivation
complete and tested against official vectors. No network interaction
yet (that begins in Phase 3).
Not ready for production use.
Next: integrating addresses into XrplWallet (0.1.3-dev).
0.1.1-dev #
Phase 2 in progress: classic address derivation from a public key.
Added #
XrplClassicAddress.accountIdFromPublicKey(publicKey): derives the 20-byte Account ID viaRIPEMD160(SHA256(publicKey))XrplClassicAddress.deriveFrom(publicKey): derives the full classic address ("r..."), reusingXrplBase58.encodeWithChecksum- 9 new unit tests (93 -> 102), including the complete official
worked example published directly in
xrpl-dev-portal(addresses.md), independently re-verified via Python before use
Design Decisions #
- New
lib/src/address/folder, sibling tocrypto/,codec/,wallet/,exceptions/- address derivation is its own concept (all of Phase 2), not a cryptographic primitive, and will integrate withXrplWalletin a later sub-version - Reuses
XrplBase58.encodeWithChecksumrather than duplicating the checksum/encoding logic - addresses and seeds share the same Base58Check-style scheme, just a different type prefix (0x00vs0x21/0x01 0xE1 0x4B)
Status #
Phase 2 in progress: classic address derivation complete and tested
against an official worked example.
Next: X-Address (0.1.2-dev).
0.1.0-dev #
Phase 1 complete. This release consolidates the full first phase of the roadmap: cryptographic fundamentals, from raw entropy through a unified wallet API, closed with a full error-handling and test suite audit.
Added #
XrplSecp256k1.deriveIntermediateKeyPair: validates thatrootPublicKeyis exactly 33 bytes, throwingXrplCryptoExceptionotherwise (previously unvalidated on this public method)- 2 new tests confirming error propagation through previously
untested layers:
XrplWallet.fromSeedwith a corrupted seed, andXrplSeed.fromBase58with an invalid base58 character (93 tests total, up from 91)
Fixed #
XrplBase58._base: corrected a doc comment copy-pasted from thealphabetfield above itXrplSecp256k1: 2 doc comments describing the root/intermediate combination as "not yet implemented" corrected to referencederiveKeyPair, which already implements itXrplEd25519: doc comment describingXrplWallet's unified API as a future plan ("will expose") corrected to present tense
Design Decisions #
- Audited every file in
lib/src/against three questions: missing edge cases, error message clarity, and documentation accuracy - three files needed no changes, confirming validation was built incrementally per sub-version rather than deferred to the end - While fixing a doc comment, a real dartdoc cross-reference briefly
introduced an import cycle between
xrpl_ed25519.dartandxrpl_wallet.dart. Reverted; documented as a standing rule:crypto/andcodec/never import fromwallet/ - This audit closes directly into
0.1.0-devrather than publishing an intermediate0.0.6-devfirst, since the code would have been identical between the two releases
Phase 1 Summary #
XrplEntropy,XrplBase58,XrplSeed,XrplKeyAlgorithm,XrplHash,XrplSecp256k1,XrplEd25519,XrplWallet- 93 tests, verified against official specs and independently
computed vectors (Python
hashlib,ecdsa,pynacl) throughout - No network interaction yet - that begins in Phase 2/3
Status #
Phase 1 complete. Not ready for production use.
Next: Phase 2 - Addresses (0.1.1-dev).
0.0.5-dev #
Phase 1 in progress: XrplWallet, the unified public API tying together seed generation and key derivation for both algorithms.
Added #
XrplWallet, in a newlib/src/wallet/folder (sibling tocrypto/,codec/,exceptions/- not nested undercrypto/, since a wallet is the layer that combines those, not a cryptographic primitive itself):XrplWallet.generate({required algorithm})XrplWallet.fromSeed(value, {required algorithm})publicKeyBytes/privateKeyBytes: always plainUint8Listregardless of algorithm (33 and 32 bytes respectively), even thoughsecp256k1andEd25519use different underlying private key types (BigIntvsUint8List) internally
- Algorithm-mismatch protection in
fromSeed(): ansEd-declared (Ed25519) seed used with a mismatched requested algorithm throwsXrplCryptoExceptioninstead of silently deriving the wrong key pair - the validation this SDK left as a known gap since0.0.3-devis now closed - 14 new unit tests (81 -> 90), including two full, real vectors
chained end-to-end through
XrplWallet(not just its building blocks): the same official secp256k1 seed (sn259rEFXrQrWyx3Q7XneWcwV6dfL) and Ed25519 seed (sEdTM1uX8pu2do5XvTnutH6HsouMaM2) already verified in earlier sub-versions
Design Decisions #
- Public and private keys are exposed only as unified
Uint8Listbytes - the SDK deliberately does not also expose each algorithm's original type (BigIntfor secp256k1) alongside the unified bytes. If a concrete need for that surfaces later, it will be added then, with real context, rather than speculatively now XrplWallet's public API is uniformly asynchronous (Future<XrplWallet>) regardless of algorithm, even though secp256k1 derivation is actually synchronous internally - this hides the sync/async split betweenXrplSecp256k1andXrplEd25519(documented indocs-sdk/phase-1/key-derivation/) behind one consistent API
Status #
Phase 1 in progress: entropy, base58 codec, family seed encoding, key
derivation (both algorithms), and the unified XrplWallet API
complete and tested.
No network interaction yet (that begins in Phase 3).
Not ready for production use.
Next: error handling and validation review (0.0.6-dev).
0.0.4-dev #
Phase 1 in progress: key pair derivation for both secp256k1 and Ed25519, the last cryptographic building block before XrplWallet.
Added #
XrplHash.sha512Half: shared SHA-512Half hashing utility used throughout key derivationXrplSecp256k1: full official algorithm -deriveRootKeyPair,deriveIntermediateKeyPair, andderiveKeyPair(the combined master key pair), usingpointycastle'sECDomainParametersXrplEd25519:deriveKeyPair, usingpackage:cryptography- 17 new unit tests (64 -> 81). Every derivation step is checked
against a real vector independently computed via Python
(
hashlib, theecdsalibrary, andpynacl/libsodium), not just our own round-trip tests
Changed #
- Added a private constructor to
XrplBase58to prevent instantiation of a static-only class, resolving the lastpanadocumentation hint from a previous release
Design Decisions #
XrplSecp256k1stays synchronous (pointycastle);XrplEd25519is asynchronous (package:cryptography). This isn't a style choice -package:cryptographydoes not support secp256k1 at all (only Ed25519, X25519, and NIST curves P-256/P-384/P-521), so the two algorithms cannot share one library here- Chose
package:cryptography(actively maintained) overed25519_edwards(synchronous, but unmaintained for ~4 years) oredwards25519(maintained, but a low-level curve library that would require implementing key derivation from scratch on top of it) - correctness and maintenance outweighed API symmetry XrplWallet(0.0.5-dev) will expose a single, uniformly asynchronous public API regardless of algorithm, wrapping secp256k1's already-synchronous path rather than reimplementing it
Status #
Phase 1 in progress: entropy, base58 codec, family seed encoding, and
full key pair derivation (both algorithms) complete and tested
against official/independent vectors.
No network interaction yet (that begins in Phase 3).
Not ready for production use.
Next: XrplWallet, the unified public API (0.0.5-dev).
0.0.3-dev #
Phase 1 in progress: family seed encoding implemented, combining entropy and the base58 codec into real, verifiable XRPL seeds.
Added #
XrplKeyAlgorithm: enum for the two XRPL signing algorithms (secp256k1,ed25519), shared across seed encoding and the upcoming key derivation stepXrplSeed.generate(algorithm:): generates a new random seed;algorithmis a required parameter, matching the SDK-wide rule that the signing algorithm is never inferred silentlyXrplSeed.fromBase58(value): decodes and checksum-verifies a seed string, recognizing both the generic0x21prefix and the Ed25519-declaringsEd...(0x01 0xE1 0x4B) prefixXrplSeed.toBase58(): encodes a seed back to its string form- 16 new unit tests (43 -> 59), including 4 test vectors taken
directly from the official
ripple-address-codectest suite (3 Ed25519, 1 secp256k1), independently re-verified via a standalone Python re-implementation before use
Design Decisions #
- Seeds support both known XRPL prefixes rather than only the generic one, so seeds generated by this SDK self-describe their algorithm to other XRPL tools (wallets, explorers, other SDKs) and avoid the same "same seed, different address" ambiguity problem at the interoperability level, not just within this SDK
- There is no dedicated secp256k1 prefix in the XRPL specification
(only Ed25519 has one), so
declaredAlgorithmisnullfor secp256k1 seeds, not an explicit enum value
Fixed #
- An initial implementation used an incorrect 2-byte Ed25519 prefix
(
0x01 0xE1), based on a misread of a manually decoded seed. Caught during verification against the officialripple-address-codecsource and a real published seed - the correct prefix is 3 bytes (0x01 0xE1 0x4B). Fixed before merging, and the test suite now uses official test vectors instead of a manually-derived one to prevent a repeat.
Status #
Phase 1 in progress: entropy, base58 codec, and family seed encoding
complete and tested against official vectors. No network interaction
yet (that begins in Phase 3).
Not ready for production use.
Next: secp256k1 and Ed25519 key pair derivation (0.0.4-dev).
0.0.2-dev #
Phase 1 in progress: XRPL base58 codec implemented and verified, including checksum-based corruption detection.
Added #
XrplBase58.encodeRaw(Uint8List): encodes raw bytes into an XRPL base58 string using the ledger's own alphabet (distinct from Bitcoin's), preserving leading zero bytes correctlyXrplBase58.decodeRaw(String): exact inverse ofencodeRaw, rejects any character outside the XRPL alphabetXrplBase58.checksumOf(Uint8List): computes the 4-byte double-SHA256 checksum (Base58Checkstyle) used by XRPL seeds and addresses, viapointycastle'sSHA256DigestXrplBase58.encodeWithChecksum(Uint8List): encodes data with the checksum appendedXrplBase58.decodeWithChecksum(String): decodes and verifies the embedded checksum, throwingXrplCryptoExceptionon a mismatch, on invalid characters, or on data too short to contain a checksum- 43 unit tests total (up from 10), including:
- round-trip tests between
encodeRaw/decodeRawacross multiple byte patterns (including leading zeros) - a checksum test vector computed independently via Python's
hashlib, not just structural assertions - a real mistyped-character scenario proving
decodeWithChecksumcatches corrupted input instead of silently returning bad data
- round-trip tests between
Changed #
lib/xrpl_flutter_sdk.dart: exportedXrplBase58now that its public API (encode/decode, with and without checksum) is complete
Design Decisions #
- Split raw conversion (
encodeRaw/decodeRaw) from checksummed conversion (encodeWithChecksum/decodeWithChecksum) instead of a single function, since not everything encoded in XRPL base58 carries a checksum, and the checksummed versions are built on top of the raw ones rather than duplicating the conversion logic decodeWithChecksumverifies the checksum unconditionally; there is no "skip verification" option, to prevent silently accepting corrupted seeds or addresses
Status #
Phase 1 in progress: entropy generation and base58 codec complete
and tested. No network interaction yet (that begins in Phase 3).
Not ready for production use.
Next: family seed encoding (0.0.3-dev), combining XrplEntropy and
XrplBase58.
0.0.1-dev #
Phase 1 in progress: entropy generation implemented and verified
against the official xrpl-keypairs specification.
Added #
XrplEntropy.generate(): generates 16 bytes of cryptographically secure random entropy usingdart:math'sRandom.secure, matching the length required by the officialxrpl-keypairslibrary (Ripple's reference implementation)XrplEntropy.fromBytes(Uint8List): restores entropy from previously saved bytes, validating length before accepting itXrplEntropy.validate(Uint8List): static validation helper, throwsXrplCryptoExceptionon invalid length instead of failing silentlyXrplCryptoException: dedicated exception type for cryptographic validation errors across the SDK- 11 unit tests covering length validation, randomness across calls, immutability of stored bytes, and error message content
example/xrpl_flutter_sdk_example.dart: working example of generating, restoring, and handling invalid entropy
Changed #
analysis_options.yaml: excludedexample/from strict analysis, sinceavoid_printis expected in example code, not library code
Design Decisions #
- Chose
dart:mathoverdart:iofor randomness so the SDK keeps the door open for Flutter Web support without rewriting Phase 1 code - Algorithm (
secp256k1/Ed25519) is a required parameter everywhere, never inferred from context, to avoid the same-seed/ different-address ambiguity documented on xrpl.org
Status #
Phase 1 in progress: entropy generation complete and tested. No
network interaction yet (that begins in Phase 3).
Not ready for production use.
Next: base58 XRPL codec (0.0.2-dev).