@usearete/sdk
v0.28.0
Published
Pure TypeScript SDK for the Arete Solana streaming platform
Maintainers
Readme
Arete TypeScript SDK
Pure TypeScript SDK for generated stack definitions, program SDKs, prepared operation execution, and chain reads.
Connected clients expose the typed six-route transaction relay as client.transactions. Wallet adapters receive it per invocation from client.transaction and client.inspectOperation, so a shared wallet can safely serve multiple clients. Sessions expose session.transactions and accept a transactions override. HTTP u64 fields are decimal strings on the wire and bigint in the SDK; sends are never automatically retried.
Installation
npm install @usearete/sdkQuick Start
import { createSession } from '@usearete/sdk';
import { MY_STACK } from './generated/my-stack';
const session = await createSession({
stacks: { myStack: MY_STACK },
});
for await (const item of session.stacks.myStack.views.MyEntity.list.use()) {
console.log(item);
}SDK Shapes
Mental Model
Think of a generated stack as a cartridge: it packages one domain's typed views, queries, programs, reads, and flows, plus any cross-program addresses, constants, defaults, or math. "Cartridge" is only a mental model; the public API continues to use the existing generated stack objects and types.
An Arete session is the console. Insert one or more generated stacks under session.stacks, optionally add standalone programs, and use the console's shared session.programs, session.chain, and session.execute(...) surfaces. Programs packaged by a stack are automatically promoted to session.programs without creating another connection or runtime.
Every semantic operation has an explicit cardinality:
instruction- exactly one Solana instructiontransaction- exactly one atomic transaction with one or more instructionsflow- one or more sequential transactions
Every semantic operation exposes one pure entrypoint:
.prepare(input)
Every connected client or session exposes one semantic execution entrypoint:
.execute(prepared, options?)
There is no semantic .resolve(), .build(), .stage(), .plan(), or .send() projection surface anymore.
Connected Stack Cartridges
Access a connected generated stack through session.stacks.<name>. Its stable namespaces are:
views- typed streaming, list, and state viewsqueries- stack-level HTTP queriesprograms- owner-scoped program SDKs; the same connected objects are promoted tosession.programsaddresses- domain address derivations, when definedconstants- domain constants and enums, when defineddefaults- reusable domain defaults, when definedmath- pure domain calculations, when definedread- connected, domain-oriented reads, when definedflows- stack-level multi-transaction operations, when defined
These are namespaces on the existing connected stack object; no separate cartridge class or type is introduced.
Program SDKs
Each packaged, attached, or standalone program is canonically available under session.programs.<name> and exposes these stable namespaces. The owner-scoped session.stacks.<name>.programs.<program> path remains available when disambiguation is needed.
programId- the deployed Solana program addressschemas- generated validation schemaspdas- exact generated PDA definitionsaddresses- semantic address derivations, when definedaccounts- typed point reads for program accountsqueries- program-level HTTP queriesread- optional program-oriented convenience reads supplied by the SDKraw- exact IDL instruction builders with synchronous.build(...)instructions- semantic one-instruction operations with.prepare(...)transactions- semantic one-transaction operations with.prepare(...)flows- program-local multi-transaction operations with.prepare(...)constants- program constants and enumsdefaults- reusable program defaultsmath- pure protocol calculations
Program Read Transports
Generated account readers use a ProgramReadDescriptor for each program. Local
generation emits an explicit local-http descriptor, so the endpoint must come
from the connection call:
const client = await Arete.connect(LOCAL_STACK, {
url: 'ws://localhost:8877',
httpUrl: 'http://localhost:8877',
});Installed hosted program SDKs instead carry a complete hosted-binding
descriptor opaquely inside the exported program cartridge. Passing that one
program object to createSession or ConnectOptions.programs automatically
activates its release-pinned account reads; consumers do not import or register
the descriptor separately. Hosted account reads ignore httpUrl and stack HTTP
endpoints. The advanced programReads override remains available, but it must
replace the complete descriptor; release, binding, and endpoint fields are never
patched independently.
Stack and program queries remain stack-HTTP operations. They continue to use
httpUrl or stack.endpoints.http and do not use a hosted Program Read binding.
Composition sessions accept only complete hosted descriptors or complete hosted
overrides, and never infer Program Read endpoints from their members.
Hosted Solana Gateway Transports
Hosted stack and program installs embed complete, independent chain and
transactions gateway bindings. Arete.connect, standalone program sessions,
and generated composition sessions select those authenticated transports by
default instead of using a LiveSpec query endpoint. Explicit transports always
remain available as overrides:
import { createHostedSolanaGatewayTransports } from '@usearete/sdk';
import { MY_STACK_HOSTED_BINDINGS } from './generated/my-stack';
const { chain, transactions } = createHostedSolanaGatewayTransports(
{
chain: MY_STACK_HOSTED_BINDINGS.chain,
transactions: MY_STACK_HOSTED_BINDINGS.transactions,
},
{
auth: { publishableKey: import.meta.env.VITE_ARETE_PUBLISHABLE_KEY },
}
);The helper mints tokens for the exact solana-gateway-binding target. read,
transaction:inspect, and transaction:send use separate scope caches. An
authentication failure is refreshed at most once, and transaction requests are
replayed only when the gateway explicitly reports that upstream dispatch did
not begin. Chain and transaction descriptors may intentionally share the same
endpoint and binding ID.
Generated hosted compositions additionally retain
create<StackName>HostedSession(options) as an explicit convenience helper.
The ordinary create<StackName>Session(options) now uses the embedded gateway
too. Local/self-hosted compositions carry no gateway descriptor and continue to
require caller-supplied chain and transactions transports.
Raw Instructions
Use raw handlers when you want exact wire control and will compose the transaction yourself:
const ix = session.programs.splToken.raw.InitializeMint2.build({
mint,
decimals: 6,
mint_authority: authority,
freeze_authority: null,
});
await session.transaction([ix]);Raw builders are the exact IDL escape hatch. Instruction names, account names, argument names, and nested objects retain the generated IDL shape, including snake_case where the IDL uses it.
Semantic Operations
Use semantic operations for normal application code. Their .prepare(...) methods accept semantic, camelCase object inputs, derive routine addresses, normalize amounts, and return prepared artifacts. Use raw only when you deliberately need the exact IDL surface.
Instruction example:
const prepared = await session.programs.tokenMetadata.instructions.createMetadataAccountV3.prepare({
mint,
mintAuthority,
payer,
updateAuthority,
name,
symbol,
uri,
});
console.log(prepared.kind); // 'instruction'
console.log(prepared.instruction);
console.log(prepared.artifacts);
await session.execute(prepared);Transaction example:
const prepared = await session.programs.cpAmm.transactions.swap.exactIn.prepare({
pool,
payer,
inputTokenMint,
amountIn: { ui: '1.25' },
minimumAmountOut: 1n,
});
console.log(prepared.kind); // 'transaction'
console.log(prepared.transaction.instructions.length);
await session.execute(prepared);Flow example:
const prepared = await session.programs.presale.flows.escrow.depositPermissionless.prepare({
presale,
owner,
maxAmount: { ui: '1000' },
});
console.log(prepared.kind); // 'flow'
console.log(prepared.plan.transactions.length);
await session.execute(prepared);Prepared Shapes
Prepared values are immutable and discriminated:
PreparedInstructionPreparedTransactionPreparedFlow
All prepared values include:
kindnameartifactsplan.transactions
Instruction values also include .instruction, and transaction values include .transaction.
Prepared instructions can be composed directly into a transaction without
extracting .instruction:
const transaction = createPreparedTransaction({
name: 'configureMint',
instructions: [initializeMint, setAuthority],
artifacts: { mint },
});Prepared instructions and prepared transactions can also be flattened into one atomic transaction without reaching into their transaction bodies:
const transaction = createPreparedTransaction({
name: 'createAndConfigureMint',
operations: [createMint, createMetadata, setAuthority],
artifacts: { mint },
});Only single-transaction operations are accepted by operations. A
PreparedFlow must retain its ordered transaction boundaries.
Child signer and error metadata is inherited unless the transaction explicitly
provides requiredSignerAddresses or errors.
Every successful execution receipt exposes its signatures in transaction order:
const receipt = await session.execute(prepared);
console.log(receipt.signatures);Instruction and transaction receipts contain one signature. Flow receipts contain one signature for each executed transaction.
Use describePreparedOperation(prepared) for a typed, JSON-safe description, or
formatPreparedOperation(prepared) for human-readable text.
const description = describePreparedOperation(prepared);
console.log(JSON.stringify(description, null, 2));Sessions
Use a session as the console for one or more generated stack cartridges and/or standalone programs behind one execution surface. Stack programs are promoted by reference; session.programs.presale === session.stacks.presale.programs.presale.
import { createSession, createSignerRegistry } from '@usearete/sdk';
const signerRegistry = createSignerRegistry([
[creatorAddress, creatorSigner],
]);
const session = await createSession(
{
stacks: {
squads: SQUADS_V4_STREAM_STACK,
presale: METEORA_PRESALE_STREAM_STACK,
},
programs: {
splToken: SPL_TOKEN_PROGRAM,
},
},
{
transport: 'http',
endpoints: { http: 'http://127.0.0.1:8081' },
wallet,
signerRegistry,
}
);
const prepared = await session.stacks.squads.flows.vaultProposal.prepare(...);
await session.execute(prepared);
// Signers can also be managed after session creation.
session.signerRegistry.register(memberAddress, memberSigner);Equivalent entrypoints:
createSession(...)Arete.session(...)
Session surface:
session.stacks.<name>- connected generated stackssession.programs.<name>- connected packaged, attached, or standalone programssession.chain- canonical generic chain readssession.signerRegistrysession.transaction(...)session.execute(...)
Prefer these connected paths in application code. Arete.connect(STACK, ...) remains available when a direct single-stack client is more convenient.
Programs are matched by identity, not by name (compareProgramIdentity(a, b) returns 'same' | 'unproven' | 'different'). A program SDK's identity is its packageReleaseHash, the program package release a registry-installed SDK was generated from; local builds have none.
- Both have one: equal hashes (or the same object) are the same program. A standalone program that a stack also provides is served by that stack's connected instance: one client, no warning. Different hashes throw
AreteErrorwith codePROGRAM_KEY_CONFLICTbefore anything connects. - At least one has none, with the same
programSpecHash: the explicitly attached program takes the key, with oneconsole.warn. A standalone session program takessession.programs.<key>, andsession.stacks.<name>.programs.<key>keeps the stack's. Two stacks with such copies promote the first stack's. - Anything else (a different or missing
programSpecHash) throwsPROGRAM_KEY_CONFLICT; usesession.stacks.<name>.programs.<key>or attach the standalone program under another key. Two stacks bundling different programs under one key both stay reachable through their stacks, and readingsession.programs.<key>throwsPROGRAM_KEY_CONFLICTnaming them.
The same rule applies to withPrograms, ConnectOptions.programs, a session member's programs, and React's useArete(stack, { programs }), where the attached program replaces the stack's for that client. isSameProgramSdk(a, b) is compareProgramIdentity(a, b) === 'same'.
extendProgram, extendPrograms, and withProgramRead drop packageReleaseHash, because a program changed outside its generated SDK is no longer provably that SDK. Generated entries stamp it last with withProgramIdentity(program, { packageReleaseHash }), after the package's own extension.
Generated stack and program objects carry their runtime extensions under registry symbols, so { ...MY_STACK } keeps read, flows, program operations and read descriptors, while Object.keys and JSON never list them. EXTENSION_API_VERSION (also arete.extensionApi in this package's package.json) versions that extension contract and changes only on a breaking change.
Chain Reads
Generic chain reads use the console-level session.chain surface.
Available reads include:
exists(address)lamports(address)minimumBalanceForRentExemption(space)clock()account(address)accounts(addresses)- batchedaccount(address), up to 100 per call, results aligned with the input ordermint(address)tokenAccount(address)balance({ owner, mint, tokenProgram? })
Example:
const rentLamports = await session.chain.minimumBalanceForRentExemption(82);
const mintInfo = await session.chain.mint(mintAddress);Streaming Views
Views are still the main streaming surface.
for await (const update of session.stacks.myStack.views.settlementGame.list.watch()) {
if (update.type === 'upsert') {
console.log(update.key, update.data);
}
}
const game = await session.stacks.myStack.views.settlementGame.state.get('game-123');
const latest = await session.stacks.ore.views.OreRound.latest.getOne();get and getOne open (or reuse) an equivalent subscription, wait for its initial snapshot, and release it. They reject with InitialDataTimeoutError after timeoutMs (5000 by default; null waits forever). getSync only reads a subscription that is already active and returns undefined when there is none.
Every options object is a protocol v2 query with independent ordered membership. Different windows and filters on the same view can run concurrently, while equivalent normalized queries share one reference-counted wire subscription:
const rounds = session.stacks.ore.views.OreRound.latest;
const firstPage = rounds.watch({ take: 10 });
const secondPage = rounds.watch({ take: 10, skip: 10 });
for await (const update of firstPage) {
if (update.type === 'remove') {
console.log(`${update.key} left the first-page window`);
}
}Completed authoritative snapshots replace membership for their exact query after reconnect. Cursor queries created with after receive incremental snapshots and merge without pruning.
Low-level QueryLease.refresh() returns a Promise<void> that resolves after the refreshed subscription's next complete snapshot is committed. Registration, send, and subscription failures reject and are published on the lease's QuerySnapshot.error, with isRefreshing cleared. Subscriptions created with snapshots disabled resolve after the refresh request is sent.
Connection recovery
autoConnect and autoReconnect default to true and control separate lifecycle phases. Set autoConnect: false to create a disconnected client that the application connects later. Set autoReconnect: false when the application wants to handle post-disconnect recovery itself:
const client = await Arete.connect(MY_STACK, {
autoConnect: false,
autoReconnect: false,
});The same independent options are available per stack member in createSession(...) and on React's AreteProvider.
Caller-supplied schema diagnostics
Core view schemas continue to filter rejected entities. React view hooks additionally accept onSchemaValidationError, which reports { view, key?, entity, error } without changing accepted data.
Safe amount parsing
toRawAmount(input, decimals) throws when user input is invalid. Form and API boundaries can use safeToRawAmount(input, decimals) instead:
import { safeToRawAmount } from '@usearete/sdk';
const result = safeToRawAmount({ ui: amountText }, 9);
if (!result.success) {
console.error(result.error);
return;
}
console.log(result.data); // bigint in raw base unitsIt returns { success: true, data } | { success: false, error } and never throws for an invalid amount input.
Update Types
type Update<T> =
| { type: 'upsert'; key: string; data: T; cursor?: string }
| { type: 'patch'; key: string; data: Partial<T>; cursor?: string }
| { type: 'remove'; key: string; cursor?: string }
| { type: 'delete'; key: string; cursor?: string };
type RichUpdate<T> =
| { type: 'created'; key: string; data: T; cursor?: string }
| { type: 'updated'; key: string; before: T; after: T; patch?: unknown; cursor?: string }
| { type: 'removed'; key: string; lastKnown?: T; cursor?: string }
| { type: 'deleted'; key: string; lastKnown?: T; cursor?: string };remove means an entity left only this query's filter or window. delete means the source entity was deleted and is removed from every query for that view.
A patch for a key the client holds no entity for (never received, or evicted by maxEntriesPerView) is discarded rather than stored as a partial entity; the entity appears with the server's next full upsert. Each discard is reported to onFrameValidationError with reason: 'unknown-key'. Replayable append-view records (frames with an offset) are events and are always applied.
Testing
@usearete/sdk/testing provides supported, dependency-light test helpers:
createWebSocketHarness()— a scripted WebSocket server; passharness.websocketFactoryasauth.websocketFactoryand answer subscriptions withframes.*builders.createFrameHarness()— the store engine without a socket.createFakeTransactionTransport()— a recording relay that can fail or stall.createWalletFixture()— a recording wallet with scripted outcomes;createTransactionOutcomeFixtures()covers every status and phase.createFetchStub(routes)— a routed, recordingfetch.
import { createWebSocketHarness, createWalletFixture } from '@usearete/sdk/testing';
const ws = createWebSocketHarness();
const client = await Arete.connect(MY_STACK, {
auth: { websocketFactory: ws.websocketFactory },
wallet: createWalletFixture(),
});Replay cursors
Updates from an append view backed by the server's journal carry cursor, the
{epoch}:{offset} position of the event. Store it with the data you derive
from that update and pass it back as after to resume exactly where you
stopped — after is exclusive, and it is never a _seq value. State and list
views project membership rather than a tape, so their updates have no cursor.
for await (const update of session.stacks.myStack.views.Trade.list.watch()) {
await db.apply(update, update.cursor); // one transaction: data + position
}Reconnects resume from the last cursor delivered on each subscription. If a
record is lost locally the stream ends with a StreamGapError carrying the
last cursor delivered before the loss, rather than skipping records silently.
Server-side refusals (cursor-expired, cursor-epoch-changed,
cursor-unknown, invalid-cursor, replay-gap, replay-lagged) end the
stream with their wire code; isReplayErrorCode identifies them, and the
failing frame — including replayWindow and recoverFrom — is on the error's
details.
License
MIT
