From 4aa59f7632901463d394f21f4218e6e67d990fff Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Thu, 7 May 2026 11:34:15 +0530 Subject: [PATCH 1/2] YNU-864: Build front door rewrite Rewrite the Nitrolite Build entrypoint around the current v1 TypeScript SDK. - Replace the old quickstart, prerequisites, and key terms pages with v1 setup guidance for @yellow-org/sdk. - Remove the duplicate legacy quick-start route. - Retarget Build navigation and existing Build links to the new getting-started quickstart. - Keep the sandbox WebSocket URL as a clear placeholder until the public endpoint is finalized. - Preserve faucet examples as hidden comments so they can be restored when the endpoint is ready. This makes the first Build path match the current SDK while giving 0.5.x users a clear pointer to the compat package. --- .../build/getting-started/key-terms.mdx | 387 ++---- .../build/getting-started/prerequisites.mdx | 323 ++--- .../build/getting-started/quickstart.mdx | 1151 ++--------------- docs/nitrolite/build/quick-start/index.md | 359 ----- docs/nitrolite/learn/index.mdx | 2 +- .../learn/introduction/supported-chains.mdx | 2 +- docusaurus.config.ts | 6 +- src/pages/nitrolite.tsx | 2 +- 8 files changed, 312 insertions(+), 1920 deletions(-) delete mode 100644 docs/nitrolite/build/quick-start/index.md diff --git a/docs/nitrolite/build/getting-started/key-terms.mdx b/docs/nitrolite/build/getting-started/key-terms.mdx index 876926f..034ba32 100644 --- a/docs/nitrolite/build/getting-started/key-terms.mdx +++ b/docs/nitrolite/build/getting-started/key-terms.mdx @@ -1,340 +1,101 @@ --- sidebar_position: 3 title: Key Terms & Mental Models -description: Essential vocabulary and conceptual frameworks for understanding Yellow Network +description: Vocabulary for the v1 Nitrolite SDK, protocol, and docs. keywords: [terminology, glossary, concepts, state channels, mental models] --- -import Tooltip from '@site/src/components/Tooltip'; -import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; - -:::warning[Work in Progress] -This page was carried over from the v0.5.x documentation and has not yet been fully updated for v1.x. Some terminology, code examples, and API references may be outdated. An update is in progress. -::: - # Key Terms & Mental Models -In this guide, you will learn the essential vocabulary and mental models for understanding Yellow Network and state channel technology. - -**Goal**: Build a solid conceptual foundation before diving into implementation. - ---- - -## Core Mental Model: Off-Chain Execution - -The fundamental insight behind Yellow Network is simple: - -> **Most interactions don't need immediate on-chain settlement.** - -Think of it like a bar tab: - -| Traditional (L1) | State Channels | -|------------------|----------------| -| Pay for each drink separately | Open a tab, pay once at the end | -| Wait for bartender each time | Instant service, settle later | -| Transaction per item | One transaction for the whole session | - -State channels apply this pattern to blockchain: **lock funds once**, **transact off-chain**, **settle once**. - ---- - -## Essential Vocabulary - -### State Channel - -A **state channel** is a secure pathway for exchanging cryptographically signed states between participants without touching the blockchain. - -**Key properties:** -- Funds are locked in a smart contract -- Participants exchange signed state updates off-chain -- Only opening and closing require on-chain transactions -- Either party can force on-chain settlement if needed - -**Analogy**: Like a private Venmo between two parties, backed by a bank escrow. - ---- - -### Channel - -A **Channel** is the on-chain representation of a state channel. It defines: - -```typescript -{ - participants: ['0xAlice', '0xBob'], // Who can participate - adjudicator: '0xContract', // Rules for state validation - challenge: 86400, // Dispute window (seconds) - nonce: 1699123456789 // Unique identifier -} -``` - -The **channelId** is computed deterministically from these parameters: - -``` -channelId = keccak256(participants, adjudicator, challenge, nonce, chainId) -``` - ---- - -### State - -A **State** is a snapshot of the channel at a specific moment: - -```typescript -{ - intent: 'OPERATE', // Purpose: INITIALIZE, OPERATE, RESIZE, FINALIZE - version: 5, // Incremental counter (higher = newer) - data: '0x...', // Application-specific data - allocations: [...], // How funds are distributed - sigs: ['0xSig1', '0xSig2'] // Participant signatures -} -``` - -**Key rule**: A higher version number always supersedes a lower one, regardless of allocations. - ---- - -### Allocation - -An **Allocation** specifies how funds should be distributed: - -```typescript -{ - destination: '0xAlice', // Recipient address - token: '0xUSDC_CONTRACT', // Token contract - amount: 50000000n // Amount in smallest unit (6 decimals for USDC) -} -``` - -The sum of allocations represents the total funds in the channel. - ---- - -### Clearnode - -A **Clearnode** is operated by independent node operators using open-source software developed and maintained by Layer3 Fintech Ltd. It is the off-chain service that: - -1. **Manages the Nitro RPC protocol** for state channel operations -2. **Provides unified balance** aggregated across multiple chains -3. **Coordinates channels** between users -4. **Hosts app sessions** for multi-party applications - -**Think of it as**: A service node that acts as your entry point to Yellow Network—operated independently, but trustless because of on-chain guarantees. - ---- - -### Unified Balance - -Your **unified balance** is the aggregation of funds across all chains where you have deposits: - -``` -Polygon: 50 USDC ┐ -Base: 30 USDC ├─→ Unified Balance: 100 USDC -Arbitrum: 20 USDC ┘ -``` - -You can: -- Transfer from unified balance instantly (off-chain) -- Withdraw to any supported chain -- Lock funds into app sessions - ---- - -### App Session - -An **App Session** is an off-chain channel built on top of the unified balance for multi-party applications: +Nitrolite uses one shared vocabulary across the SDK, protocol docs, and on-chain contracts. Learn these terms first and the rest of the docs become much easier to scan. -```typescript -{ - protocol: 'NitroRPC/0.4', - participants: ['0xAlice', '0xBob', '0xJudge'], - weights: [40, 40, 50], // Voting power - quorum: 80, // Required weight for state updates - challenge: 3600, // Dispute window - nonce: 1699123456789 -} -``` - -**Use cases**: Games, prediction markets, escrow, any multi-party coordination. - ---- - -### Session Key - -A **session key** is a temporary cryptographic key that: - -- Is generated locally on your device -- Has limited permissions and spending caps -- Expires after a specified time -- Allows gasless signing without wallet prompts - -**Flow**: -1. Generate session keypair locally -2. Main wallet authorizes the session key (one-time EIP-712 signature) -3. All subsequent operations use the session key -4. Session expires or can be revoked - ---- - -## Protocol Components - -### VirtualApp - -**VirtualApp** is the on-chain smart contract protocol: - -- Defines channel data structures -- Implements create, close, challenge, resize operations -- Provides cryptographic verification -- Currently version 0.5.0 - ---- - -### Nitro RPC - -**Nitro RPC** is the off-chain communication protocol: - -- Compact JSON array format for efficiency -- Every message is cryptographically signed -- Bidirectional real-time communication -- Currently version 0.4 - -**Message format**: -```javascript -[requestId, method, params, timestamp] - -// Example -[42, "transfer", {"destination": "0x...", "amount": "50.0"}, 1699123456789] -``` - ---- - -### Custody Contract - -The **Custody Contract** is the main on-chain entry point: - -- Locks and unlocks participant funds -- Tracks channel status (VOID → ACTIVE → FINAL) -- Validates signatures and state transitions -- Handles dispute resolution - ---- - -### Adjudicator - -An **Adjudicator** defines rules for valid state transitions: - -| Type | Rule | -|------|------| -| **SimpleConsensus** | Both participants must sign (default) | -| **Remittance** | Only sender must sign | -| **Custom** | Application-specific logic | - ---- - -## State Lifecycle - -### Channel States +:::info Canonical source +The protocol source of truth is [Protocol Terminology](../../protocol/terminology). PR7 will add a dedicated Learn glossary; until then, use this page as the builder-facing quick reference. +::: -```mermaid -stateDiagram-v2 - [*] --> VOID: Channel doesn't exist - VOID --> ACTIVE: create() - ACTIVE --> ACTIVE: Off-chain updates - ACTIVE --> DISPUTE: challenge() - ACTIVE --> FINAL: close() - DISPUTE --> ACTIVE: checkpoint() - DISPUTE --> FINAL: Timeout - FINAL --> [*]: Deleted -``` +## Core mental model + +Nitrolite lets a user and Nitronode exchange signed channel states off-chain, then enforce the latest mutually signed state on-chain when needed. The user experience is fast because most updates are signatures, not blockchain transactions. The safety model still has an on-chain exit through ChannelHub. + +## Essential vocabulary + +| Term | What it means | Builder cue | +|------|---------------|-------------| +| **Channel** | A state container shared by a user and Nitronode for one unified asset. | The thing your deposits, transfers, withdrawals, and checkpoints update. | +| **Home Channel** | The default user-Nitronode channel whose home ledger is enforced on its home chain. | Most apps start here. `deposit()`, `transfer()`, and `checkpoint()` act on the home channel for an asset. | +| **Escrow Channel** | A temporary cross-chain channel derived from a home channel for escrow operations. | You meet this when funds move between chains. | +| **Channel State** | The current agreed channel configuration: ledgers, version, and transition data. | SDK state operations return a state before you decide whether to checkpoint it. | +| **Channel Definition** | The immutable channel parameters: user, node, asset, nonce, challenge duration, and signature validators. | Fixed at channel creation. Do not model it as app-level mutable config. | +| **Ledger** | A record of asset allocations and net flows for a specific blockchain inside a channel state. | Ledger math must balance exactly. | +| **Home Ledger** | The ledger tied to the chain where the current channel state is enforced. | This is the authoritative ledger for checkpoint and challenge flows. | +| **Non-Home Ledger** | A secondary ledger for a blockchain other than the home chain. | Used by escrow and migration flows, not as a synonym for home channel. | +| **Vault** | Nitronode's per-chain pool of available funds used to cover required locking during transitions. | Cross-chain availability depends on node liquidity in the right vault. | +| **Asset** | A logical value unit, such as USDC, with a symbol and decimal precision independent of any one chain. | The SDK takes asset symbols like `'usdc'`, not token addresses, for high-level channel operations. | +| **Decimal** | An exact decimal amount represented with `decimal.js`. | Use `new Decimal(5)`, not JavaScript floating-point numbers, for SDK amounts. | +| **App Session** | An application extension that commits channel funds into a multi-party off-chain session. | Games, matching, escrow, and other multi-party flows live here. | +| **Quorum** | The minimum weighted approval needed for an app session state update. | If weights are `[40, 40, 50]` and quorum is `80`, two participants may be enough. | +| **Session Key** | A delegated signing key authorized by a participant's primary key for a scoped time window. | Useful for repeated app signatures without prompting the primary wallet every time. | +| **Challenge** | An on-chain dispute where a participant submits a signed state and opens the challenge window. | Lets an honest party enforce a newer valid state before timeout. | +| **Checkpoint** | The operation of submitting a signed state to the blockchain layer for enforcement. | `client.checkpoint('usdc')` settles the latest agreed state for that asset. | +| **Nitronode** | The v1 off-chain coordinator that serves RPC, co-signs valid states, tracks channels, and coordinates app sessions. | This is the node your SDK client connects to over WebSocket. | +| **ChannelHub** | The v1 on-chain entrypoint for channel create, deposit, withdraw, checkpoint, challenge, and close operations. | The SDK hides most contract calls, but this is the contract enforcing the state. | +| **Migration** | A transition that moves the channel's home-chain relationship as part of cross-chain operation. | Treat it as protocol state movement, not a package upgrade. | +| **Transition** | A typed operation explaining why the channel state changed. | Every state advancement carries one transition type. | + +## Channel lifecycle statuses + +`Channel.status` uses these v1 values: | Status | Meaning | |--------|---------| -| **VOID** | Channel doesn't exist on-chain | -| **INITIAL** | Created, waiting for all participants (legacy) | -| **ACTIVE** | Fully operational, off-chain updates happening | -| **DISPUTE** | Challenge period active, parties can submit newer states | -| **FINAL** | Closed, funds distributed, metadata deleted | - ---- - -### State Intents - -| Intent | When Used | Purpose | -|--------|-----------|---------| -| **INITIALIZE** | `create()` | First state when opening channel | -| **OPERATE** | Off-chain updates | Normal operation, redistribution | -| **RESIZE** | `resize()` | Add or remove funds | -| **FINALIZE** | `close()` | Final state for cooperative closure | - ---- - -## Security Concepts +| `void` | No channel exists yet. | +| `open` | The channel is active. | +| `challenged` | A challenge is active on-chain. | +| `closed` | The channel has been finalized. | -### Challenge Period +## Transition types -When a dispute arises: +These are the v1 `transition_type` literals from `docs/api.yaml`: -1. Party A submits their latest state via `challenge()` -2. **Challenge period** starts (typically 24 hours) -3. Party B can submit a newer valid state via `checkpoint()` -4. If no newer state, Party A's state becomes final after timeout +| Literal | Meaning | +|---------|---------| +| `transfer_receive` | Receive side of an off-chain transfer. | +| `transfer_send` | Send side of an off-chain transfer. | +| `release` | Return funds from an extension back to the channel. | +| `commit` | Move funds from the channel into an extension. | +| `home_deposit` | Add funds to the home channel. | +| `home_withdrawal` | Remove funds from the home channel. | +| `mutual_lock` | Lock funds through mutual agreement. | +| `escrow_deposit` | Start or advance cross-chain escrow deposit flow. | +| `escrow_lock` | Lock escrow funds while a cross-chain flow is pending. | +| `escrow_withdraw` | Withdraw through an escrow flow. | +| `migrate` | Move the channel home-chain relationship. | +| `finalize` | Finalize and close the channel state. | -**Purpose**: Gives honest parties time to respond to incorrect claims. - ---- +## Mental models that prevent bugs -### Signatures +### States first, settlement second -Two contexts for signatures: +High-level SDK calls such as `deposit()`, `withdraw()`, and `transfer()` prepare signed state updates. `checkpoint()` is the separate step that submits a state on-chain. -| Context | Hash Method | Signed By | -|---------|-------------|-----------| -| **On-chain** | Raw `packedState` (no prefix) | Main wallet | -| **Off-chain RPC** | JSON payload hash | Session key | - -**On-chain packedState**: -```javascript -keccak256(abi.encode(channelId, intent, version, data, allocations)) -``` - ---- +### Amounts are decimals, ledgers are exact -### Quorum +The builder-facing SDK uses `Decimal` values. The protocol then preserves exact ledger accounting using the asset's configured precision. This avoids rounding bugs when the same logical asset exists on multiple chains. -For app sessions, **quorum** defines the minimum voting weight required for state updates: +### App sessions spend committed channel funds -``` -Participants: [Alice, Bob, Judge] -Weights: [40, 40, 50] -Quorum: 80 +An app session does not replace a channel. It is an extension that receives funds through `commit` transitions and returns funds through `release` transitions. -Valid combinations: -- Alice + Bob = 80 ✓ -- Alice + Judge = 90 ✓ -- Bob + Judge = 90 ✓ -- Alice alone = 40 ✗ -``` - ---- - -## Quick Reference Table - -| Term | One-Line Definition | -|------|---------------------| -| **State Channel** | Off-chain execution backed by on-chain funds | -| **Clearnode** | Off-chain service coordinating state channels | -| **Unified Balance** | Aggregated funds across all chains | -| **App Session** | Multi-party application channel | -| **Session Key** | Temporary key with limited permissions | -| **Challenge Period** | Dispute resolution window | -| **Quorum** | Minimum signature weight for approval | -| **Allocation** | Fund distribution specification | -| **packedState** | Canonical payload for signing | - ---- +### Challenges are the escape hatch -## Next Steps +If cooperation fails, a participant can use a challenge to force the latest valid state into the on-chain flow. The challenge window gives the counterparty time to respond with a newer valid state. -Now that you understand the vocabulary, continue to: +## v0.5.x -> v1 term map -- **[State Channels vs L1/L2](/nitrolite/learn/core-concepts/state-channels-vs-l1-l2)** — Deep comparison with other scaling solutions -- **[Challenge Response & Disputes](/nitrolite/learn/core-concepts/challenge-response)** — On-chain dispute resolution \ No newline at end of file +| v0.5.x term | v1 term to use now | +|-------------|--------------------| +| Clearnode | Nitronode | +| VirtualApp | App Session, or the specific SDK/client API you are using | +| Custody Contract | ChannelHub | +| Adjudicator | ChannelHub validators and engine logic | +| NitroRPC/0.4 | v1 RPC | diff --git a/docs/nitrolite/build/getting-started/prerequisites.mdx b/docs/nitrolite/build/getting-started/prerequisites.mdx index 6b5dc87..2418c02 100644 --- a/docs/nitrolite/build/getting-started/prerequisites.mdx +++ b/docs/nitrolite/build/getting-started/prerequisites.mdx @@ -1,34 +1,27 @@ --- sidebar_position: 2 title: Prerequisites & Environment -description: Set up your development environment for building Yellow Apps +description: Set up a Node 20 TypeScript environment for @yellow-org/sdk. keywords: [prerequisites, setup, development, environment, Node.js, viem] --- -import Tooltip from '@site/src/components/Tooltip'; -import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; - -:::warning[Work in Progress] -This page was carried over from the v0.5.x documentation and has not yet been fully updated for v1.x. Some terminology, code examples, and API references may be outdated. An update is in progress. -::: - # Prerequisites & Environment -In this guide, you will set up a complete development environment for building applications on Yellow Network. +Use this checklist to prepare a local TypeScript project for the v1 Nitrolite SDK. -**Goal**: Have a working local environment ready for Yellow App development. - ---- +:::tip Choosing your SDK +New v1 app: use [`@yellow-org/sdk`](../sdk/typescript/getting-started). Migrating from 0.5.3: start with [`@yellow-org/sdk-compat`](../sdk/typescript-compat/overview) and the codemod, then move native flows over when you are ready. See the [SDK overview](../sdk/) for the full decision tree. +::: ## System Requirements | Requirement | Minimum | Recommended | |-------------|---------|-------------| -| **Node.js** | 18.x | 20.x or later | +| **Node.js** | 20.x | 20.x or later | | **npm/yarn/pnpm** | Latest stable | Latest stable | -| **Operating System** | macOS, Linux, Windows | macOS, Linux | +| **Operating System** | macOS, Linux, Windows | macOS or Linux | ---- +The package declares `engines.node >=20.0.0`. Some package managers only warn on an older runtime, but the SDK and its dependencies are tested against Node 20 or later. ## Required Knowledge @@ -45,70 +38,48 @@ Before building on Yellow Network, you should be comfortable with: If you're new to blockchain development, start with the [Ethereum Developer Documentation](https://ethereum.org/developers) to understand wallets, transactions, and smart contract basics. ::: ---- - ## Step 1: Install Node.js -### macOS (using Homebrew) +### macOS ```bash -# Install Homebrew if you don't have it -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - -# Install Node.js brew install node@20 - -# Verify installation -node --version # Should show v20.x.x -npm --version # Should show 10.x.x +node --version +npm --version ``` -### Linux (Ubuntu/Debian) +### Linux ```bash -# Install Node.js via NodeSource curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs - -# Verify installation node --version npm --version ``` ### Windows -Download and run the installer from [nodejs.org](https://nodejs.org/). - ---- +Download and run the current LTS installer from [nodejs.org](https://nodejs.org/), then verify `node --version` in a new terminal. ## Step 2: Install Core Dependencies -Create a new project and install the required packages: +Create a project and install the v1 SDK: ```bash -# Create project directory -mkdir yellow-app && cd yellow-app - -# Initialize project +mkdir yellow-app +cd yellow-app npm init -y - -# Install core dependencies -npm install @erc7824/nitrolite viem - -# Install development dependencies -npm install -D typescript @types/node tsx +npm install @yellow-org/sdk decimal.js viem dotenv +npm install -D typescript @types/node ``` -### Package Overview - | Package | Purpose | |---------|---------| -| `@erc7824/nitrolite` | Yellow Network SDK for state channel operations | -| `viem` | Modern Ethereum library for wallet and contract interactions | -| `typescript` | Type safety and better developer experience | -| `tsx` | Run TypeScript files directly | - ---- +| `@yellow-org/sdk` | v1 client for Nitronode payment channels | +| `decimal.js` | Exact asset amounts without floating-point rounding | +| `viem` | Wallet, address, and blockchain RPC utilities | +| `dotenv` | Local `.env` loading for development scripts | +| `typescript` | Type checking and JavaScript output | ## Step 3: Configure TypeScript @@ -124,10 +95,9 @@ Create `tsconfig.json`: "esModuleInterop": true, "skipLibCheck": true, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "." }, - "include": ["src/**/*"], - "exclude": ["node_modules"] + "include": ["src/**/*", "scripts/**/*"] } ``` @@ -137,84 +107,63 @@ Update `package.json`: { "type": "module", "scripts": { - "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/src/index.js" } } ``` ---- - ## Step 4: Set Up Environment Variables -Create `.env` for sensitive configuration: +Create `.env` for sensitive values: ```bash -# .env - Never commit this file! - -# Your wallet private key (for development only) +# .env - never commit this file PRIVATE_KEY=0x... - -# RPC endpoints -SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY -BASE_RPC_URL=https://base-sepolia.g.alchemy.com/v2/YOUR_KEY - -# Clearnode WebSocket endpoint -# Production: wss://clearnet.yellow.com/ws -# Sandbox: wss://clearnet-sandbox.yellow.com/ws -CLEARNODE_WS_URL=wss://clearnet-sandbox.yellow.com/ws +RECIPIENT=0x... +RPC_URL=https://rpc-amoy.polygon.technology +NITRONODE_WS_URL= ``` -Add to `.gitignore`: +:::info Sandbox URL - coming soon +`NITRONODE_WS_URL` intentionally uses a placeholder until the canonical public Nitronode sandbox endpoint is published. Keep the variable name now so the final URL swap is a one-line change. +::: + +Add local files to `.gitignore`: ```bash -# .gitignore .env .env.local node_modules/ dist/ ``` -Install dotenv for loading environment variables: - -```bash -npm install dotenv -``` - ---- - ## Step 5: Wallet Setup -### Development Wallet - -For development, create a dedicated wallet: +Use a dedicated development wallet. Never reuse a wallet that holds mainnet funds. -```typescript -// scripts/create-wallet.ts +```typescript title="scripts/create-wallet.ts" import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); -console.log('New Development Wallet'); -console.log('----------------------'); +console.log('New development wallet'); console.log('Address:', account.address); -console.log('Private Key:', privateKey); -console.log('\n⚠️ Save this private key securely and add to .env'); +console.log('Private key:', privateKey); +console.log('Save this key in .env as PRIVATE_KEY.'); ``` Run it: ```bash -npx tsx scripts/create-wallet.ts +npm run build +node dist/scripts/create-wallet.js ``` ### Get Test Tokens -#### Yellow Network Sandbox Faucet (Recommended) - -For testing on the Yellow Network Sandbox, you can request test tokens directly to your unified balance: + -#### Testnet Faucets (For On-Chain Testing) - -If you need on-chain test tokens for Sepolia or Base Sepolia: +If you need on-chain gas or token balances before the sandbox faucet is pinned, use the relevant testnet faucet for your chain and RPC endpoint. | Network | Faucet | |---------|--------| -| Sepolia | [sepoliafaucet.com](https://sepoliafaucet.com) | -| Base Sepolia | [base.org/faucet](https://www.coinbase.com/faucets/base-ethereum-goerli-faucet) | - -:::warning Development Only -Never use your main wallet or real funds for development. Always create a separate development wallet with test tokens. -::: - ---- +| Polygon Amoy | [Polygon Faucet](https://faucet.polygon.technology/) | +| Sepolia | [Ethereum Sepolia Faucet](https://sepoliafaucet.com/) | +| Base Sepolia | [Base Faucet](https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet) | ## Step 6: Verify Setup -Create `src/index.ts` to verify everything works: +Create `src/index.ts`: -```typescript +```typescript title="src/index.ts" import 'dotenv/config'; +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; import { createPublicClient, http } from 'viem'; -import { sepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; - -async function main() { - // Verify environment variables - const privateKey = process.env.PRIVATE_KEY; - if (!privateKey) { - throw new Error('PRIVATE_KEY not set in .env'); - } - - // Create account from private key - const account = privateKeyToAccount(privateKey as `0x${string}`); - console.log('✓ Wallet loaded:', account.address); - // Create public client - const client = createPublicClient({ - chain: sepolia, - transport: http(process.env.SEPOLIA_RPC_URL), - }); - - // Check connection - const blockNumber = await client.getBlockNumber(); - console.log('✓ Connected to Sepolia, block:', blockNumber); - - // Check balance - const balance = await client.getBalance({ address: account.address }); - console.log('✓ ETH balance:', balance.toString(), 'wei'); - - console.log('\n🎉 Environment setup complete!'); +const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; +const RPC_URL = process.env.RPC_URL; +const NITRONODE_WS_URL = process.env.NITRONODE_WS_URL; +const CHAIN_ID = 80002n; + +if (!PRIVATE_KEY) throw new Error('PRIVATE_KEY not set in .env'); +if (!RPC_URL) throw new Error('RPC_URL not set in .env'); + +const publicClient = createPublicClient({ + transport: http(RPC_URL), +}); + +const blockNumber = await publicClient.getBlockNumber(); +const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); + +console.log('Wallet loaded:', stateSigner.getAddress()); +console.log('RPC connected, block:', blockNumber.toString()); + +if (!NITRONODE_WS_URL || NITRONODE_WS_URL === '') { + console.log('Nitronode sandbox URL pending; wallet and RPC verified.'); +} else { + const client = await Client.create( + NITRONODE_WS_URL, + stateSigner, + txSigner, + withBlockchainRPC(CHAIN_ID, RPC_URL), + ); + + try { + console.log('Nitronode connected as:', client.getUserAddress()); + } finally { + await client.close(); + } } - -main().catch(console.error); ``` Run the verification: ```bash -npm run dev -``` - -Expected output: - -``` -✓ Wallet loaded: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb -✓ Connected to Sepolia, block: 12345678 -✓ ETH balance: 100000000000000000 wei - -🎉 Environment setup complete! -``` - ---- - -## Project Structure - -Recommended folder structure for Yellow Apps: - -``` -yellow-app/ -├── src/ -│ ├── index.ts # Entry point -│ ├── config.ts # Configuration -│ ├── client.ts # VirtualApp client setup -│ ├── auth.ts # Authentication logic -│ └── channels/ -│ ├── create.ts # Channel creation -│ ├── transfer.ts # Transfer operations -│ └── close.ts # Channel closure -├── scripts/ -│ └── create-wallet.ts # Utility scripts -├── .env # Environment variables (git-ignored) -├── .gitignore -├── package.json -└── tsconfig.json +npm run build +npm start ``` ---- - -## Supported Networks - -To get the current list of supported chains and contract addresses, query the Clearnode's `get_config` endpoint: +Until the sandbox URL is live, the script should stop after confirming the wallet and blockchain RPC. Once the URL is available, it also connects through `Client.create()` and prints `client.getUserAddress()`. -```javascript -// Example: Fetch supported chains and contract addresses -const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); +## When to add client options -ws.onopen = () => { - const request = { - req: [1, 'get_config', {}, Date.now()], - sig: [] // get_config is a public endpoint, no signature required - }; - ws.send(JSON.stringify(request)); -}; +Use `withBlockchainRPC(chainId, rpcURL)` when you call any method that needs on-chain settlement, including `checkpoint()`, `approveToken()`, token allowance checks, deposits, or withdrawals. -ws.onmessage = (event) => { - const response = JSON.parse(event.data); - console.log('Supported chains:', response.res[2].chains); - console.log('Contract addresses:', response.res[2].contracts); -}; -``` - -:::tip Dynamic Configuration -The `get_config` method returns real-time information about supported chains, contract addresses, and Clearnode capabilities. This ensures you always have the most up-to-date network information. -::: - ---- +Use `withApplicationID(appID)` when you want Nitronode records tagged with the application that produced them. This is optional metadata, but useful once multiple apps share the same node or wallet. ## Next Steps -Your environment is ready! Continue to: - -- **[Key Terms & Mental Models](./key-terms.mdx)** — Understand the core concepts -- **[Quickstart](./quickstart.mdx)** — Build your first Yellow App -- **[State Channels vs L1/L2](/nitrolite/learn/core-concepts/state-channels-vs-l1-l2)** — Deep dive into state channels - ---- - -## Common Issues - -### "Module not found" errors -Ensure you have `"type": "module"` in `package.json` and are using ESM imports. - -### "Cannot find module 'viem'" -Run `npm install` to ensure all dependencies are installed. - -### RPC rate limiting -Use a dedicated RPC provider (Infura, Alchemy) instead of public endpoints for production. - -### TypeScript errors with viem -Ensure your `tsconfig.json` has `"moduleResolution": "bundler"` or `"node16"`. - - +- [Quickstart](./quickstart) - make your first deposit and transfer. +- [Key Terms & Mental Models](./key-terms) - learn the vocabulary used across the docs. +- [TypeScript SDK](../sdk/typescript/getting-started) - read the full SDK guide. diff --git a/docs/nitrolite/build/getting-started/quickstart.mdx b/docs/nitrolite/build/getting-started/quickstart.mdx index c03ea1d..a479681 100644 --- a/docs/nitrolite/build/getting-started/quickstart.mdx +++ b/docs/nitrolite/build/getting-started/quickstart.mdx @@ -1,1081 +1,206 @@ --- title: Quickstart -description: Get up and running with the Yellow Network SDK in minutes. +description: Create a v1 Nitrolite client, fund a channel, and send a transfer with @yellow-org/sdk. +sidebar_position: 1 +displayed_sidebar: buildSidebar --- -:::warning[Work in Progress] -This page was carried over from the v0.5.x documentation and has not yet been fully updated for v1.x. Some terminology, code examples, and API references may be outdated. An update is in progress. -::: - -# Quickstart Guide - -This guide provides a step-by-step walkthrough of integrating with the Yellow Network using the VirtualApp SDK. We will build a script to connect to the network, authenticate, manage state channels, and transfer funds. - -## Prerequisites +# Quickstart -- [Node.js](https://nodejs.org/) (v18 or higher) -- [npm](https://www.npmjs.com/) +Build a minimal Nitrolite script that connects to Nitronode, signs channel states with your wallet, deposits USDC into a home channel, sends an off-chain transfer, and closes the connection cleanly. -## Setup - -1. **Install Dependencies** +:::info Sandbox URL - coming soon +The canonical public Nitronode sandbox WebSocket URL is still being pinned by the ops team. Use the literal placeholder shown below, then replace it in the WebSocket URL line only when the final hostname is published. +::: - ```bash - npm install - ``` +:::tip Migrating from 0.5.3? +Already on `@erc7824/nitrolite@0.5.3`? Skip ahead to [Migrating from 0.5.3](../sdk/typescript-compat/overview) - you'll probably want `@yellow-org/sdk-compat` plus the `@yellow-org/nitrolite-codemod`, not this guide. +::: -2. **Environment Variables** +## Prerequisites - Create a `.env` file in your project root: +Before you start, make sure you have: - ```bash - # .env - PRIVATE_KEY=your_sepolia_private_key_here - ALCHEMY_RPC_URL=your_alchemy_rpc_url_here - ``` +- Node.js 20 or later. +- A development wallet private key with testnet gas. +- A blockchain RPC endpoint for Polygon Amoy or your target chain. +- `decimal.js` for exact asset amounts. +- `viem` for wallet and address types used by the SDK. -## 1. Getting Funds +See [Prerequisites & Environment](./prerequisites) for a full setup checklist. -Before we write code, you need test tokens (`ytest.usd`). In the Sandbox, these tokens land in your **Unified Balance** (Off-Chain), which sits in the Yellow Network's clearing layer. +## Step 1: Install -Request tokens via the Faucet: +Create a new TypeScript project and install the v1 SDK: ```bash -curl -XPOST https://clearnet-sandbox.yellow.com/faucet/requestTokens \ - -H "Content-Type: application/json" \ - -d '{"userAddress":""}' +mkdir yellow-quickstart +cd yellow-quickstart +npm init -y +npm install @yellow-org/sdk decimal.js viem dotenv +npm install -D typescript @types/node ``` -## 2. Initialization - -First, we setup the `VirtualAppClient` with Viem. This client handles all communication with the Yellow Network nodes and smart contracts. - -```typescript -import { NitroliteClient, WalletStateSigner, createECDSAMessageSigner } from '@erc7824/nitrolite'; -import { createPublicClient, createWalletClient, http } from 'viem'; -import { sepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; -import WebSocket from 'ws'; -import 'dotenv/config'; - -// Setup Viem Clients -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); -const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.ALCHEMY_RPC_URL) }); -const walletClient = createWalletClient({ chain: sepolia, transport: http(), account }); - -// Initialize VirtualApp Client -const client = new VirtualAppClient({ - publicClient, - walletClient, - stateSigner: new WalletStateSigner(walletClient), - addresses: { - custody: '0x019B65A265EB3363822f2752141b3dF16131b262', - adjudicator: '0x7c7ccbc98469190849BCC6c926307794fDfB11F2', - }, - chainId: sepolia.id, - challengeDuration: 3600n, -}); +Set the project to ESM and add a build script: -// Connect to Sandbox Node -const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); +```json title="package.json" +{ + "type": "module", + "scripts": { + "build": "tsc" + } +} ``` -## 3. Authentication - -Authentication involves generating a temporary **Session Key** and verifying your identity using your main wallet (EIP-712). - -```typescript -// Generate temporary session key -const sessionPrivateKey = generatePrivateKey(); -const sessionSigner = createECDSAMessageSigner(sessionPrivateKey); -const sessionAccount = privateKeyToAccount(sessionPrivateKey); - -// Send auth request -const authRequestMsg = await createAuthRequestMessage({ - address: account.address, - application: 'Test app', - session_key: sessionAccount.address, - allowances: [{ asset: 'ytest.usd', amount: '1000000000' }], - expires_at: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour - scope: 'test.app', -}); -ws.send(authRequestMsg); - -// Handle Challenge (in ws.onmessage) -if (type === 'auth_challenge') { - const challenge = response.res[2].challenge_message; - // Sign with MAIN wallet - const signer = createEIP712AuthMessageSigner(walletClient, authParams, { name: 'Test app' }); - const verifyMsg = await createAuthVerifyMessageFromChallenge(signer, challenge); - ws.send(verifyMsg); +Add a minimal `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src/**/*"] } ``` -## 4. Channel Lifecycle - -### Creating a Channel - -If no channel exists, we request the Node to open one. - -```typescript -const createChannelMsg = await createCreateChannelMessage( - sessionSigner, // Sign with session key - { - chain_id: 11155111, // Sepolia - token: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', // ytest.usd - } -); -ws.send(createChannelMsg); +## Step 2: Create signers -// Listen for 'create_channel' response, then submit to chain -const createResult = await client.createChannel({ - channel, - unsignedInitialState, - serverSignature, -}); -``` +`createSigners()` builds the two signer objects the client needs: one for off-chain channel states and one for blockchain transactions. -### Funding (Resizing) +```typescript title="src/index.ts" +import 'dotenv/config'; +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; -To fund the channel, we perform a "Resize". Since your funds are in your **Unified Balance** (from the Faucet), we use `allocate_amount` to move them into the Channel. +const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; +const RECIPIENT = process.env.RECIPIENT as `0x${string}`; +const RPC_URL = process.env.RPC_URL; +const CHAIN_ID = 80002n; // Polygon Amoy +const ASSET = 'usdc'; -> **Important:** Do NOT use `resize_amount` unless you have deposited funds directly into the L1 Custody Contract. +if (!PRIVATE_KEY) throw new Error('Set PRIVATE_KEY in .env'); +if (!RECIPIENT) throw new Error('Set RECIPIENT in .env'); +if (!RPC_URL) throw new Error('Set RPC_URL in .env'); -```typescript -const resizeMsg = await createResizeChannelMessage( - sessionSigner, - { - channel_id: channelId, - allocate_amount: 20n, // Moves 20 units from Unified Balance -> Channel - funds_destination: account.address, - } -); -ws.send(resizeMsg); +const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); -// Submit resize proof to chain -await client.resizeChannel({ resizeState, proofStates }); +console.log('Wallet address:', stateSigner.getAddress()); ``` -### Closing & Withdrawing +## Step 3: Connect -Finally, we cooperatively close the channel. This settles the balance on the L1 Custody Contract, allowing you to withdraw. +Create the client with a Nitronode WebSocket URL, both signers, and a chain RPC endpoint for on-chain settlement. -```typescript -// Close Channel -const closeMsg = await createCloseChannelMessage(sessionSigner, channelId, account.address); -ws.send(closeMsg); +```typescript title="src/index.ts" +const NITRONODE_WS_URL = ''; // Replace with the canonical sandbox URL. -// Submit close to chain -await client.closeChannel({ finalState, stateData }); +const client = await Client.create( + NITRONODE_WS_URL, + stateSigner, + txSigner, + withBlockchainRPC(CHAIN_ID, RPC_URL), +); -// Withdraw from Custody Contract to Wallet -const withdrawalTx = await client.withdrawal(tokenAddress, withdrawableBalance); -console.log('Funds withdrawn:', withdrawalTx); +await client.setHomeBlockchain(ASSET, CHAIN_ID); +console.log('Connected as:', client.getUserAddress()); ``` -## Troubleshooting +## Step 4: Get test tokens -Here are common issues and solutions: - -- **`InsufficientBalance`**: - - **Cause**: Trying to use `resize_amount` (L1 funds) without depositing first. - - **Fix**: Use `allocate_amount` to fund from your Off-chain Unified Balance (Faucet). - -- **`DepositAlreadyFulfilled`**: - - **Cause**: Double-submitting a funding request or channel creation. - - **Fix**: Check if the channel is already open or funded before sending requests. - -- **`InvalidState`**: - - **Cause**: Resizing a closed channel or version mismatch. - - **Fix**: Ensure you are using the latest channel state from the Node. - -- **`operation denied: non-zero allocation`**: - - **Cause**: Too many "stale" channels open. - - **Fix**: Run the cleanup script `npx tsx close_all.ts`. - -- **Timeout waiting for User to fund Custody**: - - **Cause**: Re-running scripts without closing channels accumulates balance requirements. - - **Fix**: Run `close_all.ts` to reset. - -### Cleanup Script - -If you get stuck, use this script to close all open channels: + - // Wait for balance update - await new Promise(r => setTimeout(r, 2000)); - console.log('✓ Resize complete.'); - } else { - console.log(' Skipping resize step (already funded).'); - } +## Step 5: Deposit - // Verify Channel Balance - const channelBalances = await publicClient.readContract({ - address: client.addresses.custody, - abi: [{ - name: 'getChannelBalances', - type: 'function', - stateMutability: 'view', - inputs: [{ name: 'channelId', type: 'bytes32' }, { name: 'tokens', type: 'address[]' }], - outputs: [{ name: 'balances', type: 'uint256[]' }] - }], - functionName: 'getChannelBalances', - args: [channelId as `0x${string}`, [token as `0x${string}`]], - }) as bigint[]; - console.log(`✓ Channel funded with ${channelBalances[0]} USDC`); +Deposit builds and co-signs the next channel state off-chain. `checkpoint()` submits that state to the ChannelHub contract so the deposit is enforced on-chain. - // Check User Balance again - let finalUserBalance = 0n; - try { - const result = await publicClient.readContract({ - address: client.addresses.custody, - abi: [{ - type: 'function', - name: 'getAccountsBalances', - inputs: [{ name: 'users', type: 'address[]' }, { name: 'tokens', type: 'address[]' }], - outputs: [{ type: 'uint256[]' }], - stateMutability: 'view' - }] as const, - functionName: 'getAccountsBalances', - args: [[client.account.address], [token as `0x${string}`]], - }) as bigint[]; - finalUserBalance = result[0]; - console.log(`✓ User Custody Balance after resize: ${finalUserBalance}`); - } catch (e) { - console.warn(' Error checking final user balance:', e); - } +```typescript title="src/index.ts" +await client.approveToken(CHAIN_ID, ASSET, new Decimal(5)); - // ------------------------------------------------------------------- - // 4. Off-Chain Transfer - // ------------------------------------------------------------------- -}; +const depositState = await client.deposit(CHAIN_ID, ASSET, new Decimal(5)); +console.log('Deposit state version:', depositState.version); -// State to prevent infinite auth loops -let isAuthenticated = false; - -// Step 3: Sign the challenge with your MAIN wallet (EIP-712) -ws.onmessage = async (event) => { - const response = JSON.parse(event.data.toString()); - console.log('Received WS message:', JSON.stringify(response, null, 2)); - - if (response.error) { - console.error('RPC Error:', response.error); - process.exit(1); // Exit on error to prevent infinite loops - } - - if (response.res && response.res[1] === 'auth_challenge') { - if (isAuthenticated) { - console.log(' Ignoring auth_challenge (already authenticated)'); - return; - } - - const challenge = response.res[2].challenge_message; - - // Create EIP-712 typed data signature with main wallet - const signer = createEIP712AuthMessageSigner( - walletClient, - authParams, - { name: 'Test app' } - ); - - // Send auth_verify using builder - // We sign with the MAIN wallet for the first verification - const verifyMsg = await createAuthVerifyMessageFromChallenge( - signer, - challenge - ); - - ws.send(verifyMsg); - } - - if (response.res && response.res[1] === 'auth_verify') { - console.log('✓ Authenticated successfully'); - isAuthenticated = true; // Mark as authenticated - const sessionKey = response.res[2].session_key; - console.log(' Session key:', sessionKey); - console.log(' JWT token received'); - - // Query Ledger Balances - const ledgerMsg = await createGetLedgerBalancesMessage( - sessionSigner, - account.address, - Date.now() - ); - ws.send(ledgerMsg); - console.log(' Sent get_ledger_balances request...'); - - // Wait for 'channels' message to proceed - - } - - if (response.res && response.res[1] === 'channels') { - const channels = response.res[2].channels; - const openChannel = channels.find((c: any) => c.status === 'open'); - - // Derive token - const chainId = sepolia.id; - const supportedAsset = (config.assets as any)?.find((a: any) => a.chain_id === chainId); - const token = supportedAsset ? supportedAsset.token : '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; - - if (openChannel) { - console.log('✓ Found existing open channel'); - - // CORRECT: Check if channel is already funded - const currentAmount = BigInt(openChannel.amount || 0); // Need to parse amount - // Wait, standard RPC returns strings. Let's rely on openChannel structure. - // openChannel object from logs: { ..., amount: "40", ... } - - if (BigInt(openChannel.amount) >= 20n) { - console.log(` Channel already funded with ${openChannel.amount} USDC.`); - console.log(' Skipping resize to avoid "Insufficient Balance" errors.'); - // Call triggerResize but indicate skipping actual resize - await triggerResize(openChannel.channel_id, token, true); - } else { - await triggerResize(openChannel.channel_id, token, false); - } - } else { - console.log(' No existing open channel found, creating new one...'); - console.log(' Using token:', token, 'for chain:', chainId); - - // Request channel creation - const createChannelMsg = await createCreateChannelMessage( - sessionSigner, - { - chain_id: 11155111, // Sepolia - token: token, - } - ); - ws.send(createChannelMsg); - } - } - - if (response.res && response.res[1] === 'create_channel') { - const { channel_id, channel, state, server_signature } = response.res[2]; - activeChannelId = channel_id; - - console.log('✓ Channel prepared:', channel_id); - console.log(' State object:', JSON.stringify(state, null, 2)); - - // Transform state object to match UnsignedState interface - const unsignedInitialState = { - intent: state.intent, - version: BigInt(state.version), - data: state.state_data, // Map state_data to data - allocations: state.allocations.map((a: any) => ({ - destination: a.destination, - token: a.token, - amount: BigInt(a.amount), - })), - }; - - // Submit to blockchain - const createResult = await client.createChannel({ - channel, - unsignedInitialState, - serverSignature: server_signature, - }); - - // createChannel returns an object { txHash, ... } or just hash depending on version. - // Based on logs: { channelId: ..., initialState: ..., txHash: ... } - // We need to handle both or just the object. - const txHash = typeof createResult === 'string' ? createResult : createResult.txHash; - - console.log('✓ Channel created on-chain:', txHash); - console.log(' Waiting for transaction confirmation...'); - await publicClient.waitForTransactionReceipt({ hash: txHash }); - console.log('✓ Transaction confirmed'); - - // Retrieve token from allocations - - const token = state.allocations[0].token; - await triggerResize(channel_id, token, false); - } - - if (response.res && response.res[1] === 'resize_channel') { - const { channel_id, state, server_signature } = response.res[2]; - - console.log('✓ Resize prepared'); - console.log(' Server returned allocations:', JSON.stringify(state.allocations, null, 2)); - - // Construct the resize state object expected by the SDK - const resizeState = { - intent: state.intent, - version: BigInt(state.version), - data: state.state_data || state.data, // Handle potential naming differences - allocations: state.allocations.map((a: any) => ({ - destination: a.destination, - token: a.token, - amount: BigInt(a.amount), - })), - channelId: channel_id, - serverSignature: server_signature, - }; - - console.log('DEBUG: resizeState:', JSON.stringify(resizeState, (key, value) => - typeof value === 'bigint' ? value.toString() : value, 2)); - - let proofStates: any[] = []; - try { - const onChainData = await client.getChannelData(channel_id as `0x${string}`); - console.log('DEBUG: On-chain channel data:', JSON.stringify(onChainData, (key, value) => - typeof value === 'bigint' ? value.toString() : value, 2)); - if (onChainData.lastValidState) { - proofStates = [onChainData.lastValidState]; - } - } catch (e) { - console.log('DEBUG: Failed to fetch on-chain data:', e); - } - - // Calculate total required for the token - const token = resizeState.allocations[0].token; - const requiredAmount = resizeState.allocations.reduce((sum: bigint, a: any) => { - if (a.token === token) return sum + BigInt(a.amount); - return sum; - }, 0n); - - console.log(` Waiting for channel funding (Required: ${requiredAmount})...`); - - // Poll for User's Custody Balance (since User allocation is increasing) - let userBalance = 0n; - let retries = 0; - const userAddress = client.account.address; - - console.log(` Checking User Custody Balance for ${userAddress}... [v2]`); - - // Check initial balance first - try { - const result = await publicClient.readContract({ - address: client.addresses.custody, - abi: [ - { - type: 'function', - name: 'getAccountsBalances', - inputs: [ - { name: 'users', type: 'address[]' }, - { name: 'tokens', type: 'address[]' } - ], - outputs: [{ type: 'uint256[]' }], - stateMutability: 'view' - } - ] as const, - functionName: 'getAccountsBalances', - args: [[userAddress], [token as `0x${string}`]], - }) as bigint[]; - userBalance = result[0]; - } catch (e) { - console.warn(' Error checking initial user balance:', e); - } - - console.log(' Skipping L1 deposit (using off-chain faucet funds)...'); - - if (true) { // Skip the wait loop as we just deposited - // Define ABI fragment for getAccountsBalances - const custodyAbiFragment = [ - { - type: 'function', - name: 'getAccountsBalances', - inputs: [ - { name: 'users', type: 'address[]' }, - { name: 'tokens', type: 'address[]' } - ], - outputs: [{ type: 'uint256[]' }], - stateMutability: 'view' - } - ] as const; - - while (retries < 30) { // Wait up to 60 seconds - try { - const result = await publicClient.readContract({ - address: client.addresses.custody, - abi: custodyAbiFragment, - functionName: 'getAccountsBalances', - args: [[userAddress], [token as `0x${string}`]], - }) as bigint[]; - - userBalance = result[0]; - } catch (e) { - console.warn(' Error checking user balance:', e); - } - - if (userBalance >= requiredAmount) { - console.log(`✓ User funded in Custody (Balance: ${userBalance})`); - break; - } - await new Promise(r => setTimeout(r, 2000)); - retries++; - if (retries % 5 === 0) console.log(` User Custody Balance: ${userBalance}, Waiting...`); - } - - if (userBalance < requiredAmount) { - console.error('Timeout waiting for User to fund Custody account'); - console.warn('Proceeding with resize despite low user balance...'); - } - } else { - console.log(`✓ User funded in Custody (Balance: ${userBalance})`); - } - - console.log(' Submitting resize to chain...'); - // Submit to blockchain - const { txHash } = await client.resizeChannel({ - resizeState, - proofStates: proofStates, - }); - - console.log('✓ Channel resized on-chain:', txHash); - console.log('✓ Channel funded with 20 USDC'); - - // Skip Transfer for debugging - console.log(' Skipping transfer to verify withdrawal amount...'); - console.log(' Debug: channel_id =', channel_id); - - // Wait for server to sync state - await new Promise(r => setTimeout(r, 3000)); +const depositTx = await client.checkpoint(ASSET); +console.log('Deposit checkpoint tx:', depositTx); +``` - if (channel_id) { - console.log(' Closing channel:', channel_id); - const closeMsg = await createCloseChannelMessage( - sessionSigner, - channel_id as `0x${string}`, - account.address - ); - ws.send(closeMsg); - } else { - console.log(' No channel ID available to close.'); - } - } - // const secondaryAddress = '0x7df1fef832b57e46de2e1541951289c04b2781aa'; - // console.log(` Attempting Transfer to Secondary Wallet: ${secondaryAddress}...`); +`approveToken()` is only needed before the first checkpoint for a token, or when you need to increase allowance. - // const transferMsg = await createTransferMessage( - // sessionSigner, - // { - // destination: secondaryAddress, - // allocations: [{ - // asset: 'ytest.usd', - // amount: '10' - // }] - // }, - // Date.now() - // ); - // ws.send(transferMsg); - // console.log(' Sent transfer request...'); +## Step 6: Transfer - // if (response.res && response.res[1] === 'transfer') { - // console.log('✓ Transfer complete!'); - // console.log(' Amount: 10 USDC'); +Transfers advance the signed off-chain state. They do not need a checkpoint unless you want to settle the latest state on-chain. - // if (activeChannelId) { - // console.log(' Closing channel:', activeChannelId); - // const closeMsg = await createCloseChannelMessage( - // sessionSigner, - // activeChannelId as `0x${string}`, - // account.address - // ); - // ws.send(closeMsg); - // } else { - // console.log(' No active channel ID to close.'); - // } - // } +```typescript title="src/index.ts" +const transferState = await client.transfer( + RECIPIENT, + ASSET, + new Decimal(1), +); - if (response.res && response.res[1] === 'close_channel') { - const { channel_id, state, server_signature } = response.res[2]; - console.log('✓ Close prepared'); - console.log(' Submitting close to chain...'); +console.log('Transfer transition:', transferState.transition.type); +``` - // Submit to blockchain - const txHash = await client.closeChannel({ - finalState: { - intent: state.intent, - version: BigInt(state.version), - data: state.state_data || state.data, - allocations: state.allocations.map((a: any) => ({ - destination: a.destination, - token: a.token, - amount: BigInt(a.amount), - })), - channelId: channel_id, - serverSignature: server_signature, - }, - stateData: state.state_data || state.data || '0x', - }); +## Step 7: Clean up - console.log('✓ Channel closed on-chain:', txHash); +Always close the client when your script is done: - // Withdraw funds - console.log(' Withdrawing funds...'); - const token = state.allocations[0].token; +```typescript title="src/index.ts" +await client.close(); +``` - await new Promise(r => setTimeout(r, 2000)); // Wait for close to settle +A complete script should wrap the channel operations in `try` / `finally` so the WebSocket closes even if a request fails. - let withdrawableBalance = 0n; - try { - const result = await publicClient.readContract({ - address: client.addresses.custody, - abi: [{ - type: 'function', - name: 'getAccountsBalances', - inputs: [{ name: 'users', type: 'address[]' }, { name: 'tokens', type: 'address[]' }], - outputs: [{ type: 'uint256[]' }], - stateMutability: 'view' - }] as const, - functionName: 'getAccountsBalances', - args: [[client.account.address], [token as `0x${string}`]], - }) as bigint[]; - withdrawableBalance = result[0]; - console.log(`✓ User Custody Balance (Withdrawable): ${withdrawableBalance}`); - } catch (e) { - console.warn(' Error checking withdrawable balance:', e); - } +```typescript title="src/index.ts" +try { + await client.approveToken(CHAIN_ID, ASSET, new Decimal(5)); - if (withdrawableBalance > 0n) { - console.log(` Withdrawing ${withdrawableBalance} of ${token}...`); - const withdrawalTx = await client.withdrawal(token as `0x${string}`, withdrawableBalance); - console.log('✓ Funds withdrawn:', withdrawalTx); - } else { - console.log(' No funds to withdraw.'); - } + const depositState = await client.deposit(CHAIN_ID, ASSET, new Decimal(5)); + console.log('Deposit state version:', depositState.version); - process.exit(0); - } -}; + const depositTx = await client.checkpoint(ASSET); + console.log('Deposit checkpoint tx:', depositTx); -// Start the flow -if (ws.readyState === WebSocket.OPEN) { - ws.send(authRequestMsg); -} else { - ws.on('open', () => { - ws.send(authRequestMsg); - }); + const transferState = await client.transfer( + RECIPIENT, + ASSET, + new Decimal(1), + ); + console.log('Transfer transition:', transferState.transition.type); +} finally { + await client.close(); } ``` - - -### close_all.ts - -
-Click to view full close_all.ts - -```typescript -import { - VirtualAppClient, - WalletStateSigner, - createECDSAMessageSigner, - createEIP712AuthMessageSigner, - createAuthRequestMessage, - createAuthVerifyMessageFromChallenge, - createCloseChannelMessage, -} from '@erc7824/nitrolite'; -import { createPublicClient, createWalletClient, http } from 'viem'; -import { sepolia } from 'viem/chains'; -import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; -import WebSocket from 'ws'; -import 'dotenv/config'; -import * as readline from 'readline'; - -// Helper to prompt for input -const askQuestion = (query: string): Promise => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - return new Promise(resolve => rl.question(query, ans => { - rl.close(); - resolve(ans); - })); -}; - -// Configuration -const WS_URL = 'wss://clearnet-sandbox.yellow.com/ws'; - -async function main() { - console.log('Starting cleanup script...'); - - // Setup Viem Clients - let PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; - - if (!PRIVATE_KEY) { - console.log('PRIVATE_KEY not found in .env'); - const inputKey = await askQuestion('Please enter your Private Key: '); - if (!inputKey) { - throw new Error('Private Key is required'); - } - PRIVATE_KEY = inputKey.startsWith('0x') ? inputKey as `0x${string}` : `0x${inputKey}` as `0x${string}`; - } - - const account = privateKeyToAccount(PRIVATE_KEY); - - const ALCHEMY_RPC_URL = process.env.ALCHEMY_RPC_URL; - const FALLBACK_RPC_URL = 'https://1rpc.io/sepolia'; // Public fallback - const RPC_URL = ALCHEMY_RPC_URL || FALLBACK_RPC_URL; - const publicClient = createPublicClient({ - chain: sepolia, - transport: http(RPC_URL), - }); - const walletClient = createWalletClient({ - account, - chain: sepolia, - transport: http(RPC_URL), - }); +Run it after the sandbox URL is available and your `.env` values are set: - // Initialize VirtualApp Client - const client = new VirtualAppClient({ - publicClient, - walletClient, - addresses: { - custody: '0x019B65A265EB3363822f2752141b3dF16131b262', - adjudicator: '0x7c7ccbc98469190849BCC6c926307794fDfB11F2', - }, - challengeDuration: 3600n, - chainId: sepolia.id, - stateSigner: new WalletStateSigner(walletClient), - }); - - // Connect to WebSocket - const ws = new WebSocket(WS_URL); - const sessionPrivateKey = generatePrivateKey(); - const sessionSigner = createECDSAMessageSigner(sessionPrivateKey); - const sessionAccount = privateKeyToAccount(sessionPrivateKey); - - await new Promise((resolve, reject) => { - ws.on('open', () => resolve()); - ws.on('error', (err) => reject(err)); - }); - console.log('✓ Connected to WebSocket'); - - // Authenticate - const authParams = { - session_key: sessionAccount.address, - allowances: [{ asset: 'ytest.usd', amount: '1000000000' }], - expires_at: BigInt(Math.floor(Date.now() / 1000) + 3600), - scope: 'test.app', - }; - - const authRequestMsg = await createAuthRequestMessage({ - address: account.address, - application: 'Test app', - ...authParams - }); - ws.send(authRequestMsg); - - ws.on('message', async (data) => { - const response = JSON.parse(data.toString()); - - if (response.res) { - const type = response.res[1]; - - if (type === 'auth_challenge') { - const challenge = response.res[2].challenge_message; - const signer = createEIP712AuthMessageSigner(walletClient, authParams, { name: 'Test app' }); - const verifyMsg = await createAuthVerifyMessageFromChallenge(signer, challenge); - ws.send(verifyMsg); - } - - if (type === 'auth_verify') { - console.log('✓ Authenticated'); - - // Fetch open channels from L1 Contract - console.log('Fetching open channels from L1...'); - try { - const openChannelsL1 = await client.getOpenChannels(); - console.log(`Found ${openChannelsL1.length} open channels on L1.`); - - if (openChannelsL1.length === 0) { - console.log('No open channels on L1 to close.'); - process.exit(0); - } - - // Iterate and close - for (const channelId of openChannelsL1) { - console.log(`Attempting to close channel ${channelId}...`); - - // Send close request to Node - const closeMsg = await createCloseChannelMessage( - sessionSigner, - channelId, - account.address - ); - ws.send(closeMsg); - - // Small delay to avoid rate limits - await new Promise(r => setTimeout(r, 500)); - } - - } catch (e) { - console.error('Error fetching L1 channels:', e); - process.exit(1); - } - } - - if (type === 'close_channel') { - const { channel_id, state, server_signature } = response.res[2]; - console.log(`✓ Node signed close for ${channel_id}`); +```bash +npm run build +node dist/index.js +``` - const finalState = { - intent: state.intent, - version: BigInt(state.version), - data: state.state_data, - allocations: state.allocations.map((a: any) => ({ - destination: a.destination, - token: a.token, - amount: BigInt(a.amount), - })), - channelId: channel_id, - serverSignature: server_signature, - }; +## What just happened - try { - console.log(` Submitting close to L1 for ${channel_id}...`); - const txHash = await client.closeChannel({ - finalState, - stateData: finalState.data - }); - console.log(`✓ Closed on-chain: ${txHash}`); - } catch (e) { - // If it fails (e.g. already closed or race condition), just log and continue - console.error(`Failed to close ${channel_id} on-chain:`, e); - } - } +- `createSigners()` derived wallet-backed signers for off-chain states and on-chain transactions. +- `Client.create()` opened a v1 Nitronode WebSocket connection and attached a blockchain RPC endpoint for `checkpoint()`. +- `deposit()` prepared a mutually signed channel state, then `checkpoint()` enforced it on-chain through ChannelHub. +- `transfer()` advanced the off-chain channel state using `Decimal` amounts so asset precision stays exact. - if (response.error) { - console.error('WS Error:', response.error); - } - } - }); -} +## Next steps -main(); -``` -
+- [Configuration](../sdk/typescript/configuration) - timeouts, RPC endpoints, application IDs, and error handling. +- [Multi-party app sessions](../sdk/multi-party-app-sessions) - commit channel funds into app sessions. +- [Errors and recovery](../sdk/typescript/configuration#error-handling) - handle connection and operation failures. +- [Migrating from 0.5.3](../sdk/typescript-compat/overview) - use the compat package and codemod before moving to native v1 APIs. diff --git a/docs/nitrolite/build/quick-start/index.md b/docs/nitrolite/build/quick-start/index.md deleted file mode 100644 index e9ae8c3..0000000 --- a/docs/nitrolite/build/quick-start/index.md +++ /dev/null @@ -1,359 +0,0 @@ ---- -sidebar_position: 1 -sidebar_label: Quick Start -title: Quick Start -description: Build your first Yellow App in 5 minutes - a complete beginner's guide -keywords: [yellow sdk, quick start, tutorial, virtualapp, state channels, beginner guide] -displayed_sidebar: buildSidebar ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Quick Start Guide - -Build your first Yellow App in 5 minutes! This guide walks you through creating a simple payment application using state channels. - -## What You'll Build - -A basic payment app where users can: -- Deposit funds into a state channel -- Send instant payments to another user -- Withdraw remaining funds - -No blockchain knowledge required - we'll handle the complexity for you! - -## Prerequisites - -- **Node.js 16+** installed on your computer -- **A wallet** (MetaMask recommended) -- **Basic JavaScript/TypeScript** knowledge - -## Step 1: Installation - -Create a new project and install the Yellow SDK: - - - - -```bash showLineNumbers -mkdir my-yellow-app -cd my-yellow-app -npm init -y -npm install @erc7824/nitrolite -``` - - - - -```bash showLineNumbers -mkdir my-yellow-app -cd my-yellow-app -yarn init -y -yarn add @erc7824/nitrolite -``` - - - - -```bash showLineNumbers -mkdir my-yellow-app -cd my-yellow-app -pnpm init -pnpm add @erc7824/nitrolite -``` - - - - -## Step 2: Connect to ClearNode - -Create a file `app.js` and connect to the Yellow Network. - -:::tip Clearnode Endpoints -- **Production**: `wss://clearnet.yellow.com/ws` -- **Sandbox**: `wss://clearnet-sandbox.yellow.com/ws` (recommended for testing) -::: - -```javascript title="app.js" showLineNumbers -import { createAppSessionMessage, parseRPCResponse } from '@erc7824/nitrolite'; - -// Connect to Yellow Network (using sandbox for testing) -const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); - -ws.onopen = () => { - console.log('✅ Connected to Yellow Network!'); -}; - -ws.onmessage = (event) => { - const message = parseRPCResponse(event.data); - console.log('📨 Received:', message); -}; - -ws.onerror = (error) => { - console.error('Connection error:', error); -}; - -console.log('Connecting to Yellow Network...'); -``` - -## Step 3: Create Application Session - -Set up your wallet for signing messages: - -```javascript showLineNumbers -// Set up message signer for your wallet -async function setupMessageSigner() { - if (!window.ethereum) { - throw new Error('Please install MetaMask'); - } - - // Request wallet connection - const accounts = await window.ethereum.request({ - method: 'eth_requestAccounts' - }); - - const userAddress = accounts[0]; - - // Create message signer function - const messageSigner = async (message) => { - return await window.ethereum.request({ - method: 'personal_sign', - params: [message, userAddress] - }); - }; - - console.log('✅ Wallet connected:', userAddress); - return { userAddress, messageSigner }; -} -``` - -## Step 4: Create Application Session - -Create a session for your payment app: - -```javascript showLineNumbers -async function createPaymentSession(messageSigner, userAddress, partnerAddress) { - // Define your payment application - const appDefinition = { - protocol: 'payment-app-v1', - participants: [userAddress, partnerAddress], - weights: [50, 50], // Equal participation - quorum: 100, // Both participants must agree - challenge: 0, - nonce: Date.now() - }; - - // Initial balances (1 USDC = 1,000,000 units with 6 decimals) - const allocations = [ - { participant: userAddress, asset: 'usdc', amount: '800000' }, // 0.8 USDC - { participant: partnerAddress, asset: 'usdc', amount: '200000' } // 0.2 USDC - ]; - - // Create signed session message - const sessionMessage = await createAppSessionMessage( - messageSigner, - [{ definition: appDefinition, allocations }] - ); - - // Send to ClearNode - ws.send(sessionMessage); - console.log('✅ Payment session created!'); - - return { appDefinition, allocations }; -} -``` - -## Step 5: Send Instant Payments - -```javascript showLineNumbers -async function sendPayment(ws, messageSigner, amount, recipient) { - // Create payment message - const paymentData = { - type: 'payment', - amount: amount.toString(), - recipient, - timestamp: Date.now() - }; - - // Sign the payment - const signature = await messageSigner(JSON.stringify(paymentData)); - - const signedPayment = { - ...paymentData, - signature, - sender: await getCurrentUserAddress() - }; - - // Send instantly through ClearNode - ws.send(JSON.stringify(signedPayment)); - console.log('💸 Payment sent instantly!'); -} - -// Usage -await sendPayment(ws, messageSigner, 100000n, partnerAddress); // Send 0.1 USDC -``` - -## Step 6: Handle Incoming Messages - -```javascript showLineNumbers -// Enhanced message handling -ws.onmessage = (event) => { - const message = parseRPCResponse(event.data); - - switch (message.type) { - case 'session_created': - console.log('✅ Session confirmed:', message.sessionId); - break; - - case 'payment': - console.log('💰 Payment received:', message.amount); - // Update your app's UI - updateBalance(message.amount, message.sender); - break; - - case 'session_message': - console.log('📨 App message:', message.data); - handleAppMessage(message); - break; - - case 'error': - console.error('❌ Error:', message.error); - break; - } -}; - -function updateBalance(amount, sender) { - console.log(`Received ${amount} from ${sender}`); - // Update your application state -} -``` - -## Complete Example - -Here's a complete working example you can copy and run: - -```javascript title="SimplePaymentApp.js" showLineNumbers -import { createAppSessionMessage, parseRPCResponse } from '@erc7824/nitrolite'; - -class SimplePaymentApp { - constructor() { - this.ws = null; - this.messageSigner = null; - this.userAddress = null; - this.sessionId = null; - } - - async init() { - // Step 1: Set up wallet - const { userAddress, messageSigner } = await this.setupWallet(); - this.userAddress = userAddress; - this.messageSigner = messageSigner; - - // Step 2: Connect to ClearNode (sandbox for testing) - this.ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); - - this.ws.onopen = () => { - console.log('🟢 Connected to Yellow Network!'); - }; - - this.ws.onmessage = (event) => { - this.handleMessage(parseRPCResponse(event.data)); - }; - - return userAddress; - } - - async setupWallet() { - const accounts = await window.ethereum.request({ - method: 'eth_requestAccounts' - }); - - const userAddress = accounts[0]; - const messageSigner = async (message) => { - return await window.ethereum.request({ - method: 'personal_sign', - params: [message, userAddress] - }); - }; - - return { userAddress, messageSigner }; - } - - async createSession(partnerAddress) { - const appDefinition = { - protocol: 'payment-app-v1', - participants: [this.userAddress, partnerAddress], - weights: [50, 50], - quorum: 100, - challenge: 0, - nonce: Date.now() - }; - - const allocations = [ - { participant: this.userAddress, asset: 'usdc', amount: '800000' }, - { participant: partnerAddress, asset: 'usdc', amount: '200000' } - ]; - - const sessionMessage = await createAppSessionMessage( - this.messageSigner, - [{ definition: appDefinition, allocations }] - ); - - this.ws.send(sessionMessage); - console.log('✅ Payment session created!'); - } - - async sendPayment(amount, recipient) { - const paymentData = { - type: 'payment', - amount: amount.toString(), - recipient, - timestamp: Date.now() - }; - - const signature = await this.messageSigner(JSON.stringify(paymentData)); - - this.ws.send(JSON.stringify({ - ...paymentData, - signature, - sender: this.userAddress - })); - - console.log(`💸 Sent ${amount} instantly!`); - } - - handleMessage(message) { - switch (message.type) { - case 'session_created': - this.sessionId = message.sessionId; - console.log('✅ Session ready:', this.sessionId); - break; - case 'payment': - console.log('💰 Payment received:', message.amount); - break; - } - } -} - -// Usage -const app = new SimplePaymentApp(); -await app.init(); -await app.createSession('0xPartnerAddress'); -await app.sendPayment('100000', '0xPartnerAddress'); // Send 0.1 USDC -``` - -## What's Next? - -Congratulations! You've built your first Yellow App. Here's what to explore next: - -- **[Advanced Topics](../../learn/introduction/architecture-at-a-glance)**: Learn about architecture, multi-party applications, and production deployment -- **[API Reference](../../api-reference)**: Explore all available SDK methods and options - -## Need Help? - -- **Documentation**: Continue reading the guides for in-depth explanations -- **Community**: Join our developer community for support -- **Examples**: Check out our GitHub repository for sample applications - -You're now ready to build fast, scalable apps with Yellow SDK! \ No newline at end of file diff --git a/docs/nitrolite/learn/index.mdx b/docs/nitrolite/learn/index.mdx index 0de7b5f..651d85f 100644 --- a/docs/nitrolite/learn/index.mdx +++ b/docs/nitrolite/learn/index.mdx @@ -56,7 +56,7 @@ Detailed v1 protocol flow documentation for deposits, withdrawals, transfers, an After completing the Learn section, continue to: -- **[Build](/nitrolite/build/quick-start)** — Implement complete Yellow Applications +- **[Build](/nitrolite/build/getting-started/quickstart)** — Implement complete Yellow Applications - **[Protocol](/nitrolite/protocol/introduction)** — Authoritative protocol specification --- diff --git a/docs/nitrolite/learn/introduction/supported-chains.mdx b/docs/nitrolite/learn/introduction/supported-chains.mdx index 877cee7..198f933 100644 --- a/docs/nitrolite/learn/introduction/supported-chains.mdx +++ b/docs/nitrolite/learn/introduction/supported-chains.mdx @@ -293,6 +293,6 @@ const productionAllocations = [ ## See Also -- [Quick Start Guide](/nitrolite/build/quick-start) — Get started building with Yellow SDK +- [Quick Start Guide](/nitrolite/build/getting-started/quickstart) — Get started building with Yellow SDK - [Multi-Party App Sessions](/nitrolite/build/sdk/multi-party-app-sessions) — Create multi-party application sessions - [API Reference](/nitrolite/api-reference) — Complete SDK documentation diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 18f3161..3fec076 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -155,8 +155,8 @@ const config: Config = { customProps: { showOn: 'nitrolite' }, }, { - type: 'doc', - docId: 'build/quick-start/index', + to: '/nitrolite/build/getting-started/quickstart', + activeBasePath: '/nitrolite/build', label: 'Build', position: 'left', customProps: { showOn: 'nitrolite' }, @@ -231,7 +231,7 @@ const config: Config = { }, { label: 'Build', - to: '/nitrolite/build/quick-start', + to: '/nitrolite/build/getting-started/quickstart', }, { label: 'Protocol', diff --git a/src/pages/nitrolite.tsx b/src/pages/nitrolite.tsx index 37e7896..b1fc208 100644 --- a/src/pages/nitrolite.tsx +++ b/src/pages/nitrolite.tsx @@ -42,7 +42,7 @@ const FeatureList: FeatureItem[] = [ trading capabilities and instant cross-chain settlements. ), - link: '/nitrolite/build/quick-start', + link: '/nitrolite/build/getting-started/quickstart', }, { title: 'Join the Community', From bd7a17d9dddfa15d334b172be304cdf861ff572b Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Thu, 7 May 2026 13:40:52 +0530 Subject: [PATCH 2/2] YNU-864: Add native v1 lifecycle quickstart --- .../build/getting-started/prerequisites.mdx | 34 +- .../build/getting-started/quickstart.mdx | 282 ++++----- examples/nitrolite-v1-lifecycle/.env.example | 19 + examples/nitrolite-v1-lifecycle/.gitignore | 6 + examples/nitrolite-v1-lifecycle/README.md | 25 + .../nitrolite-v1-lifecycle/package-lock.json | 539 +++++++++++++++++ examples/nitrolite-v1-lifecycle/package.json | 25 + examples/nitrolite-v1-lifecycle/src/index.ts | 544 ++++++++++++++++++ examples/nitrolite-v1-lifecycle/tsconfig.json | 15 + 9 files changed, 1336 insertions(+), 153 deletions(-) create mode 100644 examples/nitrolite-v1-lifecycle/.env.example create mode 100644 examples/nitrolite-v1-lifecycle/.gitignore create mode 100644 examples/nitrolite-v1-lifecycle/README.md create mode 100644 examples/nitrolite-v1-lifecycle/package-lock.json create mode 100644 examples/nitrolite-v1-lifecycle/package.json create mode 100644 examples/nitrolite-v1-lifecycle/src/index.ts create mode 100644 examples/nitrolite-v1-lifecycle/tsconfig.json diff --git a/docs/nitrolite/build/getting-started/prerequisites.mdx b/docs/nitrolite/build/getting-started/prerequisites.mdx index 2418c02..5108b34 100644 --- a/docs/nitrolite/build/getting-started/prerequisites.mdx +++ b/docs/nitrolite/build/getting-started/prerequisites.mdx @@ -10,7 +10,7 @@ keywords: [prerequisites, setup, development, environment, Node.js, viem] Use this checklist to prepare a local TypeScript project for the v1 Nitrolite SDK. :::tip Choosing your SDK -New v1 app: use [`@yellow-org/sdk`](../sdk/typescript/getting-started). Migrating from 0.5.3: start with [`@yellow-org/sdk-compat`](../sdk/typescript-compat/overview) and the codemod, then move native flows over when you are ready. See the [SDK overview](../sdk/) for the full decision tree. +New v1 app: use [`@yellow-org/sdk`](../sdk/typescript/getting-started). Migrating from 0.5.3: read the [migration overview](../sdk/typescript-compat/overview) before moving native flows over. See the [SDK overview](../sdk/) for the full decision tree. ::: ## System Requirements @@ -119,14 +119,16 @@ Create `.env` for sensitive values: ```bash # .env - never commit this file -PRIVATE_KEY=0x... -RECIPIENT=0x... -RPC_URL=https://rpc-amoy.polygon.technology -NITRONODE_WS_URL= +USER_PRIVATE_KEY=0x... +APP_PRIVATE_KEY=0x... +NITRONODE_WS_URL=wss://nitronode-stress.yellow.org/v1/ws +RPC_URL=https://ethereum-sepolia-rpc.publicnode.com +CHAIN_ID=11155111 +ASSET=yellow ``` -:::info Sandbox URL - coming soon -`NITRONODE_WS_URL` intentionally uses a placeholder until the canonical public Nitronode sandbox endpoint is published. Keep the variable name now so the final URL swap is a one-line change. +:::info Test endpoint +`NITRONODE_WS_URL` should point at a v1 Nitronode endpoint for the environment you are testing against. The quickstart lifecycle example uses the current public stress endpoint and Sepolia test assets. ::: Add local files to `.gitignore`: @@ -151,7 +153,7 @@ const account = privateKeyToAccount(privateKey); console.log('New development wallet'); console.log('Address:', account.address); console.log('Private key:', privateKey); -console.log('Save this key in .env as PRIVATE_KEY.'); +console.log('Save this key in .env as USER_PRIVATE_KEY or APP_PRIVATE_KEY.'); ``` Run it: @@ -190,12 +192,12 @@ import 'dotenv/config'; import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; import { createPublicClient, http } from 'viem'; -const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; +const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY as `0x${string}`; const RPC_URL = process.env.RPC_URL; const NITRONODE_WS_URL = process.env.NITRONODE_WS_URL; -const CHAIN_ID = 80002n; +const CHAIN_ID = BigInt(process.env.CHAIN_ID || '11155111'); -if (!PRIVATE_KEY) throw new Error('PRIVATE_KEY not set in .env'); +if (!USER_PRIVATE_KEY) throw new Error('USER_PRIVATE_KEY not set in .env'); if (!RPC_URL) throw new Error('RPC_URL not set in .env'); const publicClient = createPublicClient({ @@ -203,13 +205,13 @@ const publicClient = createPublicClient({ }); const blockNumber = await publicClient.getBlockNumber(); -const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); +const { stateSigner, txSigner } = createSigners(USER_PRIVATE_KEY); console.log('Wallet loaded:', stateSigner.getAddress()); console.log('RPC connected, block:', blockNumber.toString()); -if (!NITRONODE_WS_URL || NITRONODE_WS_URL === '') { - console.log('Nitronode sandbox URL pending; wallet and RPC verified.'); +if (!NITRONODE_WS_URL) { + console.log('Nitronode URL not set; wallet and RPC verified.'); } else { const client = await Client.create( NITRONODE_WS_URL, @@ -233,7 +235,7 @@ npm run build npm start ``` -Until the sandbox URL is live, the script should stop after confirming the wallet and blockchain RPC. Once the URL is available, it also connects through `Client.create()` and prints `client.getUserAddress()`. +Without a Nitronode URL, the script stops after confirming the wallet and blockchain RPC. With a v1 Nitronode URL, it also connects through `Client.create()` and prints `client.getUserAddress()`. ## When to add client options @@ -243,6 +245,6 @@ Use `withApplicationID(appID)` when you want Nitronode records tagged with the a ## Next Steps -- [Quickstart](./quickstart) - make your first deposit and transfer. +- [Quickstart](./quickstart) - run the native v1 channel and app-session lifecycle. - [Key Terms & Mental Models](./key-terms) - learn the vocabulary used across the docs. - [TypeScript SDK](../sdk/typescript/getting-started) - read the full SDK guide. diff --git a/docs/nitrolite/build/getting-started/quickstart.mdx b/docs/nitrolite/build/getting-started/quickstart.mdx index a479681..358ad7c 100644 --- a/docs/nitrolite/build/getting-started/quickstart.mdx +++ b/docs/nitrolite/build/getting-started/quickstart.mdx @@ -1,206 +1,214 @@ --- title: Quickstart -description: Create a v1 Nitrolite client, fund a channel, and send a transfer with @yellow-org/sdk. +description: Run a native v1 TypeScript SDK channel and app-session lifecycle. sidebar_position: 1 displayed_sidebar: buildSidebar --- # Quickstart -Build a minimal Nitrolite script that connects to Nitronode, signs channel states with your wallet, deposits USDC into a home channel, sends an off-chain transfer, and closes the connection cleanly. +Run a complete native v1 Nitrolite flow with `@yellow-org/sdk`: connect to Nitronode, prepare a home channel, create an app session, deposit into it, operate, withdraw, and close the session. -:::info Sandbox URL - coming soon -The canonical public Nitronode sandbox WebSocket URL is still being pinned by the ops team. Use the literal placeholder shown below, then replace it in the WebSocket URL line only when the final hostname is published. -::: - -:::tip Migrating from 0.5.3? -Already on `@erc7824/nitrolite@0.5.3`? Skip ahead to [Migrating from 0.5.3](../sdk/typescript-compat/overview) - you'll probably want `@yellow-org/sdk-compat` plus the `@yellow-org/nitrolite-codemod`, not this guide. -::: +The runnable source lives in [`examples/nitrolite-v1-lifecycle`](https://github.com/layer-3/docs/tree/main/examples/nitrolite-v1-lifecycle). The snippets below explain the stages; the checked-in example is the source of truth. ## Prerequisites -Before you start, make sure you have: +You need: - Node.js 20 or later. -- A development wallet private key with testnet gas. -- A blockchain RPC endpoint for Polygon Amoy or your target chain. -- `decimal.js` for exact asset amounts. -- `viem` for wallet and address types used by the SDK. +- A disposable user wallet with Sepolia gas and the selected test asset. +- A separate app signer wallet. It can be unfunded for this example. +- A Sepolia RPC URL. +- A v1 Nitronode WebSocket URL. -See [Prerequisites & Environment](./prerequisites) for a full setup checklist. +See [Prerequisites & Environment](./prerequisites) for the full setup checklist. -## Step 1: Install +## Step 1: Install and configure -Create a new TypeScript project and install the v1 SDK: +Clone the docs repo, enter the tested example package, and install its isolated dependencies: ```bash -mkdir yellow-quickstart -cd yellow-quickstart -npm init -y -npm install @yellow-org/sdk decimal.js viem dotenv -npm install -D typescript @types/node +git clone https://github.com/layer-3/docs.git +cd docs/examples/nitrolite-v1-lifecycle +cp .env.example .env +npm install ``` -Set the project to ESM and add a build script: +Edit `.env`: -```json title="package.json" -{ - "type": "module", - "scripts": { - "build": "tsc" - } -} -``` +```bash title=".env" +USER_PRIVATE_KEY=0x... +APP_PRIVATE_KEY=0x... +NITRONODE_WS_URL=wss://nitronode-stress.yellow.org/v1/ws +RPC_URL=https://ethereum-sepolia-rpc.publicnode.com +CHAIN_ID=11155111 +ASSET=yellow -Add a minimal `tsconfig.json`: - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "outDir": "dist" - }, - "include": ["src/**/*"] -} +CHANNEL_DEPOSIT_AMOUNT=0.01 +APP_DEPOSIT_AMOUNT=0.005 +OPERATE_AMOUNT=0.001 +WITHDRAW_AMOUNT=0.002 ``` -## Step 2: Create signers - -`createSigners()` builds the two signer objects the client needs: one for off-chain channel states and one for blockchain transactions. +Use a dedicated test wallet. Do not reuse a wallet that holds mainnet funds. -```typescript title="src/index.ts" -import 'dotenv/config'; -import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; -import Decimal from 'decimal.js'; +## Step 2: Connect clients -const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`; -const RECIPIENT = process.env.RECIPIENT as `0x${string}`; -const RPC_URL = process.env.RPC_URL; -const CHAIN_ID = 80002n; // Polygon Amoy -const ASSET = 'usdc'; +The example creates separate SDK clients for the user and the app signer: -if (!PRIVATE_KEY) throw new Error('Set PRIVATE_KEY in .env'); -if (!RECIPIENT) throw new Error('Set RECIPIENT in .env'); -if (!RPC_URL) throw new Error('Set RPC_URL in .env'); +```typescript +const userSigners = createSigners(config.userPrivateKey); +const appSigners = createSigners(config.appPrivateKey); -const { stateSigner, txSigner } = createSigners(PRIVATE_KEY); - -console.log('Wallet address:', stateSigner.getAddress()); +const user = await Client.create( + config.wsURL, + userSigners.stateSigner, + userSigners.txSigner, + withBlockchainRPC(config.chainId, config.rpcURL), +); ``` -## Step 3: Connect +`createSigners()` derives both signer types the SDK needs: off-chain state signing and on-chain transaction signing. -Create the client with a Nitronode WebSocket URL, both signers, and a chain RPC endpoint for on-chain settlement. +## Step 3: Prepare the home channel -```typescript title="src/index.ts" -const NITRONODE_WS_URL = ''; // Replace with the canonical sandbox URL. +The script loads Nitronode config, checks the configured asset, sets the asset home blockchain, and prepares the user home channel. -const client = await Client.create( - NITRONODE_WS_URL, - stateSigner, - txSigner, - withBlockchainRPC(CHAIN_ID, RPC_URL), -); - -await client.setHomeBlockchain(ASSET, CHAIN_ID); -console.log('Connected as:', client.getUserAddress()); -``` +If the wallet already has a signed funded state, the script continues. Otherwise it runs: -## Step 4: Get test tokens +```typescript +await client.approveToken(chainId, asset, depositAmount); - +`deposit()` creates a co-signed channel state. `checkpoint()` submits that state to ChannelHub so the deposit is enforced on-chain. -## Step 5: Deposit +## Step 4: Create the app session -Deposit builds and co-signs the next channel state off-chain. `checkpoint()` submits that state to the ChannelHub contract so the deposit is enforced on-chain. +The app session has two participants: the user wallet and the app signer. Both sign the packed session definition. -```typescript title="src/index.ts" -await client.approveToken(CHAIN_ID, ASSET, new Decimal(5)); +```typescript +const definition: AppDefinitionV1 = { + applicationId: appID, + participants: [ + { walletAddress: userAddress, signatureWeight: 1 }, + { walletAddress: appAddress, signatureWeight: 1 }, + ], + quorum: 2, + nonce: BigInt(Date.now()) * 1_000_000n, +}; -const depositState = await client.deposit(CHAIN_ID, ASSET, new Decimal(5)); -console.log('Deposit state version:', depositState.version); +const payload = packCreateAppSessionRequestV1(definition, sessionData); +const userSig = await userSessionSigner.signMessage(payload); +const appSig = await appSessionSigner.signMessage(payload); -const depositTx = await client.checkpoint(ASSET); -console.log('Deposit checkpoint tx:', depositTx); +const created = await client.createAppSession(definition, sessionData, [userSig, appSig]); ``` -`approveToken()` is only needed before the first checkpoint for a token, or when you need to increase allowance. - -## Step 6: Transfer +## Step 5: Deposit into the app session -Transfers advance the signed off-chain state. They do not need a checkpoint unless you want to settle the latest state on-chain. +The user commits part of the home-channel balance into the app session with a deposit intent: -```typescript title="src/index.ts" -const transferState = await client.transfer( - RECIPIENT, - ASSET, - new Decimal(1), -); +```typescript +const update: AppStateUpdateV1 = { + appSessionId: created.appSessionId, + intent: AppStateUpdateIntent.Deposit, + version: 2n, + allocations: [ + { participant: userAddress, asset, amount: appDepositAmount }, + { participant: appAddress, asset, amount: new Decimal(0) }, + ], + sessionData: JSON.stringify({ intent: 'user_deposit' }), +}; -console.log('Transfer transition:', transferState.transition.type); +await client.submitAppSessionDeposit(update, [userSig, appSig], asset, appDepositAmount); ``` -## Step 7: Clean up +## Step 6: Operate, withdraw, and close -Always close the client when your script is done: +After the app session is funded, the remaining stages use `submitAppState()` with different intents: -```typescript title="src/index.ts" -await client.close(); +```typescript +await client.submitAppState(operateUpdate, operateSignatures); +await client.submitAppState(withdrawUpdate, withdrawSignatures); +await client.submitAppState(closeUpdate, closeSignatures); ``` -A complete script should wrap the channel operations in `try` / `finally` so the WebSocket closes even if a request fails. +- `Operate` moves a small purchase amount from the user allocation to the app signer allocation. +- `Withdraw` releases part of the user allocation back to the home channel. +- `Close` finalizes the app session and returns the remaining allocations. -```typescript title="src/index.ts" -try { - await client.approveToken(CHAIN_ID, ASSET, new Decimal(5)); +The example leaves the home channel open by default. Set `CLOSE_HOME_CHANNEL=true` only when you intentionally want to close the example channel after the app-session lifecycle. - const depositState = await client.deposit(CHAIN_ID, ASSET, new Decimal(5)); - console.log('Deposit state version:', depositState.version); +## Step 7: Run it - const depositTx = await client.checkpoint(ASSET); - console.log('Deposit checkpoint tx:', depositTx); - - const transferState = await client.transfer( - RECIPIENT, - ASSET, - new Decimal(1), - ); - console.log('Transfer transition:', transferState.transition.type); -} finally { - await client.close(); -} -``` - -Run it after the sandbox URL is available and your `.env` values are set: +Build and run the lifecycle: ```bash npm run build -node dist/index.js +npm run lifecycle +``` + +A successful run prints each stage. + +
+Sanitized sample output + +```text +== Connect clients == +user=0xUSER_WALLET +app=0xAPP_SIGNER + +== Inspect Nitronode config and set home blockchain == +node=0xNITRONODE_SIGNER version=13a90cd +chain=ethereum_sepolia id=11155111 +asset=yellow decimals=18 + +== Prepare user home channel == +signed state before prepare: version=2 transition=40 homeBalance=0.005 homeChannel=0xHOME_CHANNEL_ID +pending state before prepare: version=4 transition=41 homeBalance=0.009 homeChannel=0xHOME_CHANNEL_ID +acknowledging pending yellow channel state +ack checkpoint tx=0xCHECKPOINT_TX_HASH +home channel ready: version=5 transition=1 homeBalance=0.009 homeChannel=0xHOME_CHANNEL_ID + +== Run app-session lifecycle == +registering app=docs-v1-lifecycle-... owner=0xAPP_SIGNER +app registry is disabled on this node; continuing with app-session creation +created app session=0xAPP_SESSION_ID version=1 status=open +after create: version=1 closed=false user=0 app=0 +app-session deposit nodeSig=0xNODE_SIGNATURE +after app deposit: version=2 closed=false user=0.005 app=0 +after operate: version=3 closed=false user=0.004 app=0.001 +after withdraw: version=4 closed=false user=0.002 app=0.001 +after close: version=5 closed=true user=0 app=0 + +== Final queries == +final signed channel state: version=6 transition=40 homeBalance=0.004 homeChannel=0xHOME_CHANNEL_ID +final app session: version=5 closed=true user=0 app=0 + +== Optional cleanup == +home-channel close skipped; set CLOSE_HOME_CHANNEL=true to run cleanup + +Lifecycle complete. ``` +
+ ## What just happened -- `createSigners()` derived wallet-backed signers for off-chain states and on-chain transactions. -- `Client.create()` opened a v1 Nitronode WebSocket connection and attached a blockchain RPC endpoint for `checkpoint()`. -- `deposit()` prepared a mutually signed channel state, then `checkpoint()` enforced it on-chain through ChannelHub. -- `transfer()` advanced the off-chain channel state using `Decimal` amounts so asset precision stays exact. +- `Client.create()` opened v1 Nitronode WebSocket connections for both signers. +- `setHomeBlockchain()` selected the settlement chain for the asset. +- `approveToken()`, `deposit()`, and `checkpoint()` prepared and enforced the user's home channel. +- `packCreateAppSessionRequestV1()` and `packAppStateUpdateV1()` produced the exact hashes participants signed. +- `createAppSession()`, `submitAppSessionDeposit()`, and `submitAppState()` moved through the app-session lifecycle. ## Next steps - [Configuration](../sdk/typescript/configuration) - timeouts, RPC endpoints, application IDs, and error handling. -- [Multi-party app sessions](../sdk/multi-party-app-sessions) - commit channel funds into app sessions. -- [Errors and recovery](../sdk/typescript/configuration#error-handling) - handle connection and operation failures. -- [Migrating from 0.5.3](../sdk/typescript-compat/overview) - use the compat package and codemod before moving to native v1 APIs. +- [Multi-party app sessions](../sdk/multi-party-app-sessions) - expand the same lifecycle to more participants. +- [Key Terms & Mental Models](./key-terms) - learn the vocabulary used across the docs. diff --git a/examples/nitrolite-v1-lifecycle/.env.example b/examples/nitrolite-v1-lifecycle/.env.example new file mode 100644 index 0000000..8054039 --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/.env.example @@ -0,0 +1,19 @@ +# Disposable test wallets only. Do not reuse production keys. +USER_PRIVATE_KEY=0x... +APP_PRIVATE_KEY=0x... + +# Native v1 Nitronode and matching chain RPC. +NITRONODE_WS_URL=wss://nitronode-stress.yellow.org/v1/ws +RPC_URL=https://ethereum-sepolia-rpc.publicnode.com +CHAIN_ID=11155111 +ASSET=yellow + +# Keep these tiny for docs verification. Adjust for the faucet asset and chain. +CHANNEL_DEPOSIT_AMOUNT=0.01 +APP_DEPOSIT_AMOUNT=0.005 +OPERATE_AMOUNT=0.001 +WITHDRAW_AMOUNT=0.002 + +# Optional: uncomment to checkpoint and close the user's home channel after the +# app-session lifecycle. This is destructive to the example channel balance. +# CLOSE_HOME_CHANNEL=true diff --git a/examples/nitrolite-v1-lifecycle/.gitignore b/examples/nitrolite-v1-lifecycle/.gitignore new file mode 100644 index 0000000..ee62cd4 --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/.gitignore @@ -0,0 +1,6 @@ +.env +.env.* +!.env.example +dist/ +node_modules/ +npm-debug.log* diff --git a/examples/nitrolite-v1-lifecycle/README.md b/examples/nitrolite-v1-lifecycle/README.md new file mode 100644 index 0000000..f7ba739 --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/README.md @@ -0,0 +1,25 @@ +# Nitrolite v1 Lifecycle Example + +Runnable TypeScript example for the native `@yellow-org/sdk` channel and app-session lifecycle. + +The script: + +1. Connects a user wallet and a separate app signer. +2. Checks Nitronode config and asset support. +3. Prepares the user's home channel with `approveToken`, `deposit`, and `checkpoint` when needed. +4. Creates a two-party app session. +5. Deposits channel funds into the app session. +6. Runs `operate`, `withdraw`, and `close` app-session updates. + +## Setup + +```bash +cp .env.example .env +npm install +npm run build +npm run lifecycle +``` + +Use disposable test wallets only. The app signer can be an unfunded test wallet; the user wallet needs Sepolia gas and the selected test asset. + +Set `CLOSE_HOME_CHANNEL=true` only when you intentionally want to close the example home channel after the app-session lifecycle. diff --git a/examples/nitrolite-v1-lifecycle/package-lock.json b/examples/nitrolite-v1-lifecycle/package-lock.json new file mode 100644 index 0000000..42ea228 --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/package-lock.json @@ -0,0 +1,539 @@ +{ + "name": "@yellow-org/docs-nitrolite-v1-lifecycle", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@yellow-org/docs-nitrolite-v1-lifecycle", + "version": "1.0.0", + "dependencies": { + "@yellow-org/sdk": "1.2.1", + "decimal.js": "^10.6.0", + "dotenv": "^17.2.3", + "viem": "2.46.1" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "typescript": "~6.0.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@yellow-org/sdk": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@yellow-org/sdk/-/sdk-1.2.1.tgz", + "integrity": "sha512-ZRV1OjHLkCP3BOKaSRqsqfw8LOKo5TK5XU7LOS8JEiv7kRu3xApjCSjdof8EWkoMdwMYlU0fvRxxD5glTkuWBA==", + "license": "MIT", + "dependencies": { + "abitype": "^1.2.3", + "decimal.js": "^10.4.3", + "jest-util": "^30.3.0", + "viem": "^2.46.1", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abitype": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", + "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/ox": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.1.tgz", + "integrity": "sha512-uU0llpthaaw4UJoXlseCyBHmQ3bLrQmz9rRLIAUHqv46uHuae9SE+ukYBRIPVCnlEnHKuWjDUcDFHWx9gbGNoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.1.tgz", + "integrity": "sha512-c5YPQR/VueqoPG09Tp1JBw2iItKVRGVI0YkWekquRDZw0ciNBhO3muu2QjO9xFelOXh18q3d/kLbW83B2Oxf0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.12.1", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/nitrolite-v1-lifecycle/package.json b/examples/nitrolite-v1-lifecycle/package.json new file mode 100644 index 0000000..74f32b7 --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/package.json @@ -0,0 +1,25 @@ +{ + "name": "@yellow-org/docs-nitrolite-v1-lifecycle", + "version": "1.0.0", + "private": true, + "description": "Runnable Nitrolite v1 TypeScript SDK channel and app-session lifecycle example.", + "type": "module", + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "lifecycle": "node dist/index.js", + "start": "npm run build && npm run lifecycle" + }, + "dependencies": { + "@yellow-org/sdk": "1.2.1", + "decimal.js": "^10.6.0", + "dotenv": "^17.2.3", + "viem": "2.46.1" + }, + "devDependencies": { + "@types/node": "^24.12.2", + "typescript": "~6.0.2" + } +} diff --git a/examples/nitrolite-v1-lifecycle/src/index.ts b/examples/nitrolite-v1-lifecycle/src/index.ts new file mode 100644 index 0000000..45a3ecb --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/src/index.ts @@ -0,0 +1,544 @@ +import 'dotenv/config'; + +import { Decimal } from 'decimal.js'; +import { isAddress, type Address, type Hex } from 'viem'; +import { + AppSessionWalletSignerV1, + AppStateUpdateIntent, + Client, + EthereumMsgSigner, + createSigners, + packAppStateUpdateV1, + packCreateAppSessionRequestV1, + withBlockchainRPC, + type AppDefinitionV1, + type AppSessionInfoV1, + type AppStateUpdateV1, + type State, +} from '@yellow-org/sdk'; + +type LifecycleConfig = { + userPrivateKey: Hex; + appPrivateKey: Hex; + wsURL: string; + rpcURL: string; + chainId: bigint; + asset: string; + channelDepositAmount: Decimal; + appDepositAmount: Decimal; + operateAmount: Decimal; + withdrawAmount: Decimal; + closeHomeChannel: boolean; +}; + +type Clients = { + user: Client; + app: Client; +}; + +type WalletClientWithWriteContract = { + account?: unknown; + writeContract?: (request: Record) => Promise; +}; + +type EVMClientFactoryResult = { + walletClient?: WalletClientWithWriteContract | null; +}; + +type ClientWithEVMFactory = { + createEVMClients?: (chainId: bigint, rpcURL: string) => EVMClientFactoryResult; +}; + +function requireEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +function optionalEnv(name: string, fallback: string): string { + const value = process.env[name]?.trim(); + return value || fallback; +} + +function privateKeyEnv(name: string): Hex { + const value = requireEnv(name); + if (!/^0x[0-9a-fA-F]{64}$/.test(value)) { + throw new Error(`${name} must be a 32-byte hex private key with 0x prefix`); + } + return value as Hex; +} + +function decimalEnv(name: string, fallback: string): Decimal { + const value = optionalEnv(name, fallback); + const amount = new Decimal(value); + if (!amount.isFinite() || amount.isNegative()) { + throw new Error(`${name} must be a non-negative decimal amount`); + } + return amount; +} + +function loadConfig(): LifecycleConfig { + const userPrivateKey = privateKeyEnv('USER_PRIVATE_KEY'); + const appPrivateKey = privateKeyEnv('APP_PRIVATE_KEY'); + + if (userPrivateKey.toLowerCase() === appPrivateKey.toLowerCase()) { + throw new Error('USER_PRIVATE_KEY and APP_PRIVATE_KEY must be different wallets'); + } + + const chainId = BigInt(requireEnv('CHAIN_ID')); + const asset = requireEnv('ASSET').toLowerCase(); + const appDepositAmount = decimalEnv('APP_DEPOSIT_AMOUNT', '0.005'); + const operateAmount = decimalEnv('OPERATE_AMOUNT', '0.001'); + const withdrawAmount = decimalEnv('WITHDRAW_AMOUNT', '0.002'); + + if (!appDepositAmount.isPositive()) { + throw new Error('APP_DEPOSIT_AMOUNT must be greater than zero'); + } + if (operateAmount.plus(withdrawAmount).greaterThan(appDepositAmount)) { + throw new Error('OPERATE_AMOUNT + WITHDRAW_AMOUNT must be <= APP_DEPOSIT_AMOUNT'); + } + + return { + userPrivateKey, + appPrivateKey, + wsURL: requireEnv('NITRONODE_WS_URL'), + rpcURL: requireEnv('RPC_URL'), + chainId, + asset, + channelDepositAmount: decimalEnv('CHANNEL_DEPOSIT_AMOUNT', '0.01'), + appDepositAmount, + operateAmount, + withdrawAmount, + closeHomeChannel: optionalEnv('CLOSE_HOME_CHANNEL', 'false').toLowerCase() === 'true', + }; +} + +function stage(title: string): void { + console.log(`\n== ${title} ==`); +} + +function logState(label: string, state: State): void { + console.log( + `${label}: version=${state.version.toString()} transition=${state.transition.type} ` + + `homeBalance=${state.homeLedger.userBalance.toFixed()} homeChannel=${state.homeChannelId ?? 'none'}` + ); +} + +function logSession(label: string, session: AppSessionInfoV1, userAddress: Address, appAddress: Address, asset: string): void { + const userAllocation = allocationFor(session, userAddress, asset); + const appAllocation = allocationFor(session, appAddress, asset); + console.log( + `${label}: version=${session.version.toString()} closed=${session.isClosed} ` + + `user=${userAllocation.toFixed()} app=${appAllocation.toFixed()}` + ); +} + +function maxDecimal(left: Decimal, right: Decimal): Decimal { + return left.greaterThan(right) ? left : right; +} + +function isAllowanceError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return /allowance|approval|approve|insufficient.*allow/i.test(message); +} + +async function getLatestStateOrNull(client: Client, wallet: Address, asset: string, onlySigned: boolean): Promise { + try { + return await client.getLatestState(wallet, asset, onlySigned); + } catch { + return null; + } +} + +async function checkpointWithApproval(client: Client, config: LifecycleConfig): Promise { + try { + return await client.checkpoint(config.asset); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.toLowerCase().includes('does not require a blockchain operation')) { + return null; + } + if (!isAllowanceError(error)) { + throw error; + } + + console.log(`checkpoint needs token approval; approving ${config.channelDepositAmount.toFixed()} ${config.asset}`); + await client.approveToken(config.chainId, config.asset, config.channelDepositAmount); + return await client.checkpoint(config.asset); + } +} + +async function connectClients(config: LifecycleConfig): Promise { + const userSigners = createSigners(config.userPrivateKey); + const appSigners = createSigners(config.appPrivateKey); + + const user = await Client.create( + config.wsURL, + userSigners.stateSigner, + userSigners.txSigner, + withBlockchainRPC(config.chainId, config.rpcURL) + ); + + const app = await Client.create( + config.wsURL, + appSigners.stateSigner, + appSigners.txSigner, + withBlockchainRPC(config.chainId, config.rpcURL) + ); + + enableNodeLocalAccountTransactions(user); + enableNodeLocalAccountTransactions(app); + + return { user, app }; +} + +function enableNodeLocalAccountTransactions(client: Client): void { + const sdkClient = client as unknown as ClientWithEVMFactory; + const originalFactory = sdkClient.createEVMClients?.bind(client); + if (!originalFactory) { + return; + } + + sdkClient.createEVMClients = (chainId: bigint, rpcURL: string): EVMClientFactoryResult => { + const result = originalFactory(chainId, rpcURL); + const walletClient = result.walletClient; + if (!walletClient?.account || !walletClient.writeContract) { + return result; + } + + // Node.js public RPCs need the LocalAccount object when viem sends writes. + // Keep using SDK methods, but preserve the local account after simulation. + const localAccount = walletClient.account; + const originalWriteContract = walletClient.writeContract.bind(walletClient); + walletClient.writeContract = (request: Record): Promise => { + return originalWriteContract({ ...request, account: localAccount }); + }; + + return result; + }; +} + +async function inspectNode(client: Client, config: LifecycleConfig): Promise { + const nodeConfig = await client.getConfig(); + console.log(`node=${nodeConfig.nodeAddress} version=${nodeConfig.nodeVersion}`); + + const blockchains = await client.getBlockchains(); + const selectedChain = blockchains.find((chain) => chain.id === config.chainId); + if (!selectedChain) { + throw new Error(`Nitronode does not list configured CHAIN_ID ${config.chainId.toString()}`); + } + console.log(`chain=${selectedChain.name} id=${selectedChain.id.toString()}`); + + const assets = await client.getAssets(config.chainId); + const selectedAsset = assets.find((asset) => asset.symbol.toLowerCase() === config.asset); + if (!selectedAsset) { + throw new Error(`Nitronode does not list asset ${config.asset} on chain ${config.chainId.toString()}`); + } + console.log(`asset=${selectedAsset.symbol} decimals=${selectedAsset.decimals}`); +} + +async function setHomeBlockchain(client: Client, asset: string, chainId: bigint): Promise { + try { + await client.setHomeBlockchain(asset, chainId); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes('home blockchain is already set')) { + throw error; + } + } +} + +async function prepareHomeChannel(client: Client, config: LifecycleConfig): Promise { + const wallet = client.getUserAddress(); + await setHomeBlockchain(client, config.asset, config.chainId); + + const signedBefore = await getLatestStateOrNull(client, wallet, config.asset, true); + const latestBefore = await getLatestStateOrNull(client, wallet, config.asset, false); + + if (signedBefore) { + logState('signed state before prepare', signedBefore); + } + if (latestBefore && (!signedBefore || latestBefore.id !== signedBefore.id || latestBefore.version !== signedBefore.version)) { + logState('pending state before prepare', latestBefore); + } + + if (latestBefore && latestBefore.homeLedger.userBalance.isPositive() && !latestBefore.userSig) { + console.log(`acknowledging pending ${config.asset} channel state`); + try { + await client.acknowledge(config.asset); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.toLowerCase().includes('already acknowledged')) { + throw error; + } + } + const txHash = await checkpointWithApproval(client, config); + console.log(txHash ? `ack checkpoint tx=${txHash}` : 'acknowledged without on-chain checkpoint'); + } + + const signedAfterAck = await getLatestStateOrNull(client, wallet, config.asset, true); + const existingBalance = signedAfterAck?.homeLedger.userBalance ?? new Decimal(0); + if (signedAfterAck && existingBalance.greaterThanOrEqualTo(config.appDepositAmount)) { + logState('home channel ready', signedAfterAck); + return signedAfterAck; + } + + const deficit = config.appDepositAmount.minus(existingBalance); + const depositAmount = maxDecimal(config.channelDepositAmount, deficit); + const onChainBalance = await client.getOnChainBalance(config.chainId, config.asset, wallet); + console.log(`on-chain ${config.asset} balance=${onChainBalance.toFixed()}`); + + if (onChainBalance.lessThan(depositAmount)) { + throw new Error( + `Wallet ${wallet} needs at least ${depositAmount.toFixed()} ${config.asset} on chain ${config.chainId.toString()}` + ); + } + + console.log(`approving ${depositAmount.toFixed()} ${config.asset}`); + await client.approveToken(config.chainId, config.asset, depositAmount); + + console.log(`depositing ${depositAmount.toFixed()} ${config.asset} into the home channel`); + const depositState = await client.deposit(config.chainId, config.asset, depositAmount); + logState('deposit state', depositState); + + const txHash = await checkpointWithApproval(client, config); + console.log(txHash ? `deposit checkpoint tx=${txHash}` : 'deposit did not require an on-chain checkpoint'); + + const ready = await getLatestStateOrNull(client, wallet, config.asset, true); + if (!ready || ready.homeLedger.userBalance.lessThan(config.appDepositAmount)) { + throw new Error(`Home channel did not reach ${config.appDepositAmount.toFixed()} ${config.asset}`); + } + + logState('home channel ready', ready); + return ready; +} + +function makeAppID(): string { + const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + return `docs-v1-lifecycle-${suffix}`; +} + +function appSessionSigner(privateKey: Hex): AppSessionWalletSignerV1 { + return new AppSessionWalletSignerV1(new EthereumMsgSigner(privateKey)); +} + +async function getSession(client: Client, appSessionId: string): Promise { + const { sessions } = await client.getAppSessions({ appSessionId }); + const session = sessions[0]; + if (!session) { + throw new Error(`App session ${appSessionId} was not returned by Nitronode`); + } + return session; +} + +function allocationFor(session: AppSessionInfoV1, participant: Address, asset: string): Decimal { + const allocation = session.allocations.find( + (entry) => entry.participant.toLowerCase() === participant.toLowerCase() && entry.asset.toLowerCase() === asset + ); + return allocation?.amount ?? new Decimal(0); +} + +function nextVersion(session: AppSessionInfoV1): bigint { + return session.version + 1n; +} + +async function signUpdate( + update: AppStateUpdateV1, + userSessionSigner: AppSessionWalletSignerV1, + appSessionSignerValue: AppSessionWalletSignerV1 +): Promise<[Hex, Hex]> { + const payload = packAppStateUpdateV1(update); + const userSig = await userSessionSigner.signMessage(payload); + const appSig = await appSessionSignerValue.signMessage(payload); + return [userSig, appSig]; +} + +async function runAppSessionLifecycle(clients: Clients, config: LifecycleConfig): Promise { + const userAddress = clients.user.getUserAddress(); + const appAddress = clients.app.getUserAddress(); + const userSessionSigner = appSessionSigner(config.userPrivateKey); + const appSessionSignerValue = appSessionSigner(config.appPrivateKey); + + if (!isAddress(userAddress) || !isAddress(appAddress)) { + throw new Error('SDK returned an invalid signer address'); + } + + const appID = makeAppID(); + console.log(`registering app=${appID} owner=${appAddress}`); + try { + await clients.app.registerApp(appID, JSON.stringify({ name: 'Docs v1 lifecycle example' }), true); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!message.includes('apps.v1 group is disabled')) { + throw error; + } + console.log('app registry is disabled on this node; continuing with app-session creation'); + } + + const sessionData = JSON.stringify({ intent: 'init', source: 'yellow-docs' }); + const definition: AppDefinitionV1 = { + applicationId: appID, + participants: [ + { walletAddress: userAddress, signatureWeight: 1 }, + { walletAddress: appAddress, signatureWeight: 1 }, + ], + quorum: 2, + nonce: BigInt(Date.now()) * 1_000_000n + BigInt(Math.floor(Math.random() * 1_000_000)), + }; + + const createPayload = packCreateAppSessionRequestV1(definition, sessionData); + const createUserSig = await userSessionSigner.signMessage(createPayload); + const createAppSig = await appSessionSignerValue.signMessage(createPayload); + const created = await clients.user.createAppSession(definition, sessionData, [createUserSig, createAppSig]); + console.log(`created app session=${created.appSessionId} version=${created.version} status=${created.status}`); + + let session = await getSession(clients.user, created.appSessionId); + logSession('after create', session, userAddress, appAddress, config.asset); + + const depositUpdate: AppStateUpdateV1 = { + appSessionId: created.appSessionId, + intent: AppStateUpdateIntent.Deposit, + version: nextVersion(session), + allocations: [ + { participant: userAddress, asset: config.asset, amount: config.appDepositAmount }, + { participant: appAddress, asset: config.asset, amount: new Decimal(0) }, + ], + sessionData: JSON.stringify({ intent: 'user_deposit', amount: config.appDepositAmount.toFixed() }), + }; + const depositSigs = await signUpdate(depositUpdate, userSessionSigner, appSessionSignerValue); + const nodeSig = await clients.user.submitAppSessionDeposit( + depositUpdate, + depositSigs, + config.asset, + config.appDepositAmount + ); + console.log(`app-session deposit nodeSig=${nodeSig}`); + + session = await getSession(clients.user, created.appSessionId); + logSession('after app deposit', session, userAddress, appAddress, config.asset); + + const userAfterDeposit = allocationFor(session, userAddress, config.asset); + const appAfterDeposit = allocationFor(session, appAddress, config.asset); + if (userAfterDeposit.lessThan(config.operateAmount)) { + throw new Error('OPERATE_AMOUNT exceeds the user app-session allocation'); + } + + const operateUpdate: AppStateUpdateV1 = { + appSessionId: created.appSessionId, + intent: AppStateUpdateIntent.Operate, + version: nextVersion(session), + allocations: [ + { participant: userAddress, asset: config.asset, amount: userAfterDeposit.minus(config.operateAmount) }, + { participant: appAddress, asset: config.asset, amount: appAfterDeposit.plus(config.operateAmount) }, + ], + sessionData: JSON.stringify({ intent: 'purchase', amount: config.operateAmount.toFixed() }), + }; + const operateSigs = await signUpdate(operateUpdate, userSessionSigner, appSessionSignerValue); + await clients.user.submitAppState(operateUpdate, operateSigs); + + session = await getSession(clients.user, created.appSessionId); + logSession('after operate', session, userAddress, appAddress, config.asset); + + const userAfterOperate = allocationFor(session, userAddress, config.asset); + const appAfterOperate = allocationFor(session, appAddress, config.asset); + if (userAfterOperate.lessThan(config.withdrawAmount)) { + throw new Error('WITHDRAW_AMOUNT exceeds the user app-session allocation'); + } + + const withdrawUpdate: AppStateUpdateV1 = { + appSessionId: created.appSessionId, + intent: AppStateUpdateIntent.Withdraw, + version: nextVersion(session), + allocations: [ + { participant: userAddress, asset: config.asset, amount: userAfterOperate.minus(config.withdrawAmount) }, + { participant: appAddress, asset: config.asset, amount: appAfterOperate }, + ], + sessionData: JSON.stringify({ intent: 'user_withdraw', amount: config.withdrawAmount.toFixed() }), + }; + const withdrawSigs = await signUpdate(withdrawUpdate, userSessionSigner, appSessionSignerValue); + await clients.user.submitAppState(withdrawUpdate, withdrawSigs); + + session = await getSession(clients.user, created.appSessionId); + logSession('after withdraw', session, userAddress, appAddress, config.asset); + + const closeUpdate: AppStateUpdateV1 = { + appSessionId: created.appSessionId, + intent: AppStateUpdateIntent.Close, + version: nextVersion(session), + allocations: [ + { participant: userAddress, asset: config.asset, amount: allocationFor(session, userAddress, config.asset) }, + { participant: appAddress, asset: config.asset, amount: allocationFor(session, appAddress, config.asset) }, + ], + sessionData: JSON.stringify({ intent: 'close' }), + }; + const closeSigs = await signUpdate(closeUpdate, userSessionSigner, appSessionSignerValue); + await clients.user.submitAppState(closeUpdate, closeSigs); + + session = await getSession(clients.user, created.appSessionId); + logSession('after close', session, userAddress, appAddress, config.asset); + + return created.appSessionId; +} + +async function maybeCloseHomeChannel(client: Client, config: LifecycleConfig): Promise { + if (!config.closeHomeChannel) { + console.log('home-channel close skipped; set CLOSE_HOME_CHANNEL=true to run cleanup'); + return; + } + + const closeState = await client.closeHomeChannel(config.asset); + logState('home-channel close state', closeState); + const txHash = await checkpointWithApproval(client, config); + console.log(txHash ? `home-channel close checkpoint tx=${txHash}` : 'home-channel close did not require checkpoint'); +} + +async function main(): Promise { + const config = loadConfig(); + let clients: Clients | null = null; + + try { + stage('Connect clients'); + clients = await connectClients(config); + console.log(`user=${clients.user.getUserAddress()}`); + console.log(`app=${clients.app.getUserAddress()}`); + + stage('Inspect Nitronode config and set home blockchain'); + await inspectNode(clients.user, config); + await setHomeBlockchain(clients.app, config.asset, config.chainId); + + stage('Prepare user home channel'); + await prepareHomeChannel(clients.user, config); + + stage('Run app-session lifecycle'); + const appSessionId = await runAppSessionLifecycle(clients, config); + + stage('Final queries'); + const finalState = await getLatestStateOrNull(clients.user, clients.user.getUserAddress(), config.asset, true); + if (finalState) { + logState('final signed channel state', finalState); + } + const finalSession = await getSession(clients.user, appSessionId); + logSession('final app session', finalSession, clients.user.getUserAddress(), clients.app.getUserAddress(), config.asset); + + stage('Optional cleanup'); + await maybeCloseHomeChannel(clients.user, config); + + console.log('\nLifecycle complete.'); + } finally { + if (clients) { + await Promise.allSettled([clients.user.close(), clients.app.close()]); + } + } +} + +main() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error('\nLifecycle failed.'); + console.error(error instanceof Error ? error.message : error); + process.exit(1); + }); diff --git a/examples/nitrolite-v1-lifecycle/tsconfig.json b/examples/nitrolite-v1-lifecycle/tsconfig.json new file mode 100644 index 0000000..deb0ef6 --- /dev/null +++ b/examples/nitrolite-v1-lifecycle/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist", + "types": ["node"] + }, + "include": ["src/**/*.ts"] +}