durt2 1.10.1 copy "durt2: ^1.10.1" to clipboard
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.getCertState checked the issuer's delay before the target's status: an issuer whose next certification was not due yet got mustWaitBeforeCert for 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 handle mustConfirmIdentity.

1.10.0 #

  • A certification can be sent without the newcomer's distance evaluation (gecko#276). DuniterService.certify bundled requestDistanceEvaluationFor whenever 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 parameter requestDistanceEvaluation (default true, the behaviour so far): an app that has computed the newcomer's distance below the rule, or could not compute it, passes false and the certification is sent alone. New DuniterService.certificationRequestsEvaluation(...) says, before certifying, whether the evaluation would be bundled: the very test certify applies.
  • 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 certify counted 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.wallets no longer throws under DurtTestMode: 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) makes Durt.i.isConnected true, for code that only runs once connected. Everything WalletService reads or writes now goes through a WalletStore (ObjectBoxWalletStore in the app, InMemoryWalletStore in tests); behaviour is unchanged. New on WalletService: getAllSafes, getAllWallets, hasSafes, storeSafe and findWallet, for callers that reach safeBox and walletBox directly 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_storage on 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 (default durt2), 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 .tmp siblings included), reads each one back, destroys the files lowest priority first, and returns a LegacySecretFileReport; on a store failure it throws SecureStorageException and 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, filesConflicting names it and report.unresolved is true, as it is for an unreadable file; read that flag and tell the user, the secrets are still on disk;
    • CryptoUtils.setMacosSecureStoragePath and CryptoUtils.secureStorage are 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.
  • WalletService.checkCode() answers false for 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. A SecureStorageException now goes through; the check itself is WalletService.verifyPin(safe, pin), static, testable without a store. Callers of checkCode catch SecureStorageException and count nothing. Only the cipher's own refusal answers false; 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.decrypt then throws SecureStorageException instead of "Invalid secret code", and CryptoUtils.legacySecretFilesUnresolved tells 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 SecureStorageException instead of reading as empty. secureRead, secureWrite, secureDelete, secureContainsKey and secureReadAll logged the failure and answered null or nothing, which masterKey() 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 key masterKey() generates is read back before it is used. A caller that read null as a broken store gets the exception instead.

1.7.1 #

  • A pod outage is no longer reported as an empty document. getContacts and getCertificationQueue caught every error and answered null, 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 introduced CesiumPlusUnavailableException for exactly this distinction and fixed getProfileByAddress, but these two readers flattened it back to null one 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. null is 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 of g1.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. getBalances sent one state_call per address in a single Future.wait, so scanning 30 derivations meant 30 concurrent runtime API calls. Duniter nodes serve only 4 at a time and answer 4003 too many concurrent runtime API calls to the rest: measured on the public Ğ1 nodes, a burst of 30 lost 12 to 26 calls on g1.gyroi.de, g1.pini.fr and g1.1000i100.fr, while rpc.duniter.org and g1.axiom-team.fr answered all of them. A single rejection made the whole Future.wait fail, 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.disconnect closes 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 a TimeoutException the 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 against gtpod.elo.tf: first=500 answers, first=501 is refused) and connectionPageSize() 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 answer null for 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 that null as "this user has no description, no city, no socials" and published those absences, which uploadProfile turns 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, null on a 404 (the only case where "absent" is an answer from the pod), and the new CesiumPlusUnavailableException for 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 existing catch makes them return false without 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.testEndpoint takes an enforceMinimumVersion flag (default true, so automatic selection keeps the minimumSquidVersion floor). Callers testing an endpoint the user named themselves pass false: 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 carried indexer.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 by squid.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 to wss://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 as test/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.raceWithMinWait gains a real early exit: it returns at first 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, and SquidClientManager.init now 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.
    • SquidConnector is split into prepare() (probe race, needs nothing from Duniter) and finalize() (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: objectbox and objectbox_flutter_libs 5.3.2, flutter_secure_storage 10.3.1, multiformats 1.4.0, logger 2.7.0, graphql 5.2.4, collection 1.19.1, unorm_dart 0.3.2. Dev dependencies follow: test 1.31.0, build_runner 2.15.3, lints 6.1.0, graphql_codegen 3.0.2, meta 1.18.0.
  • The analyzer: ^9.0.0 override is kept and now states its reason: analyzer 13.1.0 and later require meta ^1.18.3, while flutter_test from 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 IdentitiesNames map: DuniterStorageService.getIdentityIndexByName(name) for an exact match, and getIdentityIndexByNameMulti(names) for a batched one (single multiIdentitiesNames RPC, order preserved, result padded to the request length). Both return null for a free name and degrade to null on a WebSocket drop rather than throwing. Lets a client treat the on-chain identity name as authoritative over a self-declared profile name. MockDuniterStorageService gains setReservedName and a nameMultiLookupCount counter so batching can be asserted in tests.
  • DuniterStorageService.accountSurvivesAtZero(address): whether an account keeps existing once emptied. True when sufficients > 0, which on Duniter v2s means the account backs an identity (pallet_identity::create_identity calls inc_sufficients on 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.pay takes a keepAlive flag (default true, unchanged behaviour for Ğ1 amounts). false selects the chain's Expendable preservation: transferAllowDeath instead of transferKeepAlive, and transferUd instead of transferUdKeepAlive. This also aligns the two units: UD amounts previously always used transferUd (Expendable) while Ğ1 amounts used transferKeepAlive (Preserve), so the existential-deposit rule silently depended on the display unit.

1.2.1 #

  • Fix macOS biometrics: BiometricService now 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 via isContact, dead-entry GC, forward-compatible unknown fields) and encrypted-blob pod I/O getContacts / 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