This is the authoritative contract for @dignetwork/dig-sdk's wallet-connector surface —
ChiaProvider, the two WalletTransport backends (injected window.chia / WalletConnect→Sage),
and the connector-selection API (ConnectOptions, ChiaProvider.listConnectors). An independent
reimplementation of this surface MUST behave as described here. Keywords MUST, MUST NOT,
SHOULD, and MAY are used in the RFC 2119 sense. Field/type names are the exported public
surface and are stable contracts.
The SDK's other pillars — DigClient (read-crypto), Paywall (monetization), the /spend
CHIP-0035 re-export, and the Vite/Next framework adapters — are documented in README.md; their
normative contracts land in this file as they are substantially touched. The read-crypto surface
DigClient consumes is normatively specified in §7.
A WalletTransport is the low-level channel ChiaProvider issues CHIP-0002 RPCs through. Exactly
two backends exist:
backend |
Description |
|---|---|
"injected" |
The DIG Browser's in-process wallet (or a compatible CHIP-0002 extension) exposed as window.chia. No relay, no pairing, no QR. |
"walletconnect" |
WalletConnect v2 → Sage, over the WalletConnect relay. Requires @walletconnect/sign-client (optional peer dependency). |
Every WalletTransport implementation MUST expose:
| Member | Contract |
|---|---|
backend |
The WalletBackend this transport is ("injected" | "walletconnect"), fixed at construction. |
chain |
The CAIP-2 chain id this transport is bound to. |
topic |
A session identifier: the real WalletConnect relay topic, or the fixed sentinel "injected" for the injected backend. |
supports(method): boolean |
True iff the active session grants method. An empty/unknown grant set MUST be treated as "granted" (fail open on capability, fail closed on the actual RPC). |
request(method, params): Promise<unknown> |
Issue one CHIP-0002 RPC. MUST reject with a DigSdkError (never a bare Error) when the method is unsupported or the transport fails. |
disconnect(): Promise<void> |
Best-effort teardown. MUST NOT throw for an already-torn-down session. |
- Detection (
isInjectedAvailable) keys on the unspoofableisDIGmarker the DIG Browser sets on itswindow.chiaprovider, NOT merely the presence ofwindow.chia— a different Chia provider could also define that global.isInjectedAvailable({ anyChia: true })widens detection to any object atwindow.chiaexposing arequestfunction. connect(eager)MUST call the provider's ownconnect(eager)when present, blocking until the user approves/rejects the origin. A provider without aconnectmethod (an older build) MUST be tolerated —request()gates capability per-method instead.supports(method)is a static allowlist overWALLET_METHODS— the injected wallet returns the full canonical method set (Sage-shaped responses), so there is no per-session negotiation.topicis always the fixed sentinel"injected"(there is no relay topic for this backend).
optionalNamespacesONLY — Sage rejectsrequiredNamespaces. The namespace advertises the fullWALLET_METHODSset for the configuredchain.- Every
request()races the underlying WC request against a per-request timeout (requestTimeoutMs, default60_000ms) and rejectsWALLET_TIMEOUTon expiry (a backgrounded mobile Sage can otherwise hang forever). request()retries ONLY a transient relay-PUBLISH failure (the request never reached Sage), up to 3 attempts with linear backoff (1200ms * attempt). A response timeout or a wallet/user rejection MUST propagate immediately — a retry after Sage already surfaced a prompt would double-prompt the user.restore(options)reconnects to an existing WC session (most-recent-first) that grants at least oneSIGN_METHODSentry — a session that cannot sign is useless to the SDK's normalized surface and MUST be skipped.- The
@walletconnect/sign-clientimport is dynamic (lazy) so the rest of the SDK loads without it; a missing/malformed module surfaces asWC_DEPENDENCY_MISSING, never a raw import error.
ChiaProvider normalizes both transports behind one CHIP-0002 surface (getAddress,
signMessage, signCoinSpends, takeOffer, balances, coins, request/supports escape hatches,
disconnect). It is constructed ONLY via ChiaProvider.connect(...) or
ChiaProvider.fromTransport(...) — there is no public constructor.
mode value |
Resolution | Backward compatibility |
|---|---|---|
"auto" (default when mode is omitted) |
Try the injected transport first; if unavailable, fall back to WalletConnect. Never asks the caller. | The pre-#63 default; unchanged. |
"injected" |
Require the injected transport. Reject NO_INJECTED_WALLET if unavailable. |
Pre-#63 value; unchanged. |
"browser-wallet" |
Alias of "injected" — identical resolution and identical NO_INJECTED_WALLET failure. Exists as the chooser-facing connector id (see §3). |
Added by #63; purely additive. |
"walletconnect" |
Require the WalletConnect transport. Reject WC_OPTIONS_REQUIRED if walletConnect options are absent. |
Unchanged. |
connect() MUST normalize "browser-wallet" to the same code path as "injected" before
dispatch; the two values MUST NEVER diverge in behavior. Whichever value the caller passed MUST be
echoed back verbatim in a thrown DigSdkError's context.mode (not the normalized value), so a
caller that passed "browser-wallet" sees "browser-wallet" in the error, not "injected".
"auto" MUST remain the default and MUST remain a silent (non-choice-presenting) resolution — it
exists for callers that target exactly one wallet and don't want a chooser. It MUST NOT be changed
to prompt, block, or otherwise diverge from its pre-#63 behavior; doing so would be a breaking
change requiring a major version bump (§5.1 of the ecosystem contract governs the bar for that).
Successfully connecting via any mode value MUST yield a ChiaProvider exposing the identical
normalized CHIP-0002 surface — a dapp's post-connect code path MUST NOT need to branch on which
connector was used (only provider.backend differs, "injected" for both "injected" and
"browser-wallet").
provider.backend reports the underlying WalletBackend ("injected" | "walletconnect") —
this is the transport identity, and is not affected by which mode alias connected it (a
"browser-wallet" connect reports backend: "injected", matching a plain "injected" connect
byte-for-byte). provider.session returns { backend, chain, topic, address }.
ChiaProvider.listConnectors(options?: { acceptAnyInjected?: boolean }): ConnectorInfo[] is the
discoverable enumeration a 'Browser Wallet vs WalletConnect' chooser UI renders from.
- MUST be synchronous and MUST NOT connect, negotiate, or otherwise mutate wallet/session state —
it is a pure detection query. A caller invoking
listConnectors()alone MUST NOT cause any wallet RPC, injected-providerconnect()call, or WalletConnect pairing to occur. - MUST always return exactly two entries, in this fixed order:
{ id: "browser-wallet", backend: "injected", label: "Browser Wallet", available }{ id: "walletconnect", backend: "walletconnect", label: "WalletConnect", available: true }
browser-wallet.availableMUST equalisInjectedAvailable({ anyChia: options?.acceptAnyInjected })evaluated at call time (re-evaluate on every call — no caching — since injection can appear after page load, e.g. an extension finishing its own startup).walletconnect.availableMUST always betrue— WalletConnect has no local presence to detect (availability is a relay-reachability question resolved only once pairing is attempted), so it is always offered as a choice.labelvalues ("Browser Wallet","WalletConnect") are the canonical chooser copy shared with the hub's own chooser (ecosystemSYSTEM.md→ canonical terminology). A consuming UI SHOULD use these labels verbatim (subject to its own i18n layer) rather than inventing new copy.- Each
ConnectorInfo.idMUST be a validConnectOptions.modevalue — passingchosen.idstraight through asmodeMUST connect via that exact connector with no further mapping required by the caller.
- No persistence. The SDK holds no storage; a caller that wants to pre-select the user's last
choice next session MUST persist
chosen.iditself (e.g.localStorage) and MUST still let the user change it — the SDK does not gate re-choosing. - No auto-connect.
listConnectors()never transitions intoconnect(); the caller decides when (and whether) to callconnect({ mode: chosen.id })after the user picks.
Every failure on this surface is a DigSdkError (never a bare Error) with a stable UPPER_SNAKE
.code plus structured .context. The catalogue is exhaustively listed in README.md §"Error
codes" and mirrored by capabilities().errorCodes; the codes this surface can throw are:
| Code | Thrown when | Context |
|---|---|---|
NO_INJECTED_WALLET |
mode: "injected" or mode: "browser-wallet" found no usable window.chia. |
mode (the caller's raw value), acceptAnyInjected |
WC_OPTIONS_REQUIRED |
WalletConnect was needed (mode: "walletconnect", or the WC leg of "auto") but no walletConnect options were supplied. |
mode (the caller's raw value) |
WC_DEPENDENCY_MISSING |
The optional @walletconnect/sign-client peer dependency is not installed/usable. |
— |
METHOD_NOT_SUPPORTED |
The active transport/session does not grant the requested CHIP-0002 method. | method |
WALLET_TIMEOUT |
A WalletConnect RPC exceeded requestTimeoutMs without a response. |
method, timeoutMs |
isDigSdkError(e, code?) is the required narrowing check (brand-based, not instanceof) since the
SDK ships several independently-bundled entry points that each inline their own DigSdkError
class identity.
- Every
ConnectOptionsfield and everymodevalue that existed before #63 ("auto","injected","walletconnect",walletConnect,chain,acceptAnyInjected) MUST continue to resolve identically. A caller who has never heard oflistConnectors()or"browser-wallet"MUST see no behavior change. - New connector-selection surface (
"browser-wallet",listConnectors) is strictly additive — it MUST be reachable without touching any existing call site, and removing it would be a breaking (major) change. provider.backendvalues ("injected"|"walletconnect") are a stable contract other code (persisted sessions, analytics, hub-side chooser logic) may branch on; they MUST NOT be renamed to the connector ids ("browser-wallet") — the two vocabularies (backend vs connector) are intentionally distinct and MUST stay so.
- The chooser labels (
"Browser Wallet","WalletConnect") and the underlying dual-transport policy MUST agree with the hub's own Connect chooser and with docs.dig.net's integration guide — a drift between the SDK's connector ids/labels and the hub's UI copy is a bug in whichever side is stale. WALLET_METHODS/SIGN_METHODS(the CHIP-0002 method surface both transports negotiate) are defined once insrc/methods.tsand MUST be identical for both transports — a dapp's method call MUST behave the same regardless of which connector is active.
DigClient is the read side of the SDK: it derives a resource's keys CLIENT-SIDE, fetches opaque
ciphertext + inclusion proofs from the dig RPC, verifies inclusion against a caller-supplied
on-chain root, and decrypts — so the serving host stays BLIND. The values in this section are
byte-identical cross-repo contracts: the URN grammar, the retrieval-key derivation, and the
JSON-RPC wire shapes MUST match dig-store's dig-resolver/dig-client-wasm, the dig-node RPC, and
the shared definitions in the ecosystem SYSTEM.md (→ "URN scheme", "content-read RPC"). This
section DOCUMENTS them; an implementation MUST NOT diverge from SYSTEM.md.
A DIG resource is addressed by a URN of the exact form:
urn:dig:chia:<store_id>[:<root>]/<resource_key>[?salt=<hex>]
<store_id>and the optional<root>are each exactly 64 lowercase-normalized hex chars (a 32-byte SHA-256).parseUrnaccepts mixed case and normalizes; emitters MUST lowercase.<root>is OPTIONAL and, when present, is ONLY a trust anchor (which generation to verify against) — it MUST NOT affect the retrieval or decryption keys (§7.2).<resource_key>is the path within the store; an empty resource key resolves to the default viewindex.html.?salt=<hex>carries the private-store secret salt (lowercased); absent for a public store.
The canonical, root-INDEPENDENT form is urn:dig:chia:<store_id>/<resource_key> — the form
whose bytes seed key derivation. reconstructUrn(storeId, resourceKey) produces it;
reconstructUrnWithRoot(...) produces the root-pinned DISPLAY form urn:dig:chia:<store_id>:<root>/ <resource_key>, which is for sharing only and MUST NOT be fed into key derivation.
The retrieval key is derived purely locally (no network) as:
retrieval_key = SHA-256(canonical rootless URN) // lowercase hex, 64 chars
i.e. the SHA-256 of urn:dig:chia:<store_id>/<resource_key> (empty key ⇒ index.html). It is
root-independent — the same resource in any generation of the store maps to the same retrieval
key — and is computed by the read-crypto wasm (retrievalKey(storeIdHex, resourceKey)). The AES-256
content key is likewise derived from the canonical rootless URN (plus the salt for a private store)
by the wasm and MUST NOT depend on the root.
DigClient calls the dig RPC over JSON-RPC 2.0 (POST, { jsonrpc:"2.0", id, method, params }). A
transport failure is RPC_TRANSPORT, an HTTP/JSON-RPC error is RPC_ERROR, and a structurally
absent/inconsistent result is RPC_MALFORMED_RESPONSE (§4 catalogue). A read NEVER concludes "not
found": the oblivious host returns indistinguishable ciphertext for any key, so a missing resource
is just opaque bytes that fail to decrypt.
dig.getContent — stream one resource's ciphertext by retrieval key.
- Params:
{ store_id, root, retrieval_key, offset, length }(all hex/number;rootis the trust anchor,retrieval_keyper §7.2). - Result:
{ total_length, offset, next_offset, complete, ciphertext, inclusion_proof, chunk_lens }—ciphertextis standard base64;chunk_lensthe per-chunk plaintext lengths. - Chunking: the client requests
length = 3 MiB(3 * 1024 * 1024bytes) per call — the backend caps each response at 3 MiB (the Lambda/API-Gateway response ceiling) — and reassembles by looping untilcompleteis true ornext_offsetis null, writing each chunk at its returnedoffsetinto atotal_lengthbuffer. This 3-MiB cap is the shared contract with the RPC.
dig.getCollection — read a collection's public, owner-independent facts.
- Params:
{ launcher_ids: string[], did? }— the item set is keyed by NFT launcher ids (the owner-independent anchor), NOT the creator DID;did(optional) is echoed back as the declared creator. - Result: a
CollectionMeta(creator DID, resolved item count, uniform royalty basis points).
dig.listCollectionItems — read a deterministic paginated page of a collection's items, each
resolved to its CURRENT on-chain state (current owner, royalty, CHIP-0007 metadata).
- Params:
{ launcher_ids: string[], offset?, limit? }— items return in input launcher-id order;limitis clamped to the server cap (200);offsetdefaults to 0. - Result: a
CollectionItemsPage—itemsplusnext_offset(null on the last page).
- Blind host / no presence oracle. The trust ROOT is always caller-supplied (resolved from the chain); the host is never the trust anchor. Because the host returns indistinguishable ciphertext for any retrieval key, resource presence is UNKNOWABLE from a read.
- wasm integrity is per-load-path (from #1156 finding 2 — mirrors
src/loader.ts+src/wasm.ts):- Byte-level SRI (fail-closed) on the Node path and on any caller-supplied
configureWasm({ wasmBytes | wasmUrl })path: the loader SHA-256s the raw wasm bytes, compares them against the pinned digest (DIG_CLIENT_WASM_SHA256, mirrored by the package'sintegrity.json), and refuses to run on a mismatch. - Pinned-package trust on the DEFAULT browser (bundler) path: the bundler resolves
@dignetwork/dig-capsule-wasm/weband instantiates the pinned package artifact, so the trust anchor there is the package supply chain — NOT byte-level SRI. An app on an untrusted delivery path opts into byte-level SRI withconfigureWasm({ wasmUrl }).
- Byte-level SRI (fail-closed) on the Node path and on any caller-supplied