178 PRs promoted from integration/all-fixes to main.
- Release PR: #345
- Tag points at:
af41bea6fc36 - Period covered: ~4 months of accumulated profile-layer, UXF, recovery, and connectivity work since the previous release lineage (PRs #128–#130 → #303–#342).
⚠️ Breaking: UXF wire-shape default flip
Default sends now emit UXF v1.0 bundles (senderUxf=true). Older SDK receivers cannot decode them. Pin a shared SDK version across senders/receivers during the transition, or pass explicit features: { senderUxf: false } to fall back to the legacy single-token TXF wire shape. See docs/uxf/UXF-TRANSFER-CUTOVER-RUNBOOK.md for the runbook.
Changed (BREAKING — wire-shape default flip)
- UXF feature flags now default-ON (T.8.D part 1 of 2 — production cutover, NO legacy code path removal). All four UXF feature flags moved from default
false→ defaulttrueinPaymentsModuleConfig.features:senderUxf—payments.send({transferMode:'instant'})(the public default) now routes through the new UXF instant-sender; conservative-mode also routes through the UXF orchestrator.recipientUxf— incoming UXF v1.0 bundles enqueue onto the bounded ingest worker pool when one is installed (no behavior change otherwise — pool isnulluntil bootstrap wires it).recipientLegacyAdapter— inbound legacy wire shapes (Sphere TXF / V6 / V5 / SDK legacy) are adapted to UXF-shapedDispositionRecords and routed through the disposition writer when a runner is installed. REQUIRED ON for cross-version interop with old senders.recoveryWorker— sending-recovery worker installs and starts oninitialize()when a republish hook has been wired (no behavior change otherwise — worker isnulluntil bootstrap installs it).- Cross-version interop caveat: a sender with
senderUxf: trueemits UXF v1.0 wire shapes (uxf-cid/uxf-car); a receiver running an older SDK without UXF ingest CANNOT decode them. Pin a shared SDK version across senders/receivers during the transition, OR pass explicitfeatures: { senderUxf: false }to fall back to the legacy single-token TXF wire shape. Testnet soak is recommended before mainnet rollout. Seedocs/uxf/UXF-TRANSFER-CUTOVER-RUNBOOK.mdfor the operator runbook and back-out procedure. - Legacy code path removal is T.8.D part 2 of 2 (deferred until soak validation completes). Until then, every flag's
falsevalue remains a fully-supported escape hatch.
Added
SPHERE_IPFS_GATEWAYenv override — single URL or comma-separated list that replacesDEFAULT_IPFS_GATEWAYSat module-init. Honored byNETWORKS[*].ipfsGateways,getIpfsGatewayUrls(), andIpfsStorageProvider. Lets e2e suites survive testnet IPFS gateway outages (#154) by pointing at a public/alternate gateway. Node-only; gated ontypeof processso it's a no-op in browser bundles.BUILTIN_IPFS_GATEWAYSis exported (from the package root) as the static (pre-override) default for consumers that need to compare. (6 unit tests intests/unit/constants.ipfs-gateway-env.test.ts.)- UXF Inter-Wallet Transfer Protocol — spec + implementation (51 of 52 plan tasks landed across 12 waves; T.8.D part 1 of 2 — flag-flip production cutover — landed in this release; T.8.D part 2 of 2 — legacy code path removal — gated on testnet soak. See
docs/uxf/UXF-TRANSFER-PROTOCOL.mdfor the canonical spec anddocs/uxf/UXF-TRANSFER-IMPL-PLAN.mdfor task breakdown):payments.importInclusionProof(tokenId, proofBytes, options)— operator escape hatch with 10 sub-cases (1, 2, 3, 4a, 4b, 5, 6, 7, 8, 9 per spec §6.3).options.allowInvalidOverride: trueflips a_invalidtoken back tovalid(case 5) or re-queues the K-1 remaining txs (case 6); cases 8/9 short-circuit even with override if the supplied proof doesn't pass verification. Override path stampsoverrideAppliedAt/overrideAppliedByaudit-trail fields on the manifest entry (sticky across CRDT merges) and emitstransfer:override-applied.payments.revalidateCascadedChildren(parentTokenId)— transitively re-evaluates dispositions for tokens whosesplitParentchain leads back to a previously-cascaded parent (e.g. after operator override unblocks a parent). Bounded depth (MAX_CHAIN_DEPTH=64); per-call-stack visited-set defends against corrupted manifest cycles.- 13 new
transfer:*events onSphereEventMap:transfer:submitted(instant-mode publish ack, distinct fromtransfer:confirmed),transfer:cascade-risk-warning(pending source produces freshly-minted child),transfer:cascade-failed(downstream notification on hard-fail),transfer:trustbase-warning(first NOT_AUTHENTICATED, refresh-and-retry),transfer:security-alert(§6.3 forbidden case OR sustained NOT_AUTHENTICATED post-refresh),transfer:proof-superseded(newer proof replaces attached proof per BFT round, W16),transfer:override-applied(importInclusionProof override fired),transfer:operator-alert('client-error'reason path, C13),transfer:fetch-failed(CID gateway-walking exhausted, W13 — NO disposition record written),transfer:ingest-queue-full(worker pool back-pressure),transfer:capability-warning(peer's wireProtocols / assetKinds mismatch outbound — informational only, no auto-coercion). - ConnectHost
IntentSchemaVersion—connect/host/ConnectHost.tsnow passes a 4th argumentschemaVersion: 'uxf-1' | 'legacy'to theonIntentcallback (default'legacy';'uxf-1'triggered by explicitparams.schemaVersion, non-emptyadditionalAssets[], or bundle envelope fields). Backward-compatible — 3-argument integrators keep working. Pure detector exported asdetectIntentSchemaVersion(). Seedocs/uxf/CONNECT-HOST-MIGRATION-NOTE.mdfor cross-repo migration (agentsphere, sphere, openclaw-unicity). - Multi-asset send —
TransferRequest.additionalAssets?: AdditionalAsset[]discriminated union ({kind:'coin', coinId, amount} | {kind:'nft', tokenId}) enables multi-coin and mixed coin+NFT transfers in a singlepayments.send()call. Per-kind validation: distinct coinIds incl. primary; distinct NFT tokenIds; coin amounts > 0. Forward-compat: receivers reject unrecognizedkindwithUNKNOWN_ASSET_KIND. See UXF-TRANSFER-PROTOCOL §4.1 anddocs/INTEGRATION.md. - Multi-asset send —
TransferRequest.additionalAssets?: AdditionalAsset[]discriminated union ({kind:'coin', coinId, amount} | {kind:'nft', tokenId}) enables multi-coin and mixed coin+NFT transfers in a singlepayments.send()call. Per-kind validation: distinct coinIds incl. primary; distinct NFT tokenIds; coin amounts > 0. Forward-compat: receivers reject unrecognizedkindwithUNKNOWN_ASSET_KIND. See UXF-TRANSFER-PROTOCOL §4.1 anddocs/INTEGRATION.md. - Canonical NFT model — NFT = token with empty/null
coinData(after zero-amount pruning); coin = non-empty. Class-disjoint at the protocol level. NFT transfers are whole-token (no split,tokenIdpreserved). Coin tokens cannot satisfy NFT targets even on tokenId match →INSUFFICIENT_BALANCEreason='nft-not-owned'. See UXF-TRANSFER-PROTOCOL §4.1. - Chain mode opt-in —
TransferRequest.allowPendingTokens?: boolean(defaultfalse). Whentrue, the source-token selector may spill over topendingtokens after exhaustingvalidones. Strict ordering: finalized-first, then pending-by-age. See UXF-TRANSFER-PROTOCOL §2.3 + §2.5. confirmNftPendingflag — requiredtruewhenallowPendingTokens: trueAND any NFT target's source has unfinalized predecessor txs. Prevents accidental cascade of irrecoverable NFT identity (NFT_PENDING_REQUIRES_CONFIRMATIONrejection without it). See UXF-TRANSFER-PROTOCOL §4.1 cascade-asymmetry warning.- Identity-binding capability hints — optional
wireProtocols: string[]andassetKinds: string[]for forward-compat. Informational only — receivers still apply the strictUNKNOWN_ASSET_KINDreject rule. See UXF-TRANSFER-PROTOCOL §10.4. - Bundle ingest concurrency — recipient runs a
MAX_INGEST_WORKERS = 16default worker pool with a bounded ingest queue. DoS defense against rogue long-running bundles. Per-tokenId mutex coordinates cross-bundle conflicts. See UXF-TRANSFER-PROTOCOL §5.0. _auditcollection — NEW (Wave T.3). Multi-representation aware: keyed by${addr}.audit.${tokenId}.${observedTokenContentHash}. StoresNOT_OUR_CURRENT_STATEandUNSPENDABLE_BY_USdispositions distinct from cryptographically broken tokens (which stay in_invalid, also widened to multi-representation key). See UXF-TRANSFER-PROTOCOL §5.4.- Periodic rescans — two orthogonal scanners promoted to in-scope (design summary): profile-pointer rescan (default 30s, detects sibling-instance updates) and per-token spent-state rescan (default 5 min/token, detects off-record spends). See UXF-TRANSFER-PROTOCOL §12.3.
- Transfer error model — canonical against
@unicitylabs/state-transition-sdk:REQUEST_ID_MISMATCHat submit = double-spend signal; sustainedPATH_NOT_INCLUDEDpastPOLLING_WINDOW(default 30 min) = oracle rejected;PATH_INVALID/NOT_AUTHENTICATEDretry up toMAX_PROOF_ERROR_RETRIES. Threat model: aggregator faulty-not-hostile; explicit threat boundary in §9.4.1. - Most-recent-proof rule — same
requestId+ same value can have multiple valid proofs across BFT rounds; canonicalize by latest BFT round (supersedes the considered-and-rejected lex-min-CID rule for proofs; lex-minbundleCidstill governs divergent-chain tie-breaks per UXF-TRANSFER-PROTOCOL §5.3 [D-conflict]). Two proofs for samerequestIdwith different values →transfer:security-alert(single-spend violation; out-of-scope hostile path). See UXF-TRANSFER-PROTOCOL §6.3. - Outbox CRDT invariants — three-tier state partition (active / soft-terminal / hard-terminal); Lamport clock with
max(local, observed)+1rule;overrideAppliedsticky flag for operator-import override stickiness; two-setcommitmentRequestIds(outstanding + completed) preventing finalized-then-re-added re-submission. See UXF-TRANSFER-PROTOCOL §7.1. - Operator escape hatches —
payments.importInclusionProof(tokenId, proofBytes, {allowInvalidOverride?})with 10-case enumeration (cases 1, 2, 3, 4a, 4b, 5, 6, 7, 8, 9);revalidateCascadedChildren(parentTokenId)(transitive). See UXF-TRANSFER-PROTOCOL §6.3 + §6.1.1.
cacheMessagesoption for CommunicationsModule —communications: { cacheMessages: false }inSphereInitOptionsdisables DM caching in memory and storage. Messages still flow throughonDirectMessage()handlers andmessage:dmevents, but are never stored. Useful for anonymous/ephemeral agents (e.g. LLM bots) that only need streaming DM reception.sendDM()still works but doesn't cache the sent message. Deduplication is skipped when caching is disabled.- Message signing —
signMessage(),verifySignedMessage(),hashSignMessage()crypto functions for secp256k1 ECDSA with recoverable signatures (Bitcoin-like double-SHA256 withSphere Signed Message:\nprefix).Sphere.signMessage(message)instance method encapsulates private key access.SIGNING_ERRORadded toSphereErrorCode.SphereInstanceinterface in ConnectHost extended withsignMessage. 22 unit tests covering signing, verification, round-trips, tampering detection, and edge cases. - Centralized logger —
loggersingleton withdebug/warn/errorlevels,globalThis-based state sharing across tsup bundles, per-tag control (logger.setTagDebug('Nostr', true)), and custom handler support SphereErrorwith typed error codes — All SDK methods throwSphereErrorwith a typed.codefield (SphereErrorCode). 15 error codes:NOT_INITIALIZED,ALREADY_INITIALIZED,INVALID_CONFIG,INVALID_IDENTITY,INSUFFICIENT_BALANCE,INVALID_RECIPIENT,TRANSFER_FAILED,STORAGE_ERROR,TRANSPORT_ERROR,AGGREGATOR_ERROR,VALIDATION_ERROR,NETWORK_ERROR,TIMEOUT,DECRYPTION_ERROR,MODULE_NOT_AVAILABLEisSphereError()type guard — Helper function for typed error handling in catch blocks- Silent failure logging — All previously silent
.catch(() => {}), empty catch blocks, and timeout-based silent failures now log vialogger.warn(operational issues) orlogger.debug(expected/non-critical) - 20 unit tests for logger module
- IPNS push-based sync via WebSocket —
IpnsSubscriptionClientconnects to/ws/ipnson IPFS gateways for real-time IPNS update notifications, with exponential backoff reconnection (5s→60s capped) and 30s keepalive pings - Fallback HTTP polling — When WebSocket is unavailable, the IPFS provider automatically polls for IPNS changes at a configurable interval (default: 90s)
- Auto-sync on import —
Sphere.import()automatically syncs with all registered token storage providers after initialization to recover tokens from IPFS - Debounced auto-sync on remote updates —
PaymentsModulesubscribes tostorage:remote-updatedevents from token storage providers and performs a debounced (500ms) sync, emitting a newsync:remote-updatesphere event storage:remote-updatedstorage event type — New event emitted byIpfsStorageProviderwhen a remote IPNS change is detected via WebSocket push or HTTP pollingsync:remote-updatesphere event — New top-level event with{ providerId, name, sequence, cid, added, removed }payload, emitted after a push-triggered sync completes- WebSocket factory injection in platform factories —
createNodeIpfsStorageProvider()andcreateBrowserIpfsStorageProvider()now automatically inject platform-appropriate WebSocket factories IpfsHttpClient.getGateways()— New public accessor returning configured gateway URLsIpfsStorageConfigextensions — New optional fields:createWebSocket,wsUrl,fallbackPollIntervalMs,syncDebounceMsIpnsUpdateEventtype — Exported fromimpl/shared/ipfsfor consumers- 24 unit tests for
IpnsSubscriptionClientcovering subscribe, message handling, reconnection, keepalive, fallback polling, and disconnect
Fixed
- IPFS token recovery via TXF merge —
mergeTxfData()now recognizes individual token entries (token-*keys) stored viasaveToken(), not just_-prefixed TXF keys; previously IPFS sync returnedadded: 0because merge couldn't find tokens in the blob - TXF parser handles individual file format —
parseTxfStorageData()now extracts tokens from{ token, meta }wrapper format used by IPFS individual token storage - Sync coalescing —
PaymentsModule.sync()now coalesces concurrent calls, preventing race conditions when multiple syncs overlap
Changed
- Types —
DEFAULT_IPFS_GATEWAYSwidened toreadonly string[](previouslyreadonly ['https://unicity-ipfs1.dyndns.org']literal tuple). Required to accommodate the newSPHERE_IPFS_GATEWAYenv override (see Added). No runtime behavior change. Consumers relying on the literal-tuple type should switch toBUILTIN_IPFS_GATEWAYS(which retains theas constshape). - All
throw new Error()in production code replaced withthrow new SphereError()— zero plain errors remaining - All
console.log/warn/errorin production code replaced withlogger.debug/warn/error— console output controlled by debug flag logger.warn()andlogger.error()are always shown regardless of debug flag;logger.debug()is hidden whendebug=falsePaymentsModule.updateTokenStorageProviders()now re-subscribes to storage events when providers changePaymentsModule.destroy()now cleans up storage event subscriptions and debounce timersIpfsStorageProvider.shutdown()now disconnects the subscription client