durt2 1.10.1
durt2: ^1.10.1 copied to clipboard
Dart library for interacting with Duniter v2s blockchains. Provides wallet management, transaction signing, Squid GraphQL requests, and more
1.10.1 #
- An identity still to confirm is reported as such even while the issuer
waits (durt#6).
DuniterStorageService.getCertStatechecked the issuer's delay before the target's status: an issuer whose next certification was not due yet gotmustWaitBeforeCertfor an identity still to confirm, and an app offered to schedule a certification the chain refuses unless the identity is confirmed by then. What the target lacks (revoked, empty account, identity to confirm) now comes before the issuer's delay. Nothing to do for callers that already handlemustConfirmIdentity.
1.10.0 #
- A certification can be sent without the newcomer's distance evaluation
(gecko#276).
DuniterService.certifybundledrequestDistanceEvaluationForwhenever the certification brought a confirmed newcomer to the threshold, on the certifier's account, and a failed evaluation may cost the certifier what it reserved. New parameterrequestDistanceEvaluation(defaulttrue, the behaviour so far): an app that has computed the newcomer's distance below the rule, or could not compute it, passesfalseand the certification is sent alone. NewDuniterService.certificationRequestsEvaluation(...)says, before certifying, whether the evaluation would be bundled: the very testcertifyapplies. - Renewing a certification no longer requests the evaluation of a
newcomer still short of the threshold. A renewed certification adds
none to the newcomer's count, yet
certifycounted it as one: renewing at four certifications out of five requested an evaluation the runtime rejects (NotEnoughCerts). Nothing to do for callers.
1.9.0 #
- Test mode has a wallet service, in memory (durt#2).
Durt.i.walletsno longer throws underDurtTestMode: it keeps its safes and wallets in memory, so creating, restoring, deriving and deleting a safe run in a test as in the app, keys derived by the same keyring.DurtTestConfig(connected: true)makesDurt.i.isConnectedtrue, for code that only runs once connected. EverythingWalletServicereads or writes now goes through aWalletStore(ObjectBoxWalletStorein the app,InMemoryWalletStorein tests); behaviour is unchanged. New onWalletService:getAllSafes,getAllWallets,hasSafes,storeSafeandfindWallet, for callers that reachsafeBoxandwalletBoxdirectly today: those still work in the app, and throw in a test, which has no ObjectBox.
1.8.0 #
- Breaking: macOS secrets live in the login Keychain; the clear-text file
is gone.
Every durt2 secret on macOS (the master key that peppers the PIN
derivation, the biometric PIN blobs) was written to a JSON file under the
user's home, in clear. gecko#259 measured, from a copy of that file and of
the ObjectBox database, a 4-digit PIN recovered offline in 22 s and the
biometric PIN decrypted in milliseconds. The file backend is removed on
every platform; macOS now goes through
flutter_secure_storageon the login Keychain (usesDataProtectionKeychain: false, since a Developer ID build outside the App Store carries no application-identifier entitlement and the data-protection Keychain refuses it), under a per-app service name. What callers must do:- call
CryptoUtils.useMacosKeychainService('<bundle id>')before any secret is touched (defaultdurt2), and never rename it afterwards: the login Keychain finds an item by (service, account); - call
CryptoUtils.migrateLegacySecretFiles([...])once at startup on macOS, with the paths of the old files most recent location first. It moves every entry (the.tmpsiblings included), reads each one back, destroys the files lowest priority first, and returns aLegacySecretFileReport; on a store failure it throwsSecureStorageExceptionand leaves every file where it was, so the next launch tries again. A file naming another master key than the one in use (the Keychain's, or the most recent file's) is a conflict: nothing of it is imported or destroyed,filesConflictingnames it andreport.unresolvedis true, as it is for an unreadable file; read that flag and tell the user, the secrets are still on disk; CryptoUtils.setMacosSecureStoragePathandCryptoUtils.secureStorageare removed (the breaking part), and so is the biometric enrollment migration from the raw Keychain plugin to the file. A macOS install that enrolled biometrics with a build older than that migration re-enrols once with the PIN.CryptoUtils.masterKey()refuses to generate a key while a migration failed or left a file unresolved in the process (the files still hold the real one), and rejects a stored key that is not 32 bytes.
- call
WalletService.checkCode()answersfalsefor a wrong PIN only. It caught every exception, so a Keychain refusal or a pending migration came back as "wrong PIN", which the host app books towards the deletion of the safe. ASecureStorageExceptionnow goes through; the check itself isWalletService.verifyPin(safe, pin), static, testable without a store. Callers ofcheckCodecatchSecureStorageExceptionand count nothing. Only the cipher's own refusal answersfalse; any other fault propagates.- A failed decryption while a legacy file is unresolved is not a wrong
PIN. The key in use may not be the one the safe was encrypted with (a
restored backup, the file of an older build):
CryptoUtils.decryptthen throwsSecureStorageExceptioninstead of "Invalid secret code", andCryptoUtils.legacySecretFilesUnresolvedtells the host app which state it is in, so it can name the conflict to the user. A stored master key that does not decode is a store failure too. DurtTestMode.installInMemorySecretStore()replaces the native secret store with a map for a host app's tests, and returns it.- A failing secret store throws
SecureStorageExceptioninstead of reading as empty.secureRead,secureWrite,secureDelete,secureContainsKeyandsecureReadAlllogged the failure and answerednullor nothing, whichmasterKey()took for "no key yet": it generated a fresh key over the real one, and every safe became undecryptable. The failure now propagates, and a keymasterKey()generates is read back before it is used. A caller that readnullas a broken store gets the exception instead.
1.7.1 #
- A pod outage is no longer reported as an empty document.
getContactsandgetCertificationQueuecaught every error and answerednull, which their callers read as "there is nothing on the pod yet". Both write the result back, so a client that read the outage as an absence published its own partial list over the complete one: the user's contacts, and the certifications waiting in their queue. 1.6.0 introducedCesiumPlusUnavailableExceptionfor exactly this distinction and fixedgetProfileByAddress, but these two readers flattened it back tonullone frame later. They now let it through, and raise it themselves when the service holds no endpoint: not being initialised is not an answer either.nullis left to the one case where the pod did answer and there is no profile or no field. Measured during the 2026-09-20 outage ofg1.data.e-is.pro, where a reinstalled Ğecko showed no contact and an empty certification queue while both were sitting on the pod. See gecko#266 for the client side.
1.7.0 #
-
Runtime API calls are now capped and retried instead of being fired all at once.
getBalancessent onestate_callper address in a singleFuture.wait, so scanning 30 derivations meant 30 concurrent runtime API calls. Duniter nodes serve only 4 at a time and answer4003 too many concurrent runtime API callsto the rest: measured on the public Ğ1 nodes, a burst of 30 lost 12 to 26 calls ong1.gyroi.de,g1.pini.frandg1.1000i100.fr, whilerpc.duniter.organdg1.axiom-team.franswered all of them. A single rejection made the wholeFuture.waitfail, which is what broke Ğecko's derivation scan on restore. Every runtime API call now goes through one shared semaphore of 4, sized on that measurement, and a call rejected for overload is retried a couple of times with a short backoff. The semaphore is shared rather than per-call-site because the node's limit applies to the connection: a derivation scan and the balance streams of the open wallets hit the same node together. Errors other than overload, and an exhausted retry budget, still propagate.Each call also gets a 15 s deadline, which the sharing makes necessary. polkadart's
WsProvider.disconnectcloses the socket without ever completing the calls still in flight, and durt2 replaces the provider on a node switch or a network change; such a call used to hang forever, penalising only its own caller. Holding a shared slot, four of them would have wedged every balance query in the app behind a semaphore that never frees up again, with no error and no log. The deadline releases the slot and surfaces aTimeoutExceptionthe callers already handle. -
No query asks an indexer connection for more than it will serve. The combined history requested a thousand universal dividends at once. Dunipod refuses any connection page above 500 and rejects the whole query rather than serving part of it, so every account owning an identity got an HTTP 400 that callers read as "this account has nothing" (gecko#250). Squid was lenient about it, which is why the number went unnoticed until a second indexer implementation appeared.
maxConnectionPageSize(500, measured againstgtpod.elo.tf:first=500answers,first=501is refused) andconnectionPageSize()are now applied where a number enters a query, so no call site can bring the default back by forgetting it; both are exported. A caller asking for more than 500 gets 500 instead of an error. Past 500 dividends, roughly sixteen months, the oldest ones fall out of the merged history: the real answer is cursor pagination, which needs the GraphQL bindings regenerated.
1.6.0 #
-
Contract change on
CesiumPlusService.getProfileByAddress: it can now throw. It used to answernullfor three different things, a 404, an unexpected HTTP status, and a network failure, swallowing the last two and only logging them. Callers that re-publish a profile read thatnullas "this user has no description, no city, no socials" and published those absences, whichuploadProfileturns into an explicit erasure: a pod hiccup was enough to wipe a profile remotely, on every device at once. The read now answers three distinct things: the document,nullon a 404 (the only case where "absent" is an answer from the pod), and the newCesiumPlusUnavailableExceptionfor everything else, including a 200 with no_source, a pod we failed to understand is not an empty profile. Writes inherit the guarantee without changing a line: their existingcatchmakes them returnfalsewithout publishing anything. Callers that only want to display a profile must now handle the exception instead of showing "no profile" for an outage. -
Squid indexers:
SquidClientManager.testEndpointtakes anenforceMinimumVersionflag (defaulttrue, so automatic selection keeps theminimumSquidVersionfloor). Callers testing an endpoint the user named themselves passfalse: the floor no longer refuses a deliberate choice on a version number alone. The sync check (recent block hashes compared with the Duniter node) and the schema probe still run in both cases. -
config/squid_endpoints.json: the gtest list carriedindexer.duniter.org, which indexes the Ğ1 chain, so the hash check rightly refused it and the two remaining gtest entries answered 502. It is replaced bysquid.gtest.bulma.sleoconnect.fr, which follows the ĞTest chain.
1.5.1 #
- Version bump only, no code change since 1.5.0.
1.5.0 #
- Refreshed the bundled g1 bootstrap endpoints. The assets shipped a SINGLE
RPC seed (
g1.p2p.legal) and a SINGLE Squid seed (g1-squid.axiom-team.fr); both are permanently decommissioned, so every fresh install or cold cache burned seconds in timeouts before falling back to the remote network config. The seeds now carry the canonical g1 lists minus those two dead hosts (11 RPC, 8 Squid), currently-healthy endpoints first, and the g1 polkadart codegen source moves towss://rpc.duniter.org. Measured on the real g1 network the same day: time to a fully usable node + indexer went from 6.4s / 7.7-9.9s (legacy flow, old seeds) to 0.4-0.7s / 0.6-0.85s (new flow, new seeds). The measurement tool is committed astest/performance/startup_timing_real_test.dart. - Startup connection overhaul: the Duniter node and the Squid indexer now
come up in about a second on a healthy network instead of many seconds,
and a single dead or slow endpoint no longer delays (or sinks) the whole
connection. Four structural fixes, each covered by deterministic tests
that drive the real connection stack against fake local endpoints
(
test/services/connection/startup_latency_test.dart):RaceToFirstSuccess.raceWithMinWaitgains a real early exit: it returns atfirst success + minWait(or when every operation settled) instead of waiting for the slowest operation or the global timeout. A dead bootstrap endpoint used to hold the Duniter connection to its own 3s+ timeout. The Duniter race additionally requires a quorum of 2 successful results (minResults) before the window may close, so freshness selection stays comparative: a fast but stale node cannot win unopposed while healthier nodes are still answering. Deliberate trade-off versus the legacy exhaustive wait: the ranking rule is identical (not-syncing, then highest block, then peers, then latency) but the compared sample is the endpoints that answered within the window, no longer every endpoint that answered before the 8s timeout.- The winning RPC endpoint's test WebSocket is promoted to the real
connection (
TestResult.provider, kept alive on demand): the second handshake and its re-verification RPCs are gone from the critical path. Losing providers are always disconnected, including late finishers. - Squid endpoints are validated by ONE combined GraphQL request
(
EndpointTester.probeSquidEndpoint: reachability + indexer version + schema probe + 5 recent blocks with hashes) instead of up to five sequential queries, andSquidClientManager.initnow trusts a single pre-validated endpoint (skipSyncValidation+ one endpoint) instead of re-validating the entire endpoint list behind a 15s wait bounded by the slowest endpoint, which could throw away a connection already won. SquidConnectoris split intoprepare()(probe race, needs nothing from Duniter) andfinalize()(strict hash sync acceptance): the orchestrator runs the probes IN PARALLEL with the Duniter connection and accepts the best candidate against fresh Duniter blocks with zero extra requests in the nominal case. Strict sync validation, custom-endpoint precedence and the background peering discovery (the wide network view) are unchanged. Because prepare() can now run while the device is still offline, an unreachable user-configured endpoint is NOT cleared there: finalize(), which only runs once Duniter proved the network is up, re-tests it and either honors it or clears it (the decision the legacy flow made in the same network-proven context). Trusted endpoints (user-fixed, local dev) no longer overwrite the discovery fast cache with themselves, so returning to auto-discovery within the cache window cannot re-select the endpoint the user just removed.- Also fixed on the way: the post-connect storage subscriptions are now
awaited inside their guard, so a subscription error is logged instead of
surfacing as an unhandled zone error; the fast-endpoint cache stores only
endpoints that passed the test (winner first) instead of every tested
endpoint including dead ones; the dead legacy
ConnectionManager(pre-orchestrator, 1900 lines, imported by nothing) is removed.
1.4.0 #
- Toolchain and dependency refresh, no API change. Built and tested against
Flutter 3.44.8 (Dart 3.12.2). The declared floors now match what the package
resolves to:
objectboxandobjectbox_flutter_libs5.3.2,flutter_secure_storage10.3.1,multiformats1.4.0,logger2.7.0,graphql5.2.4,collection1.19.1,unorm_dart0.3.2. Dev dependencies follow:test1.31.0,build_runner2.15.3,lints6.1.0,graphql_codegen3.0.2,meta1.18.0. - The
analyzer: ^9.0.0override is kept and now states its reason: analyzer 13.1.0 and later requiremeta ^1.18.3, whileflutter_testfrom the Flutter 3.44.8 SDK pins meta 1.18.0, so version solving fails without it.
1.3.0 #
- Identity-name lookup over the on-chain
IdentitiesNamesmap:DuniterStorageService.getIdentityIndexByName(name)for an exact match, andgetIdentityIndexByNameMulti(names)for a batched one (singlemultiIdentitiesNamesRPC, order preserved, result padded to the request length). Both returnnullfor a free name and degrade tonullon a WebSocket drop rather than throwing. Lets a client treat the on-chain identity name as authoritative over a self-declared profile name.MockDuniterStorageServicegainssetReservedNameand anameMultiLookupCountcounter so batching can be asserted in tests. DuniterStorageService.accountSurvivesAtZero(address): whether an account keeps existing once emptied. True whensufficients > 0, which on Duniter v2s means the account backs an identity (pallet_identity::create_identitycallsinc_sufficientson the owner key). Such an account may go below the existential deposit without being reaped, so a client can offer the full balance instead of refusing the amount or burning the change.DuniterService.paytakes akeepAliveflag (defaulttrue, unchanged behaviour for Ğ1 amounts).falseselects the chain'sExpendablepreservation:transferAllowDeathinstead oftransferKeepAlive, andtransferUdinstead oftransferUdKeepAlive. This also aligns the two units: UD amounts previously always usedtransferUd(Expendable) while Ğ1 amounts usedtransferKeepAlive(Preserve), so the existential-deposit rule silently depended on the display unit.
1.2.1 #
- Fix macOS biometrics:
BiometricServicenow stores the encrypted PIN in the platform-aware secure storage (file-based on macOS, where the Keychain is not reliably available via Flutter plugins) instead of the raw Keychain plugin. Readable legacy Keychain enrollments are migrated on first use; broken ones simply require re-enrolling. New public API:CryptoUtils.secureRead/secureWrite/secureDelete/secureContainsKey/secureReadAll.
1.2.0 #
- Contacts sync foundation (Cesium+ pods): CRDT model
ContactsSyncState/ContactSyncEntry(per-field last-write-wins, commutative merge, non- destructive removal viaisContact, dead-entry GC, forward-compatible unknown fields) and encrypted-blob pod I/OgetContacts/saveContacts(cross-preserves the co-hosted certification queue).
0.6.0 #
- Bulk storage support
- Improved network scan
- Connection orchestrator improvements
0.3.0 #
- Implement all Gecko v2s requirements
0.1.0 #
- Migrate files from Ğazelle to split responsabilities