# Zebec Stream SDK

A TypeScript SDK for interacting with the Zebec Stream protocol on Solana. This SDK provides a comprehensive interface for creating, managing, and interacting with payment streams on the Zebec Network.

## Features

- **Stream Management**: Create, cancel, pause, resume, and withdraw from payment streams
- **Token Streaming**: SPL token streaming with configurable cliff, frequency, and duration
- **Tiered Fee System**: Dynamic fee calculation based on stream amount via on-chain fee tiers
- **Token Whitelisting**: Admin-controlled token whitelist with full metadata resolution via Metaplex
- **Admin Operations**: Initialize and update global stream configuration, fee tiers, and fee vaults
- **Flexible Permissions**: Granular per-stream control — cancellation, transfer, topup, pause, and rate update permissions
- **PDA Utilities**: Helpers for deriving stream config and stream vault program-derived addresses
- **Provider Abstractions**: Separate read-only and read-write provider helpers
- **Type Safety**: Full TypeScript support with comprehensive type definitions

## Installation

```bash
npm install @zebec-network/zebec-stream-sdk
```

```bash
yarn add @zebec-network/zebec-stream-sdk
```

## Quick Start

### Setting Up the Service

```typescript
import { Connection } from "@solana/web3.js";
import { ZebecStreamService, createAnchorProvider } from "@zebec-network/zebec-stream-sdk";

// Create a connection to devnet or mainnet-beta
const connection = new Connection("https://api.devnet.solana.com");

// Create an AnchorProvider using your wallet
const provider = createAnchorProvider(connection, wallet);

// Initialize the service with a config name and network
const streamService = ZebecStreamService.create("my-app-config", provider, "devnet");
```

### Creating a Stream

```typescript
const tx = await streamService.createStream({
  sender: "SenderPublicKeyHere",
  receiver: "ReceiverPublicKeyHere",
  streamToken: "TokenMintAddressHere",
  amount: "1000",              // Amount in human-readable token units
  duration: 86400,             // Total stream duration in seconds (1 day)
  autoWithdrawFrequency: 3600, // Auto-withdrawal interval in seconds (1 hour)
  streamName: "Monthly Salary",
  startNow: true,
  startTime: Math.floor(Date.now() / 1000),
  cliffPercentage: 0,
  automaticWithdrawal: true,
  cancelableByRecipient: true,
  cancelableBySender: true,
  isPausable: true,
  transferableByRecipient: false,
  transferableBySender: false,
  canTopup: true,
  rateUpdatable: false,
});

const signature = await tx.execute();
console.log("Stream created:", signature);
```

## API Reference

### `ZebecStreamService`

The main service class for interacting with Zebec streams.

#### `ZebecStreamService.create(streamConfigName, provider, network)`

Creates a new service instance. Validates that the provider connection network matches the specified `network`.

```typescript
static create(
  streamConfigName: string,
  provider: Provider,
  network: "mainnet-beta" | "devnet"
): ZebecStreamService
```

| Parameter | Type | Description |
| --- | --- | --- |
| `streamConfigName` | `string` | Unique name identifying the stream config PDA |
| `provider` | `Provider` | Anchor or readonly provider |
| `network` | `RpcNetwork` | `"mainnet-beta"` or `"devnet"` |

---

### Stream Operations

#### `createStream(params)`

Creates a new payment stream. Automatically fetches the fee quote from the Zebec backend and includes the fee transfer instruction in the transaction.

**Parameters** (`CreateStreamParams`):

| Field | Type | Description |
| --- | --- | --- |
| `sender` | `Address` | Sender's public key |
| `receiver` | `Address` | Recipient's public key |
| `streamToken` | `Address` | SPL token mint address |
| `amount` | `Numeric` | Amount to stream in human-readable token units |
| `duration` | `number` | Stream duration in seconds |
| `autoWithdrawFrequency` | `number` | Auto-withdrawal interval in seconds (must be in config's allowed frequencies when `automaticWithdrawal` is true) |
| `streamName` | `string` | Human-readable stream name (max 128 bytes) |
| `startNow` | `boolean` | Whether to start immediately |
| `startTime` | `number` | Unix timestamp for scheduled start |
| `cliffPercentage` | `Numeric` | Percentage of amount locked until cliff expires |
| `automaticWithdrawal` | `boolean` | Enable automatic withdrawals |
| `cancelableByRecipient` | `boolean` | Allow recipient to cancel |
| `cancelableBySender` | `boolean` | Allow sender to cancel |
| `isPausable` | `boolean` | Allow stream to be paused |
| `transferableByRecipient` | `boolean` | Allow recipient to transfer |
| `transferableBySender` | `boolean` | Allow sender to transfer |
| `canTopup` | `boolean` | Allow adding funds to stream |
| `rateUpdatable` | `boolean` | Allow rate modifications |
| `feePayer?` | `Address` | Optional custom fee payer (defaults to sender) |
| `streamMetadataKeypair?` | `Keypair` | Optional custom metadata keypair |

**Returns:** `Promise<TransactionPayload>`

---

#### `cancelStream(params)`

Cancels an existing stream. Either sender or receiver can cancel (if allowed by stream permissions).

**Parameters** (`CancelStreamParams`):

| Field | Type | Description |
| --- | --- | --- |
| `streamMetadata` | `Address` | Stream metadata account address |
| `user` | `Address` | User canceling the stream (must be sender or receiver) |
| `feePayer?` | `Address` | Optional custom fee payer |

**Returns:** `Promise<TransactionPayload>`

---

#### `pauseResumeStream(params)`

Toggles a stream between paused and active states. Only the sender can call this.

**Parameters:**

| Field | Type | Description |
| --- | --- | --- |
| `streamMetadata` | `Address` | Stream metadata account address |

**Returns:** `Promise<TransactionPayload>`

---

#### `withdrawStream(params)`

Withdraws vested tokens from a stream to the receiver.

**Parameters** (`WithdrawStreamParams`):

| Field | Type | Description |
| --- | --- | --- |
| `streamMetadata` | `Address` | Stream metadata account address |
| `receiver` | `Address` | Recipient's public key |
| `withdrawer?` | `Address` | Optional custom withdrawer (defaults to receiver) |
| `feePayer?` | `Address` | Optional custom fee payer |

**Returns:** `Promise<TransactionPayload>`

---

#### `changeStreamReceiver(params)`

Transfers stream ownership to a new recipient.

**Parameters** (`ChangeStreamReceiverParams`):

| Field | Type | Description |
| --- | --- | --- |
| `streamMetadata` | `Address` | Stream metadata account address |
| `newRecipient` | `Address` | New recipient's public key |
| `signer` | `Address` | Currently authorized signer |

**Returns:** `Promise<TransactionPayload>`

---

### Information Retrieval

#### `getStreamMetadataInfo(streamMetadata, commitment?)`

Retrieves detailed information about a stream.

**Returns:** `Promise<StreamMetadataInfo>`

```typescript
const info = await streamService.getStreamMetadataInfo("StreamMetadataAddressHere");
console.log(info.parties.sender.toBase58());
console.log(info.financials.depositedAmount); // human-readable
console.log(info.schedule.startTime);
console.log(info.permissions.isPausable);
```

---

#### `getStreamConfigInfo(configName, commitment?)`

Retrieves the global stream configuration for a given config name.

**Returns:** `Promise<StreamConfigInfo>`

```typescript
const config = await streamService.getStreamConfigInfo("my-app-config");
console.log(config.frequencies);   // allowed auto-withdraw intervals
console.log(config.feeTiers);      // tiered fee schedule
console.log(config.feeVault.toBase58());
```

---

#### `getWhitelistedTokens(configName, commitment?)`

Fetches the list of whitelisted tokens with full on-chain mint info and Metaplex metadata.

**Returns:** `Promise<TokenMetadata[]>`

```typescript
const tokens = await streamService.getWhitelistedTokens("my-app-config");
tokens.forEach(t => {
  console.log(t.metadata?.symbol, t.mint.toBase58());
});
```

---

### Admin Operations

#### `initializeStreamConfig(params)`

Initializes the global stream configuration (admin only, one-time setup).

**Parameters** (`InitializeStreamConfigParams`):

```typescript
await streamService.initializeStreamConfig({
  admin: adminAddress, // optional, defaults to provider wallet
  config: {
    baseFeePercent: "0.1",
    platformFeePercent: "0.05",
    frequencies: [3600, 86400, 604800], // hourly, daily, weekly
    withdrawAccount: "WithdrawAccountAddressHere",
    feeVault: "FeeVaultAddressHere",
    feeTiers: [
      { minThreshold: "0",     maxThreshold: "1000",  feeRateInPercent: "1.0" },
      { minThreshold: "1000",  maxThreshold: "10000", feeRateInPercent: "0.75" },
      { minThreshold: "10000", maxThreshold: "99999999", feeRateInPercent: "0.5" },
    ],
  },
});
```

---

#### `updateStreamConfig(params)`

Updates an existing stream configuration (admin only).

**Parameters** (`UpdateStreamConfigParams`): Same shape as `InitializeStreamConfigParams`.

**Returns:** `Promise<TransactionPayload>`

---

#### `whiteListTokens(params)`

Adds token mint addresses to the whitelist (admin only).

**Parameters** (`WhiteListTokensParams`):

| Field | Type | Description |
| --- | --- | --- |
| `admin` | `Address` | Admin public key |
| `tokens` | `Address[]` | Array of token mint addresses to whitelist |

**Returns:** `Promise<TransactionPayload>`

---

### Low-Level Instruction Builders

Each high-level method has a corresponding instruction builder for composing custom transactions:

| Method | Returns |
| --- | --- |
| `getCreateStreamInstruction(...)` | `Promise<TransactionInstruction>` |
| `getCancelStreamInstruction(...)` | `Promise<TransactionInstruction>` |
| `getPauseResumeStreamInstruction(...)` | `Promise<TransactionInstruction>` |
| `getWithdrawStreamInstruction(...)` | `Promise<TransactionInstruction>` |
| `getChangeStreamReceiverInstruction(...)` | `Promise<TransactionInstruction>` |
| `getWhitelistTokensInstruction(...)` | `Promise<TransactionInstruction>` |
| `getInitializeStreamConfigInstruction(...)` | `Promise<TransactionInstruction>` |
| `getUpdateStreamConfigInstruction(...)` | `Promise<TransactionInstruction>` |

---

## Provider Setup

### AnchorProvider (Read/Write)

Use for operations that sign transactions:

```typescript
import { createAnchorProvider } from "@zebec-network/zebec-stream-sdk";

const provider = createAnchorProvider(connection, wallet, {
  commitment: "confirmed",
  preflightCommitment: "confirmed",
});
```

### ReadonlyProvider (Read-Only)

Use for read-only queries without a wallet:

```typescript
import { createReadonlyProvider } from "@zebec-network/zebec-stream-sdk";

const provider = createReadonlyProvider(connection, optionalWalletAddress);
```

---

## PDA Utilities

```typescript
import { deriveStreamConfigPda, deriveStreamVaultPda } from "@zebec-network/zebec-stream-sdk";

// Derive the stream config PDA
const [configPda] = deriveStreamConfigPda("my-app-config", programId);

// Derive the stream vault PDA for a given stream metadata address
const [vaultPda] = deriveStreamVaultPda(streamMetadataAddress, programId);
```

---

## Types

### `StreamMetadataInfo`

```typescript
type StreamMetadataInfo = {
  address: PublicKey;
  parties: {
    sender: PublicKey;
    receiver: PublicKey;
  };
  financials: {
    streamToken: PublicKey;
    cliffPercentage: number;
    depositedAmount: string; // human-readable
    withdrawnAmount: string; // human-readable
  };
  schedule: {
    startTime: number;
    endTime: number;
    lastWithdrawTime: number;
    frequency: number;
    duration: number;
    pausedTimestamp: number;
    pausedInterval: number;
    canceledTimestamp: number;
  };
  permissions: {
    cancelableBySender: boolean;
    cancelableByRecipient: boolean;
    automaticWithdrawal: boolean;
    transferableBySender: boolean;
    transferableByRecipient: boolean;
    canTopup: boolean;
    isPausable: boolean;
    rateUpdatable: boolean;
  };
  streamName: string;
};
```

### `StreamConfigInfo`

```typescript
type StreamConfigInfo = {
  address: PublicKey;
  admin: PublicKey;
  withdrawerAccount: PublicKey;
  whitelistedTokens: PublicKey[];
  platformFee: number;
  baseFee: number;
  frequencies: number[];
  feeTiers: FeeTier[];
  feeVault: PublicKey;
};
```

### `FeeTier`

```typescript
type FeeTier = {
  minThreshold: Numeric; // minimum USD amount for this tier
  maxThreshold: Numeric; // maximum USD amount for this tier
  feeRateInPercent: Numeric;
};
```

### `StreamFeeInfo`

Returned by the Zebec backend fee quote API and used internally during stream creation:

```typescript
type StreamFeeInfo = {
  tokenSymbol: string;
  mintAddress: string;
  chain: string;
  streamAmount: string;
  streamAmountUi: string;
  tokenPriceUsd: number;
  streamAmountUsd: number;
  feeTier: { tier: number; range: string; feeRatePercent: number };
  feeRatePercent: number;
  feeAmountUsd: number;
  feeToken: { symbol: string; decimals: number; priceUsd: number; mintAddress: string };
  feeAmount: number;
  feeAmountRaw: string;
};
```

### `TokenMetadata`

```typescript
type TokenMetadata = {
  mint: PublicKey;
  decimals: number;
  freezeAuthority: PublicKey | null;
  supply: string;
  isInitialized: boolean;
  mintAuthority: PublicKey | null;
  metadata: {
    address: PublicKey;
    updateAuthority: PublicKey;
    name: string;
    symbol: string;
    uri: string;
  } | null;
};
```

---

## Usage Examples

### Complete Stream Lifecycle

```typescript
import { Connection } from "@solana/web3.js";
import { ZebecStreamService, createAnchorProvider } from "@zebec-network/zebec-stream-sdk";

const connection = new Connection("https://api.devnet.solana.com");
const provider = createAnchorProvider(connection, wallet);
const streamService = ZebecStreamService.create("my-app-config", provider, "devnet");

// 1. Create a stream
const createTx = await streamService.createStream({
  sender: senderAddress,
  receiver: receiverAddress,
  streamToken: tokenMintAddress,
  amount: "1000",
  duration: 86400,
  autoWithdrawFrequency: 3600,
  streamName: "Test Stream",
  startNow: true,
  startTime: Math.floor(Date.now() / 1000),
  cliffPercentage: 0,
  automaticWithdrawal: true,
  cancelableByRecipient: false,
  cancelableBySender: true,
  isPausable: true,
  transferableByRecipient: false,
  transferableBySender: false,
  canTopup: false,
  rateUpdatable: false,
});
const streamSignature = await createTx.execute();
console.log("Stream created:", streamSignature);

// 2. Fetch stream info
const streamInfo = await streamService.getStreamMetadataInfo(streamMetadataAddress);
console.log("Deposited:", streamInfo.financials.depositedAmount);

// 3. Withdraw vested tokens
const withdrawTx = await streamService.withdrawStream({
  streamMetadata: streamMetadataAddress,
  receiver: receiverAddress,
});
await withdrawTx.execute();

// 4. Cancel the stream
const cancelTx = await streamService.cancelStream({
  streamMetadata: streamMetadataAddress,
  user: senderAddress,
});
await cancelTx.execute();
```

### Pause and Resume a Stream

```typescript
const pauseTx = await streamService.pauseResumeStream({
  streamMetadata: streamMetadataAddress,
});
await pauseTx.execute(); // toggles between paused and active
```

### Change Stream Receiver

```typescript
const changeTx = await streamService.changeStreamReceiver({
  streamMetadata: streamMetadataAddress,
  newRecipient: newRecipientAddress,
  signer: currentRecipientAddress,
});
await changeTx.execute();
```

### Fetch Whitelisted Tokens

```typescript
const tokens = await streamService.getWhitelistedTokens("my-app-config");
tokens.forEach((token) => {
  console.log(token.metadata?.name, token.metadata?.symbol);
  console.log("Mint:", token.mint.toBase58());
  console.log("Decimals:", token.decimals);
});
```

### Error Handling

```typescript
try {
  const tx = await streamService.createStream(params);
  const signature = await tx.execute();
  console.log("Success:", signature);
} catch (error) {
  if (error.message.includes("Invalid stream frequency")) {
    console.error("autoWithdrawFrequency must be one of the config's allowed frequencies");
  } else {
    console.error("Stream creation failed:", error);
  }
}
```

---

## Constants

| Constant | Description |
| --- | --- |
| `STREAM_PROGRAM_ID` | Program ID per network (`zSTRMmYcFF8SPdHmsAmAUjBnx4zDHvnqqGz2mPcc5QC`) |
| `STREAM_PROGRAM_LOOKUP_TABLE_ADDRESS` | Address lookup table per network |
| `SUPERAPP_BACKEND_URL` | Zebec backend URL per network (used for fee quotes) |
| `STREAM_NAME_BUFFER_SIZE` | Fixed buffer size for stream names (128 bytes) |

---

## Network Support

| Network | Status |
| --- | --- |
| Mainnet Beta | Supported |
| Devnet | Supported |
| Testnet | Not supported |

---

## Dependencies

| Package | Purpose |
| --- | --- |
| `@coral-xyz/anchor` | Anchor framework for Solana program interaction |
| `@solana/web3.js` | Solana Web3 JavaScript API |
| `@solana/spl-token` | SPL token utilities |
| `@metaplex-foundation/mpl-token-metadata` | On-chain token metadata resolution |
| `@metaplex-foundation/umi` | Metaplex UMI framework |
| `@metaplex-foundation/umi-bundle-defaults` | UMI default bundle |
| `@metaplex-foundation/umi-web3js-adapters` | UMI ↔ web3.js adapters |
| `@zebec-network/core-utils` | Zebec core utility functions (BPS conversions, etc.) |
| `@zebec-network/solana-common` | Common Solana helpers (ATAs, transaction payload, etc.) |
| `bignumber.js` | Arbitrary-precision arithmetic for token amounts |

---

## Development

### Install Dependencies

```bash
npm install
```

### Build

```bash
npm run build
```

### Clean Build Artifacts

```bash
npm run clean
```

### Run Tests

```bash
npm test
```

### Run a Single Test File

```bash
npm run test:single -- test/e2e/stream/createStream.test.ts
```

### Format Code

```bash
npm run format
```

---

## Publishing

1. Bump the version in [package.json](package.json).

2. Build the package:

   ```bash
   npm run build
   ```

3. Publish to npm:

   ```bash
   npm publish --access public
   ```

Only the `dist/` directory is included in the published package (as specified by the `files` field in [package.json](package.json)).

---

## License

This project is licensed under the MIT License.

## Support

For issues and questions:

- GitHub Issues: [Repository Issues](https://github.com/zebec-network/zebec-stream-sdk/issues)
