@donezone/client
v0.1.55
Published
Client library for interacting with Done contracts from JavaScript/TypeScript (browser, Node, Bun). Handles envelope construction, signing, transport to the Done HTTP gateway, and SSE event subscriptions.
Readme
@donezone/client
Client library for interacting with Done contracts from JavaScript/TypeScript (browser, Node, Bun). Handles envelope construction, signing, transport to the Done HTTP gateway, and SSE event subscriptions.
Table of Contents
- Concepts
- Installation
- The
DoneFacade - DoneContractClient
- DoneBackendClient
- Envelope Builders
- Low-Level Primitives
- Message Builders
- Types Reference
Concepts
Every write operation on a Done contract requires an auth envelope — a signed payload that wraps one or more CosmWasm messages with identity, nonce, expiry, and signatures. This package handles building those envelopes, signing them (via pluggable signer callbacks), and submitting them to POST /tx on the Done HTTP gateway. Read operations are plain GET /query/{contract}?msg=... calls with no signing required.
Call flow for a transaction:
Done.run(url, opts)
→ buildTransactionMessage (produces WasmMsg::Execute)
→ EnvelopeBuilder callback (user provides signing logic)
→ backend.executeEnvelope (POST /tx)Call flow for a query:
Done.query(url, opts)
→ buildQueryMessage (serialises path/body/query)
→ GET /query/{contract}?msg=...Call flow for events:
Done.subscribe(eventsUrl, topic, handler)
→ EventSource SSE connection
→ JSON-parsed events forwarded to handlerInstallation
bun add @donezone/client
# or
npm install @donezone/clientThe Done Facade
Done is a pre-configured singleton exported from the package. Most applications only need this.
import { Done, signDocDigest } from "@donezone/client";Done.config(options)
Mutates the global Done instance. Call once at app startup. Omitted fields keep their existing values.
Done.config({
doneHttp: "https://doneHttp.done.zone", // Done HTTP gateway base URL
doneEvents: "https://doneEvents.done.zone", // Done Events SSE base URL
signer: myEnvelopeBuilder, // EnvelopeBuilder (see Envelope Builders)
fetch: customFetch, // optional custom fetch implementation
eventSource: customEventSourceFactory, // optional custom EventSource factory
});Done.run(url, options?)
Executes an authenticated transaction. Returns Promise<ExecuteResponse>.
url is resolved against doneHttp. The first path segment after the base must be the contract address, followed by the route path:
const result = await Done.run("/done1contract.../buy", {
body: { minAmountOut: "1" },
funds: { uusd: "100" }, // or [{ denom: "uusd", amount: "100" }]
memo: "optional memo",
gasLimit: 500_000,
traceId: "abc-123",
signal: abortController.signal,
});
// result: { transactionHash, height, gasUsed }Requires signer to be configured on the Done instance.
Done.query<T>(url, options?)
Issues a read-only query. Returns Promise<T>.
const price = await Done.query<{ amount: string }>("/done1contract.../price");
const orders = await Done.query("/done1contract.../orders", {
search: { limit: 10, offset: 0 }, // merged into querystring
});Done.subscribe(eventsUrl, topic, handler, options?)
Opens an SSE stream. Returns an () => void cleanup function.
eventsUrl is resolved against doneEvents. The path must include the contract address (e.g. /done1contract.../events or just the address segment the events server expects).
const stop = Done.subscribe(
"/done1contract.../events",
"PriceChange", // topic string; also accepts string[] for nested paths
(event) => {
console.log(event); // parsed JSON; raw string if parsing fails
},
{
rawEvent: true, // forward the raw MessageEvent instead of parsing
signal: ac.signal, // AbortSignal closes the subscription automatically
onError: (e) => {}, // called on EventSource error events
},
);
// later:
stop();Done.create(options?)
Returns a new isolated DoneInstance with its own settings, inheriting from the current Done configuration. Useful for multi-tenant dashboards or per-request contexts.
const staging = Done.create({
doneHttp: "https://doneHttp.staging.zone",
doneEvents: "https://doneEvents.staging.zone",
signer: stagingSigner,
});
await staging.run("/done1contract.../buy", { body: { minAmountOut: "5" } });The returned instance has the same run, query, subscribe, config, create, and contract methods as the global Done.
Done.contract(address) / Done.contract(config)
Returns a DoneContractClient (or DoneContractHandle) scoped to a specific contract. The instance inherits signer, fetch, and eventSource from the Done configuration.
// Simple: address string — inherits everything from Done config
const sale = Done.contract("done1contract123");
await sale.run("/buy", { body: { minAmountOut: "1" } });
const price = await sale.query<{ amount: string }>("/price");
// Advanced: full config object
const sale = Done.contract({
baseUrl: "https://doneHttp.done.zone",
address: "done1contract123",
buildEnvelope: customSigner,
eventsPath: "/events",
});Done.buildEnvelope(draft) / Done.signDocDigest(envelope) / Done.toSignDoc(envelope)
Convenience re-exports of the low-level primitives (see Low-Level Primitives).
DoneContractClient
A strongly typed, contract-scoped client. Instantiate directly when you need per-contract configuration (e.g. a different signer or events URL).
import { DoneContractClient } from "@donezone/client";
const client = new DoneContractClient({
baseUrl: "https://doneHttp.done.zone",
address: "done1contract123",
buildEnvelope: myEnvelopeBuilder, // required to call execute/run/post
eventsPath: "/events", // default: "/events"
eventSource: myEventSourceFactory,
fetch: customFetch,
defaultMetadata: { gas_limit: 500_000 },
});Type Safety for Transactions
execute, run, and post require buildEnvelope to be present in the config. If it is absent, TypeScript will give a compile-time error (Argument of type 'string' is not assignable to parameter of type 'never') rather than a runtime throw. Clients without buildEnvelope are limited to query, get, subscribe, and transaction.
Methods
client.query<T>(path, request?, init?)
Read-only query. Returns Promise<T>.
const state = await client.query<{ count: number }>("/state");
const orders = await client.query("/orders", { query: { limit: "10" } });client.get(path, request?, init?)
Same as query but returns a DoneRequest (a PromiseLike<Response> with convenience .json<T>(), .text(), .arrayBuffer() methods). Useful when you need the raw Response.
const res = await client.get("/orders", { query: { limit: "10" } });
const data = await res.json<Order[]>();client.execute(path, request?, init?) / .run(...) / .post(...)
Execute a transaction. All three are identical. Requires buildEnvelope in config (compile-time enforced). Returns Promise<ExecuteResponse>.
const result = await client.execute("/buy", {
body: { minAmountOut: "1" },
funds: { uusd: "100" },
memo: "purchase",
gasLimit: 300_000,
traceId: "trace-xyz",
signal: ac.signal,
});
// result: { transactionHash, height, gasUsed }client.transaction(path, request?)
Builds a PreparedTransaction (the CosmosMsg + optional metadata) without submitting it. Use this to batch multiple contract calls into a single envelope manually.
const tx = client.transaction("/buy", { body: { minAmountOut: "1" } });
// tx: { msg: CosmosMsg, metadata?: EnvelopeMetadata }client.subscribe(topic, handler, options?)
Opens an SSE subscription to {baseUrl}{eventsPath}/{address}?topic={topic}. Returns () => void.
const stop = client.subscribe("Pinged", (payload) => {
console.log(payload); // parsed JSON
});
// With options:
const stop = client.subscribe(
["namespace", "event"], // string[] encodes as JSON topic
handler,
{ onError: console.error, signal: ac.signal },
);client.publishCode(request, init?)
Publishes or updates contract code. Returns Promise<PublishCodeResponse>.
await client.publishCode({ script: contractSourceCode, msg: initMsg });client.buildEnvelope(draft) / client.signDocDigest(envelope)
See Low-Level Primitives.
DoneBackendClient
The raw transport layer. Use this when you need full control over envelope construction and submission.
import { DoneBackendClient } from "@donezone/client";
const backend = new DoneBackendClient({
baseUrl: "https://doneHttp.done.zone",
fetch: customFetch, // optional
});Methods
backend.contract(address)
Returns a DoneContractHandle with transaction, query, get, and publishCode helpers. Does not include execute/run — use backend.executeEnvelope for submission.
const handle = backend.contract("done1contract123");
const tx = handle.transaction("/buy", { body: { minAmountOut: "1" } });
// tx: PreparedTransaction — pass tx.msg into buildEnvelopebackend.executeEnvelope(envelope, options?, init?)
Submit a fully-built AuthEnvelope to POST /tx. Returns Promise<ExecuteResponse>.
await backend.executeEnvelope(envelope, {
passkey: { publicKey: "base64-encoded-public-key" },
memo: "tx memo",
signal: ac.signal,
});backend.queryContract<T>(address, call, init?)
const result = await backend.queryContract<{ price: string }>(
"done1contract123",
{ path: "/price" },
);backend.queryContractRaw(address, call, init?)
Same as queryContract but returns the raw Response.
backend.publishCode(request, init?)
await backend.publishCode({
contract: "js", // or "wasm"
script: contractSource,
msg: optionalInitMsg,
});backend.getAgentQuota(userId, agent) / backend.getForwarderQuota(address)
Inspect quota usage for agent/forwarder authorization.
Envelope Builders
Envelope builders are functions with the signature (ctx: EnvelopeBuildContext) => Promise<EnvelopeBuildResult> | EnvelopeBuildResult. Pass them as signer to Done.config() or buildEnvelope to DoneContractClient.
createPasskeyEnvelopeBuilder(options)
For WebAuthn / hardware passkey signing.
import { createPasskeyEnvelopeBuilder, signDocDigest } from "@donezone/client";
const builder = createPasskeyEnvelopeBuilder({
userId: "user-abc", // or (ctx) => resolveUserId(ctx)
nonce: () => Date.now(),
expiresAt: () => Math.floor(Date.now() / 1000) + 300,
publicKey: () => storedPublicKey, // base64 string
sign: async (signDoc, ctx) => {
const digest = signDocDigest(signDoc); // SHA-256 Uint8Array
return await signWithHardware(digest); // return Uint8Array or base64 string
},
// Optional:
agent: "done1agent...",
forwarder: "done1forwarder...",
metadata: { gas_limit: 500_000 },
});
Done.config({ signer: builder });All option fields except sign can be a plain value or a (ctx: EnvelopeBuildContext) => MaybePromise<T> factory, giving per-call control.
createSessionEnvelopeBuilder(options)
For session key signing (e.g. a server-side HSM or stored keypair).
import { createSessionEnvelopeBuilder } from "@donezone/client";
const builder = createSessionEnvelopeBuilder({
userId: "user-abc",
sessionId: "session-xyz",
nonce: () => Date.now(),
expiresAt: () => Math.floor(Date.now() / 1000) + 300,
sign: async (signDoc) => {
const digest = signDocDigest(signDoc);
return await signWithSessionKey(digest); // Uint8Array or base64
},
});createDevEnvelopeBuilder(options?)
Produces unsigned envelopes for local development and testing. Never use in production.
import { createDevEnvelopeBuilder } from "@donezone/client";
const builder = createDevEnvelopeBuilder({
userId: "dev", // default: "dev"
ttlSeconds: 300, // default: 300
});Low-Level Primitives
These are useful when implementing a custom envelope builder or doing manual envelope manipulation.
buildEnvelope(draft)
Constructs and validates an AuthEnvelope from a draft. Throws if msgs is empty.
import { buildEnvelope } from "@donezone/client";
const envelope = buildEnvelope({
user_id: "user-abc",
msgs: [cosmosMsg],
nonce: Date.now(),
expires_at: Math.floor(Date.now() / 1000) + 300,
role: "Passkey", // or "Session"
signatures: { passkey: "base64sig" },
// Optional:
session_id: "...",
agent: "done1agent...",
forwarder: "done1forwarder...",
metadata: { trace_id: "...", memo: "...", gas_limit: 500_000 },
});toSignDoc(envelope)
Strips signatures from an envelope (or draft) to produce the canonical AuthEnvelopeSignDoc that is actually signed.
import { toSignDoc } from "@donezone/client";
const signDoc = toSignDoc(envelope);signDocBytes(signDoc)
Produces the canonical UTF-8 JSON bytes (keys sorted, no extra whitespace) of the sign doc. This is what gets hashed.
import { signDocBytes } from "@donezone/client";
const bytes = signDocBytes(signDoc); // Uint8ArraysignDocDigest(signDoc)
SHA-256 of the canonical sign doc bytes. This is what passkey/session hardware actually signs.
import { signDocDigest } from "@donezone/client";
const digest = signDocDigest(signDoc); // Uint8Array (32 bytes)encodeSignature(bytes) / decodeSignature(base64)
Convert between raw signature bytes and the base64 strings stored in AuthEnvelope.signatures.
import { encodeSignature, decodeSignature } from "@donezone/client";
const b64 = encodeSignature(rawSigBytes); // Uint8Array → base64 string
const raw = decodeSignature(b64); // base64 string → Uint8ArrayMessage Builders
These build the wire format for contract calls. Useful when constructing multi-message envelopes manually.
buildTransactionMessage(contractAddr, call, options?)
Builds a WasmMsg::Execute CosmosMsg in the shape the Done HTTP gateway expects.
import { buildTransactionMessage } from "@donezone/client";
const msg = buildTransactionMessage(
"done1contract123",
{ path: "/buy", body: { minAmountOut: "1" }, query: { mode: "fast" } },
{ funds: { uusd: "100" } },
);
// { wasm: { execute: { contract_addr, msg: { msg: { path, body, query } }, funds } } }The call.path must start with /.
buildQueryMessage(call)
Builds the query message object that goes into GET /query/{contract}?msg=....
import { buildQueryMessage } from "@donezone/client";
const queryMsg = buildQueryMessage({ path: "/price", query: { verbose: "1" } });
// { msg: { path: "/price", query: { verbose: "1" } } }Types Reference
Key types exported from the package:
// Envelope core
AuthEnvelope // complete signed envelope sent to /tx
AuthEnvelopeSignDoc // envelope minus signatures — the thing that gets signed
EnvelopeDraft // input to buildEnvelope (signatures optional)
EnvelopeRole // "Passkey" | "Session"
EnvelopeSignatures // { passkey?: string; session?: string }
EnvelopeMetadata // { trace_id?: string; memo?: string; gas_limit?: number }
// Messages
CosmosMsg // WasmExecuteMsg | BankSendMsg | Record<string, unknown>
WasmExecuteMsg // { wasm: { execute: { contract_addr, msg, funds? } } }
BankSendMsg // { bank: { send: { to_address, amount } } }
Coin // { denom: string; amount: string }
FundsInput // Coin[] | Record<string, string | number> | undefined
// Client config
DoneContractConfig // config for DoneContractClient constructor
DoneClientConfig // config for DoneBackendClient constructor
DoneConfigOptions // config for Done.config() / Done.create()
// Request/response
ExecuteResponse // { transactionHash: string; height: number; gasUsed: number }
TransactionRequest // options for execute/run/post (body, query, funds, memo, etc.)
QueryRequest // options for query/get (body, query)
PreparedTransaction // { msg: CosmosMsg; metadata?: EnvelopeMetadata }
SubscriptionOptions // { onError?, signal?, rawEvent? }
// Builders
EnvelopeBuilder // (ctx: EnvelopeBuildContext) => Promise<EnvelopeBuildResult> | EnvelopeBuildResult
EnvelopeBuildContext // { msg, metadata?, path, request }
EnvelopeBuildResult // { envelope: AuthEnvelope; options?: ExecuteEnvelopeOptions }
EventSourceFactory // (url: string) => EventSource-compatible object
// Passkey/session builder options
PasskeyEnvelopeBuilderOptions
SessionEnvelopeBuilderOptions
DevEnvelopeBuilderOptionsComplete Example
import {
Done,
createPasskeyEnvelopeBuilder,
signDocDigest,
} from "@donezone/client";
// Configure once at startup
Done.config({
doneHttp: "https://doneHttp.done.zone",
doneEvents: "https://doneEvents.done.zone",
signer: createPasskeyEnvelopeBuilder({
userId: () => currentUser.id,
nonce: () => Date.now(),
expiresAt: () => Math.floor(Date.now() / 1000) + 300,
publicKey: () => currentUser.passkeyPublicKey,
sign: async (signDoc) => {
const digest = signDocDigest(signDoc);
return await navigator.credentials.get(/* WebAuthn options using digest */);
},
}),
});
// Transaction
const { transactionHash } = await Done.run("/done1abc.../buy", {
body: { minAmountOut: "100" },
funds: { uusd: "1000" },
});
// Query
const price = await Done.query<{ amount: string }>("/done1abc.../price");
// Subscription
const stop = Done.subscribe(
"/done1abc.../events",
"PriceChange",
(event) => console.log("price changed:", event),
);