> For the complete documentation index, see [llms.txt](https://docs.zebec.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zebec.io/developer-docs/sdks/streaming-sdk/stellar-streaming-sdk.md).

# Stellar

Stellar Streaming SDK - TypeScript SDK for Zebec payment streams on Stellar via Soroban.

A TypeScript SDK for interacting with Zebec's Stellar Streaming Contract. This SDK provides a high-level, type-safe interface to create and manage continuous payment streams on the [Stellar](https://stellar.org/) network using [Soroban smart contracts](https://soroban.stellar.org/).

## Overview

The Stellar Streaming SDK enables developers to build applications that leverage continuous, programmable token streams on the Stellar network. It wraps the underlying Soroban contract interactions, handling transaction building, simulation, preparation, and submission while exposing strongly-typed, ergonomic APIs.

Typical use cases include:

* **Payroll** — Stream salaries to employees continuously per second / minute / hour.
* **Vesting** — Distribute tokens over time with optional cliffs.
* **Subscriptions** — Recurring payments and metered billing.
* **Grants & Bounties** — Time-locked, cancelable token distribution.

## Installation

Install the package using npm or yarn:

```bash
npm install @zebec-network/stellar-payroll-sdk
```

```bash
yarn add @zebec-network/stellar-payroll-sdk
```

### Peer dependencies

The SDK relies on the following runtime dependencies (installed automatically):

* [`@stellar/stellar-sdk`](https://www.npmjs.com/package/@stellar/stellar-sdk) `^15.0.1`
* [`@zebec-network/core-utils`](https://www.npmjs.com/package/@zebec-network/core-utils) `^1.1.1`
* [`bignumber.js`](https://www.npmjs.com/package/bignumber.js) `^11.1.1`

### Requirements

* Node.js 18+ recommended
* TypeScript 5+ (when consuming from TS projects)

## Quick start

```ts
import {
  StellarStreamingService,
  type StellarStreamingSDKConfig,
  type WalletAdapter,
  type CreateStreamParams,
} from "@zebec-network/stellar-payroll-sdk";
import { Networks, Keypair, TransactionBuilder } from "@stellar/stellar-sdk";

// 1. Configure the SDK
const config: StellarStreamingSDKConfig = {
  contractId: "CDWK5GRLUB24FMWLWEAS3NLU2JCDW7T3BMU7HWKGEZEDJRXXESGLQ4YU",
  networkPassphrase: Networks.TESTNET,
  rpcUrl: "https://soroban-testnet.stellar.org",
};

// 2. Provide a wallet adapter
const keypair = Keypair.fromSecret("S...");
const wallet: WalletAdapter = {
  getPublicKey: async () => keypair.publicKey(),
  signTransaction: async (txXDR) => {
    const tx = TransactionBuilder.fromXDR(txXDR, Networks.TESTNET);
    tx.sign(keypair);
    return tx.toXDR();
  },
  signAllTransactions: async (txXDRs) =>
    txXDRs.map((txXDR) => {
      const tx = TransactionBuilder.fromXDR(txXDR, Networks.TESTNET);
      tx.sign(keypair);
      return tx.toXDR();
    }),
};

// 3. Instantiate the service
const service = new StellarStreamingService(config, wallet);

// 4. Create a stream
const params: CreateStreamParams = {
  amount: "100",
  start_time: "0",
  duration: "3600",
  cliff_percentage: "0",
  start_now: true,
  payroll_run_id: "october-payroll-2026",
  accrual_frequency: "1",
  pausable: true,
  cancelable_by_sender: true,
  cancelable_by_recipient: false,
  transferable_by_sender: false,
  transferable_by_recipient: false,
  automatic_withdrawal: false,
  initial_buffer_amount: "0",
  auto_topup: false,
  sender_id: "sender-1",
  receiver_id: "receiver-1",
  token_price: "10000000",
  price_expiry: "1893456000",
  price_signature: "a1b2c3...",
  token_contract_id: "abcd1234...",
  xlm_price: "2000000",
  xlm_price_expiry: "1893456000",
  xlm_price_signature: "d4e5f6...",
};

const payload = await service.createStream(
  await wallet.getPublicKey(),
  "GBR...RECEIVER",
  "0a1b2c...",               // streamId (hex-encoded BytesN)
  "CTOKEN...ADDR",           // token contract address
  params,
);

const result = await payload.signAndSubmit();
console.log("Tx hash:", result.txHash);
```

## Core concepts

### Streams

A **stream** is an on-chain agreement to transfer a token amount from a sender to a receiver over a defined duration. Streams can be paused, resumed, canceled, transferred, and topped up depending on the flags set at creation.

### Stream identifiers

Stream IDs are 32-byte values passed as **hex-encoded strings**. Stellar's Soroban runtime expects fixed-size byte arrays (`BytesN`) — the SDK converts the provided hex string into the appropriate ScVal automatically.

### Fee model

Fees are configured at two layers:

1. **Global config** (set by admin) — applies by default to all streams.
2. **Tenant config** (set by admin, per sender) — overrides global fees for a specific sender.

Each layer defines a `platform_fee`, a `base_fee`, a `stream_token_fee`, and a list of `fee_tiers` (amount-banded percentage fees).

### Decimals

The SDK automatically converts human-readable amounts to atomic units using the token's on-chain decimals (fetched via `getAssetDecimals`). Inputs like `"100.5"` are scaled correctly for the underlying SAC/token contract.

## SDK methods

All write methods return a [`TransactionPayload`](#transactionpayload) that must be signed and submitted. All read methods return parsed, human-readable values directly.

The service is constructed as:

```ts
new StellarStreamingService(config: StellarStreamingSDKConfig, wallet: WalletAdapter)
```

### Admin methods

These methods configure the global protocol state and require the caller's wallet to match the contract admin.

#### `getConfig(caller: string): Promise<Config>`

Reads the global protocol configuration: fee recipient, withdraw account, platform fee, base fee, stream token fee, fee tiers, frequencies, whitelisted tokens, the canonical XLM token address, the admin signer, the XLM token contract ID, and whether XLM is accepted as a fee token.

#### `setFeeConfig(admin, params, options?): Promise<TransactionPayload>`

Sets the protocol-wide fee configuration. The wallet's public key must match `admin`.

* `params: SetFeeConfigParams` — recipient, withdraw account, platform/base/stream-token fees (percent), fee tiers, and whether XLM is accepted as a fee token.

#### `whitelistTokens(admin, tokens, options?): Promise<TransactionPayload>`

Adds a list of token contract addresses to the whitelist of streamable tokens.

#### `removeWhitelistedToken(admin, token, options?): Promise<TransactionPayload>`

Removes a single token contract address from the whitelist.

#### `setFrequencies(admin, frequencies, options?): Promise<TransactionPayload>`

Updates the list of allowed accrual frequencies (in seconds) that streams can use.

#### `updateConfig(admin, params, options?): Promise<TransactionPayload>`

Bulk-updates the fee config **and** the allowed frequencies in a single transaction.

* `params: UpdateConfigParams` — same as `SetFeeConfigParams` plus `frequencies`, `admin_signer`, and `xlm_token_contract_id`.

### Tenant methods

Tenant configs let the admin set custom fee rules for a specific sender, overriding the global config for that sender's streams.

#### `getTenantConfig(sender: string): Promise<TenantConfig>`

Reads the tenant configuration assigned to a sender address: fee recipient, withdraw account, platform/base/stream-token fees, and fee tiers.

#### `setTenantConfig(admin, params, options?): Promise<TransactionPayload>`

Creates or updates the tenant config for `params.sender`. The wallet must match `admin`.

#### `removeTenantConfig(admin, sender, options?): Promise<TransactionPayload>`

Removes a tenant config, reverting the sender to the global fee defaults.

### Token methods

#### `approveTokenSpending(caller, token, spender, amount, options?): Promise<TransactionPayload>`

Approves a `spender` to transfer up to `amount` of `token` on behalf of `caller` (SEP-41 allowance). Required before invoking flows that pull funds via allowance, such as `triggerTopup`.

* `amount` is in display units; the SDK scales it to raw units using the token's on-chain decimals.
* The allowance lifetime is pinned to the network-enforced maximum entry TTL (fetched via `getMaxEntryTtl`), so the approval persists for the largest value the network will accept.
* The wallet's public key must match `caller`.

#### `establishTrustline(caller, assets, options?): Promise<TransactionPayload>`

Establishes classic Stellar trustlines for the caller for one or more assets in a single transaction. Each asset becomes one `changeTrust` operation, using the default maximum trustline limit.

Soroban contracts cannot invoke classic Stellar operations, so trustlines (a `ChangeTrust` op) cannot be opened from inside the streaming contract — they have to be opened SDK-side. This method bypasses the Soroban `prepareTransaction` step internally since the underlying operation is classic, not a host-function call.

* `assets: Asset[]` — classic assets to trust. Construct with `new Asset(code, issuer)` from `@stellar/stellar-sdk` (also re-exported from this package).
* The wallet's public key must match `caller`.
* Submitting against an already-trusted asset is a no-op on the network — safe to call again.

```ts
import { Asset } from "@zebec-network/stellar-payroll-sdk";

const usdc = new Asset(
  "USDC",
  "GA5ZSEFYNTRECNZXVT7AYG5WDXJWFEYDKHYZQ3LZWNK3RIEGBHV4QHGN",
);
const eurc = new Asset(
  "EURC",
  "GB3Q6QDZYTHWT7E5PVS3W7FUT5GVAFC5KSZFFLPU234JZTKLATKSEPCV",
);

const payload = await service.establishTrustline(caller, [usdc, eurc]);
await payload.signAndSubmit();
```

#### `checkTrustline(caller, assets): Promise<{ asset: Asset; exists: boolean }[]>`

Checks whether `caller` has established classic Stellar trustlines for the provided assets. Looks up the corresponding `trustLine` ledger entries via the Soroban RPC in a single batched call and returns one `{ asset, exists }` entry per input asset, **in the same order**.

* `assets: Asset[]` — classic assets to check. Construct with `new Asset(code, issuer)`.
* Read-only — does not submit a transaction and does not require the caller's wallet to match the SDK's wallet.
* Throws if `assets` is empty or if any entry is `Asset.native()`; every Stellar account implicitly trusts XLM, so a native trustline check is a programming error rather than a meaningful query.

Typical pairing with `establishTrustline` — only sign for the missing trustlines:

```ts
import { Asset } from "@zebec-network/stellar-payroll-sdk";

const want = [
  new Asset("USDC", "GA5ZSEFYNTRECNZXVT7AYG5WDXJWFEYDKHYZQ3LZWNK3RIEGBHV4QHGN"),
  new Asset("EURC", "GB3Q6QDZYTHWT7E5PVS3W7FUT5GVAFC5KSZFFLPU234JZTKLATKSEPCV"),
];

const statuses = await service.checkTrustline(caller, want);
const missing = statuses.filter((s) => !s.exists).map((s) => s.asset);

if (missing.length > 0) {
  const payload = await service.establishTrustline(caller, missing);
  await payload.signAndSubmit();
}
```

#### `bulkTransfer(sender, asset, recipients, options?): Promise<TransactionPayload>`

Executes a bulk transfer to multiple recipients using classic Stellar `Payment` operations. This bypasses Soroban contract invocation entirely, making it suitable for simple token distributions (e.g. payroll bonuses, airdrops, mass payouts) without interacting with the streaming contract.

* `asset: Asset` — classic asset to transfer. Construct with `new Asset(code, issuer)` from `@stellar/stellar-sdk` (also re-exported from this package). For SAC tokens, derive the asset from the token's code and issuer; the SDK automatically computes the SAC contract ID to query on-chain decimals and adjust amounts correctly.
* `recipients` — array of `{ destination: string; amount: string }`. Each `amount` is in display units (e.g. `"100.5"`). The SDK converts each amount to the token's atomic units, accounting for the difference between the token's on-chain decimals and Stellar classic's 7-decimal precision.
* The wallet's public key must match `sender`.
* This method bypasses Soroban `prepareTransaction` internally because classic `Payment` operations are not host-function calls.
* Every destination account must exist on the ledger and must have established a trustline for the asset (unless the destination is the sender itself).

```ts
import { Asset } from "@zebec-network/stellar-payroll-sdk";

const usdc = new Asset(
  "USDC",
  "GA5ZSEFYNTRECNZXVT7AYG5WDXJWFEYDKHYZQ3LZWNK3RIEGBHV4QHGN",
);

const payload = await service.bulkTransfer(sender, usdc, [
  { destination: "GBR...RECEIVER_1", amount: "100" },
  { destination: "GBR...RECEIVER_2", amount: "250.5" },
  { destination: "GBR...RECEIVER_3", amount: "75" },
]);

const result = await payload.signAndSubmit();
console.log("Bulk transfer tx:", result.txHash);
```

### Stream methods

#### `createStream(sender, receiver, streamId, token, params, options?): Promise<TransactionPayload>`

Creates a new payment stream from `sender` to `receiver`.

* `streamId` — 32-byte hex string. The SDK encodes it as a `BytesN`.
* `token` — token contract address (Stellar Asset Contract or custom).
* `params: CreateStreamParams` — full stream configuration (see [Data types](#data-types)).

The amount in `params.amount` is scaled to the token's decimals automatically. The wallet must match `sender`.

#### `createMultipleStreams(sender, entries, options?): Promise<TransactionPayload>`

Creates multiple payment streams from `sender` in a single transaction.

* `entries: CreateStreamEntry[]` — one entry per stream, each carrying its own `stream_id`, `receiver`, `token`, and `params`.
* Each entry's `params.amount` and `params.initial_buffer_amount` are scaled to the entry's token decimals. Decimals are fetched once per unique token across the batch.
* The wallet must match `sender`.

#### `createMultipleStreamsBatch(sender, entries, options?): Promise<BatchChunk<string[]>[]>`

Creates multiple payment streams split across several transactions to stay within Soroban resource limits.

* `entries: CreateStreamEntry[]` — same as `createMultipleStreams`.
* `options.chunkSize` — optional, defaults to `5`. Number of streams per chunk.
* Returns an array of `BatchChunk<string[]>`. Each chunk carries a `payload` (a `TransactionPayload`) and `meta` (the list of `stream_id`s in that chunk).
* The chunks can be signed and submitted all at once via [`TransactionPayload.signAndSubmitBatch`](#batch-submission).
* **Not atomic** across chunks. If a chunk fails, earlier chunks may already be committed on-chain. Inspect the responses (or catch `BatchTransactionError`) to determine partial success.

```ts
const chunks = await service.createMultipleStreamsBatch(
  sender.publicKey(),
  entries,
  { chunkSize: 7 },
);

const results = await TransactionPayload.signAndSubmitBatch(chunks, {
  abortOnFailure: false,
});

for (const result of results) {
  if (result.response?.status === "SUCCESS") {
    console.log("Created streams:", result.meta);
  } else {
    console.error("Failed chunk:", result.meta, result.buildError);
  }
}
```

#### `getStream(caller, streamId): Promise<Stream>`

Reads the full state of a stream by its ID. Amounts are returned as human-readable strings (scaled down by token decimals); timestamps as numbers; status as an array of strings.

#### `getWithdrawableAmount(caller, streamId): Promise<string>`

Returns the currently-vested, withdrawable amount as a human-readable decimal string. Internally fetches the stream first to determine the token decimals.

#### `pauseResumeStream(caller, streamId, options?): Promise<TransactionPayload>`

Toggles the paused/active state of a stream. `caller` must be the stream's sender, and the stream must have `pausable: true`.

#### `pauseResumeAllStreams(caller, streamIds, options?): Promise<TransactionPayload>`

Toggles the paused/active state of multiple streams in a single transaction.

* `streamIds` — array of 32-byte hex-encoded stream IDs.
* `caller` must be the sender of every stream in the batch. Streams that are non-pausable, terminal, or whose sender does not match `caller` are silently skipped by the contract.

#### `pauseResumeAllStreamsBatch(caller, streamIds, options?): Promise<BatchChunk<string[]>[]>`

Toggles the paused/active state of multiple streams split across several transactions to stay within Soroban resource limits.

* `streamIds` — array of 32-byte hex-encoded stream IDs.
* `options.chunkSize` — optional, defaults to `5`. Number of stream IDs per chunk.
* Returns an array of `BatchChunk<string[]>`. Each chunk carries a `payload` and `meta` (the list of `streamId`s in that chunk).
* The chunks can be signed and submitted all at once via [`TransactionPayload.signAndSubmitBatch`](#batch-submission).
* **Not atomic** across chunks. If a chunk fails, earlier chunks may already be committed on-chain.

#### `cancelStream(caller, streamId, options?): Promise<TransactionPayload>`

Cancels a stream. `caller` must be the sender (if `cancelable_by_sender`) or the receiver (if `cancelable_by_recipient`). Unvested funds are refunded to the sender.

#### `cancelAllStreams(caller, streamIds, options?): Promise<TransactionPayload>`

Cancels multiple streams in a single transaction.

* `streamIds` — array of 32-byte hex-encoded stream IDs.
* The caller-eligibility rule for `cancelStream` applies to each stream individually (sender with `cancelable_by_sender`, or receiver with `cancelable_by_recipient`). Streams that fail authorization, are terminal, or lack the vault balance to cover vested debt are silently skipped by the contract.

#### `cancelAllStreamsBatch(caller, streamIds, options?): Promise<BatchChunk<string[]>[]>`

Cancels multiple streams split across several transactions to stay within Soroban resource limits.

* `streamIds` — array of 32-byte hex-encoded stream IDs.
* `options.chunkSize` — optional, defaults to `5`. Number of stream IDs per chunk.
* Returns an array of `BatchChunk<string[]>`. Each chunk carries a `payload` and `meta` (the list of `streamId`s in that chunk).
* The chunks can be signed and submitted all at once via [`TransactionPayload.signAndSubmitBatch`](#batch-submission).
* **Not atomic** across chunks. If a chunk fails, earlier chunks may already be committed on-chain.

#### `withdrawStream(caller, streamId, options?): Promise<TransactionPayload>`

Withdraws the vested amount.

* If `automatic_withdrawal == true`, `caller` must be `stream.withdraw_account`.
* If `automatic_withdrawal == false`, `caller` must be `stream.receiver`.

#### `withdrawAllStreams(caller, streamIds, options?): Promise<TransactionPayload>`

Withdraws the vested amount from multiple streams in a single transaction.

* `streamIds` — array of 32-byte hex-encoded stream IDs.
* The caller-eligibility rule for `withdrawStream` applies to each stream individually (`stream.withdraw_account` if `automatic_withdrawal == true`, otherwise `stream.receiver`); the same `caller` must satisfy this for every stream in the batch.

#### `changeRecipient(caller, streamId, newReceiver, options?): Promise<TransactionPayload>`

Reassigns the stream's receiver. `caller` must be the sender (if `transferable_by_sender`) or the current receiver (if `transferable_by_recipient`).

#### `topupStream(caller, streamId, amount, options?): Promise<TransactionPayload>`

Manually deposits additional funds into a stream. `caller` must be the stream's sender. `amount` is in display units; the SDK fetches the stream's token decimals and scales it to atomic units automatically.

#### `triggerTopup(caller, streamId, amount, options?): Promise<TransactionPayload>`

Triggers an automated top-up for a stream that has `auto_topup_enabled`. Callable by anyone (subject to contract rules). `amount` is in display units and is auto-scaled to the stream token's decimals. Requires a prior `approveTokenSpending` call so the contract can pull funds via allowance.

#### `topupAllStreams(caller, entries, options?): Promise<TransactionPayload>`

Top-ups multiple streams in a single transaction. `caller` must be the sender of every stream; entries owned by a different sender are silently skipped by the contract. No prior `approveTokenSpending` is needed because auth is collected once from `caller`.

* `entries: TopupEntry[]` — one entry per stream (see [Data types](#data-types)). Each `amount` is in display units; the SDK looks up each stream's token, fetches its decimals, and scales the amount to atomic units automatically. Duplicate stream IDs are deduplicated internally.

#### `triggerTopupAllStreams(caller, entries, options?): Promise<TransactionPayload>`

Triggers automated top-ups for multiple streams in a single transaction. Callable by anyone (subject to contract rules). Streams with `auto_topup_enabled == false` are silently skipped by the contract.

* `entries: TopupEntry[]` — one entry per stream. Each `amount` is in display units and is auto-scaled to the stream token's decimals. Requires a prior `approveTokenSpending` call for each stream's token so the contract can pull funds via allowance.

### TTL methods

Soroban contract data has a time-to-live (TTL) for persistent storage. These methods extend it.

#### `bumpStreamTtl(caller, streamId, options?): Promise<TransactionPayload>`

Extends the TTL of a specific stream's persistent storage entry. Callable by anyone.

#### `bumpConfigTtl(caller, options?): Promise<TransactionPayload>`

Extends the TTL of the protocol's config instance storage. Callable by anyone.

## Data types

All types are exported from the package root.

### `StellarStreamingSDKConfig`

SDK constructor configuration.

| Field               | Type     | Description                                                               |
| ------------------- | -------- | ------------------------------------------------------------------------- |
| `contractId`        | `string` | Soroban contract address of the streaming contract.                       |
| `networkPassphrase` | `string` | Stellar network passphrase (`Networks.TESTNET`, `Networks.PUBLIC`, etc.). |
| `rpcUrl`            | `string` | URL of a Soroban RPC endpoint.                                            |

### `WalletAdapter`

Interface every wallet implementation must satisfy.

| Field                 | Type                                                   | Description                                                                                                                                                                         |
| --------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getPublicKey`        | `() => Promise<string>`                                | Returns the source account's public key (G…).                                                                                                                                       |
| `signTransaction`     | `(txXDR: string) => Promise<string>`                   | Signs a transaction in XDR format and returns the signed XDR.                                                                                                                       |
| `signAllTransactions` | `(txXDRs: string[]) => Promise<string[]>` *(optional)* | Signs an array of transaction XDRs and returns the signed XDRs in the same order. Used by batch / multi-sign flows. Optional — only needed if you intend to use multi-sign helpers. |

### `TransactionOptions`

Optional overrides for transaction building.

| Field     | Type      | Description                                        |
| --------- | --------- | -------------------------------------------------- |
| `fee`     | `string?` | Custom fee (stroops). Defaults to `BASE_FEE`.      |
| `timeout` | `number?` | Transaction timeout in seconds. Defaults to `180`. |

### `FeeTier`

A single fee tier band, expressed in human-readable units.

| Field         | Type               | Description                            |
| ------------- | ------------------ | -------------------------------------- |
| `min_amount`  | `string \| number` | Lower bound (inclusive) for this tier. |
| `max_amount`  | `string \| number` | Upper bound (exclusive) for this tier. |
| `fee_percent` | `number`           | Fee percentage (e.g. `0.5` for 0.5%).  |

### `ParsedFeeTier`

Internal representation after scaling: amounts in atomic units and fee converted to basis points. Returned by the conversion utilities.

| Field        | Type     |
| ------------ | -------- |
| `min_amount` | `string` |
| `max_amount` | `string` |
| `fee_bps`    | `string` |

### `TenantConfig`

Read shape returned by `getTenantConfig`, and base shape for fee-related parameter types.

| Field              | Type        | Description                                  |
| ------------------ | ----------- | -------------------------------------------- |
| `fee_recipient`    | `string`    | Address that receives collected fees.        |
| `withdraw_account` | `string`    | Address that performs automatic withdrawals. |
| `platform_fee`     | `number`    | Platform-level fee percent.                  |
| `base_fee`         | `number`    | Base fee percent applied to every stream.    |
| `stream_token_fee` | `number`    | Fee percent charged in the stream token.     |
| `fee_tiers`        | `FeeTier[]` | Amount-banded percentage fees.               |

### `SetFeeConfigParams`

Parameters for `setFeeConfig`. Same fields as `TenantConfig`, plus:

| Field        | Type      | Description                             |
| ------------ | --------- | --------------------------------------- |
| `xlm_as_fee` | `boolean` | Whether XLM is accepted as a fee token. |

### `UpdateConfigParams`

Parameters for `updateConfig`. Same fields as `SetFeeConfigParams`, plus:

| Field                   | Type       | Description                                     |
| ----------------------- | ---------- | ----------------------------------------------- |
| `frequencies`           | `number[]` | Allowed accrual frequencies in seconds.         |
| `admin_signer`          | `string`   | Hex-encoded admin signer public key (32 bytes). |
| `xlm_token_contract_id` | `string`   | Hex-encoded XLM token contract ID (32 bytes).   |

### `SetTenantConfigParams`

Parameters for `setTenantConfig`. Same shape as `TenantConfig`, plus:

| Field    | Type     | Description                                      |
| -------- | -------- | ------------------------------------------------ |
| `sender` | `string` | The sender address the tenant config applies to. |

### `Config`

Read shape returned by `getConfig`. Extends `TenantConfig` with:

| Field                   | Type       | Description                                     |
| ----------------------- | ---------- | ----------------------------------------------- |
| `frequencies`           | `number[]` | Allowed accrual frequencies in seconds.         |
| `whitelisted_tokens`    | `string[]` | Token contract addresses permitted for streams. |
| `xlm_token`             | `string`   | Canonical XLM (Stellar Asset Contract) address. |
| `admin_signer`          | `string`   | Hex-encoded admin signer public key.            |
| `xlm_token_contract_id` | `string`   | Hex-encoded XLM token contract ID.              |
| `xlm_as_fee`            | `boolean`  | Whether XLM is accepted as a fee token.         |

### `CreateStreamParams`

Full parameters for creating a stream.

| Field                       | Type      | Description                                                              |
| --------------------------- | --------- | ------------------------------------------------------------------------ |
| `amount`                    | `string`  | Total stream amount (human-readable; scaled by token decimals).          |
| `start_time`                | `string`  | Unix timestamp (seconds) when streaming begins (ignored if `start_now`). |
| `duration`                  | `string`  | Stream duration in seconds.                                              |
| `cliff_percentage`          | `string`  | Percent of total that vests at start (basis-point–like input).           |
| `start_now`                 | `boolean` | If `true`, streaming starts at on-chain submission time.                 |
| `payroll_run_id`            | `string`  | Off-chain identifier linking the stream to a payroll run.                |
| `accrual_frequency`         | `string`  | Vesting tick interval, in seconds. Must be in the allowed `frequencies`. |
| `pausable`                  | `boolean` | Whether the stream supports pause/resume.                                |
| `cancelable_by_sender`      | `boolean` | Whether the sender may cancel.                                           |
| `cancelable_by_recipient`   | `boolean` | Whether the receiver may cancel.                                         |
| `transferable_by_sender`    | `boolean` | Whether the sender may reassign the receiver.                            |
| `transferable_by_recipient` | `boolean` | Whether the receiver may reassign themselves.                            |
| `automatic_withdrawal`      | `boolean` | If `true`, the configured `withdraw_account` performs withdrawals.       |
| `initial_buffer_amount`     | `string`  | Optional buffer amount (human-readable; scaled by token decimals).       |
| `auto_topup`                | `boolean` | If `true`, the stream supports automated top-ups.                        |
| `sender_id`                 | `string`  | Off-chain identifier for the sender (e.g. employer ref).                 |
| `receiver_id`               | `string`  | Off-chain identifier for the receiver (e.g. employee ref).               |
| `token_price`               | `string`  | Token price in USD.                                                      |
| `price_expiry`              | `string`  | Unix timestamp when the token price signature expires.                   |
| `price_signature`           | `string`  | Hex-encoded admin signature over the token price data.                   |
| `token_contract_id`         | `string`  | Hex-encoded token contract ID (32 bytes).                                |
| `xlm_price`                 | `string`  | XLM price in USD.                                                        |
| `xlm_price_expiry`          | `string`  | Unix timestamp when the XLM price signature expires.                     |
| `xlm_price_signature`       | `string`  | Hex-encoded admin signature over the XLM price data.                     |

### `CreateStreamEntry`

One entry in a `createMultipleStreams` batch. Carries everything `createStream` would otherwise take as positional arguments.

| Field       | Type                 | Description                                                     |
| ----------- | -------------------- | --------------------------------------------------------------- |
| `stream_id` | `string`             | 32-byte hex-encoded stream identifier (`BytesN`).               |
| `receiver`  | `string`             | Receiver account address.                                       |
| `token`     | `string`             | Token contract address (SAC or custom) for this stream.         |
| `params`    | `CreateStreamParams` | Full stream configuration; amounts are in human-readable units. |

### `TopupEntry`

One entry in a `topupAllStreams` or `triggerTopupAllStreams` batch.

| Field       | Type     | Description                                                               |
| ----------- | -------- | ------------------------------------------------------------------------- |
| `stream_id` | `string` | 32-byte hex-encoded stream identifier (`BytesN`).                         |
| `amount`    | `string` | Top-up amount in human-readable display units (scaled by token decimals). |

### `BatchChunk<T>`

A single chunk in a batch submission, pairing a deferred payload with caller-defined metadata (e.g. the stream IDs contained in the chunk).

| Field     | Type                 | Description                                                     |
| --------- | -------------------- | --------------------------------------------------------------- |
| `payload` | `TransactionPayload` | The deferred transaction payload for this chunk.                |
| `meta`    | `T`                  | Caller-defined metadata — for batch methods this is `string[]`. |

### `BatchChunkResult<T>`

Result of processing one chunk in a batch.

| Field        | Type                          | Description                                                              |
| ------------ | ----------------------------- | ------------------------------------------------------------------------ |
| `meta`       | `T`                           | The same metadata from the original `BatchChunk`.                        |
| `response`   | `Api.GetTransactionResponse?` | Present when the chunk was successfully built **and** submitted.         |
| `buildError` | `Error?`                      | Present when the chunk failed during `buildTransactionXDR` (simulation). |

Exactly one of `response` or `buildError` will be set.

## TransactionPayload

All write methods return a `TransactionPayload` rather than submitting directly, giving consumers control over the signing/submission flow.

```ts
class TransactionPayload {
  readonly server: Server;
  readonly sourcePublicKey: string;
  readonly operations: xdr.Operation[];
  readonly networkPassphrase: string;
  readonly memo?: string;

  buildTransaction(
    options?: TransactionOptions,
  ): Promise<Transaction | FeeBumpTransaction>;
  buildTransactionXDR(options?: TransactionOptions): Promise<string>;
  signAndSubmit(
    options?: TransactionOptions,
  ): Promise<Api.GetTransactionResponse>;
}
```

Build + simulate work is deferred until one of these methods is called, so the source account's sequence number and Soroban resource fees stay fresh even if the user pauses between the SDK call and the wallet popup.

* **`buildTransaction(options?)`** — Fetches a fresh sequence number, simulates the operation, applies the Soroban footprint and resource fees, and returns the prepared transaction (unsigned). Throws `"Simulation failed: ..."` if the RPC reports a simulation error.
* **`buildTransactionXDR(options?)`** — Convenience wrapper that returns the prepared transaction's base64 XDR string. Useful for handing to an external signer (hardware wallet, multi-sig coordinator, browser wallet).
* **`signAndSubmit(options?)`** — Builds, signs via the configured `WalletAdapter`, submits to the network, and polls until a final status is available.

`options` (`TransactionOptions`) shallow-merges on top of any construction-time options; per-call values win.

This separation lets you, for example, build the payload server-side and submit it client-side, or sign with multiple parties before submission.

### Batch submission

`TransactionPayload` also exposes a static `signAndSubmitBatch` method for executing multiple chunks as a single batch. All payloads must share the same server, source public key, and network passphrase. Sequence numbers are assigned contiguously starting from the current ledger sequence, so the transactions can be signed all at once (via `WalletAdapter.signAllTransactions`) and then submitted sequentially.

#### `TransactionPayload.signAndSubmitBatch(chunks, options?): Promise<BatchChunkResult<T>[]>`

* `chunks: BatchChunk<T>[]` — array of chunks to execute.
* `options.abortOnFailure` — optional, defaults to `true`. When `true`, the first failing chunk aborts the entire batch and throws a `BatchTransactionError`.
* Returns an array of `BatchChunkResult<T>` in the same order as `chunks`.

When `abortOnFailure` is `false`, failed chunks are included in the results with their error or non-success status, letting you inspect partial success:

```ts
const results = await TransactionPayload.signAndSubmitBatch(chunks, {
  abortOnFailure: false,
});

for (let i = 0; i < results.length; i++) {
  const result = results[i];
  if (result.buildError) {
    console.error(`Chunk ${i + 1} failed to build:`, result.buildError.message);
  } else if (result.response?.status !== "SUCCESS") {
    console.error(`Chunk ${i + 1} failed on-chain:`, result.response?.txHash);
  } else {
    console.log(`Chunk ${i + 1} succeeded:`, result.response.txHash);
  }
}
```

### `BatchTransactionError`

Thrown by `signAndSubmitBatch` when `abortOnFailure` is `true` and a chunk fails.

| Property      | Type                 | Description                                        |
| ------------- | -------------------- | -------------------------------------------------- |
| `results`     | `BatchChunkResult[]` | Every result processed so far, including failures. |
| `failedIndex` | `number`             | Index of the first failing chunk.                  |
| `meta`        | `unknown`            | Metadata of the first failing chunk.               |

## Wallet adapter

The SDK is signer-agnostic. Any object satisfying the `WalletAdapter` interface works — Freighter, Albedo, Rabet, a `Keypair`-backed local signer, or a custom remote signer.

### Example with a local `Keypair`

```ts
import { Keypair, Networks, TransactionBuilder } from "@stellar/stellar-sdk";

const keypair = Keypair.fromSecret(process.env.SECRET!);
const wallet: WalletAdapter = {
  getPublicKey: async () => keypair.publicKey(),
  signTransaction: async (txXDR) => {
    const tx = TransactionBuilder.fromXDR(txXDR, Networks.TESTNET);
    tx.sign(keypair);
    return tx.toXDR();
  },
  signAllTransactions: async (txXDRs) =>
    txXDRs.map((txXDR) => {
      const tx = TransactionBuilder.fromXDR(txXDR, Networks.TESTNET);
      tx.sign(keypair);
      return tx.toXDR();
    }),
};
```

### Example with Freighter (browser)

```ts
import freighter from "@stellar/freighter-api";

const wallet: WalletAdapter = {
  getPublicKey: () => freighter.getPublicKey(),
  signTransaction: (txXDR) =>
    freighter.signTransaction(txXDR, { networkPassphrase: Networks.PUBLIC }),
  signAllTransactions: async (txXDRs) =>
    Promise.all(
      txXDRs.map((txXDR) =>
        freighter.signTransaction(txXDR, {
          networkPassphrase: Networks.PUBLIC,
        }),
      ),
    ),
};
```

## Error handling

The SDK distinguishes three classes of errors:

1. **Wallet / caller mismatch** — thrown immediately when the supplied `admin`/`sender`/`caller` does not match the wallet's public key.
2. **Simulation errors** — returned by the Soroban RPC during transaction simulation. If the raw error contains a recognized contract error code, a `ContractError` is thrown instead of a plain `Error`.
3. **Transaction failures** — on-chain `FAILED` status. The SDK inspects `diagnosticEvents` for a typed `StreamError` code and throws a `ContractError` when one is found.

### `ContractError`

When the streaming contract rejects an operation it returns a `StreamError` (a numeric code). The SDK decodes this into a `ContractError` so you can react programmatically instead of parsing strings.

```ts
import {
  ContractError,
  StreamErrorCode,
} from "@zebec-network/stellar-payroll-sdk";

try {
  const payload = await service.cancelStream(caller, streamId);
  await payload.signAndSubmit();
} catch (err) {
  if (err instanceof ContractError) {
    console.error(`Contract rejected: ${err.errorName} (code ${err.code})`);

    if (err.code === StreamErrorCode.StreamNotCancelable) {
      // show user-friendly "this stream cannot be cancelled" message
    }
  } else {
    console.error("Unexpected error:", (err as Error).message);
  }
}
```

| Property    | Type     | Description                                           |
| ----------- | -------- | ----------------------------------------------------- |
| `code`      | `number` | Numeric error code returned by the contract.          |
| `errorName` | `string` | Human-readable enum name (e.g. `"StreamNotStarted"`). |
| `message`   | `string` | `"Contract error #2: StreamNotStarted"`               |

### `StreamErrorCode`

All known contract error codes are exported as a typed enum:

```ts
enum StreamErrorCode {
  StreamAlreadyCanceled = 1,
  StreamNotStarted = 2,
  StreamAlreadyEnded = 3,
  StreamNotPausable = 4,
  InsufficientFundsToWithdraw = 5,
  StreamNotCancelable = 6,
  SenderCannotBeReceiver = 7,
  InvalidStartTime = 8,
  InvalidDuration = 9,
  InvalidAmount = 10,
  InvalidCliffPercentage = 11,
  FeesExceedAmount = 12,
  InvalidTopupAmount = 14,
  AutoTopupNotEnabled = 15,
  TopupNotAvailable = 16,
  StreamNotCancelableByRecipient = 17,
  RecipientTransferNotAllowed = 18,
  UnauthorizedWithdrawal = 19,
  StreamAlreadyExists = 20,
  StreamNameTooLong = 21,
  InsufficientAllowanceForTopup = 22,
  MaxWhitelistedTokensExceeded = 30,
  TokenNotWhitelisted = 31,
  InvalidFeeConfig = 32,
  MaxFeetiersExceeded = 35,
  InvalidFrequency = 36,
  MaxFrequenciesExceeded = 37,
  PriceExpired = 38,
  TokenMismatch = 39,
  InvalidPriceSignature = 40,
  FeeTiersNotSet = 41,
}
```

### Polling timeout

`signAndSubmit` polls the RPC until the transaction is final. To prevent infinite loops, polling stops after **30 attempts** (roughly 30 seconds) and throws:

```
Transaction polling timed out after 30 attempts. Hash: <hash>
```

### Utilities

The following helpers are exported for advanced use cases:

* `parseContractError(error)` — tries to extract a `ContractError` from a raw string, number, or SDK error object.
* `parseContractErrorFromDiagnosticEvents(events)` — scans Soroban `DiagnosticEvent[]` for a contract error topic.
* `formatSimulationError(context, rawError)` — wraps a simulation error; returns `ContractError` when a code is detected, otherwise a plain `Error`.
* `formatTransactionError(resultXdr, diagnosticEvents?)` — wraps an on-chain failure; decodes diagnostic events first, then falls back to the raw `resultXdr`.

## Related

* [Streaming SDK overview](/developer-docs/sdks/streaming-sdk.md)
* [Enterprise Payroll streaming guide](/zebec-application-suite/enterprise-payroll/streaming-payroll.md)
* [Partner API overview](/developer-docs/partner-api.md)
