libwallet 0.4.67
libwallet: ^0.4.67 copied to clipboard
Multi-chain TSS cryptocurrency wallet client. Supports EVM, Bitcoin, Solana and Monacoin via direct FFI to the Go libwallet core.
0.4.67 #
- Fix OKX Solana swap signing: decode
tx.dataas base58 or base64. OKX's DEX aggregator/swapendpoint returns Solanatx.database58-encoded, but the decoder only ran it through base64. Because base58's alphabet is a subset of base64's, a base64 decode of a base58 payload succeeds but yields garbage — whose leading byte was read as a 79-signature count, surfacing asLibwalletException(500): sign okx solana transaction: signatures truncated: declared 79, tx only 708 bytes. Some routes do return genuine base64 (notably larger v0/ALT transactions), so both encodings occur. The decoder now tries both schemes and returns the candidate that parses as a structurally valid Solana transaction.
0.4.66 #
- Doc fix: Dart
promoteandpromoteMnemonicare no longer documented as secp256k1-only. The Go side gained FROST/Ed25519 support a while back; both Dart wrappers' docstrings still said "secp256k1 source wallets only in this release; ed25519 mnemonic migration is a follow-up" / "Currently supports secp256k1 wallets only; ed25519 promotion is a follow-up", leaving SDK readers to conclude the surface didn't exist. The docs now describe the actual coverage:secp256k1source → DKLs23 MPC.ed25519source → FROST MPC.promoteMnemoniccan fan out from a single BIP39 seed to chains on either curve in one call (per-ChainMigration.curve).
- Same docstrings now spell out the canonical 1-of-3 committee
[StoreKey, RemoteKey, Password]with threshold 1, matching the godoc and error guidance added on the Go side in 0.4.65.
0.4.65 #
- Document the canonical new-committee shape for Promote /
PromoteMnemonic. A field caller (tibaneapp's in-app mnemonic
import flow) was calling
Wallet:promoteMnemonicwithnewKeys = [Password]and surfacing the 500 errorNew must contain at least 2 KeyDescriptions (got 1). The validation is correct — modern FROST / DKLs23 wallets are inherently multi-party — but the error message gave no hint at the expected shape. BothPromoteandPromoteMnemonicgodocs now describe the canonical 3-key committee[StoreKey, RemoteKey, Password]with threshold 1 (matching what the standard wallet-create flow uses) and the error messages reference the godoc. No functional change in libwallet; tibaneapp's caller is being updated to mint the full committee in lockstep.
0.4.64 #
- Fix
invalid key for wallet: wlet500 on imported wallets.Wallet:importPrivateKey(wltwallet/import.go) andWallet:promoteMnemonic(wltwallet/promote_mnemonic.go) were minting the new wallet row withxuid.New("wlet")— a typo for the canonical"wlt"prefix. Every imported wallet that later flowed back throughWalletById(and there's no path that doesn't) tripped the strictPrefix != "wlt"check atapi.go:36and returned 500. The typo is corrected at both mint sites andtransfer.go's tolerant-prefix workaround (which accepted the bad prefix as a stopgap) is tightened back to strict"wlt"only. Wallets persisted with the bad prefix in the field will need to be re-imported —"wlet"is illegal at every reader site and must never reach the DB again.
0.4.63 #
- Fix FROST reshare
wi PoK verification failedagainst wdrone. Root cause:tss-libversion drift. libwallet was on v2.2.9; wdrone on v2.3.1. v2.3.1 changedcommon.SHA512_256i_TAGGEDto bind each big.Int operand's sign (extra0x00/0x01byte per operand), which is a wire-incompatible hash change. The two sides computed different Schnorr challengescfor the same inputs, so wdrone's PoK (built with v2.3.1'sc) failed verification by the new committee (using v2.2.9'sc). Sign worked because in-field signing in the current app shape uses[StoreKey + Password](no wdrone). Reshare for password reset requires wdrone in the OLD committee — and that's where the hash drift surfaced asfrost-reshare-round2 round 0, culprits [...] wi PoK verification failed.- libwallet
tss-lib v2.2.9→v2.3.1. The four real-infra reshare tests (TestRemoteWallet,TestEdDSALocalToRemoteReshare,TestReshare_FROST_StoreKeyPlusRemote_AsOldCommittee,TestReshare_FROST_PasswordPlusRemote_AsOldCommittee) all pass end-to-end against the live wdrone fleet.
- libwallet
- Wait for the full spot relay mesh before reshare init.
spot.WaitOnlinereturns atonlineCnt > 0— i.e. after just the first relay handshake. spotlib typically dials 2 relays in parallel and the second takes ~1–2 s longer; sending peer queries in that window routes via a single relay and can stall when the target peer prefers the other one.waitOnlineSpotnow pollsConnectionCount()untilconnCntis stable for 1 s (no new connections arriving) or 8 s elapses, then proceeds. Healthy reshare timings are unchanged; only fresh-process flows (test binaries, cold-start hosts) see the small new pre-roll.
0.4.62 #
- Reshare init per-attempt timeout: 90 s → 15 s. 0.4.57 raised
the spot init query ceiling to 90 s on the theory that wdrone's
internal
loadSharecould take up to its 60 s worst case; 0.4.60 then stacked a 3-attempt peer-fallback retry on top. Together that made a hard reshare failure take ~4–5 minutes before surfacing to the user, even though a healthy reshare completes in seconds end-to-end. 0.4.62 reverts the per-attempt budget to 15 s: well above the typical ~1–2 s success, short enough that a wedged wdrone fails fast. With the 3-attempt fallback still in place, a hard failure now exhausts in ~30–45 s instead of 4–5 min, and a single slow peer still gets routed around. Healthy reshare timings are unchanged. - spotlib v0.3.0 → v0.3.2. Confirmed
TestRemoteWalletagainst the live wdrone fleet succeeds with the retry on top of v0.3.2: first peer wedged, second peer succeeds in ~3 s, full TSS reshare completes in ~30 s total.
0.4.61 #
- Fix dkls23 RemoteKey share wrapper/raw-bytes mismatch.
libwallet was uploading dkls23 shares as
{"data":"<base64>"}(thedklsKeyWrapperstruct it uses for local round-tripping), but wdrone'sloadSharedecodes the share into a[]bytetarget via the cryptutil bottle pipeline — a wrapped struct producedjson: cannot unmarshal object into Go value of type []uint8and wedged every dkls23 RemoteKey reshare at the server side. The wrapper is now only used for local StoreKey/Password decrypt (wheredecryptDkls23expects it); RemoteKey uploads ship the rawdklstss.Save()bytes directly, which JSON encodes as a base64 string and round-trips into[]byteon the wdrone side. - Real-infra reshare tests are now active. Added a
TestMainthat registersSec-ClientId: com.ellipx.walletapp(the app identifier ellipx-mobile-app ships) soTestRemoteWalletandTestEdDSALocalToRemoteReshareexercise the live Crypto/WalletSign + wdrone path — previously they were hard- skipped despite the credentials being available. Backend reachability flakes still trigger the documented skip predicates; otherwise these tests fully drive create → keygen → upload → reshare → re-upload. - TestRemoteWallet's old-keys subset bug fixed: it was passing all
3 wallet keys as the old committee, tripping the dkls23 guard
requires exactly T+1=2 old signers in the active subset, got 3. Now selects one Plain + the RemoteKey, the same shape an end-user reshare would use.
0.4.60 #
- Reshare init retries on a different wdrone peer when the
selected one hangs. A wdrone can pass
/pingin sub-second and then hang on/init(slowloadShare, internal goroutine wedged) — without retry, one bad pick produced a 90-s reshare failure even when other wdrones in the fleet would have served the init promptly.spotPeer.Startnow makes up to 3 attempts, re-runningselectPeerwith previously-tried peers excluded so a retry never re-rolls into the same bad peer. The 90 s per- attempt budget is unchanged; wdrone's ownloadShareceiling is 60 s and the spare 30 s covers response routing. The surfaced error names every peer that was tried so field reports can be matched to specific wdrones. selectPeernow accepts a variadicexcludeslist and filters the candidate set before pinging. Zero excludes preserves the original behaviour exactly.
0.4.59 #
- Fix legacy WalletSign protocol naming. 0.4.57's RemoteKey
share upload sent the libwallet-internal dispatch tags
protocol=eddsa(legacy ed25519) andprotocol=gg18(legacy secp256k1) on the wire — but wdrone'sloadShare(walletsign.go:173) only recognises""/"legacy"/"frost"/"dkls23". Anything else lands in the"unsupported share protocol %q"arm and the wallet is unreshareable. Legacy uploads now sendprotocol=legacy, matching wdrone's ownreportResultconvention. The on-wire vocabulary is pinned in a newTestUploadCurveProtocolso a future refactor can't re-leak the internal tags. FROST / DKLs uploads are unchanged — they already shipped the right values. - Reshare regression tests skip on
failed to init remote: context deadline exceeded(a backend reachability flake on real-network runs) but still fail loud onno payload available …— that's the user-facing protocol-on-upload regression signature.
0.4.58 #
- Fix OKX Solana swap
illegal base64 at input 1096. OKX's Solana adapter returnstx.dataas standard base64 for most routes but as base64url (URL-safe-_instead of+/) for versioned (v0) transactions that reference address-lookup-tables — in practice routes whose serialized payload exceeds ~1 KB. The decode path usedbase64.StdEncoding.DecodeStringand tripped on the first URL-safe byte. Now normalizes the alphabet (-→+,_→/) and re-pads to a multiple of four before decoding, so both shapes round-trip. NewTestOkxDecodeSolanaTxDatacovers standard / URL-safe / raw / unpadded variants. - CI: gate publish on
lib/src/version.dart. v0.4.57 shipped to pub.dev with a stalelibwalletPackageVersion = '0.4.56'because the publish workflow's only version gate was tag vspubspec.yaml.dart-test.ymlalready runstools/bump_version.dart --checkon every master push, but that's a separate workflow —publish-dartnow re-runs the same check before doing anything else, so a drifted constant fails the publish job itself. - CI: nil-guard
Transaction.Validate(also in 0.4.57; tracked here for completeness because the panic was caught by apirouter and turned into a 500, so 0.4.57's FFI users still saw the bad path). Thetransferandsolana_transfer/solana_spl_transferbranches were callingtx.Amount.Sign()without a nil check; they now matcherc20_transfer/bitcoin_transfer.
0.4.57 #
- Fix FROST reshare timeout for
[StoreKey + RemoteKey]. The remote-share upload insetGeneratedKeywas sending only thecurveparameter (ed25519/secp256k1); wdrone keys itsCrypto/WalletSign:pulllookup by(sid, curve, protocol)so FROST shares were filed without their protocol tag and the later reshare pull returnedno payload available for curve ed25519 protocol frost. The init RPC then sat untilcontext deadline exceeded, which surfaced to the field asfailed to init remote: context deadline exceeded. Upload now sendsprotocolfor FROST/DKLs/legacy-EdDSA/legacy-ECDSA, so wdrone can find the share. - Raise reshare init timeout from 15 s to 90 s. wdrone's
loadSharehas a 60 s internal ceiling for the phplatformCrypto/WalletSign:pullfetch. The 15 s client timeout was tripping every slow DB read while wdrone was still happily loading the share. 90 s leaves a 30 s margin over wdrone's ceiling for the local handshake plus broker setup. Transaction:validateno longer panics on missing amount. Thetransferandsolana_transfer/solana_spl_transferbranches were callingtx.Amount.Sign()without a nil check, matching whaterc20_transferandbitcoin_transferalready do. A dart caller sending{type: "transfer", from, to}with no amount now gets"invalid amount"instead of a goroutine panic captured by apirouter.- CI: restore
-shortflag.TestNftMetadataself-skips in short mode because it depends on an external Ethereum contract still implementingtokenURI/uri/contractURI. Tolerate the same transienteth_getBalance-32603 in the dartAsset:listintegration tests as the existingtestRpc/simulation tests do. - Reshare regression tests (
reshare_scenarios_test.go): reproduce the FROST[StoreKey + RemoteKey]and[Password + RemoteKey]recovery flows against the real backend; conditionally skip when the GHA runner can't reach phplatform (samefailed to select peerpatternTestWalletPeersalready uses).
0.4.56 #
- Swap is OKX-only now. Jupiter Ultra, dFlow, and 1inch
adapters are removed from the tree — the OKX migration shipped
in earlier bumps left them in as a flip-back safety net behind
commented
RegisterProviderlines, and they've been unreached since.wltswap/jupiter.go,wltswap/dflow.go, andwltswap/oneinch.goare deleted; the Jupiter HTTP regression tests inwltswap/http_test.gogo with them.Swap:availabilityand theQuoteAttempt.provider*fields now advertise only"okx_solana"/"okx_evm". Host code that conditioned on the legacy names should switch to the OKX names; themissing_api_keyreason is no longer emitted (kept defined inerrors.goas a reserved code for any future adapter that ships keyless). - New
Swap:countryAvailabilityendpoint. Cheap predicate the host can call from settings / onboarding to decide whether the Swap UI should be visible at all for the user's jurisdiction. Takes ISO 3166-1 alpha-2 (case-insensitive); returns{available, country, reason}wherereasonis empty on success,"invalid_country"on a bad code, or"country_not_supported"when the code parses but isn't on the allow-list. Source of truth is OKX's published iOS app availability list at https://www.okx.com/app-availability/ios (121 jurisdictions as of the 2025-05-22 snapshot); refresh by re-fetching the page and reconciling againstokxAvailableCountriesinwltswap/country_availability.go.
0.4.55 #
- Fix "Program failed to complete" on SPL transfers with priority
fees. 0.4.53 introduced an SPL transfer path that always emits
an
AssociatedToken::CreateIdempotentprelude (~15,200 CUs) plusTransferChecked(~300 CUs). But the existingsolanaDefaultCULimit = 1_000constant was calibrated for the native SOL System-Program transfer (~200 CUs) — so any SPL transfer that triggered theComputeUnitLimitdefault (i.e. anyone usingPriorityLevelorComputeUnitPrice) ran out of compute mid-flight and got back the crypticTransaction simulation failed: Error processing Instruction 1: Program failed to completefrom the RPC. - CU limit now comes from
simulateTransaction. Before signing, the sign path builds a draft message with a 200k cap, callssimulateTransaction(sigVerify=false), readsunitsConsumed, and sets the on-wire CB limit to that value plus a 10%-or-+1000 headroom (the larger of the two). Caps at Solana's per-tx 1.4M CU maximum; never lowers a caller-pinnedComputeUnitLimit. On RPC failure falls back to a conservative hardcoded default (1k native, 30k SPL) so a network burp doesn't block the transaction. This is the same pattern Phantom and Solflare use.
0.4.54 #
- SPL Token-2022 support — including transfer-fee mints. 0.4.53
shipped SPL Token-1 (USDT/USDC) and rejected Token-2022 mints
outright with a clear error. This release adds the Token-2022
program id (
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) for both ATA derivation and the SPL instruction, plus on-chain mint introspection for the TransferFeeConfig extension. When the mint has an active transfer-fee for the current epoch, the sign path fetches the mint account, parses the TLV extension data, computes the fee with the on-chain formula (ceil(amount * bps / 10_000)capped atmaximum_fee, with epoch-based newer/older config selection), and emitsTransferCheckedWithFee(extension instruction 26→1) so the program accepts the pre-computed fee. Plain Token-2022 mints (no fee extension) use the same TransferChecked path as Token-1, just under the Token-2022 program id. - SPL transfer preflight. Before signing an SPL transfer, the
sign path now runs a fast precondition check: sender's SOL must
cover the network fee plus a conservative ATA-rent reserve
(2,039,280 lamports — covers the recipient-ATA-doesn't-exist
case; over-reserving when it does exist is recoverable for the
user), and sender's SPL token account must hold at least the
transfer amount. Surfaces as
PreflightErrorcodesinsufficient_balancewith a clear message naming the shortfall; the previous behaviour let the RPC's "insufficient funds" bubble through after the TSS sign had already run. - Best-effort: RPC failures during preflight are treated as non-blocking; the broadcast surface any real problem.
0.4.53 #
Transaction.type = "transfer"now actually transfers the asset you asked for. Previously the Solana sign/send path ignoredtx.Typeand unconditionally built a native SOL transfer, so a USDT/USDC transfer became a SOL transfer with the token base-units (e.g. 1_315_764 = 1.315764 USDT) sent as lamports of SOL (~0.00132 SOL). The fix lifts the asset resolution intoValidateand routes every chain family from there:- Solana SPL Token-1:
tx.Assetresolves to an SPL mint → the sign path derives sender + recipient associated token accounts, attaches an idempotent ATA-create instruction (so the recipient doesn't need a pre-existing token account), and emits an SPLTransferChecked(opcode 12). USDT, USDC, and every other Token-1 mint work transparently. Token-2022 mints are rejected with a clear error for now — opcode + extension handling is the next bump. - EVM ERC-20:
tx.Type = "transfer"+ an ERC-20 asset auto-routes to the existing ERC-20 path (rewrites tx.To / tx.Data to the contract). The explicit"erc20_transfer"type still works for callers that prefer it. - Bitcoin family: any non-native asset rejected at Validate time. There are no contract-level assets on bitcoin-family chains; the previous code would have silently treated the asset as native.
solana_transfer/solana_spl_transferkept as accepted aliases for back-compat.
- Solana SPL Token-1:
0.4.52 #
- Device-transfer confirm/cancel endpoints are actually reachable
now. The session-lifetime fix in 0.4.51 was correct, but the
endpoint names
Wallet:exportToDevice:confirmandWallet:exportToDevice:cancelwere structurally unreachable because of a parser disagreement between two libs in the stack:pobj.RegisterStaticsplits the name at the FIRST:to find the object (so the handler ended up atWallet/static["exportToDevice:confirm"]), whileapirouter.Callsplits the request path at the LAST:to find the method (so it looked forWallet:exportToDevice/static["confirm"]). Two different keys, lookup always missed, every confirm call produced theLibwalletException(404): Not foundTibane was seeing on 0.4.51. The earlier chain of bugs (transfer/
0.4.51 #
Wallet:exportToDevice:confirmnow resolves the session it needs. 0.4.49's "claim out of registry on handler entry" pattern (added to race-protect the new global dispatcher) deleted the session before the host could run its biometric prompt — the confirm endpoint looks the session up by sid in the same registry, found nothing, returnederrTransferSessionNotFound, and the host sawLibwalletException(404): Not found. The session now stays intransferRegistryfor the full confirm window; race protection moves to a newclaimedflag on the session itself, so concurrent pair requests for the same sid still resolve as session_not_found without disturbing the confirm path. The registry entry gets removed in the handler's defer once the confirm round trip completes (success / decline / timeout), with the existing 5-min cleanup ticker as the backstop.
0.4.50 #
wallet:transfer:pair_receivedactually reaches the host now. 0.4.49 fixed the spotlib dispatcher mismatch so the source's handler started firing — but the event was emitted toe.Emitter(), which is the in-process Go-side hub used for cross-package signals (e.g.wallet:pubkey_repaired). Host events have to go throughapirouter.BroadcastJson→ the FFI socketpair bridge wired up incshared/ffi.go. The receiver'simportFromDevicewas timing out at 90 s — the handler entered the confirm-wait select, but nothing forwarded the pair event to the host, so noexportToDeviceConfirmever came back. Switched the emit toapirouter.BroadcastJsonwith the canonical{result, event, data}envelope.
0.4.49 #
- Device transfer actually fires
wallet:transfer:pair_receivednow. The handler endpoint was registered at"transfer/<sid>", but spotlib's dispatcher (spotlib/connect.go:317-336) only matches the first path segment after the recipient id —"transfer". The lookup always missed, the source's handler was never invoked, and the receiver hung the fulltransferQueryTimeoutbefore giving up. Refactor:- One permanent handler installed at
InitEnvunder the bare"transfer"prefix, the same single-segment shapepair.gouses successfully. - Sessions are demuxed by a new
sidfield in the wire body (transferQueryBody.Sid). Receiver sends<spotID>/transferas the target; the source's handler claims the matching session out oftransferRegistryunder one lock per request. - 0.4.48's
local_offlinefast-fail on offline Spot still applies. Hosts already on 0.4.48 will see receivers fall through topeer_unreachableafter 2 minutes — bump both ends together.
- One permanent handler installed at
0.4.48 #
Wallet:exportToDevicenow fails fast when Spot isn't online. Previously the source painted a syntactically valid pairing QR even when its Spot client hadn't completed the broker handshake — becausespot.TargetId()returns a static key-derived id that's available whether or not the node is reachable. The receiver'sspot.Querythen hung the fulltransferQueryTimeout = 2 minutesbefore giving up withpeer_unreachable. Export now callswaitOnlineSpotfirst (15 s ceiling) and returnslocal_offlineif the handshake doesn't finish, so the host can surface a clear "you're offline" error and not paint a dead QR.client.wallets.exportToDevicedocuments the new error code.- New
Spot:statusendpoint, exposed asclient.spot.status(). Returns{online, targetId, connections: {total, online}}. Lets hosts positively poll Spot connectivity instead of relying on the edge-triggeredonline_statusevent, which is silently missed when Spot comes online before the host subscribes to events.
0.4.47 #
- Fix
EXC_BAD_ACCESSsegfault in(*ECPoint).ToEd25519PubKeywhen switching to a FROST wallet.EnsureEd25519Pubkey(and the legacy ed25519 sign-path self-heal) gated only onWallet.Curve == "ed25519", not onProtocol. FROST wallets carryCurve == "ed25519"too, so the helper randecryptEdDSAagainst a FROST share. fxamacker/cbor silently zero-filled the unknown fields, leaving*eddsatss.Key'sEDDSAPubnil; the chained.ToEd25519PubKey()then dereferenced a nil*ECPointand segfaulted at offset 0x10. Now both self-heal sites gate onresolveProtocol() == ProtocolLegacyEdDSA, anddecryptEdDSAfails loudly when EDDSAPub is missing so future miswired callers raise an error instead of crashing the process.
0.4.46 #
- Fix
lib/src/version.dartdrift in 0.4.45. The 0.4.45 release shipped withlibwalletPackageVersionstill at0.4.44, because the release commit editedpubspec.yamldirectly instead of going throughtools/bump_version.dart. At runtime this madeLibwalletClient.initializesee a stale-binary mismatch on every 0.4.45 install — the very condition the constant exists to detect. Both files are now bumped together. - Bundles the latest Go core, which adds Solana ChiefStaker mint pulls to the curated wlttoken set.
0.4.45 #
- Build hook prunes stale-version binaries from the shared cache.
The cache key embeds the package version (
liblibwallet-<platform>-v<version>.<ext>), so bumping the package left the previous version's binary sitting in the shared output cache forever. The hook now deletes other-version binaries for the current platform before serving the downloaded one. Best-effort: a deletion failure never breaks the build.
0.4.44 #
-
iOS: actually fixed the dual Go runtime that caused Tibane's TestFlight SIGABRT in
runtime.raise_trampoline.abi0. 0.4.43's attempt (s.static_framework = true) was the wrong tree — it killed the duplicate framework, but moved the Objective-C bridge into Runner as a hidden-visibility static-lib symbol that dlsym couldn't find, so iOS wouldn't initialise at all. The real fix is to stop force-loading the libwallet.ainto Runner at all:- Removed
s.user_target_xcconfigfrom the podspec. The-force_loadit added is what made Xcode link the Go static archive into Runner alongside the dynamic libwallet.framework that CocoaPods builds underuse_frameworks!(Flutter default). Pre-0.4.43, both copies initialised their own Go runtime; the two fought over signal-handler ownership and occasionally aborted. - The pod is back to a normal dynamic framework. The Go static
archive (
libwallet.xcframework) is linked into that framework viavendored_frameworks. The bridge.mis compiled into the same framework. Every libwallet entry point — bridge and underlying Go symbols — lives in libwallet.framework's export trie, in exactly one place.dlsym(RTLD_DEFAULT, libwallet_init)walks all loaded images and finds it there. - Kept
s.pod_target_xcconfig['GCC_SYMBOLS_PRIVATE_EXTERN'] = 'NO'and per-function__attribute__((visibility("default")))on each bridge function as belt-and-suspenders, so a future podspec/linker tweak doesn't silently re-hide the symbols.
Net for hosts: no API changes; a
pod installrebuild against the new framework eliminates the duplicate runtime. Tibane's recurring crash will stop without any host-side edits. - Removed
0.4.43 #
- Fixed: iOS TestFlight crash from two Go runtimes in the same
process. When the host app's Podfile declared
use_frameworks!(Flutter's default for Swift-using projects), CocoaPods wrapped the libwallet pod into a dynamiclibwallet.frameworkdylib — while the same static archive also got linked into the host Runner target via-force_loadfroms.user_target_xcconfig. Both copies initialised their own Go runtime; the runtimes fought for signal-handler ownership and SIGABRT'd insideruntime.raise_trampoline.abi0(typically while another goroutine was mid-getaddrinfofor a chain RPC). Reported as a TestFlight crash onnet.tibane.tibaneapp(build 54). The podspec now declaress.static_framework = true, which forces CocoaPods to treat the pod as static regardless ofuse_frameworks!— the Go runtime ends up exactly once, inside Runner.
0.4.42 #
-
OKX adapter: migrated from V5 to V6 of OKX's DEX aggregator API. OKX deprecated V5 on the day 0.4.41 shipped, so every
Crypto/Okx:quoteround-trip was returning HTTP 50050 ("V5 API is being deprecated") and surfacing to the host asOKX: provider_unavailable. The platform'sCrypto/Okx:*proxy was rewritten against/api/v6/dex/aggregator/...; libwallet caught up here:- request param
chainId→chainIndexon/quote,/swap,/approveTransaction; /quoteno longer accepts a slippage parameter (V6 applies slippage only at/swap);/swapparamslippage(fraction, e.g.0.005) →slippagePercent(percent, e.g.0.5);- response field
chainId→chainIndexon every entry; - response field
priceImpactPercentage→priceImpactPercent; - response field
slippageontx→slippagePercent, plus newmaxSpendAmount; dexRouterListflattened: the V5 nestedrouter/routerPercent/subRouterList/dexProtocol[]tree collapsed to a flat list of per-hop entries, each carrying a singledexProtocolobject plus its hop's from/to token decoration. The route builder walks entries in order and emits oneRouteHopper entry.
No host-side changes. Quote / Execute / availability surfaces and the
okx_solana/okx_evmprovider names are unchanged. - request param
-
Better diagnostics on empty OKX responses. When OKX has no route for a pair, the proxy returns a
[{}]envelope and the entry decodes into the zero value. Pre-0.4.42 this surfaced asprovider_unavailable: okx: parse toTokenAmount; now it surfaces asno_liquidity: okx: no route for <amount> <from> → <to> on chain <chainIndex>— both code and message reflect what actually happened, so the host UI can downgrade to an advisory instead of erroring.
0.4.41 #
-
Fixed: reshare ceremony was mis-routed when the host had a stale/defaulted
wallet.curve. Two layers conspired:Wallet.fromJsonsilently defaulted a missingCurveto'secp256k1'. An ed25519 wallet whose serialised payload dropped or blanked the field appeared as secp256k1 to the host, which then chose the wrong protocol for the next ceremony. The factory now keeps an empty string instead — the host can pick a default with full context if it needs one.RemoteKey:reshare(Go endpoint +RemoteKeyApi.reshareDart wrapper) accepted acurveparameter and forwarded it toCrypto/WalletSign:reshare. The backend already records the remote key's curve at issue time, so the right answer is to let the backend pick — passing curve back in just opened the foot-gun. The argument is removed on both sides; hosts that used to callremoteKeys.reshare(key: …, curve: wallet.curve)should drop thecurve:argument (breaking change for that one endpoint).
Net effect: a wallet's curve/protocol now flow from where they're authoritative — the Wallet row server-side and the remote key record on Crypto/WalletSign — instead of being re-derived from a host-side cached value that might have lost the field along the way.
0.4.40 #
-
Fixed:
client.accounts.list(wallet: id)now actually scopes to the wallet. The Go-sidewltintf.ListHelperaccepted asearchKeylist naming which request params should drive theWHEREclause, but never read those params —psql.Fetchwas always called withwhere=nil. Result: every list endpoint that passed asearchKey(Account byWallet, Network byTestNet, Token byName/Symbol/Address/Type, Contact byName/etc., WalletKey, Crash) silently returned every row in the table regardless of the filter. Apps either lived with the noise or workaround-filtered client-side. Wired up here soAccount?Wallet=…returns only that wallet's accounts; tibaneapp PR #6 (client-side defensive filter) is no longer needed. -
Swap aggregator: switched to OKX DEX (under
Crypto/Okx:*). Licensing forced the move off Jupiter / dFlow (Solana) and 1inch (EVM); every chain libwallet supported swaps on now routes through the platform's server-side OKX proxy. The legacy adapters stay in the tree behind unregisteredRegisterProviderlines — the flip-back is two//removals inwltswap/init.goplus restoringwltswap/provider.go's per-chain ordering. EVM swap coverage expanded from 11 chains (1inch's list) to 22 (OKX's), notably picking up Cronos / Manta / X Layer / World Chain / Polygon zkEVM / Mantle / Klaytn / Mode / Celo / Blast / Scroll. The 50bps platform fee is now applied server-side (the libwallet binary no longer carries an API key) and echoed back in the response so the UI's "Platform fee" line still renders. No host changes required —Swap:availability/Swap:quote/Swap:executesurface is unchanged;Quote.Providerbecomes"okx_solana"/"okx_evm"andQuote.ProviderLabelbecomes"OKX".
0.4.39 #
- ethrpc v0.2.11 → v0.2.12. Upstream landed the
DoNamedCtxfix that 0.4.38'sNetwork.DoRPCNamedCtxwas working around (the variadic-packing bug that serialised named-params as"params":[{...}]instead of"params":{...}).Network.DoRPCNamedCtxreverts to its natural one-liner (r.DoNamedCtx(ctx, method, args)); the workaround is no longer needed. No behaviour change for callers — SolanagetAssetsByOwnerand other DAS calls keep working.
0.4.38 #
-
Added: Solana on-chain transaction-history backfill. The
Account:setCurrent/Network:setCurrentside-effect that already populated the DartTransactiontable for EVM accounts (viamodchain_historyByAddress/ots_searchTransactionsAfter) now also covers Solana, viagetSignaturesForAddress+getTransaction(jsonParsed). Hosts that already wired upclient.transactions.list(from: …)/LibwalletClient.txHistoryUpdatesfor EVM get the wallet's Solana history with no additional changes — the rows just land in the same table on the sametx:history_updatedevent. Each signature condenses into one row: native SOL transfers and SPL token transfers are detected by walking the parsed instruction list (system-programtransfer, spl-token / spl-token-2022transfer/transferChecked); anything else (DEX swaps, NFT mints, multi-step program calls) gets surfaced astype: "other"with the account's net SOL balance delta as the amount. -
Added:
Network.txHistoryProvider. New JSON field on the Network model (and aNetwork.txHistoryProvider()Go method feeding it) so hosts can tell "indexer hasn't returned anything yet" from "no indexer implemented on this chain". Stable values:"modchain"(EVM, primary),"otterscan"(EVM, fallback),"signatures"(Solana),""(no provider — Bitcoin family, custom chains). Hosts that want to hide the transactions tab on chains without indexer support can checknetwork.txHistoryProvider.isEmpty. -
Added:
Transaction:backfillendpoint /client.transactions.backfill(). Forces a tx-history sweep for the current (account, network) without jugglingAccount:setCurrent/Network:setCurrentto retrigger the side-effect path. Useful for pull-to-refresh in the transactions tab, post-import refreshes, and retry-after-blip flows. Idempotent — concurrent calls are deduped via the in-flight map the existing backfill already uses. Returns{started, provider, reason?}so the caller can render an immediate "Refreshing…" state and know whether to wait for thetx:history_updatedevent or fall through (no provider). -
Fixed: Solana
getAssetsByOwnerand other DAS calls.wltnet.Network.DoRPCNamedCtxwas routing throughethrpc.RPC.DoNamedCtx, which has a variadic-packing bug that serialised the named-params map as a one-element positional array ("params": [{ownerAddress: "…"}]) instead of a true named object ("params": {ownerAddress: "…"}). Helius returnsjsonrpc error -32602: invalid type: map, expected a string at line 1 column 1for the wrong shape. Now bypasses the wrapper by callingr.SendCtx(ctx, ethrpc.NewRequestMap(method, args))directly. Unblocks Solana token auto-discovery on every Helius endpoint. Upstream fix has been dispatched to the ethrpc maintainer; the libwallet-side workaround stays in place regardless.
0.4.37 #
- Docs: cross-device-import recovery flow. The
doc/device_share.md"Restoring a backup on a fresh device — auto-rotation" section now explicitly walks through theRemoteKey:reshare→RemoteKey:validate→Wallet:resharesequence with the load-bearing detail that was tripping every host wiring this for the first time: the wallet's storedWalletKey.keyis a server-side WalletSign session that'sdoneonce the wallet was created, and callingWallet:reshareagainst it returnsinvalid status for wallet sign session: done. The fix is to runRemoteKey:reshare+:validatefirst to mint a fresh session, then pass the resulting id on both old-committee and new-committeeRemoteKeyKeyDescriptions.RemoteKeyApi.reshare's docstring now spells out that itskeyparameter is thecrws-…:crwsv-…resource id (WalletKey.key), NOT thewkey-…uuid (WalletKey.id) — passing the wrong one returned a vague "Invalid key" from the server.RemoteKeyValidation.remoteKey's docstring carries the reshare-with-new-id example inline so the IDE hover surfaces it. No runtime change; documentation only.
0.4.36 #
-
Added: device-to-device wallet transfer. New endpoints
Wallet:exportToDevice(old device, paints a QR),Wallet:exportToDevice:confirm/:cancel(decision callback after the user confirms on the source device), andWallet:importFromDevice(new device, takes the scanned code). Single Spot round trip per transfer, 5-minute single-use pairing token, AES-256-GCM payload sealed with a key derived from the QR-borne token, includes the wallet JSON + device share private keys in one shot so the destination can sign immediately without a reshare. Dart surface shipsDeviceTransferSession,DeviceShareEntry, andDeviceTransferImportResultmodels. Full implementor guide indoc/device_share.mdunder the "Device-to-device transfer" section. -
Security: tss-lib v2.2.8 → v2.2.9. Big upstream hardening release covering both DKLs23 and FROST. Highlights: FROST round-2 shares are now wire-encrypted (X25519 + HKDF + ChaCha20-Poly1305) instead of relying on broker-layer encryption alone; FROST nonce generation follows RFC 9591 §4.1 verbatim; FROST DKG round 1 has identity + sid binding hardened, and resharing carries a per-dealer Schnorr PoK on
wiwith deepXiscrub viaKey.Zeroize; DKLs23 signing gets a broker-driven Mul-then-check party (SigningCheckedParty) with identifiable abort, plus an echo-broadcast defense against K_i equivocation, an echo-coverage fix that closes the omitted-dealer hole, and sid-binding hardening that mixesTinto keygen/refresh sessions; format-magic-byte + hash validation ondklstss.ImportKey. All-additive on the libwallet side — every existing sign / reshare / promote / device-transfer test passes against 2.2.9 without code changes. -
CI: iOS simulator integration tests no longer flake. The "Application … is unknown to FrontBoard" / "Simulator device failed to launch" failures that hit ~20% of runs since the macos-latest image moved to M-series were two underlying bugs: the dart-test workflow's device-type picker landed on iPhone XS Max under iOS 18.x (which trips
FBOpenApplicationServiceErrorDomainat launch), and the "fresh sim per job" workaround didn't reset the shared CoreSimulator service's stale state. Picker now sorts by parsed iPhone model number + variant rank (base < Plus < Pro < Pro Max) so it always picks the most recent iPhone the runtime supports, and the workflow runssimctl shutdown all+simctl erase all+killall Simulatorto reset the service cleanly. Also pinnedobjective_cto 9.2.4 in the test_app (pre-arm64e-regression)- dropped the test's
path_providerdependency in favour ofDirectory.systemTempso the objective_c framework never enters the iOS build closure.
- dropped the test's
0.4.35 #
-
Modernized: TSS protocols. All new wallets are now created with modern threshold protocols — DKLs23 for secp256k1 (Bitcoin / Ethereum / …) and FROST per RFC 9591 for ed25519 (Solana / Sui / …). The legacy GG18 / eddsatss keygen paths are no longer reachable; existing wallets created under those protocols keep working (sign + reshare + promote all detect the protocol and use the matching primitive).
Wallet.protocolsurfaces"dkls23"or"frost"for new wallets; existing rows with emptyprotocolare interpreted per their curve. Modern resharing, modern promote, and the ClawdWallet keygen handshake all run through the new protocols transparently — no Dart-side API change beyond the optionalChainMigration.curvebelow. -
Added:
ChainMigration.curvefor the modernWallet:promoteMnemonicfan-out. The Go side now dispatches each chain on its curve —"secp256k1"lands on a DKLs23 wallet (Bitcoin, Ethereum, …) and"ed25519"lands on a FROST wallet (Solana, Sui, …) — so one BIP39 mnemonic can produce wallets on both curves in the same call. The Dart model picks the field up automatically when you build viaChainMigration.fromProbeRow(row); manual constructions should passcurve: 'secp256k1'orcurve: 'ed25519'explicitly. Empty defaults to"secp256k1"for backwards compatibility with pre-modern callers. -
Added:
SwapQuote.status+SwapQuote.statusMessage+SwapQuote.isExecutable.SwapApi.maxSpendablepreviously errored out on two soft-failure paths — the wallet's spendable balance can't cover network fee + rent, or the provider returned no route at the resolved amount (Jupiter's "Failed to get quotes" on dust-sized SOL trades). Hosts that built source-list UIs by hiding any asset that errored ended up hiding assets the user actually held: e.g., swap most of your SOL → USDC, end up with 0.0061 SOL, and SOL disappears from the "from" picker even though the wallet still owns it.maxSpendablenow returns aSwapQuotewithstatus == "balance_too_small"orstatus == "no_route"and a human-readablestatusMessageinstead.isExecutablereturns false for these — show the row with the message and skipSwapApi.executeuntil the conditions change.SwapApi.quote/SwapApi.quotesstill error on no-route (the user is asking for a specific trade and a silent no-route would be misleading).
0.4.33 #
- Changed: transient phplatform errors retry transparently in
every libwallet →
Crypto/WalletSign:*call (remoteNew,remoteVerify,remoteSign,remoteReshare,walletkey's key-list + setGeneratedKey, etc.). HTTP 5xx — including the "There was a database error" blip the integration tests have been tripping on — retries up to 3 attempts with 500ms / 1s backoff. 4xx errors (auth, validation, not-found) pass through immediately. Context cancellation aborts the loop. Affects runtime callers too, not just tests — a brief backend hiccup no longer surfaces as a user-visible error.
0.4.32 #
-
Fixed: Jupiter "Failed to get quotes" on tiny Solana swaps. At small input amounts (typically under ~0.01 SOL), Jupiter's RFQ market makers (JupiterZ) will gladly fill the trade — they even subsidize the gas — but stacking our 50 bps platform fee on top makes the route stop penciling and Jupiter falls back to aggregator routes that can't handle the size. The Jupiter adapter now retries once without the
referralFee/referralAccountparams on the specific "Failed to get quotes" no-route response. The retry's success path returns a Quote withfeeBps: 0andreferralFee: 0so the host's approval sheet correctly shows "no platform fee on this swap". One extra RTT only when the first attempt couldn't route. -
Added:
Asset.isNative+Asset.tokenAddresson the Dart model (plus matchingIsNative()/TokenAddress()on Go'swltasset.Asset). Useasset.isNativeto branch native-vs-token instead of inventing a matcher onAsset.type/Asset.symbol/Asset.name. libwallet emitsType: "fungible"for both native and tokens —Asset.key's.NATIVEsuffix is the only invariant native-vs-token signal, and these getters wrap it as the canonical predicate. -
Doc clarification on
Asset.type— the field's old example list ("native","erc20","spl-token") was aspirational; the runtime value is always"fungible"for balance entries. Updated to reflect reality and point readers atisNative.
0.4.31 #
- Added:
client.swap.quotes(...)— quote the same swap across every available provider for the chain in parallel and get oneQuoteAttemptper provider back. Successful attempts carry aSwapQuote(with its ownquoteIdready forswap.execute(...)); failed attempts carry a typedSwapErrorso the picker UI can render"Jupiter Ultra: Failed to get quotes"next to"dFlow: 0.00748 SOL → 1.49 USDC". The user picks; libwallet never silently switches between providers. - Changed:
swap.quote(...)no longer silently falls back from Jupiter to dFlow on Solana. If the primary provider errors, the host receives that error directly. Hosts that want comparison should callswap.quotes(...)instead; hosts that want the old "best-effort single quote" behaviour can callswap.quote(provider: 'jupiter_ultra')thenswap.quote(provider: 'dflow')themselves on failure. - Changed: Jupiter
HTTP 400 "Failed to get quotes"is now classified asno_liquidityinstead ofprovider_bad_request. This is semantically correct (it's a routing failure, not a malformed request) and matches Jupiter'sHTTP 200"empty transaction" path which already maps tono_liquidity. Hosts that branch on the error code now see one code for both no-route surfaces.
0.4.30 #
-
Fixed:
Swap:quote/Swap:maxSpendablerejectedAsset.Key-shaped token addresses. Hosts that pipedAsset.Key(the"solana.mainnet.<mint>"/"evm.1.<contract>"shape returned byAsset:list) intotokenIn.address/tokenOut.addressgot an HTTP 400 from Jupiter ("Invalid outputMint") or 1inch — libwallet was passing the prefixed string straight through to the aggregator, which only accepts bare mints / contracts. The swap entry points now strip the<type>.<chainId>.prefix; bare addresses pass through unchanged. The"NATIVE"sentinel works in both forms. -
Added:
MessageSignRequest.verifyingContractLabel— typed-data approval sheets can now show"Uniswap V3: SwapRouter02"(or"OpenSea: Seaport 1.6","Aave V3: Pool", …) above the raw0x…for EIP-712 messages whose domainverifyingContractmatches a known address in libwallet's contract registry. Empty for unknown contracts — host falls back to the raw address. DomainchainIdis normalised (JSON number, decimal string, hex string,0x-prefixed) so dApp idiosyncrasies don't matter. -
Added:
LibwalletClient.contracts.lookup(chainKey:, address:)— generic registry lookup for hosts that want to label addresses at other render sites (effect rows, watch_asset, explorer-link rows). Returns aContractLabel(address,label,kind,project) ornullfor unknown addresses. Same registry that backs the typed-data field. -
Initial registry coverage: Uniswap V2/V3 routers, Universal Router, Permit2; OpenSea Seaport 1.5 + 1.6; Aave V3 Pool; Compound V3 Comet (cUSDCv3, cWETHv3); Balancer V2 Vault; Curve 3pool. Chains: Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche. Permit2 + Seaport are deterministic-address contracts — they resolve correctly across every chain that's in the registry without per-chain copies of the entry.
0.4.29 #
-
Added:
Network.addressUrl(address)+Network.transactionUrl(hash)helpers on the Dart model. Pure-sync, return a fully-composed block-explorer URL with the right per-chain shape applied —?cluster=<id>suffix for non-mainnet Solana,/address/vs/tx/paths,""when no explorer is resolvable. Hosts no longer need to fork the per-chain composition logic to render "tap address → open in explorer" affordances on signing-critical rows. -
Added:
Network.resolvedBlockExplorer— populated by libwallet with the bare base URL after resolving the"auto"sentinel against the chain registry. Backs the URL helpers; also useful to hosts that compose other URL shapes (/token/,/block/). Empty when no canonical explorer is known (custom chains with nothing configured) — hosts should hide the affordance in that case. -
Added:
Amount.max(decimals)sentinel — resolved at build time. PassAmount.max(...)asTransaction.amountand libwallet's build path (Transaction.Validate, called bysignAndSend) substitutesbalance - actual-feeat the latest possible moment using the resolved gas estimate of the actual tx contents (recipient, calldata). Eliminates thetransactions.maxSendable→transactions.signAndSendrace where the gas price or balance can drift between the two calls — the sameValidatepass that picks the gas number also picks the amount. Native EVM only in this release; Bitcoin uses the existing UTXO-pinning path (bitcoinUtxos+bitcoinFeeRatefromMaxSendable); Solana MAX resolution is a follow-up. Wire form:{"v": "MAX", "e": <decimals>}(or bare"MAX"string). -
Added:
Transaction:maxSendableacceptsdata(EVM). When set, the EVM path runseth_estimateGaswith the calldata to get the contract's actual gas cost instead of the 21000 EOA-transfer default — the right number for previews of native swaps where the default reserved ~10x too little. The placeholder value passed to estimateGas (some swap routers revert withvalue: 0) isbalance/2, computed internally; the caller never has to specify it. Plain transfers (nodata) keep the 21000 fast-path with no extra RTT. PreferAmount.maxin the actual tx over computing max upfront — same answer with no drift-window between maxSendable and signAndSend.
0.4.28 #
- Added: Solana mainnet token-list auto-discovery on first asset
list. The first time
Asset:listruns for a (mainnet Solana, owner) pair, libwallet calls Helius DASgetAssetsByOwnerwithshowFungible: trueand seeds the user'sTokentable with the fungibles the address actually holds — name + symbol + decimals in one round trip, no manualToken:createper token. A conservative spam filter rejects entries with empty symbol, symbol > 12 chars, name > 64 chars (typical link-stuffing pattern), or non-positive decimals. SubsequentAsset:listcalls skip the discovery (config flag gated per network/owner) and use the cached Token rows for name/symbol enrichment. Devnet/testnet are excluded — DAS coverage there is patchy. - Changed: Solana SPL balances enrich names from the Token table.
SolanaTokenBalancespreviously returnedEPjFW.../EPjFW-style truncated mint fallback whenever the embedded curated registry didn't recognise a mint. Now the result is overlaid with the user's Token-table metadata (populated by the auto-discovery, byToken:create, or by post-swap registration). So newly- acquired tokens — via swap, airdrop, or transfer — surface with their proper name without a manual lookup.
0.4.27 #
- Fixed: balances stayed stale after a successful swap.
Swap:executepaths through Jupiter / dFlow / 1inch broadcast via the aggregator's own HTTP/RPC route and never calledwltintf.NotifyTxBroadcast, so the balance poller didn't get the nudge it does after every other broadcast. Both the spent (TokenIn) and earned (TokenOut) balances now refresh within ~a second of the swap landing instead of waiting up to 60 s for the next polling tick. - Added: previously-unknown swap outputs auto-register in the
user's token list. When
Swap:executelands on aTokenOutthe user has never held before, libwallet inserts aTokenrow with the metadata from the swap quote (symbol, decimals) so the new asset surfaces inToken:listwith its name instead of "Unknown". Native outputs (SOL / ETH) are skipped. Failure to add the token is non-fatal — logged but doesn't fail the swap. - Added:
wlttoken.EnsureToken(env, network, address, symbol, name, decimals, type)— Go-side helper that idempotently ensures a Token row exists for(network, address). Currently called by the swap path; available for any future caller that wants to surface a previously-unknown asset without an explicitToken:createround trip.
0.4.26 #
- Fixed: Solana devnet / testnet transaction-explorer links resolved
to "Transaction not found".
Network.TransactionUrlwas appending/tx/<hash>to the explorer base without?cluster=, so links to explorer.solana.com / solscan.io / solana.fm always queried the default cluster (mainnet). Non-mainnetChainIds now get?cluster=<id>suffixed; mainnet stays bare.
0.4.25 #
- Added:
LibwalletClient.wallets.createAgentWallet— one high-level call that opens the server-sideCrypto/WalletSign:newAgentsession (filling inmobile_spot_idfrom libwallet's own Spot client) and drives the 3-party EdDSA keygen ceremony to completion. Returns aCreateAgentWalletResultwith the new wallet id + Solana address. The host passes its existingAtOnlinesession in asapi:so libwallet doesn't have to manage bearer tokens. Replaces the previous "do four things in a row from the screen" flow. - Removed:
info.spotId()/Info:spotIdendpoint. Hosts no longer need to read the local Spot TargetId —createAgentWalletfills it in internally. The shape of the previous flow (host reads spot id, app posts newAgent, app calls initiateKeygen) collapsed to a single call. - Added: dependency on
atonline_api^0.5.0 (passed in by the host; libwallet does not store or refresh tokens).
0.4.24 #
- Added:
LibwalletClient.clawdWallet.pair(url)— verifies a ClawdWallet pairing URL (tibane://pair?agent=...&token=...) by handshaking with the agent over Spot and returns the verifiedAgentIdentity(agentSpotId,suggestedName,agentVersion,capabilities). Used as the deep-link replacement for the manual "paste agent_spot_id" field on the Create-agent-wallet flow. The app hands a URL string in; libwallet drives the entire Spot handshake. Failures throw typedPairingExceptionsubclasses —PairingURLMalformedException,PairingAgentUnreachableException(15s timeout),PairingTokenInvalidException,PairingTokenExpiredException,PairingTokenConsumedException,PairingBadRequestException, andPairingIdentityMismatchException(security: response'sagent_spot_id≠ URLagentparam). Wire contract:tibaneapp/docs/clawdwallet-pairing.md.
0.4.23 #
- Fixed:
libwalletPackageVersionwas stuck at0.4.20in the 0.4.21 and 0.4.22 publishes. The bump for those releases only touchedpubspec.yaml; the constant inlib/src/version.dartwas never updated, soLibwalletClient.initializewould trip its stale-binary mismatch check at runtime against the bundled native library. Republished with both files in sync. Usedart run tools/bump_version.dart <version>(not a hand edit) to keep them locked.
0.4.22 #
- Added:
info.spotId()returns the local Spot TargetId (k.<base64url>). Hosts pass it intoCrypto/WalletSign:newAgentasmobile_spot_idso the policy module includes the mobile in the canonical peers list for the keygen ceremony. - Changed:
peers[].idwire tag (wasspot_id) onWallet:initiateKeygenandWallet:joinSign. Aligns with tss-lib'sMessageWrapper_PartyIDprotobuf JSON tag so wdrone can unmarshal peers directly intotss.SortedPartyIDs.
0.4.21 #
- Added:
Wallet:initiateKeygen+Wallet:joinSignfor the ClawdWallet skill-gated agent-wallet protocol.initiateKeygenis the keygen leader — it sendswalletsign/<sid>/initto each peer in the committee with the canonical InitPayload, then runs the local EdDSA keygen as a share holder.joinSignis the joiner side of the threshold-sign ceremony (mobile is not in the sign committee for ClawdWallet; the agent leads). PartyID.Key is taken from the peer-suppliedkeyfield, matching wdrone's existing convention.
0.4.20 #
- Fixed:
Account:signAndSendTransaction(Solana) failed with "failed to decode account address" whenever the wallet UI was on a non-Solana network.acct.Addressis the network-specific display string and becomes"N/A"off Solana, so basing the slot lookup on it broke any host that hadn't callednetworks.setCurrentfor a Solana cluster first. Read the raw 32-byte pubkey from the stableacct.Pubkeyfield instead.
0.4.19 #
- Fixed: Solana
Account:signAndSendTransactionposted to whatever the wallet UI was currently showing — typically an EVM RPC, which then returnedThe method sendTransaction does not exist. The send path now picks an actual Solana network: ifCurrentNetworkis already a Solana cluster (so a user testing on devnet keeps that selection) it's preserved, otherwise the default Solana mainnet entry seeded byMakeDefaultNetworksis used.
0.4.18 #
- Fixed: Solana sponsored transactions silently lost the
owner's signature.
Account:signTransaction/signAndSendTransactionwrote the signature unconditionally into signature slot 0. For sponsored txs the relay (fee payer) holds slot 0 and the wallet owner is at slot 1+ — so the owner's signature clobbered the relay's slot, the relay re-signed slot 0 with its own key (overwriting the owner's), and the tx hit Solana with slot 1 still zeroed and got rejected with "missing signature for account 1". libwallet now walks the message's account-keys array to find which slot matches the signer's pubkey and writes there. Handles both legacy and v0 versioned messages. Pubkeys that aren't required signers for the transaction are rejected.
0.4.17 #
- Fixed: the FFI transport only worked on macOS. The
default library loader unconditionally opened
liblibwallet.dylib, so iOS, Android, Linux, and Windows builds either failed to find a library or loaded a stale one. Now picks the right thing per platform:- iOS uses
DynamicLibrary.process()(bridge symbols are statically linked into the host binary viaLibwalletBridge.m; there is no.dylibto dlopen); - Android constructs the versioned per-ABI filename
liblibwallet-android-<abi>-v<version>.sothathook/build.dartactually ships; - macOS / Linux / Windows use the conventional
liblibwallet.{dylib,so}/libwallet.dll. Unknown platforms or ABIs raiseUnsupportedErrorinstead of silently failing.
- iOS uses
0.4.16 #
-
Fixed: bitcoin "send max" intermittently failed with insufficient-funds at signAndSend even when the
maxSendablevalue was correct. Two races contributed:- Math mismatch —
maxSendablebudgeted vsize for 1 output (max-send → no change), butsignAndSend's coin-selection guard assumes 2 outputs (recipient + change). For an exact-max send, build's fee was strictly larger andchangewent negative. - Fee-rate drift — both calls independently asked
estimatesmartfee, and a different reading between them would push the math out of agreement.
Fix bundles two complementary parts:
MaxSendableResultnow exposesbitcoinUtxos+bitcoinFeeRatecarrying the exact selection + sat/vB it computed against.UnsignedTransactionacceptsbitcoinFeeRate(utxosalready existed). When set, the build path skips theestimatesmartfeeRPC and uses the pinned values.- New convenience
UnsignedTransaction.maxSend(m, to: ...)threads both fields automatically — recommended path for any "Send Max" button. maxSendablenow budgets at 2-output vsize so the coin-selection check passes; build emits a 1-output tx and the small ~31 vbyte × rate overestimate goes to the miner. No special-casing needed elsewhere.
- Math mismatch —
-
New:
priorityfield onTransaction:maxSendable+ the existingpriorityLevelonTransactionnow drives bitcoin fee selection. Map:"low"→estimatesmartfeetarget 144 blocks (cheap),""/"medium"→ 6 (default),"high"→ 2 (fast). CallmaxSendabletwice with different priorities to power a "cheap vs fast" comparison UI; pass the chosen result throughUnsignedTransaction.maxSendand the actual send uses the same fee budget. EVM and Solana semantics unchanged (Solana already usedpriorityLevelfor ComputeBudget pricing).
0.4.15 #
- Fixed:
transactions.maxSendablereturned 0 right after a send. The bitcoin path was still scanning onlym/0whilesignAndSendhad moved to both chains in 0.4.10 — change UTXOs landed onm/1and were invisible to maxSendable. Switched to the same combined fetchsignAndSenduses. - Performance: collapsed two bitcoin UTXO fetchers into one.
Coin selection, max-sendable, listUTXOs, sign-raw, and the
simulate dry-run all now read from a single
modchain_assetscall. Previously some paths additionally hitmodchain_lookupTxoBIP32per chain just to discover the next change index — which becomes meaningful overhead on wallets with hundreds of historical outputs. Next-change- index is now derived from the same unspent set'sm/1entries (seenextChangeIndexfor the fully-spent-history caveat).
0.4.14 #
- Fixed: every bitcoin-family
signAndSendfailed with-25 bad-txns-inputs-missingorspentsince BTC support shipped on Apr 14.parseTxoRefreversed the txid bytes before storing them inBtcTxInput.TXID, but outscript also reverses TXID at marshal time (its convention is "TXID stored in displayable / big-endian form, wire reversal happens at serialize"). The double-reverse meant every broadcast tx referenced a bogus txid that was the modchain-reported id with its bytes flipped — and the litecoin / bitcoin / dogecoin / monacoin / bitcoin- cash node correctly rejected it because no such UTXO exists. All previousbad-txns-inputs-missingorspentreports attributed to "modchain reindex lag" or "stale UTXO state" were really this bug. The 0.4.12 in-memory tracker and 0.4.12 spent-filter remain useful but were never the actual fix. - Same byte-order bug fixed in
SignRawBitcoinTx(mpurse / Counterparty path): wire-decoded TXIDs were reversed before the modchain ownership lookup, making every input report as "not owned by this account". Removed the reverse. - Regression test pins the byte-order convention so a future
refactor that flips
parseTxoRefback to the old behaviour fires loudly at test time, not in production.
0.4.13 #
- Diagnostics: bitcoin broadcast errors now carry the inputs +
raw tx hex. When
sendrawtransactionrejects a tx (e.g.bad-txns-inputs-missingorspent) the error message now includes the full(txid:vout)list libwallet selected and the hex-encoded transaction. Without these, reproducing the failure required guessing which UTXO modchain reported but the bitcoin node disagreed about. Format:sendrawtransaction: <upstream> (inputs=a:0,b:1 rawhex=...).
0.4.12 #
- Fixed: bitcoin-family
signAndSenderrored with-25 bad-txns-inputs-missingorspentin two distinct cases:- modchain returning a
txo[]entry with a non-nullspentfield. Fetch path now drops anything with a populatedspentvalue before coin selection sees it. - Back-to-back sends. Send #1 spends UTXO A and creates change UTXO B; send #2 issued seconds later picked A again because modchain hadn't reindexed past send #1's mempool tx, and the broadcast failed because the bitcoin node knew A was already spent.
- modchain returning a
- New: in-memory UTXO tracker that bridges the modchain-reindex gap. After every successful broadcast it records the inputs that were just spent + the change UTXO that was just created (with the broadcast tx hash filled in). The next coin-selection call layers this on top of modchain's response — drops the spent ones, injects the pending change. TTL: 1 hour. When modchain catches up the matching pending entry auto-prunes (the modchain ground truth replaces our local copy). Process-local state — survives within one libwallet session, lost on restart (by which time modchain is current anyway). Lets a user fire several bitcoin sends in a row without waiting for confirmations between them.
0.4.11 #
- *Fixed: bitcoin-family
signAndSenderrored with "pubkey of type ecdsa.PublicKey does not support pubkey:comp export" on the first spend that touched a non-p2wpkhUTXO (legacyp2pkhor wrapped-segwitp2sh:p2wpkh). The TSS input signer'sPublic()returned*ecdsa.PublicKey, but outscript's witness builder requires a type that implementsSerializeCompressed(). Returns*secp256k1.PublicKeydirectly now (which has the method natively). Latent since forever; surfaced by 0.4.10's m/0+m/1 coin selection because before then we only ever spent receive-chain p2wpkh outputs.
0.4.10 #
- Fixed: bitcoin-family
signAndSendcould only spend receive- chain UTXOs. Coin selection used to fetch onlym/0— anything that landed on a change address (m/1/i) showed up in the balance but couldn't be spent until it happened to land on a future receive address.buildBitcoinTxnow scans both chains via a singlemodchain_assetscall and signs each input with the right key derived from its own path (m/0/*vsm/1/*). - Fixed: bitcoin-family fee under-payment with mixed-shape
inputs. Per-input vsize is now read from the actual script
type (
p2wpkh≈ 68 vb,p2sh:p2wpkh≈ 91,p2pkh≈ 148) instead of assuming every input isp2wpkh. A wallet that received funds at a legacy address mixed with segwit no longer broadcasts an under-paid tx that stalls in the mempool. account.findAccountfallback by network-derived address. Frontends often hand back the displayedAccount.address(which is the current network's derived form, e.g.ltc1...) asfrom, but the DB column still holds the creation-time address.FindAccountnow derives each candidate's address for the current network and matches — fixesLibwalletException(404): file does not existonTransaction:maxSendable(and any otherfrom-resolving endpoint) when called from a non-EVM chain.- Fixed: bitcoin-family
Asset:list(cont.) — reverted the 0.4.9 lookupTxoBIP32 sum back tomodchain_assetsnow that the backend merges receive + change correctly. Single RPC, same source of truth as the rest of the bitcoin path. txo.pathaccepted alongside legacyi/branch. Newer modchain emitspath: "m/0/0"only; the wallet now reads the trailing segment for the BIP32 child index and falls back to the legacyifield when present.- New:
accounts.listUTXOs(id, network: ...)— returns every spendable UTXO the bitcoin-family account holds across receive + change, ordered largest amount first. Each entry carriestxo,path,amount,script,address, andheight. Pair with the newutxosfield onUnsignedTransactionto power a manual coin-selection picker. - New:
UnsignedTransaction.utxos: List<String>?— when set on abitcoin_transfer, libwallet skips greedy auto-selection and uses exactly the supplied"<txid>:<vout>"entries (each verified to be owned). Empty / null preserves the auto-selection behaviour. transactions.simulate(...)for bitcoin: dry-run preview. When the tx hasn't been built yet (noraw), the simulator runs the same coin-selection + fee mathsignAndSendwould, stops short of signing, and returns the planned shape: inputs with resolvedamount+address+path, recipient + change outputs with addresses, fee in sats, and the newbitcoinChangeandbitcoinVSizefields. Honours the manualutxosselection so a manually-picked spend previews exactly what it'll send. Decode-from-rawpath unchanged.
0.4.9 #
-
Fixed: Bitcoin-family
Asset:listreported zero balance even when the xpub held funds.bitcoinBalancewas callingmodchain_assetswith the account xpub. That endpoint can returnbalance:0 / txo:nullfor some xpubs even when spendable UTXOs exist at the standardm/0/m/1paths (observed on Litecoin: a wallet with 0.1 LTC atm/0/0surfaced as 0 inAsset:listwhile every other libwallet bitcoin path — sign, max-sendable, next-address — saw the funds correctly because they all usemodchain_lookupTxoBIP32).Switched xpub balance queries to sum
modchain_lookupTxoBIP32(m/0)+(m/1), which is now the single source of truth for bitcoin-family balance / signing / max across the whole library. Plain-address fallback (used by view-only accounts with no xpub) keepsmodchain_assetssince that path works for single addresses. Amounts stay in satoshis viaoutscript.BtcAmount, so no float drift.
0.4.8 #
-
New:
accounts.addressFormats(id, network: ...)— returns every receive-address shape available for a Bitcoin-family account on the given chain, ordered by display preference (modern first). Use it to power a "show my address as Native SegWit / SegWit-wrapped / Legacy / …" picker, or to display every form a counterparty might use to send funds (the backend already watches every key type, so funds received at any of these forms land in the same balance).Per-chain coverage:
- bitcoin → Native SegWit (
bc1...), SegWit-wrapped (3...), Legacy (1...) - litecoin → Native SegWit (
ltc1...), SegWit-wrapped (M...), Legacy (L...) - monacoin → Native SegWit (
mona1...), Legacy (M...) - bitcoin-cash → CashAddr (
bitcoincash:...) - dogecoin → Standard (
D...)
The first entry's
isDefaultis true and matches the address shown inAccount.addressfor that chain — so a frontend switching fromAccount.addressto the picker sees the same primary address. Pinned by a Go test that asserts byte-equality betweenAddressFormats[0].addressand the canonicalbitcoinAddress()output, so the default entry can never silently drift from the rest of libwallet. - bitcoin → Native SegWit (
-
New Dart models
AddressFormatandAddressFormatsResult, exported from the top-level barrel.
0.4.7 #
- Fixed: every EVM
signAndSendwithout a priorvalidatecall errored with "invalid maxFeePerGas". The fee-population block (MaxFeePerGas / MaxPriorityFeePerGas / Nonce / Gas) lived only insideTransaction:validate, so a Dart caller that built anUnsignedTransactionand shipped it straight tosignAndSendreached signing with empty fee fields.signAndSendnow runsvalidateas its first step (idempotent — only fills empty fields). Side benefit: closes a latent bug where anerc20_transferbuilt withoutvalidatewould have signed with the recipient address astoinstead of the token contract. - Fixed:
swap.maxSpendablefor SOL → SPL handed back amounts Jupiter would reject when the user didn't already hold the output mint. Jupiter / dFlow auto-injectcreateAssociatedTokenAccountfor the destination, costing ~2,039,280 lamports of rent paid by the taker. The previous max reserved only the system-account rent, leaving the wallet too tight to cover the new ATA — Jupiter then returned HTTP 400 "Failed to get quotes". Now: when the input is native SOL and the user has no ATA for the output mint, the output ATA's rent-exempt minimum is subtracted from the resolved max (probed live viagetTokenAccountsByOwner+getMinimumBalanceForRentExemption, with a canonical fallback). Doesn't apply to non-Solana chains, native→wSOL, or when the user already holds the output mint.
0.4.6 #
- New:
swap.maxSpendable(...)— returns the sameSwapQuoteshape asswap.quote(), automatically resolved to the largesttokenInamount the account can spend.quote.amountIncarries the resolved value so the UI can render "MAX → 1.234 SOL" alongside the standard quote display. Native input reserves the network fee + (Solana) rent-exempt minimums; token input returns the full balance because gas is paid in the chain's native currency. Returnsinvalid_requestif the resolved max is zero. - New:
"MAX"sentinel onswap.quote(amountIn: ...). Same end result asswap.maxSpendable— the libwallet side resolves the max amount before issuing the upstream quote, so a Max button in a swap form can wire straight toquote()without branching on a separate code path. transactions.maxSendable()now supports tokens. Previously errored with "v1 supports native assets only" for any token asset. SPL (Solana) and ERC-20 (EVM) now returnmax == balance(fees are paid in native currency, so the full token balance is spendable). Thefeefield reports the native-currency fee a token transfer would cost so the UI can warn when the user doesn't have enough native to cover gas.
0.4.5 #
- Fixed: every Solana swap quote crashed with
404 Not Foundfrom Jupiter Ultra. Jupiter's/ultra/v1/orderendpoint accepts onlyGETwith query parameters; we'd been POSTing JSON since the swap feature shipped. Switched to GET withurl.Values;/executestays POST (the signed transaction blob doesn't fit a query string). The httptest-backed adapter test is now pinned to GET so a future regression fires loudly instead of silently 404'ing in production. - Improved: surface Jupiter routing errors verbatim. When
Jupiter returns HTTP 200 with
transaction:""(insufficient funds, no route, slippage too tight) the adapter now passes through the upstreamerrorMessageinstead of the generic "Jupiter returned an empty order".
0.4.4 #
- New:
Token:listCuratedendpoint +tokens.listCurated(network)Dart method. Returns a vetted list of well-known tokens per chain (USDT / USDC / DAI / WBTC / WETH / LINK / UNI on EVM mainnet, USDC / USDT / SOL / mSOL / JUP and ~650 other Jupiter- verified mints above $1M mcap on Solana mainnet, plus hand-curated entries upstream feeds don't carry — notablyDRtvTCzfiKGhCVREmBbZdN9sB8PHeq9KdRZ3VmFhpump("Tibane Thecat", $ChiefPussy)). Frontend use cases:- "Swap to X" dropdown without asking the user to paste a contract address.
- Map an unrecognized mint / contract in the user's balances to
its
symbol+logoURI+tags. - Pass
"<type>.<chainId>"form (same shapeAsset.networkreturns — e.g."evm.1","solana.mainnet").
- New Dart model
CuratedTokenwithchainKey,address,symbol,name,decimals,type,logoUri,coingeckoId,cmcId,tags+isStablecoin/isWrappedconvenience getters. Exported from the top-level barrel. - Data source: embedded JSON per chain (go:embed), refreshed at
release time via
go generate ./wlttoken/curated/...which pulls Uniswap's default list for EVM and Jupiter's verified feed for Solana. No runtime external fetch, no API keys. Hand-curated overlays merge on top of the generated base. - SPL balance enrichment: on Solana,
Asset:listnow readsname/symbolfrom the curated registry when the mint is well-known. USDC on Solana used to surface asSymbol="EPjFWd"/Name="EPjFWdd5..."; now surfaces asSymbol="USDC"/Name="USD Coin". Unlisted mints keep the previous truncated display. - Chains seeded on day 1: EVM 1 / 10 / 56 / 137 / 324 / 8453 / 42161 / 43114 + Solana mainnet. Gnosis (100), Fantom (250), Linea (59144) are registered with empty lists — Uniswap doesn't cover them; to be filled by an alternative feed or overlay in a follow-up.
0.4.3 #
- Fixed:
Asset:listcrashed with-32602 Invalid param: WrongSizeon Solana for any wallet whose account is secp256k1 (EVM-flavoured).Account.UpdateAddressForNetworkwas blindly base58-encoding the 33-byte compressed secp256k1 pubkey as if it were a 32-byte ed25519 point; Solana'sgetBalancethen rejected the oversized pubkey. Non-ed25519 accounts on a Solana network now resolve toAddress="N/A"(same convention EVM / Bitcoin use for ed25519 accounts) and the balance call is skipped. - Fixed:
Swap:availabilityreported every Solana wallet asunsupported_chain. The live Solana network row usesChainId="mainnet"(set bywltnet/api.go), but the availability gate only accepted Solana's internal cluster name"mainnet-beta". The Swap button was hidden on Solana as a result. Gate now matches the real stored ChainId.
0.4.2 #
- Fixed:
eth_sendTransactionapproval crashed with "failed to get env" before signing. The transaction-sign approval handler was passing*env(whose embedded context is the bare psql sqlCtx) toTransaction.SignAndSend, which expects an apirouter context so it can extract the env. Now passes the original apirouter context. This also clears the cascading "unexpected end of JSON input" the dApp side reported — that was the dApp's parser choking on the stringified upstream error. - Fixed:
personal_ecRecoverreturned RPC error -32601 ("method does not exist"). The previous default-relay path forwarded it to the chain's JSON-RPC node, butpersonal_ecRecoveris a wallet- side operation and most public nodes don't implement it. Now handled locally: applies the EIP-191 prefix, runs ECDSA recovery, returns the EIP-55 address. Accepts both{27, 28}and{0, 1}v bytes, matching MetaMask / ethers / viem tolerance. - Fixed:
wallet_switchEthereumChaincrashed loading the approval back out of psql withmath/big: cannot unmarshal "1.74…e+76" into a *big.Int.Account.IL(the BIP32 intermediate value) was emitted as a raw JSON number, lost precision through float64 in theanyroundtrip the request loader does, then failed to unmarshal.Accountnow has customMarshalJSON/UnmarshalJSONthat emit IL as a JSON string and parse the string-or-number / scientific-notation forms on the way back in.
0.4.1 #
Wallet:probeActivity— new endpoint for mnemonic-backed wallets. Walks the BIP44 standard derivation paths for every supported chain (BTC BIP44 + BIP84, LTC BIP84, MONA BIP44 + BIP84, BCH BIP44, DOGE BIP44, EVM mainnet, Solana Sollet + Phantom conventions) and probes each candidate's RPC in parallel for on-chain activity. Returns one row per candidate with the derived address, pubkey, raw balance, and ahasActivityflag. Host UI uses this to auto-select which chains to migrate; per-candidate RPC errors land onrow.errorso one upstream failure doesn't fail the whole scan.Wallet:promoteMnemonic— new endpoint. Migrates a mnemonic wallet into N fresh MPC wallets, one per chain the caller picked from the probe output. Each migration derives the mnemonic at the chain's BIP32 path (full hardened BIP32 for secp256k1 viaecckd.Derive; Sollet seed[:32] / SLIP-0010 for ed25519) and runs TSS resharing on the resulting privkey. The source mnemonic wallet is NOT modified — the caller validates each migrated wallet, then deletes the source separately. secp256k1 source only in this release; ed25519 mnemonic migration is a follow-up.- Dart models added:
ProbeActivityRow,ChainMigration.ChainMigration.fromProbeRow(row, stripAddressSuffix: true)drops the trailing/0/0address-suffix so migration lands at the BIP44 account level (m/44'/60'/0') instead of a specific leaf address — preserves the ability to derive child receive / change addresses from the new MPC wallet. - Test vectors locked in (
wltwallet/bip44_vectors_test.go,derivation_test.go): the user-supplied BTC / EVM / BTC-segwit / Solana addresses for two reference mnemonics pin the derivation math so a future change can't silently land imports on the wrong address and lose user funds. - Existing mnemonic import sign path unchanged in this release —
the mnemonic wallet still signs at the BIP32 master, so direct
signing from a mnemonic wallet won't match MetaMask / Phantom
addresses. Users hitting that: run
Wallet:probeActivitythenWallet:promoteMnemonicto get proper per-chain MPC wallets, and sign from those. The direct-sign path will be rewritten to use Account-level derivation paths in a follow-up.
0.4.0 #
-
Import existing wallets: raw private keys + BIP39 mnemonics, with promote-to-MPC. Three new endpoints on
client.wallets:-
importPrivateKey({privateKey, curve, name, keys})— accepts0x-prefixed hex, bare hex, or Bitcoin-family WIF (auto-sniffed; WIF only forsecp256k1). The imported wallet is stored as a 1-of-1 share with a newRawKeycontent type and is signable immediately — no TSS rounds, just directcrypto/ecdsa/crypto/ed25519. -
importMnemonic({mnemonic, passphrase, curve, name, keys})— auto-detects the BIP39 wordlist (English, Japanese, Korean, Spanish, Chinese Simplified / Traditional, French, Italian, Czech) and stores the decoded entropy + the detected language tag, NOT the raw mnemonic string. That lets the same backup be re-rendered in any other language for display, while keeping the seed derivation stable (BIP39's PBKDF2 is sensitive to the literal mnemonic, so the original language must drive sign-time derivation). Optional BIP39 passphrase supported. -
promote(walletId, {oldKeys, newKeys, threshold})— converts an imported 1-of-1 wallet into a normal N-of-T TSS wallet via tss-lib's resharing protocol. The master pubkey and chaincode are preserved (the wallet's address does NOT change) — only the storage of the signing key changes from "single share, full privkey" to "M shares with T-threshold reconstruction". After promote the imported share row is deleted; the wallet looks identical to a freshly-created TSS wallet. secp256k1 only in this release; ed25519 promote is a follow-up.
All three reuse the existing
KeyDescriptionencryption layer (Password,StoreKey,RemoteKey,Plain) —RawKey/Mnemonicare content types, the encryption-at-rest mechanism is orthogonal. So importing withKeys: [Password(...)]works out of the box.Both curves on day 1 for the import path:
secp256k1(EVM / Bitcoin family) +ed25519(Solana). -
0.3.32 #
- iOS: ship as
.xcframework(Apple's recommended format). The podspec'sprepare_commandnow wraps the per-SDK static archives intolibwallet.xcframeworkviaxcodebuild -create-xcframework, and the spec switches fromvendored_librariestovendored_frameworks. CocoaPods picks the right slice for the active SDK at build time, eliminating the wrong-SDK "ignoring file ... built for iOS [Simulator]" warning the previous layout emitted on every link.-force_loadis still required because the FFI entry points are dlsym'd, but it now reaches into the xcframework's source slice (which exists frompod installonward) — Xcode's link-phase input validation runs before the CocoaPods Copy XCFrameworks build phase, so referencing the build-time copy path errored with "Build input file cannot be found" even though the file would exist by link time.
0.3.31 #
- Fixed:
personal_sign/eth_signTypedData_v3/_v4returned DER, not Ethereum wire format. ecrecover, viem.verifyTypedData, ethers.verifyMessage, OpenSea, Snapshot, Permit2, MetaMask test-dapp's Recover button — every off-chain Ethereum signature verifier — would reject the output with "Invalid signature v value" or a silent address mismatch. Now produces the canonical 65-byte form R(32) || S(32) || V(1) where V ∈ {27, 28} via the newwltacct.SignEthereumDigesthelper, which post-processes the TSS signer's DER output (parse → bruteforce recovery code → repack). Same fix applied to the host-directAccount:signMessageflow withMode: "evm"/"personal_sign". EIP-155 chain-id adjustment is intentionally NOT applied — that lives in the on-chain transaction signing path; off-chain flows always use legacy v.
0.3.30 #
-
Info:versionnow exposes the release tag. Newversionfield alongsidedateTag/gitTag, populated from thev*tag the binary was built from (empty on dev / non-tagged builds). The Dart wrapper got a typedclient.info.versionInfo()returning aVersionInfo(version + gitTag + dateTag) for diagnostics; the long-brokenclient.info.version()now correctly returns the release-tag string instead of the toString'd map. -
Runtime version-mismatch detection (with release-mode rejection).
LibwalletClient.initializeasynchronously callsInfo:versionand compares it against the hardcodedlibwalletPackageVersionconstant. Result is exposed asclient.ready(aFuture<void>):- Debug/test Dart VM (
dart.vm.product=false): mismatch logs an actionable warning viadart:developerandreadycompletes normally — debug runs of the in-tree test app can iterate against a locally-built binary without ceremony. - Release Dart VM (AOT,
dart.vm.product=true):readyrejects with aStateErrorcarrying the same message. Apps shouldawait client.readyafterinitializeand surface a fatal-error UI on failure — operating with mismatched wire shapes is what causes events to arrive asUnknownPendingRequest, sign approvals to error with "keys are required", etc.
Catches the same class of bug across iOS (skipped
pod install), Android (stale build-hook cache), and macOS/Linux/Windows. - Debug/test Dart VM (
-
Fixed: build-time
-Xldflags targeted the wrong package. TheMakefileandbuild.ymlhad been passing-X main.dateTag=.../-X main.gitTag=...for the entire history of the project, but those variables live inwltbase. The-Xwas a silent no-op — every release binary shipped with emptydateTag/gitTag. Corrected to use the fully qualified package path; release binaries built on or after this commit return populated values fromInfo:version. Also propagated the ldflags to the c-shared / c-archive Dart-FFI builds, which had no version metadata at all. -
Release tooling:
dart/tools/bump_version.dart. One command (dart run tools/bump_version.dart --patch/--minor/--major/ explicitX.Y.Z) rewrites bothpubspec.yamlandlib/src/version.dart— the two files that have to move in lockstep for the runtime mismatch check to work. CI runs the script's--checkmode on every push so a release commit that only touchespubspec.yamlfails fast.
0.3.29 #
- CI publish workflow: install BOTH Flutter and Dart. v0.3.28
switched from
dart-lang/setup-darttosubosito/flutter-actionso the publish job had Flutter on PATH (needed once the package pinned a Flutter SDK lower bound). But Flutter's bundled Dart does not configure pub.dev OIDC credentials, sodart pub publish --forcehung indefinitely waiting for an interactive auth flow. Install the Dart SDK action after Flutter so its credential plumbing is the one in effect.
0.3.28 #
- CI publish workflow: install Flutter alongside Dart. Once the
package declares a Flutter SDK lower bound (added in 0.3.27), plain
dart-lang/setup-dartrejectsdart pub getwith "libwallet requires the Flutter SDK, version solving failed". Switch the publish step tosubosito/flutter-actionso the Flutter+Dart SDK pair is available on PATH. Cuts a no-op release after v0.3.27 because the workflow file used at publish time is the one snapshot-bound to the triggering tag, so the fix only takes effect on a fresh tag push.
0.3.27 #
- pubspec: declare a Flutter SDK constraint. Required by pub.dev
whenever
flutter.plugin.platformsis set; published as a no-op release after v0.3.26 was rejected at validation. v0.3.26's GitHub Release assets remain valid for any pinned consumer.
0.3.26 #
-
Build hook: invalidate the binary cache on a Dart upgrade. The
hook/build.dartcache filename was version-agnostic (liblibwallet-android-arm64.so), so a previously cached binary kept serving stale code afterdart pub upgrade— the Dart layer would decode events using the new shape while the loaded.so/.dylibstill emitted the old shape. Most visible failure: post-0.3.24 signing events arriving with pre-unification type strings likesign_typed_datainstead ofmessage_sign, falling through toUnknownPendingRequest. Cached filenames now embed the package version (liblibwallet-android-arm64-v0.3.26.so); the nextdart pub getafter this upgrade re-downloads the matching binary. -
iOS: ship as a Flutter FFI plugin (fixes external
dlsymfailure). The build hook'sLinkMode = LookupInProcess()for the iOS.aarchive worked for the in-tree test app, but Flutter's iOS pipeline did not reliably pass the static archive through to Xcode's linker for external consumers — the FFI symbols (LibwalletInit, …) got dead-stripped anddlsymfailed at runtime with "symbol not found". This release addsflutter.plugin.platforms.ios.ffiPlugin: truetopubspec.yamland shipsios/libwallet.podspec, which downloads the matching per-SDK static archives from the GitHub Release atpod installtime and force-loads them into the host app target via per-SDKOTHER_LDFLAGS. Both device + simulator slices are pulled, combined into a single fat simulator archive vialipo, and the per-SDK xcconfig picks the correct one per build configuration. No code changes for app authors —flutter pub upgradethen a freshpod installis enough.
0.3.25 #
- Rich payloads on the remaining approval events. Same
decode-at-emit philosophy as 0.3.24's sign events, now applied
to
ConnectRequest,AddNetworkRequest, andWatchAssetRequest:ConnectRequestnow carriesmethod(which RPC asked),family(evm/solana/bitcoin),availableAccounts(curve-compatible accounts pre-fetched for the picker),alreadyConnectedIds(pre-check in picker; render "Reconnect" vs "Connect"), andrequestedPermissions(EIP-2255).AddNetworkRequestflags phishing vectors:isKnown(chainId in the static chain registry),knownName(the canonical name — compare tonetwork.nameto detect impersonation),alreadyExists(no-op approval), and thenameMismatchconvenience getter.WatchAssetRequestgains typed EIP-747 accessors:assetType,address,symbol,decimals,image,tokenId, plusaddressLooksInvalidandisAlreadyTrackedheuristics. The oldassetraw-map getter still works for backward compat.
0.3.24 #
-
BREAKING: unified signing events. Every on-chain transaction signing flow (
eth_sendTransaction,solana_signTransaction,solana_signAndSendTransaction,mpurse_signRawTransaction) now comes through a singleTransactionSignRequest. Every arbitrary-data signing flow (personal_sign,eth_signTypedData*,solana_signMessage,mpurse_signMessage) comes through a singleMessageSignRequest. Branch onreq.method(andreq.chain) for chain-specific copy.Removed Dart classes:
SignRequest,PersonalSignRequest,SignTypedDataRequest,SolanaSignMessageRequest,SolanaSignTransactionRequest,SolanaSignAndSendTransactionRequest,MpurseSignMessageRequest,MpurseSignTransactionRequest. -
Rich decoded payload on every event — host UIs no longer need a follow-up
Transaction:simulatecall to render an approval sheet.TransactionSignRequestcarries:decodedMethod/decodedArgs— recognised top-level operation (native_transfer,erc20_transfer,erc20_approve, …)effects— every transfer / approve at any call depthbalanceChanges— signed native-balance deltas per addresswarnings— stable-coded advisories (recipient_is_contract,erc20_approve_unlimited,net_loss_exceeds_amount, …)willRevert/revertReasonfeeAmount+feeDecimals+feeSymbol,networkName,sizeBytes,raw- chain-specific extras:
evmTransaction,solanaUnitsConsumed,solanaLogs,bitcoinInputs,bitcoinOutputs,bitcoinFeeSats
EVM gets the full simulate decoder run at request-emit time; Solana decodes the common System Program transfer locally; Bitcoin runs through the existing
simulateBitcoindecoder. -
MessageSignRequestdecoded payload:messageBytes(raw) +messageText(UTF-8 try)structuredData/structuredPrimaryType/structuredDomainfor EIP-712- Auto-detected SIWE / SIWS (
isSiwe/isSiws) with parsedsiweFields(domain,address,uri,version,chainid,nonce,issuedat,expirationtime, …) — UIs can render a friendly "Login to example.com" prompt instead of a raw message body. warnings(e.g. message contains a URL)
-
New helper types for the rich payload:
TxSignEffect,TxSignBalanceChange,TxSignWarning,TxSignBitcoinIO— exported frompackage:libwallet/libwallet.dart.
0.3.23 #
-
BREAKING: unified network-switch approval. Every network switch —
wallet_switchEthereumChain(with or without an unknown-but-recognized chain) AND cross-family action methods — now flows through a singleChainSwitchRequestevent. Two shapes distinguished by which fields are populated:- Pre-specified target (
req.targetNetwork != null): dApp named a specific chain. Render a confirm sheet. Whenreq.isNewNetworkis true, the chain isn't in the wallet yet and approval implies Add + Switch. Approve withaccounts: [accountId]only —networkis taken from the request. - Picker (
req.targetNetwork == null,req.candidateNetworkspopulated): dApp triggered a cross-family action. Render a network + account picker. Approve with bothnetwork: pickedIdandaccounts: [pickedId].
ChangeNetworkRequestandAddAndSwitchNetworkRequestare removed. Hosts pattern-matching on those need to switch toChainSwitchRequestand branch onreq.targetNetwork != null. Seedart/doc/webview_integration.mdfor the new example.AddNetworkRequestis unchanged — pure add (no switch) is a distinct intent fromwallet_addEthereumChain.On approval libwallet still does Save (when new) + SetCurrent + implicit connect server-side. Hosts do not need a separate
client.networks.setCurrent(...)call. - Pre-specified target (
0.3.22 #
- EIP-2255 wire shape fix.
wallet_requestPermissionsandwallet_getPermissionsnow return one permission entry per capability (the EIP-2255 shape) with all authorised addresses in a singlerestrictReturnedAccountscaveat — instead of one entry per account with a missingparentCapabilityfield. dApps that readperm.parentCapability(etherscan.io, MetaMask test-dapp, most wallet UI kits) now get"eth_accounts"instead ofundefined. eth_accountsis filtered to EVM-compatible addresses. Solana / Monacoin accounts that happen to be connected to the same dApp no longer leak through, and"N/A"placeholders (from ed25519 accounts re-derived for an EVM network) are dropped. Empty result is[], notnull.- Cross-chain auto-switch (
ChainSwitchRequest). When a dApp calls an action method (sign / send / connect) on a chain family different from the wallet's current network, libwallet now emits achain_switchapproval that lets the user pick BOTH the target network AND an account in one prompt. On approve, the wallet switches network and saves aConnectedSitefor(host, account)so the original method runs with the dApp already connected. Skip the prompt automatically when the wallet has no compatible network or account for the requested family — original handler errors more informatively in that case. Web3:requestrecogniseswallet_revokePermissions(EIP-2255 revoke). Was unhandled in pre-0.3.21 builds and fell through to the chain RPC relay producingfailed to decode response … invalid character 'm'. Already fixed in 0.3.21; the docs now call this out explicitly.wallet_switchEthereumChainaccepts both spec + bare-string param shapes. Etherscan and other EIP-3326-compliant dApps no longer 500 withfailed to convert map[string]interface {} to string. When the target chain isn't registered yet but is in libwallet's static metadata, emits a combinedadd_and_switch_networkapproval instead of bouncing 4902.- Network change happens server-side. When the user approves
any network-changing request (
change_network,add_and_switch_network,chain_switch), libwallet callsSetCurrentitself before returning to the dApp. Hosts no longer need to callclient.networks.setCurrent(...)afterrequests.approve(...)— the workaround is redundant on 0.3.22+. - NFTs on non-mainnet EVM chains return
[]instead of 500.Nft:liston Sepolia (and any other non-mainnet EVM chain not covered by the modchain provider) now returns an empty list so the wallet UI just renders "no NFTs" instead of failing the whole asset view. - Doc: webview integration guide gained a "Network + permission
semantics" section answering who switches the network on
approval, who switches the account, what the EIP-2255 wire shape
is, and where the typical workarounds were masking real bugs.
Step 4's example switch covers
AddNetworkRequest,ChangeNetworkRequest,AddAndSwitchNetworkRequest,ChainSwitchRequest, andWatchAssetRequestwith concrete approve calls.
0.3.21 #
- Fix:
wallet_revokePermissions(EIP-2255). Was unhandled in 0.3.20 and fell through to the chain-RPC relay, surfacing asinvalid character 'm' looking for beginning of valueon etherscan.io and any other dApp that calls it. Now revokingeth_accountsdrops everyConnectedSiterow for the requesting host (same effect assolana_disconnecton the Solana side) and returns null. Unknown permissions are silently ignored for forward-compat (matches MetaMask).
0.3.20 #
- Fix:
swap.availability()404 on 0.3.19. The handler signature included a spurious*struct{}second arg that apirouter's dispatcher didn't match, so the endpoint never resolved at call time. Realigned to the idiomatic zero-param shape used by other parameterless handlers. - Fix:
wallet_switchEthereumChainparams. 0.3.19 decoded the first param as a string, which failed withfailed to convert map[string]interface {} to stringon EIP-3326-compliant dApps like etherscan.io. Now accepts both the spec shape[{chainId: "0x…"}]and the bare-string form some non-compliant dApps still send. - New: combined add + switch approval flow. When a dApp calls
wallet_switchEthereumChainfor a chain the wallet hasn't seen yet but which libwallet recognizes from its static chain metadata, emits a singleadd_and_switch_networkapproval request (newAddAndSwitchNetworkRequestsubtype). The UI can render "etherscan.io wants to add Polygon and switch to it" in one prompt instead of bouncing the dApp with a 4902 error and forcing it to retry throughwallet_addEthereumChain. Unknown-to-libwallet chains still return 4902. - Breaking:
rawRequest/rawRequestWithProgressremoved fromLibwalletClient. The typed API namespaces cover the feature surface; the raw door encouraged callers to hardcode paths and couple themselves to internal wire shapes, which meant every server-side rename silently broke them. Migrate to the equivalent typed call (client.info.ping(),client.transactions.list(), etc).
0.3.19 #
- New
swap.availability()endpoint — UI feature-flag for the "Swap" button. No RPC calls; returns{available, network, providers, reason}in a couple of ms. Gate per specific<type>.<chainId>(e.g."evm.1"/"solana.mainnet-beta") so devnet / testnet / unsupported EVM chains don't render the Swap affordance:- Solana mainnet → available (Jupiter + dFlow).
- Solana devnet / testnet →
unsupported_chain. - EVM chains covered by 1inch (
1,10,56,100,137,250,324,8453,42161,43114,59144) → available onceOneInchAPIKeyis compiled in;missing_api_keyuntil then. - Other EVM chains →
unsupported_chain(1inch doesn't cover them — would 404 upstream). - Bitcoin-family (bitcoin, bitcoin-cash, litecoin, dogecoin,
monacoin, …) →
unsupported_chain. SwapAvailability.chainFamily/chainIdgetters splitnetworkfor apps that need the family vs. the specific id.
0.3.18 #
accounts.delete()now cascade-removes Web3 connections that reference the deleted account. Before 0.3.18 those rows were left behind pointing at a non-existent account id — every list of connected sites for that account would keep returning stale data until the user manually deleted each one. Done synchronously, so no orphan window exists. Scoped: unrelated connections for other accounts are untouched. Transactions are intentionally NOT cascaded (tx history outlives the originating account by design).
0.3.17 #
Transaction:listnow supports cursor pagination + filters that the docs always promised. Up to 0.3.16 the handler was hardcoded to 50 rows and theFrom/Networkquery params were silently ignored on this path (onlyDELETE Transactionhonoured them). Newbefore(RFC3339Nano cursor onCreated) andlimit(default 50, capped 200) params drive an infinite-scroll pattern — the response stays a flat list, clients derive the next cursor fromlast.created. Darttransactions.list()gains the new parameters with an example in the docstring.
0.3.16 #
- Fix:
Transaction:signAndSendnow backfillsFeeserver-side before saving. The new typedUnsignedTransactiondeliberately omitsfee(server is the source of truth) but signAndSend wasn't recomputing it on its own — apps that went straight to signAndSend (or used the typed shape) ended up withnullFee in tx history. Same formulas Validate already uses (gas × gasPrice on EVM,5000 + ceil(cuLimit*cuPrice/1e6)on Solana). No client change needed. Transaction:maxSendableno longer takesnetwork— it's derived from the asset key's<type>.<chainId>.prefix (the same shapeAsset:listreturns). Empty / bareNATIVEfalls back to the current network. Pre-release cleanup; 0.3.15 was not consumed externally with the old shape.
0.3.15 #
- Swap API (
SwapApi/client.swap): token swaps on Solana (Jupiter Ultra primary, dFlow fallback) and EVM (1inch). Two-step flow:quote()returns aSwapQuotewith expected output, min output after slippage, route breakdown, and a 90 s quoteId;execute(quoteId, keys)signs and broadcasts, returning aSwapResultwith the on-chain tx hash + explorer URL. All providers are wired with a 50 bps referral fee to libwallet's fee accounts (Solana:BF436…, EVM:0x17Ab…). - Approval detection + tight default on EVM swaps: for ERC-20
input tokens on 1inch,
quote()now reads the router's current allowance viaeth_calland populatesSwapQuote.requiresApproval,approvalSpender,currentAllowance, andneededAllowance. WhenrequiresApprovalis true, call the newswap.buildApproval()— it returns a richApprovalPreview(token, spender label, amount,isUnlimitedflag, current allowance, network fee, plus the validatedTransactionto sign) ready to drop into an approval sheet. Default approval amount is exactly the swap's input amount, so a compromised router can only drain what the user already agreed to. PassapprovalAmount: 'max'to opt into the classic unlimited approve, or a decimal string for a custom cap — surface the trade-off viapreview.isUnlimitedin the UI. - Richer Quote payload for UI approval sheets:
SwapQuotenow carriesproviderLabel(human-friendly name),referralFee(the 50 bps platform fee as an absolute amount in the input token's units), andnetworkFee(estimated chain gas in native currency). Apps no longer have to compute these from bps or gas*gasPrice themselves. - Known limitations in v1:
- The 1inch API key ships empty in this build; populate
wltswap.OneInchAPIKeyto enable EVM swaps. - No token resolver yet: callers pass
SwapTokenRefwithaddress+decimalsfully resolved (the data is already available fromAsset:list).
- The 1inch API key ships empty in this build; populate
0.3.14 #
Transaction:maxSendable(TransactionApi.maxSendable): new cross-chain endpoint that returns the largest amount safely sendable from an account, with a breakdown of the fee and (on Solana) rent-exempt reservations. Fixes the "tap Max → getinsufficient funds for rent" bug: apps can now pre-compute the right amount instead of letting the broadcast fail. EVM and Bitcoin supported; token (ERC-20 / SPL) assets return an explicit error — full token balance is always sendable, fees paid in native.- Solana native-send preflight:
Transaction:validatenow runs a balance / fee / rent check for Solana native transfers before signing. Typed codes:insufficient_balance,below_sender_rent,recipient_rent_not_funded— apps see a structured error with the exact shortfall instead of Solana's opaque simulator rejection. TransactionSimulation.warnings: simulate now returns a list of advisoryWarnings with stable codes. Non-blocking — the tx can still be signed; apps decide whether to confirm with the user. Initial codes:recipient_is_contract(EVM native send to a contract address),recipient_new_account(Solana recipient doesn't exist yet),erc20_approve_unlimited(approve with top bit set — drainer vector),priority_fee_recommended(Solana median priority fee > 0 but tx has no ComputeBudget).- Solana priority fees (opt-in): new
computeUnitLimit/computeUnitPrice/priorityLevelfields onUnsignedTransaction. SetpriorityLevel: "low" | "medium" | "high"to havevalidatepick a percentile of recent on-chain prioritization fees; or pincomputeUnitPrice(microlamports/CU) directly."none"opts out explicitly. Empty (default) preserves the legacy 5000-lamport flat fee — the serialized message is byte-identical to pre-0.3.14 for unchanged callers. - Solana displayed balance excludes rent reserve: a user who
receives 0.01 SOL now sees
0.01in their wallet instead of0.01089(the extra ~0.00089 is the rent-exempt minimum the account needs to stay alive on-chain and is never spendable without closing the account).maxSendablestill reports the raw balance and breakdown for apps that want to show "0.01 spendable- 0.00089 reserved + 0.000005 fee".
0.3.13 #
-
Logs routed over the event channel (fixes 0.3.12's silent logging on iOS). 0.3.12 wired every internal diagnostic through
wltlog, but the underlyinglog.Printfwrites to the Go runtime'sos.Stderr— which Flutter+iOS swallows entirely, and Flutter+Android filters out offlutter logsby default. End result: testers saw no output even withlogLevel: "debug". Fixed by routing every wltlog emission through the apirouter broadcast channel (same pipe Web3 requests / balance changes already use). -
New
LogEvent+client.logsstream: subscribe once at startup and forward todeveloper.log/printso the logs show up in Flutter's log output on every platform:import 'dart:developer' as developer; client.logs.listen((e) { developer.log(e.message, name: 'libwallet.${e.level}'); }); await client.info.setWalletInfo( clientId: '...', logLevel: kDebugMode ? 'debug' : 'off', );LogEventis also emitted on the generalclient.eventsstream for hosts that want a single subscription. -
Sink safety: a panic inside the sink falls back to stderr with no rethrow, so a broken logging pipeline can never take down a send.
0.3.12 #
- Leveled logging (
wltlog) controlled bysetWalletInfo: newLogLevelfield onWalletInfo. Valid:"debug" | "info" | "warn" | "error" | "off"; empty resolves to libwallet's auto-default —"debug"on dev binaries (gitTag empty),"info"on release binaries. Typical pattern:logLevel: kDebugMode ? "debug" : "off". Every log call site routes throughwltlog.{Debugf,Infof,Warnf, Errorf}; lines are prefixed[debug] / [info] / [warn] / [error]so testers can grep by level regardless of the host's logger.getWalletInfoalso returnseffectiveLogLevelso the host can see what libwallet actually resolved""to. - Ed25519 self-heal diagnostics: the self-heal now logs a
specific skip reason at every gate (nil account, no wallet,
GetEnv(ctx)nil,WalletByIdfailed, wrong curve, no keys, decrypt failed, empty want, already-correct), visible atdebug. The actual repair (Pubkey/Address flip) logs atinfo. Combined with the always-onwantvsacct.Pubkeyvswallet.Pubkeydump, a tester flippinglogLevel: "debug"gets everything needed to pin down why a Solana send fails. FindAccountnow runscheck()on the address-lookup path: previously only the ID-lookup branch refreshed Curve / Address — tx.From is almost always an address, so account records with an empty Curve (rare but possible) would silently short-circuit the Ed25519 self-heal's curve gate. Fixed by callingacct.check(e)after the by-Address fetch.- Pre-broadcast Ed25519 verify:
Transaction:signAndSendon Solana now runsed25519.Verify(fee_payer, message, sig)locally before sending to the RPC. Catches pubkey/key-share mismatches with a specific error message ("TSS key shares may be inconsistent with stored pubkey") instead of the generic Solana-side rejection. - Extended pre-flight repair to every Solana sign path: 0.3.11
put the pre-flight only in
Transaction:signAndSend. The shared helperwltacct.EnsureEd25519PubkeyOnAccountis now called fromAccount:signMessage(solana mode),Account:signTransaction,Account:signAndSendTransaction, and Web3solana_sign_message/solana_sign_transaction/solana_sign_send_transaction. The helper also saves the repaired Account row synchronously so the dApp's nextwindow.solana.publicKeyread returns the corrected address. - Per-RPC timing logs (at
debug): everyNetwork.DoRPC/DoRPCNamedemitsrpc: chain=X method=Y OK in Nms (B bytes)orFAIL in Nms: err. Quiet atinfo; noisy but invaluable when reproducing a bug. - Per-key-decrypt timing (
wallet-signlogs, atdebug): entry line with wallet id/threshold/keys/msg_len; per-key "decrypted in N ms (type=Password|StoreKey|…)". Pubkey mismatch detected during sign logs atwarn.
0.3.11 #
- Solana ed25519 self-heal across every sign path: 0.3.10 only
repaired the legacy pubkey encoding inside
Transaction:signAndSend. A tester reported sends still failing on 0.3.10, which turned out to be a different entry point:Account:signAndSendTransactionand the Web3solana_sign_{message,transaction,send_transaction}approvers bypassed the repair. Extracted the fix intowltacct.EnsureEd25519PubkeyOnAccountand wired it into every Solana-capable sign path, includingAccount:signMessage. The helper also saves the repaired Account row synchronously (not just via the asyncwallet:pubkey_repairedhandler) so the nextFindAccount/window.solana.publicKeyread returns the corrected address in the same request lifecycle. - Visibility log: self-heal now emits
ed25519-repair: account <id> (wallet <id>) pubkey/address repaired: ...tolog.Printfwhen it fires. If affected users report that sends still fail after upgrading, grep logs fored25519-repair:— presence confirms the native binary upgrade landed and the repair ran; absence means the app is still running a pre-0.3.9liblibwallet.<ext>from the package cache. - Regression test:
TestEdDSAWalletCreatenow asserts the storedWallet.Pubkeybyte-matches the canonical compressed-Y Ed25519 form, and that stdlibed25519.Verify(storedPubkey, msg, sig)accepts the TSS signature. Either assert would have caught the original 0.3.9 encoding bug locally — same rejection Solana does on-chain.
0.3.10 #
- Solana ed25519 self-heal now actually runs (follow-up to 0.3.9):
the self-heal path in 0.3.9 had a wrong type assertion against the
signing context — it silently never triggered, so affected wallets
kept failing every send attempt. Fixed to use
wltintf.GetEnv(ctx). Additionally, added a pre-flight repair step in the Solana send path that decrypts one key share BEFORE building the transaction and patchesacct.Pubkeyin-memory, so the first send on an upgraded install succeeds instead of needing a failed-then-retry cycle. New exported helperwltwallet.EnsureEd25519Pubkeyis a no-op when the wallet is already correct.
0.3.9 #
- Solana ed25519 pubkey fix (breaking for existing Solana wallets):
ed25519 wallets created pre-0.3.9 stored the X coordinate of the
Edwards point (big-endian) as the "public key" instead of the
standard compressed encoding (Y little-endian with X's sign bit in
the MSB of byte 31). Consequences: the displayed Solana address was
wrong, balance queries hit a different address from the one the
TSS signs with, and every
sendTransactionfailed with "Transaction did not pass signature verification". Fixed at wallet creation viaToEd25519PubKey().Serialize(). Existing broken wallets self-heal on the first sign attempt (which fails once, then the repair propagates to the wallet + linked accounts and the retry succeeds). - On-chain tx history backfill (EVM):
client.transactions.list()now includes on-chain activity, not just txs this install built. Triggered in the background onAccount:setCurrent/Network:setCurrent/ env init. First triesmodchain_historyByAddress, falls back to Otterscan'sots_searchTransactionsAfter(erigon v3). Newclient.txHistoryUpdatesstream fires when new rows land. - Immediate balance refresh after sends: every
Transaction: signAndSend/Account:signAndSendTransaction/mpurse_sendRawTransaction/solana_sign_send_transactionnow nudges the background balance poller. Users see the new balance within ~1 s instead of up to 60 s.
0.3.8 #
- Background balance polling: new
client.balanceChangesstream yields aBalancesChangedEvent(full{network, account, assets}snapshot) every 60 s when the current account / network balances change. Lifecycle-aware — pauses underLifecycle:update('background')/paused, resumes with an immediate poll onforeground/resumed/active. - RPC timeouts (reliability fix): all
Network.DoRPC/DoRPCNamedcalls are now bounded by a 30 s default deadline. A misbehaving upstream (dead Ethereum public RPC, stale Solana endpoint, etc.) can no longer wedge a goroutine forever. The balance poller uses a tighter 15 s cap. Callers that need a specific deadline can use the existingDoRPCCtx/DoRPCNamedCtx. Fixes an iOS CI hang. - Network:testRPC extended: now accepts
type=evm/solana/bitcoinand probes the right health method per family. EVM is still the default;RpcTestResultgainedsolanaVersion/solanaCluster/bitcoinChain/bitcoinBlocksfields +isEvm/isSolana/isBitcoingetters. - Android 16 KB page alignment: CI now builds every Android
.so(both the AAR and the Dart FFI set) with-Wl,-z,max-page-size=16384and verifies it withreadelf. Required for Android 15+ devices with 16 KB page size (Pixel 8+).
0.3.7 #
- Wallet-identity plumbing: new
client.info.setWalletInfo(clientId:, name?, version?)registers the host wallet with libwallet. TheclientIdis sent as theSec-ClientIdHTTP header on everyCrypto/WalletSign:*call, which the WalletSign backend uses to pick branded SMS / email copy, apply per-app rate limits, and tag audit logs.name/versionare stored for future use (untrusted display strings, diagnostics). Called once at startup; backward- compatible (header not sent if not configured). - EIP-6963 UUID fix: webview injection docs corrected — generate a
fresh UUIDv4 per page load (spec requirement), do NOT persist
across launches.
rdnsis the stable identifier dApps key off, notuuid. - Drop Unix-socket transport fallback: FFI is the only supported
transport now. Removed
LibwalletClient.connect(socketPath)/.fromSocket(socket),JsonRpcConnection, request framing helper, and the socket-based testserver binary.Transportinterface stays (test mocks still work) but has one implementation.
0.3.6 #
- WalletConnect v2: full wallet-side implementation.
client.walletConnectcovers pair / sessions / approveSession / rejectSession / respond / respondError / emitEvent / disconnect. Two sugar streams (walletConnectProposals,walletConnectRequests) deliver typedWcSessionProposal/WcSessionRequestobjects. Sessions persist across restarts (SQL-backed); relay reconnects with backoff. Protocol pieces implemented: X25519 + HKDF + ChaCha20-Poly1305 envelopes, Ed25519 relay JWT auth, CAIP-10/-2 namespace handling,wc_sessionPropose/Settle/Request/Event/Delete. Seedoc/walletconnect_integration.md. - Transaction simulation + decoding: new
client.transactions.simulate. On EVM (erigon v3 backend), usesdebug_traceCallwith thecallTracerto walk the full call frame tree and return every ERC-20 Transfer + Approval and every value-carrying CALL at any depth asTransactionSimulation.effects. Second pass withprestateTracer(diff mode) returns per-address native-balance deltas asbalanceChanges. Top-level calldata decoded intodecodedMethod+decodedArgs(native_transfer/erc20_transfer/erc20_approve/unknown). Revert reasons decoded from standardError(string)ABI. Solana wrapssimulateTransaction(logs + unitsConsumed + err). Bitcoin parses viaoutscript.BtcTx(inputs + outputs + fee). - WebView injection: new
client.web3.injectionScript(...)generates a JS blob exposing libwallet aswindow.ethereum(EIP-1193 + EIP-6963),window.solana(Wallet Standard), andwindow.mpurse(Monacoin — github.com/tadajam/mpurse). Full wiring walkthrough indoc/webview_integration.md. - Bitcoin-family message signing (via mpurse):
mpurse_signMessagesigns with the TSS key over the standard "\x18Bitcoin Signed Message:\n" / "\x19Monacoin Signed Message:\n" / etc. prefix, returning the 65-byte compact signature (base64, Bitcoin Coresignmessageformat).mpurse_signRawTransactionparses the hex, matches inputs to the user's xpub viamodchain_lookupTxoBIP32, signs each input, and returns the signed hex.mpurse_sendRawTransactionis a direct passthrough tosendrawtransaction.mpurse_sendAssetstill errors (Counterparty server interaction is out of scope). - Monacoin network support:
bitcoinAddressrecognizesmonacoinchain id and emits the bech32mona1...address viaoutscript.Out.Address("monacoin"). - Typed pending-request flow:
PendingRequestis now sealed with one subtype per Web3 request kind (ConnectRequest, PersonalSignRequest, SignTypedDataRequest, SolanaSign* / Mpurse* / …, UnknownPendingRequest). Therequestevent now carries the full request object so consumers can render the prompt on first paint without a follow-upRequest/<id>fetch. Newclient.pendingRequestsstream yields fully-parsed requests ready for pattern matching. - Example package layout: new
example/libwallet_example.dartCLI sample covering init / wallet create with live progress / account / balance / pendingRequests subscription. Satisfies pub.dev's example requirement. - pubspec: description trimmed to 129 chars (was 212) to satisfy pub.dev's metadata scan.
0.3.5 #
- Direct account signing: new
Account.signMessage,signTransaction,signAndSendTransactionendpoints let wallet-host apps sign directly without routing through the Web3 pending-request/approve flow. Removes ~80 lines of async listener code from the typical Dart integration. - View accounts (read-only):
accounts.createView(type:, address:, xpub:)creates accounts with no backing wallet — suitable for watching a counterparty address or an HD tree (xpub, bitcoin-family). Balance and NFT queries work; signing is rejected. NewAccount.isViewOnlygetter. - Progress redesign: progress events are now a single 0..1
fractioninstead of{count, running}. ECDSA wallet creation now emits fine- grained ticks during Paillier / NTilde safe-prime generation (one per prime found out of 4, per key) — previously the UI was blind for 20+ seconds per key share. Requires tss-lib v2.2.4+. - Typed-API cleanup: removed
dynamicreturns and rawMap<String, dynamic>param inputs across the API surface. New typed models:SignedMessage,RemoteKeySession,RemoteKeyValidation,NftListing,WalletBackupEntry,RpcTestResult,UnsignedTransaction. Methods liketransactions.signAndSend(UnsignedTransaction),wallets.backup(),remoteKeys.validate()now return proper model instances. Raw param maps oncontacts.update,networks.update,tokens.updatereplaced with named parameters. - Validation: reject wallet
Curvevalues outside{secp256k1, ed25519}; reject account type/curve mismatches (e.g.solanaon secp256k1,ethereumon ed25519). Bitcoin accounts now derive on BIP-44 coin_type 0 (m/44/0/0/i) instead of Ethereum's coin_type 60.
0.3.4 #
- Email 2FA:
RemoteKey:new(andremoteKeys.create) now accept an email address in addition to phone numbers. Pass eithernumberoremail— the EllipX backend routes SMS vs email verification based on whether the value contains@.
0.3.3 #
- Bitcoin balance fix:
modchain_assetsreturnsbalanceas a decimal-formatted number ("0.00000000"), not int64. Decode viaoutscript.BtcAmountwhich handles both forms. Previously failed with:json: cannot unmarshal number 0.00000000 into Go struct field. - Solana NFT fix:
getAssetsByOwner(Helius DAS API) requires named JSON-RPC params, not positional. NewNetwork.DoRPCNamed()helper. Previously failed with:invalid type: map, expected a string. - Bitcoin UTXO decode:
modchain_lookupTxoBIP32response uses the sameBtcAmountserialization foramtandbalancefields. Type switched from int64 to outscript.BtcAmount across wlttx/bitcoin.go. - EVM NFT lookup hardening: type assertions on the
modchain_assetsresponse in wltnet/nft.go could panic if any field was missing or the wrong type. Replaced with comma-ok form. - iOS Dart Tests CI timeout bumped 45 → 60 minutes (Xcode build slow).
0.3.2 #
- iOS simulator support: build hook now detects
iphoneosvsiphonesimulatorSDK and downloads the correct binary. Previously the simulator would try to link the device-only binary and fail. - Release now includes
liblibwallet-iossimulator-arm64.a(Apple Silicon) andliblibwallet-iossimulator-x64.a(Intel Mac simulators) alongside the existingliblibwallet-ios-arm64.adevice binary.
0.3.1 #
- Bitcoin HD address support:
bitcoin-type accounts now derive multi-address HD trees under their account xpub. Balance queries callmodchain_assets(xpub)which scans0..lastI+20child keys (BIP-44 style gap limit) server-side. - New
AccountApi.xpub(id): returns the BIP-32 extended public key. - New
AccountApi.nextAddress(id): returns the next clean receive (or change) address based on on-chain scan. - New
AccountApi.allAddresses(id): lists all HD addresses across receive and change chains with activity markers. Account.Addressnow points tom/0/0(first receive address) instead ofm/0for Bitcoin-family accounts. BTC/LTC/DOGE/BCH supported.
0.3.0 #
- EIP-1559 transactions: Auto-selected when the chain supports it. New
maxFeePerGasandmaxPriorityFeePerGasfields onTransaction. - ERC-20 transfers: New
erc20_transfertransaction type. Pass a token XUID inAsset, recipient inTo, and amount — libwallet encodes thetransfer(address,uint256)call automatically. - ENS / SNS name resolution: New
client.names.resolve('vitalik.eth')API. Auto-detects.eth(Ethereum) and.sol(Solana) suffixes. - Solana devnet: Routes to the correct Helius devnet RPC endpoint
when using a Solana network with
chainId: "devnet". - Local dev:
hook/build.dartnow prefers a localtestserver/liblibwallet.<ext>over downloading from GitHub Releases. - Fix: macOS dylibs built with
-headerpad_max_install_namesso Dart can bundle them without relinking.
0.2.0 #
- Auto-download native binaries from GitHub Releases at build time
- CI testing on macOS, Android emulator, and iOS simulator
- 43 integration tests covering all API endpoints
- Full dartdoc on all model fields (~130 fields)
- Comprehensive README with usage examples
0.1.0 #
- Initial release
- FFI transport with
NativeCallable.listenerfor Go→Dart callbacks - 17 typed API classes covering all libwallet endpoints
- 15 model classes with full dartdoc
- Socket transport as legacy fallback
- Native asset hook for pub.dev binary distribution