From d663d700e915500c61f2f943b45eb6fe1560f3d5 Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Fri, 8 May 2026 14:26:23 +0530 Subject: [PATCH] YNU-864: Native TS SDK polish and terminology - Add a pinned SDK decision table and align Build SDK terminology on Nitronode. - Update native TypeScript setup, config, API, and examples with sandbox placeholders, error recovery, source-verified method shapes, and browser signers that compile against SDK interfaces. - Rewrite multi-party app sessions around native v1 AppDefinitionV1, app registration, signed state updates, withdraw, and close intents. - Clarify migration-guide amount semantics and codemod source workflow. Resolves: F-008 (Build SDK slice), F-009 (typescript page slice), F-013, F-014, F-024, F-025, F-026, F-027, F-028, F-029. --- docs/nitrolite/build/sdk/go/api-reference.mdx | 6 +- .../build/sdk/go/getting-started.mdx | 32 +- docs/nitrolite/build/sdk/index.md | 23 +- docs/nitrolite/build/sdk/migration-guide.md | 61 +- .../build/sdk/multi-party-app-sessions.mdx | 689 ++++++------------ .../build/sdk/typescript/api-reference.mdx | 25 + .../build/sdk/typescript/configuration.mdx | 31 +- .../build/sdk/typescript/examples.mdx | 177 +++-- .../build/sdk/typescript/getting-started.mdx | 47 +- 9 files changed, 499 insertions(+), 592 deletions(-) diff --git a/docs/nitrolite/build/sdk/go/api-reference.mdx b/docs/nitrolite/build/sdk/go/api-reference.mdx index e73c5f1..f6a5d25 100644 --- a/docs/nitrolite/build/sdk/go/api-reference.mdx +++ b/docs/nitrolite/build/sdk/go/api-reference.mdx @@ -1,6 +1,6 @@ --- title: API Reference -description: Complete API reference for the Clearnode Go SDK +description: Complete API reference for the Nitronode Go SDK sidebar_position: 2 --- @@ -92,7 +92,7 @@ batchID, err := client.RebalanceAppSessions(ctx, signedUpdates) --- -## Session Keys — App Sessions +## Session Keys: App Sessions ```go state := app.AppSessionKeyStateV1{ @@ -112,7 +112,7 @@ states, err := client.GetLastAppKeyStates(ctx, userAddress, nil) --- -## Session Keys — Channels +## Session Keys: Channels ```go state := core.ChannelSessionKeyStateV1{ diff --git a/docs/nitrolite/build/sdk/go/getting-started.mdx b/docs/nitrolite/build/sdk/go/getting-started.mdx index fc002d2..93806ef 100644 --- a/docs/nitrolite/build/sdk/go/getting-started.mdx +++ b/docs/nitrolite/build/sdk/go/getting-started.mdx @@ -1,17 +1,17 @@ --- title: Getting Started -description: Install and set up the Clearnode Go SDK +description: Install and set up the Nitronode Go SDK sidebar_position: 1 --- # Getting Started with the Go SDK -Go SDK for Clearnode payment channels providing both high-level and low-level operations in a unified client. It has full feature parity with the TypeScript SDK. +Go SDK for Nitronode payment channels providing both high-level and low-level operations in a unified client. ## Requirements -- Go 1.21+ -- Running Clearnode instance +- Go 1.25.7 or later +- Running Nitronode instance - Blockchain RPC endpoint (for `Checkpoint` settlement) ## Installation @@ -22,6 +22,10 @@ go get github.com/layer-3/nitrolite/sdk/go ## Quick Start +:::info Sandbox URL - coming soon +Use your Nitronode WebSocket URL in `sdk.NewClient()`. The public sandbox URL is intentionally shown as `` until the canonical host is pinned. +::: + ```go package main @@ -42,7 +46,7 @@ func main() { // 2. Create unified client client, _ := sdk.NewClient( - "wss://clearnode.example.com/ws", + "", stateSigner, txSigner, sdk.WithBlockchainRPC(80002, "https://polygon-amoy.alchemy.com/v2/KEY"), @@ -120,11 +124,11 @@ txHash, _ := client.Checkpoint(ctx, "usdc") ### Channel Lifecycle -1. **Void** — No channel exists -2. **Create** — `Deposit()` creates channel on-chain via `Checkpoint()` -3. **Open** — Channel active; can deposit, withdraw, transfer -4. **Challenged** — Dispute initiated -5. **Closed** — Channel finalized +1. **Void**: No channel exists +2. **Create**: `Deposit()` creates channel on-chain via `Checkpoint()` +3. **Open**: Channel active; can deposit, withdraw, transfer +4. **Challenged**: Dispute initiated +5. **Closed**: Channel finalized ## Configuration Options @@ -150,7 +154,7 @@ if err != nil { ``` Common errors: -- `"home blockchain not set for asset"` — Missing `SetHomeBlockchain` -- `"blockchain RPC not configured for chain"` — Missing `WithBlockchainRPC` -- `"no channel exists for asset"` — `Checkpoint` without a co-signed state -- `"insufficient balance"` — Not enough funds in channel/wallet +- `"home blockchain not set for asset"`: Missing `SetHomeBlockchain` +- `"blockchain RPC not configured for chain"`: Missing `WithBlockchainRPC` +- `"no channel exists for asset"`: `Checkpoint` without a co-signed state +- `"insufficient balance"`: Not enough funds in channel/wallet diff --git a/docs/nitrolite/build/sdk/index.md b/docs/nitrolite/build/sdk/index.md index 3493fb4..3e13a68 100644 --- a/docs/nitrolite/build/sdk/index.md +++ b/docs/nitrolite/build/sdk/index.md @@ -8,27 +8,30 @@ sidebar_position: 1 Yellow Network provides official SDKs for building applications on top of Nitrolite payment channels. All SDKs share the same two-step architecture: **build and co-sign states off-chain**, then **settle on-chain when needed**. -## Available SDKs +## Choose your SDK -| Package | Language | Description | -|---------|----------|-------------| -| [`@yellow-org/sdk`](./typescript/getting-started) | TypeScript | Main SDK with full API coverage | -| [`@yellow-org/sdk-compat`](./typescript-compat/overview) | TypeScript | Compatibility layer for migrating from v0.5.3 | -| [`clearnode-go-sdk`](./go/getting-started) | Go | Go SDK with full feature parity | +| Builder type | Package | Import | Notes | +|---|---|---|---| +| New TypeScript app | [`@yellow-org/sdk@1.2.1`](./typescript/getting-started) | `import { Client, createSigners } from '@yellow-org/sdk'` | Native v1 SDK; uses `Decimal` amounts. | +| Migrating from 0.5.3 (TS) | [`@yellow-org/sdk-compat@1.2.1`](./typescript-compat/overview) | `import { NitroliteClient } from '@yellow-org/sdk-compat'` | Minimal-diff bridge for `@erc7824/nitrolite@0.5.3`; see the compat codemod workflow. | +| Go integration | [`github.com/layer-3/nitrolite/sdk/go`](./go/getting-started) | `import "github.com/layer-3/nitrolite/sdk/go"` | Root Go module; no separate Go module for the SDK package. | +| Agentic IDE / AI agents | `@yellow-org/sdk-mcp@1.2.1` | `npx -y @yellow-org/sdk-mcp` | MCP server with binary `yellow-sdk-mcp`; coming soon on npm. | +| 0.5.3 codemod | `@yellow-org/nitrolite-codemod@1.0.0` | `npx -y @yellow-org/nitrolite-codemod` | One-time migration tool; use the source workflow until the npm package publishes. | +| Smart contracts | `contracts/src/ChannelHub.sol` | See the deployment repository | v1 ChannelHub entrypoint for settlement. | ## Architecture All SDKs follow a unified design: -- **State Operations** (off-chain): `deposit()`, `withdraw()`, `transfer()`, `closeHomeChannel()`, `acknowledge()` — build and co-sign channel states without touching the blockchain. -- **Blockchain Settlement**: `checkpoint()` — the single entry point for all on-chain transactions. Routes to the correct contract method based on transition type and channel status. +- **State Operations** (off-chain): `deposit()`, `withdraw()`, `transfer()`, `closeHomeChannel()`, `acknowledge()`. Build and co-sign channel states without touching the blockchain. +- **Blockchain Settlement**: `checkpoint()`. The single entry point for all on-chain transactions. Routes to the correct contract method based on transition type and channel status. - **Low-Level Operations**: Direct RPC access for app sessions, session keys, queries, and custom flows. ```mermaid sequenceDiagram participant App participant SDK - participant Node as Clearnode + participant Node as Nitronode participant Chain as Blockchain App->>SDK: deposit(chain, asset, amount) @@ -46,4 +49,4 @@ sequenceDiagram - **New TypeScript projects**: Use [`@yellow-org/sdk`](./typescript/getting-started) directly. - **Migrating from v0.5.3**: Use [`@yellow-org/sdk-compat`](./typescript-compat/overview) to minimise code changes, then migrate to the main SDK at your own pace. -- **Go projects**: Use the [Go SDK](./go/getting-started) — it has full feature parity with the TypeScript SDK. +- **Go projects**: Use the [Go SDK](./go/getting-started) for backend services and CLI tooling. diff --git a/docs/nitrolite/build/sdk/migration-guide.md b/docs/nitrolite/build/sdk/migration-guide.md index f11d61f..d8ba405 100644 --- a/docs/nitrolite/build/sdk/migration-guide.md +++ b/docs/nitrolite/build/sdk/migration-guide.md @@ -10,13 +10,28 @@ import TabItem from '@theme/TabItem'; # Migration Guide -If you are coming from an earlier version of VirtualApp, you will need to account for the following breaking changes. +If you are coming from an earlier version of VirtualApp, first choose the migration surface: + +- If you're using `@yellow-org/sdk-compat`, use this page for legacy protocol semantics and the [compat overview](./typescript-compat/overview) for the codemod workflow. +- If you're going straight to `@yellow-org/sdk`, use this page as a semantics checklist, then implement new code with the native [TypeScript SDK guide](./typescript/getting-started). + +The `@yellow-org/nitrolite-codemod` package is planned for npm. Until it publishes, the [compat overview codemod workflow](./typescript-compat/overview) shows how to clone the source repo, build the CLI, scan your app, and apply the migration transforms. It rewrites package imports, updates dependencies, and leaves `TODO [codemod]` markers where a native v1 decision is required. + +## What semantics changed + +Amount units are the most important migration boundary: + +- If you're using `@yellow-org/sdk-compat`, channel transfers use raw asset-unit strings, such as `'5000000'` for 5 USDC with 6 decimals. See the amount-units table in the [compat overview](./typescript-compat/overview). +- If you're updating app-session allocations, keep human-decimal strings in the allocation payloads because app state is signed and shared by participants. +- If you're going straight to `@yellow-org/sdk`, use `Decimal` values for native v1 calls such as `deposit()`, `withdraw()`, `transfer()`, and app-session allocation amounts. ## 0.5.x Breaking changes +If you're using `@yellow-org/sdk-compat`, treat the snippets in this section as legacy-shape examples to compare with your current app. If you're going straight to `@yellow-org/sdk`, use the native SDK pages for replacement code. + The 0.5.x release includes fundamental protocol changes affecting session keys, channel operations, state signatures, and channel resize rules. The main objective of these changes is to enhance security, and provide better experience for developers and users by ability to limit allowances for specific applications. -**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide ClearNodes running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. +**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide Nitronode instances running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. ### Protocol Changes @@ -26,15 +41,15 @@ These protocol-level changes affect all implementations and integrations with th Session keys now have enhanced properties that define their access levels and capabilities: -- **Application field**: Determines the scope of session key permissions. Setting this to an application name (e.g., "My Trading App") grants application-scoped access with enforced allowances. Setting it to "clearnode" grants root access equivalent to the wallet itself. +- **Application field**: Determines the scope of session key permissions. Setting this to an application name (e.g., "My Trading App") grants application-scoped access with enforced allowances. Legacy root-access examples may use a broker-specific root marker because that is what older protocol versions expected. - **Allowances field**: Defines spending limits for application-scoped session keys. These limits are tracked cumulatively across all operations and are enforced by the protocol. -- **Expires_at field**: Uses a bigint timestamp (seconds since epoch). Once expired, session keys are permanently frozen and cannot be reactivated. This is particularly critical for root access keys (application set to "clearnode") - if they expire, you lose the ability to perform channel operations. +- **Expires_at field**: Uses a bigint timestamp (seconds since epoch). Once expired, session keys are permanently frozen and cannot be reactivated. This is particularly critical for legacy root access keys: if they expire, you lose the ability to perform channel operations. #### Channel Creation: Separate Create and Fund Steps -Clearnode no longer supports creating channels with an initial deposit. All channels must be created with zero balance and funded separately through a resize operation. This two-step process ensures cleaner state management and prevents edge cases in channel initialization. +Nitronode no longer supports creating channels with an initial deposit. All channels must be created with zero balance and funded separately through a resize operation. This two-step process ensures cleaner state management and prevents edge cases in channel initialization. #### State Signatures: Wallet vs Session Key Signing @@ -96,13 +111,13 @@ Implementing the new session key protocol changes: ``` - + ```typescript const authRequest = { address: '0x...', session_key: '0x...', - application: 'clearnode', // Special value for root access + application: '', // Legacy special value for root access allowances: [], // Not enforced for root access scope: 'app.create', expires_at: BigInt(Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60) // Long expiration recommended @@ -113,7 +128,7 @@ Implementing the new session key protocol changes: **Important considerations:** -- Root access keys (application: "clearnode") cannot perform channel operations after expiration +- Root access keys that use a legacy root-application marker cannot perform channel operations after expiration - Plan expiration times based on your operational needs - Application-scoped keys track cumulative spending against allowances @@ -167,7 +182,7 @@ await client.resizeChannel({ #### Resize correctly -Channel resizing must be negotiated with the ClearNode through WebSocket. Use `resize_amount` and `allocate_amount` with correct sign convention (`resize_amount = -allocate_amount`) and help users with non-zero channel balances migrate by resizing to zero or reopening channels. +Channel resizing must be negotiated with Nitronode through WebSocket. Use `resize_amount` and `allocate_amount` with correct sign convention (`resize_amount = -allocate_amount`) and help users with non-zero channel balances migrate by resizing to zero or reopening channels. Channel resize can be requested as follows: @@ -179,7 +194,7 @@ const resizeMessage = await createResizeChannelMessage(messageSigner, { funds_destination: walletAddress, }); -const resizeResponse = {}; // send the message and wait for Clearnode's response +const resizeResponse = {}; // send the message and wait for Nitronode's response const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); const resizeParams = { @@ -272,9 +287,9 @@ const types = { }; ``` -### ClearNode API +### Nitronode RPC -You should read this section only if you are using the ClearNode API directly. +You should read this section only if you are using the Nitronode RPC API directly. #### Update Authentication @@ -301,14 +316,14 @@ Use the new session key parameters with proper `application`, `allowances`, and ``` - + ```json { "req": [1, "auth_request", { "address": "0x1234567890abcdef...", "session_key": "0x9876543210fedcba...", - "application": "clearnode", + "application": "", "allowances": [], "scope": "app.create", "expires_at": 1750659456789 @@ -398,9 +413,9 @@ Response: ## 0.3.x Breaking changes -The 0.3.x release includes breaking changes to the SDK architecture, smart contract interfaces, and Clearnode API enhancements listed below. +The 0.3.x release includes breaking changes to the SDK architecture, smart contract interfaces, and Nitronode RPC enhancements listed below. -**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide ClearNodes running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. +**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide Nitronode instances running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. ### VirtualApp SDK @@ -445,7 +460,7 @@ const stateSigner = new SessionKeyStateSigner('0x...' as Hex); The `CreateChannelParams` interface has been fully restructured for better clarity. -You should use the new [`CreateChannel` ClearNode API endpoint](#added-create_channel-method) to get the response, that fully resembles the channel creation parameters. +You should use the new [`CreateChannel` Nitronode RPC endpoint](#added-create_channel-method) to get the response, that fully resembles the channel creation parameters. ```typescript // remove-start @@ -590,7 +605,7 @@ const sig: Signature = '0x...'; #### Added: Pagination Types and Parameters -To support pagination in ClearNode API requests, new types and parameters have been added. +To support pagination in Nitronode RPC requests, new types and parameters have been added. For now, only `GetLedgerTransactions` request has been updated to include pagination. @@ -605,15 +620,15 @@ export interface PaginationFilters { } ``` -### Clearnode API +### Nitronode RPC -You should read this section only if you are using the ClearNode API directly, or if you are using the VirtualApp SDK with custom ClearNode API requests. +You should read this section only if you are using the Nitronode RPC API directly, or if you are using the VirtualApp SDK with custom Nitronode RPC requests. #### Actions: Structured Request Parameters -ClearNode API requests have migrated from array-based parameters to structured object parameters for improved type safety and API clarity. +Nitronode RPC requests have migrated from array-based parameters to structured object parameters for improved type safety and API clarity. -Update all your ClearNode API requests to use object-based parameters instead of arrays. +Update all your Nitronode RPC requests to use object-based parameters instead of arrays. ```json { @@ -952,4 +967,4 @@ The contracts automatically detect and verify the appropriate signature format: - **EIP-1271**: Smart contract wallet signatures - **EIP-6492**: Signatures for undeployed contracts -No changes are needed in your contract calls - the signature verification is handled automatically by the contract. \ No newline at end of file +No changes are needed in your contract calls - the signature verification is handled automatically by the contract. diff --git a/docs/nitrolite/build/sdk/multi-party-app-sessions.mdx b/docs/nitrolite/build/sdk/multi-party-app-sessions.mdx index c661ed9..5d68755 100644 --- a/docs/nitrolite/build/sdk/multi-party-app-sessions.mdx +++ b/docs/nitrolite/build/sdk/multi-party-app-sessions.mdx @@ -1,589 +1,308 @@ --- sidebar_position: 3 title: Multi-Party Application Sessions -description: Learn how to create, manage, and close multi-party application sessions using the Yellow Network and VirtualApp protocol -keywords: [app sessions, multi-party, state channels, quorum, voting, signatures, allocations] +description: Create, update, withdraw from, and close a native v1 Nitrolite application session +keywords: [app sessions, multi-party, state channels, quorum, signatures, allocations] --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; +# Multi-Party Application Sessions -# Multi-Party Application Sessions Tutorial - -## Overview - -Application sessions in VirtualApp enable multiple participants to interact within a shared off-chain state channel. This is particularly powerful for use cases requiring coordinated actions between parties without on-chain overhead. - -This tutorial demonstrates how to create, manage, and close a multi-party application session using the Yellow Network and VirtualApp protocol. - -:::tip Run the Full Example -The complete runnable script for this tutorial is available at: -[`scripts/app_sessions/app_session_two_signers.ts`](https://github.com/stevenzeiler/yellow-sdk-tutorials/blob/main/scripts/app_sessions/app_session_two_signers.ts) - -```bash -npx tsx scripts/app_sessions/app_session_two_signers.ts -``` +:::warning Native v1 API +This page demonstrates the v1 native API. If you're on `@erc7824/nitrolite@0.5.3` and want a minimal-diff migration, see the [`@yellow-org/sdk-compat` overview](./typescript-compat/overview) first. ::: -## What is an Application Session? +Application sessions are shared off-chain states owned by an app. Participants sign each app-session creation and update. Nitronode verifies quorum, stores the latest state, and connects deposits or withdrawals to each participant's home channel. -An **application session** is a multi-party state channel that allows participants to: +The native v1 TypeScript SDK uses: -- **Execute off-chain logic** without blockchain transactions -- **Update shared state** with cryptographic signatures -- **Transfer value** between participants instantly +- `Client.create()` for each participant connection. +- `Client.registerApp()` for the `apps.v1.submit_app_version` prerequisite. +- `Client.createAppSession()` for `app_sessions.v1.create_app_session`. +- `Client.submitAppSessionDeposit()` for `app_sessions.v1.submit_deposit_state`. +- `Client.submitAppState()` for `Operate`, `Withdraw`, `Close`, and `Rebalance` updates. +- `packCreateAppSessionRequestV1()` and `packAppStateUpdateV1()` to hash the exact payload participants sign. -Unlike simple payment channels (1-to-1), application sessions support: -- Multiple participants (2+) -- Complex state logic -- Voting mechanisms (weights and quorum) -- Flexible allocation rules +There is no separate `closeAppSession()` helper in `@yellow-org/sdk@1.2.1`; closing is an app-state update with `AppStateUpdateIntent.Close`. ## Prerequisites -### Environment Setup - -You'll need two wallet seed phrases in your `.env` file: +You need two disposable wallets, a Nitronode WebSocket URL, a chain RPC URL, and enough test funds in the first user's home channel to fund the session. ```bash -WALLET_1_SEED_PHRASE="first wallet 12 or 24 word mnemonic here" -WALLET_2_SEED_PHRASE="second wallet 12 or 24 word mnemonic here" +USER_1_PRIVATE_KEY=0x... +USER_2_PRIVATE_KEY=0x... +NITRONODE_WS_URL= +RPC_URL=https://polygon-amoy.g.alchemy.com/v2/YOUR_KEY +CHAIN_ID=80002 +ASSET=usdc ``` -### Funded Wallets - -Both wallets should have: -1. **Funds in Yellow ledger** (deposited via custody contract) - -### Install Dependencies - -```bash -npm install -``` - ---- - -## Key Concepts - -### 1. App Definition - -The application definition specifies the rules of the session: - -```typescript -const appDefinition: RPCAppDefinition = { - protocol: RPCProtocolVersion.NitroRPC_0_5, - participants: [address1, address2], - weights: [50, 50], // Voting power distribution - quorum: 100, // Percentage needed for decisions (100 = unanimous) - challenge: 0, // Challenge period in seconds - nonce: Date.now(), // Unique session ID - application: 'Test app', -}; -``` - -**Key parameters:** - -| Parameter | Description | -|-----------|-------------| -| `participants` | Array of wallet addresses involved | -| `weights` | Voting power for each participant (must sum to 100 or appropriate total) | -| `quorum` | Required percentage of votes for actions (50 = majority, 100 = unanimous) | -| `challenge` | Time window for disputing state changes | -| `nonce` | Unique identifier to prevent replay attacks | - -### 2. Allocations - -Allocations define how assets are distributed among participants: - - - - -```typescript -const allocations: RPCAppSessionAllocation[] = [ - { participant: address1, asset: 'ytest.usd', amount: '0.01' }, - { participant: address2, asset: 'ytest.usd', amount: '0.00' } -]; -``` - - - - -```typescript -const allocations: RPCAppSessionAllocation[] = [ - { participant: address1, asset: 'usdc', amount: '0.01' }, - { participant: address2, asset: 'usdc', amount: '0.00' } -]; -``` - - - - -**Rules:** -- Total allocations cannot exceed session funding -- Amounts are strings (to maintain precision) -- Must account for all participants +:::info Sandbox URL - coming soon +Use your Nitronode WebSocket URL for `NITRONODE_WS_URL`. The public sandbox URL is intentionally shown as `` until the canonical host is pinned. +::: -### 3. Multi-Party Signatures +## Step 1: Connect both participants -For actions requiring consensus (closing, etc.), signatures from multiple participants are collected: +Each participant has a channel signer, a transaction signer, and an app-session signer. The channel signer signs home-channel states. The app-session signer signs app-session payloads and prefixes signatures with the v1 app-session wallet signer byte. ```typescript -// First participant signs -const closeMessage = await createCloseAppSessionMessage( - messageSigner1, - { app_session_id: sessionId, allocations: finalAllocations } +import { + AppSessionWalletSignerV1, + AppStateUpdateIntent, + Client, + EthereumMsgSigner, + createSigners, + packAppStateUpdateV1, + packCreateAppSessionRequestV1, + withBlockchainRPC, + type AppDefinitionV1, + type AppStateUpdateV1, +} from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const wsURL = process.env.NITRONODE_WS_URL ?? ''; +const chainId = BigInt(process.env.CHAIN_ID ?? '80002'); +const asset = process.env.ASSET ?? 'usdc'; + +const user1Key = process.env.USER_1_PRIVATE_KEY as `0x${string}`; +const user2Key = process.env.USER_2_PRIVATE_KEY as `0x${string}`; + +const user1 = createSigners(user1Key); +const user2 = createSigners(user2Key); + +const client1 = await Client.create( + wsURL, + user1.stateSigner, + user1.txSigner, + withBlockchainRPC(chainId, process.env.RPC_URL!) ); -// Second participant signs -const signature2 = await messageSigner2(closeMessage.req); - -// Add second signature -closeMessage.sig.push(signature2); - -// Submit with all signatures -await yellow.sendMessage(JSON.stringify(closeMessage)); -``` - ---- - -## Step-by-Step Walkthrough - -### Step 1: Connect to Yellow Network - - - - -```typescript -const yellow = new Client({ - url: 'wss://clearnet-sandbox.yellow.com/ws', -}); +const client2 = await Client.create( + wsURL, + user2.stateSigner, + user2.txSigner, + withBlockchainRPC(chainId, process.env.RPC_URL!) +); -await yellow.connect(); -console.log('Connected to Yellow clearnet (Sandbox)'); +const appSigner1 = new AppSessionWalletSignerV1(new EthereumMsgSigner(user1Key)); +const appSigner2 = new AppSessionWalletSignerV1(new EthereumMsgSigner(user2Key)); ``` - - +Always close both clients in a `finally` block when the flow ends. -```typescript -const yellow = new Client({ - url: 'wss://clearnet.yellow.com/ws', -}); - -await yellow.connect(); -console.log('Connected to Yellow clearnet (Production)'); -``` - - - +## Step 2: Prepare the user home channel -### Step 2: Set Up Participant Wallets +The session deposit comes from `client1`'s home channel. If the channel does not exist yet, create and checkpoint it first. ```typescript -// Create wallet clients for both participants -const wallet1Client = createWalletClient({ - account: mnemonicToAccount(process.env.WALLET_1_SEED_PHRASE as string), - chain: base, - transport: http(), -}); +await client1.setHomeBlockchain(asset, chainId); +await client2.setHomeBlockchain(asset, chainId); -const wallet2Client = createWalletClient({ - account: mnemonicToAccount(process.env.WALLET_2_SEED_PHRASE as string), - chain: base, - transport: http(), -}); +await client1.approveToken(chainId, asset, new Decimal('20')); +await client1.deposit(chainId, asset, new Decimal('10')); +await client1.checkpoint(asset); ``` -### Step 3: Authenticate Both Participants - -Each participant needs their own session key: - -```typescript -// Authenticate first participant -const sessionKey1 = await authenticateWallet(yellow, wallet1Client); -const messageSigner1 = createECDSAMessageSigner(sessionKey1.privateKey); +If the home channel already has enough signed balance, skip the approve/deposit/checkpoint block and continue. -// Authenticate second participant -const sessionKey2 = await authenticateWallet(yellow, wallet2Client); -const messageSigner2 = createECDSAMessageSigner(sessionKey2.privateKey); -``` +## Step 3: Define the app session -### Step 4: Define Application Configuration +The native v1 app definition uses `AppDefinitionV1`, not the legacy RPC app-definition and protocol-version shape. ```typescript -const appDefinition: RPCAppDefinition = { - protocol: RPCProtocolVersion.NitroRPC_0_5, - participants: [wallet1Client.account.address, wallet2Client.account.address], - weights: [50, 50], - quorum: 100, - challenge: 0, - nonce: Date.now(), - application: 'Test app', +const appId = `checkout-demo-${Date.now()}`; + +const definition: AppDefinitionV1 = { + applicationId: appId, + participants: [ + { walletAddress: client1.getUserAddress(), signatureWeight: 1 }, + { walletAddress: client2.getUserAddress(), signatureWeight: 1 }, + ], + quorum: 2, + nonce: BigInt(Date.now()), }; -``` - -### Step 5: Create Session with Initial Allocations - - - -```typescript -const allocations = [ - { participant: wallet1Client.account.address, asset: 'ytest.usd', amount: '0.01' }, - { participant: wallet2Client.account.address, asset: 'ytest.usd', amount: '0.00' } -]; +const sessionData = JSON.stringify({ cartId: 'cart-001' }); +``` -const sessionMessage = await createAppSessionMessage( - messageSigner1, - { definition: appDefinition, allocations } -); +## Step 4: Register the application via `apps.v1.submit_app_version` -const sessionResponse = await yellow.sendMessage(sessionMessage); -const sessionId = sessionResponse.params.appSessionId; -``` +`app_sessions.v1.create_app_session` requires the application to exist in the app registry. If the app is missing, Nitronode returns `application_not_registered`. - - +The SDK wrapper is `Client.registerApp(appID, metadata, creationApprovalNotRequired)`. It signs the packed app data with the transaction signer and submits `apps.v1.submit_app_version`. ```typescript -const allocations = [ - { participant: wallet1Client.account.address, asset: 'usdc', amount: '0.01' }, - { participant: wallet2Client.account.address, asset: 'usdc', amount: '0.00' } -]; - -const sessionMessage = await createAppSessionMessage( - messageSigner1, - { definition: appDefinition, allocations } +await client1.registerApp( + appId, + JSON.stringify({ name: 'Checkout demo', version: 1 }), + true ); - -const sessionResponse = await yellow.sendMessage(sessionMessage); -const sessionId = sessionResponse.params.appSessionId; ``` - - - -### Step 6: Update Session State +If `creationApprovalNotRequired` is `false`, pass the owner's approval signature to `createAppSession()` through the optional `{ ownerSig }` argument. -You can update allocations to reflect state changes (e.g., a transfer). Since the quorum is 100%, both participants must sign: +## Step 5: Create the session - - +Every participant signs the same create-session hash. ```typescript -const newAllocations = [ - { participant: wallet1Client.account.address, asset: 'ytest.usd', amount: '0.00' }, - { participant: wallet2Client.account.address, asset: 'ytest.usd', amount: '0.01' } -]; - -// Create update message signed by first participant -const updateMessage = await createSubmitAppStateMessage( - messageSigner1, - { app_session_id: sessionId, allocations: newAllocations } -); - -const updateMessageJson = JSON.parse(updateMessage); - -// Second participant signs the same state update -const signature2 = await messageSigner2(updateMessageJson.req as RPCData); - -// Append second signature to meet quorum requirement -updateMessageJson.sig.push(signature2); - -// Submit with all required signatures -await yellow.sendMessage(JSON.stringify(updateMessageJson)); -``` - - - +const createHash = packCreateAppSessionRequestV1(definition, sessionData); -```typescript -const newAllocations = [ - { participant: wallet1Client.account.address, asset: 'usdc', amount: '0.00' }, - { participant: wallet2Client.account.address, asset: 'usdc', amount: '0.01' } +const createSigs = [ + await appSigner1.signMessage(createHash), + await appSigner2.signMessage(createHash), ]; -// Create update message signed by first participant -const updateMessage = await createSubmitAppStateMessage( - messageSigner1, - { app_session_id: sessionId, allocations: newAllocations } +const { appSessionId, version, status } = await client1.createAppSession( + definition, + sessionData, + createSigs ); -const updateMessageJson = JSON.parse(updateMessage); - -// Second participant signs the same state update -const signature2 = await messageSigner2(updateMessageJson.req as RPCData); - -// Append second signature to meet quorum requirement -updateMessageJson.sig.push(signature2); - -// Submit with all required signatures -await yellow.sendMessage(JSON.stringify(updateMessageJson)); +console.log({ appSessionId, version, status }); ``` - - +## Step 6: Deposit into the session -### Step 7: Close Session with Multi-Party Signatures +A deposit update moves funds from the user's home channel into the app session. ```typescript -// Create close message (signed by participant 1) -const closeMessage = await createCloseAppSessionMessage( - messageSigner1, - { app_session_id: sessionId, allocations: finalAllocations } -); - -const closeMessageJson = JSON.parse(closeMessage); - -// Participant 2 signs -const signature2 = await messageSigner2(closeMessageJson.req as RPCData); -closeMessageJson.sig.push(signature2); - -// Submit with all signatures -const closeResponse = await yellow.sendMessage(JSON.stringify(closeMessageJson)); -``` - ---- - -## Running the Example - -```bash -npx tsx scripts/app_session_two_signers.ts -``` - -### Expected Output - -``` -Connected to Yellow clearnet -Wallet address: 0x1234... -Wallet address: 0x5678... -Session message created: {...} -Session message sent -Session response: { appSessionId: '0xabc...' } -Submit app state message: {...} -Wallet 2 signed close session message: 0xdef... -Close session message (with all signatures): {...} -Close session message sent -Close session response: { success: true } -``` - ---- - -## Use Cases - -:::note Asset Names in Examples -The examples below use `usdc` for production scenarios. When testing on Sandbox, replace `usdc` with `ytest.usd`. -::: - -### 1. Peer-to-Peer Escrow - -```typescript -// Buyer and seller agree on terms -const appDefinition = { - participants: [buyer, seller], - weights: [50, 50], - quorum: 100, // Both must agree to release funds - // ... +const depositUpdate: AppStateUpdateV1 = { + appSessionId, + intent: AppStateUpdateIntent.Deposit, + version: 2n, + allocations: [ + { participant: client1.getUserAddress(), asset, amount: new Decimal('10') }, + { participant: client2.getUserAddress(), asset, amount: new Decimal('0') }, + ], + sessionData, }; -// Buyer funds escrow -const allocations = [ - { participant: buyer, asset: 'usdc', amount: '0' }, - { participant: seller, asset: 'usdc', amount: '100' } // Released to seller +const depositHash = packAppStateUpdateV1(depositUpdate); +const depositSigs = [ + await appSigner1.signMessage(depositHash), + await appSigner2.signMessage(depositHash), ]; -``` -### 2. Multi-Player Gaming +const nodeSig = await client1.submitAppSessionDeposit( + depositUpdate, + depositSigs, + asset, + new Decimal('10') +); -```typescript -const appDefinition = { - participants: [player1, player2, player3, player4], - weights: [25, 25, 25, 25], - quorum: 75, // 3 out of 4 players must agree - challenge: 3600, // 1 hour challenge period - application: 'poker-game', -}; +console.log('Deposit state node signature:', nodeSig); ``` -### 3. Multi-Signature Treasury Management - -```typescript -const appDefinition = { - participants: [member1, member2, member3, member4, member5], - weights: [20, 20, 20, 20, 20], - quorum: 60, // 60% approval needed - application: 'multi-sig-treasury', -}; -``` +## Step 7: Operate on app state -### 4. Atomic Swaps +An `Operate` update changes the session's off-chain state without touching the blockchain. ```typescript -// Party A has USDC, wants ETH -// Party B has ETH, wants USDC -const allocations = [ - { participant: partyA, asset: 'usdc', amount: '100' }, - { participant: partyA, asset: 'eth', amount: '0' }, - { participant: partyB, asset: 'usdc', amount: '0' }, - { participant: partyB, asset: 'eth', amount: '0.05' } -]; +const operateUpdate: AppStateUpdateV1 = { + appSessionId, + intent: AppStateUpdateIntent.Operate, + version: 3n, + allocations: [ + { participant: client1.getUserAddress(), asset, amount: new Decimal('8') }, + { participant: client2.getUserAddress(), asset, amount: new Decimal('2') }, + ], + sessionData: JSON.stringify({ cartId: 'cart-001', purchase: 'item-123' }), +}; -// After swap -const finalAllocations = [ - { participant: partyA, asset: 'usdc', amount: '0' }, - { participant: partyA, asset: 'eth', amount: '0.05' }, - { participant: partyB, asset: 'usdc', amount: '100' }, - { participant: partyB, asset: 'eth', amount: '0' } -]; +const operateHash = packAppStateUpdateV1(operateUpdate); +await client1.submitAppState(operateUpdate, [ + await appSigner1.signMessage(operateHash), + await appSigner2.signMessage(operateHash), +]); ``` ---- +## Step 8: Withdraw remaining funds -## Advanced Topics - -### Dynamic Participants - -For applications requiring flexible participation: +Use `Withdraw` when participants move session funds back to home channels. ```typescript -// Start with 2 participants -let participants = [user1, user2]; - -// Add a third participant (requires re-creating session) -participants.push(user3); - -const newAppDefinition = { - participants, - weights: [33, 33, 34], - // ... +const withdrawUpdate: AppStateUpdateV1 = { + appSessionId, + intent: AppStateUpdateIntent.Withdraw, + version: 4n, + allocations: [ + { participant: client1.getUserAddress(), asset, amount: new Decimal('0') }, + { participant: client2.getUserAddress(), asset, amount: new Decimal('0') }, + ], + sessionData: operateUpdate.sessionData, }; -``` -### Weighted Voting - -Different participants can have different voting power: - -```typescript -const appDefinition = { - participants: [founder, participant1, participant2], - weights: [50, 30, 20], // Founder has 50% voting power - quorum: 60, // Founder + one participant = 60% - // ... -}; +const withdrawHash = packAppStateUpdateV1(withdrawUpdate); +await client1.submitAppState(withdrawUpdate, [ + await appSigner1.signMessage(withdrawHash), + await appSigner2.signMessage(withdrawHash), +]); ``` -### Challenge Periods +## Step 9: Close the session -Add time for participants to dispute state changes: +Close is another signed app-state update. Submit it after final withdrawals or with the final allocation state your app requires. ```typescript -const appDefinition = { - // ... - challenge: 86400, // 24 hours in seconds +const closeUpdate: AppStateUpdateV1 = { + ...withdrawUpdate, + intent: AppStateUpdateIntent.Close, + version: 5n, }; -// Participants have 24 hours to challenge a close request before finalization +const closeHash = packAppStateUpdateV1(closeUpdate); +await client1.submitAppState(closeUpdate, [ + await appSigner1.signMessage(closeHash), + await appSigner2.signMessage(closeHash), +]); ``` -### State Validation - -Implement custom logic to validate state transitions: +## Step 10: Query final state and close clients ```typescript -function validateStateTransition( - oldAllocations: RPCAppSessionAllocation[], - newAllocations: RPCAppSessionAllocation[] -): boolean { - // Ensure total amounts are preserved - const oldTotal = oldAllocations.reduce((sum, a) => sum + parseFloat(a.amount), 0); - const newTotal = newAllocations.reduce((sum, a) => sum + parseFloat(a.amount), 0); - - return Math.abs(oldTotal - newTotal) < 0.000001; -} -``` - ---- - -## Troubleshooting - -### "Authentication failed for participant" - -**Cause**: Session key authentication failed - -**Solution**: -- Ensure both `WALLET_1_SEED_PHRASE` and `WALLET_2_SEED_PHRASE` are set in `.env` -- Verify wallets have been authenticated on Yellow network before - -### "Unsupported token" - -**Cause**: Using the wrong asset for your environment (e.g., `usdc` on Sandbox or `ytest.usd` on Production) - -**Solution**: -- **Sandbox** (`wss://clearnet-sandbox.yellow.com/ws`): Use `ytest.usd` -- **Production** (`wss://clearnet.yellow.com/ws`): Use `usdc` - -Ensure the asset in your allocations matches the connected network. - -### "Insufficient balance" - -**Cause**: Participant doesn't have enough funds in Yellow ledger - -**Solution**: - -Deposit sufficient funds into the yellow network account unified balance for each wallet - -### "Invalid signatures" - -**Cause**: Not all required signatures were collected - -**Solution**: -- Ensure quorum is met (if quorum is 100, need all signatures) -- Check that signatures are added in correct order -- Verify message signers correspond to participants - -### "Session already closed" +const { sessions } = await client1.getAppSessions({ + wallet: client1.getUserAddress(), + status: 'closed', +}); -**Cause**: Trying to update or close an already-finalized session +const finalDefinition = await client1.getAppDefinition(appSessionId); -**Solution**: -- Create a new session -- Check session status before operations +console.log({ sessions, finalDefinition }); -### "Quorum not reached" +await client1.close(); +await client2.close(); +``` -**Cause**: Insufficient voting weight for action +## Compat alternative for migrators -**Solution**: +If you are still using `@yellow-org/sdk-compat`, do not copy the native snippets above into a compat-only client. Use the compat app-session helpers while migrating call sites, then move to the native `Client` flow when you are ready to own app definitions and signed `AppStateUpdateV1` payloads directly. ```typescript -// Example: quorum is 60, weights are [30, 30, 40] -// Need at least 2 participants to sign - -// Check current signature weight -const signatureWeight = signatures.reduce((sum, sig) => { - const participantIndex = findParticipantIndex(sig); - return sum + weights[participantIndex]; -}, 0); - -console.log(`Current weight: ${signatureWeight}, Required: ${quorum}`); +// Compat path: keep the 0.5.3-facing surface while the app migrates. +import { NitroliteClient } from '@yellow-org/sdk-compat'; + +const compatClient = await NitroliteClient.create({ + wsURL, + walletClient, + chainId: Number(chainId), + blockchainRPCs, +}); ``` ---- - -## Best Practices - -1. **Always validate allocations** before submitting state updates -2. **Store session IDs** for future reference and auditing -3. **Implement timeout handling** for multi-party signatures -4. **Use appropriate quorum settings** based on trust model -5. **Test with small amounts** before production use -6. **Keep participants informed** of state changes -7. **Handle disconnections gracefully** (participants may come back) -8. **Document application logic** for all participants +## Troubleshooting ---- +| Symptom | Likely cause | Fix | +|---|---|---| +| `application_not_registered` | `createAppSession()` ran before app registration. | Call `registerApp()` first; it submits `apps.v1.submit_app_version`. | +| `quorum_not_met` | Not enough participant signatures for the app definition's quorum. | Sign the same packed hash with enough app-session signers. | +| `invalid_app_state` | The signed payload does not match the submitted update. | Recompute `packAppStateUpdateV1(update)` and collect fresh signatures. | +| `channel_not_found` | The depositing user has no ready home channel for `asset`. | Deposit and checkpoint the home channel before `submitAppSessionDeposit()`. | +| `ongoing_transition` | A prior channel transition is still pending. | Acknowledge or checkpoint the pending state, then retry the app-session update. | ## Further Reading -- [Protocol](/nitrolite/protocol/introduction) — Protocol specification and architecture -- [SDK Reference](/nitrolite/build/sdk) — Complete SDK documentation +- [TypeScript SDK examples](./typescript/examples) +- [TypeScript SDK API reference](./typescript/api-reference) +- App-session RPC catalogue: `/nitrolite/api-reference/app-sessions-v1` +- App registry RPC catalogue: `/nitrolite/api-reference/apps-v1` diff --git a/docs/nitrolite/build/sdk/typescript/api-reference.mdx b/docs/nitrolite/build/sdk/typescript/api-reference.mdx index 830eee2..5284f2d 100644 --- a/docs/nitrolite/build/sdk/typescript/api-reference.mdx +++ b/docs/nitrolite/build/sdk/typescript/api-reference.mdx @@ -15,6 +15,7 @@ These methods build and co-sign a state off-chain. Use [`checkpoint()`](#checkpo ### `deposit(blockchainId, asset, amount)` Prepares a deposit state. Creates a new channel if none exists, otherwise advances the existing state. +Backing RPC: creates new channels through `channels.v1.request_creation`, then submits signed states through `channels.v1.submit_state`. ```typescript const state = await client.deposit(80002n, 'usdc', new Decimal(100)); @@ -38,6 +39,7 @@ const txHash = await client.checkpoint('usdc'); ### `withdraw(blockchainId, asset, amount)` Prepares a withdrawal state to remove funds from the channel. +Backing RPC: submits the signed withdrawal state through `channels.v1.submit_state`. ```typescript const state = await client.withdraw(80002n, 'usdc', new Decimal(25)); @@ -59,6 +61,7 @@ const txHash = await client.checkpoint('usdc'); ### `transfer(recipientWallet, asset, amount)` Prepares an off-chain transfer to another wallet. For existing channels, no checkpoint is needed. +Backing RPC: submits the sender state through `channels.v1.submit_state`; the node creates the receiver state and returns the signed pair. ```typescript const state = await client.transfer('0xRecipient...', 'usdc', new Decimal(50)); @@ -79,6 +82,7 @@ const state = await client.transfer('0xRecipient...', 'usdc', new Decimal(50)); ### `closeHomeChannel(asset)` Prepares a finalize state to close the user's channel for a specific asset. +Backing RPC: submits the finalize transition through `channels.v1.submit_state`. ```typescript const state = await client.closeHomeChannel('usdc'); @@ -90,6 +94,7 @@ const txHash = await client.checkpoint('usdc'); ### `acknowledge(asset)` Acknowledges a received state (e.g., after receiving a transfer). +Backing RPC: submits the acknowledgement transition through `channels.v1.submit_state`. ```typescript const state = await client.acknowledge('usdc'); @@ -114,12 +119,14 @@ const txHash = await client.checkpoint('usdc'); ``` **Requires:** Blockchain RPC via `withBlockchainRPC()` and a co-signed state. +Backing RPC: reads channel context through `channels.v1.get_latest_state` and `channels.v1.get_home_channel`, then sends the on-chain ChannelHub transaction. --- ### `challenge(state)` Submits an on-chain challenge for a channel. Initiates a dispute period. +Backing RPC: none for submission; the SDK verifies the signed state locally and sends the on-chain ChannelHub challenge transaction. ```typescript const state = await client.getLatestState(wallet, 'usdc', true); @@ -131,6 +138,7 @@ const txHash = await client.challenge(state); ### `approveToken(chainId, asset, amount)` Approves the ChannelHub contract to spend ERC-20 tokens. Required before depositing. +Backing RPC: none; this is an ERC-20 approval transaction against the token contract resolved from Nitronode asset config. ```typescript const txHash = await client.approveToken(80002n, 'usdc', new Decimal(1000)); @@ -141,6 +149,7 @@ const txHash = await client.approveToken(80002n, 'usdc', new Decimal(1000)); ### `checkTokenAllowance(chainId, tokenAddress, owner)` Checks the current token allowance for the ChannelHub contract. +Backing RPC: none; this reads the ERC-20 allowance through the configured blockchain RPC. ```typescript const allowance = await client.checkTokenAllowance(80002n, '0xToken...', '0xOwner...'); @@ -157,6 +166,8 @@ const chains = await client.getBlockchains(); // Supported blockchains const assets = await client.getAssets(); // All assets (or pass blockchainId) ``` +Backing RPC: `node.v1.ping`, `node.v1.get_config`, and `node.v1.get_assets`. `getBlockchains()` reads the blockchains returned by `node.v1.get_config`. + --- ## User Queries @@ -174,6 +185,8 @@ const { transactions, metadata } = await client.getTransactions(wallet, { const allowances = await client.getActionAllowances(wallet); ``` +Backing RPC: `user.v1.get_balances`, `user.v1.get_transactions`, and `user.v1.get_action_allowances`. + --- ## Channel Queries @@ -193,6 +206,8 @@ const escrow = await client.getEscrowChannel(escrowChannelId); const state = await client.getLatestState(wallet, asset, true); ``` +Backing RPC: `channels.v1.get_channels`, `channels.v1.get_home_channel`, `channels.v1.get_escrow_channel`, and `channels.v1.get_latest_state`. + --- ## App Registry @@ -208,6 +223,8 @@ const { apps, metadata } = await client.getApps({ await client.registerApp('my-app', '{"name": "My App"}', false); ``` +Backing RPC: `apps.v1.get_apps` and `apps.v1.submit_app_version`. Register the app before creating sessions for its `applicationId`. + --- ## App Sessions @@ -233,6 +250,8 @@ await client.submitAppState(appUpdate, quorumSigs); const batchId = await client.rebalanceAppSessions(signedUpdates); ``` +Backing RPC: `app_sessions.v1.create_app_session`, `app_sessions.v1.submit_deposit_state`, `app_sessions.v1.submit_app_state`, and `app_sessions.v1.rebalance_app_sessions`. + ### Query ```typescript @@ -240,6 +259,8 @@ const { sessions, metadata } = await client.getAppSessions(opts); const definition = await client.getAppDefinition(appSessionId); ``` +Backing RPC: `app_sessions.v1.get_app_sessions` and `app_sessions.v1.get_app_definition`. + --- ## App Session Keys @@ -260,6 +281,8 @@ await client.submitSessionKeyState({ ...state, user_sig: sig }); const states = await client.getLastKeyStates('0x1234...'); ``` +Backing RPC: `app_sessions.v1.submit_session_key_state` and `app_sessions.v1.get_last_key_states`. + --- ## Channel Session Keys @@ -279,6 +302,8 @@ await client.submitChannelSessionKeyState({ ...state, user_sig: sig }); const states = await client.getLastChannelKeyStates('0x1234...'); ``` +Backing RPC: `channels.v1.submit_session_key_state` and `channels.v1.get_last_key_states`. + --- ## Utilities diff --git a/docs/nitrolite/build/sdk/typescript/configuration.mdx b/docs/nitrolite/build/sdk/typescript/configuration.mdx index 675f2a2..c040beb 100644 --- a/docs/nitrolite/build/sdk/typescript/configuration.mdx +++ b/docs/nitrolite/build/sdk/typescript/configuration.mdx @@ -10,6 +10,10 @@ sidebar_position: 3 Configuration is passed as variadic options to `Client.create()`: +:::info Sandbox URL - coming soon +Use your Nitronode WebSocket URL for `wsURL`. The public sandbox URL is intentionally shown as `` until the canonical host is pinned. +::: + ```typescript import { Client, @@ -20,7 +24,7 @@ import { } from '@yellow-org/sdk'; const client = await Client.create( - wsURL, + '', stateSigner, txSigner, withBlockchainRPC(chainId, rpcURL), // Blockchain RPC (required for checkpoint) @@ -57,10 +61,33 @@ withErrorHandler((error) => { ## Home Blockchain -`setHomeBlockchain(asset, blockchainId)` sets the default blockchain for an asset. Required before `transfer()` on a new channel (where no chain context exists yet). +`setHomeBlockchain(asset, blockchainId)` sets the default blockchain for an asset. It is required when `transfer()` may need to create a new home channel, because `transfer(recipientWallet, asset, amount)` does not take a chain ID. It is not required for transfers within an existing home channel. + +```typescript +await client.setHomeBlockchain('usdc', 80002n); +``` ```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY as `0x${string}`); + +const client = await Client.create( + '', + stateSigner, + txSigner, + withBlockchainRPC(80002n, process.env.POLYGON_AMOY_RPC!), +); + +// Required only if this transfer could create the USDC home channel. await client.setHomeBlockchain('usdc', 80002n); + +await client.transfer( + '0xRecipient000000000000000000000000000000000000', + 'usdc', + new Decimal('1.25'), +); ``` :::warning diff --git a/docs/nitrolite/build/sdk/typescript/examples.mdx b/docs/nitrolite/build/sdk/typescript/examples.mdx index 84494e5..449d9cd 100644 --- a/docs/nitrolite/build/sdk/typescript/examples.mdx +++ b/docs/nitrolite/build/sdk/typescript/examples.mdx @@ -6,6 +6,10 @@ sidebar_position: 4 # Examples +:::info Sandbox URL - coming soon +Use your Nitronode WebSocket URL in each `Client.create()` call below. The public sandbox URL is intentionally shown as `` until the canonical host is pinned. +::: + ## Basic Deposit and Transfer ```typescript @@ -13,10 +17,10 @@ import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; import Decimal from 'decimal.js'; async function basicExample() { - const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY as `0x${string}`); const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner, withBlockchainRPC(80002n, process.env.RPC_URL!) @@ -59,10 +63,10 @@ import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; import Decimal from 'decimal.js'; async function multiChainExample() { - const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY as `0x${string}`); const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner, withBlockchainRPC(80002n, process.env.POLYGON_RPC!), @@ -94,9 +98,9 @@ async function multiChainExample() { import { Client, createSigners } from '@yellow-org/sdk'; async function queryTransactions() { - const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY as `0x${string}`); const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner ); @@ -123,41 +127,60 @@ async function queryTransactions() { ## App Session Workflow ```typescript -import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import { + AppSessionWalletSignerV1, + AppStateUpdateIntent, + Client, + EthereumMsgSigner, + createSigners, + packAppStateUpdateV1, + packCreateAppSessionRequestV1, + withBlockchainRPC, + type AppDefinitionV1, + type AppStateUpdateV1, +} from '@yellow-org/sdk'; import Decimal from 'decimal.js'; async function appSessionExample() { - const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY as `0x${string}`); + const appSigner = new AppSessionWalletSignerV1( + new EthereumMsgSigner(process.env.PRIVATE_KEY as `0x${string}`) + ); + const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner, withBlockchainRPC(80002n, process.env.RPC_URL!) ); try { - const definition = { + const definition: AppDefinitionV1 = { applicationId: 'chess-v1', participants: [ { walletAddress: client.getUserAddress(), signatureWeight: 1 }, - { walletAddress: '0xOpponent...', signatureWeight: 1 }, ], - quorum: 2, + quorum: 1, nonce: 1n, }; + await client.registerApp('chess-v1', '{"name":"Chess"}', true); + + const createHash = packCreateAppSessionRequestV1(definition, '{}'); + const createSig = await appSigner.signMessage(createHash); + const { appSessionId } = await client.createAppSession( definition, '{}', - ['sig1', 'sig2'] + [createSig] ); console.log('Session created:', appSessionId); // Deposit to app session - const appUpdate = { + const appUpdate: AppStateUpdateV1 = { appSessionId, - intent: 1, - version: 1n, + intent: AppStateUpdateIntent.Deposit, + version: 2n, allocations: [{ participant: client.getUserAddress(), asset: 'usdc', @@ -166,9 +189,12 @@ async function appSessionExample() { sessionData: '{}', }; + const depositHash = packAppStateUpdateV1(appUpdate); + const depositSig = await appSigner.signMessage(depositHash); + const nodeSig = await client.submitAppSessionDeposit( appUpdate, - ['sig1'], + [depositSig], 'usdc', new Decimal(50) ); @@ -196,56 +222,77 @@ import { type TransactionSigner, withBlockchainRPC, } from '@yellow-org/sdk'; -import { createWalletClient, custom, type WalletClient } from 'viem'; +import { + createWalletClient, + custom, + type Address, + type Hex, + type WalletClient, +} from 'viem'; import { sepolia } from 'viem/chains'; // Adapt viem WalletClient to SDK's StateSigner (EIP-191 signatures) -class WalletStateSigner implements StateSigner { +class BrowserStateSigner implements StateSigner { constructor(private wc: WalletClient) {} - async sign(payload: Uint8Array): Promise { - return this.wc.signMessage({ - account: this.wc.account!, - message: { raw: payload }, - }); + getAddress(): Address { + return this.wc.account!.address; } - getAddress(): string { - return this.wc.account!.address; + async signMessage(hash: Hex): Promise { + return await this.wc.signMessage({ + account: this.wc.account!, + message: { raw: hash }, + }); } } // Adapt viem WalletClient to SDK's TransactionSigner -class WalletTransactionSigner implements TransactionSigner { +class BrowserTransactionSigner implements TransactionSigner { constructor(private wc: WalletClient) {} - async sign(payload: Uint8Array): Promise { - return this.wc.signTypedData({ + getAddress(): Address { + return this.wc.account!.address; + } + + async sendTransaction(tx: Parameters[0]): Promise { + return await this.wc.sendTransaction(tx); + } + + async signMessage({ raw }: { raw: Hex }): Promise { + return await this.wc.signMessage({ account: this.wc.account!, - domain: { name: 'Nitrolite', version: '1', chainId: 1 }, - types: { Nitrolite: [{ name: 'operation', type: 'bytes' }] }, - primaryType: 'Nitrolite', - message: { operation: `0x${Buffer.from(payload).toString('hex')}` }, + message: { raw }, }); } - getAddress(): string { - return this.wc.account!.address; + async signPersonalMessage(hash: Hex): Promise { + return await this.wc.signMessage({ + account: this.wc.account!, + message: { raw: hash }, + }); } } async function connectWithWallet() { + const transport = custom(window.ethereum!); + const requestClient = createWalletClient({ + chain: sepolia, + transport, + }); + + const [address] = await requestClient.requestAddresses(); const walletClient = createWalletClient({ + account: address, chain: sepolia, - transport: custom(window.ethereum!), + transport, }); - const [address] = await walletClient.requestAddresses(); - const stateSigner = new ChannelDefaultSigner(new WalletStateSigner(walletClient)); - const txSigner = new WalletTransactionSigner(walletClient); + const stateSigner = new ChannelDefaultSigner(new BrowserStateSigner(walletClient)); + const txSigner = new BrowserTransactionSigner(walletClient); const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner, withBlockchainRPC(11155111n, 'https://rpc.sepolia.io'), @@ -287,6 +334,52 @@ async function depositWithApproval( } ``` +## Errors and Recovery + +```typescript +import { Client } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +async function recoverableDeposit( + client: Client, + chainId: bigint, + asset: string, + amount: Decimal, +) { + try { + await client.deposit(chainId, asset, amount); + return await client.checkpoint(asset); + } catch (error) { + const message = error instanceof Error ? error.message.toLowerCase() : ''; + + if (message.includes('allowance') || message.includes('insufficient')) { + await client.approveToken(chainId, asset, amount); + return await client.checkpoint(asset); + } + + if (message.includes('ongoing')) { + await client.acknowledge(asset); + return await client.checkpoint(asset); + } + + if (message.includes('blockchain client')) { + throw new Error(`Missing withBlockchainRPC(${chainId}n, rpcURL) for ${asset}`); + } + + if (message.includes('asset')) { + const assets = await client.getAssets(chainId); + throw new Error(`Unsupported asset ${asset}. Supported assets: ${assets.map((a) => a.symbol).join(', ')}`); + } + + if (message.includes('channel')) { + throw new Error(`No ready ${asset} channel. Deposit first or call setHomeBlockchain('${asset}', ${chainId}n).`); + } + + throw error; + } +} +``` + ## Channel Session Keys Session keys let users delegate signing authority so operations don't require repeated wallet prompts. From the example-app: @@ -353,10 +446,10 @@ async function setupSessionKey(client: Client) { import { Client, createSigners, withErrorHandler } from '@yellow-org/sdk'; async function monitorConnection() { - const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY as `0x${string}`); const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner, withErrorHandler((error) => { diff --git a/docs/nitrolite/build/sdk/typescript/getting-started.mdx b/docs/nitrolite/build/sdk/typescript/getting-started.mdx index 23b3758..698cb24 100644 --- a/docs/nitrolite/build/sdk/typescript/getting-started.mdx +++ b/docs/nitrolite/build/sdk/typescript/getting-started.mdx @@ -6,10 +6,10 @@ sidebar_position: 1 # Getting Started with @yellow-org/sdk -The TypeScript SDK for Clearnode payment channels provides both high-level and low-level operations in a unified client. +The TypeScript SDK for Nitronode payment channels provides both high-level and low-level operations in a unified client. :::tip Migrating from v0.5.3? -If you are migrating from `@layer-3/nitrolite@v0.5.3`, consider using the [`@yellow-org/sdk-compat`](../typescript-compat/overview) package first. It maps the familiar v0.5.3 API to the v1.0.0 runtime with minimal code changes. +If you are migrating from the published `@erc7824/nitrolite@0.5.3` package, consider using [`@yellow-org/sdk-compat`](../typescript-compat/overview) first. Older internal docs may refer to the same 0.5.3 codebase as `@layer-3/nitrolite`; use the published package name when auditing app dependencies. ::: ## Installation @@ -26,11 +26,15 @@ pnpm add @yellow-org/sdk - **Node.js** 20.0.0 or later - **TypeScript** 5.3.0 or later (for development) -- A running **Clearnode** instance or access to a public node +- A running **Nitronode** instance or access to a public node - A **blockchain RPC endpoint** for on-chain operations via `checkpoint()` ## Quick Start +:::info Sandbox URL - coming soon +Use your Nitronode WebSocket URL in the `Client.create()` call below. The public sandbox URL is intentionally shown as `` until the canonical host is pinned. +::: + ```typescript import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; import Decimal from 'decimal.js'; @@ -43,7 +47,7 @@ async function main() { // 2. Create unified client const client = await Client.create( - 'wss://clearnode.example.com/ws', + '', stateSigner, txSigner, withBlockchainRPC(80002n, 'https://polygon-amoy.alchemy.com/v2/KEY') @@ -81,7 +85,12 @@ main().catch(console.error); The `Client` is the single entry point for all operations. ```typescript -import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import { + Client, + createSigners, + withBlockchainRPC, + withHandshakeTimeout, +} from '@yellow-org/sdk'; // Step 1: Create signers from private key const { stateSigner, txSigner } = createSigners('0x1234...'); @@ -100,6 +109,18 @@ const client = await Client.create( await client.setHomeBlockchain('usdc', 80002n); ``` +## Errors and Recovery + +The SDK README keeps the [full error catalogue](https://github.com/layer-3/nitrolite/tree/main/sdk/ts#error-handling). These are the failures most builders should handle in their first integration: + +| Error | Common cause | Recovery | +|---|---|---| +| Insufficient allowance | `checkpoint()` needs ERC-20 approval before the first deposit. | Call `approveToken(chainId, asset, amount)`, then retry `checkpoint(asset)`. | +| Ongoing transition | A previous state transition is waiting for acknowledgement or settlement. | Query the latest state, acknowledge pending funds when needed, or wait for the in-flight checkpoint. | +| Missing RPC | `withBlockchainRPC(chainId, rpcURL)` was not configured for an on-chain operation. | Add the RPC option for every chain you deposit, withdraw, approve, or checkpoint on. | +| Asset not supported | The Nitronode config does not list the requested asset on the selected chain. | Call `getAssets(chainId)` and use one of the returned asset symbols. | +| Channel not found | The wallet has no home channel or no signed state for that asset yet. | Deposit first, or call `setHomeBlockchain(asset, chainId)` before a transfer that may create the home channel. | + ## Signer Types The SDK provides two signer types: @@ -156,11 +177,11 @@ const txHash = await client.checkpoint('usdc'); ### Channel Lifecycle -1. **Void** — No channel exists -2. **Create** — `deposit()` creates channel on-chain via `checkpoint()` -3. **Open** — Channel active; can deposit, withdraw, transfer -4. **Challenged** — Dispute initiated (advanced) -5. **Closed** — Channel finalized (advanced) +1. **Void**: No channel exists +2. **Create**: `deposit()` creates channel on-chain via `checkpoint()` +3. **Open**: Channel active; can deposit, withdraw, transfer +4. **Challenged**: Dispute initiated (advanced) +5. **Closed**: Channel finalized (advanced) ### TypeScript Conventions @@ -179,6 +200,6 @@ const wallet: Address = '0x1234...'; ## Next Steps -- [API Reference](./api-reference) — Full method documentation -- [Configuration](./configuration) — Client options and error handling -- [Examples](./examples) — Complete working examples +- [API Reference](./api-reference): Full method documentation +- [Configuration](./configuration): Client options and error handling +- [Examples](./examples): Complete working examples