# @thru/sdk

Open-source TypeScript SDK for the Thru RISC-V Layer-1 blockchain: typed RPC client, transaction builder, keys and crypto, protobufs, and ABI reflection.

The SDK exposes rich domain models (blocks, accounts, transactions, events, proofs) that hide the underlying protobuf transport.

## Installation

```bash
npm install @thru/sdk
```

### TypeScript Configuration

For optimal import resolution, use modern module resolution:

```json
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "module": "ESNext",
    "target": "ES2020",
    "isolatedModules": true
  }
}
```

If you rely on Node’s ESM support without a bundler, use `"moduleResolution": "nodenext"`.

## Basic Usage

```ts
import { createThruClient } from "@thru/sdk";
import {
  Account,
  Block,
  ChainEvent,
  ConsensusStatus,
  Transaction,
  TransactionStatusSnapshot,
} from "@thru/sdk";

const thru = createThruClient({
  baseUrl: "https://rpc.alphanet.thru.org",
});

// Fetch the latest finalized block
const height = await thru.blocks.getBlockHeight();
const latestBlock: Block = await thru.blocks.get({ slot: height.finalized });
console.log(latestBlock.header.blockHash);

// Fetch an account – returns the Account domain object
const account: Account = await thru.accounts.get("taExampleAddress...");
console.log(account.meta?.balance);

// Build, sign, submit, and track a transaction
const { rawTransaction } = await thru.transactions.buildAndSign({
  feePayer: {
    publicKey: "taFeePayerAddress...",
    privateKey: feePayerSecretKeyBytes,
  },
  program: programIdentifierBytes,
});

// Submit and track with one RPC so no live tracking update can be missed.
for await (const update of thru.transactions.sendAndTrack(rawTransaction, {
  timeoutMs: 60_000,
})) {
  console.log(update.status, update.executionResult?.consumedComputeUnits);
  if (
    update.executionResult ||
    update.consensusStatus === ConsensusStatus.FINALIZED ||
    update.consensusStatus === ConsensusStatus.CLUSTER_EXECUTED
  ) {
    break;
  }
}
```

## Client Configuration

`createThruClient` accepts advanced transport options so you can customize networking, interceptors, and default call behaviour:

```ts
const thru = createThruClient({
  baseUrl: "https://rpc.alphanet.thru.org",
  transportOptions: {
    useBinaryFormat: false,
    defaultTimeoutMs: 10_000,
  },
  interceptors: [authInterceptor],
  callOptions: {
    timeoutMs: 5_000,
    headers: [["x-team", "sdk"]],
  },
});
```

- `transportOptions` are passed to `createGrpcWebTransport`. Provide custom fetch implementations, JSON/binary options, or merge additional interceptors.
- `interceptors` let you append cross-cutting logic (auth, metrics) without re-implementing transports.
- `callOptions` act as defaults for **every** RPC. You can set timeouts, headers, or a shared `AbortSignal`, and each module call merges in per-request overrides.

### Legacy transaction signing

RFC-8032 is always the default. Networks that have not completed the signing
cutover can opt into the pre-cutover outer-transaction signature scheme:

```ts
import {
  createThruClient,
  TransactionSigningScheme,
} from "@thru/sdk";

const thru = createThruClient({
  baseUrl: "https://legacy-rpc.example",
  transactionSigningScheme: TransactionSigningScheme.Legacy,
});
```

For manually built transactions, `transaction.legacySign(privateKey)` is the
explicit compatibility equivalent of the RFC-8032-only
`transaction.sign(privateKey)`. The schemes are not interchangeable, and the
SDK never retries with legacy signing automatically.

## Domain Models

The SDK revolves around immutable domain classes. They copy mutable buffers, expose clear invariants, and provide conversion helpers where needed.

| API surface | Domain class |
| --- | --- |
| Blocks | `Block`, `BlockHeader`, `BlockFooter` |
| Accounts | `Account`, `AccountMeta`, `AccountData` |
| Transactions | `Transaction`, `TransactionStatusSnapshot`, `TrackTransactionUpdate` |
| Events | `ChainEvent` |
| Proofs | `StateProof` |
| Height | `HeightSnapshot` |
| Node version | `VersionInfo` |

All classes are exported from the root package for easy access:

```ts
import { Block, Account, ChainEvent } from "@thru/sdk";
```

### Primitives

`Pubkey` and `Signature` wrap the 32-byte Ed25519 public key and 64-byte signature primitives, respectively. They centralize validation, conversion, and proto interop so you can work with either Thru-formatted strings (`ta...` / `ts...`), hex, or raw bytes without sprinkling helpers throughout your app.

```ts
import { Pubkey, Signature } from "@thru/sdk";

const payer = Pubkey.from("taDs2...");          // accepts ta string, hex, or Uint8Array
const sig = Signature.from("ts8Lk...");         // accepts ts string, hex, or Uint8Array

payer.toBytes();           // defensive copy
payer.toThruFmt();         // "ta..." string
payer.toProtoPubkey();     // thru.common.v1.Pubkey
payer.toProtoTaPubkey();   // thru.common.v1.TaPubkey

sig.toBytes();
sig.toThruFmt();          // "ts..." string
sig.toProtoSignature();   // thru.common.v1.Signature
sig.toProtoTsSignature(); // thru.common.v1.TsSignature

// Helper namespace now returns these domain objects:
const parsed = sdk.helpers.createPubkey("taFeePayerAddress...");
const signature = sdk.helpers.createSignature(sigBytes);
```

The bound client accepts either raw bytes or the new primitives; call `.toBytes()` on `Pubkey`/`Signature` when you need to interop with legacy code.

## View Options

When fetching resources, you can control which parts of the resource are returned using view options. This allows you to optimize network usage by only fetching the data you need.

### AccountView

Controls which sections of account resources are returned:

```ts
import { AccountView } from "@thru/sdk";

// Fetch only the account address (lightweight existence check)
const account = await thru.accounts.get(address, {
  view: AccountView.PUBKEY_ONLY,
});

// Fetch only account metadata (balance, flags, owner, etc.)
const account = await thru.accounts.get(address, {
  view: AccountView.META_ONLY,
});

// Fetch only account data bytes (program data)
const account = await thru.accounts.get(address, {
  view: AccountView.DATA_ONLY,
});

// Fetch everything: address, metadata, and data (default)
const account = await thru.accounts.get(address, {
  view: AccountView.FULL,
});
```

| View Option | Returns | Use Case |
| --- | --- | --- |
| `AccountView.PUBKEY_ONLY` | Only the account `address` | Quick existence check |
| `AccountView.META_ONLY` | `address` + `meta` (balance, flags, owner, dataSize, seq, nonce) | Display account summary without data |
| `AccountView.DATA_ONLY` | `address` + `data` (raw bytes) | Fetch program data without metadata |
| `AccountView.FULL` | `address` + `meta` + `data` | Complete account information |

### BlockView

Controls how much of a block resource is returned:

```ts
import { BlockView } from "@thru/sdk";

// Fetch only block header (slot, hash, producer, etc.)
const block = await thru.blocks.get({ slot }, {
  view: BlockView.HEADER_ONLY,
});

// Fetch header and footer (execution status)
const block = await thru.blocks.get({ slot }, {
  view: BlockView.HEADER_AND_FOOTER,
});

// Fetch only block body (transactions)
const block = await thru.blocks.get({ slot }, {
  view: BlockView.BODY_ONLY,
});

// Fetch everything: header, body, and footer (default)
const block = await thru.blocks.get({ slot }, {
  view: BlockView.FULL,
});
```

| View Option | Returns | Use Case |
| --- | --- | --- |
| `BlockView.HEADER_ONLY` | Only block `header` (metadata) | Display block summary without transactions |
| `BlockView.HEADER_AND_FOOTER` | `header` + `footer` (execution status) | Check execution status without transactions |
| `BlockView.BODY_ONLY` | Only block `body` (transactions) | Fetch transactions without header metadata |
| `BlockView.FULL` | `header` + `body` + `footer` | Complete block information |

### TransactionView

Controls how much of a transaction resource is returned:

```ts
import { TransactionView } from "@thru/sdk";

// Fetch only transaction signature
const tx = await thru.transactions.get(signature, {
  view: TransactionView.SIGNATURE_ONLY,
});

// Fetch only transaction header (signature, fee payer, etc.)
const tx = await thru.transactions.get(signature, {
  view: TransactionView.HEADER_ONLY,
});

// Fetch header and body (instructions)
const tx = await thru.transactions.get(signature, {
  view: TransactionView.HEADER_AND_BODY,
});

// Fetch everything: header, body, and execution results (default)
const tx = await thru.transactions.get(signature, {
  view: TransactionView.FULL,
});
```

| View Option | Returns | Use Case |
| --- | --- | --- |
| `TransactionView.SIGNATURE_ONLY` | Only transaction `signature` | Quick existence check |
| `TransactionView.HEADER_ONLY` | Only transaction `header` (signature, fee payer, compute budget) | Display transaction summary without instructions |
| `TransactionView.HEADER_AND_BODY` | `header` + `body` (instructions) | Fetch transaction without execution results |
| `TransactionView.FULL` | `header` + `body` + execution results | Complete transaction information |

**Note:** If no view is specified, the default is `FULL` for all resource types.

## Streaming APIs

Every streaming endpoint yields an async iterable of domain models:

```ts
// Blocks
for await (const { block } of thru.streaming.streamBlocks()) {
  console.log(block.header.slot);
}

// Account updates
for await (const { update } of thru.streaming.streamAccountUpdates("taAddress")) {
  if (update.kind === "snapshot") {
    console.log(update.snapshot.account.meta?.balance);
  }
}

// Events
for await (const { event } of thru.streaming.streamEvents()) {
  console.log((event as ChainEvent).timestampNs);
}

// Transaction tracking for an existing signature. This is a live stream; when
// submitting a raw transaction yourself, prefer transactions.sendAndTrack().
for await (const update of thru.streaming.trackTransaction(signature)) {
  console.log(update.status, update.executionResult?.consumedComputeUnits);
}
```

## Filters

Server-side filtering is supported everywhere via CEL expressions:

```ts
import { Filter, FilterParamValue } from "@thru/sdk";

const ownerFilter = new Filter({
  expression: "account.meta.owner.value == params.owner",
  params: {
    owner: FilterParamValue.pubkey("taExampleAddress..."),
    min_balance: FilterParamValue.uint(1_000_000n),
  },
});

const accounts = await thru.accounts.list({ filter: ownerFilter });
```

Accepted parameter kinds:
- `stringValue`
- `bytesValue`
- `boolValue`
- `intValue`
- `doubleValue`
- `uintValue`
- `pubkeyValue`
- `signatureValue`
- `taPubkeyValue`
- `tsSignatureValue`

Functions that take filters:
- List APIs: `thru.accounts.list`, `thru.blocks.list`, `thru.transactions.listForAccount`
- Streams: `thru.streaming.streamBlocks`, `thru.streaming.streamAccountUpdates`, `thru.streaming.streamTransactions`, `thru.streaming.streamEvents`

Use the helper constructors on `FilterParamValue` to safely build parameters from raw bytes, ta/ts-encoded strings, or simple numbers.

## Modules Overview

- `thru.blocks` — fetch/stream blocks and height snapshots
- `thru.accounts` — read account state or build create-account transactions
- `thru.transactions` — build, sign, submit, track, and inspect transactions
- `thru.events` — query event history
- `thru.proofs` — generate state proofs
- `thru.consensus` — build version contexts and stringify consensus states
- `thru.streaming` — streaming wrappers for blocks, accounts, transactions, events
- `thru.helpers` — address, signature, and block-hash conversion helpers

The public surface is fully domain-based; reaching for lower-level protobuf structures is no longer necessary.

## Streaming helpers

Async iterable utilities make it easier to consume streaming APIs:

```ts
import { collectStream, firstStreamValue } from "@thru/sdk";

const updates = await collectStream(thru.streaming.streamBlocks({ startSlot: height.finalized }), {
  limit: 5,
});

const firstEvent = await firstStreamValue(thru.streaming.streamEvents());
```

`collectStream` gathers values (optionally respecting `AbortSignal`s), `firstStreamValue` returns the first item, and `forEachStreamValue` lets you run async handlers for each streamed update.

## Explicit account compression and restoration

Compression is built into the normal client. No separate compression package,
transport adapter, or program factory is needed. Canonical compression,
multicall and uploader addresses are defaults; `programAddresses` provides
optional overrides for private deployments.

Include a compressed keypair fee payer in `accounts` to restore it first using
its own signature. This supports canonical EOA accounts with zero data. The
helper submits a proof-bearing NOOP, confirms it, then restores the remaining
accounts with the updated nonce. Deleted accounts cannot be restored, and the
384-slot compression cooldown still applies. Set `fee: 0n` on networks that
allow zero-fee transactions when the payer has no native balance. Wallet intent
callers still require an active fee payer.

```ts
import { createThruClient, CompressionError } from '@thru/sdk';

const sdk = createThruClient({ baseUrl });
const statuses = await sdk.compression.getAccountStatuses({ accounts });

try {
  const result = await sdk.compression.decompressAccounts({
    accounts,
    feePayer: { publicKey, privateKey }, // Same convention as buildAndSign.
    fee: 0n,
  });
  // Build and sign the application transaction AFTER restoration succeeds.
  // Prerequisite transactions may have advanced this payer's nonce.
} catch (error) {
  if (error instanceof CompressionError) {
    // NOT_READY includes details.retrySlot when known.
    // Preserve details.result (partial progress, signatures, upload handles).
    // TRANSACTION_UNCERTAIN must be reconciled before further submissions.
  }
  throw error; // Do not continue with the application transaction on failure.
}
```

Wallet callers use the existing transaction-intent signing flow. The wallet
receives base64 instruction data and account addresses, and returns base64 signed
transaction bytes. It never receives an SDK `Transaction` or exposes private keys:

```ts
const context = await wallet.getSigningContext();
await sdk.compression.decompressAccounts({
  accounts,
  feePayer: { publicKey: context.feePayerPublicKey },
  walletAddress,
  signTransaction: intent => wallet.signTransaction(intent),
});
// Now prepare and sign the application intent as usual.
```

For an existing signing session, use `feePayer: { publicKey: session.publicKey }`,
`walletAddress: session.walletAddress`, and
`signTransaction: intent => session.signTransaction(intent)` instead. The selected
wallet address is not the managed fee payer. If wallet/session fallback changes
the payer, the helper rejects before submitting: refresh the signing context and
retry explicitly. The helper validates the signed passkey wrapper and nested
instruction. Wallet intents currently use fee 0; keypair calls retain the SDK
fee default unless overridden. Upload authority is the wallet account for wallet
calls and the payer for keypair calls. Large wallet restores stage their proof in
an upload account so variable passkey envelopes cannot invalidate proof pointers.

Small restorations batch through multicall; large images use temporary uploads.
Successful earlier steps remain committed if later steps fail. Cooldown returns
`NOT_READY` promptly. `cleanupUploads({ uploads, feePayer, ... })` can retry cleanup
from `error.details.result.uploads`; wallet cleanup also takes the wallet address
and intent callback. Handles retain their uploader address across configuration
changes. Unresolved submissions block cleanup that might destroy staging still
needed by a pending restoration.

Each SDK client serializes compression operations per fee payer and remembers
uncertain submissions across calls. Supply a stable `journal` with async `load()`
and `save(pending | undefined)` to survive process/page restarts; store the pending
signature, nonce and validity slot durably before sending. Scope the journal to
one network and payer, and reuse it on subsequent calls or a new client. It must
not contain private keys. `sdk.compression.reconcilePending({ feePayer: { publicKey },
journal })` only reads chain/journal state and never signs. Do not discard a
pending record to bypass uncertainty, or share the payer with concurrent
application submissions. An advanced nonce with a missing execution result
remains unresolved, even after expiry.

The compression service uses `sdk.compression.compressAccount({ account,
feePayer: { publicKey, privateKey }, fee: 0n, journal })`. It handles eligibility,
rate limits and scan checkpoints; the SDK handles the actual transaction and its
reconciliation. Decompression is explicit and caller-funded. Existing
build/sign/send APIs never automatically restore or retry application transactions.

Shared instruction codecs live in `thru-ts-client-sdk/domain/programs` and are
re-exported by `@thru/programs`, so the SDK does not depend on that package.
Regenerate them with `node scripts/generate-compression-codecs.mjs` (set
`THRU_ABI_BIN` if the ABI CLI is not at its repository default path), and verify
reproducibility with `--check`. The script reads the canonical ABI sources; the
re-export shims in `@thru/programs` should not receive duplicate generated code.
