From c0557fef29bf3471c57839ba06d89d2bc7195520 Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Fri, 6 Mar 2026 19:42:47 +0530 Subject: [PATCH 1/4] feat: set up v1.x docs version with SDK documentation and protocol flows - Freeze 0.5.x docs into versioned_docs/version-0.5.x/ - Remove contracts/ and protocol/decentralized-layer/ from 0.5.x (v1-only content) - Clean contractsSidebar from frozen 0.5.x sidebar - Bump package.json to 1.x, update docusaurus.config.ts version entries - Fix i18n version label override (was stuck on 0.5.x) SDK Documentation (15 new pages under build/sdk/): - @yellow-org/sdk: getting-started, api-reference, configuration, examples - @yellow-org/sdk-compat: overview, migration-overview, migration-onchain, migration-offchain - Go SDK: getting-started, api-reference - SDK overview with architecture diagram Protocol Flows (9 new pages under learn/protocol-flows/): - Architecture (Petal Diagram), Transfer Flow, App Session Deposit - Home Channel: creation, deposit, withdrawal, withdraw-on-create - Escrow: deposit, withdrawal (with cross-chain coming-soon notes) Other: - Move Yellow Token content to learn/core-concepts/yellow-token.mdx - Remove Contracts from navbar/footer (sidebar-only in v1.x) - Add protocol-flows category and yellow-token to learnSidebar Made-with: Cursor --- docs/build/sdk/_category_.json | 6 + docs/build/sdk/go/_category_.json | 6 + docs/build/sdk/go/api-reference.mdx | 165 +++ docs/build/sdk/go/getting-started.mdx | 156 +++ docs/build/sdk/index.md | 49 + .../sdk/typescript-compat/_category_.json | 6 + .../typescript-compat/migration-offchain.mdx | 108 ++ .../typescript-compat/migration-onchain.mdx | 80 ++ .../typescript-compat/migration-overview.mdx | 69 ++ docs/build/sdk/typescript-compat/overview.mdx | 235 ++++ docs/build/sdk/typescript/_category_.json | 6 + docs/build/sdk/typescript/api-reference.mdx | 317 +++++ docs/build/sdk/typescript/configuration.mdx | 130 ++ docs/build/sdk/typescript/examples.mdx | 377 ++++++ docs/build/sdk/typescript/getting-started.mdx | 184 +++ docs/learn/core-concepts/yellow-token.mdx | 71 ++ docs/learn/index.mdx | 19 + docs/learn/protocol-flows/_category_.json | 6 + .../protocol-flows/app-session-deposit.mdx | 315 +++++ docs/learn/protocol-flows/architecture.mdx | 105 ++ docs/learn/protocol-flows/escrow-deposit.mdx | 542 +++++++++ .../protocol-flows/escrow-withdrawal.mdx | 517 ++++++++ .../protocol-flows/home-channel-creation.mdx | 465 +++++++ .../protocol-flows/home-channel-deposit.mdx | 408 +++++++ .../home-channel-withdraw-on-create.mdx | 433 +++++++ .../home-channel-withdrawal.mdx | 415 +++++++ docs/learn/protocol-flows/transfer-flow.mdx | 424 +++++++ docusaurus.config.ts | 19 +- .../current.json | 2 +- package-lock.json | 37 +- package.json | 4 +- sidebars.ts | 10 + static/img/protocol-petal-diagram.png | Bin 0 -> 250045 bytes .../api-reference/_category_.json | 8 + .../api-reference/app-sessions.md | 154 +++ .../version-0.5.x/api-reference/index.md | 263 ++++ .../version-0.5.x/build/_category_.json | 10 + .../version-0.5.x/build/quick-start/index.md | 359 ++++++ versioned_docs/version-0.5.x/guides/index.md | 13 + .../version-0.5.x/guides/migration-guide.md | 955 +++++++++++++++ .../guides/multi-party-app-sessions.mdx | 591 +++++++++ .../version-0.5.x/learn/_category_.json | 8 + .../learn/advanced/_category_.json | 6 + .../learn/advanced/managing-session-keys.mdx | 193 +++ .../learn/core-concepts/_category_.json | 8 + .../learn/core-concepts/app-sessions.mdx | 180 +++ .../core-concepts/challenge-response.mdx | 155 +++ .../learn/core-concepts/message-envelope.mdx | 143 +++ .../learn/core-concepts/session-keys.mdx | 177 +++ .../core-concepts/state-channels-vs-l1-l2.mdx | 141 +++ .../learn/getting-started/_category_.json | 8 + .../learn/getting-started/key-terms.mdx | 339 ++++++ .../learn/getting-started/prerequisites.mdx | 379 ++++++ .../learn/getting-started/quickstart.mdx | 1077 +++++++++++++++++ versioned_docs/version-0.5.x/learn/index.mdx | 78 ++ .../learn/introduction/_category_.json | 6 + .../introduction/architecture-at-a-glance.mdx | 257 ++++ .../learn/introduction/supported-chains.mdx | 301 +++++ .../learn/introduction/what-yellow-solves.mdx | 145 +++ .../version-0.5.x/manuals/_category_.json | 8 + versioned_docs/version-0.5.x/manuals/index.md | 13 + .../manuals/request-asset-support.md | 169 +++ .../manuals/request-blockchain-support.md | 166 +++ .../manuals/running-clearnode-locally.md | 222 ++++ .../version-0.5.x/protocol/_category_.json | 0 .../protocol/app-layer/_category_.json | 6 + .../app-layer/off-chain/_category_.json | 6 + .../app-layer/off-chain/app-sessions.mdx | 761 ++++++++++++ .../app-layer/off-chain/authentication.mdx | 467 +++++++ .../app-layer/off-chain/channel-methods.mdx | 594 +++++++++ .../app-layer/off-chain/message-format.mdx | 406 +++++++ .../protocol/app-layer/off-chain/overview.mdx | 242 ++++ .../protocol/app-layer/off-chain/queries.mdx | 877 ++++++++++++++ .../app-layer/off-chain/transfers.mdx | 377 ++++++ .../app-layer/on-chain/_category_.json | 6 + .../app-layer/on-chain/channel-lifecycle.mdx | 425 +++++++ .../app-layer/on-chain/data-structures.mdx | 212 ++++ .../protocol/app-layer/on-chain/overview.mdx | 50 + .../protocol/app-layer/on-chain/security.mdx | 331 +++++ .../app-layer/on-chain/signature-formats.mdx | 61 + .../version-0.5.x/protocol/architecture.mdx | 233 ++++ .../protocol/communication-flows.mdx | 753 ++++++++++++ .../version-0.5.x/protocol/glossary.mdx | 415 +++++++ .../protocol/implementation-checklist.mdx | 566 +++++++++ .../version-0.5.x/protocol/introduction.mdx | 81 ++ .../protocol/protocol-reference.mdx | 343 ++++++ .../version-0.5.x/protocol/terminology.mdx | 79 ++ .../version-0.5.x/tutorials/_category_.json | 8 + .../version-0.5.x/tutorials/index.md | 13 + .../version-0.5.x-sidebars.json | 90 ++ versions.json | 3 + 91 files changed, 19574 insertions(+), 49 deletions(-) create mode 100644 docs/build/sdk/_category_.json create mode 100644 docs/build/sdk/go/_category_.json create mode 100644 docs/build/sdk/go/api-reference.mdx create mode 100644 docs/build/sdk/go/getting-started.mdx create mode 100644 docs/build/sdk/index.md create mode 100644 docs/build/sdk/typescript-compat/_category_.json create mode 100644 docs/build/sdk/typescript-compat/migration-offchain.mdx create mode 100644 docs/build/sdk/typescript-compat/migration-onchain.mdx create mode 100644 docs/build/sdk/typescript-compat/migration-overview.mdx create mode 100644 docs/build/sdk/typescript-compat/overview.mdx create mode 100644 docs/build/sdk/typescript/_category_.json create mode 100644 docs/build/sdk/typescript/api-reference.mdx create mode 100644 docs/build/sdk/typescript/configuration.mdx create mode 100644 docs/build/sdk/typescript/examples.mdx create mode 100644 docs/build/sdk/typescript/getting-started.mdx create mode 100644 docs/learn/core-concepts/yellow-token.mdx create mode 100644 docs/learn/protocol-flows/_category_.json create mode 100644 docs/learn/protocol-flows/app-session-deposit.mdx create mode 100644 docs/learn/protocol-flows/architecture.mdx create mode 100644 docs/learn/protocol-flows/escrow-deposit.mdx create mode 100644 docs/learn/protocol-flows/escrow-withdrawal.mdx create mode 100644 docs/learn/protocol-flows/home-channel-creation.mdx create mode 100644 docs/learn/protocol-flows/home-channel-deposit.mdx create mode 100644 docs/learn/protocol-flows/home-channel-withdraw-on-create.mdx create mode 100644 docs/learn/protocol-flows/home-channel-withdrawal.mdx create mode 100644 docs/learn/protocol-flows/transfer-flow.mdx create mode 100644 static/img/protocol-petal-diagram.png create mode 100644 versioned_docs/version-0.5.x/api-reference/_category_.json create mode 100644 versioned_docs/version-0.5.x/api-reference/app-sessions.md create mode 100644 versioned_docs/version-0.5.x/api-reference/index.md create mode 100644 versioned_docs/version-0.5.x/build/_category_.json create mode 100644 versioned_docs/version-0.5.x/build/quick-start/index.md create mode 100644 versioned_docs/version-0.5.x/guides/index.md create mode 100644 versioned_docs/version-0.5.x/guides/migration-guide.md create mode 100644 versioned_docs/version-0.5.x/guides/multi-party-app-sessions.mdx create mode 100644 versioned_docs/version-0.5.x/learn/_category_.json create mode 100644 versioned_docs/version-0.5.x/learn/advanced/_category_.json create mode 100644 versioned_docs/version-0.5.x/learn/advanced/managing-session-keys.mdx create mode 100644 versioned_docs/version-0.5.x/learn/core-concepts/_category_.json create mode 100644 versioned_docs/version-0.5.x/learn/core-concepts/app-sessions.mdx create mode 100644 versioned_docs/version-0.5.x/learn/core-concepts/challenge-response.mdx create mode 100644 versioned_docs/version-0.5.x/learn/core-concepts/message-envelope.mdx create mode 100644 versioned_docs/version-0.5.x/learn/core-concepts/session-keys.mdx create mode 100644 versioned_docs/version-0.5.x/learn/core-concepts/state-channels-vs-l1-l2.mdx create mode 100644 versioned_docs/version-0.5.x/learn/getting-started/_category_.json create mode 100644 versioned_docs/version-0.5.x/learn/getting-started/key-terms.mdx create mode 100644 versioned_docs/version-0.5.x/learn/getting-started/prerequisites.mdx create mode 100644 versioned_docs/version-0.5.x/learn/getting-started/quickstart.mdx create mode 100644 versioned_docs/version-0.5.x/learn/index.mdx create mode 100644 versioned_docs/version-0.5.x/learn/introduction/_category_.json create mode 100644 versioned_docs/version-0.5.x/learn/introduction/architecture-at-a-glance.mdx create mode 100644 versioned_docs/version-0.5.x/learn/introduction/supported-chains.mdx create mode 100644 versioned_docs/version-0.5.x/learn/introduction/what-yellow-solves.mdx create mode 100644 versioned_docs/version-0.5.x/manuals/_category_.json create mode 100644 versioned_docs/version-0.5.x/manuals/index.md create mode 100644 versioned_docs/version-0.5.x/manuals/request-asset-support.md create mode 100644 versioned_docs/version-0.5.x/manuals/request-blockchain-support.md create mode 100644 versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md create mode 100644 versioned_docs/version-0.5.x/protocol/_category_.json create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/_category_.json create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/_category_.json create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/app-sessions.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/authentication.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/channel-methods.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/message-format.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/overview.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/queries.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/off-chain/transfers.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/on-chain/_category_.json create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/on-chain/channel-lifecycle.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/on-chain/data-structures.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/on-chain/overview.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/on-chain/security.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/app-layer/on-chain/signature-formats.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/architecture.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/communication-flows.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/glossary.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/implementation-checklist.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/introduction.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/protocol-reference.mdx create mode 100644 versioned_docs/version-0.5.x/protocol/terminology.mdx create mode 100644 versioned_docs/version-0.5.x/tutorials/_category_.json create mode 100644 versioned_docs/version-0.5.x/tutorials/index.md create mode 100644 versioned_sidebars/version-0.5.x-sidebars.json create mode 100644 versions.json diff --git a/docs/build/sdk/_category_.json b/docs/build/sdk/_category_.json new file mode 100644 index 0000000..32cf882 --- /dev/null +++ b/docs/build/sdk/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "SDK", + "position": 2, + "collapsed": false, + "collapsible": false +} diff --git a/docs/build/sdk/go/_category_.json b/docs/build/sdk/go/_category_.json new file mode 100644 index 0000000..3a2be2d --- /dev/null +++ b/docs/build/sdk/go/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Go SDK", + "position": 4, + "collapsed": false, + "collapsible": false +} diff --git a/docs/build/sdk/go/api-reference.mdx b/docs/build/sdk/go/api-reference.mdx new file mode 100644 index 0000000..e73c5f1 --- /dev/null +++ b/docs/build/sdk/go/api-reference.mdx @@ -0,0 +1,165 @@ +--- +title: API Reference +description: Complete API reference for the Clearnode Go SDK +sidebar_position: 2 +--- + +# Go SDK API Reference + +## State Operations (Off-Chain) + +```go +client.Deposit(ctx, blockchainID, asset, amount) // Prepare deposit state +client.Withdraw(ctx, blockchainID, asset, amount) // Prepare withdrawal state +client.Transfer(ctx, recipientWallet, asset, amount) // Prepare transfer state +client.CloseHomeChannel(ctx, asset) // Prepare finalize state +client.Acknowledge(ctx, asset) // Acknowledge received state +``` + +All return `(*core.State, error)`. Use `Checkpoint` to settle on-chain. + +--- + +## Blockchain Settlement + +```go +client.Checkpoint(ctx, asset) // Settle latest state on-chain +client.Challenge(ctx, state) // Submit on-chain challenge +client.ApproveToken(ctx, chainID, asset, amount) // Approve ChannelHub to spend tokens +client.GetOnChainBalance(ctx, chainID, asset, wallet) // Query on-chain token balance +``` + +`Checkpoint` routes automatically based on transition type and channel status: +- **Void** → creates channel +- **Deposit/Withdrawal** → checkpoints state +- **Finalize** → closes channel + +--- + +## Node Information + +```go +client.Ping(ctx) // Health check +client.GetConfig(ctx) // Node configuration +client.GetBlockchains(ctx) // Supported blockchains +client.GetAssets(ctx, &blockchainID) // Supported assets (nil for all) +``` + +--- + +## User Queries + +```go +client.GetBalances(ctx, wallet) // User balances +client.GetTransactions(ctx, wallet, opts) // Transaction history (paginated) +``` + +--- + +## Channel Queries + +```go +client.GetHomeChannel(ctx, wallet, asset) // Home channel info +client.GetEscrowChannel(ctx, escrowChannelID) // Escrow channel info +client.GetLatestState(ctx, wallet, asset, onlySigned) // Latest state +``` + +--- + +## App Registry + +```go +apps, meta, err := client.GetApps(ctx, &sdk.GetAppsOptions{ + AppID: &appID, + OwnerWallet: &wallet, +}) + +err := client.RegisterApp(ctx, "my-app", `{"name": "My App"}`, false) +``` + +--- + +## App Sessions + +```go +sessions, meta, err := client.GetAppSessions(ctx, opts) +def, err := client.GetAppDefinition(ctx, appSessionID) +sessionID, version, status, err := client.CreateAppSession(ctx, def, data, sigs) +nodeSig, err := client.SubmitAppSessionDeposit(ctx, update, sigs, asset, amount) +err := client.SubmitAppState(ctx, update, sigs) +batchID, err := client.RebalanceAppSessions(ctx, signedUpdates) +``` + +--- + +## Session Keys — App Sessions + +```go +state := app.AppSessionKeyStateV1{ + UserAddress: client.GetUserAddress(), + SessionKey: "0xSessionKey...", + Version: 1, + ApplicationIDs: []string{"app1"}, + AppSessionIDs: []string{}, + ExpiresAt: time.Now().Add(24 * time.Hour), +} +sig, err := client.SignSessionKeyState(state) +state.UserSig = sig +err = client.SubmitAppSessionKeyState(ctx, state) + +states, err := client.GetLastAppKeyStates(ctx, userAddress, nil) +``` + +--- + +## Session Keys — Channels + +```go +state := core.ChannelSessionKeyStateV1{ + UserAddress: client.GetUserAddress(), + SessionKey: "0xSessionKey...", + Version: 1, + Assets: []string{"usdc", "weth"}, + ExpiresAt: time.Now().Add(24 * time.Hour), +} +sig, err := client.SignChannelSessionKeyState(state) +state.UserSig = sig +err = client.SubmitChannelSessionKeyState(ctx, state) + +states, err := client.GetLastChannelKeyStates(ctx, userAddress, nil) +``` + +--- + +## Utilities + +```go +client.Close() // Close connection +client.WaitCh() // Connection monitor channel +client.SignState(state) // Sign a state (advanced) +client.GetUserAddress() // Get signer's address +client.SetHomeBlockchain(asset, chainID) // Set default blockchain +``` + +--- + +## Types + +```go +// Core types +core.State // Channel state +core.Channel // Channel info +core.Transition // State transition +core.Transaction // Transaction record +core.Asset // Asset info +core.Blockchain // Blockchain info +core.ChannelSessionKeyStateV1 // Channel session key state + +// App types +app.AppV1 // Application definition +app.AppInfoV1 // Application info with timestamps +app.AppSessionInfoV1 // Session info +app.AppDefinitionV1 // Session definition +app.AppStateUpdateV1 // Session update +app.AppSessionKeyStateV1 // App session key state +``` diff --git a/docs/build/sdk/go/getting-started.mdx b/docs/build/sdk/go/getting-started.mdx new file mode 100644 index 0000000..fc002d2 --- /dev/null +++ b/docs/build/sdk/go/getting-started.mdx @@ -0,0 +1,156 @@ +--- +title: Getting Started +description: Install and set up the Clearnode Go SDK +sidebar_position: 1 +--- + +# Getting Started with the Go SDK + +Go SDK for Clearnode payment channels providing both high-level and low-level operations in a unified client. It has full feature parity with the TypeScript SDK. + +## Requirements + +- Go 1.21+ +- Running Clearnode instance +- Blockchain RPC endpoint (for `Checkpoint` settlement) + +## Installation + +```bash +go get github.com/layer-3/nitrolite/sdk/go +``` + +## Quick Start + +```go +package main + +import ( + "context" + "fmt" + "github.com/layer-3/nitrolite/pkg/core" + "github.com/layer-3/nitrolite/pkg/sign" + sdk "github.com/layer-3/nitrolite/sdk/go" + "github.com/shopspring/decimal" +) + +func main() { + // 1. Create signers from private key + msgSigner, _ := sign.NewEthereumMsgSigner(privateKeyHex) + stateSigner, _ := core.NewChannelDefaultSigner(msgSigner) + txSigner, _ := sign.NewEthereumRawSigner(privateKeyHex) + + // 2. Create unified client + client, _ := sdk.NewClient( + "wss://clearnode.example.com/ws", + stateSigner, + txSigner, + sdk.WithBlockchainRPC(80002, "https://polygon-amoy.alchemy.com/v2/KEY"), + ) + defer client.Close() + + ctx := context.Background() + + // 3. Build and co-sign states off-chain + state, _ := client.Deposit(ctx, 80002, "usdc", decimal.NewFromInt(100)) + fmt.Printf("Deposit state version: %d\n", state.Version) + + // 4. Settle on-chain via Checkpoint + txHash, _ := client.Checkpoint(ctx, "usdc") + fmt.Printf("On-chain tx: %s\n", txHash) + + // 5. Transfer (off-chain only) + state, _ = client.Transfer(ctx, "0xRecipient...", "usdc", decimal.NewFromInt(50)) + + // 6. Low-level operations on the same client + config, _ := client.GetConfig(ctx) + balances, _ := client.GetBalances(ctx, client.GetUserAddress()) +} +``` + +## Creating a Client + +```go +// Step 1: Create signers +msgSigner, err := sign.NewEthereumMsgSigner("0x1234...") +stateSigner, err := core.NewChannelDefaultSigner(msgSigner) +txSigner, err := sign.NewEthereumRawSigner("0x1234...") + +// Step 2: Create unified client +client, err := sdk.NewClient( + wsURL, + stateSigner, // core.ChannelSigner for channel states + txSigner, // sign.Signer for blockchain transactions + sdk.WithBlockchainRPC(chainID, rpcURL), + sdk.WithHandshakeTimeout(10*time.Second), + sdk.WithPingInterval(5*time.Second), +) + +// Step 3: (Optional) Set home blockchain for assets +err = client.SetHomeBlockchain("usdc", 80002) +``` + +## Channel Signers + +The Go SDK wraps raw signers with a `ChannelSigner` interface that prepends a type byte to every signature: + +| Type | Byte | Struct | Usage | +|------|------|--------|-------| +| Default | `0x00` | `core.ChannelDefaultSigner` | Main wallet signs directly | +| Session Key | `0x01` | `core.ChannelSessionKeySignerV1` | Delegated session key | + +```go +// Default signer (wraps EthereumMsgSigner with 0x00 prefix) +msgSigner, _ := sign.NewEthereumMsgSigner(privateKeyHex) +channelSigner, _ := core.NewChannelDefaultSigner(msgSigner) +client, _ := sdk.NewClient(wsURL, channelSigner, txSigner, opts...) +``` + +## Key Concepts + +### Two-Step Pattern + +```go +// Step 1: Build and co-sign state off-chain +state, _ := client.Deposit(ctx, 80002, "usdc", decimal.NewFromInt(100)) + +// Step 2: Settle on-chain (when needed) +txHash, _ := client.Checkpoint(ctx, "usdc") +``` + +### Channel Lifecycle + +1. **Void** — No channel exists +2. **Create** — `Deposit()` creates channel on-chain via `Checkpoint()` +3. **Open** — Channel active; can deposit, withdraw, transfer +4. **Challenged** — Dispute initiated +5. **Closed** — Channel finalized + +## Configuration Options + +```go +sdk.WithBlockchainRPC(chainID, rpcURL) // Required for Checkpoint +sdk.WithHandshakeTimeout(duration) // Default: 5s +sdk.WithPingInterval(duration) // Default: 5s +sdk.WithErrorHandler(func(error)) // Connection error handler +``` + +## Error Handling + +```go +state, err := client.Deposit(ctx, 80002, "usdc", amount) +if err != nil { + log.Printf("State error: %v", err) +} + +txHash, err := client.Checkpoint(ctx, "usdc") +if err != nil { + log.Printf("Checkpoint error: %v", err) +} +``` + +Common errors: +- `"home blockchain not set for asset"` — Missing `SetHomeBlockchain` +- `"blockchain RPC not configured for chain"` — Missing `WithBlockchainRPC` +- `"no channel exists for asset"` — `Checkpoint` without a co-signed state +- `"insufficient balance"` — Not enough funds in channel/wallet diff --git a/docs/build/sdk/index.md b/docs/build/sdk/index.md new file mode 100644 index 0000000..3493fb4 --- /dev/null +++ b/docs/build/sdk/index.md @@ -0,0 +1,49 @@ +--- +title: SDK Overview +description: Yellow Network SDKs for building state channel applications +sidebar_position: 1 +--- + +# SDK Overview + +Yellow Network provides official SDKs for building applications on top of Nitrolite payment channels. All SDKs share the same two-step architecture: **build and co-sign states off-chain**, then **settle on-chain when needed**. + +## Available SDKs + +| Package | Language | Description | +|---------|----------|-------------| +| [`@yellow-org/sdk`](./typescript/getting-started) | TypeScript | Main SDK with full API coverage | +| [`@yellow-org/sdk-compat`](./typescript-compat/overview) | TypeScript | Compatibility layer for migrating from v0.5.3 | +| [`clearnode-go-sdk`](./go/getting-started) | Go | Go SDK with full feature parity | + +## Architecture + +All SDKs follow a unified design: + +- **State Operations** (off-chain): `deposit()`, `withdraw()`, `transfer()`, `closeHomeChannel()`, `acknowledge()` — build and co-sign channel states without touching the blockchain. +- **Blockchain Settlement**: `checkpoint()` — the single entry point for all on-chain transactions. Routes to the correct contract method based on transition type and channel status. +- **Low-Level Operations**: Direct RPC access for app sessions, session keys, queries, and custom flows. + +```mermaid +sequenceDiagram + participant App + participant SDK + participant Node as Clearnode + participant Chain as Blockchain + + App->>SDK: deposit(chain, asset, amount) + SDK->>Node: Build & co-sign state + Node-->>SDK: Co-signed state + SDK-->>App: State object + + App->>SDK: checkpoint(asset) + SDK->>Chain: Create/checkpoint/close channel + Chain-->>SDK: Transaction hash + SDK-->>App: tx hash +``` + +## Choosing an SDK + +- **New TypeScript projects**: Use [`@yellow-org/sdk`](./typescript/getting-started) directly. +- **Migrating from v0.5.3**: Use [`@yellow-org/sdk-compat`](./typescript-compat/overview) to minimise code changes, then migrate to the main SDK at your own pace. +- **Go projects**: Use the [Go SDK](./go/getting-started) — it has full feature parity with the TypeScript SDK. diff --git a/docs/build/sdk/typescript-compat/_category_.json b/docs/build/sdk/typescript-compat/_category_.json new file mode 100644 index 0000000..5139e8f --- /dev/null +++ b/docs/build/sdk/typescript-compat/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "TypeScript Compat SDK", + "position": 3, + "collapsed": false, + "collapsible": false +} diff --git a/docs/build/sdk/typescript-compat/migration-offchain.mdx b/docs/build/sdk/typescript-compat/migration-offchain.mdx new file mode 100644 index 0000000..a5efde4 --- /dev/null +++ b/docs/build/sdk/typescript-compat/migration-offchain.mdx @@ -0,0 +1,108 @@ +--- +title: "Migration: Off-Chain" +description: Off-chain migration from v0.5.3 to the compat layer +sidebar_position: 4 +--- + +# Off-Chain Migration Guide + +Covers authentication, app sessions, transfers, ledger queries, and event polling when migrating from v0.5.3. + +## Authentication + +v1.0.0 handles authentication internally when using `NitroliteClient`. For legacy WebSocket-auth code paths, the compat layer keeps `createAuthRequestMessage`, `createAuthVerifyMessage`, `createAuthVerifyMessageWithJWT`, and `createEIP712AuthMessageSigner` available. + +## App Sessions + +### List + +**Before:** `createGetAppSessionsMessage` + `sendRequest` + `parseGetAppSessionsResponse` + +**After:** + +```typescript +const sessions = await client.getAppSessionsList(); +``` + +### Create + +**Before:** + +```typescript +const msg = await createAppSessionMessage(signer.sign, { definition, allocations }); +const raw = await sendRequest(msg); +parseCreateAppSessionResponse(raw); +``` + +**After:** + +```typescript +await client.createAppSession(definition, allocations); +``` + +### Close + +**Before:** `createCloseAppSessionMessage` + send + parse + +**After:** + +```typescript +await client.closeAppSession(appSessionId, allocations); +``` + +### Submit State + +**Before:** `createSubmitAppStateMessage` + send + +**After:** + +```typescript +await client.submitAppState(params); +``` + +## Transfers + +**Before:** + +```typescript +const msg = await createTransferMessage(signer.sign, { destination, allocations }); +await sendRequest(msg); +``` + +**After:** + +```typescript +await client.transfer(destination, allocations); +``` + +## Ledger Queries + +**Before:** `createGetLedgerBalancesMessage` / `createGetLedgerEntriesMessage` + send + parse + +**After:** + +```typescript +const balances = await client.getBalances(); +const entries = await client.getLedgerEntries(); +``` + +## Event Polling + +v0.5.3 used WebSocket push events (`ChannelUpdate`, `BalanceUpdate`). v1.0.0 uses polling. The `EventPoller` bridges this gap: + +```typescript +import { EventPoller } from '@yellow-org/sdk-compat'; + +const poller = new EventPoller(client, { + onChannelUpdate: (channels) => updateUI(channels), + onBalanceUpdate: (balances) => updateBalances(balances), + onAssetsUpdate: (assets) => updateAssets(assets), + onError: (err) => console.error(err), +}, 5000); + +poller.start(); +``` + +## RPC Compatibility Helpers + +The `create*Message` and `parse*Response` functions still exist so existing imports compile. Most are transitional placeholders. Prefer `NitroliteClient` methods directly for new code. diff --git a/docs/build/sdk/typescript-compat/migration-onchain.mdx b/docs/build/sdk/typescript-compat/migration-onchain.mdx new file mode 100644 index 0000000..73a0ee4 --- /dev/null +++ b/docs/build/sdk/typescript-compat/migration-onchain.mdx @@ -0,0 +1,80 @@ +--- +title: "Migration: On-Chain" +description: On-chain migration from v0.5.3 to the compat layer +sidebar_position: 3 +--- + +# On-Chain Migration Guide + +Covers deposits, withdrawals, channel operations, amount handling, and contract addresses when migrating from v0.5.3. + +## Deposits + +**Before (v0.5.3):** Manual approve → deposit → createChannel + +```typescript +await approveToken(custody, tokenAddress, amount); +await sendRequest(createDepositMessage(signer.sign, { token: tokenAddress, amount })); +await sendRequest(createCreateChannelMessage(signer.sign, { token: tokenAddress, amount })); +``` + +**After (compat):** Single call — approval and channel creation are implicit + +```typescript +await client.deposit(tokenAddress, amount); +``` + +## Withdrawals + +**Before (v0.5.3):** Manual close → checkpoint → withdraw + +```typescript +const closeMsg = await createCloseChannelMessage(signer.sign, { channel_id }); +const raw = await sendRequest(closeMsg); +await sendRequest(createWithdrawMessage(signer.sign, { token, amount })); +``` + +**After (compat):** Single call + +```typescript +await client.withdrawal(tokenAddress, amount); +``` + +## Channel Operations + +| Operation | v0.5.3 | Compat | +|-----------|--------|--------| +| Create | Explicit `createChannel()` | Implicit on first `deposit()` | +| Close | `createCloseChannelMessage` + send + parse | `client.closeChannel()` | +| Resize | `createResizeChannelMessage` + send + parse | `client.resizeChannel({ allocate_amount, token })` | + +## Amount Handling + +**Before (v0.5.3):** Raw `BigInt` everywhere; app must handle decimals + +```typescript +const amount = 11_000_000n; // 11 USDC (6 decimals) +``` + +**After (compat):** Accepts both; conversion handled internally + +```typescript +// Raw BigInt still works +await client.deposit(tokenAddress, 11_000_000n); + +// Or use helpers +const formatted = client.formatAmount(tokenAddress, 11_000_000n); // "11.0" +const parsed = client.parseAmount(tokenAddress, "11.0"); // 11_000_000n +``` + +For transfers and allocations, compat accepts human-readable strings: `{ asset: 'usdc', amount: '5.0' }`. + +## Contract Addresses + +**Before (v0.5.3):** Manual config + +```typescript +const addresses = { custody: '0x...', adjudicator: '0x...' }; +``` + +**After (compat):** Fetched from clearnode `get_config` — no manual setup. The `addresses` field in config is deprecated and ignored. diff --git a/docs/build/sdk/typescript-compat/migration-overview.mdx b/docs/build/sdk/typescript-compat/migration-overview.mdx new file mode 100644 index 0000000..9ac4ecc --- /dev/null +++ b/docs/build/sdk/typescript-compat/migration-overview.mdx @@ -0,0 +1,69 @@ +--- +title: Migration Overview +description: Migrating from v0.5.3 to the compat layer +sidebar_position: 2 +--- + +# Migrating from v0.5.3 to Compat Layer + +This guide explains how to migrate your Nitrolite dApp from the v0.5.3 SDK to the compat layer, which bridges the old API to the v1.0.0 runtime with minimal code changes. + +## Why Use the Compat Layer + +A direct migration from v0.5.3 to v1.0.0 touches **20+ files** per app with deep, scattered rewrites. The compat layer reduces this to **~5 file changes** per app. + +## Installation + +```bash +npm install @yellow-org/sdk-compat +# Peer dependencies +npm install @yellow-org/sdk viem +``` + +## Import Swap + +| Before (v0.5.3) | After (compat) | +|-----------------|----------------| +| `import { createGetChannelsMessage, parseGetChannelsResponse } from '@layer-3/nitrolite'` | `import { NitroliteClient } from '@yellow-org/sdk-compat'` | +| Types: `AppSession`, `LedgerChannel`, `RPCAppDefinition` | Same types — re-exported from `@yellow-org/sdk-compat` | + +For **types**, just change the package name. For **functions**, switch to client methods instead of `create*Message` / `parse*Response`. + +## The Key Pattern Change + +**Before (v0.5.3):** create-sign-send-parse + +```typescript +const msg = await createGetChannelsMessage(signer.sign, addr); +const raw = await sendRequest(msg); +const parsed = parseGetChannelsResponse(raw); +const channels = parsed.params.channels; +``` + +**After (compat):** direct client method + +```typescript +const client = await NitroliteClient.create(config); +const channels = await client.getChannels(); +``` + +## What Stays the Same + +- **Type shapes:** `AppSession`, `LedgerChannel`, `RPCAppDefinition`, `RPCBalance`, `RPCAsset`, etc. +- **Response formats:** Balances, ledger entries, app sessions — same structure as v0.5.3. +- **Auth helpers:** `createAuthRequestMessage`, `createAuthVerifyMessage`, `createAuthVerifyMessageWithJWT` remain available. + +## What Changes + +| Concern | v0.5.3 | Compat | +|---------|--------|--------| +| WebSocket | App creates and manages `WebSocket` | Managed internally by the client | +| Signing | App passes `signer.sign` into every message | Internal — client uses `WalletClient` | +| Amounts | Raw `BigInt` everywhere | Compat accepts both; conversion handled internally | +| Contract addresses | Manual config | Fetched from clearnode `get_config` | +| Channel creation | Explicit `createChannel()` | Implicit on first `deposit()` | + +## Next Steps + +- [On-Chain Changes](./migration-onchain) — Deposits, withdrawals, channel operations, amount handling +- [Off-Chain Changes](./migration-offchain) — App sessions, transfers, ledger queries, event polling diff --git a/docs/build/sdk/typescript-compat/overview.mdx b/docs/build/sdk/typescript-compat/overview.mdx new file mode 100644 index 0000000..718aa23 --- /dev/null +++ b/docs/build/sdk/typescript-compat/overview.mdx @@ -0,0 +1,235 @@ +--- +title: Overview +description: Compatibility layer bridging Nitrolite SDK v0.5.3 API to v1.0.0 +sidebar_position: 1 +--- + +# TypeScript Compat SDK + +Compatibility layer that bridges the Nitrolite SDK **v0.5.3 API** to the **v1.0.0 runtime**, letting existing dApps upgrade to the new protocol with minimal code changes. + +## Why Use the Compat Layer + +The v1.0.0 protocol introduces breaking changes across 14 dimensions — wire format, authentication, WebSocket lifecycle, unit system, asset resolution, and more. A direct migration touches 20+ files per app with deep, scattered rewrites. + +The compat layer centralises this complexity into **~1,000 lines** that absorb the protocol differences, reducing per-app integration effort by an estimated **56–70%**. + +## Installation + +```bash +npm install @yellow-org/sdk-compat +# peer dependencies +npm install @yellow-org/sdk viem +``` + +## Quick Start + +```typescript +import { NitroliteClient, blockchainRPCsFromEnv } from '@yellow-org/sdk-compat'; + +// Create client (replaces new Client(ws, signer)) +const client = await NitroliteClient.create({ + wsURL: 'wss://clearnode.example.com/ws', + walletClient, // viem WalletClient with account + chainId: 11155111, // Sepolia + blockchainRPCs: blockchainRPCsFromEnv(), +}); + +// Deposit (creates channel if needed) +await client.deposit(tokenAddress, 11_000_000n); + +// Query +const channels = await client.getChannels(); +const balances = await client.getBalances(); +const sessions = await client.getAppSessionsList(); + +// Transfer +await client.transfer(recipientAddress, [{ asset: 'usdc', amount: '5.0' }]); + +// Cleanup +await client.closeChannel(); +await client.close(); +``` + +## Method Cheat Sheet + +### Channel Operations + +| Method | Description | +|--------|-------------| +| `deposit(token, amount)` | Deposit to channel (creates if needed) | +| `depositAndCreateChannel(token, amount)` | Alias for `deposit()` | +| `withdrawal(token, amount)` | Withdraw from channel | +| `closeChannel(params?)` | Close open channels (optionally for a specific token) | +| `resizeChannel({ allocate_amount, token })` | Resize an existing channel | +| `challengeChannel({ state })` | Challenge a channel on-chain | +| `createChannel()` | No-op in v1 (channel creation is implicit on `deposit()`) | + +### Queries + +| Method | Description | +|--------|-------------| +| `getChannels()` | List all ledger channels | +| `getChannelData(channelId)` | Full channel + state for a specific channel | +| `getBalances(wallet?)` | Get ledger balances | +| `getLedgerEntries(wallet?)` | Get transaction history | +| `getAppSessionsList(wallet?, status?)` | List app sessions | +| `getLastAppSessionsListError()` | Last error from `getAppSessionsList()` (if any) | +| `getAssetsList()` | List supported assets | +| `getAccountInfo()` | Aggregate balance + channel count | +| `getConfig()` | Node configuration | + +### Transfers + +| Method | Description | +|--------|-------------| +| `transfer(destination, allocations)` | Off-chain transfer to another participant | + +### App Sessions + +| Method | Description | +|--------|-------------| +| `createAppSession(definition, allocations, quorumSigs?)` | Create an app session | +| `closeAppSession(appSessionId, allocations, quorumSigs?)` | Close an app session | +| `submitAppState(params)` | Submit state update (operate/deposit/withdraw/close) | +| `getAppDefinition(appSessionId)` | Get session definition | + +### App Session Signing Helpers + +| Helper | Description | +|--------|-------------| +| `packCreateAppSessionHash(params)` | Deterministic hash for `createAppSession` quorum signing | +| `packSubmitAppStateHash(params)` | Deterministic hash for `submitAppState` quorum signing | +| `toWalletQuorumSignature(signature)` | Prefix wallet signature for app-session quorum format | +| `toSessionKeyQuorumSignature(signature)` | Prefix session key signature (`0xa2`) for quorum format | + +### Session Keys + +| Method | Description | +|--------|-------------| +| `signChannelSessionKeyState(state)` | Sign a channel session-key state | +| `submitChannelSessionKeyState(state)` | Register channel session-key | +| `getLastChannelKeyStates(userAddress, sessionKey?)` | Get active channel session-key states | +| `signSessionKeyState(state)` | Sign an app-session key state | +| `submitSessionKeyState(state)` | Register app-session key | +| `getLastKeyStates(userAddress, sessionKey?)` | Get active app-session key states | + +### Asset Resolution + +| Method | Description | +|--------|-------------| +| `resolveToken(tokenAddress)` | Look up asset info by token address | +| `resolveAsset(symbol)` | Look up asset info by symbol | +| `resolveAssetDisplay(tokenAddress, chainId?)` | Get display-friendly symbol + decimals | +| `getTokenDecimals(tokenAddress)` | Get decimals for a token | +| `formatAmount(tokenAddress, rawAmount)` | Raw bigint → human-readable string | +| `parseAmount(tokenAddress, humanAmount)` | Human-readable string → raw bigint | +| `findOpenChannel(tokenAddress, chainId?)` | Find an open channel for a given token | + +### Lifecycle + +| Method | Description | +|--------|-------------| +| `ping()` | Health check | +| `close()` | Close the WebSocket connection | +| `refreshAssets()` | Re-fetch the asset map from the clearnode | + +## Properties + +| Property | Type | Description | +|----------|------|-------------| +| `innerClient` | `Client` (readonly) | The underlying v1.0.0 SDK Client | +| `userAddress` | `Address` (readonly) | The connected wallet address | + +## Configuration + +```typescript +interface NitroliteClientConfig { + wsURL: string; // Clearnode WebSocket URL + walletClient: WalletClient; // viem WalletClient with account + chainId: number; // Chain ID (e.g. 11155111) + blockchainRPCs?: Record; // Chain ID → RPC URL map + channelSessionKeySigner?: { // Optional session key for quick approvals + sessionKeyPrivateKey: Hex; + walletAddress: Address; + metadataHash: Hex; + authSig: Hex; + }; +} +``` + +### Environment Variables + +`blockchainRPCsFromEnv()` reads from `NEXT_PUBLIC_BLOCKCHAIN_RPCS`: + +```text +NEXT_PUBLIC_BLOCKCHAIN_RPCS=11155111:https://rpc.sepolia.io,1:https://mainnet.infura.io/v3/KEY +``` + +## Accessing the v1.0.0 SDK + +The underlying v1.0.0 `Client` is exposed for advanced use cases not covered by the compat surface: + +```typescript +const v1Client = client.innerClient; +await v1Client.getHomeChannel(wallet, 'usdc'); +await v1Client.checkpoint('usdc'); +await v1Client.approveToken(chainId, 'usdc', amount); +``` + +## Error Handling + +| Error Class | Code | Description | +|-------------|------|-------------| +| `AllowanceError` | `ALLOWANCE_INSUFFICIENT` | Token approval needed | +| `UserRejectedError` | `USER_REJECTED` | User cancelled in wallet | +| `InsufficientFundsError` | `INSUFFICIENT_FUNDS` | Not enough balance | +| `NotInitializedError` | `NOT_INITIALIZED` | Client not connected | + +```typescript +import { getUserFacingMessage, AllowanceError } from '@yellow-org/sdk-compat'; + +try { + await client.deposit(token, amount); +} catch (err) { + // Convert raw errors to typed compat errors + const typed = NitroliteClient.classifyError(err); + if (typed instanceof AllowanceError) { + // prompt user to approve token spending + } + showToast(getUserFacingMessage(err)); +} +``` + +## Event Polling + +v0.5.3 used WebSocket push events. v1.0.0 uses polling. The `EventPoller` bridges this gap: + +```typescript +import { EventPoller } from '@yellow-org/sdk-compat'; + +const poller = new EventPoller(client, { + onChannelUpdate: (channels) => updateUI(channels), + onBalanceUpdate: (balances) => updateBalances(balances), + onAssetsUpdate: (assets) => updateAssets(assets), + onError: (err) => console.error(err), +}, 5000); + +poller.start(); +``` + +## Next.js Integration + +Add to `transpilePackages` in `next.config.ts`: + +```typescript +const nextConfig = { + transpilePackages: ['@yellow-org/sdk', '@yellow-org/sdk-compat'], +}; +``` + +## Migration Guides + +- [Migration Overview](./migration-overview) — Pattern changes, import swaps +- [On-Chain Changes](./migration-onchain) — Deposits, withdrawals, channels +- [Off-Chain Changes](./migration-offchain) — Auth, app sessions, transfers diff --git a/docs/build/sdk/typescript/_category_.json b/docs/build/sdk/typescript/_category_.json new file mode 100644 index 0000000..9fdfa4a --- /dev/null +++ b/docs/build/sdk/typescript/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "TypeScript SDK", + "position": 2, + "collapsed": false, + "collapsible": false +} diff --git a/docs/build/sdk/typescript/api-reference.mdx b/docs/build/sdk/typescript/api-reference.mdx new file mode 100644 index 0000000..830eee2 --- /dev/null +++ b/docs/build/sdk/typescript/api-reference.mdx @@ -0,0 +1,317 @@ +--- +title: API Reference +description: Complete API reference for the Yellow TypeScript SDK +sidebar_position: 2 +--- + +# TypeScript SDK API Reference + +All methods are available on the `Client` instance. State operations return `Promise`, settlement returns transaction hashes, and queries return typed response objects. + +## State Operations (Off-Chain) + +These methods build and co-sign a state off-chain. Use [`checkpoint()`](#checkpointasset) to settle on-chain. + +### `deposit(blockchainId, asset, amount)` + +Prepares a deposit state. Creates a new channel if none exists, otherwise advances the existing state. + +```typescript +const state = await client.deposit(80002n, 'usdc', new Decimal(100)); +const txHash = await client.checkpoint('usdc'); +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `blockchainId` | `bigint` | Target blockchain ID | +| `asset` | `string` | Asset symbol (e.g. `'usdc'`) | +| `amount` | `Decimal` | Amount to deposit | + +**Scenarios:** +- No channel exists → creates new channel with initial deposit +- Channel exists → advances the existing state with a deposit transition + +--- + +### `withdraw(blockchainId, asset, amount)` + +Prepares a withdrawal state to remove funds from the channel. + +```typescript +const state = await client.withdraw(80002n, 'usdc', new Decimal(25)); +const txHash = await client.checkpoint('usdc'); +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `blockchainId` | `bigint` | Target blockchain ID | +| `asset` | `string` | Asset symbol | +| `amount` | `Decimal` | Amount to withdraw | + +**Requires:** Existing channel with sufficient balance. + +--- + +### `transfer(recipientWallet, asset, amount)` + +Prepares an off-chain transfer to another wallet. For existing channels, no checkpoint is needed. + +```typescript +const state = await client.transfer('0xRecipient...', 'usdc', new Decimal(50)); +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `recipientWallet` | `string` | Recipient address | +| `asset` | `string` | Asset symbol | +| `amount` | `Decimal` | Amount to transfer | + +**Requires:** Existing channel with sufficient balance, or home blockchain configured via `setHomeBlockchain()` for new channels. + +--- + +### `closeHomeChannel(asset)` + +Prepares a finalize state to close the user's channel for a specific asset. + +```typescript +const state = await client.closeHomeChannel('usdc'); +const txHash = await client.checkpoint('usdc'); +``` + +--- + +### `acknowledge(asset)` + +Acknowledges a received state (e.g., after receiving a transfer). + +```typescript +const state = await client.acknowledge('usdc'); +``` + +**Requires:** Home blockchain configured via `setHomeBlockchain()` when no channel exists. + +--- + +## Blockchain Settlement + +### `checkpoint(asset)` + +Settles the latest co-signed state on-chain. Routes to the correct contract method based on transition type and on-chain channel status: + +- **Channel not on-chain** (Void) → creates the channel +- **Deposit/Withdrawal** on existing channel → checkpoints the state +- **Finalize** → closes the channel + +```typescript +const txHash = await client.checkpoint('usdc'); +``` + +**Requires:** Blockchain RPC via `withBlockchainRPC()` and a co-signed state. + +--- + +### `challenge(state)` + +Submits an on-chain challenge for a channel. Initiates a dispute period. + +```typescript +const state = await client.getLatestState(wallet, 'usdc', true); +const txHash = await client.challenge(state); +``` + +--- + +### `approveToken(chainId, asset, amount)` + +Approves the ChannelHub contract to spend ERC-20 tokens. Required before depositing. + +```typescript +const txHash = await client.approveToken(80002n, 'usdc', new Decimal(1000)); +``` + +--- + +### `checkTokenAllowance(chainId, tokenAddress, owner)` + +Checks the current token allowance for the ChannelHub contract. + +```typescript +const allowance = await client.checkTokenAllowance(80002n, '0xToken...', '0xOwner...'); +``` + +--- + +## Node Information + +```typescript +await client.ping(); // Health check +const config = await client.getConfig(); // Node configuration +const chains = await client.getBlockchains(); // Supported blockchains +const assets = await client.getAssets(); // All assets (or pass blockchainId) +``` + +--- + +## User Queries + +```typescript +const balances = await client.getBalances(wallet); + +const { transactions, metadata } = await client.getTransactions(wallet, { + asset: 'usdc', // filter by asset + txType: 'transfer', // filter by transaction type + page: 1, + pageSize: 50, +}); + +const allowances = await client.getActionAllowances(wallet); +``` + +--- + +## Channel Queries + +```typescript +const { channels, metadata } = await client.getChannels(wallet, { + status: 'open', // filter by status + asset: 'usdc', // filter by asset + channelType: 'home', // 'home' or 'escrow' + pagination: { offset: 0, limit: 20 }, +}); + +const channel = await client.getHomeChannel(wallet, asset); +const escrow = await client.getEscrowChannel(escrowChannelId); + +// onlySigned: if true, returns only the latest state with both signatures +const state = await client.getLatestState(wallet, asset, true); +``` + +--- + +## App Registry + +```typescript +const { apps, metadata } = await client.getApps({ + appId: 'my-app', + ownerWallet: '0x1234...', + page: 1, + pageSize: 10, +}); + +await client.registerApp('my-app', '{"name": "My App"}', false); +``` + +--- + +## App Sessions + +### Create and Manage + +```typescript +const { appSessionId, version, status } = await client.createAppSession( + definition, // AppDefinitionV1 + sessionData, // JSON string + signatures // string[] +); + +const nodeSig = await client.submitAppSessionDeposit( + appUpdate, // AppStateUpdateV1 + quorumSigs, // string[] + asset, // string + depositAmount // Decimal +); + +await client.submitAppState(appUpdate, quorumSigs); + +const batchId = await client.rebalanceAppSessions(signedUpdates); +``` + +### Query + +```typescript +const { sessions, metadata } = await client.getAppSessions(opts); +const definition = await client.getAppDefinition(appSessionId); +``` + +--- + +## App Session Keys + +```typescript +const sig = await client.signSessionKeyState({ + user_address: '0x1234...', + session_key: '0xabcd...', + version: '1', + application_ids: ['app1'], + app_session_ids: [], + expires_at: String(Math.floor(Date.now() / 1000) + 86400), + user_sig: '0x', +}); + +await client.submitSessionKeyState({ ...state, user_sig: sig }); + +const states = await client.getLastKeyStates('0x1234...'); +``` + +--- + +## Channel Session Keys + +```typescript +const sig = await client.signChannelSessionKeyState({ + user_address: '0x1234...', + session_key: '0xabcd...', + version: '1', + assets: ['usdc'], + expires_at: String(Math.floor(Date.now() / 1000) + 86400), + user_sig: '0x', +}); + +await client.submitChannelSessionKeyState({ ...state, user_sig: sig }); + +const states = await client.getLastChannelKeyStates('0x1234...'); +``` + +--- + +## Utilities + +```typescript +await client.close(); // Close WebSocket connection +client.waitForClose(); // Promise that resolves on close +await client.signState(state); // Sign a state (advanced) +client.getUserAddress(); // Get signer's address +await client.setHomeBlockchain('usdc', 80002n); // Set default chain for asset +``` + +--- + +## Type Imports + +```typescript +import type { + State, + Channel, + Transaction, + BalanceEntry, + Asset, + Blockchain, + ActionAllowance, + AppSessionInfoV1, + AppDefinitionV1, + AppStateUpdateV1, + SignedAppStateUpdateV1, + AppSessionKeyStateV1, + ChannelSessionKeyStateV1, + PaginationMetadata, + AppV1, + AppInfoV1, +} from '@yellow-org/sdk'; +``` diff --git a/docs/build/sdk/typescript/configuration.mdx b/docs/build/sdk/typescript/configuration.mdx new file mode 100644 index 0000000..675f2a2 --- /dev/null +++ b/docs/build/sdk/typescript/configuration.mdx @@ -0,0 +1,130 @@ +--- +title: Configuration +description: Client options, signers, and error handling for the TypeScript SDK +sidebar_position: 3 +--- + +# Configuration + +## Client Options + +Configuration is passed as variadic options to `Client.create()`: + +```typescript +import { + Client, + createSigners, + withBlockchainRPC, + withHandshakeTimeout, + withErrorHandler, +} from '@yellow-org/sdk'; + +const client = await Client.create( + wsURL, + stateSigner, + txSigner, + withBlockchainRPC(chainId, rpcURL), // Blockchain RPC (required for checkpoint) + withHandshakeTimeout(10000), // Connection timeout in ms (default: 5000) + withErrorHandler(errorFn), // Connection error handler +); +``` + +### `withBlockchainRPC(chainId, rpcURL)` + +Configures an EVM blockchain RPC endpoint. Required for `checkpoint()`, `approveToken()`, and other on-chain operations. Can be called multiple times for multi-chain setups: + +```typescript +const client = await Client.create( + wsURL, stateSigner, txSigner, + withBlockchainRPC(80002n, process.env.POLYGON_RPC!), + withBlockchainRPC(11155111n, process.env.SEPOLIA_RPC!), +); +``` + +### `withHandshakeTimeout(ms)` + +Sets the WebSocket handshake timeout. Defaults to 5000ms. + +### `withErrorHandler(fn)` + +Registers a callback for connection errors: + +```typescript +withErrorHandler((error) => { + console.error('[Connection Error]', error); +}) +``` + +## Home Blockchain + +`setHomeBlockchain(asset, blockchainId)` sets the default blockchain for an asset. Required before `transfer()` on a new channel (where no chain context exists yet). + +```typescript +await client.setHomeBlockchain('usdc', 80002n); +``` + +:::warning +This mapping is **immutable** once set for the client instance. The asset must be supported on the specified blockchain. +::: + +## Error Handling + +All errors include context about what failed: + +```typescript +try { + const state = await client.deposit(80002n, 'usdc', amount); + const txHash = await client.checkpoint('usdc'); +} catch (error) { + console.error('Operation failed:', error); +} +``` + +### Common Errors + +| Error Message | Cause | Solution | +|--------------|-------|----------| +| `"channel not created, deposit first"` | Transfer before deposit | Deposit funds first | +| `"home blockchain not set for asset"` | Missing `setHomeBlockchain()` | Call `setHomeBlockchain()` before transfer | +| `"blockchain client not configured"` | Missing `withBlockchainRPC()` | Add `withBlockchainRPC()` configuration | +| `"insufficient balance"` | Not enough funds | Deposit more funds | +| `"failed to sign state"` | Invalid private key or state | Check signer configuration | +| `"no channel exists for asset"` | Checkpoint without a co-signed state | Call `deposit()`, `withdraw()`, etc. first | +| `"transition type ... does not require a blockchain operation"` | Checkpoint on unsupported transition | Only checkpoint after deposit, withdraw, close, or acknowledge | + +## BigInt for Chain IDs + +Chain IDs use JavaScript's `bigint` type: + +```typescript +const polygonAmoy = 80002n; +const ethereum = 1n; +const sepolia = 11155111n; + +await client.deposit(polygonAmoy, 'usdc', amount); +``` + +## Decimal.js for Amounts + +All token amounts use `Decimal` from `decimal.js` to avoid floating-point issues: + +```typescript +import Decimal from 'decimal.js'; + +const amount = new Decimal(100); +const precise = new Decimal('123.456'); + +await client.deposit(chainId, 'usdc', amount); +``` + +## Viem Integration + +The SDK uses `viem` for Ethereum interactions: + +```typescript +import { privateKeyToAccount } from 'viem/accounts'; +import { EthereumMsgSigner } from '@yellow-org/sdk'; + +const account = privateKeyToAccount('0x...'); +const stateSigner = new EthereumMsgSigner(account); +``` diff --git a/docs/build/sdk/typescript/examples.mdx b/docs/build/sdk/typescript/examples.mdx new file mode 100644 index 0000000..84494e5 --- /dev/null +++ b/docs/build/sdk/typescript/examples.mdx @@ -0,0 +1,377 @@ +--- +title: Examples +description: Complete working examples for the Yellow TypeScript SDK +sidebar_position: 4 +--- + +# Examples + +## Basic Deposit and Transfer + +```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +async function basicExample() { + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(80002n, process.env.RPC_URL!) + ); + + try { + console.log('User:', client.getUserAddress()); + + await client.setHomeBlockchain('usdc', 80002n); + + // Step 1: Build and co-sign deposit state + const depositState = await client.deposit(80002n, 'usdc', new Decimal(100)); + console.log('Deposit state version:', depositState.version); + + // Step 2: Settle on-chain + const txHash = await client.checkpoint('usdc'); + console.log('On-chain tx:', txHash); + + // Check balance + const balances = await client.getBalances(client.getUserAddress()); + console.log('Balances:', balances); + + // Transfer 50 USDC (off-chain, no checkpoint needed) + const transferState = await client.transfer( + '0xRecipient...', + 'usdc', + new Decimal(50) + ); + console.log('Transfer state version:', transferState.version); + } finally { + await client.close(); + } +} +``` + +## Multi-Chain Operations + +```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +async function multiChainExample() { + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(80002n, process.env.POLYGON_RPC!), + withBlockchainRPC(11155111n, process.env.SEPOLIA_RPC!) + ); + + try { + await client.setHomeBlockchain('usdc', 80002n); + await client.setHomeBlockchain('eth', 11155111n); + + // Deposit on different chains + await client.deposit(80002n, 'usdc', new Decimal(100)); + await client.checkpoint('usdc'); + + await client.deposit(11155111n, 'eth', new Decimal(0.1)); + await client.checkpoint('eth'); + + const balances = await client.getBalances(client.getUserAddress()); + balances.forEach(b => console.log(`${b.asset}: ${b.balance}`)); + } finally { + await client.close(); + } +} +``` + +## Transaction History with Pagination + +```typescript +import { Client, createSigners } from '@yellow-org/sdk'; + +async function queryTransactions() { + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner + ); + + try { + const wallet = client.getUserAddress(); + const result = await client.getTransactions(wallet, { + page: 1, + pageSize: 10, + }); + + console.log(`Total: ${result.metadata.totalCount}`); + console.log(`Page ${result.metadata.page} of ${result.metadata.pageCount}`); + + result.transactions.forEach((tx, i) => { + console.log(`${i + 1}. ${tx.txType}: ${tx.amount} ${tx.asset}`); + }); + } finally { + await client.close(); + } +} +``` + +## App Session Workflow + +```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +async function appSessionExample() { + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(80002n, process.env.RPC_URL!) + ); + + try { + const definition = { + applicationId: 'chess-v1', + participants: [ + { walletAddress: client.getUserAddress(), signatureWeight: 1 }, + { walletAddress: '0xOpponent...', signatureWeight: 1 }, + ], + quorum: 2, + nonce: 1n, + }; + + const { appSessionId } = await client.createAppSession( + definition, + '{}', + ['sig1', 'sig2'] + ); + console.log('Session created:', appSessionId); + + // Deposit to app session + const appUpdate = { + appSessionId, + intent: 1, + version: 1n, + allocations: [{ + participant: client.getUserAddress(), + asset: 'usdc', + amount: new Decimal(50), + }], + sessionData: '{}', + }; + + const nodeSig = await client.submitAppSessionDeposit( + appUpdate, + ['sig1'], + 'usdc', + new Decimal(50) + ); + console.log('Deposit signature:', nodeSig); + + const { sessions } = await client.getAppSessions({ + wallet: client.getUserAddress(), + }); + console.log(`Found ${sessions.length} sessions`); + } finally { + await client.close(); + } +} +``` + +## Browser Wallet Integration (viem) + +The example-app demonstrates using viem `WalletClient` (e.g. MetaMask) instead of private keys. You need to implement the `StateSigner` and `TransactionSigner` interfaces: + +```typescript +import { + Client, + ChannelDefaultSigner, + type StateSigner, + type TransactionSigner, + withBlockchainRPC, +} from '@yellow-org/sdk'; +import { createWalletClient, custom, type WalletClient } from 'viem'; +import { sepolia } from 'viem/chains'; + +// Adapt viem WalletClient to SDK's StateSigner (EIP-191 signatures) +class WalletStateSigner implements StateSigner { + constructor(private wc: WalletClient) {} + + async sign(payload: Uint8Array): Promise { + return this.wc.signMessage({ + account: this.wc.account!, + message: { raw: payload }, + }); + } + + getAddress(): string { + return this.wc.account!.address; + } +} + +// Adapt viem WalletClient to SDK's TransactionSigner +class WalletTransactionSigner implements TransactionSigner { + constructor(private wc: WalletClient) {} + + async sign(payload: Uint8Array): Promise { + return this.wc.signTypedData({ + account: this.wc.account!, + domain: { name: 'Nitrolite', version: '1', chainId: 1 }, + types: { Nitrolite: [{ name: 'operation', type: 'bytes' }] }, + primaryType: 'Nitrolite', + message: { operation: `0x${Buffer.from(payload).toString('hex')}` }, + }); + } + + getAddress(): string { + return this.wc.account!.address; + } +} + +async function connectWithWallet() { + const walletClient = createWalletClient({ + chain: sepolia, + transport: custom(window.ethereum!), + }); + + const [address] = await walletClient.requestAddresses(); + const stateSigner = new ChannelDefaultSigner(new WalletStateSigner(walletClient)); + const txSigner = new WalletTransactionSigner(walletClient); + + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(11155111n, 'https://rpc.sepolia.io'), + ); + + return client; +} +``` + +## Allowance Handling + +On-chain operations like `checkpoint()` require ERC-20 token approval. The example-app pattern detects allowance errors and retries after approval: + +```typescript +import { Client } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +const MAX_APPROVE = new Decimal('1000000000'); + +async function depositWithApproval( + client: Client, + chainId: bigint, + asset: string, + amount: Decimal, +) { + await client.deposit(chainId, asset, amount); + + try { + await client.checkpoint(asset); + } catch (error: any) { + const msg = error?.message ?? ''; + if (msg.includes('allowance') || msg.includes('insufficient')) { + await client.approveToken(chainId, asset, MAX_APPROVE); + await client.checkpoint(asset); + } else { + throw error; + } + } +} +``` + +## Channel Session Keys + +Session keys let users delegate signing authority so operations don't require repeated wallet prompts. From the example-app: + +```typescript +import { + Client, + ChannelSessionKeyStateSigner, + getChannelSessionKeyAuthMetadataHashV1, + type ChannelSessionKeyStateV1, +} from '@yellow-org/sdk'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; + +async function setupSessionKey(client: Client) { + const skPrivateKey = generatePrivateKey(); + const skAccount = privateKeyToAccount(skPrivateKey); + const userAddress = client.getUserAddress(); + + const assets = ['usdc']; + const expiresAt = String(Math.floor(Date.now() / 1000) + 86400); // 24h + + // Check existing version + const existing = await client.getLastChannelKeyStates(userAddress, skAccount.address); + const version = existing.length > 0 + ? String(Number(existing[0].version) + 1) + : '1'; + + const state: ChannelSessionKeyStateV1 = { + user_address: userAddress, + session_key: skAccount.address, + version, + assets, + expires_at: expiresAt, + user_sig: '0x', + }; + + // Sign and submit + const sig = await client.signChannelSessionKeyState(state); + state.user_sig = sig; + await client.submitChannelSessionKeyState(state); + + // Compute metadata hash for creating a session-key-based signer + const metadataHash = getChannelSessionKeyAuthMetadataHashV1( + BigInt(version), + assets, + BigInt(expiresAt), + ); + + // Rebuild client with session key signer (no more wallet prompts) + const sessionSigner = new ChannelSessionKeyStateSigner( + skPrivateKey, + userAddress as `0x${string}`, + metadataHash, + sig, + ); + + return { sessionSigner, skPrivateKey, metadataHash, authSig: sig }; +} +``` + +## Connection Monitoring + +```typescript +import { Client, createSigners, withErrorHandler } from '@yellow-org/sdk'; + +async function monitorConnection() { + const { stateSigner, txSigner } = createSigners(process.env.PRIVATE_KEY!); + + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withErrorHandler((error) => { + console.error('Connection error:', error); + }) + ); + + client.waitForClose().then(() => { + console.log('Connection closed, reconnecting...'); + }); + + const config = await client.getConfig(); + console.log('Connected to:', config.nodeAddress); + + await new Promise(resolve => setTimeout(resolve, 30000)); + await client.close(); +} +``` diff --git a/docs/build/sdk/typescript/getting-started.mdx b/docs/build/sdk/typescript/getting-started.mdx new file mode 100644 index 0000000..23b3758 --- /dev/null +++ b/docs/build/sdk/typescript/getting-started.mdx @@ -0,0 +1,184 @@ +--- +title: Getting Started +description: Install and set up the Yellow TypeScript SDK +sidebar_position: 1 +--- + +# Getting Started with @yellow-org/sdk + +The TypeScript SDK for Clearnode payment channels provides both high-level and low-level operations in a unified client. + +:::tip Migrating from v0.5.3? +If you are migrating from `@layer-3/nitrolite@v0.5.3`, consider using the [`@yellow-org/sdk-compat`](../typescript-compat/overview) package first. It maps the familiar v0.5.3 API to the v1.0.0 runtime with minimal code changes. +::: + +## Installation + +```bash +npm install @yellow-org/sdk +# or +yarn add @yellow-org/sdk +# or +pnpm add @yellow-org/sdk +``` + +## Requirements + +- **Node.js** 20.0.0 or later +- **TypeScript** 5.3.0 or later (for development) +- A running **Clearnode** instance or access to a public node +- A **blockchain RPC endpoint** for on-chain operations via `checkpoint()` + +## Quick Start + +```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; +import Decimal from 'decimal.js'; + +async function main() { + // 1. Create signers from private key + const { stateSigner, txSigner } = createSigners( + process.env.PRIVATE_KEY as `0x${string}` + ); + + // 2. Create unified client + const client = await Client.create( + 'wss://clearnode.example.com/ws', + stateSigner, + txSigner, + withBlockchainRPC(80002n, 'https://polygon-amoy.alchemy.com/v2/KEY') + ); + + try { + // 3. Build and co-sign deposit state off-chain + const state = await client.deposit(80002n, 'usdc', new Decimal(100)); + console.log('Deposit state version:', state.version); + + // 4. Settle on-chain via checkpoint + const txHash = await client.checkpoint('usdc'); + console.log('On-chain tx:', txHash); + + // 5. Transfer (off-chain only, no checkpoint needed) + const transferState = await client.transfer( + '0xRecipient...', + 'usdc', + new Decimal(50) + ); + + // 6. Low-level operations on the same client + const config = await client.getConfig(); + const balances = await client.getBalances(client.getUserAddress()); + } finally { + await client.close(); + } +} + +main().catch(console.error); +``` + +## Creating a Client + +The `Client` is the single entry point for all operations. + +```typescript +import { Client, createSigners, withBlockchainRPC } from '@yellow-org/sdk'; + +// Step 1: Create signers from private key +const { stateSigner, txSigner } = createSigners('0x1234...'); + +// Step 2: Create unified client +const client = await Client.create( + wsURL, + stateSigner, // For signing channel states + txSigner, // For signing blockchain transactions + withBlockchainRPC(chainId, rpcURL), // Required for checkpoint() + withHandshakeTimeout(10000), // Optional: connection timeout +); + +// Step 3: (Optional) Set home blockchain for assets +// Required for transfer() operations that may trigger channel creation +await client.setHomeBlockchain('usdc', 80002n); +``` + +## Signer Types + +The SDK provides two signer types: + +### EthereumMsgSigner (for channel states) + +Signs channel state updates with EIP-191 "Ethereum Signed Message" prefix. Used for all off-chain operations. + +```typescript +import { EthereumMsgSigner } from '@yellow-org/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; + +// From private key +const signer = new EthereumMsgSigner('0x...'); + +// From viem account +const account = privateKeyToAccount('0x...'); +const signer2 = new EthereumMsgSigner(account); +``` + +### EthereumRawSigner (for blockchain transactions) + +Signs raw hashes directly without prefix. Used for on-chain operations like deposits, withdrawals, and channel creation. + +```typescript +import { EthereumRawSigner } from '@yellow-org/sdk'; + +const signer = new EthereumRawSigner('0x...'); +``` + +### createSigners() Helper + +Creates both signers from a single private key: + +```typescript +import { createSigners } from '@yellow-org/sdk'; + +const { stateSigner, txSigner } = createSigners('0x...'); +``` + +## Key Concepts + +### Two-Step Pattern + +Payment channels use versioned states signed by both user and node: + +```typescript +// Step 1: Build and co-sign state off-chain +const state = await client.deposit(chainId, 'usdc', amount); + +// Step 2: Settle on-chain (when needed) +const txHash = await client.checkpoint('usdc'); +``` + +### Channel Lifecycle + +1. **Void** — No channel exists +2. **Create** — `deposit()` creates channel on-chain via `checkpoint()` +3. **Open** — Channel active; can deposit, withdraw, transfer +4. **Challenged** — Dispute initiated (advanced) +5. **Closed** — Channel finalized (advanced) + +### TypeScript Conventions + +```typescript +// Use bigint literals for chain IDs +const polygonAmoy = 80002n; + +// Use Decimal.js for amounts +import Decimal from 'decimal.js'; +const amount = new Decimal(100); + +// Viem Address type for wallet addresses +import type { Address } from 'viem'; +const wallet: Address = '0x1234...'; +``` + +## Next Steps + +- [API Reference](./api-reference) — Full method documentation +- [Configuration](./configuration) — Client options and error handling +- [Examples](./examples) — Complete working examples diff --git a/docs/learn/core-concepts/yellow-token.mdx b/docs/learn/core-concepts/yellow-token.mdx new file mode 100644 index 0000000..cee7a03 --- /dev/null +++ b/docs/learn/core-concepts/yellow-token.mdx @@ -0,0 +1,71 @@ +--- +title: YELLOW Token +description: Understanding the YELLOW utility token and its role in the Yellow Network +sidebar_position: 6 +--- + +# YELLOW Token + +YELLOW is the utility token that provides access to the goods and services supplied by Layer3 Fintech Ltd. within the Yellow Network. It has a fixed supply of **10 billion tokens** — no new tokens can ever be created, and there is no burn mechanism. + +## Token Functions + +### 1. Mandatory Security Deposit for Node Operators + +Every node operator must post YELLOW tokens as collateral to register on the network. This is the core mechanism that makes the network secure. + +- **Prevents spam attacks** — flooding the network with malicious nodes requires real collateral for each one. +- **Deters fraud** — if a node participates in a fraudulent transaction, its collateral is automatically seized ("slashed") through on-chain fraud proofs. +- **Scales with responsibility** — as a node guards higher-value accounts, the protocol requires more collateral at risk. + +The minimum collateral starts at 10,000 YELLOW and scales up to 125,000 YELLOW as the network grows. + +### 2. Service Access Fee + +All network services — clearing, settlement, data delivery, app subscriptions — require the consumption of YELLOW as a service access fee. Users who hold YELLOW pay fees directly at a discounted rate. + +Protocol fees from clearing and trading operations are locked into the collateral of the nodes that processed them, increasing those operators' slashing exposure and strengthening network security over time. + +### 3. Dispute Resolution Access + +App builders who register applications on the network post YELLOW as a service quality guarantee. Users who have disputes with an application pay a processing fee in YELLOW to access independent arbitration. If the dispute is upheld, the app builder's collateral can be slashed. + +## On-Chain Contract + +The `YellowToken` contract is a standard ERC-20 with EIP-2612 permit functionality. The entire 10 billion supply is minted to the Treasury at deployment. + +| Property | Value | +|----------|-------| +| Name | Yellow | +| Symbol | YELLOW | +| Supply | 10,000,000,000 (fixed) | +| Decimals | 18 | +| Permit | EIP-2612 gasless approvals | +| Mint/Burn | None — supply is immutable | + +## Token Allocation + +| Allocation | Percentage | Purpose | +|------------|-----------|---------| +| Founders | 10% | Subject to 6-month cliff and 60-month linear vesting | +| Token Sales | 12.5% | Distributed to participants who require YELLOW for service access | +| Community Treasury | 30% | Grants for app builders who consume YELLOW for services | +| Foundation Treasury | 20% | Funds ongoing R&D and delivery of Yellow Network services | +| Network Growth Incentives | 25% | Distributed automatically based on network scale | +| Ecosystem Accessibility Reserve | 2.5% | Ensures YELLOW remains accessible for its intended utility | + +## What YELLOW Is Not + +- It does not represent ownership in Layer3 Fintech Ltd. or any affiliated entity. +- It does not entitle holders to dividends, profit-sharing, or any form of financial return. +- It is not designed to maintain a stable value — there is no peg, no reserve backing, and no stabilisation mechanism. +- Holding YELLOW alone does not grant participation in protocol parameter administration — that requires actively operating a node. + +## Key Numbers + +| Metric | Value | +|--------|-------| +| Total supply | 10,000,000,000 (fixed) | +| Minimum node collateral | 10,000 YELLOW (scales to 125,000) | +| Collateral unlock period | 14 days | +| Fee range | 0.1% — 0.4% (dynamic) | diff --git a/docs/learn/index.mdx b/docs/learn/index.mdx index 89cc6d6..228dd13 100644 --- a/docs/learn/index.mdx +++ b/docs/learn/index.mdx @@ -52,6 +52,22 @@ Deep dive into the technology powering Yellow Network. **[Message Envelope](./core-concepts/message-envelope.mdx)** — Overview of the Nitro RPC message format and communication protocol. +**[YELLOW Token](./core-concepts/yellow-token.mdx)** — The utility token powering network services, node operator collateral, and dispute resolution. + +--- + +## Protocol Flows + +Detailed v1 protocol flow documentation for deposits, withdrawals, transfers, and app sessions. + +**[Protocol Architecture](./protocol-flows/architecture.mdx)** — The Petal Diagram: Home Chain, Cross-Chain, Transfers, and App Sessions. + +**[Transfer Flow](./protocol-flows/transfer-flow.mdx)** — Off-chain transfers between users via the Clearnode. + +**[Home Channel Flows](./protocol-flows/home-channel-creation.mdx)** — Channel creation, deposits, and withdrawals on the home chain. + +**[App Session Deposit](./protocol-flows/app-session-deposit.mdx)** — Depositing funds into app sessions. + --- ## Next Steps @@ -76,3 +92,6 @@ After completing the Learn section, continue to: | [Session Keys](./core-concepts/session-keys) | 8 min | Intermediate | | [Challenge-Response](./core-concepts/challenge-response) | 6 min | Intermediate | | [Message Envelope](./core-concepts/message-envelope) | 5 min | Intermediate | +| [YELLOW Token](./core-concepts/yellow-token) | 8 min | Intermediate | +| [Protocol Architecture](./protocol-flows/architecture) | 10 min | Intermediate | +| [Transfer Flow](./protocol-flows/transfer-flow) | 12 min | Advanced | diff --git a/docs/learn/protocol-flows/_category_.json b/docs/learn/protocol-flows/_category_.json new file mode 100644 index 0000000..33db8c1 --- /dev/null +++ b/docs/learn/protocol-flows/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Protocol Flows", + "position": 5, + "collapsed": false, + "collapsible": false +} diff --git a/docs/learn/protocol-flows/app-session-deposit.mdx b/docs/learn/protocol-flows/app-session-deposit.mdx new file mode 100644 index 0000000..c74e615 --- /dev/null +++ b/docs/learn/protocol-flows/app-session-deposit.mdx @@ -0,0 +1,315 @@ +--- +title: "App Session Deposit Flow" +description: "A comprehensive breakdown of the App Session Deposit flow in the Nitrolite v1.0 protocol, covering dual-state coordination, commit/release transitions, and validation." +sidebar_position: 3 +--- + +# App Session Deposit Flow + +This document provides a comprehensive breakdown of the **App Session Deposit** flow as defined in the Nitrolite v1.0 protocol. This operation allows a user to deposit funds from their **channel state** (Unified Balance) into an **existing App Session**, locking those funds for use within the application. + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> SC["SenderClient"] + SC <--> N["Node (Clearnode)"] + + style U stroke:#333 + style SC stroke:#333 + style N stroke:#333 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating the deposit | +| **SenderClient** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that validates and coordinates both app session and channel states | + +--- + +## Prerequisites + +Before the deposit flow begins: + +1. **SenderClient** is connected to the Node via WebSocket. +2. **Node** contains user's state with **Home Channel** information. +3. An **App Session** already exists (created via `create_app_session`). +4. User is a **participant** in the target App Session. + +--- + +## Key Data Structures + +### newAppState (App State Update) + +```yaml +app_session_id: appSessionId # Hex identifier of the app session +intent: deposit # Specifies this is a deposit operation +version: # Next version number +allocations: # Updated fund distribution + - participant: "0xUser1..." # User making the deposit + asset: "usdc" + amount: "150.0" # Amount after deposit (+50 added) + - participant: "0xUser2..." # Another participant in the session + asset: "usdc" + amount: "50.0" # Not affected by this deposit +session_data: "{...}" # JSON stringified session data +``` + +### Dual-State Coordination + +Unlike a simple transfer, App Session Deposit requires updating **two states** simultaneously: + +1. **App Session State** -- Shows increased allocations for the depositor. +2. **User Channel State** -- Shows funds committed (locked) via `commit` transition. + +--- + +## Phase 1: Deposit Initiation + +```mermaid +sequenceDiagram + actor User + actor SenderClient + + User->>SenderClient: submit_app_state(newAppState, sigQuorum) + Note over SenderClient: User initiates deposit request +``` + +The **User** calls `submit_app_state` on the **SenderClient** with: + +| Parameter | Description | +| --- | --- | +| `newAppState` | App state update with `intent: deposit` and new allocations | +| `sigQuorum` | Array of signatures meeting the quorum requirement | + +--- + +## Phase 2: Fetching Current App Session State + +```mermaid +sequenceDiagram + actor SenderClient + actor Node + + SenderClient->>Node: get_app_sessions(app_session_id) + Node->>SenderClient: Returns actual app session state
+``` + +1. **SenderClient** requests the current App Session state from the Node using `get_app_sessions` with the `app_session_id` filter. +2. The Node returns `` containing current version, allocations for all participants, and session data. + +--- + +## Phase 3: Client-Side Validation and User State Preparation + +```mermaid +sequenceDiagram + actor SenderClient + actor Node + + SenderClient-->>SenderClient: ValidateSessionAppState(
currentAppState, newAppState, sigQuorum) + Note right of SenderClient: intent=deposit
userWallet=appParticipant
only participant deposits
validate quorum + + SenderClient->>Node: GetLastState(UserWallet, asset) + Node->>SenderClient: Returns state with home chain +``` + +### Validation Step Details + +The client performs **ValidateSessionAppState** checking: + +| Check | Description | +| --- | --- | +| `intent = deposit` | Confirms this is a deposit operation | +| `userWallet = appParticipant` | User must be a participant in the session | +| `only participant deposits` | Only the depositing participant's allocation increases | +| `validate quorum` | Signatures meet the quorum threshold | + +### Building the User's New Channel State + +```mermaid +sequenceDiagram + actor SenderClient + + Note over SenderClient: createNextState(currentState) -> state + Note over SenderClient: state.setID(CalculateStateID(
state.userWallet, state.asset,
cycleId, state.version)) + Note over SenderClient: NewTransition(commit,
state.ID(), appSessionId, amount) + Note over SenderClient: state.applyTransitions(transitions) -> true + Note over SenderClient: signState(state) -> userSig +``` + +| Step | Operation | Description | +| --- | --- | --- | +| 1 | `createNextState(currentState)` | Create new state with incremented version | +| 2 | `state.setID(...)` | Calculate deterministic state ID | +| 3 | `NewTransition(commit, ...)` | Create **commit** transition linking to `appSessionId` | +| 4 | `applyTransitions(...)` | Apply transition, reducing user's available balance | +| 5 | `signState(state)` | User signs the new channel state | + +### The `commit` Transition + +The **commit** transition is used for locking funds into an App Session: + +| Field | Value | +| --- | --- | +| `type` | `commit` | +| `account_id` | `appSessionId` (the target app session) | +| `amount` | Amount being deposited | +| `tx_hash` | State ID reference | + +The `commit` transition locks funds from the user's Unified Balance. The reverse operation (`release`) unlocks funds when withdrawing from an app session. + +--- + +## Phase 4: Submitting to Node + +```mermaid +sequenceDiagram + actor SenderClient + actor Node + + SenderClient->>Node: SubmitDepositState(newAppState,
sigQuorum, state, userSig) + + Note over Node: Perform existing app session
state validation steps + Note over Node: GetLastState(userWallet, asset)
returns currentState + Note over Node: EnsureNoOngoingTransitions() + Note over Node: ValidateStateTransition(
currentState, state) + Note over Node: EnsureSameDepositTokenAmount(
newAppState, newUserState) + Note over Node: StoreState(state) +``` + +### API Method: `submit_deposit_state` + +| Parameter | Type | Description | +| --- | --- | --- | +| `app_state_update` | app_state_update | The app session state update | +| `quorum_sigs` | string[] | Signatures for quorum | +| `user_state` | state | User's new channel state with commit transition | + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | App session validation | Verify app state update is valid | +| 2 | `GetLastState(...)` | Fetch current user channel state | +| 3 | `EnsureNoOngoingTransitions()` | Prevent race conditions | +| 4 | `ValidateStateTransition(...)` | Verify state version and signatures | +| 5 | `EnsureSameDepositAssetAmount(...)` | Amount in app state matches commit transition | +| 6 | `StoreState(state)` | Persist both states | + +`EnsureSameDepositAssetAmount` is the key validation ensuring the user's channel state (commit amount) exactly matches the increase in their app session allocation. This prevents discrepancies between the two states. + +:::info Off-chain App Session Semantics +From the on-chain protocol: +- App sessions are off-chain sub-channels governed by an external server. +- Funds may be **locked** into a session (flow to Node), or **unlocked** from a session (flow to User). +- Only signatures are required for persistence. +- Session effects are netted into cumulative net flows of the next enforceable state. +::: + +--- + +## Phase 5: Notifications and Completion + +```mermaid +sequenceDiagram + actor Node + actor SenderClient + actor User + + Node->>SenderClient: Sends AppSessionUpdate + Node->>SenderClient: Return node signature + SenderClient->>User: Returns success and tx hash +``` + +### What Gets Returned + +1. **AppSessionUpdate** -- Notification of the updated app session state. +2. **Node signature** -- Confirms the Node has accepted both states. +3. **Success and tx hash** -- User-facing confirmation. + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor SenderClient + actor Node + + rect rgb(0, 0, 144) + Note over SenderClient: Phase 1: Initiation + User->>SenderClient: submit_app_state(newAppState, sigQuorum) + end + + rect rgb(0, 206, 0) + Note over SenderClient,Node: Phase 2: Fetch App Session + SenderClient->>Node: GetAppSessionState(app_session_id) + Node->>SenderClient: Returns currentAppState + end + + rect rgb(0, 0, 0) + Note over SenderClient: Phase 3: Validate and Build + SenderClient-->>SenderClient: ValidateSessionAppState(...) + SenderClient->>Node: GetLastState(UserWallet, asset) + Node->>SenderClient: Returns state with home chain + Note over SenderClient: Build new state with commit transition + end + + rect rgb(221, 0, 0) + Note over SenderClient,Node: Phase 4: Submit and Validate + SenderClient->>Node: SubmitDepositState(newAppState, sigQuorum, state, userSig) + Note over Node: Validate both states and store + end + + rect rgb(0, 0, 230) + Note over Node,User: Phase 5: Complete + Node->>SenderClient: AppSessionUpdate + node signature + SenderClient->>User: Returns success and tx hash + end +``` + +--- + +## Key Concepts Summary + +### Dual-State Coordination + +| State | What Changes | +| --- | --- | +| **App Session State** | `allocations` array increases for the depositor | +| **User Channel State** | `commit` transition locks funds from Unified Balance | + +### Transition Types for App Sessions + +| Transition | From / To | Purpose | +| --- | --- | --- | +| `commit` | Unified Balance to App Session | Lock funds for deposit | +| `release` | App Session to Unified Balance | Unlock funds on withdraw | + +### Why Two States? + +Having two different states (App Session State and User Channel State) creates an **atomicity challenge** -- if one state updates without the other, the system would be in an inconsistent state. + +This is solved by: + +1. **Single API endpoint** -- Both states are submitted together via `submit_deposit_state`, ensuring the Node processes them as a single atomic operation. +2. **Verifiable accounting** -- Channel state tracks fund flows, app session tracks allocations. +3. **Linked validation** -- `EnsureSameDepositAssetAmount` validates that both states reflect the same deposit amount before either is stored. + +:::info Note on Quorum +The `quorum_sigs` parameter must be assembled by the **application itself**. This means the application is responsible for collecting signatures from each participant (based on their signature weights) to meet the quorum threshold before submitting the deposit state to the Node. +::: + +--- + +## Related Flows + +- [Transfer Communication Flow](./transfer-flow) +- [Home Channel Creation Flow](./home-channel-creation) +- [Home Channel Withdrawal Flow](./home-channel-withdrawal) diff --git a/docs/learn/protocol-flows/architecture.mdx b/docs/learn/protocol-flows/architecture.mdx new file mode 100644 index 0000000..c21c1ed --- /dev/null +++ b/docs/learn/protocol-flows/architecture.mdx @@ -0,0 +1,105 @@ +--- +title: "Protocol Architecture (Petal Diagram)" +description: "A visual and technical breakdown of the Nitrolite Protocol architecture, covering the four core petals and three operational layers." +sidebar_position: 1 +--- + +# Protocol Architecture (Petal Diagram) + +The **Petal Diagram** is the central architectural reference for the Nitrolite Protocol. It maps how different actions -- Deposits, Withdrawals, Transfers, and App Sessions -- interact with various blockchains, all centered on a user's channel ("My Channel"). + +Nitrolite Protocol Petal Diagram + +*The diagram represents the architecture centered on a user's channel and maps how different actions interact with various blockchains.* + +--- + +## The Four Petals + +### Petal 1: My Home Chain (Deposits and Withdrawals) + +The **Home Chain** is the blockchain chosen by the user when they first create their channel or deposit a specific token. + +:::info Important Choice +When selecting your Home Chain, consider that this is the blockchain where your funds will be **enforced on-chain**. If you are unable to agree with the Node on a next state (e.g., in a dispute scenario), you can withdraw your funds on this specific blockchain. Choose a chain where you have easy access and are comfortable transacting. +::: + +**The Flow:** + +- **Deposit:** When a user deposits for the first time, that specific chain (e.g., Polygon) becomes the "Home Chain" for that token. +- **Optimization:** This process improves upon the previous version by **skipping the custody ledger step**, reducing one step and improving UX. +- **Mechanism:** Users transfer funds directly from their ERC-20 token balance via approvals, saving one transaction compared to the old protocol. +- **Withdrawal:** The withdrawal process follows the exact same direct logic as the deposit. + +**Key Benefit:** Direct fund transfers without intermediate custody steps result in reduced gas costs and improved UX. + +--- + +### Petal 2: Another Chain (Cross-Chain Support) + +When a user already has a Home Chain (e.g., Polygon) with funds and wants to deposit funds from a *different* chain (e.g., Linea or Base), the protocol uses a specialized bridge mechanism. + +**The Challenge:** Since the user's Home Chain is already defined, the protocol needs a different mechanism to handle deposits from a non-home chain. + +**The Solution -- The Bridge:** + +- The protocol implements a specialized bridge solution. +- Cross-chain deposits work similarly to cross-chain transfers, but with **atomic properties** -- ensuring the action either fully completes or doesn't happen at all. +- Atomicity is achieved not via smart contract infrastructure alone, but with the help of the participants. Both the User and the Node can see whether the process is failing on one chain and can cancel the process on another chain. + +**Key Property:** Atomic execution guarantees -- no partial states or stuck funds. + +--- + +### Petal 3: Transfers (Sender and Receiver State) + +Unlike state changes within a single channel, transfers between *two different users* are not strictly "atomic" because the two users have **unrelated states**. You cannot update the receiver's state on-chain immediately when the sender initiates the transfer. + +**The Solution -- Aggregated State Updates:** + +- **Sender Action:** The sender submits their new state (showing funds sent). +- **Verification:** If the Clearnode accepts this state as valid, it prepares a "pending state" for the Receiver. +- **Receiver Action (Aggregation):** If a receiver gets multiple transfers from different people, they do **not** need to sign a state update for every single transfer. +- **Efficiency:** The "receive" updates are **aggregated**. Only the Node signs the aggregated states; the Receiver doesn't need to. However, when such states may be aggregated with a "send", "lock", or "withdraw", then the Receiver needs to sign these new states. +- It is in the Receiver's security interest to own the latest state (to be able to challenge with it if the network goes down). The main challenge for the Receiver is to obtain the "receive" state signed by the Node if they are offline or not connected to the Node. +- In v1.0, not only the receiver but anyone can subscribe for state updates, enabling users to hire **User Watchtowers** that listen to "receive" state updates and store such states for the User. + +**Key Optimization:** Batch processing reduces signature overhead and improves throughput. + +--- + +### Petal 4: App Sessions (The "Virtual Layer") + +App Sessions function as a **Virtual Layer** within the protocol. + +**The Node Mechanism:** + +- When a user enters an App Session (locks funds), those funds move to the Node. +- These funds are technically locked on the Node's wallet and are **not directly enforced by the State Channel** during the session. + +**Security and Risk:** + +- If the Node misbehaves, the user can currently only recover funds that are *not* locked in an active session. + +:::note +This is a limitation in the current version of the protocol. Future work to improve App Session security through Node Watchtowers and cryptographic proofs is planned. +::: + +- To simplify security, the protocol restricts App Sessions to **one participant deposit of one token per session update**. + +**Future Validation -- Node Watchtowers:** + +- **Third-Party Node Observers (Node Watchtowers)** will validate actions on the Virtual Layer. +- If they detect misbehavior by the Node, they can intervene or slash, ensuring the system remains trustless. + +**UX Philosophy:** The Virtual Layer stays flexible (allowing flexible app sessions, settlements, and swaps), while the **Fundamental Layer** (Deposits/Withdrawals/Transfers) remains strictly enforced on-chain. + +--- + +## Protocol Layers + +| Layer | Description | Security Level | +| --- | --- | --- | +| **Fundamental Layer** | Deposits/Withdrawals (on-chain) and Transfers (off-chain) | Strictly enforced on-chain (High Security) | +| **Virtual Layer** | App Sessions and internal movements | Broker/Watchtower model (High Flexibility) | +| **Cross-Chain** | Managed via specialized bridge | Atomic swap properties | diff --git a/docs/learn/protocol-flows/escrow-deposit.mdx b/docs/learn/protocol-flows/escrow-deposit.mdx new file mode 100644 index 0000000..a4b8f8c --- /dev/null +++ b/docs/learn/protocol-flows/escrow-deposit.mdx @@ -0,0 +1,542 @@ +--- +title: "Escrow Channel Deposit Flow" +description: "A comprehensive breakdown of the Escrow Channel Deposit flow for cross-chain deposits via short-lived escrow channels in the Nitrolite v1.0 protocol." +sidebar_position: 8 +--- + +# Escrow Channel Deposit Flow + +This document provides a comprehensive breakdown of the **Escrow Channel Deposit** flow as defined in the Nitrolite v1.0 protocol. This operation allows a user to deposit funds from a **Non-Home Chain** (a blockchain different from where their home channel exists) into their unified balance through a **short-lived Escrow Channel**. + +This is a **cross-chain bridging operation** that uses a two-phase approach (Preparation + Execution) to move liquidity across blockchains without requiring atomic cross-chain verification. + +:::caution Cross-Chain Status +Cross-chain functionality is not yet fully implemented. While channels can be created on any chain with a Nitro deployment, cross-chain operations like escrow deposit and withdrawal are planned for shortly after launch. +::: + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> C["Client"] + C <--> N["Node (Clearnode)"] + N <--> HC["HomeChain"] + N <--> EC["EscrowChain"] + + style U stroke:#333 + style C stroke:#333 + style N stroke:#333 + style HC stroke:#4CAF50 + style EC stroke:#FF9800 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating the cross-chain deposit | +| **Client** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that validates, coordinates, and bridges state transitions | +| **HomeChain** | The blockchain where the user's home channel exists | +| **EscrowChain** | The non-home blockchain where the user is depositing funds from | + +--- + +## Prerequisites + +Before the escrow deposit flow begins: + +1. **User already has a home channel** on the HomeChain. +2. **Node** contains the user's state with Home Channel information. +3. **Client** is connected to the Node via WebSocket. + +This flow handles the "Another Chain" petal from the Nitrolite architecture diagram. When a user wants to deposit from a chain that is NOT their home chain, they cannot directly deposit -- instead, they use an escrow mechanism. + +--- + +## Key Concepts + +### What is an Escrow Entity? + +An **Escrow Entity** is a short-lived channel created on a non-home chain specifically for cross-chain deposits. It acts as a bridge (note: "entity" is used instead of "channel" to avoid confusion with the channel concept and lifecycle): + +- User locks funds on the **Escrow Chain** (non-home). +- Node provides equivalent liquidity on the **Home Chain**. +- **Happy case**: The escrow is finalized once User and Node sign the execution phase state and submit it on-chain. +- **Unhappy case**: Either party challenges the escrow if the opposite party decides not to continue cooperating. After challenge, escrow funds are distributed back. + +### Two-Phase Cross-Chain Operations + +Since one chain cannot directly observe or verify another chain's state, cross-chain actions are **two-phase** and **optimistic**: + +| Phase | Purpose | +| --- | --- | +| **Preparation Phase** | Lock liquidity on both chains, create escrow object | +| **Execution Phase** | Update allocations and net flows, finalize the operation | + +### Transition Types Used + +| Transition | Description | +| --- | --- | +| `mutual_lock` | Initial lock of funds preparing for cross-chain movement | +| `escrow_deposit` | Finalizes the escrow deposit, updating allocations | + +--- + +## Phase 1: Deposit Initiation + +```mermaid +sequenceDiagram + actor User + actor Client + + User->>Client: async deposit(blockchainId, asset, amount) + Note over Client: User initiates cross-chain deposit +``` + +The **User** calls the `deposit` function on the **Client** SDK with three parameters: + +| Parameter | Description | Example | +| --- | --- | --- | +| `blockchainId` | The blockchain ID where funds are coming FROM (non-home chain) | `59144` (Linea) | +| `asset` | The asset symbol to deposit | `usdc` | +| `amount` | The amount to deposit | `100.0` | + +--- + +## Phase 2: Fetching Current State + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns state +``` + +1. **Client** requests the **latest state** from the Node. +2. The Node looks up the state using `UserWallet` and `asset`. +3. The Node returns the current **state** object containing the **Home Channel** information. + +--- + +## Phase 3: Building the Preparation State (Mutual Lock) + +```mermaid +sequenceDiagram + actor Client + + Note over Client: createNextState(currentState) returns state + Note over Client: state.setID(CalculateStateID(state.userWallet,
state.asset, state.cycleId, state.version)) + Note over Client: GetTokenAddress(blockchainId, asset) + Note over Client: state.setEscrowToken(blockchainId, tokenAddress) + Note over Client: GetEscrowChannelID(homeChannelDef, state.version) + Note over Client: NewTransition(mutualLockT, state.ID(),
homeChannelID, amount) + Note over Client: state.applyTransitions(transitions) returns true + Note over Client: signState(state) returns userSig +``` + +### 3.1 Create Next State + +``` +createNextState(currentState) -> state +``` + +The Client creates a new state object based on the current state with an incremented version. + +### 3.2 Calculate State ID + +``` +state.setID(CalculateStateID(state.userWallet, state.asset, state.cycleId, state.version)) +``` + +The **State ID** is a deterministic hash computed from user wallet, asset, cycle, and version. + +### 3.3 Get Token Address for Escrow Chain + +``` +GetTokenAddress(blockchainId, asset) -> tokenAddress +``` + +The Client resolves the token contract address for the specified asset on the escrow (non-home) chain. + +### 3.4 Set Escrow Token + +``` +state.setEscrowToken(blockchainId, tokenAddress) +``` + +The state is updated to include the escrow chain's token information in the `escrow_ledger`. + +### 3.5 Get Escrow Channel ID + +``` +GetEscrowChannelID(homeChannelDef, state.version) -> escrowChannelID +``` + +A deterministic escrow channel ID is computed based on the home channel definition and current version. + +### 3.6 Create Mutual Lock Transition + +``` +NewTransition(mutual_lock, state.ID(), homeChannelID, amount) +``` + +The **mutual_lock** transition prepares funds for cross-chain movement: + +| Field | Value | +| --- | --- | +| `type` | `mutual_lock` | +| `tx_hash` | State ID reference | +| `account_id` | Home Channel ID | +| `amount` | Amount to lock | + +### 3.7 Apply and Sign + +``` +state.applyTransitions(transitions) -> true +signState(state) -> userSig +``` + +The transition is applied to the state and the user signs it. + +--- + +## Phase 4: Node Validates and Stores Escrow Channel + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset) returns currentState + Note right of Node: EnsureNoOngoingTransitions() + Note right of Node: ValidateStateTransition(currentState, state) + Note right of Node: StoreEscrowChannel(escrow_channel) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | `GetLastState(...)` | Fetch current user state | +| 2 | `EnsureNoOngoingTransitions()` | Block other operations during escrow | +| 3 | `ValidateStateTransition(...)` | Verify version, signatures, balances | +| 4 | `StoreEscrowChannel(...)` | Create escrow channel record | +| 5 | `StoreState(state)` | Persist the new state | + +:::warning Atomic Operations +Once an escrow deposit starts with `mutual_lock`, **the Node stops issuing new states** until `escrow_deposit` finalizes. This ensures atomicity of cross-chain operations. +::: + +--- + +## Phase 5: On-Chain Escrow Initiation + +```mermaid +sequenceDiagram + actor Client + actor EscrowChain + actor Node + + Note over Client: PackChannelDefinition(channelDef) + Note over Client: PackState(channelId, state) + Client->>EscrowChain: initiateEscrowDeposit(packedChannelDef, packedState) + EscrowChain->>Client: Return Tx Hash + EscrowChain-->>Node: Emits EscrowDepositInitiated Event + Note right of Node: HandleEscrowDepositInitiated() + Note right of Node: UpdateEscrowChannel(escrow_channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate +``` + +### 5.1 Pack Channel Definition and State + +``` +PackChannelDefinition(channelDef) -> packedChannelDef +PackState(channelId, state) -> packedState +``` + +The Client serializes the channel definition and state for on-chain submission. + +### 5.2 On-Chain Transaction + +``` +initiateEscrowDeposit(packedChannelDef, packedState) +``` + +The Client submits a transaction to the **EscrowChain** smart contract, which: + +- Locks the user's funds on the escrow chain +- Creates an escrow object with a timeout +- Emits `EscrowDepositInitiated` event + +### 5.3 Node Event Handling + +The Node listens for blockchain events and: + +1. **HandleEscrowDepositInitiated** -- Processes the event. +2. **UpdateEscrowChannel** -- Updates the escrow channel status. +3. Sends **ChannelUpdate** and **BalanceUpdate** notifications to the Client. + +--- + +## Phase 6: Home Chain Escrow Initiation + +```mermaid +sequenceDiagram + actor Node + actor HomeChain + actor Client + + Node->>HomeChain: initiateEscrowDeposit(homeChannelId, packedState) + HomeChain-->>Node: Emits EscrowDepositInitiatedOnHome Event + Node-->>Node: HandleEscrowDepositInitiatedOnHome() + Node-->>Client: Sends ChannelUpdate and BalanceUpdate +``` + +The **Node** initiates escrow deposit on the **Home Chain**: + +1. Submits `initiateEscrowDeposit(homeChannelId, packedState)` to Home Chain contract. +2. Home Chain emits `EscrowDepositInitiatedOnHome` event. +3. Node handles the event internally. +4. Sends updated notifications to Client. + +The initiation on the home chain ensures that the Node's liquidity commitment is recorded on-chain, providing security guarantees for the cross-chain operation. + +--- + +## Phase 7: Building the Execution State (Escrow Deposit) + +This phase starts when the Client sees the `EscrowDepositInitiatedOnHome` event on the Home Chain. + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns state + Note over Client: createNextState(currentState) returns state + Note over Client: state.setID(CalculateStateID(state.userWallet,
state.asset, state.cycleId, state.version)) + Note over Client: NewTransition(escrow_depositT, state.ID(),
homeChannelID, amount) + Note over Client: state.applyTransitions(transitions) returns true + Note over Client: signState(state) returns userSig +``` + +### 7.1 Fetch Updated State + +The Client fetches the latest state which now reflects the escrowed funds. + +### 7.2 Create Escrow Deposit Transition + +``` +NewTransition(escrow_deposit, state.ID(), homeChannelID, amount) +``` + +The **escrow_deposit** transition finalizes the cross-chain deposit: + +| Field | Value | +| --- | --- | +| `type` | `escrow_deposit` | +| `tx_hash` | State ID reference | +| `account_id` | Home Channel ID | +| `amount` | Deposited amount | + +--- + +## Phase 8: Submitting Execution State + +```mermaid +sequenceDiagram + actor Client + actor Node + actor User + + Client->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset) returns currentState + Note right of Node: EnsureNoOngoingTransitions() + Note right of Node: ValidateStateTransition(currentState, state) + Note right of Node: StoreState(state) + Node->>Client: Return node signature + + Client-->>User: Returns success +``` + +1. Client submits the execution state with `escrow_deposit` transition. +2. Node validates and stores the state. +3. Client returns success to the User. + +At this point, the user's unified balance is updated to reflect the deposited funds. The escrow mechanism has effectively "bridged" the funds from the non-home chain. + +--- + +## Phase 9: Escrow Finalization (Automatic or Fast Unlock) + +```mermaid +sequenceDiagram + actor Node + actor EscrowChain + actor Client + + Note over Node: PackState(channelId, state) + Node->>EscrowChain: finalizeEscrowDeposit(escrowChannelId, packedState) + EscrowChain->>Node: Return Tx Hash + EscrowChain-->>Node: Emits EscrowDepositFinalized Event + Note right of Node: HandleEscrowDepositFinalized() + Note right of Node: UpdateEscrowChannel(escrow_channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate +``` + +### Two Unlock Options + +These are **Node funds** that require an unlock: + +| Option | Description | +| --- | --- | +| **Automatic Release** | Escrowed funds are released after the lock period expires | +| **Fast Unlock** | Node calls `FinalizeEscrowDeposit` on escrow chain to release funds immediately | + +:::warning +"Automatic" unlock means the funds **will be released eventually after the `unlockAt` timestamp is reached**, not exactly when the timestamp is reached. Each on-chain action checks whether Node's funds can be unlocked, and if so, the unlock is performed. There is also a manual method `purgeEscrowDeposits(maxToPurge)` for explicit cleanup. +::: + +### Finalization Steps + +1. **PackState** -- Node prepares the final state. +2. **FinalizeEscrowDeposit** -- Submits to Escrow Chain contract. +3. **EscrowDepositFinalized** event emitted. +4. Node updates internal state and notifies Client. + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor Client + actor Node + actor HomeChain + actor EscrowChain + + rect rgb(40, 40, 100) + Note over User,Client: Phase 1: Initiation + User->>Client: async deposit(blockchainId, asset, amount) + end + + rect rgb(40, 100, 40) + Note over Client,Node: Phase 2-4: State Preparation + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state + Note over Client: Build mutual_lock state + Client->>Node: SubmitState(state, userSig) + Node->>Client: Return node signature + end + + rect rgb(150, 100, 40) + Note over Client,EscrowChain: Phase 5: On-Chain Escrow + Client->>EscrowChain: initiateEscrowDeposit(...) + EscrowChain-->>Node: EscrowDepositInitiated Event + end + + rect rgb(40, 100, 100) + Note over Node,HomeChain: Phase 6: Home Chain Checkpoint + Node->>HomeChain: initiateEscrowDeposit(homeChannelId, packedState) + HomeChain-->>Node: EscrowDepositInitiatedOnHome Event + end + + rect rgb(100, 40, 100) + Note over Client,Node: Phase 7-8: Execution State + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state + Note over Client: Build escrow_deposit state + Client->>Node: SubmitState(state, userSig) + Node->>Client: Return node signature + Client-->>User: Returns success + end + + rect rgb(100, 100, 40) + Note over Node,EscrowChain: Phase 9: Finalization + Node->>EscrowChain: finalizeEscrowDeposit(...) + EscrowChain-->>Node: EscrowDepositFinalized Event + Node-->>Client: ChannelUpdate and BalanceUpdate + end +``` + +--- + +## Key Concepts Summary + +### State Transitions Overview + +```mermaid +flowchart LR + subgraph Preparation["Preparation Phase"] + A["Current State"] --> B["mutual_lock Transition"] + B --> C["Escrowed State"] + end + subgraph Execution["Execution Phase"] + C --> D["escrow_deposit Transition"] + D --> E["Final State"] + end + + style Preparation fill:#1a1a2e,stroke:#16213e + style Execution fill:#0f3460,stroke:#16213e +``` + +### On-Chain vs Off-Chain Actions + +| Action | Chain | Purpose | +| --- | --- | --- | +| `SubmitState` (mutual_lock) | Off-chain (Node) | Prepare escrow | +| `initiateEscrowDeposit` | **On-chain (Escrow)** | Lock funds on non-home chain | +| `initiateEscrowDeposit` | **On-chain (Home)** | Record state on home chain | +| `SubmitState` (escrow_deposit) | Off-chain (Node) | Execute escrow | +| `finalizeEscrowDeposit` | **On-chain (Escrow)** | Release locked funds | + +### Security Guarantees + +From the on-chain protocol: + +- **Preparation phase**: User locks funds on the non-home chain. Node locks equal liquidity on the home chain. An escrow object with timeouts is created. +- **Execution phase**: A signed execution state updates allocations and net flows. +- **Recoverability**: Every escrow phase must be completable or revertible via timeout and challenge on at least one chain. + +--- + +## Error Recovery + +### What if the process stalls? + +| Scenario | Recovery | +| --- | --- | +| Node doesn't respond | User can challenge with the last signed state | +| On-chain transaction fails | Retry or wait for timeout to revert | +| Network issues | Escrowed funds released automatically after lock period | + +### Challenge Mechanism + +If an escrow process is challenged and the challenge period expires without resolution, the finalize function: + +1. Does not invoke the channel engine. +2. Manually unlocks the locked funds to the Node. +3. Sets status to `FINALIZED`. + +:::warning +If an escrow was challenged, then the on-chain channel **must also be challenged and closed**. It is not possible to continue operating a channel after any related escrow was challenged. +::: + +--- + +## Related Flows + +- [Transfer Communication Flow](./transfer-flow) +- [App Session Deposit Flow](./app-session-deposit) +- [Home Channel Deposit Flow](./home-channel-deposit) +- [Escrow Channel Withdrawal Flow](./escrow-withdrawal) diff --git a/docs/learn/protocol-flows/escrow-withdrawal.mdx b/docs/learn/protocol-flows/escrow-withdrawal.mdx new file mode 100644 index 0000000..e740011 --- /dev/null +++ b/docs/learn/protocol-flows/escrow-withdrawal.mdx @@ -0,0 +1,517 @@ +--- +title: "Escrow Channel Withdrawal Flow" +description: "A comprehensive breakdown of the Escrow Channel Withdrawal flow for cross-chain withdrawals via short-lived escrow channels in the Nitrolite v1.0 protocol." +sidebar_position: 9 +--- + +# Escrow Channel Withdrawal Flow + +This document provides a comprehensive breakdown of the **Escrow Channel Withdrawal** flow as defined in the Nitrolite v1.0 protocol. This operation allows a user to withdraw funds from their **unified balance** to a **Non-Home Chain** (a blockchain different from where their home channel exists) through a **short-lived Escrow Channel**. + +This is the reverse operation of Escrow Channel Deposit -- it's a **cross-chain bridging out** operation that uses a two-phase approach (Preparation + Execution) to move liquidity from the home chain to a different blockchain. + +:::caution Cross-Chain Status +Cross-chain functionality is not yet fully implemented. While channels can be created on any chain with a Nitro deployment, cross-chain operations like escrow deposit and withdrawal are planned for shortly after launch. +::: + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> C["Client"] + C <--> N["Node (Clearnode)"] + N <--> HC["HomeChain"] + N <--> EC["EscrowChain"] + + style U stroke:#333 + style C stroke:#333 + style N stroke:#333 + style HC stroke:#4CAF50 + style EC stroke:#FF9800 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating the cross-chain withdrawal | +| **Client** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that validates, coordinates, and bridges state transitions | +| **HomeChain** | The blockchain where the user's home channel exists (funds originate here) | +| **EscrowChain** | The non-home blockchain where the user wants to receive funds | + +--- + +## Prerequisites + +Before the escrow withdrawal flow begins: + +1. **User already has a home channel** on the HomeChain. +2. **Node** contains the user's state with Home Channel information. +3. **Client** is connected to the Node via WebSocket. +4. **User has sufficient balance** in their unified balance to withdraw. + +This flow handles the "bridging out" scenario. When a user wants to receive funds on a chain that is NOT their home chain, they use the escrow withdrawal mechanism where the Node locks liquidity on the target chain. + +--- + +## Key Concepts + +### Escrow Withdrawal vs Escrow Deposit + +| Operation | Direction | User Action | Node Action | +| --- | --- | --- | --- | +| **Escrow Deposit** | Non-Home to Home | User locks funds on escrow chain | Node provides liquidity on home chain | +| **Escrow Withdrawal** | Home to Non-Home | User locks funds on home chain | Node provides liquidity on escrow chain | + +### Transition Types Used + +| Transition | Description | +| --- | --- | +| `escrow_lock` | Lock funds from unified balance, preparing for withdrawal | +| `escrow_withdraw` | Finalize the withdrawal, releasing funds on escrow chain | + +--- + +## Phase 1: Withdrawal Initiation + +```mermaid +sequenceDiagram + actor User + actor Client + + User->>Client: async withdraw(blockchainId, asset, amount) + Note over Client: User initiates cross-chain withdrawal +``` + +The **User** calls the `withdraw` function on the **Client** SDK with three parameters: + +| Parameter | Description | Example | +| --- | --- | --- | +| `blockchainId` | The blockchain ID where funds should be received (non-home chain) | `59144` (Linea) | +| `asset` | The asset symbol to withdraw | `usdc` | +| `amount` | The amount to withdraw | `100.0` | + +--- + +## Phase 2: Fetching Current State + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns state +``` + +1. **Client** requests the **latest state** from the Node. +2. The Node looks up the state using `UserWallet` and `asset`. +3. The Node returns the current **state** object containing the **Home Channel** information. + +--- + +## Phase 3: Building the Preparation State (Escrow Lock) + +```mermaid +sequenceDiagram + actor Client + + Note over Client: createNextState(currentState) returns state + Note over Client: state.setID(CalculateStateID(state.userWallet,
state.asset, state.cycleId, state.version)) + Note over Client: GetTokenAddress(blockchainId, asset) + Note over Client: state.setEscrowToken(blockchainId, tokenAddress) + Note over Client: GetEscrowChannelID(homeChannelDef, state.version) + Note over Client: NewTransition(escrowLockT, state.ID(),
escrowChannelID, amount) + Note over Client: state.applyTransitions(transitions) returns true + Note over Client: signState(state) returns userSig +``` + +### 3.1 Create Next State + +``` +createNextState(currentState) -> state +``` + +The Client creates a new state object based on the current state with an incremented version. + +### 3.2 Calculate State ID + +``` +state.setID(CalculateStateID(state.userWallet, state.asset, state.cycleId, state.version)) +``` + +The **State ID** is a deterministic hash computed from user wallet, asset, cycle, and version. + +### 3.3 Get Token Address for Escrow Chain + +``` +GetTokenAddress(blockchainId, asset) -> tokenAddress +``` + +The Client resolves the token contract address for the specified asset on the escrow (non-home) chain. + +### 3.4 Set Escrow Token + +``` +state.setEscrowToken(blockchainId, tokenAddress) +``` + +The state is updated to include the escrow chain's token information in the `escrow_ledger`. + +### 3.5 Get Escrow Channel ID + +``` +GetEscrowChannelID(homeChannelDef, state.version) -> escrowChannelID +``` + +A deterministic escrow channel ID is computed based on the home channel definition and current version. + +### 3.6 Create Escrow Lock Transition + +``` +NewTransition(escrow_lock, state.ID(), escrowChannelID, amount) +``` + +The **escrow_lock** transition locks funds from the user's unified balance: + +| Field | Value | +| --- | --- | +| `type` | `escrow_lock` | +| `tx_hash` | State ID reference | +| `account_id` | Escrow Channel ID | +| `amount` | Amount to lock | + +### 3.7 Apply and Sign + +``` +state.applyTransitions(transitions) -> true +signState(state) -> userSig +``` + +The transition is applied to the state and the user signs it. + +--- + +## Phase 4: Node Validates and Stores Escrow Channel + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset) returns currentState + Note right of Node: EnsureNoOngoingTransitions() + Note right of Node: ValidateStateTransition(currentState, state) + Note right of Node: StoreEscrowChannel(escrow_channel) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | `GetLastState(...)` | Fetch current user state | +| 2 | `EnsureNoOngoingTransitions()` | Block other operations during escrow | +| 3 | `ValidateStateTransition(...)` | Verify version, signatures, balances | +| 4 | `StoreEscrowChannel(...)` | Create escrow channel record | +| 5 | `StoreState(state)` | Persist the new state | + +:::warning Atomic Operations +Once an escrow withdrawal starts with `escrow_lock`, **the Node stops issuing new states** until `escrow_withdraw` finalizes. This ensures atomicity of cross-chain operations. +::: + +--- + +## Phase 5: On-Chain Escrow Initiation + +```mermaid +sequenceDiagram + actor Client + actor Node + actor EscrowChain + + Note over Client: PackChannelDefinition(channelDef) + Note over Client: PackState(channelId, state) + Client->>EscrowChain: initiateEscrowWithdrawal(packedChannelDef, packedState) + EscrowChain->>Client: Return Tx Hash + EscrowChain-->>Node: Emits EscrowWithdrawalInitiated Event + Note right of Node: HandleEscrowWithdrawalInitiated() + Note right of Node: UpdateEscrowChannel(escrow_channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate +``` + +### 5.1 Pack Channel Definition and State + +``` +PackChannelDefinition(channelDef) -> packedChannelDef +PackState(channelId, state) -> packedState +``` + +The Client serializes the channel definition and state for on-chain submission. + +### 5.2 Client On-Chain Transaction + +``` +initiateEscrowWithdrawal(packedChannelDef, packedState) +``` + +The **Client** submits a transaction to the **EscrowChain** smart contract, which: + +- Locks the Node's liquidity on the escrow chain +- Creates an escrow object with timeouts +- Emits `EscrowWithdrawalInitiated` event + +:::warning Security Measure +In cross-chain operations (escrow deposit, escrow withdrawal, channel migration), the **first on-chain transaction is always submitted by the User/Client**. This guards against DOS attacks where a user would initiate an action, the Node would need to perform a transaction, and the user disappears. +::: + +### 5.3 Node Event Handling + +The Node listens for blockchain events and: + +1. **HandleEscrowWithdrawalInitiated** -- Processes the event. +2. **UpdateEscrowChannel** -- Updates the escrow channel status. +3. Sends **ChannelUpdate** and **BalanceUpdate** notifications to the Client. + +--- + +## Phase 6: Building the Execution State (Escrow Withdrawal) + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns state + Note over Client: createNextState(currentState) returns state + Note over Client: state.setID(CalculateStateID(state.userWallet,
state.asset, state.cycleId, state.version)) + Note over Client: NewTransition(escrow_withdrawalT, state.ID(),
escrowChannelID, amount) + Note over Client: state.applyTransitions(transitions) returns true + Note over Client: signState(state) returns userSig +``` + +### 6.1 Fetch Updated State + +The Client fetches the latest state which now reflects the locked funds. + +### 6.2 Create Escrow Withdrawal Transition + +``` +NewTransition(escrow_withdraw, state.ID(), escrowChannelID, amount) +``` + +The **escrow_withdraw** transition finalizes the cross-chain withdrawal: + +| Field | Value | +| --- | --- | +| `type` | `escrow_withdraw` | +| `tx_hash` | State ID reference | +| `account_id` | Escrow Channel ID | +| `amount` | Withdrawn amount | + +--- + +## Phase 7: Submitting Execution State + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset) returns currentState + Note right of Node: ValidateStateTransition(currentState, state) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +1. Client submits the execution state with `escrow_withdraw` transition. +2. Node validates and stores the state. +3. Node returns its signature confirming acceptance. + +--- + +## Phase 8: Escrow Finalization + +```mermaid +sequenceDiagram + actor Client + actor Node + actor EscrowChain + + Note over Node: PackState(channelId, state) + Client->>EscrowChain: finalizeEscrowWithdrawal(escrowChannelId, packedState) + EscrowChain->>Client: Return Tx Hash + EscrowChain-->>Node: Emits EscrowWithdrawalFinalized Event + Note right of Node: HandleEscrowWithdrawalFinalized() + Note right of Node: UpdateEscrowChannel(escrow_channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate +``` + +### 8.1 Pack Final State + +``` +PackState(channelId, state) -> packedState +``` + +The Node prepares the final state for on-chain submission. + +### 8.2 Client Finalizes On-Chain + +``` +finalizeEscrowWithdrawal(escrowChannelId, packedState) +``` + +The **Client** submits a transaction to finalize the withdrawal: + +- Releases the Node's locked funds to the User on the escrow chain. +- User receives funds on the non-home chain. +- Emits `EscrowWithdrawalFinalized` event. + +At this point, the user's wallet on the EscrowChain receives the withdrawn funds. + +### 8.3 Node Event Handling + +The Node: + +1. **HandleEscrowWithdrawalFinalized** -- Processes the event. +2. **UpdateEscrowChannel** -- Marks escrow as completed. +3. Sends final **ChannelUpdate** and **BalanceUpdate** notifications. + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor Client + actor Node + actor HomeChain + actor EscrowChain + + rect rgb(40, 40, 100) + Note over User,Client: Phase 1: Initiation + User->>Client: async withdraw(blockchainId, asset, amount) + end + + rect rgb(40, 100, 40) + Note over Client,Node: Phase 2-4: State Preparation + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state + Note over Client: Build escrow_lock state + Client->>Node: SubmitState(state, userSig) + Node->>Client: Return node signature + end + + rect rgb(150, 100, 40) + Note over Client,EscrowChain: Phase 5: On-Chain Escrow + Client->>EscrowChain: initiateEscrowWithdrawal(...) + EscrowChain-->>Node: EscrowWithdrawalInitiated Event + Node-->>Client: ChannelUpdate and BalanceUpdate + end + + rect rgb(100, 40, 100) + Note over Client,Node: Phase 6-7: Execution State + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state + Note over Client: Build escrow_withdraw state + Client->>Node: SubmitState(state, userSig) + Node->>Client: Return node signature + end + + rect rgb(100, 100, 40) + Note over Client,EscrowChain: Phase 8: Finalization + Client->>EscrowChain: finalizeEscrowWithdrawal(...) + EscrowChain-->>Node: EscrowWithdrawalFinalized Event + Node-->>Client: ChannelUpdate and BalanceUpdate + end +``` + +--- + +## Key Concepts Summary + +### State Transitions Overview + +```mermaid +flowchart LR + subgraph Preparation["Preparation Phase"] + A["Current State"] --> B["escrow_lock Transition"] + B --> C["Locked State"] + end + subgraph Execution["Execution Phase"] + C --> D["escrow_withdraw Transition"] + D --> E["Final State"] + end + + style Preparation fill:#1a1a2e,stroke:#16213e + style Execution fill:#0f3460,stroke:#16213e +``` + +### Comparison: Escrow Deposit vs Withdrawal + +| Aspect | Escrow Deposit | Escrow Withdrawal | +| --- | --- | --- | +| **Direction** | Non-Home to Home | Home to Non-Home | +| **Preparation Transition** | `mutual_lock` | `escrow_lock` | +| **Execution Transition** | `escrow_deposit` | `escrow_withdraw` | +| **Who initiates on-chain?** | Client (initiateEscrowDeposit) | Client (initiateEscrowWithdrawal) | +| **Who finalizes on-chain?** | Node (finalizeEscrowDeposit) | Client (finalizeEscrowWithdrawal) | +| **Who provides liquidity?** | Node on home chain | Node on escrow chain | + +### On-Chain vs Off-Chain Actions + +| Action | Chain | Who | Purpose | +| --- | --- | --- | --- | +| `SubmitState` (escrow_lock) | Off-chain (Node) | Client | Lock funds in preparation | +| `initiateEscrowWithdrawal` | **On-chain (Escrow)** | Client | Lock Node's liquidity | +| `SubmitState` (escrow_withdraw) | Off-chain (Node) | Client | Execute withdrawal | +| `finalizeEscrowWithdrawal` | **On-chain (Escrow)** | Client | Release funds to User | + +### Security Guarantees + +From the on-chain protocol: + +- **Preparation phase**: Node locks withdrawal liquidity on the non-home chain. +- **Execution phase**: Signed state updates allocations and net flows so that User receives funds on the non-home chain. +- If enforcement stalls, challenges and timeouts guarantee completion or reversion. + +--- + +## Error Recovery + +### What if the process stalls? + +| Scenario | Recovery | +| --- | --- | +| Node doesn't respond | User can challenge with the last signed state | +| On-chain transaction fails | Retry or wait for timeout to revert | +| Network issues | User can challenge or wait for timeout-based recovery | + +### Challenge Resolution + +If an escrow process is challenged and the challenge period expires without resolution: + +1. The finalize function handles this explicitly. +2. Manually unlocks the locked funds to the Node. +3. Sets status to `FINALIZED`. + +:::warning +If an escrow was challenged, then the on-chain channel **must also be challenged and closed**. It is not possible to continue operating a channel after any related escrow was challenged. +::: + +--- + +## Related Flows + +- [Transfer Communication Flow](./transfer-flow) +- [App Session Deposit Flow](./app-session-deposit) +- [Escrow Channel Deposit Flow](./escrow-deposit) +- [Home Channel Withdrawal Flow](./home-channel-withdrawal) diff --git a/docs/learn/protocol-flows/home-channel-creation.mdx b/docs/learn/protocol-flows/home-channel-creation.mdx new file mode 100644 index 0000000..bfcde3c --- /dev/null +++ b/docs/learn/protocol-flows/home-channel-creation.mdx @@ -0,0 +1,465 @@ +--- +title: "Home Channel Creation Flow" +description: "A comprehensive breakdown of the Home Channel Creation From Scratch flow, the initial onboarding process for new users in the Nitrolite v1.0 protocol." +sidebar_position: 4 +--- + +# Home Channel Creation Flow + +This document provides a comprehensive breakdown of the **Home Channel Creation From Scratch** flow as defined in the Nitrolite v1.0 protocol. This is the **initial onboarding flow** for new users -- it creates their first channel (Home Channel) on a blockchain and establishes their initial state with the Node. + +This flow is triggered when a user attempts to deposit for the first time and has no existing state in the system. + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> C["Client"] + C <--> N["Node (Clearnode)"] + C --> HC["HomeChain"] + HC -.-> N + + style U stroke:#333 + style C stroke:#333 + style N stroke:#333 + style HC stroke:#4CAF50 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating their first deposit | +| **Client** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that validates, stores, and coordinates state transitions | +| **HomeChain** | The blockchain where the user's home channel will be created | + +--- + +## Prerequisites + +Before the home channel creation flow begins: + +1. **Client** is connected to the Node via WebSocket. +2. **User has no existing state** in the system (first-time user). +3. **User has funds** on the target blockchain to deposit. + +This flow handles the "My Home Chain" petal from the Nitrolite architecture. The first blockchain a user deposits to becomes their "Home Chain" for that specific token. + +--- + +## Key Concepts + +### What is a Home Channel? + +The **Home Channel** is the primary channel between a User and Node on a specific blockchain. + +| Aspect | Description | +| --- | --- | +| **Definition** | Created when user first deposits a specific token | +| **Location** | Lives on the "Home Chain" for that token | +| **Security** | Provides on-chain enforcement guarantees | +| **Persistence** | Long-lived, unlike short-lived escrow channels | + +### Channel Definition + +A channel is uniquely identified by its definition: + +| Field | Description | +| --- | --- | +| `nonce` | Unique number to prevent replay attacks | +| `challengeDuration` | Challenge period for disputes (in seconds) | +| `user` | User wallet address | +| `node` | Node wallet address | +| `metadata` | A `bytes32` variable to contain any additional value | + +--- + +## Phase 1: Deposit Initiation + +```mermaid +sequenceDiagram + actor User + actor Client + + User->>Client: deposit(blockchainId, asset, amount) + Note over Client: User initiates first deposit +``` + +The **User** calls the `deposit` function on the **Client** SDK with three parameters: + +| Parameter | Description | Example | +| --- | --- | --- | +| `blockchainId` | The blockchain ID to deposit on | `137` (Polygon) | +| `asset` | The asset symbol to deposit | `usdc` | +| `amount` | The amount to deposit | `100.0` | + +--- + +## Phase 2: Checking for Existing State + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset)
returns nil + Node->>Client: Returns "channel_not_found" error +``` + +1. **Client** requests the latest state from the Node. +2. **Node** looks up the state for the user wallet and asset. +3. Since this is a new user, **no state exists** -- Node returns a `channel_not_found` error. +4. The Client checks the error type to decide whether to request channel creation or just submit state. + +:::info Branch Point +If a state exists, the flow would continue as a regular `home_deposit`. The `channel_not_found` error triggers the channel creation flow instead. +::: + +--- + +## Phase 3: Building the Initial State + +```mermaid +sequenceDiagram + actor Client + + Note over Client: newChannelDefinition() + Note over Client: newEmptyState(asset) returns state + Note over Client: GetTokenAddress(blockchainId, asset) + Note over Client: state.setHomeToken(blockchainId, tokenAddress) + Note over Client: state.ApplyChannelCreation(channelDef,
blockchainId, tokenAddress, nodeAddress) + Note over Client: state.ApplyHomeDepositTransition(amount) + Note over Client: signState(state) returns userSig +``` + +### 3.1 Create Channel Definition + +``` +newChannelDefinition() -> channelDef +``` + +Creates a new channel definition with: + +| Field | Value | +| --- | --- | +| `nonce` | Unique number (can be random, derived from timestamp, or an increment from the previous nonce) | +| `challenge` | Default challenge period (e.g., 86400 seconds = 24 hours) | +| `user` | User wallet address | +| `node` | Node wallet address | +| `metadata` | A `bytes32` variable to contain any additional value | + +### 3.2 Create Empty State + +``` +newEmptyState(asset) -> state +``` + +Creates an initial state object: + +| Field | Value | +| --- | --- | +| `version` | 1 (initial version) | +| `asset` | The asset being deposited | +| `user_wallet` | User's wallet address | +| `epoch` | 0 (initial epoch) | +| `home_ledger` | Empty ledger (zero balances) | + +### 3.3 Apply Channel Creation + +``` +state.ApplyChannelCreation(channelDef, blockchainId, tokenAddress, nodeAddress) +``` + +Sets up the state with the channel definition, token information, and node address. This also computes and sets the State ID internally. + +### 3.4 Set Home Token + +``` +GetTokenAddress(blockchainId, asset) -> tokenAddress +state.setHomeToken(blockchainId, tokenAddress) +``` + +Sets the home ledger token information: + +| Field | Value | +| --- | --- | +| `blockchain_id` | Target blockchain ID | +| `token_address` | Token contract address on that chain | + +### 3.5 Apply Deposit Transition + +``` +state.ApplyHomeDepositTransition(amount) +``` + +Creates the initial deposit transition: + +| Field | Value | +| --- | --- | +| `type` | `home_deposit` | +| `tx_hash` | State ID reference | +| `account_id` | User wallet address | +| `amount` | Deposit amount | + +### 3.6 Sign + +``` +signState(state) -> userSig +``` + +The user signs the state. + +--- + +## Phase 4: Requesting Channel Creation + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: RequestCreateChannel(channelDef, state, userSig) + Note right of Node: GetLastState(userWallet, asset) returns nil + Note right of Node: ValidateChannelDefinition(channelDef) + Note right of Node: ValidateStateTransition(nil, state) + Note right of Node: StoreChannel(channel) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +### API Method: `request_creation` + +| Parameter | Type | Description | +| --- | --- | --- | +| `state` | state | The initial state to be submitted | +| `channel_definition` | channel_definition | Definition of the channel to be created | + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | `GetLastState(...)` | Confirm no existing state | +| 2 | `ValidateChannelDefinition(...)` | Validate channel parameters | +| 3 | `ValidateStateTransition(nil, state)` | Validate initial state (from nil) | +| 4 | `StoreChannel(channel)` | Create channel record | +| 5 | `StoreState(state)` | Store the initial state | + +### Channel Validation Rules + +The Node validates: + +- Channel definition nonce is unique +- Challenge period is within acceptable bounds +- Initial state version is 1 +- User signature is valid +- Deposit amount is positive + +After validation, the Node signs the state. This dual-signature (User + Node) is required for on-chain enforcement. + +--- + +## Phase 5: On-Chain Channel Creation + +```mermaid +sequenceDiagram + actor Client + actor HomeChain + actor Node + + Note over Client: GetChannelId(channelDef) + Note over Client: PackChannelDefinition(channelDef) + Note over Client: PackState(channelId, state) + Client->>HomeChain: createChannel(packedChannelDef, packedState) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: Emits ChannelCreated + ChannelDeposited Event +``` + +### 5.1 Get Channel ID + +``` +GetChannelId(channelDef) -> channelId +``` + +The channel ID is a deterministic hash of the channel definition: `channelId = hash(channelDef)`. + +### 5.2 Pack for On-Chain Submission + +``` +PackChannelDefinition(channelDef) -> packedChannelDef +PackState(channelId, state) -> packedState +``` + +Serializes the data for smart contract consumption. + +### 5.3 On-Chain Transaction + +``` +createChannel(packedChannelDef, packedState) +``` + +The **Client** submits a transaction to the **HomeChain** smart contract, which: + +- Creates the channel on-chain with status `OPERATING` +- Pulls funds from the user (via ERC-20 approve/transferFrom) +- Locks funds in the channel +- Emits `ChannelCreated` event +- Additionally emits `ChannelDeposited` event (since the creation action is a deposit) + +### On-Chain State After Creation + +| Field | Value | +| --- | --- | +| `channel_id` | Hash of definition | +| `status` | `OPERATING` | +| `state_version` | 0 | +| `locked_funds` | Deposit amount | + +--- + +## Phase 6: Event Handling and Completion + +```mermaid +sequenceDiagram + actor Node + actor Client + actor User + + Note right of Node: HandleChannelCreated() + Note right of Node: UpdateChannel(channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate + Client-->>User: Returns success +``` + +The Node listens for blockchain events and: + +1. **HandleChannelCreated()** -- Processes the creation event. +2. **UpdateChannel(channel)** -- Updates channel status to reflect on-chain state. + +| Event | Description | +| --- | --- | +| `ChannelUpdate` | Notifies client of new channel status | +| `BalanceUpdate` | Notifies client of new balance | + +The Client returns success to the User, confirming: + +- Home channel created successfully +- Funds deposited and locked on-chain +- User can now perform off-chain operations + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor Client + actor Node + actor HomeChain + + rect rgb(40, 40, 100) + Note over User,Client: Phase 1: Initiation + User->>Client: deposit(blockchainId, asset, amount) + end + + rect rgb(100, 40, 40) + Note over Client,Node: Phase 2: Check Existing State + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns channel_not_found error + end + + rect rgb(40, 100, 40) + Note over Client: Phase 3: Build Initial State + Note over Client: newChannelDefinition() + newEmptyState() + Note over Client: Build deposit transition + sign + end + + rect rgb(100, 100, 40) + Note over Client,Node: Phase 4: Request Creation + Client->>Node: RequestCreateChannel(channelDef, state, userSig) + Note over Node: Validate + Store + Node->>Client: Return node signature + end + + rect rgb(100, 40, 100) + Note over Client,HomeChain: Phase 5: On-Chain Creation + Client->>HomeChain: createChannel(...) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: ChannelCreated + ChannelDeposited Events + end + + rect rgb(40, 100, 100) + Note over Node,User: Phase 6: Completion + Node-->>Client: ChannelUpdate and BalanceUpdate + Client-->>User: Returns success + end +``` + +--- + +## Key Concepts Summary + +### State Lifecycle + +```mermaid +flowchart LR + A["No State
(New User)"] --> B["Initial State v1
(home_deposit)"] + B --> C["On-Chain Channel
(OPERATING)"] + + style A fill:#f44336 + style B fill:#ff9800 + style C fill:#4caf50 +``` + +### Channel Creation vs Regular Deposit + +| Aspect | Channel Creation | Home Deposit | +| --- | --- | --- | +| **When** | First deposit for asset | Subsequent deposits | +| **Node Check** | Returns `channel_not_found` error | Returns existing state | +| **API Method** | `request_creation` | `submit_state` | +| **On-Chain** | `createChannel` | `depositToChannel` | +| **Result** | New channel created | Existing channel updated | + +### Security Invariants + +- A channel is created with an initial signed state. +- `version` = 1. +- `intent` = DEPOSIT. +- Funds are pulled from the User (home chain). +- Channel enters `OPERATING` status. +- A channel identified by `channelId = hash(Definition)` can be created at most once. + +--- + +## On-Chain Optimization + +The Nitrolite architecture improves upon the previous version by **skipping the custody ledger step**. Users transfer funds directly from their ERC-20 token balance via approvals, saving one transaction compared to the old protocol. + +This means: +1. User approves the Custody contract. +2. Channel creation pulls funds directly. +3. No intermediate custody step required. + +--- + +## Error Scenarios + +| Scenario | Cause | Resolution | +| --- | --- | --- | +| **Channel already exists** | Duplicate creation attempt | Use `submit_state` instead | +| **Invalid definition** | Bad nonce or challenge period | Retry with valid parameters | +| **Insufficient balance** | User lacks ERC-20 tokens | User needs to obtain tokens first | +| **Transaction revert** | On-chain failure | Check gas, retry | + +--- + +## Related Flows + +- [Transfer Communication Flow](./transfer-flow) +- [App Session Deposit Flow](./app-session-deposit) +- [Escrow Channel Deposit Flow](./escrow-deposit) +- [Home Channel Deposit Flow](./home-channel-deposit) +- [Home Channel Withdrawal Flow](./home-channel-withdrawal) diff --git a/docs/learn/protocol-flows/home-channel-deposit.mdx b/docs/learn/protocol-flows/home-channel-deposit.mdx new file mode 100644 index 0000000..71b528b --- /dev/null +++ b/docs/learn/protocol-flows/home-channel-deposit.mdx @@ -0,0 +1,408 @@ +--- +title: "Home Channel Deposit Flow" +description: "A comprehensive breakdown of the Home Channel Deposit flow for adding funds to an existing channel in the Nitrolite v1.0 protocol." +sidebar_position: 5 +--- + +# Home Channel Deposit Flow + +This document provides a comprehensive breakdown of the **Home Channel Deposit** flow as defined in the Nitrolite v1.0 protocol. This operation allows a user to deposit additional funds into their **existing home channel** on their Home Chain -- the blockchain where their channel currently exists. + +This is a **single-chain operation** that creates a new state with a deposit transition, gets it co-signed by the Node, and then enforces it on-chain via `depositToChannel`. + +:::info +This flow is for **subsequent deposits** to an existing channel. For the initial deposit (creating a channel), see [Home Channel Creation Flow](./home-channel-creation). +::: + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> C["Client"] + C <--> N["Node (Clearnode)"] + C --> HC["HomeChain"] + HC -.-> N + + style U stroke:#333 + style C stroke:#333 + style N stroke:#333 + style HC stroke:#4CAF50 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating the deposit | +| **Client** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that validates and stores state transitions | +| **HomeChain** | The blockchain where the user's home channel exists | + +--- + +## Prerequisites + +Before the home channel deposit flow begins: + +1. **Client** is connected to the Node via WebSocket. +2. **User already has a home channel** on the HomeChain. +3. **Node** contains the user's state with Home Channel information. +4. **User has funds** on the HomeChain to deposit (ERC-20 tokens). +5. **No ongoing operation** exists for this channel (the Clearnode will deny the request otherwise). + +:::note +The "no ongoing operation" requirement applies to ALL operations except `finalize_escrow_deposit`, `finalize_escrow_withdrawal`, and `finalize_migration`. +::: + +--- + +## Key Concepts + +### Deposit vs Other Operations + +| Operation | Direction | On-Chain Action | +| --- | --- | --- | +| **home_deposit** | External to Channel | `createChannel` (first deposit) or `depositToChannel` | +| **home_withdrawal** | Channel to External | `withdrawFromChannel` (can also be done with `createChannel`) | +| **escrow_withdraw** | Channel to Non-Home Chain | `initiateEscrowWithdrawal` | + +### The depositToChannel Mechanism + +The `depositToChannel` on-chain call enforces the latest signed state for deposits. It: + +- Updates the on-chain state version +- Pulls funds from the user (via ERC-20 approve/transferFrom) +- Adds funds to the channel's locked balance +- Provides settlement guarantees + +--- + +## Phase 1: Deposit Initiation + +```mermaid +sequenceDiagram + actor User + actor Client + + User->>Client: deposit(blockchainId, asset, amount) + Note over Client: User initiates deposit request +``` + +The **User** calls the `deposit` function on the **Client** SDK with three parameters: + +| Parameter | Description | Example | +| --- | --- | --- | +| `blockchainId` | The blockchain ID to deposit on (home chain) | `137` (Polygon) | +| `asset` | The asset symbol to deposit | `usdc` | +| `amount` | The amount to deposit | `100.0` | + +--- + +## Phase 2: Fetching Current State + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns a state with home chain +``` + +1. **Client** requests the **latest state** from the Node. +2. The Node looks up the state using `UserWallet` and `asset`. +3. The Node returns the current **state** object containing Home Channel information, current balances, and latest version. + +--- + +## Phase 3: Building the Deposit State + +```mermaid +sequenceDiagram + actor Client + + Note over Client: createNextState(currentState) returns state + Note over Client: state.ApplyHomeDepositTransition(amount) + Note over Client: signState(state) returns userSig +``` + +### 3.1 Create Next State + +``` +createNextState(currentState) -> state +``` + +The Client creates a new state object based on the current state with an incremented version. The State ID is computed internally by this method. + +### 3.2 Apply Deposit Transition + +``` +state.ApplyHomeDepositTransition(amount) +``` + +Creates and applies the deposit transition internally: + +| Field | Value | +| --- | --- | +| `type` | `home_deposit` | +| `tx_hash` | State ID reference | +| `account_id` | User wallet address | +| `amount` | Deposit amount | + +The transition updates the state: +- `user_balance` increases by the deposit amount +- `user_net_flow` becomes positive (funds flowing in) + +### 3.3 Sign + +``` +signState(state) -> userSig +``` + +The user signs the state. + +--- + +## Phase 4: Submitting State to Node + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset)
returns currentState + Note right of Node: EnsureNoOngoingTransitions() + Note right of Node: ValidateStateTransition(
currentState, state) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | `GetLastState(...)` | Fetch current user state | +| 2 | `EnsureNoOngoingTransitions()` | Prevent conflicts with other operations | +| 3 | `ValidateStateTransition(...)` | Verify version, signatures, balances | +| 4 | `StoreState(state)` | Persist the new state | + +### Validation Rules + +The Node validates: + +- **Version** is `currentState.version + 1` +- **User signature** is valid +- **No ongoing transitions** (atomic operations) +- **Deposit amount** is positive + +A state with intent `DEPOSIT` must include a positive user net-flow delta. + +--- + +## Phase 5: On-Chain Deposit + +```mermaid +sequenceDiagram + actor Client + actor HomeChain + actor Node + + Note over Client: PackState(channelId, state) + Client->>HomeChain: depositToChannel(channelId, packedState) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: Emits ChannelDeposited Event +``` + +### 5.1 Pack State for On-Chain + +``` +PackState(channelId, state) -> packedState +``` + +Serializes the state for smart contract consumption. + +### 5.2 On-Chain Deposit + +``` +depositToChannel(channelId, packedState) +``` + +The **Client** submits a transaction to the **HomeChain** smart contract, which: + +- Validates the state signatures (User + Node) +- Verifies the state version is newer than on-chain version +- Calculates the net-flow delta +- **Pulls funds from user** (via ERC-20 approve/transferFrom) +- Increases the locked funds in the channel +- Emits `ChannelDeposited` event + +### On-Chain State Changes + +| Field | Change | +| --- | --- | +| `state_version` | Updated to new version | +| `locked_funds` | Increased by deposit amount | +| User's token balance | Decreased by deposit amount | + +--- + +## Phase 6: Event Handling and Completion + +```mermaid +sequenceDiagram + actor Node + actor Client + actor User + + Note right of Node: HandleChannelDeposited() + Note right of Node: UpdateChannel(channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate + Client-->>User: Returns success +``` + +The Node listens for blockchain events and: + +1. **HandleChannelDeposited()** -- Processes the deposit event. +2. **UpdateChannel(channel)** -- Updates the channel's on-chain state version and locked funds. + +| Event | Description | +| --- | --- | +| `ChannelUpdate` | Notifies client of new channel state | +| `BalanceUpdate` | Notifies client of new balance | + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor Client + actor Node + actor HomeChain + + rect rgb(40, 40, 100) + Note over User,Client: Phase 1: Initiation + User->>Client: deposit(blockchainId, asset, amount) + end + + rect rgb(40, 100, 40) + Note over Client,Node: Phase 2: Fetch State + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state with home chain + end + + rect rgb(100, 100, 40) + Note over Client: Phase 3: Build Deposit State + Note over Client: createNextState -> ApplyHomeDepositTransition + Note over Client: signState + end + + rect rgb(100, 40, 100) + Note over Client,Node: Phase 4: Submit to Node + Client->>Node: SubmitState(state, userSig) + Note over Node: Validate + Store + Node->>Client: Return node signature + end + + rect rgb(40, 100, 100) + Note over Client,HomeChain: Phase 5: On-Chain Deposit + Client->>HomeChain: depositToChannel(channelId, packedState) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: ChannelDeposited Event + end + + rect rgb(100, 40, 40) + Note over Node,User: Phase 6: Completion + Node-->>Client: ChannelUpdate and BalanceUpdate + Client-->>User: Returns success + end +``` + +--- + +## Key Concepts Summary + +### State Transition Flow + +```mermaid +flowchart LR + A["Current State
(balance: 100)"] --> B["home_deposit
Transition (50)"] + B --> C["New State
(balance: 150)"] + C --> D["depositToChannel
On-Chain"] + D --> E["Funds Locked
in Channel"] + + style A fill:#4caf50 + style E fill:#2196f3 +``` + +### Deposit vs On-Chain Enforcement + +| Aspect | Description | +| --- | --- | +| **Off-chain (SubmitState)** | Updates state with Node, gets dual signature | +| **On-chain (depositToChannel)** | Enforces state, locks actual funds | + +Without the on-chain deposit call, the deposit is only recorded off-chain. The `depositToChannel` is what actually moves the tokens from the user's wallet into the channel. + +### Net Flow Semantics + +- User signs a state with `intent = DEPOSIT`. +- User net flow becomes **positive**. +- On enforcement: funds are pulled from User, channel locked funds increase. + +--- + +## Comparison with Related Flows + +| Flow | On-Chain Action | Direction | +| --- | --- | --- | +| **Home Deposit (this flow)** | `depositToChannel` | External to Channel | +| **Home Deposit (first time)** | `createChannel` | External to Channel | +| **Home Withdrawal** | `withdrawFromChannel` | Channel to External | +| **Escrow Withdrawal** | `initiateEscrowWithdrawal` | Channel to Non-Home | +| **Transfer** | None (off-chain only) | User to User | + +--- + +## Security Guarantees + +### Validation Invariants + +| Invariant | Description | +| --- | --- | +| **Version monotonicity** | Every valid state has a strictly increasing version | +| **Version uniqueness** | No two different states with the same version | +| **Signature authorization** | State must be signed by both User and Node | +| **Positive deposit** | Deposit amount must be greater than zero | + +### What Protects the User? + +1. **Dual signatures** -- Both User and Node must agree to the state. +2. **On-chain enforcement** -- `depositToChannel` validates all signatures. +3. **Challenge mechanism** -- User can challenge if Node misbehaves. +4. **ERC-20 approval** -- User explicitly approves the token transfer. + +--- + +## Error Scenarios + +| Scenario | Cause | Resolution | +| --- | --- | --- | +| **No existing channel** | User hasn't created a channel yet | Use "From Scratch" creation flow | +| **Ongoing transition** | Atomic operation in progress | Wait for completion | +| **Invalid signature** | Corrupted or wrong key | Regenerate signature | +| **Stale version** | Race condition | Refetch state and retry | +| **Insufficient ERC-20 balance** | User lacks tokens | Obtain tokens first | +| **Missing approval** | ERC-20 not approved for contract | Approve contract first | + +--- + +## Related Flows + +- [Home Channel Creation Flow](./home-channel-creation) +- [Home Channel Withdrawal Flow](./home-channel-withdrawal) +- [Transfer Communication Flow](./transfer-flow) +- [Escrow Channel Deposit Flow](./escrow-deposit) diff --git a/docs/learn/protocol-flows/home-channel-withdraw-on-create.mdx b/docs/learn/protocol-flows/home-channel-withdraw-on-create.mdx new file mode 100644 index 0000000..5187395 --- /dev/null +++ b/docs/learn/protocol-flows/home-channel-withdraw-on-create.mdx @@ -0,0 +1,433 @@ +--- +title: "Home Channel Withdraw on Create Flow" +description: "A comprehensive breakdown of the special-case flow for users who have received off-chain funds but need to create an on-chain channel to withdraw them." +sidebar_position: 7 +--- + +# Home Channel Withdraw on Create Flow + +This document provides a comprehensive breakdown of the **Home Channel Withdraw On Create From State** flow as defined in the Nitrolite v1.0 protocol. This is a **special case flow** that handles users who have received off-chain funds (have a pending state) but don't yet have an on-chain channel. + +This scenario commonly occurs when a user receives a transfer before ever depositing -- they need to create an on-chain channel to withdraw their received funds. + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> C["Client"] + C <--> N["Node (Clearnode)"] + C --> HC["HomeChain"] + HC -.-> N + + style U stroke:#333 + style C stroke:#333 + style N stroke:#333 + style HC stroke:#4CAF50 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating the withdrawal | +| **Client** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that contains the user's pending state | +| **HomeChain** | The blockchain where the channel will be created | + +--- + +## Prerequisites + +Before this flow begins: + +1. **Client** is connected to the Node via WebSocket. +2. **Node contains a state with no channel** -- user has an off-chain state (e.g., from receiving transfers) but no on-chain home channel. +3. **User wants to withdraw** funds from their off-chain balance. + +:::info Key Scenario +This flow is critical for users who receive funds before ever depositing. For example, if Alice sends funds to Bob, but Bob has never created a channel, Bob's state exists off-chain in the Node. When Bob wants to withdraw, this flow creates his channel and withdraws in one operation. +::: + +--- + +## Key Concepts + +### State Without Channel + +Unlike a new user (who has no state at all), this user has an off-chain state but no on-chain channel: + +| Aspect | New User | This User | +| --- | --- | --- | +| **Has off-chain state** | No | Yes | +| **Has on-chain channel** | No | No | +| **Has balance** | No | Yes (from receives) | +| **Node returns** | `channel_not_found` error | State with no home chain | + +### Why This Flow Exists + +The Nitrolite protocol allows users to **receive off-chain transfers without having a channel**. The Node tracks these states and signs them. However, to withdraw, the user must: + +1. Create an on-chain channel (to have a settlement destination). +2. Submit the withdrawal state via channel creation. + +This flow combines both operations. + +--- + +## Phase 1: Withdrawal Initiation + +```mermaid +sequenceDiagram + actor User + actor Client + + User->>Client: withdraw(blockchainId, asset, amount) + Note over Client: User initiates withdrawal request +``` + +The **User** calls the `withdraw` function on the **Client** SDK: + +| Parameter | Description | Example | +| --- | --- | --- | +| `blockchainId` | The blockchain ID to create channel and withdraw on | `137` (Polygon) | +| `asset` | The asset symbol to withdraw | `usdc` | +| `amount` | The amount to withdraw | `50.0` | + +--- + +## Phase 2: Fetching Current State (No Home Chain) + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns a state with no home chain +``` + +1. **Client** requests the latest state from the Node. +2. **Node** finds the user's state (from receiving transfers). +3. **Node returns a state with no home chain** -- indicating off-chain balance exists but no channel. + +### The State Returned + +| Field | Value | +| --- | --- | +| `version` | >= 1 (from receive transitions) | +| `home_channel_id` | null/empty | +| `home_ledger` | Contains balance from receives | +| `transitions` | Contains `transfer_receive` entries | + +Unlike "creation from scratch" (which returns an error), this returns an actual state -- but without a home channel ID. + +--- + +## Phase 3: Building the Withdrawal State with Channel Definition + +```mermaid +sequenceDiagram + actor Client + + Note over Client: newChannelDefinition() + Note over Client: createNextState(currentState) returns state + Note over Client: GetTokenAddress(blockchainId, asset) + Note over Client: state.setHomeToken(blockchainId, tokenAddress) + Note over Client: state.ApplyChannelCreation(channelDef,
blockchainId, tokenAddress, nodeAddress) + Note over Client: state.ApplyHomeWithdrawalTransition(amount) + Note over Client: signState(state) returns userSig +``` + +### 3.1 Create Channel Definition + +``` +newChannelDefinition() -> channelDef +``` + +Since no channel exists, the Client needs to create one: + +| Field | Value | +| --- | --- | +| `nonce` | Unique number (can be random, derived from timestamp, or an increment from the previous nonce) | +| `challengeDuration` | Default challenge period | +| `user` | User wallet address | +| `node` | Node wallet address | +| `metadata` | Application-specific metadata (`bytes32`) | + +### 3.2 Create Next State + +``` +createNextState(currentState) -> state +``` + +Creates a new state based on the **existing pending state** (not a new empty state). + +### 3.3 Apply Channel Creation + +``` +state.ApplyChannelCreation(channelDef, blockchainId, tokenAddress, nodeAddress) +``` + +Sets up the state with the channel definition, token information, and node address. This also computes and sets the State ID internally. + +### 3.4 Apply Withdrawal Transition + +``` +state.ApplyHomeWithdrawalTransition(amount) +``` + +Creates and applies the withdrawal transition internally: + +| Field | Value | +| --- | --- | +| `type` | `home_withdrawal` | +| `tx_hash` | State ID reference | +| `account_id` | User wallet address | +| `amount` | Withdrawal amount | + +### 3.5 Sign + +The user signs the state. + +--- + +## Phase 4: Requesting Channel Creation with Withdrawal + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: RequestCreateChannel(channelDef, state, userSig) + Note right of Node: GetLastState(userWallet, asset)
returns pendingState + Note right of Node: ValidateChannelDefinition(channelDef) + Note right of Node: ValidateStateTransition(pendingState, state) + Note right of Node: StoreChannel(channel) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +### Key Difference from Regular Creation + +| Aspect | Regular Creation | This Flow | +| --- | --- | --- | +| **Previous state** | nil (no state) | pendingState (has balance) | +| **Validation** | `ValidateStateTransition(nil, state)` | `ValidateStateTransition(pendingState, state)` | +| **Initial transition** | `home_deposit` | `home_withdrawal` | + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | `GetLastState(...)` | Get the pending state (with balance) | +| 2 | `ValidateChannelDefinition(...)` | Ensure valid nonce and challenge | +| 3 | `ValidateStateTransition(...)` | Validate from pending to withdrawal state | +| 4 | `StoreChannel(channel)` | Create channel record | +| 5 | `StoreState(state)` | Store the withdrawal state | + +### Validation Rules + +The Node validates: + +- **Version continuity** from pending state +- **Sufficient balance** for withdrawal (from received funds) +- **Valid signatures** +- **No overdraft** beyond received balance + +--- + +## Phase 5: On-Chain Channel Creation + +```mermaid +sequenceDiagram + actor Client + actor HomeChain + actor Node + + Note over Client: GetChannelId(channelDef) + Note over Client: PackChannelDefinition(channelDef) + Note over Client: PackState(channelId, state) + Client->>HomeChain: createChannel(packedChannelDef, packedState) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: Emits ChannelCreated + ChannelWithdrawn Events +``` + +### What Happens On-Chain + +The `createChannel` call does **both** operations atomically: + +1. **Creates the channel** on the blockchain (emits `ChannelCreated`). +2. **Processes the withdrawal** state with negative net flow (emits `ChannelWithdrawn`). +3. **Releases funds** to the user's wallet. + +### On-Chain State After Creation + +| Field | Value | +| --- | --- | +| `channel_id` | Hash of definition | +| `status` | `OPERATING` | +| `state_version` | Current version | +| `locked_funds` | Balance minus withdrawal | + +### Where Do Funds Come From? + +Since this is a withdrawal without deposit: + +- **Node must provide liquidity** -- the Node covers the withdrawal from its own funds. +- **Net effect**: User receives tokens, Node's locked funds increase. +- **Security**: Node trusts the off-chain state it already signed. + +--- + +## Phase 6: Event Handling and Completion + +```mermaid +sequenceDiagram + actor Node + actor Client + actor User + + Note right of Node: HandleChannelCreated() + Note right of Node: UpdateChannel(channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate + Client-->>User: Returns success +``` + +The Client returns success to the User, confirming: + +- Home channel created successfully +- Withdrawal processed +- Funds received in their wallet + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor Client + actor Node + actor HomeChain + + rect rgb(40, 40, 100) + Note over User,Client: Phase 1: Initiation + User->>Client: withdraw(blockchainId, asset, amount) + end + + rect rgb(100, 40, 40) + Note over Client,Node: Phase 2: Fetch Pending State + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state with no home chain + end + + rect rgb(40, 100, 40) + Note over Client: Phase 3: Build State + Note over Client: newChannelDefinition() + createNextState() + Note over Client: Build withdrawal transition + sign + end + + rect rgb(100, 100, 40) + Note over Client,Node: Phase 4: Request Creation + Client->>Node: RequestCreateChannel(channelDef, state, userSig) + Note over Node: Validate from pendingState + Node->>Client: Return node signature + end + + rect rgb(100, 40, 100) + Note over Client,HomeChain: Phase 5: On-Chain Creation + Client->>HomeChain: createChannel(...) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: ChannelCreated + ChannelWithdrawn Events + end + + rect rgb(40, 100, 100) + Note over Node,User: Phase 6: Completion + Node-->>Client: ChannelUpdate and BalanceUpdate + Client-->>User: Returns success + end +``` + +--- + +## Key Concepts Summary + +### State Lifecycle in This Flow + +```mermaid +flowchart LR + A["Pending State
(no channel, has balance
from receives)"] --> B["Create Channel +
Withdrawal State"] + B --> C["On-Chain Channel
(OPERATING)"] + C --> D["User Receives
Withdrawn Funds"] + + style A fill:#ff9800 + style D fill:#4caf50 +``` + +### Comparison: Three Channel Creation Scenarios + +| Scenario | User Has State? | Initial Transition | API Method | +| --- | --- | --- | --- | +| **From Scratch** | No (`channel_not_found` error) | `home_deposit` | `request_creation` | +| **Withdraw on Create** | Yes (pending state) | `home_withdrawal` | `request_creation` | +| **Regular Withdrawal** | Yes + Channel exists | `home_withdrawal` | `submit_state` | + +### Who Provides Withdrawal Funds? + +```mermaid +flowchart LR + subgraph Off-Chain["Off-Chain (Node DB)"] + A["User has 100 USDC
(from receives)"] + end + subgraph On-Chain["On-Chain (Smart Contract)"] + B["Node provides 50 USDC
(from liquidity pool)"] + end + C["User's Wallet
receives 50 USDC"] + + A -.->|balance tracked| B + B -->|actual tokens| C + + style A fill:#2196f3 + style B fill:#ff9800 + style C fill:#4caf50 +``` + +The Node trusts the off-chain state because it already co-signed all receive transitions. + +--- + +## Security Considerations + +### Why Is This Safe? + +1. **Node already signed receives** -- All `transfer_receive` transitions were co-signed by Node. +2. **Version continuity** -- Withdrawal state must increment from pending state. +3. **Balance enforcement** -- Cannot withdraw more than received balance. +4. **On-chain verification** -- Smart contract validates both signatures. + +### Invariants Preserved + +| Invariant | How It Applies | +| --- | --- | +| **Version monotonicity** | Withdrawal version > pending state version | +| **Signature authorization** | Both User and Node sign the withdrawal state | +| **No overdraft** | Withdrawal amount must not exceed received balance | + +--- + +## Error Scenarios + +| Scenario | Cause | Resolution | +| --- | --- | --- | +| **No pending state** | User has never received funds | Use regular "from scratch" flow | +| **Insufficient balance** | Withdrawal exceeds received amount | Reduce withdrawal amount | +| **Channel already exists** | User already has a home channel | Use regular withdrawal flow | + +--- + +## Related Flows + +- [Home Channel Creation Flow](./home-channel-creation) +- [Home Channel Withdrawal Flow](./home-channel-withdrawal) +- [Transfer Communication Flow](./transfer-flow) diff --git a/docs/learn/protocol-flows/home-channel-withdrawal.mdx b/docs/learn/protocol-flows/home-channel-withdrawal.mdx new file mode 100644 index 0000000..3b5d455 --- /dev/null +++ b/docs/learn/protocol-flows/home-channel-withdrawal.mdx @@ -0,0 +1,415 @@ +--- +title: "Home Channel Withdrawal Flow" +description: "A comprehensive breakdown of the Home Channel Withdrawal flow for withdrawing funds from a user's unified balance back to their wallet in the Nitrolite v1.0 protocol." +sidebar_position: 6 +--- + +# Home Channel Withdrawal Flow + +This document provides a comprehensive breakdown of the **Home Channel Withdrawal** flow as defined in the Nitrolite v1.0 protocol. This operation allows a user to withdraw funds from their **unified balance** back to their wallet on the **Home Chain** -- the blockchain where their channel currently exists. + +This is a **single-chain operation** that updates the state and checkpoints it on-chain to release funds. + +:::note +The Home Chain may differ from where the channel was originally created, as users can migrate their Home Chain explicitly. +::: + +--- + +## Actors in the Flow + +```mermaid +graph LR + U["User"] --> C["Client"] + C <--> N["Node (Clearnode)"] + C --> HC["HomeChain"] + HC -.-> N + + style U stroke:#333 + style C stroke:#333 + style N stroke:#333 + style HC stroke:#4CAF50 +``` + +| Actor | Role | +| --- | --- | +| **User** | The human user initiating the withdrawal | +| **Client** | SDK/Application managing states on behalf of the user | +| **Node** | The Clearnode that validates and stores state transitions | +| **HomeChain** | The blockchain where the user's home channel exists | + +--- + +## Prerequisites + +Before the home channel withdrawal flow begins: + +1. **Client** is connected to the Node via WebSocket. +2. **User already has a home channel** on the HomeChain. +3. **Node** contains the user's state with Home Channel information. +4. **User has sufficient balance** in their unified balance to withdraw. +5. **No ongoing operation** exists for this channel (the Clearnode will deny the request otherwise). + +:::note +The "no ongoing operation" requirement applies to ALL operations except `finalize_escrow_deposit`, `finalize_escrow_withdrawal`, and `finalize_migration`. +::: + +--- + +## Key Concepts + +### Withdrawal vs Other Operations + +| Operation | Direction | On-Chain Action | +| --- | --- | --- | +| **home_deposit** | External to Channel | `createChannel` (first deposit) or `depositToChannel` | +| **home_withdrawal** | Channel to External | `withdrawFromChannel` (can also be done with `createChannel`) | +| **escrow_withdraw** | Channel to Non-Home Chain | `finalizeEscrowWithdrawal` or `initiateEscrowWithdrawal` | + +### The withdrawFromChannel Mechanism + +The `withdrawFromChannel` on-chain call enforces the latest signed state. For withdrawals: + +- Updates the on-chain state version +- Releases locked funds to the user +- Provides settlement guarantees + +--- + +## Phase 1: Withdrawal Initiation + +```mermaid +sequenceDiagram + actor User + actor Client + + User->>Client: withdraw(blockchainId, asset, amount) + Note over Client: User initiates withdrawal request +``` + +The **User** calls the `withdraw` function on the **Client** SDK with three parameters: + +| Parameter | Description | Example | +| --- | --- | --- | +| `blockchainId` | The blockchain ID to withdraw to (home chain) | `137` (Polygon) | +| `asset` | The asset symbol to withdraw | `usdc` | +| `amount` | The amount to withdraw | `50.0` | + +--- + +## Phase 2: Fetching Current State + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: GetLastState(UserWallet, asset) + Note right of Node: GetLastState(userWallet, asset) + Node->>Client: Returns a state with home chain +``` + +1. **Client** requests the **latest state** from the Node. +2. The Node looks up the state using `UserWallet` and `asset`. +3. The Node returns the current **state** object containing Home Channel information, current balances, and latest version. + +--- + +## Phase 3: Building the Withdrawal State + +```mermaid +sequenceDiagram + actor Client + + Note over Client: createNextState(currentState) returns state + Note over Client: state.setID(CalculateStateID(state.userWallet,
state.asset, cycleId, state.version)) + Note over Client: NewTransition(withdrawalT, state.ID(),
userWallet, amount) + Note over Client: state.applyTransitions(transitions) returns true + Note over Client: signState(state) returns userSig +``` + +### 3.1 Create Next State + +``` +createNextState(currentState) -> state +``` + +The Client creates a new state object based on the current state with an incremented version. + +### 3.2 Calculate State ID + +``` +state.setID(CalculateStateID(state.userWallet, state.asset, cycleId, state.version)) +``` + +The **State ID** is a deterministic hash computed from user wallet, asset, cycle, and version. + +### 3.3 Create Withdrawal Transition + +``` +NewTransition(home_withdrawal, state.ID(), userWallet, amount) +``` + +The **home_withdrawal** transition reduces the user's balance: + +| Field | Value | +| --- | --- | +| `type` | `home_withdrawal` | +| `tx_hash` | State ID reference | +| `account_id` | User wallet address | +| `amount` | Withdrawal amount | + +### 3.4 Apply and Sign + +``` +state.applyTransitions(transitions) -> true +signState(state) -> userSig +``` + +The transition is applied to the state: +- `user_balance` decreases by the withdrawal amount +- `user_net_flow` becomes negative (funds flowing out) + +The user then signs the state. + +--- + +## Phase 4: Submitting State to Node + +```mermaid +sequenceDiagram + actor Client + actor Node + + Client->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset)
returns currentState + Note right of Node: EnsureNoOngoingTransitions() + Note right of Node: ValidateStateTransition(
currentState, state) + Note right of Node: StoreState(state) + Node->>Client: Return node signature +``` + +### Node Validation Steps + +| Step | Operation | Purpose | +| --- | --- | --- | +| 1 | `GetLastState(...)` | Fetch current user state | +| 2 | `EnsureNoOngoingTransitions()` | Prevent conflicts with other operations | +| 3 | `ValidateStateTransition(...)` | Verify version, signatures, balances | +| 4 | `StoreState(state)` | Persist the new state | + +### Validation Rules + +The Node validates: + +- **Version** is `currentState.version + 1` +- **User signature** is valid +- **Sufficient balance** for withdrawal +- **No ongoing transitions** (atomic operations) + +A state with intent `WITHDRAW` must include a negative user net-flow delta and must not increase user allocation beyond previous allocation. + +--- + +## Phase 5: On-Chain Checkpoint + +```mermaid +sequenceDiagram + actor Client + actor HomeChain + actor Node + + Note over Client: PackState(channelId, state) + Client->>HomeChain: withdrawFromChannel(channelId, packedState) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: Emits ChannelWithdrawn Event +``` + +### 5.1 Pack State for On-Chain + +``` +PackState(channelId, state) -> packedState +``` + +Serializes the state for smart contract consumption. + +### 5.2 On-Chain Withdrawal + +``` +withdrawFromChannel(channelId, packedState) +``` + +The **Client** submits a transaction to the **HomeChain** smart contract, which: + +- Validates the state signatures (User + Node) +- Verifies the state version is newer than on-chain version +- Calculates the net-flow delta +- **Pushes funds to user** (for withdrawals) +- Updates the locked funds in the channel +- Emits `ChannelWithdrawn` event + +### On-Chain State Changes + +| Field | Change | +| --- | --- | +| `state_version` | Updated to new version | +| `locked_funds` | Decreased by withdrawal amount | +| User's wallet | Receives withdrawn tokens | + +--- + +## Phase 6: Event Handling and Completion + +```mermaid +sequenceDiagram + actor Node + actor Client + actor User + + Note right of Node: HandleChannelWithdrawn() + Note right of Node: UpdateChannel(channel) + Node-->>Client: Sends ChannelUpdate and BalanceUpdate + Client-->>User: Returns success +``` + +The Node listens for blockchain events and: + +1. **HandleChannelWithdrawn()** -- Processes the withdrawal event. +2. **UpdateChannel(channel)** -- Updates the channel's on-chain state version. + +| Event | Description | +| --- | --- | +| `ChannelUpdate` | Notifies client of new channel state | +| `BalanceUpdate` | Notifies client of new balance | + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor User + actor Client + actor Node + actor HomeChain + + rect rgb(40, 40, 100) + Note over User,Client: Phase 1: Initiation + User->>Client: withdraw(blockchainId, asset, amount) + end + + rect rgb(40, 100, 40) + Note over Client,Node: Phase 2: Fetch State + Client->>Node: GetLastState(UserWallet, asset) + Node->>Client: Returns state with home chain + end + + rect rgb(100, 100, 40) + Note over Client: Phase 3: Build Withdrawal State + Note over Client: createNextState -> setID -> NewTransition + Note over Client: applyTransitions -> signState + end + + rect rgb(100, 40, 100) + Note over Client,Node: Phase 4: Submit to Node + Client->>Node: SubmitState(state, userSig) + Note over Node: Validate + Store + Node->>Client: Return node signature + end + + rect rgb(40, 100, 100) + Note over Client,HomeChain: Phase 5: On-Chain Withdrawal + Client->>HomeChain: withdrawFromChannel(channelId, packedState) + HomeChain->>Client: Return Tx Hash + HomeChain-->>Node: ChannelWithdrawn Event + end + + rect rgb(100, 40, 40) + Note over Node,User: Phase 6: Completion + Node-->>Client: ChannelUpdate and BalanceUpdate + Client-->>User: Returns success + end +``` + +--- + +## Key Concepts Summary + +### State Transition Flow + +```mermaid +flowchart LR + A["Current State
(balance: 100)"] --> B["home_withdrawal
Transition (50)"] + B --> C["New State
(balance: 50)"] + C --> D["Checkpoint
On-Chain"] + D --> E["Funds Released
to User Wallet"] + + style A fill:#4caf50 + style E fill:#2196f3 +``` + +### Withdrawal vs Checkpoint + +| Aspect | Description | +| --- | --- | +| **Off-chain (SubmitState)** | Updates state with Node, gets dual signature | +| **On-chain (withdrawFromChannel)** | Enforces state, releases actual funds | + +Without the on-chain withdrawal call, the withdrawal is only recorded off-chain. The `withdrawFromChannel` is what actually moves the tokens. + +### Net Flow Semantics + +- User signs a state with `intent = WITHDRAW`. +- User net flow becomes **negative**. +- On enforcement: funds are pushed to User, channel locked funds decrease. + +--- + +## Security Guarantees + +### Validation Invariants + +| Invariant | Description | +| --- | --- | +| **Version monotonicity** | Every valid state has a strictly increasing version | +| **Version uniqueness** | No two different states with the same version | +| **Signature authorization** | State must be signed by both User and Node | +| **No overdraft** | Cannot withdraw more than available balance | + +### What Protects the User? + +1. **Dual signatures** -- Both User and Node must agree to the state. +2. **On-chain enforcement** -- Checkpoint validates all signatures. +3. **Challenge mechanism** -- User can challenge if Node misbehaves. + +--- + +## Error Scenarios + +| Scenario | Cause | Resolution | +| --- | --- | --- | +| **Insufficient balance** | Withdrawal exceeds unified balance | Reduce withdrawal amount | +| **Ongoing transition** | Atomic operation in progress | Wait for completion | +| **Invalid signature** | Corrupted or wrong key | Regenerate signature | +| **Stale version** | Race condition | Refetch state and retry | +| **Checkpoint revert** | On-chain validation failure | Check state validity | + +--- + +## Comparison with Related Flows + +| Flow | On-Chain Action | Direction | +| --- | --- | --- | +| **Home Deposit** | `depositToChannel` | External to Channel | +| **Home Withdrawal** | `withdrawFromChannel` | Channel to External | +| **Escrow Withdrawal** | `initiateEscrowWithdrawal` or `finalizeEscrowWithdrawal` | Channel to Non-Home | +| **Transfer** | None (off-chain only) | User to User | + +--- + +## Related Flows + +- [Transfer Communication Flow](./transfer-flow) +- [Home Channel Creation Flow](./home-channel-creation) +- [Escrow Channel Withdrawal Flow](./escrow-withdrawal) +- [App Session Deposit Flow](./app-session-deposit) diff --git a/docs/learn/protocol-flows/transfer-flow.mdx b/docs/learn/protocol-flows/transfer-flow.mdx new file mode 100644 index 0000000..94201d8 --- /dev/null +++ b/docs/learn/protocol-flows/transfer-flow.mdx @@ -0,0 +1,424 @@ +--- +title: "Transfer Communication Flow" +description: "A comprehensive breakdown of the off-chain transfer flow in the Nitrolite v1.0 protocol, including actors, phases, state transitions, and security guarantees." +sidebar_position: 2 +--- + +# Transfer Communication Flow + +This document provides a comprehensive breakdown of the **off-chain transfer flow** as defined in the Nitrolite v1.0 protocol. The transfer operation moves funds between users instantly without blockchain transactions, leveraging **state transitions** managed through the **Clearnode** (Node). + +--- + +## Actors in the Flow + +```mermaid +graph LR + SU["SenderUser"] --> SC["SenderClient"] + SC <--> N["Node (Clearnode)"] + N <--> RC["ReceiverClient"] + + style SU stroke:#333 + style SC stroke:#333 + style N stroke:#333 + style RC stroke:#333 +``` + +| Actor | Role | +| --- | --- | +| **SenderUser** | The human user initiating the transfer | +| **SenderClient** | SDK/Application that manages states on behalf of the sender | +| **Node** | The Clearnode that validates, stores, and coordinates state transitions | +| **ReceiverClient** | SDK/Application that receives notifications for the recipient | + +--- + +## Prerequisites + +Before the transfer flow begins: + +1. **SenderClient** is connected to the Node via WebSocket. +2. **Node** contains the sender's current **state** with their **Home Channel** information. + +:::info Receiver Account Not Required +It is possible to send funds to a user who does not have an opened channel with the Node. The Node will create the receiver's state during the transfer process. +::: + +--- + +## Phase 1: Transfer Initiation + +```mermaid +sequenceDiagram + actor SenderUser + actor SenderClient + + SenderUser->>SenderClient: transfer(DestinationUserWallet, asset, amount) + Note over SenderClient: User initiates transfer request +``` + +The **SenderUser** calls the `transfer` function on the **SenderClient** SDK with three parameters: + +| Parameter | Description | Example | +| --- | --- | --- | +| Recipient | The wallet address of the recipient | `0xReceiver...` | +| `asset` | The asset symbol to transfer | `usdc` | +| `amount` | The amount to transfer | `50.0` | + +--- + +## Phase 2: Fetching Current State + +```mermaid +sequenceDiagram + actor SenderClient + actor Node + + SenderClient->>Node: GetLastState(SenderUserWallet, asset) + Note right of Node: Retrieves latest state
for user + asset combination + Node->>SenderClient: Returns state with Home Channel +``` + +1. **SenderClient** requests the **latest state** from the Node. +2. The Node looks up the state using `SenderUserWallet` and `asset`. +3. The Node returns a **state** object containing the **Home Channel** information. + +### The State Object + +| Field | Description | +| --- | --- | +| `id` | Deterministic hash ID of the state | +| `transitions` | List of transitions (state changes) | +| `asset` | Asset type of the state | +| `user_wallet` | User wallet address | +| `epoch` | User Epoch Index | +| `version` | Version of the state | +| `home_channel_id` | Identifier for the Home Channel | +| `home_ledger` | User and node balances for the home channel | +| `user_sig` / `node_sig` | Signatures (optional) | + +--- + +## Phase 3: Building the New Sender State + +```mermaid +sequenceDiagram + actor SenderClient + + Note over SenderClient: SenderClient: newState = lastState.NextState() + Note over SenderClient: state.ApplyTransferSendTransition(
receiver, amount) + Note over SenderClient: signState(state) returns userSig +``` + +### 3.1 Create Next State + +``` +createNextState(currentState) -> state +``` + +The client creates a new state object by calling `newState = lastState.NextState()`. Under the hood, this keeps most fields untouched but increments the version and depending on the previous state, removes or keeps transitions. + +### 3.2 Calculate State ID + +``` +state.setID(CalculateStateID(state.userWallet, state.asset, cycleId, state.version)) +``` + +The **State ID** is a deterministic hash computed from: + +- `state.userWallet` -- Sender's wallet address +- `state.asset` -- Asset being transferred +- `epoch` -- Current epoch identifier +- `state.version` -- Incremented version number + +The client calls `state.ApplyTransferSendTransition(receiver, amount)`. Under the hood, this calculates the transaction hash, adds the transition, and updates the home ledger. + +A **transition** object is created with: + +| Field | Value | +| --- | --- | +| `type` | `transfer_send` | +| `tx_hash` | Calculated transaction hash | +| `account_id` | Receiver's wallet address | + +The **transition types** are defined in `api.yaml`: + +- `transfer_send` -- Sender side of a transfer +- `transfer_receive` -- Receiver side of a transfer +- And others: `release`, `commit`, `home_deposit`, `home_withdrawal`, etc. + +### 3.3 Apply Transitions + +``` +state.applyTransitions(transitions) -> true +``` + +The transition is applied to the state, which: + +- Updates the `home_ledger` balances +- Reduces the sender's `user_balance` +- Records the transition in the state's `transitions` array + +:::info Off-chain Transfer Semantics +When a User **sends** funds off-chain: +- User allocation decreases +- Node net flow increases + +These changes are reflected only in cumulative net flows until enforced on-chain. +::: + +### 3.4 Sign the State + +``` +signState(state) -> userSig +``` + +The sender signs the packed state using their signer. This creates the `user_sig` field that authorizes the state change. The signing algorithm can differ, and so the resulting signature format may vary. + +--- + +## Phase 4: Submitting State to Node + +```mermaid +sequenceDiagram + actor SenderClient + actor Node + + SenderClient->>Node: SubmitState(state, userSig) + Note right of Node: GetLastState(userWallet, asset)
returns currentState + Note right of Node: EnsureNoOngoingTransitions() + Note right of Node: ValidateAdvancement(
currentState, proposedState) + Note right of Node: StoreState(state) +``` + +### 4.1 Fetch Current State + +The Node retrieves the current stored state to compare against the submitted state. + +### 4.2 Ensure No Ongoing Transitions + +``` +EnsureNoOngoingTransitions() +``` + +The Node checks that there are no pending/incomplete transitions for this user. This prevents race conditions and ensures state consistency. + +:::warning +If there's an ongoing transition, the Clearnode will return a relevant error and the submission is rejected. + +**Atomic Operations**: `escrow_deposit`, `escrow_withdrawal`, and `home-chain migration` operations are considered as one atomic operation. This means that if, for example, an escrow deposit was started with `initiate_escrow_deposit` transition, then no other states will be issued apart from `finalize_escrow_deposit`. Only after finalization can a `transfer` (or other non-`finalize_escrow_deposit` transition) be accepted by the Node. +::: + +:::info +Even in case of `home_deposit`, the Node won't accept a new state until it knows that the previous state is enforced on-chain. +::: + +### 4.3 Validate State Advancement + +``` +ValidateAdvancement(currentState, proposedState) +``` + +State advancement validation is done by applying effects from the proposed state to the current one. If there is any difference between the proposed state and the one built by the Clearnode, it will return a relevant error (e.g., `version should be X`, `asset shouldn't change`, etc.). + +The Node validates: + +- Version is `currentState.version + 1` +- User's signature is valid (recovers to sender's wallet) +- Balances are consistent (no overdraft) +- Transition type is valid (`transfer_send`) +- Amount matches the transition + +:::tip Security Invariants +From the on-chain protocol: +- **Version monotonicity**: For a given channel, every valid state has a strictly increasing `version`. +- **Version uniqueness**: A party never signs a state with a `version` that was already signed for this channel. No two different states with the same `version` may exist for the same channel. +- **Signature authorization**: Every enforceable state must be signed by both User and Node. +::: + +### 4.4 Node Signs and Stores State + +``` +signState(state) -> nodeSig +StoreState(state) +``` + +:::info 2-Signature Rule +The Node also signs the sender's state. In v1, only a 2-signature state (both User and Node signatures) can be valid. After signing, the Node stores the state locally and sends it as a notification to subscribers. +::: + +The validated and dual-signed state is stored in the Node's database. At this point, the sender's side of the transfer is complete. + +--- + +## Phase 5: Creating Receiver State + +```mermaid +sequenceDiagram + actor Node + + Note right of Node: CreateReceiverState(DestinationUserWallet) + Note right of Node: GetLastState(
DestinationUserWallet, asset) + Note over Node: createNextState(receiver_state)
returns new_receiver_state + Note over Node: new_receiver_state.setID(
CalculateStateID(...)) + Note over Node: NewTransition(transfer_receive,
new_receiver_state.ID(),
DestinationUserWallet, amount) + Note over Node: new_receiver_state.applyTransitions(
transitions) returns true + Note over Node: signState(new_receiver_state)
returns nodeSig +``` + +### Key Difference: Node Signs for Receiver + +Unlike the sender flow where the user signs, the **receiver's state is signed by the Node**. + +:::info Aggregated State Updates +The "receive" updates are aggregated. Only the Node signs the aggregated states -- the Receiver doesn't need to. However, when such states may be aggregated with a "send"/"lock"/"withdraw", then the Receiver needs to sign these new states. + +**On-Chain Enforcement**: A User can decide to bring the "receive" state on-chain (for security purposes or because the Node became offline), for which the User must also sign the state. The User's signature will suffice, as the state already contains the Node's signature. +::: + +### 5.1 Create or Fetch Receiver State + +The receiver state is built in a similar way to how the sender state is built on the client. The Node: + +1. Gets the last receiver state for that asset +2. Creates the next state +3. Applies the `transfer_receive` transition +4. Signs and stores it in the DB + +If the receiver doesn't have a state for this asset yet, the Node creates a new one with `NewVoidState(userWallet, asset)`, then proceeds with the same steps. + +### 5.2 Build Next Receiver State + +Each transition has the following structure: + +| Field | Value | +| --- | --- | +| `type` | `transfer_receive` | +| `tx_hash` | Calculated transaction hash | +| `account_id` | Receiver's wallet address | +| `amount` | Transfer amount (positive, credited) | + +### 5.3 Node Signs the State + +``` +signState(new_receiver_state) -> nodeSig +``` + +The **Node** signs the receiver's state. This is valid because: + +- It's in the receiver's security interest to obtain these states. +- The Node acts as a trusted coordinator for receive operations. +- Receivers can later aggregate these with their own signed operations. + +:::info Off-chain Receive Semantics +When a User **receives** funds off-chain: +- User allocation increases +- Node net flow increases + +These changes are reflected only in cumulative net flows until enforced on-chain. +::: + +:::warning Receiver Security +The Node supports receiving funds by users who don't have a channel or are not online. However, these states can't be enforced on-chain until there is a user signature. This is not a bug, but rather a UX consideration for how the user should provide their signature -- this is planned to be addressed in future releases. +::: + +--- + +## Phase 6: Notifications and Completion + +After the receiver state is created, the Node sends notifications to both the sender and receiver clients. + +--- + +## Complete Flow Diagram + +```mermaid +sequenceDiagram + actor SenderUser + actor SenderClient + actor Node + actor ReceiverClient + + rect rgb(0, 0, 0) + Note over SenderClient: Phase 1: Initiation + SenderUser->>SenderClient: transfer(DestinationUserWallet, asset, amount) + end + + rect rgb(0, 0, 235) + Note over SenderClient,Node: Phase 2: Fetch State + SenderClient->>Node: GetLastState(SenderUserWallet, asset) + Node->>SenderClient: Returns state with home chain + end + + rect rgb(255, 0, 0) + Note over SenderClient: Phase 3: Build Sender State + Note over SenderClient: createNextState -> setID -> NewTransition -> apply -> sign + end + + rect rgb(0, 160, 0) + Note over SenderClient,Node: Phase 4: Submit and Validate + SenderClient->>Node: SubmitState(state, userSig) + Note over Node: Validate and Store + end + + rect rgb(255, 182, 193) + Note over Node: Phase 5: Create Receiver State + Note over Node: Fetch/Create -> Build -> Sign + end + + rect rgb(0, 0, 0) + Note over Node,ReceiverClient: Phase 6: Notify + Node->>SenderClient: ChannelUpdate and BalanceUpdate + Node->>ReceiverClient: ChannelUpdate and BalanceUpdate + Node->>SenderClient: Return node signature + SenderClient->>SenderUser: Returns success and tx hash + end +``` + +--- + +## Key Concepts Summary + +### State vs Transition + +| Concept | Description | +| --- | --- | +| **State** | A complete snapshot of user's balance and channel info at a version | +| **Transition** | An individual operation that changes the state (transfer, deposit, etc.) | + +### Why Node Signs Receiver State + +1. **Efficiency**: Receivers don't need to be online to receive funds. +2. **Aggregation**: Multiple receives can be batched into one state. +3. **Security**: Receivers can obtain states later via User Watchtowers. +4. **Flexibility**: Only when receivers perform active operations (send/withdraw) do they need to sign. + +--- + +### Security Model Summary + +- **Authorization**: All state changes require valid signatures. +- **Monotonicity**: `version` strictly increases. +- **Replay resistance**: No two states with the same version can coexist. +- **Latest-state dominance**: The economically correct outcome is always determined by the latest valid signed state, regardless of enforcement order. + +### Transaction Types + +| Type | Description | +| --- | --- | +| `transfer` | Direct transfer between users | +| `release` | Release funds from lock | +| `commit` | Commit funds to lock | +| `home_deposit` | Deposit to home channel | +| `home_withdrawal` | Withdraw from home channel | +| `escrow_deposit/lock/withdraw` | Escrow channel operations | +| `migrate` | Cross-chain migration | + +--- + +## Related Flows + +- [Home Channel Deposit Flow](./home-channel-deposit) +- [Home Channel Withdrawal Flow](./home-channel-withdrawal) +- [Escrow Channel Deposit Flow](./escrow-deposit) +- [App Session Deposit Flow](./app-session-deposit) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 510d3aa..61f3d68 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -69,8 +69,13 @@ const config: Config = { includeCurrentVersion: true, versions: { current: { - label: require('./package.json').version, - path: '', // Root path (docs/) + label: '1.x', + path: '', + banner: 'none', + }, + '0.5.x': { + label: '0.5.x', + path: '0.5.x', banner: 'none', }, }, @@ -131,12 +136,6 @@ const config: Config = { label: 'Protocol', position: 'left', }, - { - type: 'doc', - docId: 'contracts/index', - label: 'Contracts', - position: 'left', - }, { type: 'doc', docId: 'manuals/index', @@ -192,10 +191,6 @@ const config: Config = { label: 'Build', to: '/docs/build/quick-start', }, - { - label: 'Contracts', - to: '/docs/contracts', - }, { label: 'Manuals', to: '/docs/manuals', diff --git a/i18n/en/docusaurus-plugin-content-docs/current.json b/i18n/en/docusaurus-plugin-content-docs/current.json index 58c22ac..4399037 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current.json +++ b/i18n/en/docusaurus-plugin-content-docs/current.json @@ -1,6 +1,6 @@ { "version.label": { - "message": "0.5.x", + "message": "1.x", "description": "The label for version current" }, "sidebar.learnSidebar.category.Introduction": { diff --git a/package-lock.json b/package-lock.json index 1d23b04..fbd3b59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "docs", - "version": "0.5.x", + "version": "1.x", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docs", - "version": "0.5.x", + "version": "1.x", "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", @@ -172,7 +172,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.49.1.tgz", "integrity": "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", @@ -324,7 +323,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2172,7 +2170,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2195,7 +2192,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2305,7 +2301,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2727,7 +2722,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3527,7 +3521,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz", "integrity": "sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/babel": "3.9.2", "@docusaurus/bundler": "3.9.2", @@ -3709,7 +3702,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4753,7 +4745,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -5236,7 +5227,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -6149,7 +6139,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6511,7 +6500,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6579,7 +6567,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6625,7 +6612,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.1.tgz", "integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", @@ -7142,7 +7128,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7451,7 +7436,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -8165,7 +8149,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -8491,7 +8474,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -8901,7 +8883,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -10222,7 +10203,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15644,7 +15624,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -16222,7 +16201,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17126,7 +17104,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -17949,7 +17926,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -17959,7 +17935,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -18015,7 +17990,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -18044,7 +18018,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -19961,8 +19934,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", @@ -20043,7 +20015,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20427,7 +20398,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -20675,7 +20645,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 86fc036..4e3eff3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "0.5.x", + "version": "1.x", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -61,4 +61,4 @@ "overrides": { "serialize-javascript": "^7.0.3" } -} +} \ No newline at end of file diff --git a/sidebars.ts b/sidebars.ts index a044597..5fee934 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -51,6 +51,16 @@ const sidebars: SidebarsConfig = { 'learn/core-concepts/session-keys', 'learn/core-concepts/challenge-response', 'learn/core-concepts/message-envelope', + 'learn/core-concepts/yellow-token', + ], + collapsible: false, + collapsed: false, + }, + { + type: 'category', + label: 'Protocol Flows', + items: [ + {type: 'autogenerated', dirName: 'learn/protocol-flows'}, ], collapsible: false, collapsed: false, diff --git a/static/img/protocol-petal-diagram.png b/static/img/protocol-petal-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..bd83b445f4afaf278a6e9f7d6e422ad43dec6457 GIT binary patch literal 250045 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

%fs)lgzS-`22>Yqjptc(wg3Y2 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 R=os*$B&Q}@_=j2Ge*iC|Qmp_0 literal 0 HcmV?d00001 diff --git a/versioned_docs/version-0.5.x/api-reference/_category_.json b/versioned_docs/version-0.5.x/api-reference/_category_.json new file mode 100644 index 0000000..7bcedf5 --- /dev/null +++ b/versioned_docs/version-0.5.x/api-reference/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "API Reference", + "position": 6, + "link": { + "type": "doc", + "id": "api-reference/index" + } +} \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/api-reference/app-sessions.md b/versioned_docs/version-0.5.x/api-reference/app-sessions.md new file mode 100644 index 0000000..60dad55 --- /dev/null +++ b/versioned_docs/version-0.5.x/api-reference/app-sessions.md @@ -0,0 +1,154 @@ +--- +sidebar_position: 3 +title: App Sessions +description: API reference for virtual application session management +keywords: [app session, create, submit, close, virtual application, api] +displayed_sidebar: apiSidebar +--- + +# App Sessions + +API methods for managing virtual application sessions. + +## create_app_session + +Creates a virtual application session between participants. + +**Request:** +```json +{ + "req": [1, "create_app_session", { + "definition": { + "protocol": "NitroRPC/0.2" | "NitroRPC/0.4", + "participants": Address[], + "weights": number[], + "quorum": number, + "challenge": number, + "nonce": number + }, + "allocations": AppAllocation[], + "session_data": string // Optional + }, timestamp], + "sig": Hex[] +} +``` + +**Response:** +```json +{ + "res": [1, "create_app_session", { + "app_session_id": Hex, + "version": string, + "status": "open" + }, timestamp], + "sig": [Hex] +} +``` + +## submit_app_state + +Updates session state and redistributes funds. + +### NitroRPC/0.2 + +**Request:** +```json +{ + "req": [1, "submit_app_state", { + "app_session_id": Hex, + "allocations": AppAllocation[], + "session_data": string // Optional + }, timestamp], + "sig": Hex[] +} +``` + +### NitroRPC/0.4 + +**Request:** +```json +{ + "req": [1, "submit_app_state", { + "app_session_id": Hex, + "intent": "operate" | "deposit" | "withdraw", + "version": number, + "allocations": AppAllocation[], + "session_data": string // Optional + }, timestamp], + "sig": Hex[] +} +``` + +**Response:** +```json +{ + "res": [1, "submit_app_state", { + "app_session_id": Hex, + "version": string, + "status": "open" + }, timestamp], + "sig": [Hex] +} +``` + +## close_app_session + +Closes session and finalizes fund distribution. + +**Request:** +```json +{ + "req": [1, "close_app_session", { + "app_session_id": Hex, + "allocations": AppAllocation[], + "session_data": string // Optional + }, timestamp], + "sig": Hex[] +} +``` + +**Response:** +```json +{ + "res": [1, "close_app_session", { + "app_session_id": Hex, + "version": string, + "status": "closed" + }, timestamp], + "sig": [Hex] +} +``` + +## Types + +### AppAllocation +```typescript +interface AppAllocation { + participant: Address; + asset: string; + amount: string; +} +``` + +### AppDefinition +```typescript +interface AppDefinition { + protocol: "NitroRPC/0.2" | "NitroRPC/0.4"; + participants: Address[]; + weights: number[]; + quorum: number; + challenge: number; + nonce: number; +} +``` + +## Intent Types (NitroRPC/0.4) + +- `operate`: Redistribute existing session funds +- `deposit`: Add funds from participants' unified balances +- `withdraw`: Remove funds to participants' unified balances + +## Session Status + +- `open`: Session is active and accepting state updates +- `closed`: Session is finalized, no further updates allowed \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/api-reference/index.md b/versioned_docs/version-0.5.x/api-reference/index.md new file mode 100644 index 0000000..caeb7ac --- /dev/null +++ b/versioned_docs/version-0.5.x/api-reference/index.md @@ -0,0 +1,263 @@ +--- +sidebar_position: 9 +title: API Reference +description: Complete Yellow SDK method documentation with examples and type definitions +keywords: [api reference, methods, types, virtualapp client, documentation] +displayed_sidebar: apiSidebar +--- + +# API Reference + +Complete reference for all Yellow SDK methods, types, and utilities. + +## VirtualAppRPC Functions + +### Message Creation + +#### `createAppSessionMessage(signer: MessageSigner, sessions: AppSession[]): Promise` + +Creates a signed application session message. + +**Parameters:** +```typescript +type MessageSigner = (payload: any) => Promise; + +interface AppSession { + definition: AppDefinition; + allocations: AppAllocation[]; +} + +interface AppDefinition { + protocol: string; + participants: Address[]; + weights: number[]; + quorum: number; + challenge: number; + nonce: number; +} + +interface AppAllocation { + participant: Address; + asset: string; + amount: string; +} +``` + +#### `createStateUpdateMessage(signer: MessageSigner, update: StateUpdate): Promise` + +Creates a signed state update message. + +#### `parseRPCResponse(data: string): RPCMessage` + +Parses ClearNode response messages. + +**Returns:** +```typescript +interface RPCMessage { + id?: number; + method?: string; + params?: any; + result?: any; + error?: RPCError; +} +``` + +### Utilities + +#### `computeChannelId(channel: Channel): ChannelId` + +Calculates deterministic channel identifier. + +#### `computeStateHash(state: State, channelId: ChannelId): Hash` + +Calculates state hash for signatures. + +#### `validateSignature(state: State, signature: Hex, signer: Address): Promise` + +Verifies state signature. + +## Type Definitions + +### Core Types + +```typescript +type Address = `0x${string}`; +type Hash = `0x${string}`; +type Hex = `0x${string}`; +type ChannelId = Hash; + +interface Channel { + participants: Address[]; + adjudicator: Address; + challenge: bigint; + nonce: bigint; +} + +interface State { + intent: StateIntent; + version: bigint; + data: Hex; + allocations: Allocation[]; + sigs: Hex[]; +} + +interface Allocation { + destination: Address; + token: Address; + amount: bigint; +} + +enum StateIntent { + OPERATE = 0, + INITIALIZE = 1, + RESIZE = 2, + FINALIZE = 3 +} + +enum ChannelStatus { + VOID = 0, + INITIAL = 1, + ACTIVE = 2, + DISPUTE = 3, + FINAL = 4 +} +``` + +### Connection Configuration + +```typescript +interface ClearNodeConfig { + endpoint: string; // WebSocket endpoint + apiKey?: string; // Optional authentication + timeout: number; // Connection timeout + retryAttempts: number; // Reconnection attempts +} + +const config = { + endpoint: 'wss://clearnet-sandbox.yellow.com/ws', // or wss://clearnet.yellow.com/ws for production + timeout: 30000, + retryAttempts: 3 +}; +``` + +### RPC Types + +```typescript +interface RPCRequest { + id: number; + method: string; + params: any[]; + timestamp?: number; +} + +interface RPCResponse { + id: number; + result?: any; + error?: RPCError; + timestamp: number; +} + +interface RPCError { + code: number; + message: string; + data?: any; +} +``` + +## Error Types + +### Client Errors + +```typescript +class YellowSDKError extends Error { + constructor(message: string, public code: string, public context?: any) { + super(message); + this.name = 'YellowSDKError'; + } +} + +class NetworkError extends YellowSDKError { + constructor(message: string, context?: any) { + super(message, 'NETWORK_ERROR', context); + } +} + +class ContractError extends YellowSDKError { + constructor(message: string, context?: any) { + super(message, 'CONTRACT_ERROR', context); + } +} + +class ValidationError extends YellowSDKError { + constructor(message: string, context?: any) { + super(message, 'VALIDATION_ERROR', context); + } +} +``` + +### Error Handling + +```javascript +try { + const sessionMessage = await createAppSessionMessage(messageSigner, sessionData); + ws.send(sessionMessage); +} catch (error) { + if (error instanceof NetworkError) { + // Handle network connectivity issues + await this.reconnectToClearNode(); + } else if (error instanceof ValidationError) { + // Handle input validation errors + console.error('Invalid session parameters:', error.context); + } else if (error instanceof SigningError) { + // Handle wallet signing errors + console.error('Message signing failed:', error.message); + } else { + // Handle unexpected errors + console.error('Unexpected error:', error); + } +} +``` + +## Constants + +### Network Endpoints + +```typescript +const CLEARNODE_ENDPOINTS = { + PRODUCTION: 'wss://clearnet.yellow.com/ws', + SANDBOX: 'wss://clearnet-sandbox.yellow.com/ws', + LOCAL: 'ws://localhost:8080/ws' +}; + +const PROTOCOLS = { + PAYMENT: 'payment-app-v1', + GAMING: 'gaming-app-v1', + ESCROW: 'escrow-app-v1', + TOURNAMENT: 'tournament-v1', + SUBSCRIPTION: 'subscription-v1' +}; +``` + +### Message Types + +```typescript +const MESSAGE_TYPES = { + SESSION_CREATE: 'session_create', + SESSION_MESSAGE: 'session_message', + PAYMENT: 'payment', + STATE_UPDATE: 'state_update', + PARTICIPANT_JOIN: 'participant_join', + SESSION_CLOSE: 'session_close', + ERROR: 'error' +}; + +const SESSION_STATUS = { + PENDING: 'pending', + ACTIVE: 'active', + CLOSING: 'closing', + CLOSED: 'closed', + ERROR: 'error' +}; +``` + +This API reference provides everything you need to integrate VirtualAppRPC into your applications with confidence and precision. \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/build/_category_.json b/versioned_docs/version-0.5.x/build/_category_.json new file mode 100644 index 0000000..6efac87 --- /dev/null +++ b/versioned_docs/version-0.5.x/build/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Build", + "position": 3, + "link": { + "type": "generated-index", + "title": "Build with Yellow SDK", + "description": "Ready to create fast, scalable applications using Yellow Network's state channel infrastructure? This section provides everything you need to start building.", + "slug": "/build" + } +} \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/build/quick-start/index.md b/versioned_docs/version-0.5.x/build/quick-start/index.md new file mode 100644 index 0000000..d397f78 --- /dev/null +++ b/versioned_docs/version-0.5.x/build/quick-start/index.md @@ -0,0 +1,359 @@ +--- +sidebar_position: 2 +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/advanced/architecture)**: 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/versioned_docs/version-0.5.x/guides/index.md b/versioned_docs/version-0.5.x/guides/index.md new file mode 100644 index 0000000..6b80a03 --- /dev/null +++ b/versioned_docs/version-0.5.x/guides/index.md @@ -0,0 +1,13 @@ +--- +title: Guides +description: Comprehensive guides and tutorials +sidebar_position: 1 +displayed_sidebar: guidesSidebar +--- + +# Guides + +**[Migration Guide](/docs/guides/migration-guide)** - Guide for migrating from previous versions of the Yellow SDK. + + +**[Multi-Party App Sessions](/docs/guides/multi-party-app-sessions)** - Learn how to create, manage, and close multi-party application sessions using the Yellow Network and VirtualApp protocol. diff --git a/versioned_docs/version-0.5.x/guides/migration-guide.md b/versioned_docs/version-0.5.x/guides/migration-guide.md new file mode 100644 index 0000000..f11d61f --- /dev/null +++ b/versioned_docs/version-0.5.x/guides/migration-guide.md @@ -0,0 +1,955 @@ +--- +sidebar_position: 2 +title: Migration Guide +description: Guide to migrate to newer versions of VirtualApp +keywords: [migration, upgrade, breaking changes, nitrolite, erc7824] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Migration Guide + +If you are coming from an earlier version of VirtualApp, you will need to account for the following breaking changes. + +## 0.5.x Breaking changes + +The 0.5.x release includes fundamental protocol changes affecting session keys, channel operations, state signatures, and channel resize rules. The main objective of these changes is to enhance security, and provide better experience for developers and users by ability to limit allowances for specific applications. + +**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide ClearNodes running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. + +### Protocol Changes + +These protocol-level changes affect all implementations and integrations with the Yellow Network. + +#### Session Keys: Applications, Allowances, and Expiration + +Session keys now have enhanced properties that define their access levels and capabilities: + +- **Application field**: Determines the scope of session key permissions. Setting this to an application name (e.g., "My Trading App") grants application-scoped access with enforced allowances. Setting it to "clearnode" grants root access equivalent to the wallet itself. + +- **Allowances field**: Defines spending limits for application-scoped session keys. These limits are tracked cumulatively across all operations and are enforced by the protocol. + +- **Expires_at field**: Uses a bigint timestamp (seconds since epoch). Once expired, session keys are permanently frozen and cannot be reactivated. This is particularly critical for root access keys (application set to "clearnode") - if they expire, you lose the ability to perform channel operations. + +#### Channel Creation: Separate Create and Fund Steps + +Clearnode no longer supports creating channels with an initial deposit. All channels must be created with zero balance and funded separately through a resize operation. This two-step process ensures cleaner state management and prevents edge cases in channel initialization. + +#### State Signatures: Wallet vs Session Key Signing + +A fundamental change in how channel states are signed: + +- **Channels created before v0.5.0**: The participant address is the session key, and all states must be signed by that session key. + +- **Channels created after v0.5.0**: The participant address is the wallet address, and all states must be signed by the wallet. + +This change improves security and aligns with standard practices, but requires careful handling during the transition period. + +#### Resize Operations: Strict Channel Balance Rules + +The protocol now enforces strict rules about channel balances and their impact on other operations: + +- **Blocked operations**: Users with any channel containing non-zero amounts cannot perform transfers, submit app states with deposit intent, or create app sessions with non-zero allocations. + +- **Resizing state**: After a resize request, channels enter a "resizing" state with locked funds until the on-chain transaction is confirmed. If a channel remains stuck in this state for an extended period, the recommended action is to close the channel and create a new one. + +- **Allocate amount semantics**: The resize operation uses `allocate_amount` where negative values withdraw from the channel to unified balance, and positive values deposit to the channel. + +:::warning +**Legacy channel migration**: Users with existing channels containing non-zero amounts must either resize them to zero (by providing "resize_amount" as 0 and "allocate_amount" as your **negative** on-chain balance) or close them to enable full protocol functionality. If you are unsure how to adjust resize parameters, the safe option is to close the old on-chain channel entirely, and open a new one. +::: + +#### Non-Zero Channel Allocations: Operation Restrictions + +The following operations will return errors if the user has any channel with non-zero amount: + +- **Transfer**: Returns error code indicating blocked due to non-zero channel balance +- **Submit App State** (with deposit intent): Rejected if attempting to deposit +- **Create App Session** (with allocations): Rejected if attempting to allocate + +The returned error has the following format: `operation denied: non-zero allocation in channel(s) detected owned by wallet

"` + +### VirtualApp SDK + +You should definitely read this section if you are using the VirtualApp SDK. + +#### Update Authentication + +Implementing the new session key protocol changes: + + + + + ```typescript + const authRequest = { + address: '0x...', + session_key: '0x...', + application: 'My Trading App', // Application name for confined access + allowances: [ + { asset: 'usdc', amount: '1000.0' }, + { asset: 'eth', amount: '0.5' } + ], + scope: 'app.create', + expires_at: BigInt(Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60) // 7 days + }; + ``` + + + + + ```typescript + const authRequest = { + address: '0x...', + session_key: '0x...', + application: 'clearnode', // Special value for root access + allowances: [], // Not enforced for root access + scope: 'app.create', + expires_at: BigInt(Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60) // Long expiration recommended + }; + ``` + + + + +**Important considerations:** +- Root access keys (application: "clearnode") cannot perform channel operations after expiration +- Plan expiration times based on your operational needs +- Application-scoped keys track cumulative spending against allowances + +#### Auth Verify Changes + +The `createEIP712AuthMessageSigner` function signature has changed to align with the new session key structure. + +```typescript +const eip712SigningFunction = createEIP712AuthMessageSigner( + walletClient, + { + scope: authMessage.scope, + // remove-next-line + application: authMessage.application, + // remove-next-line + participant: authMessage.session_key, + // remove-next-line + expire: authMessage.expire, + // add-next-line + session_key: authMessage.session_key, + // add-next-line + expires_at: authMessage.expires_at, + allowances: authMessage.allowances, + }, + getAuthDomain(), +); +``` + +#### Migrate Channel Creation + +Channels must now be created with zero initial deposit and funded separately via the `resizeChannel` method: + +```typescript +const { channelId } = await client.createChannel({ + chain_id: 1, + token: tokenAddress, + // remove-next-line + amount: BigInt(1000000), // Initial deposit + // remove-next-line + session_key: '0x...' // Optional +}); + +// add-start +// Step 2: Fund the channel separately +await client.resizeChannel({ + channel_id: channelId, + amount: BigInt(1000000), +}); +// add-end +``` + +#### Resize correctly + +Channel resizing must be negotiated with the ClearNode through WebSocket. Use `resize_amount` and `allocate_amount` with correct sign convention (`resize_amount = -allocate_amount`) and help users with non-zero channel balances migrate by resizing to zero or reopening channels. + +Channel resize can be requested as follows: + +```typescript +const resizeMessage = await createResizeChannelMessage(messageSigner, { + channel_id: channelId, + resize_amount: BigInt(50), // Positive = deposit to channel, negative = withdraw from channel to custody ledger + allocate_amount: BigInt(-50), // Negative = deposit to unified balance, negative = withdraw from unified balance to channel + funds_destination: walletAddress, +}); + +const resizeResponse = {}; // send the message and wait for Clearnode's response + +const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); +const resizeParams = { + resizeState: { + channelId, + ...resizeResponseParams.state, + serverSignature: resizeResponseParams.serverSignature, + data: resizeResponseParams.state.stateData as Hex, + version: BigInt(resizeResponseParams.state.version), + }, + // `previousState` is either initial or previous resizing state, depending on which has higher version number + // can be obtained with `await (client.getChannelData(channelId)).lastValidState` + proofStates: [previousState], +} + +const {txHash} = await client.resizeChannel(resizeParams); +``` + +Here is how you can migrate your channels: + +```typescript +// Check and migrate channels with non-zero amounts +const channels = await client.getOpenChannels(); + +for (const channel of channels) { + if (channel.amount > 0) { + // Must empty channel to enable transfers/app operations + const resizeMessage = await createResizeChannelMessage(messageSigner, { + channel_id: channel.channelId, + resize_amount: BigInt(0), + allocate_amount: -BigInt(channel.amount), + funds_destination: walletAddress, + }); + + // perform the resize as shown above + } +} +``` + + +**Critical:** Operations blocked when any channel has non-zero amount: +- Off-chain transfers +- App state submissions with deposit intent +- Creating app sessions with allocations + +#### Test State Signatures + +If you plan to work with on-chain channels opened PRIOR to v0.5.0, then on VirtualAppClient initialization the `stateSigner` you specify must be based on a Session Key used in the channel as participant. Even if this session key is or will expire, you still need to provide a `stateSigner` based on it. + +On the other hand, if you plan to work with channels created SINCE v0.5.0, you can specify the `stateSigner` based on the `walletClient` you have specified. + +#### Manage Session Keys + +New methods have been added for comprehensive session key management, including retrieval and revocation. + +```typescript +// Get all active session keys +const sessionKeys = await client.getSessionKeys(); + +// Revoke a specific session key +await client.revokeSessionKey({ + session_key: '0x...' +}); + +// Session key data structure +interface RPCSessionKey { + id: string; + sessionKey: Address; + application: string; + allowances: RPCAllowanceUsage[]; // Includes usage tracking + scope: string; + expiresAt: bigint; + createdAt: bigint; +} +``` + +#### EIP-712 Signatures: String-based Amounts + +EIP-712 signature types now use string values for amounts instead of numeric types to support better precision with decimal values. + +```typescript +const types = { + Allowance: [ + { name: 'asset', type: 'string' }, + // remove-next-line + { name: 'amount', type: 'uint256' }, + // add-next-line + { name: 'amount', type: 'string' }, + ] +}; +``` + +### ClearNode API + +You should read this section only if you are using the ClearNode API directly. + +#### Update Authentication + +Use the new session key parameters with proper `application`, `allowances`, and `expires_at` fields: + + + + + ```json + { + "req": [1, "auth_request", { + "address": "0x1234567890abcdef...", + "session_key": "0x9876543210fedcba...", + "application": "My Trading App", + "allowances": [ + { "asset": "usdc", "amount": "1000.0" }, + { "asset": "eth", "amount": "0.5" } + ], + "scope": "app.create", + "expires_at": 1719123456789 + }, 1619123456789], + "sig": ["0x..."] + } + ``` + + + + + ```json + { + "req": [1, "auth_request", { + "address": "0x1234567890abcdef...", + "session_key": "0x9876543210fedcba...", + "application": "clearnode", + "allowances": [], + "scope": "app.create", + "expires_at": 1750659456789 + }, 1619123456789], + "sig": ["0x..."] + } + ``` + + + + +#### Migrate Channel Creation + +Implement the two-step process (create empty, then resize to fund) + +The `create_channel` method no longer accepts `amount` and `session_key` parameters: + +```json +{ + "req": [1, "create_channel", { + "chain_id": 137, + "token": "0xeeee567890abcdef...", + // remove-next-line + "amount": "100000000", + // remove-next-line + "session_key": "0x1234567890abcdef..." + }, 1619123456789], + "sig": ["0x9876fedcba..."] +} +``` + +#### Manage Session Keys + +New methods for session key operations have been added. + +##### Get Session Keys + +Request: +```json +{ + "req": [1, "get_session_keys", {}, 1619123456789], + "sig": ["0x..."] +} +``` + +Response: +```json +{ + "res": [1, "get_session_keys", { + "session_keys": [{ + "id": "sk_123", + "session_key": "0x9876543210fedcba...", + "application": "My Trading App", + "allowances": [ + { "asset": "usdc", "amount": "1000.0", "used": "250.0" } + ], + "scope": "app.create", + "expires_at": 1719123456789, + "created_at": 1619123456789 + }] + }, 1619123456789], + "sig": ["0x..."] +} +``` + +##### Revoke Session Key Request + +Request: +```json +{ + "req": [1, "revoke_session_key", { + "session_key": "0x1234567890abcdef..." + }, 1619123456789], + "sig": ["0x..."] +} +``` + +Response: +```json +{ + "res": [1, "revoke_session_key", { + "session_key": "0x1234567890abcdef..." + }, 1619123456789], + "sig": ["0x..."] +} +``` + +## 0.3.x Breaking changes + +The 0.3.x release includes breaking changes to the SDK architecture, smart contract interfaces, and Clearnode API enhancements listed below. + +**Not ready to migrate?** Unfortunately, at this time Yellow Network does not provide ClearNodes running the previous version of the protocol, so you will need to migrate to the latest version to continue using the Network. + +### VirtualApp SDK + +You should definitely read this section if you are using the VirtualApp SDK. + +#### Client: Replaced `stateWalletClient` with `StateSigner` + +The `stateWalletClient` parameter of `VirtualAppClient` has been replaced with a required `stateSigner` parameter that implements the `StateSigner` interface. + +When initializing the client, you should use either `WalletStateSigner` or `SessionKeyStateSigner` to handle state signing. + +```typescript +// remove-next-line +import { createNitroliteClient } from '@erc7824/nitrolite'; +// add-start +import { + createVirtualAppClient, + WalletStateSigner +} from '@erc7824/nitrolite'; +// add-end + +const client = createVirtualAppClient({ + publicClient, + walletClient, + // remove-next-line + stateWalletClient: sessionWalletClient, + // add-next-line + stateSigner: new WalletStateSigner(walletClient), + addresses, +}); +``` + +**For session key signing:** + +```typescript +import { SessionKeyStateSigner } from '@erc7824/nitrolite'; + +const stateSigner = new SessionKeyStateSigner('0x...' as Hex); +``` + +#### Actions: Modified `createChannel` Parameters + +The `CreateChannelParams` interface has been fully restructured for better clarity. + +You should use the new [`CreateChannel` ClearNode API endpoint](#added-create_channel-method) to get the response, that fully resembles the channel creation parameters. + +```typescript +// remove-start +const { channelId, initialState, txHash } = await client.createChannel( + tokenAddress, + { + initialAllocationAmounts: [amount1, amount2], + stateData: '0x...', + } +); +// remove-end +// add-start +const { channelId, initialState, txHash } = await client.createChannel({ + channel: { + participants: [address1, address2], + adjudicator: adjudicatorAddress, + challenge: 86400n, + nonce: 42n, + }, + unsignedInitialState: { + intent: StateIntent.Initialize, + version: 0n, + data: '0x', + allocations: [ + { destination: address1, token: tokenAddress, amount: amount1 }, + { destination: address2, token: tokenAddress, amount: amount2 }, + ], + }, + serverSignature: '0x...', +}); +// add-end +``` + +#### Actions: Structured Typed RPC Request Parameters + +RPC requests now use endpoint-specific object-based parameters instead of untyped arrays for improved type safety. + +You should update your RPC request creation code to use the new structured format and RPC types. + +```typescript +// remove-start +const request = VirtualAppRPC.createRequest( + requestId, + RPCMethod.GetChannels, + [participant, status], + timestamp +); +// remove-end +// add-start +const request = VirtualAppRPC.createRequest({ + method: RPCMethod.GetChannels, + params: { + participant, + status, + }, + requestId, + timestamp, +}); +// add-end +``` + +#### Actions: Standardized Channel Operations Responses + +The responses for `CloseChannel` and `ResizeChannel` methods have been aligned with newly added `CreateChannel` endpoint for consistency. + +Update your response handling code to use the new `RPCChannelOperation` type. + +```typescript +// remove-start +export interface ResizeChannelResponseParams { + channelId: Hex; + stateData: Hex; + intent: number; + version: number; + allocations: RPCAllocation[]; + stateHash: Hex; + serverSignature: ServerSignature; +} + +export interface CloseChannelResponseParams { + channelId: Hex; + intent: number; + version: number; + stateData: Hex; + allocations: RPCAllocation[]; + stateHash: Hex; + serverSignature: ServerSignature; +} +// remove-end +// add-start +export interface RPCChannelOperation { + channelId: Hex; + state: RPCChannelOperationState; + serverSignature: Hex; +} + +export interface CreateChannelResponse extends GenericRPCMessage { + method: RPCMethod.CreateChannel; + params: RPCChannelOperation & { + channel: RPCChannel; + }; +} + +export interface ResizeChannelResponse extends GenericRPCMessage { + method: RPCMethod.ResizeChannel; + params: RPCChannelOperation; +} + +export interface CloseChannelResponse extends GenericRPCMessage { + method: RPCMethod.CloseChannel; + params: RPCChannelOperation; +} +// add-end +``` + +#### Actions: Modified `Signature` Type + +The `Signature` struct has been replaced with a simple `Hex` type to support EIP-1271 and EIP-6492 signatures. + +Update your signature-handling code to use the new `Hex` type. Still, if using VirtualApp utils correctly, you will not need to change anything, as the utils will handle the conversion for you. + +```typescript +// remove-start +interface Signature { + v: number; + r: Hex; + s: Hex; +} + +const sig: Signature = { + v: 27, + r: '0x...', + s: '0x...' +}; +// remove-end +// add-start +type Signature = Hex; + +const sig: Signature = '0x...'; +// add-end +``` + +#### Added: Pagination Types and Parameters + +To support pagination in ClearNode API requests, new types and parameters have been added. + +For now, only `GetLedgerTransactions` request has been updated to include pagination. + +```typescript +export interface PaginationFilters { + /** Pagination offset. */ + offset?: number; + /** Number of transactions to return. */ + limit?: number; + /** Sort order by created_at. */ + sort?: 'asc' | 'desc'; +} +``` + +### Clearnode API + +You should read this section only if you are using the ClearNode API directly, or if you are using the VirtualApp SDK with custom ClearNode API requests. + +#### Actions: Structured Request Parameters + +ClearNode API requests have migrated from array-based parameters to structured object parameters for improved type safety and API clarity. + +Update all your ClearNode API requests to use object-based parameters instead of arrays. + +```json +{ + // remove-next-line + "req": [1, "auth_request", [{ + // add-next-line + "req": [1, "auth_request", { + "address": "0x1234567890abcdef...", + "session_key": "0x9876543210fedcba...", + "app_name": "Example App", + // remove-next-line + "allowances": [ "usdc", "100.0" ], + // add-start + "allowances": [ + { + "asset": "usdc", + "amount": "100.0" + } + ], + // add-end + "scope": "app.create", + "expire": "3600", + "application": "0xApp1234567890abcdef..." + // remove-next-line + }], 1619123456789], + // add-next-line + }, 1619123456789], + "sig": ["0x5432abcdef..."] +} +``` + +#### Added: `create_channel` Method + +A new `create_channel` method has been added to facilitate the improved single-transaction channel opening flow. + +Use this method to request channel creation parameters from the broker, then submit the returned data to the smart contract via VirtualApp SDK or directly. + +**Request:** +```json +{ + "req": [1, "create_channel", { + "chain_id": 137, + "token": "0xeeee567890abcdef...", + "amount": "100000000", + "session_key": "0x1234567890abcdef..." // Optional + }, 1619123456789], + "sig": ["0x9876fedcba..."] +} +``` + +**Response:** +```json +{ + "res": [1, "create_channel", { + "channel_id": "0x4567890123abcdef...", + "channel": { + "participants": ["0x1234567890abcdef...", "0xbbbb567890abcdef..."], + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 3600, + "nonce": 1619123456789 + }, + "state": { + "intent": 1, + "version": 0, + "state_data": "0xc0ffee", + "allocations": [ + { + "destination": "0x1234567890abcdef...", + "token": "0xeeee567890abcdef...", + "amount": "100000000" + }, + { + "destination": "0xbbbb567890abcdef...", + "token": "0xeeee567890abcdef...", + "amount": "0" + } + ] + }, + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" + }, 1619123456789], + "sig": ["0xabcd1234..."] +} +``` + +#### API: Standardized Channel Operation Responses + +The responses for `create_channel`, `close_channel`, and `resize_channel` methods have been unified for consistency. + +Update your response parsing to handle the new unified structure with `channel_id`, `state`, and `server_signature` fields. + +```json +// remove-start +{ + "res": [1, "close_channel", { + "channelId": "0x4567890123abcdef...", + "intent": 3, + "version": 123, + "stateData": "0x0000000000000000000000000000000000000000000000000000000000001ec7", + "allocations": [...], + "stateHash": "0x...", + "serverSignature": "0x..." + }, 1619123456789], + "sig": ["0xabcd1234..."] +} +// remove-end +// add-start +{ + "res": [1, "close_channel", { + "channel_id": "0x4567890123abcdef...", + "state": { + "intent": 3, + "version": 123, + "state_data": "0xc0ffee", + "allocations": [ + { + "destination": "0x1234567890abcdef...", + "token": "0xeeee567890abcdef...", + "amount": "50000" + } + ] + }, + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" + }, 1619123456789], + "sig": ["0xabcd1234..."] +} +// add-end +``` + +#### Added: Pagination Metadata + +Pagination-supporting endpoints now include a `metadata` struct in their responses with pagination information. + +Update your response handling for `get_channels`, `get_app_sessions`, `get_ledger_entries`, and `get_ledger_transactions` to use the new metadata structure. + +```json +// remove-start +{ + "res": [1, "get_channels", [ + [ + { + "channel_id": "0xfedcba9876543210...", + "status": "open", + // ... channel data + } + ] + ], 1619123456789], + "sig": ["0xabcd1234..."] +} +// remove-end +// add-start +{ + "res": [1, "get_channels", { + "channels": [ + { + "channel_id": "0xfedcba9876543210...", + "status": "open", + // ... channel data + } + ], + "metadata": { + "page": 1, + "per_page": 10, + "total_count": 56, + "page_count": 6 + } + }, 1619123456789], + "sig": ["0xabcd1234..."] +} +// add-end +``` + +The metadata fields provide: +- `page`: Current page number +- `per_page`: Number of items per page +- `total_count`: Total number of items available +- `page_count`: Total number of pages + +### Contracts + +You should read this section only if you are using the VirtualApp smart contracts directly. + +#### Action: Replaced `Signature` Struct with `bytes` + +The `Signature` struct has been removed and replaced with `bytes` type to support EIP-1271, EIP-6492, and other signature formats. + +Update all contract interactions that use signatures to pass `bytes` instead of the struct. + +```solidity +// remove-start +struct Signature { + uint8 v; + bytes32 r; + bytes32 s; +} + +function join( + bytes32 channelId, + uint256 index, + Signature calldata sig +) external returns (bytes32); + +function challenge( + bytes32 channelId, + State calldata candidate, + State[] calldata proofs, + Signature calldata challengerSig +) external; +// remove-end +// add-start +// Signature struct is removed + +function join( + bytes32 channelId, + uint256 index, + bytes calldata sig +) external returns (bytes32); + +function challenge( + bytes32 channelId, + State calldata candidate, + State[] calldata proofs, + bytes calldata challengerSig +) external; +// add-end +``` + +#### Actions: Updated `State` Signature Array + +The `State` struct now uses `bytes[]` for signatures instead of `Signature[]`. + +```solidity +struct State { + uint8 intent; + uint256 version; + bytes data; + Allocation[] allocations; + // remove-next-line + Signature[] sigs; + // add-next-line + bytes[] sigs; +} +``` + +#### Added: Auto-Join Channel Creation Flow + +Channels can now become operational immediately after the `create()` call if all participant signatures are provided. + +When calling `create()` with complete signatures from all participants, the channel automatically becomes active without requiring a separate `join()` call. + +**Single signature (requires join):** +```solidity +// Create channel with only creator's signature +State memory initialState = State({ + intent: StateIntent.Fund, + version: 0, + data: "0x", + allocations: allocations, + sigs: [creatorSignature] // Only one signature +}); + +bytes32 channelId = custody.create(channel, initialState); +// Channel status: JOINING - requires server to call join() +``` + +**Complete signatures (auto-active):** +```solidity +// Create channel with all participants' signatures +State memory initialState = State({ + intent: StateIntent.Fund, + version: 0, + data: "0x", + allocations: allocations, + sigs: [creatorSignature, serverSignature] // All signatures +}); + +bytes32 channelId = custody.create(channel, initialState); +// Channel status: ACTIVE - ready for use immediately +``` + +#### Actions: Update Adjudicator Contracts for EIP-712 Support + +A new `EIP712AdjudicatorBase` base contract has been added to support EIP-712 typed structured data signatures in adjudicator implementations. + +The `EIP712AdjudicatorBase` provides: +- **Domain separator retrieval**: Gets EIP-712 domain separator from the channel implementation contract +- **ERC-5267 compliance**: Automatically handles EIP-712 domain data retrieval +- **Ownership management**: Built-in access control for updating channel implementation address +- **Graceful fallbacks**: Returns `NO_EIP712_SUPPORT` constant when EIP-712 is not available + +If you have custom adjudicator contracts, inherit from `EIP712AdjudicatorBase` to enable EIP-712 signature verification. + +```solidity +// remove-start +import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; +import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; + +contract MyAdjudicator is IAdjudicator { + function adjudicate( + Channel calldata chan, + State calldata candidate, + State[] calldata proofs + ) external view override returns (bool valid) { + return candidate.validateUnanimousSignatures(chan); + } +} +// remove-end +// add-start +import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; +import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; +import {EIP712AdjudicatorBase} from "./EIP712AdjudicatorBase.sol"; + +contract MyAdjudicator is IAdjudicator, EIP712AdjudicatorBase { + constructor(address owner, address channelImpl) + EIP712AdjudicatorBase(owner, channelImpl) {} + + function adjudicate( + Channel calldata chan, + State calldata candidate, + State[] calldata proofs + ) external override returns (bool valid) { + bytes32 domainSeparator = getChannelImplDomainSeparator(); + return candidate.validateUnanimousStateSignatures(chan, domainSeparator); + } +} +// add-end +``` + +#### Added: Enhanced Signature Support + +Smart contracts now support EIP-191, EIP-712, EIP-1271, and EIP-6492 signature formats for greater compatibility. + +The contracts automatically detect and verify the appropriate signature format: +- **Raw ECDSA**: Traditional `(r, s, v)` signatures +- **EIP-191**: Personal message signatures (`\x19Ethereum Signed Message:\n`) +- **EIP-712**: Typed structured data signatures +- **EIP-1271**: Smart contract wallet signatures +- **EIP-6492**: Signatures for undeployed contracts + +No changes are needed in your contract calls - the signature verification is handled automatically by the contract. \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/guides/multi-party-app-sessions.mdx b/versioned_docs/version-0.5.x/guides/multi-party-app-sessions.mdx new file mode 100644 index 0000000..c5ab11e --- /dev/null +++ b/versioned_docs/version-0.5.x/guides/multi-party-app-sessions.mdx @@ -0,0 +1,591 @@ +--- +sidebar_position: 3 +title: Multi-Party Application Sessions +description: Learn how to create, manage, and close multi-party application sessions using the Yellow Network and VirtualApp protocol +keywords: [app sessions, multi-party, state channels, quorum, voting, signatures, allocations] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Multi-Party Application Sessions Tutorial + +## Overview + +Application sessions in VirtualApp enable multiple participants to interact within a shared off-chain state channel. This is particularly powerful for use cases requiring coordinated actions between parties without on-chain overhead. + +This tutorial demonstrates how to create, manage, and close a multi-party application session using the Yellow Network and VirtualApp protocol. + +:::tip Run the Full Example +The complete runnable script for this tutorial is available at: +[`scripts/app_sessions/app_session_two_signers.ts`](https://github.com/stevenzeiler/yellow-sdk-tutorials/blob/main/scripts/app_sessions/app_session_two_signers.ts) + +```bash +npx tsx scripts/app_sessions/app_session_two_signers.ts +``` +::: + +## What is an Application Session? + +An **application session** is a multi-party state channel that allows participants to: + +- **Execute off-chain logic** without blockchain transactions +- **Update shared state** with cryptographic signatures +- **Transfer value** between participants instantly + +Unlike simple payment channels (1-to-1), application sessions support: +- Multiple participants (2+) +- Complex state logic +- Voting mechanisms (weights and quorum) +- Flexible allocation rules + +## Prerequisites + +### Environment Setup + +You'll need two wallet seed phrases in your `.env` file: + +```bash +WALLET_1_SEED_PHRASE="first wallet 12 or 24 word mnemonic here" +WALLET_2_SEED_PHRASE="second wallet 12 or 24 word mnemonic here" +``` + +### Funded Wallets + +Both wallets should have: +1. **Funds in Yellow ledger** (deposited via custody contract) + +### Install Dependencies + +```bash +npm install +``` + +--- + +## Key Concepts + +### 1. App Definition + +The application definition specifies the rules of the session: + +```typescript +const appDefinition: RPCAppDefinition = { + protocol: RPCProtocolVersion.NitroRPC_0_5, + participants: [address1, address2], + weights: [50, 50], // Voting power distribution + quorum: 100, // Percentage needed for decisions (100 = unanimous) + challenge: 0, // Challenge period in seconds + nonce: Date.now(), // Unique session ID + application: 'Test app', +}; +``` + +**Key parameters:** + +| Parameter | Description | +|-----------|-------------| +| `participants` | Array of wallet addresses involved | +| `weights` | Voting power for each participant (must sum to 100 or appropriate total) | +| `quorum` | Required percentage of votes for actions (50 = majority, 100 = unanimous) | +| `challenge` | Time window for disputing state changes | +| `nonce` | Unique identifier to prevent replay attacks | + +### 2. Allocations + +Allocations define how assets are distributed among participants: + + + + +```typescript +const allocations: RPCAppSessionAllocation[] = [ + { participant: address1, asset: 'ytest.usd', amount: '0.01' }, + { participant: address2, asset: 'ytest.usd', amount: '0.00' } +]; +``` + + + + +```typescript +const allocations: RPCAppSessionAllocation[] = [ + { participant: address1, asset: 'usdc', amount: '0.01' }, + { participant: address2, asset: 'usdc', amount: '0.00' } +]; +``` + + + + +**Rules:** +- Total allocations cannot exceed session funding +- Amounts are strings (to maintain precision) +- Must account for all participants + +### 3. Multi-Party Signatures + +For actions requiring consensus (closing, etc.), signatures from multiple participants are collected: + +```typescript +// First participant signs +const closeMessage = await createCloseAppSessionMessage( + messageSigner1, + { app_session_id: sessionId, allocations: finalAllocations } +); + +// Second participant signs +const signature2 = await messageSigner2(closeMessage.req); + +// Add second signature +closeMessage.sig.push(signature2); + +// Submit with all signatures +await yellow.sendMessage(JSON.stringify(closeMessage)); +``` + +--- + +## Step-by-Step Walkthrough + +### Step 1: Connect to Yellow Network + + + + +```typescript +const yellow = new Client({ + url: 'wss://clearnet-sandbox.yellow.com/ws', +}); + +await yellow.connect(); +console.log('Connected to Yellow clearnet (Sandbox)'); +``` + + + + +```typescript +const yellow = new Client({ + url: 'wss://clearnet.yellow.com/ws', +}); + +await yellow.connect(); +console.log('Connected to Yellow clearnet (Production)'); +``` + + + + +### Step 2: Set Up Participant Wallets + +```typescript +// Create wallet clients for both participants +const wallet1Client = createWalletClient({ + account: mnemonicToAccount(process.env.WALLET_1_SEED_PHRASE as string), + chain: base, + transport: http(), +}); + +const wallet2Client = createWalletClient({ + account: mnemonicToAccount(process.env.WALLET_2_SEED_PHRASE as string), + chain: base, + transport: http(), +}); +``` + +### Step 3: Authenticate Both Participants + +Each participant needs their own session key: + +```typescript +// Authenticate first participant +const sessionKey1 = await authenticateWallet(yellow, wallet1Client); +const messageSigner1 = createECDSAMessageSigner(sessionKey1.privateKey); + +// Authenticate second participant +const sessionKey2 = await authenticateWallet(yellow, wallet2Client); +const messageSigner2 = createECDSAMessageSigner(sessionKey2.privateKey); +``` + +### Step 4: Define Application Configuration + +```typescript +const appDefinition: RPCAppDefinition = { + protocol: RPCProtocolVersion.NitroRPC_0_5, + participants: [wallet1Client.account.address, wallet2Client.account.address], + weights: [50, 50], + quorum: 100, + challenge: 0, + nonce: Date.now(), + application: 'Test app', +}; +``` + +### Step 5: Create Session with Initial Allocations + + + + +```typescript +const allocations = [ + { participant: wallet1Client.account.address, asset: 'ytest.usd', amount: '0.01' }, + { participant: wallet2Client.account.address, asset: 'ytest.usd', amount: '0.00' } +]; + +const sessionMessage = await createAppSessionMessage( + messageSigner1, + { definition: appDefinition, allocations } +); + +const sessionResponse = await yellow.sendMessage(sessionMessage); +const sessionId = sessionResponse.params.appSessionId; +``` + + + + +```typescript +const allocations = [ + { participant: wallet1Client.account.address, asset: 'usdc', amount: '0.01' }, + { participant: wallet2Client.account.address, asset: 'usdc', amount: '0.00' } +]; + +const sessionMessage = await createAppSessionMessage( + messageSigner1, + { definition: appDefinition, allocations } +); + +const sessionResponse = await yellow.sendMessage(sessionMessage); +const sessionId = sessionResponse.params.appSessionId; +``` + + + + +### Step 6: Update Session State + +You can update allocations to reflect state changes (e.g., a transfer). Since the quorum is 100%, both participants must sign: + + + + +```typescript +const newAllocations = [ + { participant: wallet1Client.account.address, asset: 'ytest.usd', amount: '0.00' }, + { participant: wallet2Client.account.address, asset: 'ytest.usd', amount: '0.01' } +]; + +// Create update message signed by first participant +const updateMessage = await createSubmitAppStateMessage( + messageSigner1, + { app_session_id: sessionId, allocations: newAllocations } +); + +const updateMessageJson = JSON.parse(updateMessage); + +// Second participant signs the same state update +const signature2 = await messageSigner2(updateMessageJson.req as RPCData); + +// Append second signature to meet quorum requirement +updateMessageJson.sig.push(signature2); + +// Submit with all required signatures +await yellow.sendMessage(JSON.stringify(updateMessageJson)); +``` + + + + +```typescript +const newAllocations = [ + { participant: wallet1Client.account.address, asset: 'usdc', amount: '0.00' }, + { participant: wallet2Client.account.address, asset: 'usdc', amount: '0.01' } +]; + +// Create update message signed by first participant +const updateMessage = await createSubmitAppStateMessage( + messageSigner1, + { app_session_id: sessionId, allocations: newAllocations } +); + +const updateMessageJson = JSON.parse(updateMessage); + +// Second participant signs the same state update +const signature2 = await messageSigner2(updateMessageJson.req as RPCData); + +// Append second signature to meet quorum requirement +updateMessageJson.sig.push(signature2); + +// Submit with all required signatures +await yellow.sendMessage(JSON.stringify(updateMessageJson)); +``` + + + + +### Step 7: Close Session with Multi-Party Signatures + +```typescript +// Create close message (signed by participant 1) +const closeMessage = await createCloseAppSessionMessage( + messageSigner1, + { app_session_id: sessionId, allocations: finalAllocations } +); + +const closeMessageJson = JSON.parse(closeMessage); + +// Participant 2 signs +const signature2 = await messageSigner2(closeMessageJson.req as RPCData); +closeMessageJson.sig.push(signature2); + +// Submit with all signatures +const closeResponse = await yellow.sendMessage(JSON.stringify(closeMessageJson)); +``` + +--- + +## Running the Example + +```bash +npx tsx scripts/app_session_two_signers.ts +``` + +### Expected Output + +``` +Connected to Yellow clearnet +Wallet address: 0x1234... +Wallet address: 0x5678... +Session message created: {...} +Session message sent +Session response: { appSessionId: '0xabc...' } +Submit app state message: {...} +Wallet 2 signed close session message: 0xdef... +Close session message (with all signatures): {...} +Close session message sent +Close session response: { success: true } +``` + +--- + +## Use Cases + +:::note Asset Names in Examples +The examples below use `usdc` for production scenarios. When testing on Sandbox, replace `usdc` with `ytest.usd`. +::: + +### 1. Peer-to-Peer Escrow + +```typescript +// Buyer and seller agree on terms +const appDefinition = { + participants: [buyer, seller], + weights: [50, 50], + quorum: 100, // Both must agree to release funds + // ... +}; + +// Buyer funds escrow +const allocations = [ + { participant: buyer, asset: 'usdc', amount: '0' }, + { participant: seller, asset: 'usdc', amount: '100' } // Released to seller +]; +``` + +### 2. Multi-Player Gaming + +```typescript +const appDefinition = { + participants: [player1, player2, player3, player4], + weights: [25, 25, 25, 25], + quorum: 75, // 3 out of 4 players must agree + challenge: 3600, // 1 hour challenge period + application: 'poker-game', +}; +``` + +### 3. Multi-Signature Treasury Management + +```typescript +const appDefinition = { + participants: [member1, member2, member3, member4, member5], + weights: [20, 20, 20, 20, 20], + quorum: 60, // 60% approval needed + application: 'multi-sig-treasury', +}; +``` + +### 4. Atomic Swaps + +```typescript +// Party A has USDC, wants ETH +// Party B has ETH, wants USDC +const allocations = [ + { participant: partyA, asset: 'usdc', amount: '100' }, + { participant: partyA, asset: 'eth', amount: '0' }, + { participant: partyB, asset: 'usdc', amount: '0' }, + { participant: partyB, asset: 'eth', amount: '0.05' } +]; + +// After swap +const finalAllocations = [ + { participant: partyA, asset: 'usdc', amount: '0' }, + { participant: partyA, asset: 'eth', amount: '0.05' }, + { participant: partyB, asset: 'usdc', amount: '100' }, + { participant: partyB, asset: 'eth', amount: '0' } +]; +``` + +--- + +## Advanced Topics + +### Dynamic Participants + +For applications requiring flexible participation: + +```typescript +// Start with 2 participants +let participants = [user1, user2]; + +// Add a third participant (requires re-creating session) +participants.push(user3); + +const newAppDefinition = { + participants, + weights: [33, 33, 34], + // ... +}; +``` + +### Weighted Voting + +Different participants can have different voting power: + +```typescript +const appDefinition = { + participants: [founder, participant1, participant2], + weights: [50, 30, 20], // Founder has 50% voting power + quorum: 60, // Founder + one participant = 60% + // ... +}; +``` + +### Challenge Periods + +Add time for participants to dispute state changes: + +```typescript +const appDefinition = { + // ... + challenge: 86400, // 24 hours in seconds +}; + +// Participants have 24 hours to challenge a close request before finalization +``` + +### State Validation + +Implement custom logic to validate state transitions: + +```typescript +function validateStateTransition( + oldAllocations: RPCAppSessionAllocation[], + newAllocations: RPCAppSessionAllocation[] +): boolean { + // Ensure total amounts are preserved + const oldTotal = oldAllocations.reduce((sum, a) => sum + parseFloat(a.amount), 0); + const newTotal = newAllocations.reduce((sum, a) => sum + parseFloat(a.amount), 0); + + return Math.abs(oldTotal - newTotal) < 0.000001; +} +``` + +--- + +## Troubleshooting + +### "Authentication failed for participant" + +**Cause**: Session key authentication failed + +**Solution**: +- Ensure both `WALLET_1_SEED_PHRASE` and `WALLET_2_SEED_PHRASE` are set in `.env` +- Verify wallets have been authenticated on Yellow network before + +### "Unsupported token" + +**Cause**: Using the wrong asset for your environment (e.g., `usdc` on Sandbox or `ytest.usd` on Production) + +**Solution**: +- **Sandbox** (`wss://clearnet-sandbox.yellow.com/ws`): Use `ytest.usd` +- **Production** (`wss://clearnet.yellow.com/ws`): Use `usdc` + +Ensure the asset in your allocations matches the connected network. + +### "Insufficient balance" + +**Cause**: Participant doesn't have enough funds in Yellow ledger + +**Solution**: + +Deposit sufficient funds into the yellow network account unified balance for each wallet + +### "Invalid signatures" + +**Cause**: Not all required signatures were collected + +**Solution**: +- Ensure quorum is met (if quorum is 100, need all signatures) +- Check that signatures are added in correct order +- Verify message signers correspond to participants + +### "Session already closed" + +**Cause**: Trying to update or close an already-finalized session + +**Solution**: +- Create a new session +- Check session status before operations + +### "Quorum not reached" + +**Cause**: Insufficient voting weight for action + +**Solution**: + +```typescript +// Example: quorum is 60, weights are [30, 30, 40] +// Need at least 2 participants to sign + +// Check current signature weight +const signatureWeight = signatures.reduce((sum, sig) => { + const participantIndex = findParticipantIndex(sig); + return sum + weights[participantIndex]; +}, 0); + +console.log(`Current weight: ${signatureWeight}, Required: ${quorum}`); +``` + +--- + +## Best Practices + +1. **Always validate allocations** before submitting state updates +2. **Store session IDs** for future reference and auditing +3. **Implement timeout handling** for multi-party signatures +4. **Use appropriate quorum settings** based on trust model +5. **Test with small amounts** before production use +6. **Keep participants informed** of state changes +7. **Handle disconnections gracefully** (participants may come back) +8. **Document application logic** for all participants + +--- + +## Further Reading + +- [App Sessions Core Concepts](/docs/learn/core-concepts/app-sessions) — Understanding app sessions +- [App Session Methods](/docs/protocol/app-layer/off-chain/app-sessions) — Complete API reference +- [Client-Side App Session Signing Guide](/docs/guides/client-side-app-session-signing) — Signing implementation details +- [Session Keys](/docs/learn/core-concepts/session-keys) — Managing session keys diff --git a/versioned_docs/version-0.5.x/learn/_category_.json b/versioned_docs/version-0.5.x/learn/_category_.json new file mode 100644 index 0000000..931b60f --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Learn", + "position": 2, + "link": { + "type": "doc", + "id": "learn/index" + } +} \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/learn/advanced/_category_.json b/versioned_docs/version-0.5.x/learn/advanced/_category_.json new file mode 100644 index 0000000..83cb82e --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/advanced/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Advanced", + "position": 3, + "collapsible": false, + "collapsed": false +} \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/learn/advanced/managing-session-keys.mdx b/versioned_docs/version-0.5.x/learn/advanced/managing-session-keys.mdx new file mode 100644 index 0000000..f1f4c31 --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/advanced/managing-session-keys.mdx @@ -0,0 +1,193 @@ +--- +sidebar_position: 1 +title: Managing Session Keys +description: Create, list, and revoke session keys with complete API examples +keywords: [session keys, authentication, API, create, revoke, manage] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Managing Session Keys + +This guide covers the operational details of creating, listing, and revoking session keys via the Clearnode API. + +:::info Prerequisites +Before diving into session key management, make sure you understand the core concepts: what session keys are, how applications and allowances work, and the expiration rules. See **[Session Keys](../core-concepts/session-keys.mdx)** for the conceptual foundation. +::: + +--- + +## How to Manage Session Keys + +### Clearnode + +#### Create and Configure + +To create a session key, use the `auth_request` method during authentication. This registers the session key with its configuration: + +**Request:** + +```json +{ + "req": [ + 1, + "auth_request", + { + "address": "0x1234567890abcdef...", + "session_key": "0x9876543210fedcba...", + "application": "Chess Game", + "allowances": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ], + "scope": "app.create", + "expires_at": 1762417328 + }, + 1619123456789 + ], + "sig": ["0x5432abcdef..."] +} +``` + +**Parameters:** + +- `address` (required): The wallet address that owns this session key +- `session_key` (required): The address of the session key to register +- `application` (optional): Name of the application using this session key (defaults to "clearnode" if not provided) +- `allowances` (optional): Array of asset allowances specifying spending limits +- `scope` (optional): Permission scope (e.g., "app.create", "ledger.readonly"). **Note:** This feature is not yet implemented +- `expires_at` (required): Unix timestamp (in seconds) when this session key expires + +:::note +When authenticating with an already registered session key, you must still fill in all fields in the request, at least with arbitrary values. This is required by the request itself, however, the values will be ignored as the system uses the session key configuration stored during initial registration. This behavior will be improved in future versions. +::: + +#### List Active Session Keys + +Use the `get_session_keys` method to retrieve all active (non-expired) session keys for the authenticated user: + +**Request:** + +```json +{ + "req": [1, "get_session_keys", {}, 1619123456789], + "sig": ["0x9876fedcba..."] +} +``` + +**Response:** + +```json +{ + "res": [ + 1, + "get_session_keys", + { + "session_keys": [ + { + "id": 1, + "session_key": "0xabcdef1234567890...", + "application": "Chess Game", + "allowances": [ + { + "asset": "usdc", + "allowance": "100.0", + "used": "45.0" + }, + { + "asset": "eth", + "allowance": "0.5", + "used": "0.0" + } + ], + "scope": "app.create", + "expires_at": "2024-12-31T23:59:59Z", + "created_at": "2024-01-01T00:00:00Z" + } + ] + }, + 1619123456789 + ], + "sig": ["0xabcd1234..."] +} +``` + +**Response Fields:** + +- `id`: Unique identifier for the session key record +- `session_key`: The address of the session key +- `application`: Application name this session key is authorized for +- `allowances`: Array of allowances with usage tracking: + - `asset`: Symbol of the asset (e.g., "usdc", "eth") + - `allowance`: Maximum amount the session key can spend + - `used`: Amount already spent by this session key +- `scope`: Permission scope (omitted if empty) +- `expires_at`: When this session key expires (ISO 8601 format) +- `created_at`: When the session key was created (ISO 8601 format) + +#### Revoke a Session Key + +To immediately invalidate a session key, use the `revoke_session_key` method: + +**Request:** + +```json +{ + "req": [ + 1, + "revoke_session_key", + { + "session_key": "0xabcdef1234567890..." + }, + 1619123456789 + ], + "sig": ["0x9876fedcba..."] +} +``` + +**Response:** + +```json +{ + "res": [ + 1, + "revoke_session_key", + { + "session_key": "0xabcdef1234567890..." + }, + 1619123456789 + ], + "sig": ["0xabcd1234..."] +} +``` + +**Permission Rules:** + +- A wallet can revoke any of its session keys +- A session key can revoke itself +- A session key with `application: "clearnode"` can revoke other session keys belonging to the same wallet +- A non-"clearnode" session key cannot revoke other session keys (only itself) + +**Important Notes:** + +- Revocation is **immediate and cannot be undone** +- After revocation, any operations attempted with the revoked session key will fail with a validation error +- The revoked session key will no longer appear in the `get_session_keys` response +- Revocation is useful for security purposes when a session key may have been compromised + +**Error Cases:** + +- Session key does not exist, belongs to another wallet, or is expired: `"operation denied: provided address is not an active session key of this user"` +- Non-"clearnode" session key attempting to revoke another session key: `"operation denied: insufficient permissions for the active session key"` + +### VirtualApp SDK + +The VirtualApp SDK provides a higher-level abstraction for managing session keys. For detailed information on using session keys with the VirtualApp SDK, please refer to the SDK documentation. + diff --git a/versioned_docs/version-0.5.x/learn/core-concepts/_category_.json b/versioned_docs/version-0.5.x/learn/core-concepts/_category_.json new file mode 100644 index 0000000..e2a77fe --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/core-concepts/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Core Concepts", + "position": 3, + "collapsible": false, + "collapsed": false +} + + diff --git a/versioned_docs/version-0.5.x/learn/core-concepts/app-sessions.mdx b/versioned_docs/version-0.5.x/learn/core-concepts/app-sessions.mdx new file mode 100644 index 0000000..9532ee8 --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/core-concepts/app-sessions.mdx @@ -0,0 +1,180 @@ +--- +sidebar_position: 2 +title: App Sessions +description: Multi-party application channels with custom governance and state management +keywords: [app sessions, multi-party, governance, quorum, NitroRPC] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# App Sessions + +App sessions are off-chain channels built on top of the unified balance that enable multi-party applications with custom governance rules. + +**Goal**: Understand how app sessions work for building multi-party applications. + +--- + +## What is an App Session? + +An **app session** is a temporary shared account where multiple participants can: + +- Lock funds from their unified balance +- Execute application-specific logic (games, escrow, predictions) +- Redistribute funds based on outcomes +- Close and release funds back to unified balances + +Think of it as a programmable escrow with custom voting rules. + +--- + +## App Session vs Payment Channel + +| Feature | Payment Channel | App Session | +|---------|-----------------|-------------| +| **Participants** | Always 2 | 2 or more | +| **Governance** | Both must sign | Quorum-based | +| **Fund source** | On-chain deposit | Unified balance | +| **Mid-session changes** | Via resize (on-chain) | Via intent (off-chain) | +| **Use case** | Transfers | Applications | + +--- + +## App Session Definition + +Every app session starts with a **definition** that specifies the rules: + +| Field | Description | +|-------|-------------| +| `protocol` | Version (`NitroRPC/0.4` recommended) | +| `participants` | Wallet addresses (order matters for signatures) | +| `weights` | Voting power per participant | +| `quorum` | Minimum weight required for state updates | +| `challenge` | Dispute window in seconds | +| `nonce` | Unique identifier (typically timestamp) | + +The `app_session_id` is computed deterministically from the definition using `keccak256(JSON.stringify(definition))`. + +--- + +## Governance with Quorum + +The quorum system enables flexible governance patterns. + +### How It Works + +1. Each participant has a **weight** (voting power) +2. State updates require signatures with total weight ≥ **quorum** +3. Not everyone needs to sign—just enough to meet quorum + +### Common Patterns + +| Pattern | Setup | Use Case | +|---------|-------|----------| +| **Unanimous** | `weights: [50, 50]`, `quorum: 100` | Both must agree | +| **Trusted Judge** | `weights: [0, 0, 100]`, `quorum: 100` | App determines outcome | +| **2-of-3 Escrow** | `weights: [40, 40, 50]`, `quorum: 80` | Any two can proceed | +| **Weighted Voting** | `weights: [20, 25, 30, 25]`, `quorum: 51` | Majority by weight | + +--- + +## Session Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Open: create_app_session + Open --> Open: submit_app_state + Open --> Closed: close_app_session + Closed --> [*] +``` + +### 1. Creation + +- Funds locked from participants' unified balances +- All participants with non-zero allocations must sign +- Status becomes `open`, version starts at `1` + +### 2. State Updates + +- Redistribute funds with `submit_app_state` +- Version must increment by exactly 1 +- Quorum of signatures required + +### 3. Closure + +- Final allocations distributed to unified balances +- Session becomes `closed` (cannot reopen) +- Quorum of signatures required + +--- + +## Intent System (NitroRPC/0.4) + +The intent system enables dynamic fund management during active sessions: + +| Intent | Purpose | Rule | +|--------|---------|------| +| **OPERATE** | Redistribute existing funds | Sum unchanged | +| **DEPOSIT** | Add funds from unified balance | Sum increases | +| **WITHDRAW** | Remove funds to unified balance | Sum decreases | + +:::info Allocations Are Final State +Allocations always represent the **final state**, not the delta. The Clearnode computes deltas internally. +::: + +--- + +## Fund Flow + +```mermaid +graph TB + subgraph Unified["Unified Balances"] + UA["Alice: 200 USDC"] + UB["Bob: 200 USDC"] + end + + subgraph Session["App Session"] + SA["Alice: 100 USDC"] + SB["Bob: 100 USDC"] + end + + UA -->|"create (lock)"| SA + UB -->|"create (lock)"| SB + + SA -->|"close (release)"| UA + SB -->|"close (release)"| UB + + style Unified fill:#e1f5ff,stroke:#333 + style Session fill:#ffe1f5,stroke:#333 +``` + +--- + +## Protocol Versions + +| Version | Status | Key Features | +|---------|--------|--------------| +| **NitroRPC/0.2** | Legacy | Basic state updates only | +| **NitroRPC/0.4** | Current | Intent system (OPERATE, DEPOSIT, WITHDRAW) | + +Always use `NitroRPC/0.4` for new applications. Protocol version is set at creation and cannot be changed. + +--- + +## Best Practices + +1. **Set appropriate challenge periods**: 1 hour minimum, 24 hours recommended +2. **Include commission participants**: Apps often have a judge that takes a small fee +3. **Plan for disputes**: Design allocations that can be verified by third parties +4. **Version carefully**: Each state update must be exactly `current + 1` + +--- + +## Deep Dive + +For complete method specifications and implementation details: + +- **[App Session Methods](/docs/protocol/app-layer/off-chain/app-sessions.mdx)** — Complete method specifications +- **[Communication Flows](/docs/protocol/communication-flows.mdx#app-session-lifecycle-flow)** — Sequence diagrams +- **[Implementation Checklist](/docs/protocol/implementation-checklist.mdx#state-management)** — Building app session support diff --git a/versioned_docs/version-0.5.x/learn/core-concepts/challenge-response.mdx b/versioned_docs/version-0.5.x/learn/core-concepts/challenge-response.mdx new file mode 100644 index 0000000..4729658 --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/core-concepts/challenge-response.mdx @@ -0,0 +1,155 @@ +--- +sidebar_position: 4 +title: Challenge-Response & Disputes +description: How Yellow Network handles disputes and ensures fund safety +keywords: [challenge, dispute, security, settlement, fund recovery] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Challenge-Response & Disputes + +In this guide, you will learn how Yellow Network resolves disputes and ensures your funds are always recoverable. + +**Goal**: Understand the security guarantees that make off-chain transactions safe. + +--- + +## Why Challenge-Response Matters + +In any off-chain system, a critical question arises: **What if someone tries to cheat?** + +State channels solve this with a challenge-response mechanism: + +1. Anyone can submit a state to the blockchain +2. Counterparties have time to respond with a newer state +3. The newest valid state always wins +4. Funds are distributed according to that state + +--- + +## The Trust Model + +State channels are **trustless** because: + +| Guarantee | How It's Achieved | +|-----------|-------------------| +| **Fund custody** | Smart contract holds funds, not Clearnode | +| **State validity** | Only signed states are accepted | +| **Dispute resolution** | On-chain fallback if disagreement | +| **Recovery** | You can always get your funds back | + +--- + +## Channel Dispute Flow + +### Scenario: Clearnode Becomes Unresponsive + +You have a channel with 100 USDC. The Clearnode stops responding. + +**Your options:** + +1. Wait for Clearnode to recover +2. Force settlement on-chain via challenge + +### The Process + +1. **Initiate Challenge**: Submit your latest signed state to the blockchain +2. **Challenge Period**: Contract sets a timer (e.g., 24 hours) +3. **Response Window**: Counterparty can submit a newer state +4. **Resolution**: After timeout, challenged state becomes final + +```mermaid +stateDiagram-v2 + [*] --> ACTIVE + ACTIVE --> DISPUTE: challenge() + DISPUTE --> ACTIVE: checkpoint() with newer state + DISPUTE --> FINAL: Timeout expires + FINAL --> [*] + + note right of DISPUTE: Anyone can submit
newer valid state +``` + +--- + +## Why This Works + +### States Are Ordered + +Every state has a version number. A newer (higher version) state always supersedes older states. + +### States Are Signed + +With the default SimpleConsensus adjudicator, both parties must sign every state. If someone signed a state, they can't later claim they didn't agree. + +:::note Other Adjudicators +Different adjudicators may have different signing requirements. For example, a Remittance adjudicator may only require the sender's signature. The signing rules are defined by the channel's adjudicator contract. +::: + +### Challenge Period Provides Fairness + +The waiting window ensures honest parties have time to respond. Network delays don't cause losses. + +### On-Chain Contract is Neutral + +The smart contract accepts any valid signed state, picks the highest version, and distributes funds exactly as specified. + +--- + +## Challenge Period Selection + +| Duration | Trade-offs | +|----------|------------| +| **1 hour** | Fast resolution, tight response window | +| **24 hours** | Balanced (recommended) | +| **7 days** | Maximum safety, slow settlement | + +The Custody Contract enforces a minimum of 1 hour. + +--- + +## Checkpoint vs Challenge + +| Operation | Purpose | Channel Status | +|-----------|---------|----------------| +| `checkpoint()` | Record state without dispute | Stays ACTIVE | +| `challenge()` | Force dispute resolution | Changes to DISPUTE | + +Use checkpoint for safety snapshots. Use challenge when you need to force settlement. + +--- + +## What Happens If... + +| Scenario | Outcome | +|----------|---------| +| **Clearnode goes offline** | Challenge with latest state, withdraw after timeout | +| **You lose state history** | Challenge with old state; counterparty submits newer if they have it | +| **Counterparty submits wrong state** | Submit your newer state via checkpoint | +| **Block reorg occurs** | Replay events from last confirmed block | + +--- + +## Key Takeaways + +| Concept | Remember | +|---------|----------| +| **Challenge** | Force on-chain dispute resolution | +| **Response** | Submit newer state to defeat challenge | +| **Timeout** | After period, challenged state becomes final | +| **Checkpoint** | Record state without dispute | + +:::success Security Guarantee +You can **always** recover your funds according to the latest mutually signed state, regardless of counterparty behavior. +::: + +--- + +## Deep Dive + +For technical implementation details: + +- **[Channel Lifecycle](/docs/protocol/app-layer/on-chain/channel-lifecycle.mdx)** — Full state machine +- **[Security Considerations](/docs/protocol/app-layer/on-chain/security.mdx)** — Threat model and best practices +- **[Communication Flows](/docs/protocol/communication-flows.mdx#challenge-response-closure-flow)** — Sequence diagrams diff --git a/versioned_docs/version-0.5.x/learn/core-concepts/message-envelope.mdx b/versioned_docs/version-0.5.x/learn/core-concepts/message-envelope.mdx new file mode 100644 index 0000000..cd812ce --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/core-concepts/message-envelope.mdx @@ -0,0 +1,143 @@ +--- +sidebar_position: 5 +title: Message Envelope (RPC Protocol) +description: Overview of the Nitro RPC message format and communication protocol +keywords: [Nitro RPC, message format, WebSocket, protocol, signatures] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Message Envelope (RPC Protocol) + +In this guide, you will learn the essentials of how messages are structured and transmitted in Yellow Network. + +**Goal**: Understand the Nitro RPC protocol at a conceptual level. + +--- + +## Protocol Overview + +**Nitro RPC** is a lightweight RPC protocol optimized for state channel communication: + +| Feature | Benefit | +|---------|---------| +| **Compact format** | ~30% smaller than traditional JSON-RPC | +| **Signature-based auth** | Every message is cryptographically verified | +| **Bidirectional** | Real-time updates via WebSocket | +| **Ordered timestamps** | Replay attack prevention | + +--- + +## Message Structure + +Every Nitro RPC message uses a compact JSON array format: + +| Component | Type | Description | +|-----------|------|-------------| +| **requestId** | uint64 | Unique identifier for correlation | +| **method** | string | RPC method name (snake_case) | +| **params/result** | object | Method-specific data | +| **timestamp** | uint64 | Unix milliseconds | + +### Request Wrapper + +``` +{ "req": [requestId, method, params, timestamp], "sig": [...] } +``` + +### Response Wrapper + +``` +{ "res": [requestId, method, result, timestamp], "sig": [...] } +``` + +### Error Response + +``` +{ "res": [requestId, "error", { "error": "description" }, timestamp], "sig": [...] } +``` + +--- + +## Signature Format + +Each signature is a 65-byte ECDSA signature (r + s + v) represented as a 0x-prefixed hex string. + +| Context | What's Signed | Who Signs | +|---------|---------------|-----------| +| **Requests** | JSON payload hash | Session key (or main wallet) | +| **Responses** | JSON payload hash | Clearnode | + +--- + +## Method Categories + +| Category | Methods | +|----------|---------| +| **Auth** | `auth_request`, `auth_verify` | +| **Channels** | `create_channel`, `close_channel`, `resize_channel` | +| **Transfers** | `transfer` | +| **App Sessions** | `create_app_session`, `submit_app_state`, `close_app_session` | +| **Queries** | `get_ledger_balances`, `get_channels`, `get_app_sessions`, etc. | + +--- + +## Notifications + +The Clearnode pushes real-time updates: + +| Notification | When Sent | +|--------------|-----------| +| `bu` (balance update) | Balance changed | +| `cu` (channel update) | Channel status changed | +| `tr` (transfer) | Incoming/outgoing transfer | +| `asu` (app session update) | App session state changed | + +--- + +## Communication Flow + +```mermaid +sequenceDiagram + participant Client + participant Clearnode + + Client->>Clearnode: Request (signed) + Clearnode->>Clearnode: Verify signature + Clearnode->>Clearnode: Process + Clearnode->>Client: Response (signed) + Client->>Client: Verify signature + + Clearnode-->>Client: Notification (async) +``` + +--- + +## Protocol Versions + +| Version | Status | Key Features | +|---------|--------|--------------| +| **NitroRPC/0.2** | Legacy | Basic state updates | +| **NitroRPC/0.4** | Current | Intent system, enhanced validation | + +Always use NitroRPC/0.4 for new implementations. + +--- + +## Key Points + +1. **Compact arrays** instead of verbose JSON objects +2. **Every message signed** for authenticity +3. **Timestamps** prevent replay attacks +4. **Bidirectional** WebSocket for real-time updates + +--- + +## Deep Dive + +For complete technical specifications: + +- **[Message Format](/docs/protocol/app-layer/off-chain/message-format.mdx)** — Full format specification +- **[Off-Chain Overview](/docs/protocol/app-layer/off-chain/overview.mdx)** — Protocol architecture +- **[Implementation Checklist](/docs/protocol/implementation-checklist.mdx#off-chain-rpc)** — Building RPC support diff --git a/versioned_docs/version-0.5.x/learn/core-concepts/session-keys.mdx b/versioned_docs/version-0.5.x/learn/core-concepts/session-keys.mdx new file mode 100644 index 0000000..7a3b861 --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/core-concepts/session-keys.mdx @@ -0,0 +1,177 @@ +--- +sidebar_position: 3 +title: Session Keys +description: Delegated keys for secure, gasless application interactions +keywords: [session keys, authentication, signatures, allowances, security] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Session Keys + +Session keys are delegated keys that enable applications to perform operations on behalf of a user's wallet with specified spending limits, permissions, and expiration times. They provide a secure way to grant limited access to applications without exposing the main wallet's private key. + +:::important +Session keys are **no longer used as on-chain channel participant addresses** for new channels created after the v0.5.0 release. For all new channels, the wallet address is used directly as the participant address. However, session keys still function correctly for channels that were created before v0.5.0, ensuring backward compatibility. +::: + +**Goal**: Understand how session keys enable seamless UX while maintaining security. + +--- + +## Why Session Keys Matter + +Every blockchain operation traditionally requires a wallet signature popup. For high-frequency applications like games or trading, this creates terrible UX—imagine 40+ wallet prompts during a chess game. + +Session keys solve this by allowing you to **sign once**, then operate seamlessly for the duration of the session. + +--- + +## Core Concepts + +### General Rules + +:::important +When authenticating with an already registered session key, you must still provide all parameters in the `auth_request`. However, the configuration values (`application`, `allowances`, `scope`, and `expires_at`) from the request will be ignored, as the system uses the settings from the initial registration. You may provide arbitrary values for these fields, as they are required by the request format but will not be used. +::: + +### Applications + +Each session key is associated with a specific **application name**, which identifies the application or service that will use the session key. The application name is also used to identify **app sessions** that are created using that session key. + +This association serves several purposes: + +- **Application Isolation**: Different applications get separate session keys, preventing one application from using another's delegated access +- **Access Control**: Operations performed with a session key are validated against the application specified during registration +- **Single Active Key**: Only one session key can be active per wallet+application combination. Registering a new session key for the same application automatically invalidates any existing session key for that application + +:::important +Only one session key is allowed per wallet+application combination. If you register a new session key for the same application, the old one is automatically invalidated and removed from the database. +::: + +#### Special Application: "clearnode" + +Session keys registered with the application name `"clearnode"` receive special treatment: + +- **Root Access**: These session keys bypass spending allowance validation and application restrictions +- **Full Permissions**: They can perform any operation the wallet itself could perform +- **Backward Compatibility**: This special behavior facilitates migration from older versions +- **Expiration Still Applies**: Even with root access, the session key expires according to its `expires_at` timestamp + +:::note +The "clearnode" application name is primarily for backward compatibility and will be deprecated after a migration period for developers. +::: + +### Expiration + +All session keys must have an **expiration timestamp** (`expires_at`) that defines when the session key becomes invalid: + +- **Future Timestamp Required**: The expiration time must be set to a future date when registering a session key +- **Automatic Invalidation**: Once the expiration time passes, the session key can no longer be used for any operations +- **No Re-registration**: It is not possible to re-register an expired session key. You must create a new session key instead +- **Applies to All Keys**: Even "clearnode" application session keys must respect the expiration timestamp + +### Allowances + +Allowances define **spending limits** for session keys, specifying which assets the session key can spend and how much: + +```json +{ + "allowances": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ] +} +``` + +#### Allowance Validation + +- **Supported Assets Only**: All assets specified in allowances must be supported by the system. Unsupported assets cause authentication to fail +- **Usage Tracking**: The system tracks spending per session key by recording which session key was used for each ledger debit operation +- **Spending Limits**: Once a session key reaches its spending cap for an asset, further operations requiring that asset are rejected with: `"operation denied: insufficient session key allowance: X required, Y available"` +- **Empty Allowances**: Providing an empty `allowances` array (`[]`) means zero spending allowed for all assets—any operation attempting to spend funds will be rejected + +#### Allowances for "clearnode" Application + +Session keys with `application: "clearnode"` are exempt from allowance enforcement: + +- **No Spending Limits**: Allowance checks are bypassed entirely +- **Full Financial Access**: These keys can spend any amount of any supported asset +- **Expiration Still Matters**: Even without allowance restrictions, the session key still expires according to its `expires_at` timestamp + +--- + +## Session Key Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Unauthenticated + Unauthenticated --> Authenticated: auth_verify success + Authenticated --> Authenticated: Using session key + Authenticated --> Expired: expires_at reached + Authenticated --> Exhausted: Allowance depleted + Authenticated --> Revoked: Manual revocation + Expired --> Unauthenticated: Re-authenticate + Exhausted --> Unauthenticated: Re-authenticate + Revoked --> Unauthenticated: Re-authenticate +``` + +--- + +## Security Model + +| Approach | Risk if Compromised | UX Impact | +|----------|---------------------|-----------| +| **Main wallet always** | Full wallet access | Constant prompts | +| **Session key (limited)** | Only allowance at risk | Seamless | +| **Session key (unlimited)** | Unified balance at risk | Seamless but risky | + +:::warning Session Key Compromise +If a session key is compromised, attackers can only spend up to the configured allowance before expiration. This is why setting appropriate limits is critical. +::: + +--- + +## Best Practices + +### For Users + +1. **Set reasonable allowances**: Don't authorize more than you'll use +2. **Use short expirations**: 24 hours is usually sufficient +3. **Different keys for different apps**: Isolate risk per application +4. **Monitor spending**: Use `get_session_keys` to check usage +5. **Revoke when done**: Clean up unused sessions + +### For Developers + +1. **Secure storage**: Encrypt session keys at rest +2. **Never transmit private keys**: Session key stays on device +3. **Handle expiration gracefully**: Prompt re-authentication before expiry +4. **Verify Clearnode signatures**: Always validate response signatures +5. **Clear on logout**: Delete session keys when user logs out + +--- + +## Alternative: Main Wallet as Root Signer + +You can skip session keys entirely and sign every request with your main wallet. Use this approach for: + +- Single operations +- High-value transactions +- Maximum security required +- Non-interactive applications + +--- + +## Next Steps + +- **[Managing Session Keys](../advanced/managing-session-keys.mdx)** — Create, list, and revoke session keys with full API examples +- **[Authentication Flow](/docs/protocol/app-layer/off-chain/authentication.mdx)** — Full 3-step authentication protocol +- **[Communication Flows](/docs/protocol/communication-flows.mdx#authentication-flow)** — Sequence diagrams for auth diff --git a/versioned_docs/version-0.5.x/learn/core-concepts/state-channels-vs-l1-l2.mdx b/versioned_docs/version-0.5.x/learn/core-concepts/state-channels-vs-l1-l2.mdx new file mode 100644 index 0000000..84199eb --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/core-concepts/state-channels-vs-l1-l2.mdx @@ -0,0 +1,141 @@ +--- +sidebar_position: 1 +title: State Channels vs L1/L2 +description: Compare state channels with Layer 1 and Layer 2 scaling solutions +keywords: [state channels, L1, L2, scaling, comparison, rollups, VirtualApp] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# State Channels vs L1/L2 + +In this guide, you will learn how state channels compare to Layer 1 and Layer 2 solutions, and when each approach is the right choice. + +**Goal**: Understand where state channels fit in the blockchain scaling landscape. + +--- + +## Solution Comparison + +| Solution | Throughput | Latency | Cost per Op | Best For | +|----------|------------|---------|-------------|----------| +| **Layer 1** | 15-65K TPS | 1-15 sec | $0.001-$50 | Settlement, contracts | +| **Layer 2** | 2,000-4,000 TPS | 1-10 sec | $0.01-$0.50 | General dApps | +| **State Channels** | **Unlimited*** | **< 1 sec** | **$0** | High-frequency, known parties | + +*\*Theoretically unlimited—no consensus bottleneck. Real-world throughput depends on signature generation, network latency, and application logic. Benchmarking documentation coming soon.* + +--- + +## How State Channels Work + +State channels operate on a simple principle: + +1. **Lock funds** in a smart contract (on-chain) +2. **Exchange signed states** directly between participants (off-chain) +3. **Settle** when done or if there's a dispute (on-chain) + +The key insight: most interactions between parties don't need immediate on-chain settlement. + +--- + +## State Channel Advantages + +### Instant Finality + +Unlike L2 solutions that still have block times, state channels provide sub-second finality: + +| Solution | Transaction Flow | +|----------|------------------| +| L1 | Transaction → Mempool → Block → Confirmation | +| L2 | Transaction → Sequencer → L2 Block → L1 Data | +| Channels | Signature → Validation → Done | + +### Zero Operational Cost + +| Operation | L1 Cost | L2 Cost | State Channel | +|-----------|---------|---------|---------------| +| 100 transfers | $500-5000 | $10-50 | **$0** | +| 1000 transfers | $5000-50000 | $100-500 | **$0** | + +### Privacy + +Off-chain transactions are only visible to participants. Only opening and final states appear on-chain. + +--- + +## State Channel Limitations + +### Known Participants + +Channels work between specific participants. Yellow Network addresses this through Clearnodes—off-chain service providers that coordinate channels and provide a unified balance across multiple users and chains. + +### Liquidity Requirements + +Funds must be locked upfront. You can't spend more than what's locked in the channel. + +### Liveness Requirements + +Participants must respond to challenges within the challenge period. Users should ensure they can monitor for challenges or use services that provide this functionality. + +--- + +## When to Use Each + +| Choose | When | +|--------|------| +| **L1** | Deploying contracts, one-time large transfers, final settlement | +| **L2** | General dApps, many unknown users, complex smart contracts | +| **State Channels** | Known parties, real-time speed, high frequency, zero gas needed | + +--- + +## Decision Framework + +```mermaid +flowchart TD + A[Transaction] --> B{Known counterparty?} + B -->|No| C[Use L1/L2] + B -->|Yes| D{High frequency?} + D -->|Yes| E[Use State Channel] + D -->|No| F{Large value?} + F -->|Yes| C + F -->|No| E + + style E fill:#9999ff,stroke:#333,color:#111 + style C fill:#99ff99,stroke:#333,color:#111 +``` + +--- + +## How Yellow Network Addresses Limitations + +| Limitation | Solution | +|------------|----------| +| Known participants | Clearnode coordination layer | +| Liquidity | Unified balance across chains | +| Liveness | Always-on Clearnode monitoring | + +--- + +## Key Takeaways + +State channels shine when you have identified participants who will interact frequently—like players in a game, counterparties in a trade, or parties in a payment relationship. + +:::success State Channel Sweet Spot +- Real-time interactions between known parties +- High transaction volumes +- Zero gas costs required +- Instant finality needed +::: + +--- + +## Deep Dive + +For technical details on channel implementation: + +- **[Architecture](/docs/protocol/architecture.mdx)** — System design and fund flows +- **[Channel Lifecycle](/docs/protocol/app-layer/on-chain/channel-lifecycle.mdx)** — State machine and operations +- **[Data Structures](/docs/protocol/app-layer/on-chain/data-structures.mdx)** — Channel and state formats diff --git a/versioned_docs/version-0.5.x/learn/getting-started/_category_.json b/versioned_docs/version-0.5.x/learn/getting-started/_category_.json new file mode 100644 index 0000000..c9b64d7 --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/getting-started/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Getting Started", + "position": 2, + "collapsible": false, + "collapsed": false +} + + diff --git a/versioned_docs/version-0.5.x/learn/getting-started/key-terms.mdx b/versioned_docs/version-0.5.x/learn/getting-started/key-terms.mdx new file mode 100644 index 0000000..2a8221a --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/getting-started/key-terms.mdx @@ -0,0 +1,339 @@ +--- +sidebar_position: 3 +title: Key Terms & Mental Models +description: Essential vocabulary and conceptual frameworks for understanding Yellow Network +keywords: [terminology, glossary, concepts, state channels, mental models] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# 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: + +```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 + +```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 +``` + +| 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 + +### Challenge Period + +When a dispute arises: + +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 + +**Purpose**: Gives honest parties time to respond to incorrect claims. + +--- + +### Signatures + +Two contexts for signatures: + +| 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)) +``` + +--- + +### Quorum + +For app sessions, **quorum** defines the minimum voting weight required for state updates: + +``` +Participants: [Alice, Bob, Judge] +Weights: [40, 40, 50] +Quorum: 80 + +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 | + +--- + +## Next Steps + +Now that you understand the vocabulary, continue to: + +- **[State Channels vs L1/L2](../core-concepts/state-channels-vs-l1-l2.mdx)** — Deep comparison with other scaling solutions +- **[App Sessions](../core-concepts/app-sessions.mdx)** — Multi-party application patterns +- **[Session Keys](../core-concepts/session-keys.mdx)** — Authentication and security + +For complete definitions, see the **[Glossary](/docs/protocol/glossary.mdx)**. \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/learn/getting-started/prerequisites.mdx b/versioned_docs/version-0.5.x/learn/getting-started/prerequisites.mdx new file mode 100644 index 0000000..0462dfd --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/getting-started/prerequisites.mdx @@ -0,0 +1,379 @@ +--- +sidebar_position: 2 +title: Prerequisites & Environment +description: Set up your development environment for building Yellow Apps +keywords: [prerequisites, setup, development, environment, Node.js, viem] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Prerequisites & Environment + +In this guide, you will set up a complete development environment for building applications on Yellow Network. + +**Goal**: Have a working local environment ready for Yellow App development. + +--- + +## System Requirements + +| Requirement | Minimum | Recommended | +|-------------|---------|-------------| +| **Node.js** | 18.x | 20.x or later | +| **npm/yarn/pnpm** | Latest stable | Latest stable | +| **Operating System** | macOS, Linux, Windows | macOS, Linux | + +--- + +## Required Knowledge + +Before building on Yellow Network, you should be comfortable with: + +| Topic | Why It Matters | +|-------|----------------| +| **JavaScript/TypeScript** | SDK and examples are in TypeScript | +| **Async/await patterns** | All network operations are asynchronous | +| **Basic Web3 concepts** | Wallets, transactions, signatures | +| **ERC-20 tokens** | Fund management involves token operations | + +:::tip New to Web3? +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) + +```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 +``` + +### Linux (Ubuntu/Debian) + +```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/). + +--- + +## Step 2: Install Core Dependencies + +Create a new project and install the required packages: + +```bash +# Create project directory +mkdir yellow-app && cd yellow-app + +# Initialize project +npm init -y + +# Install core dependencies +npm install @erc7824/nitrolite viem + +# Install development dependencies +npm install -D typescript @types/node tsx +``` + +### 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 | + +--- + +## Step 3: Configure TypeScript + +Create `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` + +Update `package.json`: + +```json +{ + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + } +} +``` + +--- + +## Step 4: Set Up Environment Variables + +Create `.env` for sensitive configuration: + +```bash +# .env - Never commit this file! + +# Your wallet private key (for development only) +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 +``` + +Add 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: + +```typescript +// 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('Address:', account.address); +console.log('Private Key:', privateKey); +console.log('\n⚠️ Save this private key securely and add to .env'); +``` + +Run it: + +```bash +npx tsx scripts/create-wallet.ts +``` + +### 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: + +```bash +curl -XPOST https://clearnet-sandbox.yellow.com/faucet/requestTokens \ + -H "Content-Type: application/json" \ + -d '{"userAddress":""}' +``` + +Replace `` with your actual wallet address. + +:::tip No On-Chain Operations Needed +Test tokens (ytest.USD) are credited directly to your unified balance on the Sandbox Clearnode. No deposit or channel operations are required—you can start transacting immediately! +::: + +#### Testnet Faucets (For On-Chain Testing) + +If you need on-chain test tokens for Sepolia or Base Sepolia: + +| 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. +::: + +--- + +## Step 6: Verify Setup + +Create `src/index.ts` to verify everything works: + +```typescript +import 'dotenv/config'; +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!'); +} + +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 +``` + +--- + +## Supported Networks + +To get the current list of supported chains and contract addresses, query the Clearnode's `get_config` endpoint: + +```javascript +// Example: Fetch supported chains and contract addresses +const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); + +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)); +}; + +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. +::: + +--- + +## 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](../core-concepts/state-channels-vs-l1-l2.mdx)** — 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"`. + + diff --git a/versioned_docs/version-0.5.x/learn/getting-started/quickstart.mdx b/versioned_docs/version-0.5.x/learn/getting-started/quickstart.mdx new file mode 100644 index 0000000..debe91d --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/getting-started/quickstart.mdx @@ -0,0 +1,1077 @@ +--- +title: Quickstart +description: Get up and running with the Yellow Network SDK in minutes. +--- + +# 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 + +- [Node.js](https://nodejs.org/) (v18 or higher) +- [npm](https://www.npmjs.com/) + +## Setup + +1. **Install Dependencies** + + ```bash + npm install + ``` + +2. **Environment Variables** + + Create a `.env` file in your project root: + + ```bash + # .env + PRIVATE_KEY=your_sepolia_private_key_here + ALCHEMY_RPC_URL=your_alchemy_rpc_url_here + ``` + +## 1. Getting Funds + +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. + +Request tokens via the Faucet: + +```bash +curl -XPOST https://clearnet-sandbox.yellow.com/faucet/requestTokens \ + -H "Content-Type: application/json" \ + -d '{"userAddress":""}' +``` + +## 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, +}); + +// Connect to Sandbox Node +const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); +``` + +## 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); +} +``` + +## 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); + +// Listen for 'create_channel' response, then submit to chain +const createResult = await client.createChannel({ + channel, + unsignedInitialState, + serverSignature, +}); +``` + +### Funding (Resizing) + +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. + +> **Important:** Do NOT use `resize_amount` unless you have deposited funds directly into the L1 Custody Contract. + +```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); + +// Submit resize proof to chain +await client.resizeChannel({ resizeState, proofStates }); +``` + +### Closing & Withdrawing + +Finally, we cooperatively close the channel. This settles the balance on the L1 Custody Contract, allowing you to withdraw. + +```typescript +// Close Channel +const closeMsg = await createCloseChannelMessage(sessionSigner, channelId, account.address); +ws.send(closeMsg); + +// Submit close to chain +await client.closeChannel({ finalState, stateData }); + +// Withdraw from Custody Contract to Wallet +const withdrawalTx = await client.withdrawal(tokenAddress, withdrawableBalance); +console.log('Funds withdrawn:', withdrawalTx); +``` + +## Troubleshooting + +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: + +```bash +npx tsx close_all.ts +``` + +## Complete Code + +### index.ts + +
+Click to view full index.ts + +```typescript +import { + VirtualAppClient, + WalletStateSigner, + createTransferMessage, + createGetConfigMessage, + createECDSAMessageSigner, + createEIP712AuthMessageSigner, + createAuthVerifyMessageFromChallenge, + createCreateChannelMessage, + createResizeChannelMessage, + createGetLedgerBalancesMessage, + createAuthRequestMessage, + createCloseChannelMessage +} from '@erc7824/nitrolite'; +import type { + RPCNetworkInfo, + RPCAsset, + RPCData +} 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'; + +console.log('Starting script...'); + +// 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); + })); +}; + +// Your wallet private key (use environment variables in production!) +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); + +// Create viem clients +const ALCHEMY_RPC_URL = process.env.ALCHEMY_RPC_URL; +const FALLBACK_RPC_URL = 'https://1rpc.io/sepolia'; // Public fallback + +const publicClient = createPublicClient({ + chain: sepolia, + transport: http(ALCHEMY_RPC_URL || FALLBACK_RPC_URL), +}); + +const walletClient = createWalletClient({ + chain: sepolia, + transport: http(), + account, +}); + +interface Config { + assets?: RPCAsset[]; + networks?: RPCNetworkInfo[]; + [key: string]: any; +} + +async function fetchConfig(): Promise { + const signer = createECDSAMessageSigner(PRIVATE_KEY); + const message = await createGetConfigMessage(signer); + + const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); + + return new Promise((resolve, reject) => { + ws.onopen = () => { + ws.send(message); + }; + + ws.onmessage = (event) => { + try { + const response = JSON.parse(event.data.toString()); + // Response format: [requestId, method, result, timestamp] + // or VirtualAppRPCMessage structure depending on implementation + // Based on types: VirtualAppRPCMessage { res: RPCData } + // RPCData: [RequestID, RPCMethod, object, Timestamp?] + + if (response.res && response.res[2]) { + resolve(response.res[2] as Config); + ws.close(); + } else if (response.error) { + reject(new Error(response.error.message || 'Unknown RPC error')); + ws.close(); + } + } catch (err) { + reject(err); + ws.close(); + } + }; + + ws.onerror = (error) => { + reject(error); + ws.close(); + }; + }); +} + +// Initialize VirtualApp client +console.log('Fetching configuration...'); +const config = await fetchConfig(); +console.log('Configuration fetched. Assets count:', config.assets?.length); + +const client = new VirtualAppClient({ + publicClient, + walletClient, + // Use WalletStateSigner for signing states + stateSigner: new WalletStateSigner(walletClient), + // Contract addresses + addresses: { + custody: '0x019B65A265EB3363822f2752141b3dF16131b262', + adjudicator: '0x7c7ccbc98469190849BCC6c926307794fDfB11F2', + }, + chainId: sepolia.id, + challengeDuration: 3600n, // 1 hour challenge period +}); + +console.log('✓ Client initialized'); +console.log(' Wallet Address:', account.address); +console.log(' (Please ensure this address has Sepolia ETH)'); + +// Connect to Clearnode WebSocket (using sandbox for testing) +const ws = new WebSocket('wss://clearnet-sandbox.yellow.com/ws'); + +// Step 1: Generate session keypair locally +const sessionPrivateKey = generatePrivateKey(); +const sessionAccount = privateKeyToAccount(sessionPrivateKey); +const sessionAddress = sessionAccount.address; + +// Helper: Create a signer for the session key +const sessionSigner = createECDSAMessageSigner(sessionPrivateKey); + +// Step 2: Send auth_request +const authParams = { + session_key: sessionAddress, // Session key you generated + allowances: [{ // Add allowance for ytest.usd + asset: 'ytest.usd', + amount: '1000000000' // Large amount + }], + expires_at: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour in seconds + scope: 'test.app', +}; + +const authRequestMsg = await createAuthRequestMessage({ + address: account.address, // Your main wallet address + application: 'Test app', // Match domain name + ...authParams +}); + +// We need to capture channelId to close it. +let activeChannelId: string | undefined; + +// Helper function to trigger resize +const triggerResize = async (channelId: string, token: string, skipResize: boolean = false) => { + console.log(' Using existing channel:', channelId); + + // Add delay to ensure Node indexes the channel + console.log(' Waiting 5s for Node to index channel...'); + await new Promise(resolve => setTimeout(resolve, 5000)); + + // For withdrawal, we don't need to check user balance or allowance + // because the Node (counterparty) is the one depositing funds. + + + // For withdrawal, we don't deposit (we are withdrawing off-chain funds). + // ------------------------------------------------------------------- + // 3. Fund Channel (Resize) + // ------------------------------------------------------------------- + // We use 'allocate_amount' to move funds from the User's Unified Balance (off-chain) + // into the Channel. This assumes the user has funds in their Unified Balance (e.g. from faucet). + + const amountToFund = 20n; + if (!skipResize) console.log('\nRequesting resize to fund channel with 20 tokens...'); + + if (!skipResize) { + const resizeMsg = await createResizeChannelMessage( + sessionSigner, + { + channel_id: channelId as `0x${string}`, + // resize_amount: 10n, // <-- This requires L1 funds in Custody (which we don't have) + allocate_amount: amountToFund, // <-- This pulls from Unified Balance (Faucet) (Variable name adjusted) + funds_destination: account.address, + } + ); + + ws.send(resizeMsg); + + // Wait for resize confirmation + console.log(' Waiting for resize confirmation...'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Resize timeout')), 30000); + const handler = (data: any) => { + const msg = JSON.parse(data.toString()); + if (msg.res && msg.res[1] === 'resize_channel') { + const payload = msg.res[2]; + if (payload.channel_id === channelId) { + clearTimeout(timeout); + ws.off('message', handler); + resolve(); + } + } + }; + ws.on('message', handler); + }); + + // Wait for balance update + await new Promise(r => setTimeout(r, 2000)); + console.log('✓ Resize complete.'); + } else { + console.log(' Skipping resize step (already funded).'); + } + + // 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`); + + // 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); + } + + // ------------------------------------------------------------------- + // 4. Off-Chain Transfer + // ------------------------------------------------------------------- +}; + +// 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)); + + 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}...`); + + // const transferMsg = await createTransferMessage( + // sessionSigner, + // { + // destination: secondaryAddress, + // allocations: [{ + // asset: 'ytest.usd', + // amount: '10' + // }] + // }, + // Date.now() + // ); + // ws.send(transferMsg); + // console.log(' Sent transfer request...'); + + // if (response.res && response.res[1] === 'transfer') { + // console.log('✓ Transfer complete!'); + // console.log(' Amount: 10 USDC'); + + // 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.'); + // } + // } + + 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...'); + + // 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', + }); + + console.log('✓ Channel closed on-chain:', txHash); + + // Withdraw funds + console.log(' Withdrawing funds...'); + const token = state.allocations[0].token; + + await new Promise(r => setTimeout(r, 2000)); // Wait for close to settle + + 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); + } + + 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.'); + } + + process.exit(0); + } +}; + +// Start the flow +if (ws.readyState === WebSocket.OPEN) { + ws.send(authRequestMsg); +} else { + ws.on('open', () => { + ws.send(authRequestMsg); + }); +} +``` + +
+ +### 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), + }); + + // 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}`); + + 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, + }; + + 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); + } + } + + if (response.error) { + console.error('WS Error:', response.error); + } + } + }); +} + +main(); +``` +
diff --git a/versioned_docs/version-0.5.x/learn/index.mdx b/versioned_docs/version-0.5.x/learn/index.mdx new file mode 100644 index 0000000..469ff8b --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/index.mdx @@ -0,0 +1,78 @@ +--- +title: Learn +description: Master Yellow Network and state channel technology +sidebar_position: 1 +displayed_sidebar: learnSidebar +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Learn + +Welcome to the Yellow Network learning path. This section builds your understanding from fundamentals to advanced concepts. + +Yellow Network is a decentralized clearing and settlement infrastructure that operates as a Layer 3 overlay on top of existing blockchains. It enables businesses — brokers, exchanges, and application developers — to move digital assets across multiple blockchain networks through a unified peer-to-peer ledger, without relying on a centralized intermediary. The network is formed by independent node operators who run open-source clearnode software supplied by Layer3 Fintech Ltd. on their own infrastructure. + +--- + +## Introduction + +Start here to understand what Yellow Network solves and how it works. + +**[What Yellow Solves](./introduction/what-yellow-solves.mdx)** — Understand the core problems: scaling, cost, and speed. Learn why state channels are the answer for high-frequency applications. + +**[Architecture at a Glance](./introduction/architecture-at-a-glance.mdx)** — See how the three protocol layers (on-chain, off-chain, application) work together to enable fast, secure transactions. + +--- + +## Getting Started + +Get hands-on with Yellow Network in minutes. + +**[Quickstart: Your First Channel](./getting-started/quickstart.mdx)** — Create a state channel, perform an off-chain transfer, and verify the transaction in under 10 minutes. + +**[Prerequisites & Environment](./getting-started/prerequisites.mdx)** — Set up a complete development environment with Node.js, TypeScript, and the VirtualApp SDK. + +**[Key Terms & Mental Models](./getting-started/key-terms.mdx)** — Build your vocabulary and conceptual framework for understanding state channels. + +--- + +## Core Concepts + +Deep dive into the technology powering Yellow Network. + +**[State Channels vs L1/L2](./core-concepts/state-channels-vs-l1-l2.mdx)** — Compare state channels with Layer 1 and Layer 2 solutions. Understand when each approach is the right choice. + +**[App Sessions](./core-concepts/app-sessions.mdx)** — Multi-party application channels with custom governance and state management. + +**[Session Keys](./core-concepts/session-keys.mdx)** — Delegated keys for secure, gasless interactions without repeated wallet prompts. + +**[Challenge-Response & Disputes](./core-concepts/challenge-response.mdx)** — How Yellow Network handles disputes and ensures your funds are always recoverable. + +**[Message Envelope](./core-concepts/message-envelope.mdx)** — Overview of the Nitro RPC message format and communication protocol. + +--- + +## Next Steps + +After completing the Learn section, continue to: + +- **[Build](/docs/build/quick-start)** — Implement complete Yellow Applications +- **[Protocol Reference](/docs/protocol/introduction)** — Authoritative protocol specification + +--- + +## Quick Reference + +| Topic | Time | Difficulty | +|-------|------|------------| +| [What Yellow Solves](./introduction/what-yellow-solves) | 5 min | Beginner | +| [Architecture at a Glance](./introduction/architecture-at-a-glance) | 8 min | Beginner | +| [Quickstart](./getting-started/quickstart) | 10 min | Beginner | +| [Key Terms](./getting-started/key-terms) | 10 min | Beginner | +| [State Channels vs L1/L2](./core-concepts/state-channels-vs-l1-l2) | 12 min | Intermediate | +| [App Sessions](./core-concepts/app-sessions) | 8 min | Intermediate | +| [Session Keys](./core-concepts/session-keys) | 8 min | Intermediate | +| [Challenge-Response](./core-concepts/challenge-response) | 6 min | Intermediate | +| [Message Envelope](./core-concepts/message-envelope) | 5 min | Intermediate | diff --git a/versioned_docs/version-0.5.x/learn/introduction/_category_.json b/versioned_docs/version-0.5.x/learn/introduction/_category_.json new file mode 100644 index 0000000..558995f --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/introduction/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Introduction", + "position": 1, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.5.x/learn/introduction/architecture-at-a-glance.mdx b/versioned_docs/version-0.5.x/learn/introduction/architecture-at-a-glance.mdx new file mode 100644 index 0000000..df980db --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/introduction/architecture-at-a-glance.mdx @@ -0,0 +1,257 @@ +--- +sidebar_position: 2 +title: Architecture at a Glance +description: High-level overview of Yellow Network's three-layer architecture +keywords: [architecture, state channels, VirtualApp, Clearnode, smart contracts] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Architecture at a Glance + +In this guide, you will learn how Yellow Network's three protocol layers work together to enable fast, secure, off-chain transactions. + +--- + +## The Three Layers + +Yellow Network consists of three interconnected layers, each with a specific responsibility: + +```mermaid +graph TB + subgraph Application["Application Layer"] + direction TB + APP["Your Application
Games, Payments, DeFi"] + end + + subgraph OffChain["Off-Chain Layer"] + direction LR + CLIENT["Client SDK"] + BROKER["Clearnode"] + end + + subgraph OnChain["On-Chain Layer"] + direction TB + CONTRACTS["Custody & Adjudicator Contracts"] + end + + subgraph Blockchain["Blockchain Layer"] + direction TB + CHAIN["Ethereum, Polygon, Base, etc."] + end + + APP --> CLIENT + CLIENT <-->|"Nitro RPC Protocol"| BROKER + CLIENT -.->|"On-chain operations"| CONTRACTS + BROKER -.->|"Monitors events"| CONTRACTS + CONTRACTS --> CHAIN + + style Application fill:#e1f5ff,stroke:#9ad7ff,color:#111 + style OffChain fill:#fff4e1,stroke:#ffd497,color:#111 + style OnChain fill:#ffe1f5,stroke:#ffbde6,color:#111 + style Blockchain fill:#f0f0f0,stroke:#c9c9c9,color:#111 +``` + +| Layer | Purpose | Speed | Cost | +|-------|---------|-------|------| +| **Application** | Your business logic and user interface | — | — | +| **Off-Chain** | Instant state updates via Nitro RPC | < 1 second | Zero gas | +| **On-Chain** | Fund custody, disputes, final settlement | Block time | Gas fees | + +--- + +## On-Chain Layer: Security Foundation + +The on-chain layer provides cryptographic guarantees through smart contracts: + +### Custody Contract + +The **Custody Contract** is the core of VirtualApp's on-chain implementation. It handles: + +- **Channel Creation**: Lock funds and establish participant relationships +- **Dispute Resolution**: Process challenges and validate states +- **Final Settlement**: Distribute funds according to signed final state +- **Fund Management**: Deposit and withdrawal operations + +### Adjudicator Contracts + +**Adjudicators** validate state transitions according to application-specific rules: + +- **SimpleConsensus**: Both participants must sign (default for payment channels) +- **Custom Adjudicators**: Application-specific validation logic + +:::info On-Chain Operations +You only touch the blockchain for: + +1. Opening a channel (lock funds) +2. Resizing a channel (add or remove funds) +3. Closing a channel (unlock and distribute funds) +4. Disputing a state (if counterparty is uncooperative) + +::: + +--- + +## Off-Chain Layer: Speed and Efficiency + +The off-chain layer handles high-frequency operations without blockchain transactions. + +### 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: + +- Manages the Nitro RPC protocol for state channel operations +- Provides a unified balance across multiple chains +- Coordinates payment channels between users +- Hosts app sessions for multi-party applications + +### Nitro RPC Protocol + +**Nitro RPC** is a lightweight protocol optimized for state channel communication: + +- **Compact format**: JSON array structure reduces message size by ~30% +- **Signed messages**: Every request and response is cryptographically signed +- **Real-time updates**: Bidirectional communication via WebSocket + +```javascript +// Compact Nitro RPC format +[requestId, method, params, timestamp] + +// Example: Transfer 50 USDC +[42, "transfer", {"destination": "0x...", "amount": "50.0", "asset": "usdc"}, 1699123456789] +``` + +--- + +## How Funds Flow + +This diagram shows how your tokens move through the system: + +```mermaid +graph TB + A["User Wallet
(ERC-20)"] -->|"1. deposit"| B["Available Balance
(Custody Contract)"] + B -->|"2. resize"| C["Channel-Locked
(Custody Contract)"] + C <-->|"3. resize"| D["Unified Balance
(Clearnode)"] + D -->|"4. open session"| E["App Sessions
(Applications)"] + E -->|"5. close session"| D + D -->|"6. resize/close"| B + B -->|"7. withdraw"| A + + style A fill:#90EE90,stroke:#333,color:#111 + style B fill:#87CEEB,stroke:#333,color:#111 + style C fill:#FFD700,stroke:#333,color:#111 + style D fill:#DDA0DD,stroke:#333,color:#111 + style E fill:#FFA07A,stroke:#333,color:#111 +``` + +### Fund States + +| State | Location | What It Means | +|-------|----------|---------------| +| **User Wallet** | Your EOA | Full control, on-chain | +| **Available Balance** | Custody Contract | Deposited, ready for channels | +| **Channel-Locked** | Custody Contract | Committed to a specific channel | +| **Unified Balance** | Clearnode | Available for off-chain operations | +| **App Session** | Application | Locked in a specific app session | + +--- + +## Channel Lifecycle + +A payment channel progresses through distinct states: + +```mermaid +stateDiagram-v2 + [*] --> VOID + VOID --> ACTIVE: create() with both signatures + ACTIVE --> ACTIVE: Off-chain updates (zero gas) + ACTIVE --> ACTIVE: resize() (add/remove funds) + ACTIVE --> FINAL: close() (cooperative) + ACTIVE --> DISPUTE: challenge() (if disagreement) + DISPUTE --> ACTIVE: checkpoint() (newer state) + DISPUTE --> FINAL: Timeout expires + FINAL --> [*] + + note right of ACTIVE: This is where
99% of activity happens +``` + +:::info Legacy Flow +The diagram above shows the recommended flow where both participants sign the initial state, creating the channel directly in ACTIVE status. A legacy flow also exists where only the creator signs initially (status becomes INITIAL), and other participants call `join()` separately. See [Channel Lifecycle](/docs/protocol/app-layer/on-chain/channel-lifecycle) for details. +::: + +### Typical Flow + +1. **Create**: Both parties sign initial state → channel becomes ACTIVE +2. **Operate**: Exchange signed states off-chain (unlimited, zero gas) +3. **Close**: Both sign final state → funds distributed + +### Dispute Path (Rare) + +If your counterparty becomes unresponsive: + +1. **Challenge**: Submit your latest signed state on-chain +2. **Wait**: Challenge period (typically 24 hours) allows counterparty to respond +3. **Finalize**: If no newer state is submitted, your state becomes final + +--- + +## Communication Patterns + +### Opening a Channel + +```mermaid +sequenceDiagram + participant Client + participant Clearnode + participant Blockchain + + Client->>Clearnode: create_channel request + Clearnode->>Client: channel config + Clearnode signature + Client->>Client: Sign state + Client->>Blockchain: create() with BOTH signatures + Blockchain->>Blockchain: Verify, lock funds, emit event + Blockchain-->>Clearnode: Event detected + Clearnode->>Client: Channel now ACTIVE +``` + +### Off-Chain Transfer + +```mermaid +sequenceDiagram + participant Sender + participant Clearnode + participant Receiver + + Sender->>Clearnode: transfer(destination, amount) + Clearnode->>Clearnode: Validate, update ledger + Clearnode->>Sender: Confirmed ✓ + Clearnode->>Receiver: balance_update notification + + Note over Sender,Receiver: Complete in < 1 second, zero gas +``` + +--- + +## Key Takeaways + +| Concept | What to Remember | +|---------|------------------| +| **On-Chain** | Only for opening, closing, disputes—security layer | +| **Off-Chain** | Where all the action happens—speed layer | +| **Clearnode** | Your gateway to the network—coordination layer | +| **State Channels** | Lock once, transact unlimited times, settle once | + +:::success Security Guarantee +At every stage, funds remain cryptographically secured. You can always recover your funds according to the latest valid signed state, even if a Clearnode becomes unresponsive. +::: + +--- + +## Next Steps + +Ready to start building? Continue to: + +- **[Quickstart](../getting-started/quickstart.mdx)** — Create your first channel in minutes +- **[Prerequisites](../getting-started/prerequisites.mdx)** — Set up your development environment +- **[Core Concepts](../core-concepts/state-channels-vs-l1-l2.mdx)** — Deep dive into state channels diff --git a/versioned_docs/version-0.5.x/learn/introduction/supported-chains.mdx b/versioned_docs/version-0.5.x/learn/introduction/supported-chains.mdx new file mode 100644 index 0000000..55e0560 --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/introduction/supported-chains.mdx @@ -0,0 +1,301 @@ +--- +sidebar_position: 3 +title: Supported Chains & Assets +description: Complete list of supported blockchains and assets on Yellow Network +keywords: [supported chains, blockchains, assets, tokens, USDC, Base, Polygon, Ethereum, sandbox, production] +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Supported Chains & Assets + +This page lists all blockchains and assets currently supported on Yellow Network. + +--- + +## Environments + +Yellow Network operates two separate environments: + +| Environment | Purpose | URL | +|-------------|---------|-----| +| **Sandbox** | Development and testing | `wss://clearnet-sandbox.yellow.com/ws` | +| **Production** | Live operations with real assets | `wss://clearnet.yellow.com/ws` | + +:::info Environment Separation +- Sandbox uses **testnet** blockchains and test tokens (e.g., `ytest.usd`) +- Production uses **mainnet** blockchains and real assets (e.g., `usdc`) +- Assets and blockchains from one environment cannot be used in the other +::: + +--- + +## Supported Blockchains + + + + +| Blockchain | Chain ID | Status | +|------------|----------|--------| +| Base Sepolia | 84532 | ✅ Active | +| Polygon Amoy | 80002 | ✅ Active | +| Ethereum Sepolia | 11155111 | ✅ Active | + + + + +| Blockchain | Chain ID | Status | +|------------|----------|--------| +| Ethereum | 1 | ✅ Active | +| BNB Smart Chain | 56 | ✅ Active | +| Polygon | 137 | ✅ Active | +| World Chain | 480 | ✅ Active | +| Base | 8453 | ✅ Active | +| Linea | 59144 | ✅ Active | +| XRPL EVM Sidechain | 1440000 | ✅ Active | + + + + +--- + +## Supported Assets + + + + +| Asset | Symbol | Description | +|-------|--------|-------------| +| Yellow Test USD | `ytest.usd` | Test stablecoin for sandbox development | + +:::tip Getting Test Tokens +Use the Yellow Network faucet to receive `ytest.usd` tokens for testing in the Sandbox environment. +::: + + + + +| Asset | Symbol | Blockchains | Decimals | +|-------|--------|-------------|----------| +| USD Coin | `usdc` | Ethereum, BNB Smart Chain, Polygon, World Chain, Base, Linea, XRPL EVM | 6 | +| Tether USD | `usdt` | BNB Smart Chain, Base, Linea | 6 | +| Ethereum | `eth` | Base, Linea | 18 | +| Wrapped Ether | `weth` | BNB Smart Chain, Polygon | 18 | +| BNB | `bnb` | BNB Smart Chain | 18 | +| Chainlink | `link` | BNB Smart Chain | 18 | +| XRP | `xrp` | XRPL EVM Sidechain | 18 | +| Beatwav | `beatwav` | Ethereum, Polygon | 18 | + + + + +--- + +## Assets by Blockchain + + + + +### Base Sepolia (84532) + +| Asset | Symbol | +|-------|--------| +| Yellow Test USD | `ytest.usd` | + +### Polygon Amoy (80002) + +| Asset | Symbol | +|-------|--------| +| Yellow Test USD | `ytest.usd` | + + + + +### Ethereum (1) + +| Asset | Symbol | +|-------|--------| +| USD Coin | `usdc` | +| Beatwav | `beatwav` | + +### BNB Smart Chain (56) + +| Asset | Symbol | +|-------|--------| +| BNB | `bnb` | +| USD Coin | `usdc` | +| Tether USD | `usdt` | +| Wrapped Ether | `weth` | +| Chainlink | `link` | + +### Polygon (137) + +| Asset | Symbol | +|-------|--------| +| USD Coin | `usdc` | +| Wrapped Ether | `weth` | +| Beatwav | `beatwav` | + +### World Chain (480) + +| Asset | Symbol | +|-------|--------| +| USD Coin | `usdc` | + +### Base (8453) + +| Asset | Symbol | +|-------|--------| +| Ethereum | `eth` | +| USD Coin | `usdc` | +| Tether USD | `usdt` | + +### Linea (59144) + +| Asset | Symbol | +|-------|--------| +| Ethereum | `eth` | +| USD Coin | `usdc` | +| Tether USD | `usdt` | + +### XRPL EVM Sidechain (1440000) + +| Asset | Symbol | +|-------|--------| +| USD Coin | `usdc` | +| XRP | `xrp` | + + + + +--- + +## Contract Addresses + +Contract addresses vary by blockchain. See the [deployment repository](https://github.com/erc7824/nitrolite/tree/main/contract/deployments) for the latest addresses. + + + + +### Base Sepolia (84532) + +| Contract | Address | +|----------|---------| +| Custody | `0x019B65A265EB3363822f2752141b3dF16131b262` | +| Adjudicator | `0x7c7ccbc98469190849BCC6c926307794fDfB11F2` | + +### Polygon Amoy (80002) + +| Contract | Address | +|----------|---------| +| Custody | `0x019B65A265EB3363822f2752141b3dF16131b262` | +| Adjudicator | `0x7c7ccbc98469190849BCC6c926307794fDfB11F2` | + + + + +### Ethereum (1) + +| Contract | Address | +|----------|---------| +| Custody | `0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f` | +| Adjudicator | `0x14980dF216722f14c42CA7357b06dEa7eB408b10` | + +### BNB Smart Chain (56) + +| Contract | Address | +|----------|---------| +| Custody | `0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f` | +| Adjudicator | `0x14980dF216722f14c42CA7357b06dEa7eB408b10` | + +### Polygon (137) + +| Contract | Address | +|----------|---------| +| Custody | `0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6` | +| Adjudicator | `0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C` | + +### World Chain (480) + +| Contract | Address | +|----------|---------| +| Custody | `0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f` | +| Adjudicator | `0x14980dF216722f14c42CA7357b06dEa7eB408b10` | + +### Base (8453) + +| Contract | Address | +|----------|---------| +| Custody | `0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6` | +| Adjudicator | `0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C` | + +### Linea (59144) + +| Contract | Address | +|----------|---------| +| Custody | `0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f` | +| Adjudicator | `0x14980dF216722f14c42CA7357b06dEa7eB408b10` | + +### XRPL EVM Sidechain (1440000) + +| Contract | Address | +|----------|---------| +| Custody | `0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f` | +| Adjudicator | `0x14980dF216722f14c42CA7357b06dEa7eB408b10` | + + + + +--- + +## Requesting New Support + +Need support for a blockchain or asset not listed here? + +- **[Request Blockchain Support](/docs/manuals/request-blockchain-support)** — Guide for adding new blockchain networks +- **[Request Asset Support](/docs/manuals/request-asset-support)** — Guide for adding new tokens/assets + +--- + +## Quick Code Reference + +### Connecting to the Right Environment + +```typescript +import { Client } from "yellow-ts"; + +// Sandbox (for testing with ytest.usd) +const sandboxClient = new Client({ + url: 'wss://clearnet-sandbox.yellow.com/ws', +}); + +// Production (for real assets like usdc) +const productionClient = new Client({ + url: 'wss://clearnet.yellow.com/ws', +}); +``` + +### Using the Correct Asset + +```typescript +// Sandbox environment +const sandboxAllocations = [ + { participant: address1, asset: 'ytest.usd', amount: '100.0' }, + { participant: address2, asset: 'ytest.usd', amount: '0.0' } +]; + +// Production environment +const productionAllocations = [ + { participant: address1, asset: 'usdc', amount: '100.0' }, + { participant: address2, asset: 'usdc', amount: '0.0' } +]; +``` + +--- + +## See Also + +- [Quick Start Guide](/docs/build/quick-start) — Get started building with Yellow SDK +- [Multi-Party App Sessions](/docs/guides/multi-party-app-sessions) — Create multi-party application sessions +- [API Reference](/docs/api-reference) — Complete SDK documentation diff --git a/versioned_docs/version-0.5.x/learn/introduction/what-yellow-solves.mdx b/versioned_docs/version-0.5.x/learn/introduction/what-yellow-solves.mdx new file mode 100644 index 0000000..994980d --- /dev/null +++ b/versioned_docs/version-0.5.x/learn/introduction/what-yellow-solves.mdx @@ -0,0 +1,145 @@ +--- +sidebar_position: 1 +title: What Yellow Solves +description: Understand the core problems Yellow Network addresses - scaling, cost, and speed +keywords: [Yellow Network, state channels, blockchain scaling, off-chain, Web3] +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# What Yellow Solves + +In this guide, you will learn why Yellow Network exists, what problems it addresses, and how it provides a faster, cheaper way to build Web3 applications. Yellow Network is developed and maintained by Layer3 Fintech Ltd. and operated by independent node operators running the issuer's open-source software. + +--- + +## The Blockchain Scalability Problem + +Every blockchain transaction requires global consensus. While this guarantees security and decentralization, it creates three fundamental limitations: + +| Challenge | Impact on Users | +|-----------|-----------------| +| **High Latency** | Transactions take 15 seconds to several minutes for confirmation | +| **High Costs** | Gas fees spike during network congestion, making microtransactions impractical | +| **Limited Throughput** | Networks like Ethereum process ~15-30 transactions per second | + +For applications requiring real-time interactions—gaming, trading, micropayments—these constraints make traditional blockchain unusable as a backend. + +--- + +## How Yellow Network Solves This + +Yellow Network uses **state channels** to move high-frequency operations off-chain while preserving blockchain-level security guarantees. + +### The Core Insight + +Most interactions between parties don't need immediate on-chain settlement. Consider a chess game with a 10 USDC wager: + +- **On-chain approach**: Every move requires a transaction → 40+ transactions → $100s in fees +- **State channel approach**: Lock funds once, play off-chain, settle once → 2 transactions → minimal fees + +State channels let you execute unlimited off-chain operations between on-chain checkpoints. + +### What You Get + +| Feature | Benefit | +|---------|---------| +| **Instant Transactions** | Sub-second finality (< 1 second typical) | +| **Zero Gas Costs** | Off-chain operations incur no blockchain fees | +| **Unlimited Throughput*** | No consensus bottleneck limiting operations | +| **Blockchain Security** | Funds are always recoverable via on-chain contracts | + +*\*Theoretically unlimited—state channels have no blockchain consensus overhead. Real-world performance depends on signature generation speed, network latency between participants, and application complexity. We'll be publishing detailed benchmarks soon.* + +--- + +## The VirtualApp Protocol + +Yellow Network is built on **VirtualApp**, a state channel protocol designed for EVM-compatible chains. VirtualApp provides: + +- **Fund Custody**: Smart contracts that securely lock and release assets +- **Dispute Resolution**: Challenge-response mechanism ensuring fair outcomes +- **Final Settlement**: Cryptographic guarantees that final allocations are honored + +:::tip When to Use Yellow Network +Choose Yellow Network when your application needs: + +- Real-time interactions between users +- Microtransactions or streaming payments +- High transaction volumes without gas costs +- Multi-party coordination with instant settlement + +::: + +--- + +## Chain Abstraction with Clearnode + +A **Clearnode** is operated by independent node operators using the issuer's open-source software. It serves as your entry point to Yellow Network. When you connect to a Clearnode: + +1. **Deposit** tokens into the Custody Contract on any supported chain +2. **Resize** your channel to move funds to your unified balance +3. **Transact** instantly with any other user on the network +4. **Withdraw** back through the Custody Contract to any supported chain + +:::note Fund Flow +Funds flow through the Custody Contract (on-chain) before reaching your unified balance (off-chain). The `resize` operation moves funds between your on-chain available balance and your off-chain unified balance. See [Architecture](./architecture-at-a-glance#how-funds-flow) for the complete flow. +::: + +For example, deposit 50 USDC on Polygon and 50 USDC on Base—after resizing, your unified balance shows 100 USDC. You can then withdraw all 100 USDC to Arbitrum if you choose. + +```mermaid +graph LR + A["Deposit on Polygon
50 USDC"] --> C["Unified Balance
100 USDC"] + B["Deposit on Base
50 USDC"] --> C + C --> D["Withdraw to Arbitrum
100 USDC"] + + style C fill:#90EE90,stroke:#333,color:#111 +``` + +--- + +## Real-World Applications + +### Payment Applications + +- **Micropayments**: Pay-per-article, API usage billing, content monetization +- **Streaming payments**: Subscription services, hourly billing, real-time payroll +- **P2P transfers**: Instant remittances without intermediaries + +### Gaming Applications + +- **Turn-based games**: Chess, poker, strategy games with wagers +- **Real-time multiplayer**: In-game economies with instant transactions +- **Tournaments**: Prize pools and automated payouts + +### DeFi Applications + +- **High-frequency trading**: Execute trades without MEV concerns +- **Prediction markets**: Real-time betting with instant settlement +- **Escrow services**: Multi-party coordination with dispute resolution + +--- + +## Security Model + +Yellow Network maintains blockchain-level security despite operating off-chain: + +| Guarantee | How It's Achieved | +|-----------|-------------------| +| **Fund Safety** | All funds locked in audited smart contracts | +| **Dispute Resolution** | Challenge period allows contesting incorrect states | +| **Cryptographic Proof** | Every state transition is signed by participants | +| **Recovery Guarantee** | Users can always recover funds via on-chain contracts | + +If a Clearnode becomes unresponsive or malicious, you can submit your latest signed state to the blockchain and recover your funds after a challenge period. + +--- + +## Next Steps + +Now that you understand what Yellow solves, continue to: + +- **[Architecture at a Glance](./architecture-at-a-glance.mdx)** — See how the protocol layers work together +- **[Quickstart](../getting-started/quickstart.mdx)** — Create your first state channel in minutes diff --git a/versioned_docs/version-0.5.x/manuals/_category_.json b/versioned_docs/version-0.5.x/manuals/_category_.json new file mode 100644 index 0000000..176732f --- /dev/null +++ b/versioned_docs/version-0.5.x/manuals/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Manuals", + "position": 4, + "link": { + "type": "doc", + "id": "manuals/index" + } +} \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/manuals/index.md b/versioned_docs/version-0.5.x/manuals/index.md new file mode 100644 index 0000000..04d75bf --- /dev/null +++ b/versioned_docs/version-0.5.x/manuals/index.md @@ -0,0 +1,13 @@ +--- +title: Manuals +description: Comprehensive manuals and documentation +displayed_sidebar: manualsSidebar +--- + +# Manuals + +:::info Work in Progress +This section is currently under development. Comprehensive manuals and documentation will be available soon. +::: + +Coming soon: Detailed manuals covering all aspects of the platform. \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/manuals/request-asset-support.md b/versioned_docs/version-0.5.x/manuals/request-asset-support.md new file mode 100644 index 0000000..db9c19c --- /dev/null +++ b/versioned_docs/version-0.5.x/manuals/request-asset-support.md @@ -0,0 +1,169 @@ +--- +title: Requesting Asset Support +description: Guide for requesting asset and token support in Clearnode Sandbox or Production environments +sidebar_label: Request Asset Support +keywords: [clearnode, asset, token, configuration, ERC20, production, sandbox] +--- + +# Requesting Asset Support + +This guide is primarily for **project teams, token issuers, and integration partners** who need to add support for new assets or tokens in the Clearnode infrastructure. Following this process will enable Clearnode to recognize and process transfers of your asset across supported blockchain networks. + +## What Happens When Support is Added + +Once an asset is supported in Clearnode: + +- Users can deposit and withdraw the asset through Clearnode's state channels +- The asset becomes available for instant, off-chain transfers via the VirtualApp protocol +- The asset can be used in cross-chain operations (deposit on one chain, withdraw on another) for blockchains it was configured on +- Applications built on Yellow Network can integrate the asset for payments and settlements +- The asset's unified balance is tracked across all supported blockchains + +:::info ERC20 Compatibility +Adding support for ERC20 tokens is straightforward and requires only configuration changes. Non-standard token implementations may require additional development work and testing. +::: + +:::warning Liquidity Requirements for Withdrawals +For a token to be immediately withdrawable on a specific blockchain, Clearnode must be provided with sufficient liquidity on that chain. Without liquidity, users can deposit tokens but cannot withdraw them until liquidity is available. Please contact our Business team to arrange liquidity provision before requesting asset support. +::: + +## Understanding Environments + +Clearnode operates in two distinct environments with separate asset configurations: + +| Environment | Purpose | Typical Assets | +|-------------|---------|----------------| +| **Sandbox** | Development, testing, and experimentation | Testnet tokens (Sepolia USDC, Amoy ETH, test tokens, etc.) | +| **Production** | Live operations with real assets | Mainnet tokens (USDC, USDT, DAI, etc.) | + +**Configuration file location:** `clearnode/chart/config//assets.yaml` + +**Important Considerations:** + +- **Production will not support test network tokens** - mainnet tokens only +- **Sandbox will not support mainnet tokens** - testnet tokens only +- You must decide which environment needs the asset support based on your use case +- If you need support in both environments (e.g., testnet token for development, mainnet token for production), you must submit configuration changes to both files + +## How to Request Support + +Asset support is requested by creating or modifying a configuration file in the [nitrolite](https://github.com/erc7824/nitrolite) repository: + +1. **Fork the repository**: `https://github.com/erc7824/nitrolite` +2. **Navigate to the appropriate configuration file**: + - For Sandbox: `clearnode/chart/config/sandbox/assets.yaml` + - For Production: `clearnode/chart/config/prod/assets.yaml` +3. **Add your asset configuration** at the end of the list (see next section for structure) +4. **Submit a Pull Request** with a clear description of the asset being added +5. **Wait for review** by the development team + +The next section provides detailed guidance on the configuration structure and whether you need to add a new asset or just a token. + +## Asset Configuration Structure + +### Understanding Assets vs Tokens + +In Clearnode's configuration model: + +- An **asset** represents a logical currency or token type (e.g., "USDC", "ETH") +- A **token** is a specific implementation of that asset on a particular blockchain + +**Example:** USDC is an asset that has different token implementations: + +- USDC on Polygon at address `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` +- USDC on Base at address `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` +- USDC on Ethereum at address `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` + +### Before You Start: Check if the Asset Exists + +Before adding a new asset, check if Clearnode already supports the asset on other blockchains. If the asset exists but your target blockchain is not listed, you only need to add a **token entry** to the existing asset. If the asset doesn't exist at all, you need to add both the **asset** and its **token(s)**. + +#### Scenario 1: Asset exists, add a token to a new blockchain + +```yaml +assets: + - name: "USD Coin" + symbol: "usdc" + tokens: + - blockchain_id: 137 + address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" + decimals: 6 + # Add your new token here + - blockchain_id: 8453 # Base + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + decimals: 6 +``` + +#### Scenario 2: New asset, add both asset and token(s) + +```yaml +assets: + - name: "My New Token" + symbol: "mnt" + tokens: + - blockchain_id: 137 + address: "0xYourTokenAddressOnPolygon" + decimals: 18 +``` + +### Configuration Fields + +#### Asset Level + +Each asset entry requires the following fields: + +```yaml +assets: + - name: "USD Coin" # Human-readable name + symbol: "usdc" # Ticker symbol (lowercase) + disabled: false # If set to `true`, then it is not loaded into configuration + tokens: [...] # Array of token implementations +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | No | Human-readable name of the asset (e.g., "USD Coin"). If omitted, defaults to the symbol. | +| `symbol` | **Yes** | Ticker symbol for the asset (must be lowercase, e.g., "usdc", "eth") | +| `disabled` | No | Set to `true` to temporarily disable processing this asset (default: `false`) | +| `tokens` | **Yes** | Array of blockchain-specific token implementations | + +#### Token Level + +Each token within an asset requires the following fields: + +```yaml +tokens: + - name: "USD Coin on Polygon" # Token-specific name + symbol: "usdc" # Token-specific symbol + blockchain_id: 137 # Chain ID + disabled: false # Skip processing (default: false) + address: "0x3c499c542cEF..." # Contract address + decimals: 6 # Token decimals +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | No | Token name on this blockchain (inherits from asset if not specified) | +| `symbol` | No | Token symbol on this blockchain (inherits from asset if not specified) | +| `blockchain_id` | **Yes** | Chain ID where this token is deployed (must match a supported blockchain) | +| `disabled` | No | Set to `true` to temporarily disable this token (default: `false`) | +| `address` | **Yes** | Token's smart contract address (must be a valid address on the specified chain) | +| `decimals` | **Yes** | Number of decimal places the token uses (e.g., 6 for USDC, 18 for ETH) | + +### Prerequisites for Adding Assets + +Before submitting your configuration: + +1. **Blockchain Support**: Ensure the blockchain (by `blockchain_id`) is already supported in `blockchains.yaml` +2. **Token Deployment**: The token contract must be deployed and verified on the target blockchain +3. **ERC20 Compliance**: Token should follow the standard ERC20 interface for seamless integration +4. **Correct Decimals**: Verify the token's decimal places (commonly 6 or 18, but varies by token) +5. **Valid Address**: Double-check the contract address is correct and matches the intended blockchain + +:::warning Blockchain Must Be Supported First +You cannot add a token on a blockchain that is not yet supported in Clearnode. If your target blockchain is not in the `blockchains.yaml` configuration, you must first follow the [Request Blockchain Support](./request-blockchain-support.md) guide before requesting asset support. +::: + +## Need Help? + +If you have questions about asset support requests, encounter issues during configuration, or need clarification on any part of this process, please don't hesitate to contact the development team. diff --git a/versioned_docs/version-0.5.x/manuals/request-blockchain-support.md b/versioned_docs/version-0.5.x/manuals/request-blockchain-support.md new file mode 100644 index 0000000..065fe16 --- /dev/null +++ b/versioned_docs/version-0.5.x/manuals/request-blockchain-support.md @@ -0,0 +1,166 @@ +--- +title: Requesting Blockchain Support +description: Guide for requesting blockchain network support in Clearnode Sandbox or Production environments +sidebar_label: Request Blockchain Support +keywords: [clearnode, blockchain, configuration, EVM, production, sandbox] +--- + +# Requesting Blockchain Support + +This guide is primarily for **blockchain integrators, infrastructure teams, and project partners** who need to add support for new blockchain networks in the Clearnode infrastructure. Following this process will enable Clearnode to connect to your blockchain and provide off-chain clearing and settlement services. + +## What Happens When Support is Added + +Once a blockchain is supported in Clearnode: + +- Clearnode can establish RPC connections to the specified blockchain network +- The VirtualApp protocol smart contracts (custody, adjudicator, and balance checker) will be monitored on that chain +- Users can deposit and withdraw assets on that blockchain through Clearnode +- State channels can leverage that blockchain for on-chain settlement when needed +- Assets deployed on the supported blockchain become available for cross-chain clearing + +:::warning Non-EVM and Limited Infrastructure Support +If your blockchain support request concerns a **non-EVM blockchain** or a blockchain with **limited infrastructure support** (e.g., scarce RPC providers, incomplete tooling, experimental networks), **you must contact the development team before proceeding** with this guide. These cases require custom development and cannot be handled through standard configuration. + +Contact the dev team via [GitHub Issues](https://github.com/layer-3/nitrolite/issues). +::: + +## Understanding Environments + +Clearnode operates in two distinct environments with separate configurations: + +| Environment | Purpose | Typical Networks | +|-------------|---------|------------------| +| **Sandbox** | Development, testing, and experimentation | Testnets (Sepolia, Polygon Amoy, etc.) | +| **Production** | Live operations with real assets | Mainnets (Ethereum, Polygon, Base, Linea, etc.) | + +**Configuration file location:** `clearnode/chart/config//blockchains.yaml` + +**Important Considerations:** + +- **Production will not support test networks** - mainnet blockchains only +- **Sandbox will not support mainnet networks** - testnet blockchains only +- You must decide which environment needs the blockchain support based on your use case +- If you need support in both environments (e.g., testnet for development, mainnet for production), you must submit configuration changes to both files + +## How to Request Support + +Blockchain support is requested by creating or modifying a configuration file in the [nitrolite](https://github.com/erc7824/nitrolite) repository: + +1. **Fork the repository**: `https://github.com/erc7824/nitrolite` +2. **Navigate to the appropriate configuration file**: + - For Sandbox: `clearnode/chart/config/sandbox/blockchains.yaml` + - For Production: `clearnode/chart/config/prod/blockchains.yaml` +3. **Add your blockchain configuration** at the end of the `blockchains` list (see next section for structure) +4. **Submit a Pull Request** with a clear description of the blockchain being added +5. **Wait for review** by the development team + +The next section provides detailed guidance on the configuration structure. + +## Blockchain Configuration Structure + +### Overview + +The `blockchains.yaml` file contains two main sections: + +- `default_contract_addresses`: Default smart contract addresses applied to all blockchains (unless overridden) +- `blockchains`: Array of blockchain configurations + +### Configuration Fields + +Each blockchain entry requires the following fields: + +```yaml +blockchains: + - name: polygon # Blockchain name (lowercase, underscores allowed) + id: 137 # Chain ID for validation + disabled: false # Whether to disable (default: false) + block_step: 10000 # Block range for scanning (default: 10000) + contract_addresses: # Override default contract addresses + custody: "0x..." + adjudicator: "0x..." + balance_checker: "0x..." +``` + +**Field Descriptions:** + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | **Yes** | Unique identifier for the blockchain (lowercase, underscores allowed, e.g., `polygon`, `base`, `arbitrum_one`) | +| `id` | **Yes** | Chain ID used for validation (must match the blockchain's official chain ID) | +| `disabled` | No | Set to `true` to disable or `false` to enable (default: `false`) | +| `block_step` | No | Number of blocks to scan per query when monitoring events (default: `10000`). Adjust based on blockchain or RPC provider performance. | +| `contract_addresses` | No | Override default contract addresses for this specific blockchain | + +### Contract Deployment + +When requesting the addition of a new blockchain, addresses of the infrastructure smart contracts must be provided: + +- **Custody Contract**: Manages user deposits and withdrawals +- **Adjudicator Contract**: Handles dispute resolution for state channels +- **Balance Checker Contract**: Provides efficient balance queries + +:::info Smart contract deployment +For now, you don't need to deploy these contracts yourself. The development team will handle contract deployment on the new blockchain as part of the support process. + +You can submit your request with smart contract addresses set to placeholder values (e.g., `0x0000000000000000000000000000000000000000`). The team will replace them with the actual deployed addresses during integration. +::: + +:::info Coming Soon: Cross-Chain Contract Deployment Tool +We are developing a tool to simplify the deployment of VirtualApp protocol smart contracts across multiple blockchains with deterministic addresses. This will enable deploying contracts to the same address on different chains, making configuration management significantly easier. +::: + +Read on to learn how to specify contract addresses in the configuration. + +You have two options for providing contract addresses: + +#### Option 1: Using Default Contract Addresses + +If you deploy contracts at the addresses specified in the `default_contract_addresses`, you don't need to specify `contract_addresses` in each blockchain entry. + +```yaml +default_contract_addresses: + custody: "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + adjudicator: "0xcbbc03a873c11beeFA8D99477E830be48d8Ae6D7" + balance_checker: "0x2352c63A83f9Fd126af8676146721Fa00924d7e4" + +blockchains: + - name: polygon + id: 137 + enabled: true + - name: base + id: 8453 + enabled: true +``` + +This approach is cleaner when contracts are deployed at identical addresses. + +#### Option 2: Blockchain-Specific Contract Addresses + +If contract addresses differ on your blockchain, specify them individually: + +```yaml +blockchains: + - name: polygon + id: 137 + enabled: true + contract_addresses: + custody: "0xPolygonCustodyAddress..." + adjudicator: "0xPolygonAdjudicatorAddress..." + balance_checker: "0xPolygonBalanceCheckerAddress..." + - name: base + id: 8453 + enabled: true + contract_addresses: + custody: "0xBaseCustodyAddress..." + adjudicator: "0xBaseAdjudicatorAddress..." + balance_checker: "0xBaseBalanceCheckerAddress..." +``` + +:::warning Contract Address Requirements +Each blockchain **must have all three contract addresses configured** either through `default_contract_addresses` or blockchain-specific `contract_addresses`. If defaults are not provided, every blockchain must explicitly define all three addresses. Missing contract addresses will cause Clearnode to fail on startup. +::: + +## Need Help? + +If you have questions about blockchain support requests, encounter issues during integration, or need clarification on any part of this process, please don't hesitate to contact the development team. diff --git a/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md b/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md new file mode 100644 index 0000000..42912a4 --- /dev/null +++ b/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md @@ -0,0 +1,222 @@ +# Running Clearnode Locally + +This manual explains how to run a Clearnode locally using Docker Compose for development and testing purposes. Clearnode is an open-source implementation of a message broker node developed and maintained by Layer3 Fintech Ltd., providing ledger services for the VirtualApp protocol, which enables efficient off-chain payment channels with on-chain settlement capabilities. Independent node operators run this software on their own infrastructure to provide network services. + +## Prerequisites + +- Docker and Docker Compose installed on your system +- Git (to clone the repository) + +## Quick Start + +### 1. Clone the Repository + +```bash +git clone https://github.com/erc7824/nitrolite.git +cd virtualapp/clearnode +``` + +### 2. Configuration Setup + +Create a configuration directory: + +```bash +cp -r config/compose/example config/compose/local +``` + +### 3. Configure Blockchain Connections + +Edit `config/compose/local/blockchains.yaml` to configure your blockchain connections. Here's an example: + +```yaml +default_contract_addresses: + custody: "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + adjudicator: "0xcbbc03a873c11beeFA8D99477E830be48d8Ae6D7" + balance_checker: "0x2352c63A83f9Fd126af8676146721Fa00924d7e4" + +blockchains: + - name: polygon + id: 137 + disabled: false + block_step: 10000 + - name: base + id: 8453 + disabled: true +``` + +### 4. Configure Assets + +Edit `config/compose/local/assets.yaml` to configure supported assets: + +```yaml +assets: + - name: "USD Coin" + symbol: "usdc" + tokens: + - blockchain_id: 137 + address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" + decimals: 6 + - name: "Wrapped Ether" + symbol: "weth" + tokens: + - blockchain_id: 137 + address: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619" + decimals: 18 +``` + +### 5. Environment Variables + +Create a `.env` file in `config/compose/local/.env` with the following: + +```bash +# Required +BROKER_PRIVATE_KEY=your_private_key_here + +# Add RPC endpoints for each enabled blockchain +POLYGON_BLOCKCHAIN_RPC=wss://my-polygon-rpc.example.com +# BASE_BLOCKCHAIN_RPC=wss://my-base-rpc.example.com + +# Optional configuration +CLEARNODE_LOG_LEVEL=info +``` + +### 6. Start Services + +Run the following command to start all services: + +```bash +docker compose up +``` + +This will start: +- Clearnode service on port 8000 (WebSocket/HTTP) +- PostgreSQL database +- Prometheus metrics on port 4242 + +### 7. Stop Services + +To stop all services: + +```bash +docker compose down +``` + +## Configuration Reference + +### Environment Variables + +| Variable | Description | Required | Default | +|------------------------------------|--------------------------------------------------|----------|--------------| +| `BROKER_PRIVATE_KEY` | Private key used for signing broker messages | Yes | - | +| `DATABASE_DRIVER` | Database driver to use (postgres/sqlite) | No | sqlite | +| `CLEARNODE_CONFIG_DIR_PATH` | Path to directory containing configuration files | No | . | +| `CLEARNODE_DATABASE_URL` | Database connection string | No | clearnode.db | +| `CLEARNODE_LOG_LEVEL` | Logging level (debug, info, warn, error) | No | info | +| `HTTP_PORT` | Port for the HTTP/WebSocket server | No | 8000 | +| `METRICS_PORT` | Port for Prometheus metrics | No | 4242 | +| `MSG_EXPIRY_TIME` | Time in seconds for message timestamp validation | No | 60 | +| `_BLOCKCHAIN_RPC` | RPC endpoint for each enabled blockchain | Yes | - | + +### Blockchain Configuration (blockchains.yaml) + +**Configuration Structure:** + +- **default_contract_addresses**: That's the optional set of default contract addresses applied to all blockchains unless overridden + - `custody`: Custody contract address + - `adjudicator`: Adjudicator contract address + - `balance_checker`: Balance checker contract address + +- **blockchains**: Array of blockchain configurations + - `name`: Blockchain name (required; lowercase, underscores allowed) + - `id`: Chain ID for validation (required) + - `disabled`: Whether to disable this blockchain (optional, default: false) + - `block_step`: Block range for scanning (optional, default: 10000) + - `contract_addresses`: Override default addresses for this specific blockchain (optional) + - `custody`: Custody contract address + - `adjudicator`: Adjudicator contract address + - `balance_checker`: Balance checker contract address + +:::warning +Even though both `default_contract_addresses` and blockchain-specific `contract_addresses` are described as optional, each blockchain must have all required contract addresses set. If no defaults are provided under `default_contract_addresses`, you must specify `custody`, `adjudicator`, and `balance_checker` addresses for every blockchain in its `contract_addresses` section. Otherwise, Clearnode will fail to start due to missing contract address configuration. +::: + +RPC endpoints follow the pattern: `_BLOCKCHAIN_RPC` + +Example: +```bash +MY_NETWORK_BLOCKCHAIN_RPC=wss://my-network-rpc.example.com +``` + +### Asset Configuration (assets.yaml) + +**Configuration Structure:** + +- **assets**: Array of asset configurations + - `name`: Human-readable name of the asset (e.g., "USD Coin") + - `symbol`: Ticker symbol for the asset (required; lowercase, e.g., "usdc") + - `disabled`: Whether to skip processing this asset (optional, default: false) + - `tokens`: Array of blockchain-specific token implementations + - `name`: Token name on this blockchain (optional, inherits from asset) + - `symbol`: Token symbol on this blockchain (optional, inherits from asset) + - `blockchain_id`: Chain ID where this token is deployed (required) + - `disabled`: Whether to skip processing this token (optional, default: false) + - `address`: Token's contract address (required) + - `decimals`: Number of decimal places for the token (required) + +**Asset Token Inheritance:** +- If a token's `name` is not specified, it uses the asset's `name` +- If a token's `symbol` is not specified, it uses the asset's `symbol` +- If an asset's `name` is not specified, it defaults to the asset's `symbol` + +## Key Features + +- **Multi-Chain Support**: Connect to multiple EVM blockchains simultaneously +- **Off-Chain Payments**: Efficient payment channels for high-throughput transactions +- **Virtual Applications**: Create multi-participant applications +- **Message Forwarding**: Bi-directional message routing between participants +- **Flexible Database**: Support for both PostgreSQL and SQLite +- **Prometheus Metrics**: Built-in monitoring on port 4242 +- **Quorum-Based Signatures**: Multi-signature schemes with weight-based quorums + +## Troubleshooting + +### Common Issues + +1. **Port Conflicts**: If you encounter port conflicts, check which services are running on ports 8000 (HTTP/WebSocket) or 4242 (metrics) and either stop them or modify the ports in docker-compose.yml + +2. **RPC Configuration**: Ensure RPC endpoints match the pattern `_BLOCKCHAIN_RPC` and that the chain ID matches your configuration + +3. **Configuration Files**: Make sure `blockchains.yaml` and `assets.yaml` are properly formatted YAML files in your CONFIG_DIR_PATH + +4. **Database Connection**: If using PostgreSQL, ensure the database service is running and accessible + +### Useful Commands + +Check service status: + +```bash +docker-compose ps +``` + +View logs for a specific service: + +```bash +docker-compose logs -f +``` + +Restart a specific service: + +```bash +docker-compose restart +``` + +Clean up (remove containers, networks, and volumes): + +```bash +docker-compose down -v +``` + +## Development Tips + +1. **Debug Mode**: Set `CLEARNODE_LOG_LEVEL=debug` for verbose logging +2. **Database Access**: Use a database client to connect to `localhost:5432` with PostgreSQL credentials diff --git a/versioned_docs/version-0.5.x/protocol/_category_.json b/versioned_docs/version-0.5.x/protocol/_category_.json new file mode 100644 index 0000000..e69de29 diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/_category_.json b/versioned_docs/version-0.5.x/protocol/app-layer/_category_.json new file mode 100644 index 0000000..bcc7d72 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "App Layer (VirtualApp)", + "position": 5, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/_category_.json b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/_category_.json new file mode 100644 index 0000000..a6fb890 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Off-Chain RPC Protocol", + "position": 2, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/app-sessions.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/app-sessions.mdx new file mode 100644 index 0000000..e143e7e --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/app-sessions.mdx @@ -0,0 +1,761 @@ +--- +sidebar_position: 6 +title: App Session Methods +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# App Session Methods + +App sessions enable multi-party applications with custom governance rules, allowing complex interactions on top of payment channels. + +--- + +## Overview + +App sessions are off-chain channels built on top of the unified balance, intended for app developers to create application-specific interactions. They act as a "box" or shared account where multiple participants can transfer funds and execute custom logic with governance rules. + +### Key Features + +**Multi-Party Governance**: Define custom voting weights and quorum rules for state updates. + +**Application-Specific State**: Store arbitrary application data (game state, escrow conditions, etc.). + +**Flexible Fund Management**: Transfer, redistribute, add, or withdraw funds during session lifecycle. + +**Instant Updates**: All state changes happen off-chain with zero gas fees. + +:::info For App Developers +App sessions are specifically designed for app developers building trustless multi-party applications like games, prediction markets, escrow, and collaborative finance. +::: + +--- + +## Protocol Versions + +App sessions support multiple protocol versions for backward compatibility. + +### Version Comparison + +| Feature | NitroRPC/0.2 (Legacy) | NitroRPC/0.4 (Current) | +|---------|----------------------|------------------------| +| **State Updates** | Basic only | Intent-based (OPERATE, DEPOSIT, WITHDRAW) | +| **Add Funds to Active Session** | ❌ No | ✅ Yes (DEPOSIT intent) | +| **Remove Funds from Active Session** | ❌ No | ✅ Yes (WITHDRAW intent) | +| **Fund Redistribution** | ✅ Yes | ✅ Yes (OPERATE intent) | +| **Error Handling** | Basic | Enhanced validation | +| **Modify Total Funds** | Must close & recreate | Can update during session | +| **Recommended For** | Legacy support only | All new implementations | + +:::caution Protocol Version Selection +The protocol version is specified in the app definition during creation and **cannot be changed** for an existing session. Always use **NitroRPC/0.4** for new app sessions. +::: + +--- + +## create_app_session + +### Name + +`create_app_session` + +### Usage + +Creates a new virtual application session on top of the unified balance. An app session is a "box" or shared account where multiple participants can transfer funds and execute application-specific logic with custom governance rules. The app definition specifies participants, their voting weights, quorum requirements for state updates, and the protocol version. Funds are transferred from participants' unified balance accounts to a dedicated App Session Account for the duration of the session. App sessions enable complex multi-party applications like games, prediction markets, escrow, and collaborative finance—all operating off-chain with instant state updates and zero gas fees. + +### When to Use + +When multiple participants need to interact with shared funds and application state in a trustless manner. Examples include turn-based games, betting pools, escrow arrangements, multi-signature treasuries, prediction markets, and any application requiring multi-signature state management. + +### Prerequisites + +- All participants with non-zero initial allocations must be [authenticated](./authentication) +- All such participants must have sufficient available balance +- All such participants must sign the creation request +- Protocol version must be supported (NitroRPC/0.2 or NitroRPC/0.4) + +### Request + +:::tip Quick Reference +Common structures: [AppDefinition](#appdefinition) • [Allocation](#allocation) +::: + +| Parameter | Type | Required | Description | See Also | +|-----------|------|----------|-------------|----------| +| `definition` | AppDefinition | Yes | Configuration defining the app session rules and participants | [↓ Structure](#appdefinition) | +| `allocations` | Allocation[] | Yes | Initial funds to transfer from participants' unified balance accounts | [↓ Structure](#allocation) | +| `session_data` | string | No | Application-specific initial state (JSON string, max 64KB recommended)
This is application-specific; protocol doesn't validate content | — | + +#### Session Identifier {#session-identifier} + +`app_session_id` is derived deterministically from the entire App definition: + +```javascript +appSessionId = keccak256(JSON.stringify({ + application: "...", + protocol: "NitroRPC/0.4", + participants: [...], + weights: [...], + quorum: 100, + challenge: 86400, + nonce: 123456 +})) +``` + +- Includes `application`, `protocol`, `participants`, `weights`, `quorum`, `challenge`, and `nonce` +- Does **not** include `chainId` because sessions live entirely off-chain +- Client can recompute locally to verify clearnode responses +- `nonce` uniqueness is critical: same definition ⇒ same ID + +Implementation reference: `clearnode/app_session_service.go`. + +#### AppDefinition {#appdefinition} + +| Field | Type | Required | Description | Default | Allowed Values | Notes | +|-------|------|----------|-------------|---------|----------------|-------| +| `protocol` | string | Yes | Protocol version for this app session | — | `"NitroRPC/0.2"` \| `"NitroRPC/0.4"` | Version cannot be changed after creation; use 0.4 for new sessions | +| `participants` | address[] | Yes | Array of all participant wallet addresses | — | Min: 2 participants | Order is important - indices used for signatures and weights
Last participant often represents the application/judge | +| `weights` | int64[] | Yes | Voting power for each participant | — | — | Length must match participants array
Order corresponds to participants array
Absolute values matter for quorum; don't need to sum to 100 | +| `quorum` | uint64 | Yes | Minimum total weight required to approve state updates | — | — | Sum of signers' weights must be ≥ quorum | +| `challenge` | uint64 | No | Challenge period in seconds for disputes | 86400 (24 hours) | — | Only relevant if app session state is ever checkpointed on-chain | +| `nonce` | uint64 | Yes | Unique identifier | — | — | Typically timestamp; ensures uniqueness | + +**Example**: +```json +{ + "protocol": "NitroRPC/0.4", + "participants": ["0x742d35Cc...", "0x8B3192f2...", "0x456789ab..."], + "weights": [50, 50, 100], + "quorum": 100, + "challenge": 3600, + "nonce": 1699123456789 +} +``` + +#### Allocation {#allocation} + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `participant` | address | Yes | Participant wallet address (must be in `definition.participants`) | +| `asset` | string | Yes | Asset identifier (e.g., `"usdc"`) | +| `amount` | string | Yes | Amount in human-readable format (e.g., `"100.0"`) | + +**Example**: +```json +[ + {"participant": "0x742d35Cc...", "asset": "usdc", "amount": "100.0"}, + {"participant": "0x8B3192f2...", "asset": "usdc", "amount": "100.0"}, + {"participant": "0x456789ab...", "asset": "usdc", "amount": "0.0"} + ] +``` + +**Note**: Participants with zero allocation don't need to sign creation. + +### Response + +| Parameter | Type | Description | Format/Structure | Example | Notes | +|-----------|------|-------------|------------------|---------|-------| +| `app_session_id` | string | Unique identifier for the created app session | 0x-prefixed hex string (32 bytes) | `"0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba"` | Use this for all subsequent operations on this session | +| `status` | string | App session status | `"open"` | `"open"` | Values: `"open"` or `"closed"` | +| `version` | number | Current state version | `1` | `1` | Always starts at 1 | + +The Go service returns only these fields on creation. To fetch full metadata (application, participants, quorum, weights, session_data, protocol, challenge, nonce, timestamps), call [`get_app_sessions`](./queries#get_app_sessions) after creation. + +--- + +## Governance Models + +App sessions support flexible governance through custom weights and quorum configurations. + +### Example 1: Simple Two-Player Game + +``` +Participants: [Alice, Bob] +Weights: [1, 1] +Quorum: 2 + +Result: Both players must sign every state update +Use case: Chess, poker, betting between two parties +``` + +**Governance**: Cooperative - both parties must agree to all changes. + +### Example 2: Game with Judge + +``` +Participants: [Alice, Bob, Judge] +Weights: [0, 0, 100] +Quorum: 100 + +Result: Only judge can update state +Use case: Games where application determines outcome +``` + +**Governance**: Authoritative - application/judge has full control. + +### Example 3: Multi-Party Escrow + +``` +Participants: [Buyer, Seller, Arbiter] +Weights: [40, 40, 50] +Quorum: 80 + +Result: Any 2 parties can approve + - Buyer + Seller (80) + - Buyer + Arbiter (90) + - Seller + Arbiter (90) +Use case: Escrowed transactions with dispute resolution +``` + +**Governance**: Flexible 2-of-3 - any two can proceed, preventing single-party blocking. + +### Example 4: Weighted Multi-Party Voting + +``` +Participants: [User1, User2, User3, User4, Contract] +Weights: [20, 25, 30, 25, 0] +Quorum: 51 + +Result: Majority of weighted votes required (51 out of 100) +Use case: Collaborative funds management +``` + +**Governance**: Weighted majority - decisions require majority approval by weight. + +### Example 5: Watch Tower + +``` +Participants: [Alice, Bob, WatchTower] +Weights: [40, 40, 100] +Quorum: 80 + +Result: + - Normal operation: Alice + Bob (80) + - Emergency: WatchTower alone (100) +Use case: Automated monitoring and intervention +``` + +**Governance**: Dual-mode - normal requires cooperation, emergency allows automated action. + +:::tip Governance Flexibility +By adjusting weights and quorum, you can implement any governance model from fully cooperative (all must sign) to fully authoritative (single party controls) to complex weighted voting systems. +::: + +--- + +## Fund Transfer Mechanics + +When an app session is created, funds are transferred from the unified balance account to a dedicated App Session Account: + +```mermaid +graph TB + A["Alice's Unified Account
Balance: 200 USDC"] + + B["Create App Session
Alice transfers 100 USDC"] + + C["Alice's Unified Account
Balance: 100 USDC"] + + D["App Session Account
Balance: 100 USDC
(Beneficiary: Alice)"] + + A -->|create_app_session| B + B --> C + B --> D + + style A fill:#e1f5ff + style B fill:#fff5e1 + style C fill:#e1ffe1 + style D fill:#ffe1f5 +``` + +**Balance State Changes**: + +``` +Before Creation: + Alice's Unified Account: + Balance: 200 USDC + +After Creating Session with 100 USDC: + Alice's Unified Account: + Balance: 100 USDC + + App Session Account: + Balance: 100 USDC (Beneficiary: Alice) +``` + +### Signature Requirements + +All participants with non-zero initial allocations MUST sign the create_app_session request. The clearnode validates that: + +1. All required signatures are present +2. Signatures are valid for respective participants +3. Total weight of signers >= quorum (must be met for creation) + +--- + +## submit_app_state + +### Name + +`submit_app_state` + +### Usage + +Submits a state update for an active app session. State updates can redistribute funds between participants (OPERATE intent), add funds to the session (DEPOSIT intent), or remove funds from the session (WITHDRAW intent). The intent system is only available in NitroRPC/0.4; version 0.2 sessions only support fund redistribution without explicit intent. Each state update increments the version number, and must be signed by participants whose combined weights meet the quorum requirement. The allocations field always represents the FINAL state after the operation, not the delta. + +### When to Use + +During app session lifecycle to update the state based on application logic. Examples include recording game moves, updating scores, reallocating funds based on outcomes, adding stakes, or partially withdrawing winnings. + +### Prerequisites + +- App session must exist and be in "open" status +- Signers must meet quorum requirement +- For DEPOSIT intent: Depositing participant must sign (in addition to quorum) +- For DEPOSIT intent: Depositing participant must have sufficient available balance +- For WITHDRAW intent: Session must have sufficient funds to withdraw +- NitroRPC/0.4: `version` must be **exactly current_version + 1** +- NitroRPC/0.2: **omit** `intent` and `version` (service rejects them); only OPERATE-style redistribution is supported +- If using a session key, spending allowances for that key are enforced + +### Request + +| Parameter | Type | Required | Description | Format | Example | Notes / See Also | +|-----------|------|----------|-------------|--------|---------|------------------| +| `app_session_id` | string | Yes | Identifier of the app session to update | 0x-prefixed hex string (32 bytes) | `"0x9876543210fedcba..."` | - | +| `intent` | string | Yes for v0.4, No for v0.2 | Type of operation (NitroRPC/0.4 only) | Allowed: `"operate"` \| `"deposit"` \| `"withdraw"` | `"operate"` | Omit for NitroRPC/0.2 sessions (treated as operate) | +| `version` | number | Yes | Expected next version number | - | `2` | Must be exactly currentVersion + 1; prevents conflicts | +| `allocations` | Allocation[] | Yes | **FINAL allocation state after this update**

⚠️ **IMPORTANT**: This is the target state, NOT the delta | See [Allocation](#allocation) above | After operate from [100, 100] where Alice loses 25 to Bob:
`[{"participant": "0xAlice", "asset": "usdc", "amount": "75.0"}, {"participant": "0xBob", "asset": "usdc", "amount": "125.0"}]` | Clearnode validates based on intent rules (see below) | +| `session_data` | string | No | Updated application-specific state | JSON string | `"{\"currentMove\":\"e2e4\",\"turn\":\"black\"}"` | Can be updated independently of allocations | + +### Response + +| Parameter | Type | Description | Format/Structure | Example | Notes | +|-----------|------|-------------|------------------|---------|-------| +| `app_session_id` | string | Session identifier (echoed) | - | - | - | +| `version` | number | Confirmed new version number | - | `2` | - | +| `status` | string | Updated session status | `"open"` | `"open"` | Minimal response (no metadata echoed) | + +The Go handler returns an `AppSessionResponse` type, but for state submissions it only includes `app_session_id`, `version`, and `status` (and does not echo session metadata). Use [`get_app_sessions`](./queries#get_app_sessions) to read the full session record. + +--- + +## Intent System (NitroRPC/0.4) + +The intent system defines the type of operation being performed. Each intent has specific validation rules. + +### Intent: OPERATE (Redistribute Existing Funds) + +**Purpose**: Move funds between participants without changing total amount in session. + +**Rules**: +- Sum of allocations MUST equal sum before operation +- No funds added or removed from session +- Quorum requirement MUST be met +- Depositing participant signature NOT required + +**Example**: + +``` +Current state (version 1): + Alice: 100 USDC + Bob: 100 USDC + Total: 200 USDC + +Update (version 2, intent: "operate"): + Allocations: [ + {"participant": "0xAlice", "asset": "usdc", "amount": "75.0"}, + {"participant": "0xBob", "asset": "usdc", "amount": "125.0"} + ] + +Result: + Alice: 75 USDC (-25) + Bob: 125 USDC (+25) + Total: 200 USDC (unchanged) ✓ + +Validation: Sum before (200) == Sum after (200) ✓ +``` + +**Use Cases**: +- Record game outcome (winner gets opponent's stake) +- Update prediction market positions +- Rebalance shared pool +- Penalize or reward participants + +:::tip OPERATE Intent +Use OPERATE for simple fund redistributions within the session. The total amount remains constant—funds just move between participants. +::: + +--- + +### Intent: DEPOSIT (Add Funds to Session) + +**Purpose**: Add funds from a participant's unified balance into the session. + +**Rules**: +- Sum of allocations MUST be greater or equal to sum before operation +- Increase MUST come from available balance of depositing participant +- Depositing participant MUST sign (even if quorum is met without them) +- Quorum requirement MUST still be met +- Allocations show FINAL amounts (not delta) +- If signed via a session key, spending caps for that key are enforced + +**Example**: + +``` +Current state (version 1): + Alice: 100 USDC + Bob: 100 USDC + Total: 200 USDC + +Alice's Unified Balance: + Available: 50 USDC + +Update (version 2, intent: "deposit"): + Allocations: [ + {"participant": "0xAlice", "asset": "usdc", "amount": "150.0"}, + {"participant": "0xBob", "asset": "usdc", "amount": "100.0"} + ] + Signatures: [AliceSig, QuorumSigs...] + +Calculation: + Alice deposit amount = 150 (new) - 100 (old) = 50 USDC + +Result: + Alice: 150 USDC (100 + 50 deposited) + Bob: 100 USDC (unchanged) + Total: 250 USDC (+50) ✓ + +Alice's Unified Balance After: + Available: 0 USDC (50 transferred to App Session Account) + +App Session Account After: + Balance: 250 USDC (increased by 50) + +Validation: + - Sum after (250) > Sum before (200) ✓ + - Alice signed ✓ + - Alice had 50 available ✓ +``` + +**Use Cases**: +- Top up game stake mid-game +- Add collateral to escrow +- Increase position in prediction market +- Buy into ongoing game + +:::caution DEPOSIT Intent +**Critical Understanding**: The allocations array shows FINAL amounts, not the deposit amount. The clearnode calculates the deposit by comparing previous and new allocations for each participant. +::: + +--- + +### Intent: WITHDRAW (Remove Funds from Session) + +**Purpose**: Remove funds from session back to a participant's unified balance. + +**Rules**: +- Sum of allocations MUST be less or equal to sum before operation +- Decrease is returned to participant's available balance +- Withdrawing participant signature NOT specifically required (quorum sufficient) +- Quorum requirement MUST be met +- Allocations show FINAL amounts (not delta) + +**Example**: + +``` +Current state (version 1): + Alice: 150 USDC + Bob: 100 USDC + Total: 250 USDC + +Update (version 2, intent: "withdraw"): + Allocations: [ + {"participant": "0xAlice", "asset": "usdc", "amount": "150.0"}, + {"participant": "0xBob", "asset": "usdc", "amount": "75.0"} + ] + Signatures: [QuorumSigs...] + +Calculation: + Bob withdrawal amount = 100 (old) - 75 (new) = 25 USDC + +Result: + Alice: 150 USDC (unchanged) + Bob: 75 USDC (100 - 25 withdrawn) + Total: 225 USDC (-25) ✓ + +Bob's Unified Balance After: + Available: +25 USDC + +App Session Account After: + Balance: 225 USDC (decreased by 25) + +Validation: + - Sum after (225) < Sum before (250) ✓ + - Quorum met ✓ +``` + +**Use Cases**: +- Cash out partial winnings mid-game +- Remove collateral when no longer needed +- Withdraw partial winnings from a shared session +- Reduce wager in ongoing game + +--- + +## Version Management + +- NitroRPC/0.4: each update MUST be exactly `previous_version + 1`, or it is rejected. +- NitroRPC/0.2: omit `intent` and `version`; providing either results in `"incorrect request: specified parameters are not supported in this protocol"`. + +--- + +## Quorum Validation + +For every update, the clearnode validates quorum: + +```mermaid +graph TD + A[Receive State Update] --> B{Calculate Total Weight} + B --> C[Sum weights of all signers] + C --> D{Total Weight >= Quorum?} + D -->|Yes| E[✓ Update Accepted] + D -->|No| F[✗ Reject: Quorum Not Met] + + style A fill:#e1f5ff + style E fill:#e1ffe1 + style F fill:#ffe1e1 +``` + +**Validation Logic**: + +``` +totalWeight = sum of weights for all signers +if (totalWeight >= definition.quorum) { + ✓ Update accepted +} else { + ✗ Reject: "Quorum not met" +} +``` + +**Example** (using Game with Judge scenario): + +``` +Participants: [Alice, Bob, Judge] +Weights: [0, 0, 100] +Quorum: 100 + +Valid signature combinations: + - Judge alone: weight = 100 >= 100 ✓ + - Alice + Bob: weight = 0 >= 100 ✗ + - Alice + Bob + Judge: weight = 100 >= 100 ✓ +``` + +--- + +## close_app_session + +### Name + +`close_app_session` + +### Usage + +Closes an active app session and distributes all funds from the App Session Account according to the final allocations. Once closed, the app session cannot be reopened; participants must create a new session if they want to continue. The final allocations determine how funds are returned to each participant's unified balance account. Closing requires quorum signatures. The final session_data can record the outcome or final state of the application. All funds in the App Session Account are released immediately. + +### When to Use + +When application logic has completed and participants want to finalize the outcome and retrieve their funds. Examples include game ending, escrow condition met, prediction market settled, or any application reaching its natural conclusion. + +### Prerequisites + +- App session must exist and be in "open" status +- Signers must meet quorum requirement +- Final allocations must not exceed total funds in session +- Sum of final allocations must equal total session funds + +### Request + +| Parameter | Type | Required | Description | Format/Structure | Example | Notes | +|-----------|------|----------|-------------|------------------|---------|-------| +| `app_session_id` | string | Yes | Identifier of the app session to close | 0x-prefixed hex string (32 bytes) | `"0x9876543210fedcba..."` | - | +| `allocations` | Allocation[] | Yes | Final distribution of all funds in the session

**IMPORTANT**: Must account for ALL funds; sum must equal session total

**Structure (per allocation)**:
• `participant` (address) - Participant wallet address
• `asset` (string) - Asset identifier
• `amount` (string) - Final amount for this participant | See structure | 200 USDC total, winner takes most:
`[{"participant": "0xAlice", "asset": "usdc", "amount": "180.0"}, {"participant": "0xBob", "asset": "usdc", "amount": "15.0"}, {"participant": "0xJudge", "asset": "usdc", "amount": "5.0"}]` | Can allocate zero to participants (they get nothing) | +| `session_data` | string | No | Final application state or outcome record | JSON string | `"{\"result\":\"Alice wins\",\"finalScore\":\"3-1\"}"` | Useful for recording outcome for history/analytics | + +### Response + +| Parameter | Type | Description | Format/Structure | Example | Notes | +|-----------|------|-------------|------------------|---------|-------| +| `app_session_id` | string | Session identifier (echoed) | - | - | - | +| `status` | string | Final status | Value: "closed" | `"closed"` | Minimal response | +| `version` | number | New session version | - | `2` | Incremented on close | + +:::note close_app_session response +The handler returns an `AppSessionResponse` type in Go, but on close it only populates `app_session_id`, `status`, and `version`. For full metadata after closure, query [`get_app_sessions`](./queries#get_app_sessions). +::: +--- + +## Fund Distribution on Closure + +When an app session closes, funds return to participants' unified balances: + +``` +Before Closure: + Alice's Unified Account: + Balance: 100 USDC + +App Session Account 0x98765: + Alice: 100 USDC + Bob: 100 USDC + Total: 200 USDC + +Close with final allocations: + Alice: 180 USDC + Bob: 20 USDC + +After Closure: + Alice's Unified Account: + Balance: 280 USDC (100 + 180 received from session) + + Bob's Unified Account: + Balance: 20 USDC (received from session) + + App Session Account 0x98765: + Closed (Balance: 0 USDC) +``` + +### Allocation Rules + +1. **Must Sum to Total**: + - `sum(final_allocations) MUST equal sum(current_allocations)` + - Clearnode validates this; cannot create or destroy funds during close + +2. **Can Be Zero**: + - Participants can receive zero in final allocation (lost everything) + - Example: Losing player in a winner-takes-all game + +3. **Accounting for Participants**: + - It is recommended to include an entry for every participant (use zero for losers). + - If you omit a participant, the service treats them as receiving zero, as long as per-asset totals still match the session balance. + +4. **Can Include Non-Financial Participants**: + - Example: Judge/application can receive commission + - `{"participant": "0xJudge", "asset": "usdc", "amount": "5.0"}` + +--- + +## Closure Examples + +### Example 1: Chess Game + +``` +Initial: + White: 100 USDC + Black: 100 USDC + Judge: 0 USDC + Total: 200 USDC + +Final (White wins): + White: 190 USDC (won 90) + Black: 0 USDC (lost 100) + Judge: 10 USDC (5% commission) + Total: 200 USDC ✓ +``` + +### Example 2: Escrow (Buyer Satisfied) + +``` +Initial: + Buyer: 100 USDC + Seller: 0 USDC + Arbiter: 0 USDC + Total: 100 USDC + +Final (Successful delivery): + Buyer: 0 USDC + Seller: 99 USDC (payment) + Arbiter: 1 USDC (fee) + Total: 100 USDC ✓ +``` + +### Example 3: Escrow (Dispute, Buyer Refunded) + +``` +Initial: + Buyer: 100 USDC + Seller: 0 USDC + Arbiter: 0 USDC + Total: 100 USDC + +Final (Arbiter ruled for buyer): + Buyer: 95 USDC (refund minus fee) + Seller: 0 USDC + Arbiter: 5 USDC (dispute fee) + Total: 100 USDC ✓ +``` + +### Example 4: Prediction Market + +``` +Initial: + User1: 50 USDC (bet YES) + User2: 50 USDC (bet YES) + User3: 40 USDC (bet NO) + Oracle: 0 USDC + Total: 140 USDC + +Final (Outcome: YES): + User1: 68.25 USDC (split pot proportionally) + User2: 68.25 USDC + User3: 0 USDC (lost) + Oracle: 3.50 USDC (2.5% fee) + Total: 140 USDC ✓ +``` + +:::success Final Distribution +All participants receive funds according to the final allocations, whether they won, lost, or served as neutral parties (judges, arbiters, oracles). The total is always preserved. +::: + +--- + +{/* TODO: Document actual error codes from implementation. Currently removed as placeholder errors were inaccurate. */} + +--- + +## Implementation Notes + +**State Management**: +- Always use `intent: "operate"` for simple redistributions +- Always specify FINAL allocations, never deltas +- The clearnode computes deltas internally by comparing with previous state +- Version numbers must be strictly sequential +- The session_data field can be updated in any intent + +**Performance**: +- Updates are instant (< 1 second) and off-chain +- Zero gas fees for all operations +- All updates are logged for audit trail + +**Notifications**: +- Participants are notified on all active connections of state changes +- Closed sessions remain queryable for history + +**Irreversibility**: +- Closure is instant and atomic +- All funds released simultaneously +- Once closed, cannot be reopened +- To continue, create a new session + +--- + +## Next Steps + +Explore other protocol features: + +- **[Queries & Notifications](./queries)** - Query session history and receive real-time updates +- **[Transfers](./transfers)** - Move funds between unified balances +- **[Channel Methods](./channel-methods)** - Manage underlying payment channels + +For foundational concepts: +- **[Message Format](./message-format)** - Understand request/response structure +- **[Authentication](./authentication)** - Manage session keys and security diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/authentication.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/authentication.mdx new file mode 100644 index 0000000..09da4d4 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/authentication.mdx @@ -0,0 +1,467 @@ +--- +sidebar_position: 3 +title: Authentication +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Authentication + +Authentication with Clearnode can be done in two ways: using your **main wallet as a root signer** for all requests, or delegating to session keys via a secure 3-step challenge-response protocol. + +--- + +## Overview + +There are two authentication approaches: + +1. **Main Wallet (Root Signer)**: Sign every request with your main wallet. Simple but requires user interaction for each operation. + +2. **Session Keys (Delegated)**: Establish an authenticated session once, then use a session key for subsequent operations without repeatedly prompting the main wallet. + +:::info Main Wallet as Root Signer +You can **skip the session key flow entirely** and use your main wallet to sign all requests. This provides maximum security but requires wallet interaction for every operation. Simply sign each request with your main wallet's private key instead of creating a session key. +::: + +### Why Session Keys? + +Session keys provide **flexible security management**: + +- **Granular Permissions**: Specify which operations the session key can perform +- **Spending Allowances**: Set maximum spending limits per asset +- **Time-Bounded**: Automatic expiration reduces risk of key compromise +- **Application-Scoped**: Different keys for different apps +- **User Experience**: No repeated wallet prompts during active session + +:::success Flexible Security Management +Session keys give users a flexible way to manage security of their funds by providing specific permissions and allowances for specific apps, balancing convenience with security. +::: + +### Choosing Your Approach + +| Aspect | Main Wallet (Root Signer) | Session Keys (Delegated) | +|--------|---------------------------|--------------------------| +| **Setup** | None - use immediately | One-time 3-step flow | +| **UX** | Wallet prompt for every operation | Sign once, use for duration | +| **Security** | Maximum - full control always | Balanced - limited by allowances | +| **Use Case** | Single operations, high-value transactions | Interactive apps, frequent operations | +| **Revocation** | Not needed | Can be revoked anytime | +| **Best For** | One-time actions, security-critical operations | Gaming, trading bots, dApps with frequent interactions | + +:::tip When to Use Each +- **Use Main Wallet**: For single channel creation, large transfers, or when maximum security is required +- **Use Session Keys**: For interactive applications, gaming, automated operations, or when user experience matters +::: + +### Session Key Authentication Flow + +The 3-step process ensures both security and usability: + +```mermaid +sequenceDiagram + box rgb(255,225,225) User Wallet + participant UW as User Wallet + end + box rgb(225,245,255) Client + participant Client + end + box rgb(255,225,245) Clearnode + participant Clearnode + end + + Note over Client: Step 1: Register Session Key + Client->>Client: Generate session keypair (locally) + Client->>Client: Prepare auth parameters (address, session_key, application, allowances, expires_at) + Client->>Clearnode: auth_request (public endpoint, no signature) + + Note over Clearnode: Step 2: Challenge + Clearnode->>Clearnode: Validate parameters + Clearnode->>Clearnode: Generate challenge UUID + Clearnode->>Client: auth_challenge (challenge_message) + + Note over Client: Step 3: Verify Session Key + Client->>Client: Create EIP-712 typed data with challenge + Client->>UW: Request EIP-712 signature + UW-->>Client: Sign with main wallet + Client->>Clearnode: auth_verify (EIP-712 signature by main wallet) + + Note over Clearnode: Complete + Clearnode->>Clearnode: Recover address from EIP-712 signature + Clearnode->>Clearnode: Validate signature matches main wallet + Clearnode->>Clearnode: Create session (with allowances) + Clearnode->>Clearnode: Generate JWT token + Clearnode->>Client: Session established (address, session_key, jwt_token, success) + + Note over Client,Clearnode: All subsequent requests signed with session key + +``` + +:::info Challenge-Response Pattern +This pattern ensures that: +1. User owns the main wallet (EIP-712 signature in Step 3) +2. Challenge is unique and cannot be replayed +3. No private keys are ever transmitted +4. Session key is authorized by the main wallet +::: + +--- + +## Step 1: auth_request + +### Name + +`auth_request` + +### Usage + +Initiates authentication with Clearnode by registering a session key. The client sends authentication parameters to register a session key that can act on their behalf. The session key can have restricted permissions including spending limits (allowances), operation scope, and expiration time. + +**Important**: `auth_request` is a **public endpoint** and does not require a signature. The client simply needs to prepare and send the authentication parameters. + +### When to Use + +**Optional**: Use this when you want to delegate signing to a session key instead of using your main wallet for every request. This is the first step in establishing an authenticated session with Clearnode. + +If you prefer to use your main wallet as a root signer for all operations, you can skip this entire authentication flow. + +### Prerequisites + +- User has a wallet with funds +- Client can generate a keypair (e.g., secp256k1) +- Client can prepare authentication parameters locally + +### Request + +| Parameter | Type | Required | Description | Default | Example | Notes | +|-----------|------|----------|-------------|---------|---------|-------| +| `address` | string (wallet address) | Yes | User's main wallet address that owns the funds | - | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | - | +| `session_key` | string (wallet address) | Yes | Wallet address of the locally-generated session keypair | - | `"0x9876543210fedcba9876543210fedcba98765432"` | The private key never leaves the client | +| `application` | string | No | Application identifier for analytics and session management | `"clearnode"` | `"chess-game-app"` | Helps track which app is using which session | +| `allowances` | Array\ | No | Spending limits for this session key

**Structure (per allowance)**:
• `asset` (string) - Asset identifier (e.g., "usdc", "eth")
• `amount` (string) - Maximum amount this session can spend | Unrestricted if omitted/empty | `[{"asset": "usdc", "amount": "100.0"}]` | If empty/omitted, no spending cap is enforced | +| `scope` | string | No | Comma-separated list of permitted operations | All operations permitted | `"app.create,app.submit,transfer"` | Future feature, not fully enforced yet | +| `expires_at` | number | Yes | Unix timestamp (milliseconds) when the session key expires | — | `1762417328000` | Provide a 13-digit Unix ms timestamp; no server default is applied | + +:::tip Spending Allowances +If you omit `allowances` the session key is unrestricted. Specify explicit allowances to bound risk if a session key is compromised. +::: + +Allowances are validated against the broker’s supported assets. Unsupported symbols will cause authentication to fail. + +### Response + +| Parameter | Type | Description | Format | Example | Purpose | +|-----------|------|-------------|--------|---------|---------| +| `challenge_message` | string | UUID that client must sign with session key to prove ownership | UUID v4 | `"550e8400-e29b-41d4-a716-446655440000"` | Proves client controls session key without exposing private key | + +### Signature + +Request **does NOT require a signature** as `auth_request` is a public endpoint. + +**Process**: +1. Client prepares authentication parameters (address, session_key, application, allowances, expires_at) +2. Client stores these parameters locally for use in Step 3 (auth_verify) +3. Client sends request to Clearnode +4. Clearnode validates all parameters before generating a challenge + +:::tip Parameter Storage +Keep the authentication parameters (especially `address`, `session_key`, `application`, `allowances`, `scope`, and `expires_at`) stored locally until Step 3, as you'll need them to create the EIP-712 signature. +::: + +### Next Step + +Upon receiving the `challenge_message`, client must prepare an EIP-712 signature (or reuse a previously issued `jwt`) and call `auth_verify`. + +### Error Cases + +:::note Error Codes +Currently, the protocol does not use standardized error codes. Errors are returned as descriptive messages. +::: + +Common error scenarios: + +| Error | Description | Recovery | +|-------|-------------|----------| +| **Invalid address format** | Main wallet address is malformed | Verify address format (0x + 40 hex chars) | +| **Invalid session key format** | Session key address is malformed | Verify session key format | +| **Invalid parameters** | One or more parameters are invalid or missing | Check all required parameters | +| **Session key already registered** | This session key is already in use | Generate a new session keypair | + + +--- + +## Step 2: auth_challenge + +### Name + +`auth_challenge` + +### Usage + +Server-generated response to `auth_request` containing a challenge that the client must sign to prove control of the session key. This implements a challenge-response authentication pattern to prevent replay attacks and verify the client controls the private key of the session key they registered. + +### When to Use + +Automatically sent by Clearnode in response to valid `auth_request`. Client does not explicitly call this; it's part of the authentication flow. + +### Request + +N/A (server-initiated response to `auth_request`) + +### Response + +| Parameter | Type | Description | Format | Purpose | Example | Generation | Lifetime | +|-----------|------|-------------|--------|---------|---------|------------|----------| +| `challenge_message` | string | Randomly generated UUID that client must sign | UUID v4 | Prevents replay attacks, proves session key ownership | `"550e8400-e29b-41d4-a716-446655440000"` | Cryptographically secure random UUID | Single use, expires after 5 minutes if not verified | + +### Signature + +The challenge is returned as a normal RPC response (server signs the envelope like any other RPC response). + +### Next Step + +Client signs the challenge with session key private key and calls `auth_verify`. + +:::info Challenge Uniqueness +Each challenge is unique and single-use. It expires after 5 minutes if not verified. This prevents replay attacks where an attacker might try to reuse a captured challenge signature. +::: + +--- + +## Step 3: auth_verify + +### Name + +`auth_verify` + +### Usage + +Completes the authentication flow by submitting the signed challenge from `auth_challenge`. If the signature is valid and matches the registered session key, the authentication is complete and the session key can be used to sign subsequent requests. This proves the client controls the private key without ever transmitting it. + +### When to Use + +Immediately after receiving `auth_challenge` response. This is the final step in authentication. + +### Prerequisites + +- Completed `auth_request` and received `auth_challenge` +- Have the challenge_message +- Have the session key private key (client-side only) + +### Request + +| Parameter | Type | Required | Description | Example | Notes | +|-----------|------|----------|-------------|---------|-------| +| `challenge` | string | Yes | The challenge_message received from auth_challenge | `"550e8400-e29b-41d4-a716-446655440000"` | Must be the exact challenge from Step 2 | +| `jwt` | string | No | Existing JWT for re-login without signature | `"eyJhbGciOi..."` | If provided, signature is not required | + +### Response + +| Parameter | Type | Description | Example | Notes | +|-----------|------|-------------|---------|-------| +| `address` | string (wallet address) | Authenticated user's main wallet address | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | Confirms which account is authenticated | +| `session_key` | string (wallet address) | Confirmed session key wallet address | `"0x9876543210fedcba9876543210fedcba98765432"` | The authorized session key | +| `jwt_token` | string | JWT token for authenticated API calls | `"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` | Store securely; validity follows the provided `expires_at` | +| `success` | boolean | Authentication success indicator | `true` | Indicates if authentication completed successfully | + +### Signature + +If `jwt` is omitted, the request **MUST** include an EIP-712 signature signed by the **main wallet** (NOT the session key). If `jwt` is present, no signature is required. + +**EIP-712 Typed Data Structure**: + +```typescript +{ + types: { + EIP712Domain: [ + { name: "name", type: "string" } + ], + Policy: [ + { name: "challenge", type: "string" }, + { name: "scope", type: "string" }, + { name: "wallet", type: "address" }, + { name: "session_key", type: "address" }, + { name: "expires_at", type: "uint64" }, + { name: "allowances", type: "Allowance[]" } + ], + Allowance: [ + { name: "asset", type: "string" }, + { name: "amount", type: "string" } + ] + }, + primaryType: "Policy", + domain: { + name: // From auth_request + }, + message: { + challenge: , // From auth_challenge + scope: , // From auth_request + wallet:
, // From auth_request + session_key: , // From auth_request + expires_at: , // From auth_request (13-digit Unix ms) + allowances: // From auth_request + } +} +``` + +**Signing Process**: +1. Client creates EIP-712 typed data with challenge and all parameters from Step 1 +2. User's wallet signs the typed data: `signature = signTypedData(typedData, mainWalletPrivateKey)` +3. Client sends request with EIP-712 signature in `sig` array + +:::danger Critical Security Requirement +The `auth_verify` signature MUST be an **EIP-712 signature signed by the main wallet**, not the session key. This proves the main wallet owner authorizes the session key to act on their behalf. The signature binds the challenge to the session key authorization. +::: + +### Next Step + +Session is authenticated. All subsequent private method calls should be signed with the session key. You may also re-authenticate later by sending `auth_verify` with the previously issued `jwt` (no signature required). + +### Error Cases + +:::note Error Codes +Currently, the protocol does not use standardized error codes. Errors are returned as descriptive messages. +::: + +Common error scenarios: + +| Error | Description | Recovery | +|-------|-------------|----------| +| **Invalid signature** | EIP-712 signature doesn't match main wallet or is malformed | Verify main wallet private key used for signing, check EIP-712 structure | +| **Challenge expired** | Challenge older than 5 minutes | Restart auth flow from `auth_request` | +| **Challenge already used** | Challenge has been verified already | Generate new session or use existing if still valid | +| **Invalid challenge** | Challenge not found in pending auths | Ensure `auth_request` succeeded first | +| **Challenge mismatch** | Challenge doesn't match pending auth | Use exact challenge from `auth_challenge` | + + +--- + +## Complete Authentication Flow Example + +Putting it all together: + +```mermaid +stateDiagram-v2 + [*] --> Unauthenticated + + Unauthenticated --> PreparingAuth: Generate session keypair + PreparingAuth --> WaitingForChallenge: auth_request
(public, no signature) + + WaitingForChallenge --> CreatingEIP712: Receive challenge_message + CreatingEIP712 --> SigningWithWallet: Create EIP-712 typed data + SigningWithWallet --> WaitingForConfirmation: auth_verify
(EIP-712 sig by main wallet) + + WaitingForConfirmation --> Authenticated: Session + JWT established + + Authenticated --> Authenticated: Use session key for requests + Authenticated --> SessionExpired: Timeout (expires_at reached) + Authenticated --> SessionInvalidated: Spending limit exceeded + Authenticated --> SessionRevoked: Manual revocation + + SessionExpired --> Unauthenticated: Must re-authenticate + SessionInvalidated --> Unauthenticated: Must re-authenticate + SessionRevoked --> Unauthenticated: Must re-authenticate + + PreparingAuth --> Unauthenticated: Error (retry) + WaitingForChallenge --> Unauthenticated: Timeout (5 min) + CreatingEIP712 --> Unauthenticated: Error (retry) + SigningWithWallet --> Unauthenticated: Error (retry) +``` + +--- + +## Session Management + +### Session Lifecycle + +1. **Creation**: After successful `auth_verify` +2. **Active**: Can perform operations until expiration or allowance exceeded +3. **Expiration**: Automatic after specified duration +4. **Invalidation**: When spending allowances exhausted +5. **Revocation**: User or the clearnode can revoke manually + +### Checking Session Status + +Use `get_session_keys` to view active sessions and their remaining allowances. The response includes session details with current allowance usage and respects the `expires_at` provided during `auth_request`. + +### Session Expiration Handling + +When a session expires according to the `expires_at` you provided, the clearnode will return an error response: + +```json +{ + "res": [ + , + "error", + { + "error": "session expired, please re-authenticate" + }, + + ], + "sig": [] +} +``` + +:::note Error Format +The protocol does not use numeric error codes. Errors are returned as method `"error"` with a descriptive message in the params. +::: + +**Recovery**: Re-authenticate by running the 3-step flow again. + +### Spending Allowance Tracking + +The clearnode tracks spending by monitoring all ledger debit operations: + +``` +Initial state: + allowance = specified_limit + used = 0 + remaining = specified_limit + +After operations: + allowance = specified_limit (unchanged) + used = sum_of_all_debits + remaining = allowance - used + +When operation exceeds remaining (for assets with an allowance): + Error: "Session key allowance exceeded: amount_required, remaining_available" +``` + +:::warning Allowance Enforcement +When a session key reaches its spending cap, all further operations are rejected. The user must create a new session with fresh allowances or use their main wallet directly. +::: + +--- + +## Security Best Practices + +### For Users + +1. **Set Spending Limits**: Always specify `allowances` when creating sessions +2. **Short Expirations**: Use shorter expiration times for sensitive operations +3. **Application Scoping**: Use different session keys for different applications +4. **Monitor Usage**: Regularly check session key spending via `get_session_keys` +5. **Revoke When Done**: Revoke sessions when application use is complete + +### For Developers + +1. **Secure Storage**: Store session key private keys securely (encrypted storage, secure enclaves) +2. **Never Transmit**: Never send session key private keys over network +3. **Handle Expiration**: Implement automatic re-authentication on session expiry +4. **Clear on Logout**: Delete session keys when user logs out +5. **Verify Signatures**: Always verify the clearnode's signatures on responses + +--- + +## Next Steps + +Now that you're authenticated, you can: + +- **[Create Channels](./channel-methods)** - Open payment channels and deposit funds +- **[Transfer Funds](./transfers)** - Send instant off-chain payments +- **[Manage App Sessions](./app-sessions)** - Create multi-party application channels +- **[Query Data](./queries)** - Check balances, transactions, and channel status + +For protocol fundamentals, see: +- **[Message Format](./message-format)** - Understand request/response structure +- **[Off-Chain RPC Overview](./overview)** - High-level protocol overview diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/channel-methods.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/channel-methods.mdx new file mode 100644 index 0000000..744730d --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/channel-methods.mdx @@ -0,0 +1,594 @@ +--- +sidebar_position: 4 +title: Channel Management Methods +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Channel Management Methods + +Channel management methods enable clients to create, modify, and close payment channels with a clearnode on various blockchain networks. + +--- + +## Overview + +Payment channels are the foundation of the VirtualApp protocol. They lock funds on-chain while enabling instant off-chain operations within a unified balance. + +### Channel Lifecycle Summary + +```mermaid +stateDiagram-v2 + [*] --> Requesting: create_channel (off-chain) + Requesting --> OnChain: User submits create() transaction + OnChain --> ACTIVE: Contract locks user funds (status = open) + + ACTIVE --> Resizing: resize_channel (optional) + Resizing --> ACTIVE: User submits resize() transaction + + ACTIVE --> Closing: close_channel (cooperative) + Closing --> [*]: Funds distributed + + ACTIVE --> Disputing: challenge() (non-cooperative) + Disputing --> [*]: Challenge period then close() + + +``` + +--- + +## create_channel + +### Name + +`create_channel` + +### Usage + +Initiates the creation of a payment channel between user and a clearnode on a specific blockchain. The clearnode validates the request, generates a channel configuration with a unique nonce, prepares the initial funding state, and signs it. The user receives the complete channel data and the clearnode's signature, which they must then submit to the blockchain's Custody contract via the `create()` function to finalize channel creation and lock funds on-chain. This two-step process (off-chain preparation, on-chain execution) ensures the clearnode has agreed on channel creation and received an on-chain confirmation that it was created. + +### When to Use + +When a user wants to establish a payment channel on a specific blockchain network. This is the first operation after authentication if the user doesn't have an open channel yet. On subsequent connections, users won't need to create a channel again unless they closed it. + +:::info Two-Step Process +Channel creation is intentionally split into two steps: +1. **Off-chain preparation**: The clearnode prepares and signs the initial state +2. **On-chain execution**: User submits transaction to create the channel + +This ensures the clearnode has committed to the channel before the user submits the on-chain transaction. +::: + +### Prerequisites + +- User must be [authenticated](./authentication) +- Target blockchain and token must be supported by the clearnode +- User must have native currency for gas fees + +### Request + +| Parameter | Type | Required | Description | Default | Example | Notes | +|-----------|------|----------|-------------|---------|---------|-------| +| `chain_id` | uint32 | Yes | Blockchain network identifier

**Examples**:
• 1: Ethereum Mainnet
• 137: Polygon
• 8453: Base
• 42161: Arbitrum One
• 10: Optimism | — | `137` | Use `get_config` to see supported chains | +| `token` | string (wallet address) | Yes | ERC-20 token contract address on the specified chain

Format: 0x-prefixed hex (20 bytes) | — | `"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"` | Must be supported; see `get_assets` | + +:::info Initial Channel State +Channels are created with **zero initial balance** for both participants. To add funds to the channel, use the `resize_channel` method after creation. The challenge period is set to 1 hour (3600 seconds) by default. +::: + +### Response + +:::tip Quick Reference +Structures: [Channel](#channel-structure) • [State](#state-structure) • [StateAllocation](#stateallocation) +::: + +| Parameter | Type | Description | See Also | +|-----------|------|-------------|----------| +| `channel_id` | string | Computed channel identifier (0x-prefixed hex, 32 bytes) | — | +| `channel` | Channel | On-chain channel params | [↓ Structure](#channel-structure) | +| `state` | State | Initial state (intent INITIALIZE, version 0, empty data, zero allocations) | [↓ Structure](#state-structure) | +| `server_signature` | string | Clearnode signature over packed state (hex string) | — | + +#### Channel Structure + +| Field | Type | Description | Notes | +|-------|------|-------------|-------| +| `participants` | wallet address[] | Array of two wallet addresses: [User, Clearnode] | Order: Index 0 = User, Index 1 = Clearnode
Order is critical for signature verification | +| `adjudicator` | wallet address | Adjudicator contract address for this channel | Typically SimpleConsensus for payment channels
Validates state transitions during disputes | +| `challenge` | uint64 | Challenge period in seconds | Default: 3600 seconds (1 hour) | +| `nonce` | uint64 | Unique identifier for this channel | Ensures channelId uniqueness even with same participants
Server-generated timestamp or counter | + +**Example**: +```json +{ + "participants": ["0x742d35Cc...", "0x123456Cc..."], + "adjudicator": "0xAdjudicator123...", + "challenge": 86400, + "nonce": 1699123456 +} +``` + +#### State Structure + +| Field | Type | Description | Notes | +|-------|------|-------------|-------| +| `intent` | StateIntent | State purpose indicator | For creation: `INITIALIZE` (1) | +| `version` | uint64 | State sequence number | For creation: `0` | +| `state_data` | string | State data (hex) | For creation: `"0x"` | +| `allocations` | StateAllocation[] | Fund allocations (raw units) | Order matches participants array; both `0` on creation | + +**Example**: +```json +{ + "intent": 1, + "version": 0, + "state_data": "0x", + "allocations": [ + {"participant": "0x742d35Cc...", "token": "0x2791Bca1...", "amount": "0"}, + {"participant": "0x123456Cc...", "token": "0x2791Bca1...", "amount": "0"} + ] +} +``` + +#### StateAllocation Structure + +| Field | Type | Description | +|-------|------|-------------| +| `participant` | string (wallet address) | Participant's wallet address | +| `token` | string (wallet address) | Token contract address | +| `amount` | string | Amount in smallest unit (e.g., `"100000000"` for 100 USDC with 6 decimals) | + +:::tip Clearnode Signature First +The clearnode provides its signature BEFORE the user commits funds on-chain. This ensures both parties have committed to the channel before any on-chain transaction occurs. +::: + +### Next Steps After Receiving Response + +1. **Verify Channel Data** + - Recompute `channelId` = `keccak256(abi.encode(channel))` + - Verify computed ID matches response `channel_id` + - Check participants[0] is your wallet address + - Verify token matches your request + +2. **Verify the Clearnode's Signature** + - Compute `packedState` = `abi.encode(channelId, state.intent, state.version, state.data, state.allocations)` + - Recover signer from `server_signature` + - Verify signer is the clearnode's known wallet address + +3. **Sign State with Your Key** + - Sign `packedState` with your participant key + - Include your signature when submitting to blockchain + +4. **Submit On-Chain Transaction** + - Call `Custody.create(channel, state, yourSignature, clearnodeSignature)` + - Wait for transaction confirmation + +5. **Monitor for Channel Creation** + - Listen for `Opened` event (emitted right after transaction is mined) + - Or poll `get_channels` until channel appears with status "open" + +6. **Channel Active** + - Channel appears in `get_channels` with status "open" + - Channel starts with zero balance + - Use `resize_channel` to add funds to the channel + +### Error Cases + +:::note Error Format +The protocol does not use numeric error codes. Errors are returned as method `"error"` with descriptive messages. +::: + +Common error scenarios: + +| Error | Description | Recovery | +|-------|-------------|----------| +| **Authentication required** | Not authenticated | Complete [authentication flow](./authentication) | +| **Unsupported chain** | `chain_id` not supported | Use `get_config` | +| **Token not supported** | Token not in asset config for chain | Use `get_assets` | +| **Invalid signature** | Caller did not sign request | Sign with channel participant wallet | +| **Channel already exists** | Open channel with broker already exists | Use existing channel or close it first | +| **Failed to prepare state** | Internal packing/signing issue | Retry or contact support | + +### Implementation Notes + +- The nonce is generated by the clearnode to ensure uniqueness +- The channelId can be computed client-side: `keccak256(abi.encode(channel))` +- The packedState should be verified: `abi.encode(channelId, state.intent, state.version, state.data, state.allocations)` +- Users should verify the clearnode's signature before proceeding +- The challenge period can be customized but most users should use defaults + +### Sequence Diagram + + +```mermaid +sequenceDiagram + participant User + participant Clearnode + participant Blockchain + + Note over User: 1. Request Channel Creation + User->>Clearnode: create_channel(chain_id, token) + + Note over Clearnode: 2. Prepare Channel + Clearnode->>Clearnode: Generate unique nonce + Clearnode->>Clearnode: Create channel config + Clearnode->>Clearnode: Create initial state (intent: INITIALIZE, version: 0) + Clearnode->>Clearnode: Sign state + + Clearnode->>User: {channel, state, server_signature, channel_id} + + Note over User: 3. Verify & Sign + User->>User: Verify Clearnode signature + User->>User: Sign state with participant key + + Note over User: 4. Submit On-Chain + User->>Blockchain: Custody.create(channel, state, signatures) + + Note over Blockchain: 5. Create Channel (Status: ACTIVE) + Blockchain->>Blockchain: Verify signatures + Blockchain->>Blockchain: Create channel with status ACTIVE + Blockchain->>Blockchain: Emit Opened event + + Blockchain-->>User: Channel Active (zero balance) + Blockchain-->>Clearnode: Channel Active (zero balance) + + Note over User,Clearnode: Use resize_channel to add funds + +``` + +--- + +## close_channel + +### Name + +`close_channel` + +### Usage + +Initiates cooperative closure of an active payment channel. The clearnode signs a final state with StateIntent.FINALIZE reflecting the current balance distribution. The user receives this clearnode-signed final state which they must submit to the blockchain's Custody contract via the `close()` function. This is the preferred and most efficient way to close a channel as it requires only one on-chain transaction and completes immediately without a challenge period. Both parties must agree on the final allocation for cooperative closure to work. + +### When to Use + +When a user wants to withdraw funds from an active channel and both user and the clearnode agree on the final balance distribution. This should be the default closure method when both parties are online and cooperative. + +:::success Preferred Closure Method +Cooperative closure is **fast (1 transaction)**, **cheap (low gas)**, and **immediate (no waiting period)**. Always use this method when possible. Challenge-response closure should only be used when the clearnode is unresponsive or disputes the final state. +::: + +### Prerequisites + +- Channel must exist and be in ACTIVE status +- User must be authenticated +- User must have native currency for gas fees +- Both parties must agree on final allocations (implicitly, by the clearnode signing) + +### Request + +| Parameter | Type | Required | Description | Default | Example | Notes | +|-----------|------|----------|-------------|---------|---------|-------| +| `channel_id` | string | Yes | Identifier of the channel to close | - | `"0xabcdef1234567890..."` | From get_channels or stored after creation | +| `funds_destination` | string (wallet address) | Yes | Address where your share of channel funds should be sent | - | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | Typically your wallet address | + +### Response + +| Parameter | Type | Description | Example | Notes | +|-----------|------|-------------|---------|-------| +| `channel_id` | string | Channel identifier | `"0xabcdef1234..."` | — | +| `state` | State | Final state with intent FINALIZE and version = current+1
`state_data`: `"0x"`
`allocations`: final fund distribution (raw units) | See [State structure](#state-structure) | `channel` field is omitted in close responses | +| `server_signature` | string | Clearnode signature over packed state | `"0xabcdef987654..."` | Hex string | + +### Next Steps After Receiving Response + +1. **Verify Final Allocations** + - Check allocations match expectations + - Verify total matches total locked funds + - Ensure your allocation is correct + +2. **Verify the Clearnode's Signature** + - Compute `packedState` = `abi.encode(channelId, state.intent, state.version, state.data, state.allocations)` + - Verify signature is from the clearnode + +3. **Sign Final State** + - Sign `packedState` with your participant key + - Include your signature when submitting to blockchain + +4. **Submit On-Chain** + - Call `Custody.close(channelId, state, yourSignature, clearnodeSignature)` on blockchain + - Both signatures must be present + +5. **Wait for Confirmation** + - Transaction confirms + - Funds distributed according to allocations + +6. **Channel Closed** + - Channel deleted from chain + - Funds in your wallet or custody available balance + +7. **Withdraw if Needed** + - If funds in custody, call `withdraw()` to move to wallet + +### Error Cases + +:::note Error Format +The protocol does not use numeric error codes. Errors are returned as method `"error"` with descriptive messages. +::: + +Common error scenarios: + +| Error | Description | Recovery | +|-------|-------------|----------| +| **Authentication required** | Not authenticated | Re-authenticate | +| **Channel not found** | Invalid `channel_id` | Verify with `get_channels` | +| **Channel challenged** | Participant has challenged channels | Resolve challenges first | +| **Channel not open/resizing** | Status not `open` or `resizing` | Only open/resizing channels can close | +| **Invalid signature** | Caller did not sign request | Sign with channel participant wallet | +| **Token/asset not found** | Asset config missing | Ensure channel token is supported | +| **Insufficient/negative balance** | Ledger balance retrieval or negative balance | Ensure balances are non-negative; retry | +| **Failed to pack/sign state** | Internal packing/signing issue | Retry or contact support | + +### Comparison: Cooperative vs Challenge Closure + +| Aspect | Cooperative (this method) | Challenge | +|--------|---------------------------|-----------| +| **Speed** | Fast (1 transaction) | Slow (challenge period + 1 transaction) | +| **Gas Cost** | Low (~100k gas) | High (~200k+ gas, 2+ transactions) | +| **Requirements** | Both parties online & agree | Works if other party unresponsive | +| **Waiting Period** | None (immediate) | 24+ hours (challenge duration) | +| **Use When** | Normal operations | Disputes or unresponsiveness | + +:::caution When to Use Challenge Closure +Only use challenge closure (on-chain `challenge()` function) when: +- Clearnode is unresponsive +- Clearnode disputes the final allocation +- Cooperative closure fails repeatedly + +Challenge closure requires waiting for the challenge period to expire before funds are released. +::: + +### Implementation Notes + +- The StateIntent.FINALIZE (3) signals this is a final state +- All participants must sign the final state for it to be accepted on-chain +- The allocations determine where funds go when channel closes +- Clearnode will only sign if the allocations match the current state of the unified balance +- After closing, funds are distributed according to the allocations specified +- Users may need to call `withdraw()` separately to move funds from custody ledger to their wallet + +--- + +## resize_channel + +### Name + +`resize_channel` + +### Usage + +Adjusts the allocations of an existing channel by locking or unlocking funds **without closing the channel**. Unlike older implementations, this uses the `resize()` function on the Custody contract to perform an **in-place update** of the channel's allocations. The same channelId persists throughout the operation, and the channel remains in ACTIVE status. Clearnode prepares a resize state with delta amounts (positive for deposit, negative for withdrawal) that all participants must sign before submitting on-chain. + +### When to Use + +When a user wants to adjust channel allocations while keeping the same channel active. This is more efficient than closing and reopening, and maintains the channel's history and state version continuity. + +:::tip In-Place Update +The resize operation updates the channel **in place**. The channelId **stays the same**, and the channel remains ACTIVE throughout. This is the current implementation of channel allocation adjustment. +::: + +### Prerequisites + +- Channel must exist and be in ACTIVE status +- User must be authenticated +- Positive deltas require enough available unified balance +- Negative deltas require sufficient channel balance +- All participants must sign the resize state (consensus required) +- User must have native currency for gas fees + +### Request + +| Parameter | Type | Required | Description | Default | Example | Notes | +|-----------|------|----------|-------------|---------|---------|-------| +| `channel_id` | string | Yes | Identifier of the channel to resize (stays the same) | - | `"0xabcdef1234567890..."` | 0x-prefixed hex string (32 bytes)
This channel_id will NOT change after resize | +| `allocate_amount` | string (decimal) | No | Amount to add/remove between unified balance and the channel before resize | `0` | `"50.0"` | Decimal string; can be used together with `resize_amount`; at least one of the two must be non-zero | +| `resize_amount` | string (decimal) | No | Delta to apply to the channel: positive to deposit, negative to withdraw | `0` | `"75.0"` or `"-100.0"` | Decimal string; can be used together with `allocate_amount`; at least one of the two must be non-zero | +| `funds_destination` | string (wallet address) | Yes | Destination for the user's allocation in the resize state | - | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | 0x-prefixed hex string (20 bytes) | + +### Response + +| Parameter | Type | Description | Example | Notes | +|-----------|------|-------------|---------|-------| +| `channel_id` | string | Same channel identifier (unchanged) | `"0xabcdef1234567890..."` | This does NOT change (in-place update) | +| `state` | State | Resize state to be submitted on-chain
• `intent` = RESIZE (2)
• `version` = current+1
• `state_data` = ABI-encoded `int256[2]` of `[resize_amount, allocate_amount]` (raw units)
• `allocations` = final absolute allocations after resize | See [State structure](#state-structure) | `channel` field is omitted in resize responses | +| `server_signature` | string | Clearnode signature over packed state | `"0x9876fedcba..."` | Hex string | + +### Next Steps After Receiving Response + +The client must submit the resize state to the blockchain: + +1. **Verify the resize state** + - Check channel_id matches (should be unchanged) + - Verify intent is RESIZE (2) + - Confirm version is current + 1 + - Check allocations reflect the requested change + +2. **Sign the resize state** + ``` + packedState = abi.encode( + channel_id, + state.intent, // StateIntent.RESIZE (2) + state.version, // Incremented version + state.data, // ABI-encoded int256[] deltas + state.allocations // Final allocations + ) + user_signature = sign(packedState, participant_private_key) + ``` + +3. **Ensure sufficient balance** + - Positive deltas require enough available unified balance to cover `allocate_amount + resize_amount` (after decimals conversion) + - Negative deltas require the channel to have sufficient funds being deallocated + +4. **Call `Custody.resize()` on-chain** + ``` + custody.resize( + channel_id, // Same channel_id + state, // Resize state + yourSignature, + clearnodeSignature + ) + ``` + +5. **Wait for transaction confirmation** + - Channel remains ACTIVE (no status change) + - Funds locked or unlocked based on delta + - Expected deposits updated to new amounts + +6. **Monitor for `Resized` event** + ```javascript + event Resized(bytes32 indexed channelId, int256[] deltaAllocations) + ``` + - Emitted when resize completes + - Contains the delta amounts applied + - Confirms operation success + +7. **Update local state** + - Channel_id remains the same (no replacement needed) + - Unified balance automatically updated + - Version incremented + +### Error Cases + +| Error | Cause | Resolution | +|-------|-------|------------| +| Authentication required | Not authenticated | Complete authentication flow | +| Channel not found | Invalid channel_id | Verify with `get_channels` | +| Channel challenged | Participant has challenged channels | Resolve challenged channels first | +| Operation denied: resize already ongoing | Channel status is `resizing` | Wait for existing resize to complete | +| Operation denied: channel is not open | Status not `open` | Only open channels can resize | +| Invalid signature | Caller not among channel signers | Sign request with channel participant | +| Token/asset not found for channel | Asset config missing for channel token/chain | Ensure channel token is supported | +| Resize operation requires non-zero amounts | Both `resize_amount` and `allocate_amount` are zero | Provide a non-zero value | +| Insufficient unified balance | New channel amount would exceed available balance | Reduce amounts or add funds | +| New channel amount must be positive | Resize would make channel balance negative | Reduce withdrawal | +| Failed to pack resize amounts/state | Internal packing/signing error | Retry; contact support if persistent | + +### Resize Scenarios + +#### Scenario 1: Depositing Additional Funds + +**Initial State**: +``` +Channel (on Polygon): 20 USDC +Channel (on Celo): 5 USDC +Unified balance: 25 USDC total +``` + +**Operation**: +```javascript +resize_channel({ + channel_id: "0xCelo_Channel_Id", // Resize Celo channel + allocate_amount: "0", + resize_amount: "75.0", // Deposit 75 USDC + funds_destination: "0x742d35Cc..." // Required, even for deposits +}) +``` + +**Result**: +``` +Channel (on Polygon): 20 USDC (unchanged) +Channel (on Celo): 80 USDC (5 + 75 = 80) +Unified balance: 100 USDC total (reduced available balance to fund deposit) +Same channel_id on Celo (unchanged) +``` + +--- + +#### Scenario 2: Withdrawing Funds + +**Initial State**: +``` +Channel (on Polygon): 100 USDC +Unified balance: 100 USDC total (all locked in channel) +``` + +**Operation**: +```javascript +resize_channel({ + channel_id: "0xPolygon_Channel_Id", + allocate_amount: "0", + resize_amount: "-100.0", // Withdraw all 100 USDC + funds_destination: "0x742d35Cc..." // User's wallet +}) +``` + +**Result**: +``` +Channel (on Polygon): 0 USDC (100 - 100 = 0) +Unified balance: 0 USDC +100 USDC returned to available balance (unified) +Same channel_id (unchanged) +Channel still ACTIVE (can be used again or closed) +``` + +--- + +#### Scenario 3: Complex Multi-Chain Rebalancing + +**Initial State**: +``` +Channel (on Polygon): 20 USDC +Channel (on Celo): 80 USDC +Unified balance: 100 USDC total +Want to withdraw all on Polygon (100 USDC) +``` + +**Operation**: +```javascript +// First, allocate Celo funds to Polygon channel +resize_channel({ + channel_id: "0xPolygon_Channel_Id", + allocate_amount: "80.0", // Allocate from Celo + resize_amount: "-100.0", // Withdraw 100 total + funds_destination: "0x742d35Cc..." +}) +``` + +**Result**: +``` +Channel (on Polygon): 0 USDC +Channel (on Celo): 0 USDC (deallocated) +100 USDC withdrawn to user's wallet +``` + +:::caution Complex Rebalancing +Multi-chain rebalancing with `allocate_amount` is an advanced feature. For simple deposit/withdrawal on a single channel, use only `resize_amount` with `allocate_amount` = "0". +::: + +### Implementation Notes + +- The `resize()` function operates **in place** on the same channel +- channelId **never changes** (no new channel created) +- Channel remains in **ACTIVE** status throughout +- State **version increments** like any state update +- Delta amounts are encoded as **int256[]** in state.data +- Positive deltas increase channel balance (and reduce available unified balance) +- Negative deltas decrease channel balance (and increase available unified balance) +- **All participants must sign** the resize state (consensus required) +- More gas-efficient than close + reopen +- Unified balance automatically updated by clearnode +- Channel history and state continuity preserved + +--- + +## Next Steps + +Explore other off-chain operations: + +- **[Transfers](./transfers)** - Send instant off-chain payments using unified balance +- **[App Sessions](./app-sessions)** - Create multi-party application channels +- **[Queries](./queries)** - Check channel status, balances, and history + +For protocol fundamentals: +- **[Authentication](./authentication)** - Understand authorization and session management +- **[Message Format](./message-format)** - Learn request/response structure +- **[On-Chain Protocol](/docs/protocol/app-layer/on-chain/overview)** - Deep dive into smart contracts diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/message-format.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/message-format.mdx new file mode 100644 index 0000000..6f5155f --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/message-format.mdx @@ -0,0 +1,406 @@ +--- +sidebar_position: 2 +title: Message Format +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Message Format + +The Nitro RPC protocol uses a compact, efficient message format for all communication between clients and a clearnode. + +--- + +## General Structure + +Every Nitro RPC message consists of a compact JSON array format: + +```javascript +[requestId, method, params, timestamp] +``` + +:::tip Compact Format +This array-based format reduces message overhead by approximately 30% compared to traditional JSON-RPC, making it ideal for high-frequency state channel operations. +::: + +### Components + +| Component | Type | Description | +|-----------|------|-------------| +| **requestId** | uint64 | Unique identifier for the request, used to correlate responses | +| **method** | string | Remote method name to be invoked | +| **params** | object | Method-specific parameters as a JSON object | +| **timestamp** | uint64 | Server-provided timestamp in milliseconds | + +#### requestId + +- **Purpose**: Correlate requests with their responses +- **Type**: Unsigned 64-bit integer +- **Generation**: Client-generated, must be unique per connection +- **Range**: 0 to 2^64-1 +- **Example**: `1`, `42`, `9876543210` + +#### method + +- **Purpose**: Specify which RPC method to invoke +- **Type**: String +- **Format**: snake_case (e.g., `create_channel`, not `createChannel`) +- **Examples**: `auth_request`, `transfer`, `create_app_session` + +#### params + +- **Purpose**: Provide method-specific parameters +- **Type**: JSON object +- **Content**: Varies by method +- **Example**: `{"chain_id": 137, "token": "0x...", "amount": "100000000"}` +- **Reference**: See [Authentication](./authentication), [Channel Methods](./channel-methods), [Transfers](./transfers), [App Sessions](./app-sessions), and [Queries](./queries) for parameter specifications + +#### timestamp + +- **Purpose**: Request ordering and replay attack prevention +- **Type**: Unsigned 64-bit integer (Unix milliseconds) +- **Generation**: Client-provided on requests; server-provided on responses +- **Example**: `1699123456789` (November 5, 2023, 01:57:36 UTC) + +--- + +## Request Message + +A complete request message wraps the payload array and includes signatures. + +### Structure + +```json +{ + "req": [requestId, method, params, timestamp], + "sig": [signature1, signature2, ...] +} +``` + +### Fields + +#### req + +The request payload as a 4-element array containing: +- Request ID +- Method name +- Parameters object +- Timestamp + +#### sig + +Array of ECDSA signatures, one or more depending on the operation: +- **Single signature**: Most operations (signed by client's session key) +- **Multiple signatures**: Multi-party operations (e.g., app session creation) + +### Signature Format + +Each signature is: +- **Format**: 0x-prefixed hex string +- **Length**: 65 bytes (130 hex characters + "0x" prefix) +- **Components**: r (32 bytes) + s (32 bytes) + v (1 byte) +- **Algorithm**: ECDSA over secp256k1 curve +- **Hash**: keccak256 of the exact `req` array bytes + +**Example Signature**: +``` +0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01 +``` + +:::info EVM-Specific Format +This signature format (ECDSA over secp256k1 with keccak256 hashing) is specific to EVM-compatible chains. If the protocol extends to support non-EVM chains in the future, signature formats may need to be adapted to match those chains' native cryptographic primitives. +::: + +:::caution Signature Security +Signatures are computed over the keccak256 hash of the JSON-encoded `req` array. The JSON encoding MUST be consistent (same key ordering, no extra whitespace) to ensure signature validity. +::: + +### Complete Example + +```json +{ + "req": [ + 1, + "auth_request", + { + "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", + "session_key": "0x9876543210fedcba9876543210fedcba98765432", + "application": "trading-dex", + "allowances": [ + {"asset": "usdc", "amount": "1000.0"}, + {"asset": "eth", "amount": "0.5"} + ], + "scope": "transfer,app.create", + "expires_at": 1762417328123 + }, + 1699123456789 + ], + "sig": [ + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01" + ] +} +``` + +--- + +## Response Message + +The clearnode sends response messages with the same structure, replacing `params` with `result`. + +### Structure + +```json +{ + "res": [requestId, method, result, timestamp], + "sig": [signature1, ...] +} +``` + +### Fields + +#### res + +The response payload as a 4-element array: +- Same **requestId** (to correlate with request) +- **method** (response method name) + - Usually matches the request method + - **Exception**: `auth_request` → response has `auth_challenge` method + - **Exception**: Errors → response has `error` method +- **result** (method-specific response data, replaces params) +- **timestamp** (server response time) + +#### sig + +The clearnode's signature(s) over the response: +- Proves response authenticity +- Verifies response hasn't been tampered with +- Enables non-repudiation + +### Complete Example + +```json +{ + "res": [ + 1, + "auth_challenge", + { + "challenge_message": "550e8400-e29b-41d4-a716-446655440000" + }, + 1699123457000 + ], + "sig": [ + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab" + ] +} +``` + +--- + +## Error Response + +When an error occurs, the clearnode sends an error response with method set to `"error"`. + +### Structure + +```json +{ + "res": [ + requestId, + "error", + { + "error": "Error description message" + }, + timestamp + ], + "sig": ["0xServerSignature..."] +} +``` + +The result object at position 2 contains a single `"error"` field with a descriptive error message string. + +### Error Examples + +**Authentication Required**: +```json +{ + "res": [ + 5, + "error", + { + "error": "Authentication required: session not established" + }, + 1699123456789 + ], + "sig": ["0xServerSignature..."] +} +``` + +**Insufficient Balance**: +```json +{ + "res": [ + 12, + "error", + { + "error": "Insufficient balance: required 100 USDC, available 75 USDC" + }, + 1699123456790 + ], + "sig": ["0xServerSignature..."] +} +``` + +**Method Not Found**: +```json +{ + "res": [ + 8, + "error", + { + "error": "Method not found: 'invalid_method'" + }, + 1699123456791 + ], + "sig": ["0xServerSignature..."] +} +``` + +:::tip Error Handling +Check the response method field (position 1 in `res` array). If it equals `"error"`, extract the error message from the result object's `error` field. The error message provides human-readable context about what went wrong. +::: + +--- + +## Payload Hash Computation + +Every RPC message (request or response) is signed over the exact serialized `req` or `res` array bytes. + +### What is Signed + +- **Requests**: The `req` array `[requestId, method, params, timestamp]` exactly as sent +- **Responses**: The `res` array `[requestId, method, result, timestamp]` exactly as received + +### Hash Formula + +```javascript +payloadHash = keccak256() +``` + +Use the same bytes you transmit (or receive) when computing/verifying the hash; do not re-serialize with different spacing or key ordering. + +### Example + +**Request Payload**: +```json +[42,"create_app_session",{"definition":{...},"allocations":[...]},1699123456789] +``` + +Hash that exact byte string, then sign it (client for requests, clearnode for responses). + +--- + +## Message Flow Diagram + +The following diagram illustrates the complete request-response cycle: + +```mermaid +sequenceDiagram + box rgb(135,206,235) Client + participant Client + end + box rgb(255,182,193) Clearnode + participant Clearnode + end + + Note over Client: Generate Request + Client->>Client: Create payload: [request_id, method, params, 0] + Client->>Client: Sign payload with session key + + Client->>Clearnode: Send Request {req, sig} + + Note over Clearnode: Process Request + Clearnode->>Clearnode: Verify signature + Clearnode->>Clearnode: Validate parameters + Clearnode->>Clearnode: Execute method logic + Clearnode->>Clearnode: Generate result + + Note over Clearnode: Generate Response + Clearnode->>Clearnode: Create response: [request_id, method, result, timestamp] + Clearnode->>Clearnode: Sign response + + Clearnode->>Client: Send Response {res, sig} + + Note over Client: Process Response + Client->>Client: Verify Clearnode signature + Client->>Client: Correlate by request_id + Client->>Client: Handle result + +``` + +--- + +## Signature Verification Process + +Both clients and a clearnode MUST verify signatures on all messages. + +### Client Verifying a Clearnode Response + +1. **Extract Response**: Get `res` array from response +2. **Compute Hash**: `hash = keccak256()` +3. **Recover Address**: Use `sig` to recover signer address +4. **Verify**: Confirm recovered address matches the clearnode's known address + +### A Clearnode Verifying Client Request + +1. **Extract Request**: Get `req` array from request +2. **Compute Hash**: `hash = keccak256()` +3. **Recover Address**: Use `sig` to recover signer address +4. **Verify**: Confirm recovered address matches authenticated user or registered session key + +:::warning Signature Verification Requirements +**Most** messages MUST be cryptographically signed and verified. All state-changing operations (channel creation/closure, transfers, app sessions) and authenticated methods require valid signatures. However, **some query methods** (such as `get_config`) may be accessed without signatures. Refer to individual method specifications for signature requirements. +::: + +--- + +## JSON Encoding Consistency + +To ensure signature validity, JSON encoding MUST be consistent across all implementations. + +### Requirements + +1. **Key Ordering**: Object keys MUST be in a deterministic order +2. **No Whitespace**: Remove all unnecessary whitespace +3. **No Trailing Commas**: Standard JSON (no trailing commas) +4. **UTF-8 Encoding**: Use UTF-8 character encoding +5. **Number Format**: Numbers as strings for large integers (avoid precision loss) + +### Canonical Example + +**Consistent** (valid for signing): +```json +[1,"transfer",{"amount":"100","asset":"usdc","destination":"0x..."},1699123456] +``` + +**Inconsistent** (would produce different hash): +```json +[ 1, "transfer", { "destination": "0x...", "amount": "100", "asset": "usdc" }, 1699123456 ] +``` + +:::tip Implementation Note +Use a JSON library that supports canonical JSON serialization, or implement strict key ordering and whitespace removal before computing hashes. +::: + +--- + +## Next Steps + +Now that you understand the message format, explore how it's used in practice: + +- **[Authentication](./authentication)** - Learn the 3-step authentication flow +- **[Channel Methods](./channel-methods)** - See request/response examples for channel operations +- **[Transfers](./transfers)** - Understand transfer message structure +- **[App Sessions](./app-sessions)** - Explore multi-signature app session messages + +For a high-level overview, return to **[Off-Chain RPC Overview](./overview)**. diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/overview.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/overview.mdx new file mode 100644 index 0000000..fe7f310 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/overview.mdx @@ -0,0 +1,242 @@ +--- +sidebar_position: 1 +title: Off-Chain RPC Overview +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Off-Chain RPC Protocol Overview + +The Off-Chain RPC Protocol defines how clients communicate with a clearnode to perform state channel operations without touching the blockchain. + +--- + +## What is Nitro RPC? + +**Nitro RPC** is a lightweight RPC protocol designed for state channel communication. It uses a compact JSON array format for efficiency and includes signature-based authentication. + +:::info Protocol Purpose +Nitro RPC enables clients to interact with a clearnode for channel management, fund transfers, and application-specific operations—all happening off-chain with instant finality and zero gas costs. +::: + +--- + +## Key Features + +### 1. Compact Message Format + +Nitro RPC uses a streamlined JSON array format instead of verbose JSON objects, reducing message size and improving network efficiency. + +```javascript +// Compact format: [requestId, method, params, timestamp] +[1, "create_channel", {"chain_id": 137, "token": "0x...", "amount": "1000000"}, 1699123456789] +``` + +:::tip Efficiency Benefit +The compact array format reduces bandwidth usage by approximately 30% compared to traditional JSON-RPC, crucial for high-frequency state channel updates. +::: + +### 2. Signature-Based Authentication + +Every request and response is cryptographically signed, ensuring: +- **Message authenticity**: Verify sender identity +- **Message integrity**: Detect tampering +- **Non-repudiation**: Proof of communication + +### 3. Multi-Signature Support + +Supports operations requiring multiple participants' signatures: +- Channel creation (user + a clearnode) +- App session state updates (multiple participants based on quorum) +- Cooperative channel closure + +### 4. Timestamp-Based Request Ordering + +All messages include timestamps (client-provided on requests, server-provided on responses) enabling: +- Request ordering +- Replay attack prevention +- Audit trail for debugging + +### 5. Channel-Aware Message Structure + +The protocol understands channel concepts natively: +- Packed states +- Multi-party signatures +- State versioning + +--- + +## Protocol Versions + +Nitro RPC has evolved to support advanced features while maintaining backward compatibility. + +### Version Comparison + +| Feature | NitroRPC/0.2 | NitroRPC/0.4 | +|---------|-------------|-------------| +| **Status** | Legacy | **Current** | +| **Basic State Updates** | ✅ | ✅ | +| **Intent System** | ❌ | ✅ | +| **DEPOSIT Intent** | ❌ | ✅ (add funds to app sessions) | +| **WITHDRAW Intent** | ❌ | ✅ (remove funds from app sessions) | +| **OPERATE Intent** | Implicit only | ✅ Explicit | +| **Recommended** | No | **Yes** | + +:::caution Version Recommendation +**Always use NitroRPC/0.4** for new implementations. Version 0.2 is maintained for backward compatibility only and lacks the intent system required for flexible app session management. +::: + +### NitroRPC/0.2 (Legacy) + +**Features**: +- Basic state updates for app sessions +- All updates redistribute existing funds +- Cannot add or remove funds from active sessions +- Must close and recreate sessions to change total funds + +**Use Case**: Maintained for existing applications, not recommended for new development. + +### NitroRPC/0.4 (Current) + +**Features**: +- Intent-based state updates: **OPERATE**, **DEPOSIT**, **WITHDRAW** +- Add funds to active app sessions (DEPOSIT) +- Remove funds from active sessions (WITHDRAW) +- Better error handling and validation +- Enhanced security checks + +**Use Case**: All new implementations should use this version. + +--- + +## Communication Architecture + +Nitro RPC enables bidirectional real-time communication between clients and a clearnode. + +```mermaid +graph TB + A[Client Application] <-->|RPC Connection| B[Clearnode] + B <-->|Event Monitoring| C[Blockchain] + B <-->|State Management| D[Database] + + A -->|"1. RPC Request
(signed)"| B + B -->|"2. Process & Validate"| D + B -->|"3. RPC Response
(signed)"| A + + B -.->|"Async Notifications"| A + C -.->|"Blockchain Events"| B + + style A fill:#e1f5ff + style B fill:#ffe1f5 + style C fill:#f5ffe1 + style D fill:#fff5e1 +``` + +### Connection Flow + +1. **Client Establishes Connection**: Open persistent connection to a clearnode +2. **Authentication**: Complete 3-step auth flow (auth_request → auth_challenge → auth_verify) +3. **RPC Communication**: Send requests, receive responses +4. **Notifications**: Receive real-time updates (balance changes, channel events) +5. **Keep-Alive**: Periodic ping/pong to maintain connection (optional, depends upon the implementation chosen) + +--- + +## Message Categories + +Nitro RPC methods are organized into functional categories: + +### 1. Authentication Methods + +Establish and manage authenticated sessions: +- `auth_request` - Initiate authentication (response: `auth_challenge`) +- `auth_verify` - Complete authentication with challenge response + +### 2. Channel Management Methods + +Create and manage payment channels: +- `create_channel` - Open new channel +- `close_channel` - Cooperatively close channel +- `resize_channel` - Adjust channel allocations + +### 3. Transfer Methods + +Move funds between users: +- `transfer` - Send funds off-chain with instant settlement + +### 4. App Session Methods + +Manage multi-party application channels: +- `create_app_session` - Create new app session +- `submit_app_state` - Update session state (with intents) +- `close_app_session` - Finalize and distribute funds + +### 5. Query Methods + +Read state and configuration: +- Public: `get_config`, `get_assets`, `get_app_definition`, `get_channels`, `get_app_sessions`, `get_ledger_entries`, `get_ledger_transactions`, `ping` +- Private (auth required): `get_ledger_balances`, `get_rpc_history`, `get_user_tag`, `get_session_keys` + +### 6. Notifications (Server-to-Client) + +Real-time updates: +- `bu` (balance update) - Balance changed +- `cu` (channel update) - Channel status changed +- `tr` (transfer) - Incoming/outgoing transfer +- `asu` (app session update) - App session state changed + +--- + +## Security Model + +The Off-Chain RPC Protocol provides multiple layers of security: + +### Cryptographic Security + +- **ECDSA Signatures**: Every message signed with secp256k1 +- **Keccak256 Hashing**: Message integrity verification +- **Challenge-Response Auth**: Prove key ownership without exposing private keys + +### Protocol-Level Security + +- **Request Ordering**: Timestamps prevent replay attacks +- **Session Expiration**: Session keys have time limits +- **Spending Allowances**: Limit session key spending power +- **Signature Verification**: All operations require valid signatures + +### Network Security + +- **TLS Encrypted Communication**: Encrypted communication channel +- **Origin Validation**: Prevent unauthorized connections + +:::success Strong Security Model +The combination of cryptographic signatures, challenge-response authentication, and spending allowances ensures that even if a session key is compromised, damage is limited by spending caps and expiration times. +::: + +--- + +## Next Steps + +Explore the detailed specifications for each part of the protocol: + +- **[Message Format](./message-format)** - Learn the request/response structure +- **[Authentication](./authentication)** - Implement secure session management +- **[Channel Methods](./channel-methods)** - Create and manage payment channels +- **[Transfers](./transfers)** - Enable instant off-chain payments +- **[App Sessions](./app-sessions)** - Build multi-party applications +- **[Queries & Notifications](./queries)** - Read state and receive updates + +--- + +## Key Concepts + +Before diving into specific methods, ensure you understand these core concepts from the protocol foundation: + +- **Channel** - Payment channel locking funds on-chain +- **State** - Snapshot of channel at a point in time +- **Participant** - Entity in a channel (user, a clearnode) +- **Unified Balance** - Aggregated balance across chains +- **Session Key** - Temporary key with spending limits + +Refer to the **[Terminology](/docs/protocol/terminology)** page for complete definitions. diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/queries.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/queries.mdx new file mode 100644 index 0000000..18ec0bd --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/queries.mdx @@ -0,0 +1,877 @@ +--- +sidebar_position: 7 +title: Query Methods & Notifications +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Query Methods & Notifications + +Query methods retrieve information from a clearnode, while notifications provide real-time updates about state changes. + +--- + +## Overview + +The Nitro RPC protocol provides two types of information retrieval: + +**Query Methods**: Client-initiated requests to retrieve current state information (balances, channels, sessions, transactions). + +**Notifications**: Server-initiated messages sent to all relevant active connections when events occur (balance changes, channel updates, incoming transfers). + +:::tip Real-Time Updates +Combine query methods for initial state retrieval with notifications for ongoing monitoring. This pattern ensures your application always reflects the latest state without constant polling. +::: + +--- + +## Query Methods Summary + +| Method | Authentication | Purpose | Pagination | +|--------|---------------|---------|------------| +| `get_config` | Public | Retrieve clearnode configuration | No | +| `get_assets` | Public | List supported assets | No | +| `get_app_definition` | Public | Fetch the definition for a specific app session | No | +| `get_channels` | Public | List payment channels | Yes | +| `get_app_sessions` | Public | List app sessions | Yes | +| `get_ledger_balances` | Private | Query current balances | No | +| `get_ledger_entries` | Public | Detailed accounting entries | Yes | +| `get_ledger_transactions` | Public | User-facing transaction history | Yes | +| `get_rpc_history` | Private | Fetch recent RPC invocations | Yes | +| `get_user_tag` | Private | Retrieve user's alphanumeric tag | No | +| `get_session_keys` | Private | List active session keys | Yes | +| `ping` | Public | Connection health check | No | + +:::info Authentication +**Public methods** can be called without authentication. **Private methods** require completing the [authentication flow](./authentication) first. +::: + +:::note Pagination defaults +Unless explicitly provided, paginated methods default to `limit = 10` (maximum 100) and `offset = 0`, matching the broker’s `ListOptions`. +::: + +--- + +## get_config + +### Name + +`get_config` + +### Usage + +Retrieves the clearnode's configuration: broker address plus supported blockchains and their custody/adjudicator contracts. + +### Request + +No parameters. + +### Response + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `broker_address` | string | Clearnode's wallet address | `"0xbbbb567890abcdef..."` | +| `networks` | array\ | List of supported blockchain networks | See structure below | + +#### BlockchainInfo Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `chain_id` | uint32 | Network identifier | `137` (Polygon) | +| `name` | string | Human-readable blockchain name | `"Polygon"` | +| `custody_address` | string | Custody contract address on this chain | `"0xCustodyContractAddress..."` | +| `adjudicator_address` | string | Adjudicator contract address on this chain | `"0xAdjudicatorAddress..."` | + +**Use Cases**: +- Discover supported chains and contract addresses +- Verify clearnode wallet address + +--- + +## get_assets + +### Name + +`get_assets` + +### Usage + +Retrieves all supported assets and their configurations across supported blockchains. + +### Request + +| Parameter | Type | Required | Description | Example | Notes | +|-----------|------|----------|-------------|---------|-------| +| `chain_id` | uint32 | No | Filter by specific chain | `137` | If omitted, returns assets for all chains | + +### Response + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `assets` | array\ | List of supported assets | See structure below | + +#### Asset Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `token` | string | Token contract address | `"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"` | +| `chain_id` | uint32 | Blockchain network identifier | `137` | +| `symbol` | string | Token symbol | `"usdc"` | +| `decimals` | uint8 | Number of decimal places | `6` | + +**Use Cases**: +- Display supported assets in UI +- Validate asset identifiers before transfers +- Get contract addresses for specific chains + +--- + +## get_app_definition + +### Name + +`get_app_definition` + +### Usage + +Retrieves the immutable definition for a given app session so clients can verify governance parameters and participants. + +### Request + +| Parameter | Type | Required | Description | Example | +|-----------|------|----------|-------------|---------| +| `app_session_id` | string | Yes | Target app session identifier | `"0x9876543210fedcba..."` | + +### Response + +Returns the [AppDefinition](../off-chain/app-sessions#appdefinition) structure: + +| Field | Type | Description | +|-------|------|-------------| +| `protocol` | string | Protocol version (`"NitroRPC/0.2"` or `"NitroRPC/0.4"`) | +| `participants` | array\ | Wallet addresses authorized for this session | +| `weights` | array\ | Voting weight per participant (aligned with `participants` order) | +| `quorum` | uint64 | Minimum combined weight required for updates | +| `challenge` | uint64 | Dispute timeout (seconds) | +| `nonce` | uint64 | Unique instance identifier | + +**Use Cases**: +- Validate session metadata before signing states +- Display governance rules in UI +- Confirm protocol version compatibility + +--- + +## get_channels + +### Name + +`get_channels` + +### Usage + +Lists all channels for a specific participant address across all supported chains. + +### Request + +| Parameter | Type | Required | Description | Default | Example | +|-----------|------|----------|-------------|---------|---------| +| `participant` | string | No | Participant wallet address to query | (empty = all channels) | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | +| `status` | string | No | Filter by status | - | `"open"` | +| `offset` | number | No | Pagination offset | `0` | `42` | +| `limit` | number | No | Number of channels to return | `10` (max 100) | `10` | +| `sort` | string | No | Sort order by created_at | `"desc"` | `"desc"` | + +**Allowed status values**: `"open"` \| `"closed"` \| `"challenged"` \| `"resizing"` + +### Response + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `channels` | array\ | List of channels | See structure below | + +#### Channel Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `channel_id` | string | Unique channel identifier | `"0xabcdef..."` | +| `participant` | string | User's wallet address | `"0x742d35Cc..."` | +| `status` | string | Channel status | `"open"` | +| `token` | string | Asset contract address | `"0x2791Bca1..."` | +| `wallet` | string | Participant's wallet address | `"0x742d35Cc..."` | +| `amount` | string | Total channel capacity (human-readable) | `"100.0"` | +| `chain_id` | uint32 | Blockchain network identifier | `137` | +| `adjudicator` | string | Dispute resolution contract address | `"0xAdjudicator..."` | +| `challenge` | uint64 | Dispute timeout period (seconds) | `3600` | +| `nonce` | uint64 | Unique nonce ensuring channel uniqueness | `1699123456789` | +| `version` | uint64 | Current state version | `5` | +| `created_at` | string | Channel creation timestamp (ISO 8601) | `"2023-05-01T12:00:00Z"` | +| `updated_at` | string | Last modification timestamp (ISO 8601) | `"2023-05-01T14:30:00Z"` | + +**Use Cases**: +- Display user's open channels +- Check channel status before operations +- Monitor multi-chain channel distribution + +--- + +## get_app_sessions + +### Name + +`get_app_sessions` + +### Usage + +Lists all app sessions for a participant, sorted by creation date (newest first by default). Optionally filter by status (open/closed). Returns complete session information including participants, voting weights, quorum, protocol version, and current state. Supports pagination for large result sets. + +### Request + +| Parameter | Type | Required | Description | Default | Allowed Values | Example | +|-----------|------|----------|-------------|---------|----------------|---------| +| `participant` | string (address) | No | Filter by participant wallet address | (empty = all sessions) | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | +| `status` | string | No | Filter by status | - | `"open"` | +| `offset` | number | No | Pagination offset | 0 | - | `42` | +| `limit` | number | No | Number of sessions to return | 10 (max 100) | - | `10` | +| `sort` | string | No | Sort order by created_at | "desc" | `"desc"` | + +**Allowed status values**: `"open"` \| `"closed"` + +### Response + +| Parameter | Type | Description | See Also | +|-----------|------|-------------|----------| +| `app_sessions` | array\ | List of app sessions | See structure below | + +#### AppSessionInfo + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `app_session_id` | string | Unique identifier | `"0x9876543210fedcba..."` | +| `application` | string | Application identifier | `"VirtualAppChess"` | +| `status` | string | Current status | `"open"` \| `"closed"` | +| `participants` | array\ | All participant wallet addresses | `["0x742d35Cc...", "0x8B3192f2..."]` | +| `weights` | array\ | Voting weights per participant | `[50, 50, 100]` | +| `quorum` | uint64 | Required weight for state updates | `100` | +| `protocol` | string | Protocol version | `"NitroRPC/0.4"` | +| `challenge` | uint64 | Challenge period in seconds | `86400` | +| `version` | number | Current state version | `5` | +| `nonce` | uint64 | Unique session identifier | `1699123456789` | +| `session_data` | string | Current application state | `"{\"gameType\":\"chess\",\"turn\":\"white\"}"` | +| `created_at` | string (timestamp) | Creation timestamp | `"2023-05-01T12:00:00Z"` | +| `updated_at` | string (timestamp) | Last update timestamp | `"2023-05-01T14:30:00Z"` | + +**Use Cases**: +- Display user's active games or escrows +- Monitor session history +- Paginate through large session lists + +:::tip Pagination Best Practice +When dealing with users who have many app sessions, use pagination with reasonable `limit` values (10-50) to improve performance and user experience. +::: + +--- + +## get_ledger_balances + +### Name + +`get_ledger_balances` + +### Usage + +Retrieves the ledger balances for an account. If no parameters are provided, returns the authenticated user's unified balance across all assets. Can also query balance within a specific app session by providing the app_session_id. Returns all tracked assets (including those that currently evaluate to zero). + +### Request + +| Parameter | Type | Required | Description | Format | Example | +|-----------|------|----------|-------------|--------|---------| +| `account_id` | string | No | Account or app session identifier | 0x-prefixed hex string or wallet address | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | + +:::info App Session Balances +To query balance within a specific app session, provide the `app_session_id` as the `account_id`. +::: + +### Response + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `ledger_balances` | array\ | Balance per asset | See structure below | + +#### Balance Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `asset` | string | Asset identifier | `"usdc"` | +| `amount` | string | Balance in human-readable format | `"100.0"` | + +**Use Cases**: +- Display user's current balances +- Check available funds before operations +- Monitor balance changes in real-time + +--- + +## get_ledger_entries + +### Name + +`get_ledger_entries` + +### Usage + +Retrieves detailed ledger entries for an account, providing a complete audit trail of all debits and credits. Each entry represents one side of a double-entry bookkeeping transaction. Used for detailed financial reconciliation and accounting. Supports filtering by account, asset, and pagination. Sorted by creation date (newest first by default). + +### Request + +| Parameter | Type | Required | Description | Default | Allowed Values | Example | +|-----------|------|----------|-------------|---------|----------------|---------| +| `account_id` | string | No | Filter by account identifier | - | - | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | +| `wallet` | string (address) | No | Filter by wallet address | - | - | `"0x742d35Cc..."` | +| `asset` | string | No | Filter by asset | - | - | `"usdc"` | +| `offset` | number | No | Pagination offset | 0 | - | - | +| `limit` | number | No | Number of entries to return | 10 (max 100) | - | - | +| `sort` | string | No | Sort order by created_at | "desc" | "asc" \| "desc" | - | + +### Response + +| Parameter | Type | Description | Structure | Example | +|-----------|------|-------------|-----------|---------| +| `ledger_entries` | array\ | List of ledger entries | See structure below | + +#### LedgerEntry Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `id` | number | Unique entry identifier | `123` | +| `account_id` | string | Account this entry belongs to | `"0x742d35Cc..."` | +| `account_type` | number | Ledger account classification (`1000`=asset, `2000`=liability, etc.) | `1000` | +| `asset` | string | Asset symbol | `"usdc"` | +| `participant` | string | Participant wallet address | `"0x742d35Cc..."` | +| `credit` | string | Credit amount (incoming funds, "0.0" if debit) | `"100.0"` | +| `debit` | string | Debit amount (outgoing funds, "0.0" if credit) | `"25.0"` | +| `created_at` | string | Entry creation timestamp (ISO 8601) | `"2023-05-01T12:00:00Z"` | + +Account types follow the broker’s GAAP-style codes: `1000` series for assets, `2000` liabilities, `3000` equity, `4000` revenue, and `5000` expenses. + +### Double-Entry Bookkeeping + +Every transaction creates two entries: + +``` +Transfer: Alice sends 50 USDC to Bob + +Entry 1 (Alice's ledger): + account_id: Alice's address + asset: usdc + credit: 0.0 + debit: 50.0 + +Entry 2 (Bob's ledger): + account_id: Bob's address + asset: usdc + credit: 50.0 + debit: 0.0 +``` + +:::info Accounting Principle +The double-entry system ensures that the total of all debits always equals the total of all credits, providing mathematical proof of accounting accuracy. This is the same principle used by traditional financial institutions. +::: + +**Use Cases**: +- Detailed financial reconciliation +- Audit trail generation +- Accounting system integration +- Verify balance calculations + +--- + +## get_ledger_transactions + +### Name + +`get_ledger_transactions` + +### Usage + +Retrieves user-facing transaction history showing transfers, deposits, withdrawals, and app session operations. Unlike ledger entries (which show accounting details), this provides a simplified view of financial activity with sender, receiver, amount, and transaction type. Supports filtering by asset and transaction type. Sorted by creation date (newest first by default). + +### Request + +| Parameter | Type | Required | Description | Default | Allowed Values | Example | +|-----------|------|----------|-------------|---------|----------------|---------| +| `account_id` | string | No | Filter by account identifier | - | - | `"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"` | +| `asset` | string | No | Filter by asset | - | - | `"usdc"` | +| `tx_type` | string | No | Filter by transaction type | - | "transfer" \| "deposit" \| "withdrawal" \| "app_deposit" \| "app_withdrawal" \| "escrow_lock" \| "escrow_unlock" | `"transfer"` | +| `offset` | number | No | Pagination offset | 0 | - | - | +| `limit` | number | No | Number of transactions to return | 10 (max 100) | - | - | +| `sort` | string | No | Sort order by created_at | "desc" | "asc" \| "desc" | - | + +### Response + +| Parameter | Type | Description | Example | +|-----------|------|-------------|---------| +| `ledger_transactions` | array\ | List of transactions | See structure below | + +#### LedgerTransaction Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `id` | number | Unique transaction identifier | `1` | +| `tx_type` | string | Transaction type | `"transfer"` | +| `from_account` | string | Sender account identifier (wallet, channel, or app session) | `"0x742d35Cc..."` | +| `from_account_tag` | string | Sender's user tag (empty if none) | `"NQKO7C"` | +| `to_account` | string | Receiver account identifier (wallet, channel, or app session) | `"0x8B3192f2..."` | +| `to_account_tag` | string | Receiver's user tag (empty if none) | `"UX123D"` | +| `asset` | string | Asset symbol | `"usdc"` | +| `amount` | string | Transaction amount | `"50.0"` | +| `created_at` | string | Transaction timestamp (ISO 8601) | `"2023-05-01T12:00:00Z"` | + +`from_account` and `to_account` mirror the broker’s internal `AccountID` values, so they can reference wallets, app session escrow accounts, or channel escrows. + +### Transaction Types + +| Type | Description | Direction | +|------|-------------|-----------| +| **transfer** | Direct transfer between unified balances | Off-chain ↔ Off-chain | +| **deposit** | Funds deposited from channel to unified balance | On-chain → Off-chain | +| **withdrawal** | Funds withdrawn from unified balance to channel | Off-chain → On-chain | +| **app_deposit** | Funds moved from unified balance into app session | Unified → App Session | +| **app_withdrawal** | Funds released from app session to unified balance | App Session → Unified | +| **escrow_lock** | Funds temporarily locked for blockchain operations | Unified → Escrow | +| **escrow_unlock** | Funds released from escrow after blockchain confirmation | Escrow → Unified | + +**Use Cases**: +- Display transaction history in UI +- Export transaction records +- Monitor specific transaction types +- Track payment flows + +--- + +## get_rpc_history + +### Name + +`get_rpc_history` + +### Usage + +Returns the authenticated user's recent RPC invocations, including signed request and response payloads. Useful for audit trails and debugging client integrations. + +### Request + +| Parameter | Type | Required | Description | Default | Example | +|-----------|------|----------|-------------|---------|---------| +| `offset` | number | No | Pagination offset | `0` | `20` | +| `limit` | number | No | Maximum entries to return | `10` (max 100) | `25` | +| `sort` | string | No | Sort order by timestamp | `"desc"` | `"asc"` | + +### Response + +| Parameter | Type | Description | See Also | +|-----------|------|-------------|----------| +| `rpc_entries` | array\ | Recorded invocations | See structure below | + +#### RPCEntry Structure + +| Field | Type | Description | +|-------|------|-------------| +| `id` | number | Internal history identifier | +| `sender` | string | Wallet that issued the call | +| `req_id` | number | Request sequence number | +| `method` | string | RPC method name | +| `params` | string | JSON-encoded request parameters | +| `timestamp` | number | Unix timestamp (seconds) | +| `req_sig` | array\ | Signatures attached to the request | +| `response` | string | JSON-encoded response payload | +| `res_sig` | array\ | Response signatures | + +**Use Cases**: +- Debug client/server mismatches +- Provide user-facing audit logs +- Verify signed payloads during dispute resolution + +--- + +## get_user_tag + +### Name + +`get_user_tag` + +### Usage + +Retrieves the authenticated user's unique alphanumeric tag. User tags provide a human-readable alternative to addresses for [transfer](./transfers) operations, similar to username systems. Tags are automatically generated upon first interaction with a clearnode and remain constant. This is a convenience feature for improving user experience. + +### Request + +No parameters. + +### Response + +| Parameter | Type | Description | Format | Example | Notes | +|-----------|------|-------------|--------|---------|-------| +| `tag` | string | User's unique alphanumeric tag | 6 uppercase alphanumeric characters | `"UX123D"` | Can be used in transfer operations as destination_user_tag | + +### Usage in Transfers + +Instead of using full address: + +```javascript +transfer({destination: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", ...}) +``` + +Users can use the tag: + +```javascript +transfer({destination_user_tag: "UX123D", ...}) +``` + +:::note Human-Readable Addresses +User tags make it easier for users to share their "address" verbally or in non-technical contexts, similar to payment apps like Venmo or Cash App usernames. +::: + +--- + +## get_session_keys + +### Name + +`get_session_keys` + +### Usage + +Retrieves all active (non-expired) session keys for the authenticated user. Shows each session key's address, application name, spending allowances, current usage, expiration, and permissions. Used for managing delegated keys and monitoring spending caps. Only returns session keys (not custody signers). + +### Authentication + +Required (private method) + +### Request + +| Parameter | Type | Required | Description | Default | Example | +|-----------|------|----------|-------------|---------|---------| +| `offset` | number | No | Pagination offset | `0` | `20` | +| `limit` | number | No | Results per page | `10` (max 100) | `25` | +| `sort` | string | No | Sort order by created_at | `"desc"` | `"asc"` | + +### Response + +| Parameter | Type | Description | See Also | +|-----------|------|-------------|----------| +| `session_keys` | array\ | List of active session keys | See structure below | + +#### SessionKeyInfo Structure + +| Field | Type | Description | Default | Notes | +|-------|------|-------------|---------|-------| +| `id` | number | Internal identifier | — | — | +| `session_key` | string (address) | Session key address | — | — | +| `application` | string | Application name for this session | `"clearnode"` | — | +| `allowances` | array\ | Spending limits and usage | — | See structure below | +| `scope` | string | Permission scope | — | Future feature, not fully enforced yet | +| `expires_at` | string (timestamp) | Session expiration time (ISO 8601 format) | — | — | +| `created_at` | string (timestamp) | Session creation time (ISO 8601 format) | — | — | + +**Example**: +```json +{ + "id": 1, + "session_key": "0x9876543210fedcba...", + "application": "Chess Game", + "allowances": [ + {"asset": "usdc", "allowance": "100.0", "used": "45.0"} + ], + "scope": "app.create,transfer", + "expires_at": "2023-05-02T12:00:00Z", + "created_at": "2023-05-01T12:00:00Z" +} +``` + +#### AllowanceUsage + +| Field | Type | Description | +|-------|------|-------------| +| `asset` | string | Asset identifier (e.g., `"usdc"`) | +| `allowance` | string | Total spending limit | +| `used` | string | Amount already spent | + +### Spending Tracking + +The clearnode tracks session key spending by monitoring all ledger debit operations: + +``` +Initial: allowance = 100 USDC, used = 0 USDC +After transfer of 45 USDC: allowance = 100 USDC, used = 45 USDC +Remaining = 55 USDC available for future operations +``` + +When a session key reaches its spending cap, further operations are rejected: + +``` +Error: "operation denied: insufficient session key allowance: 60 required, 55 available" +``` + +:::tip Spending Caps +Session key allowances provide important security: even if a session key is compromised, the maximum loss is limited to the allowance amount. +::: + +**Use Cases**: +- Display active sessions in UI +- Monitor spending against caps +- Manage session lifecycles +- Security auditing + +--- + +## ping + +### Name + +`ping` + +### Usage + +Simple connectivity check to verify the clearnode is responsive and the RPC connection is alive. Returns immediately with success. Used for heartbeat, connection testing, and latency measurement. + +### Authentication + +Not required (public method) + +### Request + +No parameters required (empty object `{}`). + +### Response + +The response method should be `"pong"`. + +| Parameter | Type | Description | Value/Example | Notes | +|-----------|------|-------------|---------------|-------| +| (empty) | object | Empty object or confirmation data | `{}` | Response indicates successful connection | + +### Use Cases + +**Heartbeat**: Periodic ping to keep RPC connection alive +```javascript +setInterval(() => clearnode.call("ping"), 30000) // Every 30 seconds +``` + +**Latency Measurement**: Measure round-trip time +```javascript +const start = Date.now() +await clearnode.call("ping") +const latency = Date.now() - start +console.log(`Latency: ${latency}ms`) +``` + +**Health Check**: Verify connection before critical operations +```javascript +try { + await clearnode.call("ping") + // Connection healthy, proceed with operation +} catch (error) { + // Connection lost, reconnect +} +``` + +**Authentication Status**: Test if session is still valid +```javascript +const response = await clearnode.call("ping") +// If no auth error, session is active +``` + +--- + +## Notifications (Server-to-Client) + +The clearnode sends unsolicited notifications to clients via RPC when certain events occur. These are not responses to requests, but asynchronous messages initiated by the server. + +```mermaid +sequenceDiagram + box rgb(225,245,255) Client + participant Client + end + box rgb(255,225,245) Clearnode + participant Clearnode + end + box rgb(255,245,225) Events + participant Event as Event Source + end + + Note over Client,Clearnode: RPC Connection Established + + Event->>Clearnode: Transfer (incoming/outgoing) + Clearnode->>Client: tr (transfer) notification + + Event->>Clearnode: Balance changed + Clearnode->>Client: bu (balance update) notification + + Event->>Clearnode: Channel opened + Clearnode->>Client: cu (channel update) notification + + Event->>Clearnode: App session updated + Clearnode->>Client: asu (app session update) notification + +``` + +### Notification Types + +| Method | Description | Data Structure | +|--------|-------------|----------------| +| `bu` | Balance update | `balance_updates` array with updated balances | +| `cu` | Channel update | Full `Channel` object | +| `tr` | Transfer (incoming/outgoing) | `transactions` array with transfer details | +| `asu` | App session update | `app_session` object and `participant_allocations` | + +--- + +## bu (Balance Update) + +### Method + +`bu` + +### When Sent + +Whenever account balances change due to transfers, app session operations, or channel operations. + +### Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `balance_updates` | array\ | Updated balances for affected accounts | See structure below | + +#### LedgerBalance Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `asset` | string | Asset symbol | `"usdc"` | +| `amount` | string | New balance amount | `"150.0"` | + +**Use Cases**: +- Update balance display in real-time +- Trigger UI animations for balance changes +- Log balance history for analytics + +--- + +## cu (Channel Update) + +### Method + +`cu` + +### When Sent + +When a channel's state changes (opened, resized, challenged, closed). + +### Structure + +The notification contains the complete updated `Channel` object. See [Channel Structure](#channel-structure) in the `get_channels` section for the full field list. + +**Use Cases**: +- Update channel status in UI +- Alert user when channel becomes active +- Monitor for unexpected channel closures + +--- + +## tr (Transfer) + +### Method + +`tr` + +### When Sent + +When a transfer affects the user's account (both incoming and outgoing transfers). + +### Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `transactions` | array\ | Array of transaction objects for the transfer | See structure below | + +The `LedgerTransaction` structure is identical to the one returned by `get_ledger_transactions`. See [LedgerTransaction Structure](#ledgertransaction-structure) for the full field list. + +**Use Cases**: +- Display incoming/outgoing payment notifications +- Play sound/show toast for transfers +- Update transaction history in real-time + +:::success Real-Time Payments +Combine `tr` notifications with `bu` (balance update) to provide immediate feedback when users send or receive funds. +::: + +--- + +## asu (App Session Update) + +### Method + +`asu` + +### When Sent + +When an app session state changes (new state submitted, session closed, deposits/withdrawals). + +### Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `app_session` | AppSession | Complete app session object | See `get_app_sessions` for structure | +| `participant_allocations` | array\ | Current allocations for each participant | See structure below | + +#### AppAllocation Structure + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `participant` | string | Participant wallet address | `"0x742d35Cc..."` | +| `asset` | string | Asset symbol | `"usdc"` | +| `amount` | string | Allocated amount | `"50.0"` | + +**Use Cases**: +- Update game UI when opponent makes a move +- Refresh session state in real-time +- Alert when session is closed +- Sync multi-participant applications + +--- + +## Implementation Notes + +**Connection Management**: +- Maintain persistent connection for notifications +- Implement automatic reconnection on disconnect +- Re-fetch current state after reconnection + +**Notification Handling**: +- All notifications are asynchronous +- No response required from client +- Multiple notifications may arrive rapidly (batch if needed) + +**Best Practices**: +- Use query methods for initial state retrieval +- Use notifications for ongoing monitoring +- Don't rely solely on notifications (could be missed during disconnect) +- Implement periodic state refresh as backup + +**Pagination**: +- For methods with pagination, use reasonable `limit` values + +--- + +## Next Steps + +Explore other protocol features: + +- **[App Sessions](./app-sessions)** - Create and manage multi-party applications +- **[Transfers](./transfers)** - Send funds between users +- **[Channel Methods](./channel-methods)** - Manage payment channels + +For protocol fundamentals: +- **[Authentication](./authentication)** - Manage session keys +- **[Message Format](./message-format)** - Understand request/response structure diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/transfers.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/transfers.mdx new file mode 100644 index 0000000..ae9f66f --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/off-chain/transfers.mdx @@ -0,0 +1,377 @@ +--- +sidebar_position: 5 +title: Transfer Method +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Transfer Method + +Transfer method enable instant, off-chain fund movement between users. + +--- + +## Overview + +The transfer system allows users to send funds to each other instantly using their unified balance, without any on-chain transactions. Transfers are backed by the security of underlying payment channels and use double-entry bookkeeping for accounting accuracy. + +### Why Use Transfer? + +**Instant Settlement**: Transfers complete immediately with instant finality. + +**No Blockchain Fees**: No blockchain transactions means no gas costs for both sender and recipient. + +**Cross-Chain Unified**: Send from your unified balance across multiple chains. + +**Auditable**: Complete transaction history with double-entry ledger tracking. + +:::success Instant Off-Chain Payments +Transfers provide the speed and convenience of traditional payment networks while maintaining the security guarantees of blockchain-backed channels. +::: + +--- + +## transfer + +### Name + +`transfer` + +### Usage + +Transfer funds from the authenticated user's unified balance to another user's unified balance within the Yellow Network. This is a purely off-chain operation, which results in instant settlement. The transfer updates internal ledger entries using double-entry bookkeeping principles and creates a transaction record for both parties. The security guarantee comes from the underlying on-chain channels that back the unified balance. + +### When to Use + +When sending funds to another Yellow Network user. Common use cases include peer-to-peer payments, merchant payments, tipping. + +### Prerequisites + +- Sender must be [authenticated](./authentication) +- Sender must have sufficient available balance in unified account +- Recipient must be identified by valid wallet address or user tag + +:::info Recipient Requirements +The recipient does not need to have an existing balance or account on the clearnode. Transfers can be sent to any valid wallet address, and the recipient's account will be created automatically on the first login if it doesn't exist. +::: + +### Request + +| Parameter | Type | Required | Description | Format | Example | Notes | +|-----------|------|----------|-------------|--------|---------|-------| +| `destination` | string (wallet address) | Yes (if `destination_user_tag` not provided) | Recipient's wallet address | 0x-prefixed hex string (20 bytes) | `"0x8B3192f2F7b1b34f2e4e7B8C9D1E0F2A3B4C5D6E"` | - | +| `destination_user_tag` | string | Yes (if destination not provided) | Recipient's randomly generated user identifier | Alphanumeric string | `"UX123D"` | Alternative to address; internal feature, may change | +| `allocations` | TransferAllocation[] | Yes (minimum: 1) | Assets and amounts to transfer | Array of allocation objects | `[{"asset": "usdc", "amount": "50.0"}]` | See structure below | + +#### TransferAllocation Structure + +Each allocation in the `allocations` array specifies an asset and amount to transfer: + +| Field | Type | Required | Description | Format | Example | +|-------|------|----------|-------------|--------|---------| +| `asset` | string | Yes | Asset symbol identifier | Lowercase string | `"usdc"`, `"eth"`, `"weth"`, `"btc"` | +| `amount` | string | Yes | Amount to transfer in human-readable format | Decimal string | `"50.0"`, `"0.01"` | + +**Notes**: +- Asset symbols must be lowercase +- Use `get_assets` method to see all supported assets +- Amounts are in human-readable format (e.g., "50.0" for 50 USDC) +- Clearnode handles conversion to smallest unit internally +- Multiple assets can be transferred in a single operation + +**Example**: +```json +{ + "allocations": [ + { + "asset": "usdc", + "amount": "50.0" + }, + { + "asset": "eth", + "amount": "0.01" + } + ] +} +``` + +### Response + +The response contains an array of transactions, with one transaction for each asset being transferred: + +| Parameter | Type | Description | Example | Notes | +|-----------|------|-------------|---------|-------| +| `transactions` | LedgerTransaction[] | Array of transaction objects for each asset | See below | One transaction per asset transferred | + +**LedgerTransaction Structure** (per transaction): + +| Field | Type | Description | Example | +|-------|------|-------------|---------| +| `id` | number | Numeric transaction identifier | `1` | +| `tx_type` | string | Transaction type | `"transfer"` | +| `from_account` | string | Sender account identifier (wallet/app session/channel) | `"0x1234567890abcdef..."` | +| `from_account_tag` | string | Sender's user tag (if exists) | `"NQKO7C"` | +| `to_account` | string | Recipient account identifier | `"0x9876543210abcdef..."` | +| `to_account_tag` | string | Recipient's user tag (if exists) | `"UX123D"` | +| `asset` | string | Asset symbol that was transferred | `"usdc"` | +| `amount` | string | Amount transferred for this asset (decimal string) | `"50.0"` | +| `created_at` | string | ISO 8601 timestamp | `"2023-05-01T12:00:00Z"` | + +**Example Response**: + +```json +{ + "transactions": [ + { + "id": 1, + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": 2, + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "eth", + "amount": "0.1", + "created_at": "2023-05-01T12:00:00Z" + } + ] +} +``` + +--- + +## Off-Chain Processing + +When a transfer is executed, the clearnode performs the following operations: + +```mermaid +sequenceDiagram + box rgb(225,245,255) Client A + participant Alice as Client A (Sender) + end + box rgb(255,225,245) Service + participant Clearnode + end + box rgb(225,255,225) Client B + participant Bob as Client B (Recipient) + end + + Note over Alice: 1. Send Transfer Request + Alice->>Clearnode: transfer({ destination, allocations }) + + Note over Clearnode: 2. Validate + Clearnode->>Clearnode: Verify authentication + Clearnode->>Clearnode: Check available balance + Clearnode->>Clearnode: Validate allocations + + Note over Clearnode: 3. Update Ledger + Clearnode->>Clearnode: Create debit entry (Alice -50 USDC) + Clearnode->>Clearnode: Create credit entry (Bob +50 USDC) + Clearnode->>Clearnode: Record transaction + + Note over Clearnode: 4. Send Responses & Notifications + Note over Bob: 5. Balance Updated + Bob->>Bob: Balance +50 USDC + + Clearnode->>Alice: tr (transfer) notification + Clearnode->>Alice: bu (balance update) notification + Clearnode->>Bob: tr (transfer) notification + Clearnode->>Bob: bu (balance update) notification + Clearnode->>Alice: response + +``` + +### Step-by-Step Process + +#### 1. Validates Request + +The clearnode performs comprehensive validation: +- Verifies authentication and signature +- Checks sender has sufficient available balance in unified account +- Validates allocations format and asset support + +#### 2. Updates Ledger (Double-Entry Bookkeeping) + +Every transfer creates two ledger entries - one for the sender and one for the recipient. The ledger uses double-entry bookkeeping principles where each entry has both `credit` and `debit` fields, with amounts always recorded as positive values. + +:::info Double-Entry Bookkeeping +The double-entry system ensures that the total of all debits always equals the total of all credits, providing mathematical proof of accounting accuracy. Every transfer is recorded twice - once as a debit to the sender's account and once as a credit to the recipient's account. +::: + +#### 3. Records Transaction + +A user-facing transaction record is created for each asset being transferred, containing information about the sender, recipient, asset, and amount. + +#### 4. Sends Notifications + +- **Both parties** receive `tr` (transfer) notification with transaction details +- **Both parties** receive `bu` (balance update) notification with updated balances + +#### 5. Response + +- **Sender** receives response with transaction details + +--- + +## Unified Balance Mechanics + +The unified balance aggregates funds from all chains. + +### Example: Multi-Chain Aggregation + +``` +User deposited: + $10 USDC on Ethereum + $5 USDC on Polygon + $3 USDC on Base + +Unified Balance: $18 USDC total + +User can transfer: Any amount up to $18 USDC +``` + +### Account Types + +The ledger system maintains three types of accounts: + +1. **Unified Account**: Main account identified by wallet address. This is where user funds are stored and can be transferred or withdrawn. + +2. **App Session Account**: Identified by app session ID. Participant wallets are beneficiaries of this account. Funds in app sessions are locked for the duration of the session. + +3. **Channel Escrow Account**: Temporary account that locks funds when user requests blockchain operations like resize. Funds remain in this account until the transaction is confirmed on-chain. + + + +{/* TODO: Document actual error codes from implementation. Currently removed as placeholder errors were inaccurate. */} + + +--- + +## Transaction History Query Methods + +Users can query their transfer history using two methods for different levels of detail. + +--- + +### get_ledger_transactions + +Retrieves user-facing transaction log with sender, recipient, amount, and type. This endpoint provides a view of transactions where the specified account appears as either the sender or receiver. + +:::info Public Endpoint +This is a public endpoint - authentication is not required. +::: + +#### Request + +| Parameter | Type | Required | Description | Format | Example | Notes | +|-----------|------|----------|-------------|--------|---------|-------| +| `account_id` | string | No | Filter by account ID (wallet, app session, or channel) | Hex string or ID | `"0x1234567890abcdef..."` | Returns transactions for this account | +| `asset` | string | No | Filter by asset symbol | Lowercase string | `"usdc"` | Returns transactions for this asset only | +| `tx_type` | string | No | Filter by transaction type | `transfer`, `deposit`, `withdrawal`, `app_deposit`, `app_withdrawal`, `escrow_lock`, `escrow_unlock` | `"transfer"` | Returns only this type of transaction | +| `offset` | number | No | Pagination offset | `0` | `42` | Defaults to `0` | +| `limit` | number | No | Number of transactions to return | `10` (max 100) | `10` | Defaults to 10 if omitted | +| `sort` | string | No | Sort order by created_at | `"asc"` or `"desc"` | `"desc"` | Default: `"desc"` | + +#### Response + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ledger_transactions` | LedgerTransaction[] | Array of transaction objects | + +**LedgerTransaction Structure**: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | number | Unique transaction reference | +| `tx_type` | string | Transaction type | +| `from_account` | string | Sender account identifier | +| `from_account_tag` | string | Sender's user tag (empty if none) | +| `to_account` | string | Recipient account identifier | +| `to_account_tag` | string | Recipient's user tag (empty if none) | +| `asset` | string | Asset symbol | +| `amount` | string | Transaction amount (decimal string) | +| `created_at` | string | ISO 8601 timestamp | + +--- + +### get_ledger_entries + +Retrieves detailed accounting entries showing all debits and credits. This endpoint provides double-entry bookkeeping records for detailed reconciliation and audit trails. + +:::info Public Endpoint +This is a public endpoint - authentication is not required. +::: + +#### Request + +| Parameter | Type | Required | Description | Format | Example | Notes | +|-----------|------|----------|-------------|--------|---------|-------| +| `account_id` | string | No | Filter by account ID (wallet/app session/channel) | Hex string or ID | `"0x1234567890abcdef..."` | Returns entries for this account | +| `wallet` | string | No | Filter by participant wallet | 0x-prefixed hex string (20 bytes) | `"0x1234567890abcdef..."` | Returns entries for this participant | +| `asset` | string | No | Filter by asset symbol | Lowercase string | `"usdc"` | Returns entries for this asset only | +| `offset` | number | No | Pagination offset | `0` | `42` | Defaults to `0` | +| `limit` | number | No | Number of entries to return | `10` (max 100) | `10` | Defaults to 10 if omitted | +| `sort` | string | No | Sort order by created_at | `"asc"` or `"desc"` | `"desc"` | Default: `"desc"` | + +#### Response + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ledger_entries` | LedgerEntry[] | Array of ledger entry objects | + +**LedgerEntry Structure**: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | number | Unique entry ID | +| `account_id` | string | Account identifier | +| `account_type` | number | Account type (`1000`=asset, `2000`=liability, etc.) | +| `asset` | string | Asset symbol | +| `participant` | string | Participant wallet address | +| `credit` | string | Credit amount (positive value or "0.0") | +| `debit` | string | Debit amount (positive value or "0.0") | +| `created_at` | string | ISO 8601 timestamp | + +--- + +## Implementation Notes + +**Performance**: +- Transfers are instant (< 1 second) and atomic +- No blockchain transaction required +- No blockchain fees + +**Features**: +- Unified balance is updated immediately +- Transfer can include multiple assets in one operation +- Transaction IDs can be used to track and query transfer status via `get_ledger_transactions` + +**Audit Trail**: +- Clearnode maintains complete audit trail of all transfers +- Double-entry bookkeeping ensures mathematical accuracy +- All records queryable via `get_ledger_*` methods + +--- + +## Next Steps + +Explore other off-chain operations: + +- **[App Sessions](./app-sessions)** - Create multi-party application channels +- **[Queries & Notifications](./queries)** - Check balances, transactions, and receive updates +- **[Channel Methods](./channel-methods)** - Manage payment channels + +For protocol fundamentals: +- **[Authentication](./authentication)** - Understand authorization and session management +- **[Message Format](./message-format)** - Learn request/response structure diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/_category_.json b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/_category_.json new file mode 100644 index 0000000..a75feed --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "On-Chain Contracts", + "position": 1, + "collapsible": false, + "collapsed": false +} diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/channel-lifecycle.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/channel-lifecycle.mdx new file mode 100644 index 0000000..e77838d --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/channel-lifecycle.mdx @@ -0,0 +1,425 @@ +--- +sidebar_position: 3 +title: "Channel Lifecycle" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Channel Lifecycle + +## State Transitions Overview + +The lifecycle of a channel moves through well-defined states depending on how participants interact with the custody contract. + +```mermaid +stateDiagram-v2 + [*] --> VOID + VOID --> INITIAL: create() + VOID --> ACTIVE: create() (sigs from all participants) + INITIAL --> ACTIVE: join() (all participants) + ACTIVE --> ACTIVE: resize() + ACTIVE --> ACTIVE: checkpoint() + ACTIVE --> DISPUTE: challenge() + ACTIVE --> FINAL: close() (cooperative) + DISPUTE --> ACTIVE: checkpoint() (newer state) + DISPUTE --> FINAL: challenge period expires + FINAL --> [*] + + note right of ACTIVE: Operational state
Off-chain updates occur here + note right of DISPUTE: Challenge period active
Parties can submit newer states +``` + +Use the sections below for details on each phase. + +## Creation Phase + +**Purpose**: Initiate a new channel with specified participants and initial funding. + +**Process**: + +1. The Creator: + - Constructs a Channel configuration with participants, adjudicator, challenge period, and nonce + - Prepares an initial State with application-specific app data + - Defines expected token deposits for all participants in `state.allocations` + - Signs the computed packedState of this initial state + - Includes Creator's signature in `state.sigs` at position 0 + - Calls either `create(...)` or `depositAndCreate(...)` function with the channel configuration and initial signed state + +:::tip Implicit Join (Immediate Activation) +If the Creator obtains the second participant's signature on the initial state **before** calling `create()`, they can supply both signatures in `state.sigs` (positions 0 and 1). When the contract detects `sigs.length == 2`: +- It verifies both signatures +- Locks funds from both participants +- Transitions directly to `ACTIVE` status (skipping `INITIAL`) +- Emits both `Joined` and `Opened` events + +This "implicit join" is the **recommended approach** for faster channel activation and reduced gas costs (single transaction instead of two). +::: + +2. The contract: + - Verifies the Creator's signature on the funding packedState + - Verifies Creator has sufficient balance to fund their allocation + - Locks the Creator's funds according to the allocation + - Sets the channel status to `INITIAL` + - Emits a `Created` event with channelId, channel configuration, and expected deposits + +```mermaid +sequenceDiagram + participant Creator + participant Contract + + Note over Contract: Status = VOID + Creator->>Creator: Construct Channel config + Creator->>Creator: Create initial State + Creator->>Creator: Sign packedState + Creator->>Contract: create(channel, state) + Contract->>Contract: Verify signature + Contract->>Contract: Lock Creator funds + Contract->>Contract: Set status to INITIAL + Note over Contract: Status = INITIAL + Contract->>Creator: Emit Created event +``` + +:::info Participant versus Caller address +The first participant address is usually different from the caller (EOA or contract), thus enabling channel operation delegation. This can be fruitful as users can fund channels for other ones. +::: + +## Joining Phase + +:::info Two Channel Opening Flows +There are two ways to open a channel: +1. **Modern/Recommended**: Provide ALL signatures in `create()` → channel immediately ACTIVE (see [Architecture](../../architecture#app-layer-architecture)) +2. **Legacy/Manual**: Provide only creator's signature in `create()` → status INITIAL → separate `join()` calls → ACTIVE + +This section documents flow #2. Most implementations use flow #1. +::: + +**Purpose**: Allow other participants to join and fund the channel (when using separate join flow). + +**Process**: + +1. Each non-Creator participant: + - Verifies the channelId and expected allocations + - Signs the same funding packedState + - Calls the `join` function with channelId, their participant index, and signature + +2. The contract: + - Verifies the participant's signature against the funding packedState + - Confirms the signer matches the expected participant at the given index + - Locks the participant's funds according to the allocation + - Tracks the actual deposit in the channel metadata + - Emits a `Joined` event with channelId and participant index + +3. When all participants have joined, the contract: + - Verifies that all expected deposits are fulfilled + - Sets the channel status to `ACTIVE` + - Emits an `Opened` event with channelId + +```mermaid +sequenceDiagram + participant P as Participant + participant C as Contract + participant S as System + + rect rgb(200, 220, 250) + Note right of C: Status INITIAL + P->>P: Sign funding packedState + P->>C: join(channelId, index, signature) + C->>C: Verify signature + C->>C: Lock participant funds + C->>P: Emit Joined event + end + + alt All participants joined + Note right of C: Status ACTIVE + C->>C: Set status to ACTIVE + C->>S: Emit Opened event + end + +``` + +:::success Channel Activation +The channel becomes operational only when ALL participants have successfully joined and funded their allocations. +::: + +## Active Phase + +**Purpose**: Enable off-chain state updates while channel is operational. + +### Off-Chain Updates + +Participants: +- Exchange and sign state updates off-chain via the Nitro RPC protocol +- Maintain a record of the latest valid state +- Use application-specific data in the `state.data` field + +Each new state: +- May update allocations when assets are transferred (though allocations can remain unchanged between states, e.g., game moves without fund transfers) +- MUST be signed by the necessary participants according to adjudicator rules +- MUST comply with the validation rules of the channel's adjudicator + +The on-chain contract remains unchanged during the active phase unless participants choose to checkpoint a state. + +:::tip Off-Chain Efficiency +During the active phase, state updates occur entirely off-chain with zero gas costs and sub-second latency. +::: + +## Checkpointing + +**Purpose**: Record a state on-chain without entering dispute mode. + +**Process**: + +1. Any participant: + - Calls the `checkpoint` function with a valid state and required proofs + +2. The contract: + - Verifies the submitted state via the adjudicator + - If valid and more recent than the previously checkpointed state, stores it + - Emits a `Checkpointed` event with channelId + +```mermaid +graph LR + A[Active Channel
Status: ACTIVE] -->|checkpoint| B[Verify State] + B -->|Valid| C[Store State] + C --> D[Emit Event] + D --> E[Remain Active
Status: ACTIVE] + + style A fill:#90EE90 + style E fill:#90EE90 +``` + +:::note Optional Operation +Checkpointing is optional but recommended for long-lived channels or after significant value transfers. +::: + +## Closure - Cooperative + +**Purpose**: Close channel to distribute locked funds, after all participants have agreed on the final state. + +**Process**: + +1. Any participant: + - Prepare a final State with `intent` equal to `FINALIZE`. + - Collects signatures from all participants on this final state + - Calls the `close` function with channelId, final state, and any required proofs + +2. The contract: + - Verifies all participant signatures on the closing packedState + - Verifies the state has `intent` equal to `FINALIZE`. + - Distributes funds according to the final state's allocations + - Sets the channel status to `FINAL` + - Deletes the channel metadata + - Emits a `Closed` event + +```mermaid +sequenceDiagram + participant User + participant Contract + + Note over Contract: Status = ACTIVE + User->>User: Create final State (intent=FINALIZE) + User->>User: Collect all signatures + User->>Contract: close(channelId, state, proofs) + Contract->>Contract: Verify all signatures + Contract->>Contract: Verify intent = FINALIZE + Contract->>Contract: Distribute funds + Contract->>Contract: Set status to FINAL + Note over Contract: Status = FINAL + Contract->>Contract: Delete metadata + Contract->>User: Emit Closed event +``` + +:::success Preferred Method +**This is the preferred closure method as it is fast and gas-efficient.** It requires only one transaction and completes immediately without a challenge period. +::: + +## Closure - Challenge-Response + +**Purpose**: Handle closure when participants disagree or one party is unresponsive. + +### Challenge Process + +1. To initiate a challenge, a participant: + - Calls the `challenge` function with their latest valid state and required proofs + +:::note Latest State Location +The participant's latest state may only exist off-chain and not be known on-chain yet. The challenge process brings this off-chain state on-chain for validation. +::: + +2. The contract: + - Verifies the submitted state via the adjudicator + - If valid, stores the state and starts the challenge period + - Sets a challenge expiration timestamp (current time + challenge duration) + - Sets the channel status to `DISPUTE` + - Emits a `Challenged` event with channelId and expiration time + +```mermaid +sequenceDiagram + participant User + participant Contract + participant Timer + + Note over Contract: Status = ACTIVE + User->>Contract: challenge(channelId, state, proofs) + Contract->>Contract: Verify state + Contract->>Contract: Store state + Contract->>Contract: Set status to DISPUTE + Note over Contract: Status = DISPUTE + Contract->>Timer: Start challenge period + Contract->>User: Emit Challenged event +``` + +### Resolving Challenge with Checkpoint + +During the challenge period, any participant: +- Submits a more recent valid state by calling `checkpoint()` +- If the new state is valid and more recent (as determined by the adjudicator or IComparable interface), the contract updates the stored state, resets the challenge period, and returns the channel to `ACTIVE` status + +### Challenge Period Elapse + +After the challenge period expires, any participant: +- Call `close` with an empty candidate and proof to distribute funds according to the last valid challenged state + +The contract: +- Verifies the challenge period has elapsed +- Distributes funds according to the challenged state's allocations +- Sets channel status to `FINAL` +- Deletes the channel metadata +- Emits a `Closed` event + +:::warning Key Principle +The challenge mechanism gives parties time to prove they have a newer state. If no one responds with a newer state, the challenged state is assumed correct. +::: + +**Complete Challenge-Response Flow**: + +```mermaid +stateDiagram-v2 + [*] --> Active + Active --> Dispute: challenge() + Dispute --> Active: checkpoint() with newer state + Dispute --> Final: close() after timeout + Final --> [*] + + note right of Dispute: Challenge period active
Parties can submit
newer states +``` + +## Resize Protocol {#resize-protocol} + +**Purpose**: Adjust funds locked in the channel by locking or unlocking funds **without closing the channel**. + +**Process**: + +1. Any participant: + - Calls the `resize` function with: + - The channelId (remains unchanged) + - A candidate State with: + - `intent` = `StateIntent.RESIZE` + - `version` = precedingState.version + 1 + - `data` = ABI-encoded `int256[]` containing delta amounts (positive for deposit, negative for withdrawal) respectively for participants + - `allocations` = Allocation[] after resize (absolute amounts) + - Signatures from **ALL participants** (consensus required) + - An array of proof states containing the previous state (`version-1`) first and its proof later in the array + +:::note Deposit Requirement +The participant depositing must have at least the corresponding amount in their Custody ledger account (available balance) to lock additional funds to the channel. +::: + +2. The contract: + - Verifies the channel is in ACTIVE status + - Verifies all participants have signed the resize state + - Decodes delta amounts from `candidate.data` + - Validates adjudicator approves the preceding state + - For positive deltas: Locks additional funds from custody account + - For negative deltas: Unlocks funds back to custody account + - Updates expected deposits to match new allocations + - Emits `Resized(channelId, deltaAllocations)` event + +3. The channel: + - **channelId remains UNCHANGED** (same channel persists) + - Status remains **ACTIVE** throughout + - Version increments by 1 + - No new channel is created + +```mermaid +sequenceDiagram + participant User + participant Contract + + Note over Contract: Status = ACTIVE + User->>Contract: resize(channelId, resizeState, proofs) + Note right of Contract: Same channelId
State version + 1
Intent = RESIZE + Contract->>Contract: Verify signatures (all participants) + Contract->>Contract: Decode delta amounts from state.data + Contract->>Contract: Lock funds (positive deltas) + Contract->>Contract: Unlock funds (negative deltas) + Contract->>Contract: Update expected deposits + Note over Contract: Status = ACTIVE + Contract->>User: Resized(channelId, deltas) + Note right of User: Same channelId
Channel still ACTIVE +``` + +**Use Cases**: +- Increasing funds locked in the channel (positive delta: adding funds) +- Decreasing funds locked in the channel (negative delta: removing funds) +- Adjusting fund distribution while maintaining channel continuity + +:::tip In-Place Update +The resize operation updates the channel **in place**. The channelId stays the same, and the channel remains ACTIVE throughout. This differs from closing and reopening, which would create a new channel. +::: + +:::note Implicit Transfer with Resize +It is possible to combine a transfer (change of allocations among participants) with a resize operation. For example: +- Previous state allocations: `[5, 10]` +- Desired transfer: 2 tokens from second to first participant → `[7, 8]` +- Additional changes: first participant withdraws all 7, second participant deposits 6 +- Delta amounts: `[-7, 6]` +- Resize state allocations: `[0, 14]` + +**Rule**: `sum(allocations_resize_state) = sum(allocations_prev_state) + sum(delta_amounts)` +For this example: `14 = 15 + (-1)` ✓ +::: + +## State Transition Summary + +The complete channel lifecycle state machine: + +```mermaid +stateDiagram-v2 + [*] --> VOID: Initial + VOID --> INITIAL: create() + VOID --> ACTIVE: create() with all sigs + INITIAL --> ACTIVE: join() all + ACTIVE --> ACTIVE: checkpoint() + ACTIVE --> ACTIVE: resize() + ACTIVE --> DISPUTE: challenge() + ACTIVE --> FINAL: close() cooperative + DISPUTE --> ACTIVE: checkpoint() newer + DISPUTE --> FINAL: close() after timeout + FINAL --> [*]: Deleted + + note right of VOID: Channel does not exist + note right of INITIAL: Awaiting participants + note right of ACTIVE: Operational
Off-chain updates + note right of DISPUTE: Challenge active
Response period + note right of FINAL: Funds distributed
Ready for deletion +``` + +**Valid Transitions**: + +| From | To | Trigger | Requirements | +|------|----|---------|--------------| +| VOID | INITIAL | `create()` | Creator signature, sufficient balance, INITIALIZE intent || VOID | ACTIVE | `create()` with all sigs | All participants' signatures, sufficient balances | +| INITIAL | ACTIVE | `join()` | All participants joined and funded | +| ACTIVE | ACTIVE | `checkpoint()` | Valid newer state | +| ACTIVE | ACTIVE | `resize()` | All signatures, valid deltas, sufficient balance | +| ACTIVE | DISPUTE | `challenge()` | Valid state newer than latest known on-chain | +| ACTIVE | FINAL | `close()` | All signatures, FINALIZE intent | +| DISPUTE | ACTIVE | `checkpoint()` | Valid newer state | +| DISPUTE | FINAL | `close()` | Challenge period expired | +| FINAL | VOID | Automatic | Metadata deleted | + +:::note Channel Deletion +When a channel reaches FINAL status, the channel metadata is deleted from the chain and funds are distributed according to the final state allocations. +::: diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/data-structures.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/data-structures.mdx new file mode 100644 index 0000000..f236dfe --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/data-structures.mdx @@ -0,0 +1,212 @@ +--- +sidebar_position: 2 +title: "Data Structures" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Data Structures + +## Channel + +Represents the configuration of a state channel. + +```solidity +struct Channel { + address[] participants; // List of participants in the channel + address adjudicator; // Contract that validates state transitions + uint64 challenge; // Duration in seconds for dispute resolution + uint64 nonce; // Unique identifier for the channel +} +``` + +**Fields**: + +- `participants`: An ordered array of participant addresses. Index 0 is typically the Creator, index 1 is the clearnode. +- `adjudicator`: Address of the adjudicator contract responsible for validating state transitions. +- `challenge`: Challenge period duration in seconds. Determines a time window when a challenge can be resolved by a counterparty. Otherwise, a channel is considered closed and funds can be withdrawn. +- `nonce`: A unique number that, combined with other fields, creates a unique channel identifier. + +:::info Participant versus Caller Address +The first participant address is usually different from the caller (EOA or contract), thus enabling channel operation delegation. This can be fruitful as users can fund channels for other ones. +::: + +## State + +Represents a snapshot of channel state at a point in time. + +```solidity +struct State { + StateIntent intent; // Intent of the state (INITIALIZE, OPERATE, RESIZE, FINALIZE) + uint256 version; // State version incremental number to compare most recent + bytes data; // Application-specific data + Allocation[] allocations; // Asset allocation for each participant + bytes[] sigs; // Participant signatures authorizing the packed state payload +} +``` + +**Fields**: + +- `intent`: The intent of this state, indicating its purpose (see StateIntent enum). +- `version`: Incremental version number used to compare and validate state freshness. Higher versions supersede lower versions. +- `data`: Application-specific data which adjudicators can operate on. For a `resize(...)` state must contain `allocationDeltas`. For more information, please check the [resize operation docs](./channel-lifecycle#resize-protocol). +- `allocations`: Array of allocations defining how funds are distributed. +- `sigs`: Array of participant signatures over the canonical packed state payload. Order corresponds to the Channel's participants array. + +## Allocation + +Specifies how a particular amount of a token should be allocated. + +```solidity +struct Allocation { + address destination; // Recipient of funds + address token; // ERC-20 token address + uint256 amount; // Token amount in smallest unit +} +``` + +**Fields**: + +- `destination`: Address that will receive the funds when channel closes. +- `token`: Contract address of the ERC-20 token (or zero address for native currency). +- `amount`: Amount in the token's smallest unit (wei for ETH, considering decimals for ERC-20). + +## Signatures + +Signatures in VirtualApp are stored as raw `bytes` so the protocol can validate multiple scheme formats. + +```solidity +struct Signature { + uint8 v; // Recovery identifier + bytes32 r; // First 32 bytes of signature + bytes32 s; // Second 32 bytes of signature +} +``` + +At a minimum VirtualApp currently recognizes the following signature families (see the [Signature Formats](./signature-formats) reference for the full specification): + +- **Raw/Pre-EIP-191 ECDSA** – Signs `keccak256(packedState)` without any prefix. +- **EIP-191 (version `0x45`)** – Signs a structured message that prefixes the packed state with the Ethereum signed message header and length. +- **EIP-712 Typed Data** – Signs `keccak256(abi.encode(domainSeparator, hashStruct(state)))`. +- **EIP-1271 Smart-Contract Signatures** – Arbitrary bytes validated via `isValidSignature` on the signer contract. +- **EIP-6492 Counterfactual Signatures** – Wraps deployment data to prove a not-yet-deployed ERC-4337 wallet authorized the state. + +Refer to the dedicated page for verification order, payload layouts, and implementation guidance. + +## Amount + +Represents a quantity of a specific token. + +```solidity +struct Amount { + address token; // ERC-20 token address + uint256 amount; // Token amount +} +``` + +## Channel Status + +Enum representing the lifecycle stage of a channel. + +```solidity +enum Status { + VOID, // Channel does not exist + INITIAL, // Creation in progress, awaiting all participants + ACTIVE, // Fully funded and operational + DISPUTE, // Challenge period active + FINAL // Ready to be closed and deleted +} +``` + +## Protocol Constants + +### Participant Indices + +```solidity +constant uint256 CLIENT_IDX = 0; // Client/Creator participant index +constant uint256 SERVER_IDX = 1; // Server/Clearnode participant index +constant uint256 PART_NUM = 2; // Number of participants (always 2) +``` + +### Challenge Period + +```solidity +uint256 public constant MIN_CHALLENGE_PERIOD = 1 hours; +``` + +The minimum challenge period enforced by the Custody Contract. Channel configurations must specify a challenge period of at least 1 hour. + +### EIP-712 Type Hashes + +The protocol uses EIP-712 structured data signing with the following domain parameters: + +```solidity +// EIP-712 Domain +name: "VirtualApp:Custody" +version: "0.3.0" +``` + +Type hashes for state validation: + +```solidity +// State hash computation for signatures +bytes32 constant STATE_TYPEHASH = keccak256( + "AllowStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)" +); + +// Challenge state hash computation +bytes32 public constant CHALLENGE_STATE_TYPEHASH = keccak256( + "AllowChallengeStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)" +); +``` + +These type hashes enable human-readable signature prompts in wallets and improve security by preventing signature replay attacks across different contexts. + +## Identifier Computation + +### Channel Identifier + +The channelId MUST be computed as: + +```javascript +channelId = keccak256( + abi.encode( + channel.participants, + channel.adjudicator, + channel.challenge, + channel.nonce, + chainId + ) +) +``` + +This creates a deterministic, unique identifier for each channel. + +:::info App Session Identifiers +App sessions use a different computation: `keccak256(JSON.stringify(definition))` where definition includes the app configuration but **not** `chainId`, since sessions are entirely off-chain. See [Off-chain › App Sessions › Session Identifier](../off-chain/app-sessions#session-identifier) for details. +::: + +:::note Deterministic IDs +Channel IDs are deterministically computed from the channel configuration, ensuring the same configuration always produces the same identifier. +::: + +### Packed State + +The legacy state hash concept was removed in v0.3.0 when non-ECDSA signatures were introduced. Instead, participants use the **packed state** payload for signing: + +```javascript +packedState = abi.encode( + channelId, + state.intent, + state.version, + state.data, + state.allocations +) +``` + +The packed state is simply `abi.encode(channelId, state.intent, state.version, state.data, state.allocations)`. This byte array is fed into the selected signing scheme (EIP-712 hashing, ERC-1271 contract checks, NO_EIP712 fallback, etc.). Each scheme may wrap or hash `packedState` as needed, but the canonical payload MUST be the input. + +:::warning Signature Verification +All state updates MUST be verified by checking signatures against the canonical `packedState` payload (after the signing method applies its required hashing/wrapping) before accepting them on-chain. +::: diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/overview.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/overview.mdx new file mode 100644 index 0000000..f1dd387 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/overview.mdx @@ -0,0 +1,50 @@ +--- +sidebar_position: 1 +title: "Overview" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# On-Chain Protocol Overview + +The on-chain protocol defines the smart contract interfaces and data structures that form the foundation of VirtualApp's security guarantees. This layer operates on the blockchain and handles: + +- **Fund Custody**: Secure locking and unlocking of participant assets +- **Dispute Resolution**: Challenge-response mechanism for disagreements +- **Final Settlement**: Distribution of funds according to validated states +- **Channel Lifecycle**: State transitions from creation to closure + +## Key Responsibilities + +The on-chain layer MUST provide: + +1. **Deterministic channel identifiers** computed from channel configuration +2. **Signature verification** to authenticate state updates +3. **State validation** through adjudicator contracts +4. **Challenge periods** to ensure fair dispute resolution +5. **Fund safety** guaranteeing users can always recover their assets + +:::info EVM Compatibility +The initial version of VirtualApp is designed for EVM-compatible blockchains including Ethereum, Polygon, Base, and other EVM chains. Support for additional networks is continuously expanding. +::: + +## Contract Interfaces + +The protocol defines three primary contract interfaces: + +- **IChannel**: Core channel lifecycle operations (create, join, challenge, close) +- **IDeposit**: Token deposit and withdrawal management +- **IChannelReader**: Read-only queries for channel state and status + +These interfaces are implemented by the **Custody Contract**, which serves as the main entry point for on-chain operations. + + +## Next Steps + +The following sections detail: + +- [Data Structures](./data-structures): Core types and identifier computation +- [Channel Lifecycle](./channel-lifecycle): Complete state machine and operations +- [Security Considerations](./security): Threat model and best practices + diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/security.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/security.mdx new file mode 100644 index 0000000..1298fc2 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/security.mdx @@ -0,0 +1,331 @@ +--- +sidebar_position: 4 +title: "Security Considerations" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Security Considerations + +## Current Limitations + +The current Custody contract implementation has the following limitations: + +- **Two-participant channels only**: Channels support exactly 2 participants +- **Participant role constraint**: First participant must always be a client, while second must be a Clearnode +- **Single allocation per participant**: Each participant can have only 1 allocation +- **Same-token allocations**: Both allocations must be for the same token +- **Minimum challenge duration**: Challenge duration is set to be no less than 1 hour +- **No re-challenge**: It is not possible to challenge an already challenged channel +- **No direct EOA resize**: It is not possible to resize directly from or to your EOA; you must deposit to or withdraw funds from the Custody contract first +- **Channel required for withdrawal**: It is not possible to withdraw your funds from the Unified Balance on a chain with no open channel without opening a channel first. In a future major release, we plan to merge these steps in one operation +- **Separate resize and balance operations**: It is not possible to top-up a Unified Balance from or withdraw to your EOA balance in the same `resize(...)` operation. You must deposit your funds prior to or withdraw after the `resize(...)` operation. In a future major release, we plan to merge these steps in one operation + +:::note Future Improvements +Many of these limitations are implementation-specific and are planned to be addressed in future major releases. They do not represent fundamental protocol constraints. +::: + +## Threat Model + +### Assumptions + +The protocol operates under the following security assumptions: + +- **At least one honest party per channel** willing to enforce their rights +- **Blockchain is secure and censorship-resistant** within reasonable bounds +- **Cryptographic primitives are secure** (ECDSA, keccak256) +- **Participants have access to the blockchain** to submit challenges within the challenge period + +:::info Trust Model +VirtualApp is designed as a **trustless protocol** - no single party can steal funds or prevent others from recovering their legitimate share. +::: + +### Protected Against + +The protocol provides protection against: + +- **Replay attacks** via version number checking in Custody contract +- **State withholding** via challenge mechanism +- **Unauthorized state transitions** via signature verification +- **Funds theft** - all transitions require valid signatures from appropriate parties + +### Not Protected Against + +The protocol cannot protect against: + +- **All participants colluding** to violate application rules +- **Blockchain-level attacks** (51% attacks, MEV exploitation, etc.) +- **Denial of service by blockchain congestion** - may affect ability to respond to challenges + +:::warning Blockchain Dependency +The security of VirtualApp channels depends on the underlying blockchain's liveness and security. Extended blockchain downtime during a challenge period could prevent parties from responding. +::: + +## Security Properties + +### Funds Safety + +**Property**: Participants can always recover their funds according to the latest valid signed state, even if other participants become unresponsive. + +**Mechanism**: The challenge-response system ensures that: +1. Any party can initiate closure unilaterally +2. Challenge period allows time for others to respond with newer states +3. Newest valid state always wins +4. Funds are distributed according to the final accepted state + +```mermaid +graph TB + A["User has latest
signed state"] --> B{"Other participant responsive?"} + B -->|Yes| C["Cooperative close
Fast & cheap"] + B -->|No| D["Challenge with
latest state"] + D --> E["Wait challenge
period"] + E --> F{"Communication continuation suggested?"} + F -->|No| G["Close & recover funds"] + F -->|Yes| H["Create and submit a
newer state via checkpoint(...)"] + H --> E + + style C fill:#90EE90 + style G fill:#90EE90 + +``` + +### State Validity + +**Property**: Only states signed by the required participants (as determined by the adjudicator) can be accepted. + +**Mechanism**: +- Every state update requires cryptographic signatures +- Signatures are verified against the packedState +- Adjudicator validates state transitions according to application rules +- Invalid states are rejected on-chain + +:::success Cryptographic Security +State validity is enforced through [supported signatures](./signature-formats), all of which are supported by Ethereum itself. +::: + +#### EIP-712 Signature Support + +VirtualApp supports **EIP-712 (Typed Structured Data)** signatures in addition to raw ECDSA and EIP-191. This provides significant security and user experience advantages: + +**Security Benefits**: +- **Domain Separation**: Signatures are bound to a specific contract and chain, preventing replay attacks across different applications or networks +- **Type Safety**: Structured data hashing ensures only valid state structures can be signed, preventing malformed data injection +- **Semantic Clarity**: Each field's type and purpose is cryptographically enforced, reducing ambiguity attacks + +**User Experience Benefits**: +- **Human-Readable**: Modern wallets (MetaMask, Ledger, etc.) display EIP-712 signatures as structured fields instead of opaque hex strings +- **Transparency**: Users see exactly what `channelId`, `intent`, `version`, `allocations`, and `data` they're signing +- **Trust**: Clear presentation reduces phishing risks and increases user confidence + +**Example Wallet Display**: +``` +Sign Typed Data: + channelId: 0xabcd1234... + intent: OPERATE (1) + version: 5 + allocations: + [0] destination: 0x742d35Cc..., token: USDC, amount: 100.00 + [1] destination: 0x123456Cc..., token: USDC, amount: 0.00 +``` + +Compared to EIP-191 which would show: +``` +Sign Message: +0x1ec5000000000000000000000000000000000000000000000000000000001234abcd... +[500+ more hex characters] +``` + +**Implementation Note**: The protocol accepts **all three formats** (raw ECDSA, EIP-191, EIP-712) for maximum compatibility, but EIP-712 is **strongly recommended** for production applications due to its superior security and UX properties. + +Supporting EIP-712 signatures also differentiates VirtualApp by keeping state channel operations wallet-friendly and lowering integration friction compared to protocols limited to raw message signing. + +### Liveness + +**Property**: As long as the blockchain is live and accepts transactions within the challenge period, honest participants can enforce their rights. + +**Requirements**: +- Blockchain must be operational +- Participant must be able to submit transactions +- Challenge period must be sufficient for transaction confirmation + +**Recommended Challenge Periods**: +- **High-value channels**: 24-48 hours (default: 24 hours / 86400 seconds) +- **Medium-value channels**: 12-24 hours +- **Low-value rapid channels**: 6-12 hours + +:::caution Challenge Period Trade-offs +Longer challenge periods provide more security but slower dispute resolution. Shorter periods enable faster closure but require more vigilant monitoring. +::: + +### Censorship Resistance + +**Property**: Since anyone can submit challenges and responses, censorship of a single participant does not prevent channel closure. + +**Mechanism**: +- Any participant can initiate challenge +- Any participant can respond to challenge +- Multiple participants can attempt the same operation +- As long as one honest party can transact, the channel can be resolved + +## Attack Vectors and Mitigations + +### Replay Attacks + +**Attack**: Resubmitting old signed states to revert channel to a previous favorable allocation. + +**Mitigation**: +- Adjudicators MUST implement version checking to verify that a supplied "candidate" is indeed supported by a supplied "proof". +- Higher version numbers supersede lower versions +- On-chain contract tracks the highest version seen +- Old states are automatically rejected + +```mermaid +graph LR + A[State v10 submitted] --> B{Compare versions} + B -->|v10 > v5| C[Accept new state] + B -->|v10 < v20| D[Reject old state] + + style C fill:#90EE90 + style D fill:#FFB6C1 +``` + +:::tip Version Monotonicity +Always ensure state versions increase monotonically. Never sign two different states with the same version number. +::: + +### State Withholding + +**Attack**: Refusing to cooperate in closing channel, holding funds hostage. + +**Mitigation**: +- Challenge mechanism allows unilateral closure +- Challenge period ensures fair dispute resolution +- Latest signed state always prevails + +**Example Scenario**: +``` +1. Alice and Bob have channel with $1000 each +2. After trading, valid state shows Alice: $1500, Bob: $500 +3. Bob refuses to cooperate in cooperative close +4. Alice initiates challenge with latest signed state +5. Bob has access only to an older state, meaning he is unable to resolve the challenge +6. After challenge period elapses, Alice's state becomes the final one +7. Alice recovers her $1500 +``` + +### Challenge Griefing + +**Attack**: Repeatedly challenging with old states to delay closure and grief the counterparty. + +**Mitigation**: +- Each valid newer state resets the challenge period +- Attacker must pay gas for each challenge attempt +- Eventually attacker runs out of old states +- Newest state always wins regardless of challenge count +- The party being griefed can checkpoint with the latest valid state, impeding the griefer from challenging with any intermediate state + +:::note Economic Disincentive +Challenge griefing is economically costly for the attacker (gas fees) while only causing time delay, not fund loss, for the victim. +::: + +### Front-Running + +**Attack**: Observing pending challenge transaction and front-running with a newer state. + +**Mitigation**: +- **This is actually desired behavior** in VirtualApp +- The newest state should always win +- Front-running helps ensure the most recent state is used +- Both parties benefit from accurate state resolution + +## Best Practices + +### For Users + +**Essential Practices**: + +1. **Never sign duplicate versions**: Never sign two different states with the same version number +2. **Keep records**: Maintain a record of the latest state you've signed +3. **Monitor events**: Watch the blockchain for channel events (Challenged, Closed) +4. **Respond promptly**: React to challenges within the challenge period +5. **Verify adjudicators**: Only use adjudicator contracts from trusted sources + +:::danger Critical Rule +**NEVER sign two different states with the same version number.** This creates ambiguity about the true latest state and can lead to disputes. +::: + +### For Implementers + +**Implementation Requirements**: + +1. **Validate thoroughly**: Check all inputs before submitting transactions +2. **Use adjudicators wisely**: Leverage adjudicators to enforce application rules +3. **Set appropriate challenge periods**: Balance security needs with user experience +4. **Implement proper key management**: Secure storage for participant private keys +5. **Log state transitions**: Maintain audit trail of all state updates + +**Sample Validation Checklist**: + +```markdown +Before submitting state on-chain: +☐ Verify all required signatures present +☐ Verify signatures are valid for expected participants +☐ Verify state version is sequential +☐ Verify allocations sum correctly +☐ Verify magic numbers (CHANOPEN/CHANCLOSE) if applicable +☐ Verify channelId matches expected value +☐ Test with small amounts first +``` + +### For Adjudicator Developers + +**Critical Requirements**: + +1. **Implement strict version comparison**: Ensure newer states always supersede older ones +2. **Validate state transitions**: Enforce application-specific rules correctly +3. **Optimize for gas efficiency**: Validation happens on-chain during disputes +4. **Consider edge cases**: Handle all possible state transition scenarios +5. **Audit thoroughly**: Security review before deployment is essential + +:::warning Adjudicator Responsibility +Adjudicators are critical to channel security. A flawed adjudicator can undermine the entire channel's safety guarantees. +::: + +:::caution Before Implementing Your Own Adjudicator +The Adjudicator is an incredibly important part of the VirtualApp protocol. Yellow Network is built on top of a specific adjudicator, which if changed, will render interoperability and security guarantees impossible. Before starting to implement your own Adjudicator, please be sure to advise the VirtualApp developer team, so that your work is not left out. +::: + +## Security Guarantees Summary + +| Property | Guarantee | Mechanism | +|----------|-----------|-----------| +| **Funds Safety** | Cannot lose funds with valid signed state | Challenge-response + signatures | +| **State Validity** | Only properly signed states accepted | Signature verification | +| **Liveness** | Can always close if blockchain is live | Unilateral challenge mechanism | +| **Censorship Resistance** | Any party can enforce closure | Multiple submission paths | +| **No Replay** | Old states cannot be reused | Version number validation | + +:::success Strong Security Model +VirtualApp provides **strong security guarantees** built on top of Layer 1 blockchain security, while enabling Layer 2 scalability and efficiency. +::: + +## Emergency Procedures + +### If a Clearnode Becomes Unresponsive + +1. **Retrieve latest signed state** from local storage +2. **Initiate challenge** on-chain with latest state +3. **Close the channel** after challenge period expires +4. **Funds are recovered** according to latest valid state + +### If You Have Been Challenged + +1. **Check for the latest state** - make sure the channel was challenged with the latest state. If not, you should checkpoint it with one to avoid funds loss +2. **Ensure blockchain access** - check network connectivity +3. **Use appropriate gas prices** - ensure timely confirmation +4. **Have backup RPC endpoints** - don't rely on single provider + +:::tip Monitoring Best Practice +Set up automated monitoring with alerts for channel events. This ensures you can respond quickly to challenges even if you're not actively watching. +::: diff --git a/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/signature-formats.mdx b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/signature-formats.mdx new file mode 100644 index 0000000..69c6a10 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/app-layer/on-chain/signature-formats.mdx @@ -0,0 +1,61 @@ +--- +sidebar_position: 3 +title: "Signature Formats" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Signature Formats + +VirtualApp treats each signature inside `State.sigs` as an opaque `bytes` value. At verification time the Custody contract inspects that payload to detect which validation flow to run. This page captures the current formats the protocol accepts and how they are evaluated. + +## Supported Formats + +### Raw / Pre-EIP-191 ECDSA + +- Signs the raw packedState with no prefix. +- Produces the canonical `(v, r, s)` tuple encoded as 65 bytes. +- Recommended for chain-agnostic clients or when hardware-wallet compatibility is required. + +### EIP-191 (`0x45`) Ethereum Signed Message + +- Payload: `keccak256(\"\\x19Ethereum Signed Message:\\n\" + len(packedState) + packedState)`. +- Matches the UX most wallets expose when calling `eth_sign`. +- VirtualApp stores the resulting `(v, r, s)` so adjudicators can re-create the prefixed hash for verification. + +### EIP-712 Typed Data + +- Payload: `keccak256(\"\\x19\\x01\" ++ domainSeparator ++ hashStruct(state))`. +- Domain separator includes chain ID, verifying contract, and an application-specific salt to prevent replay. +- Provides the strongest replay protection when both parties agree on the domain definition. + +### EIP-1271 Smart-Contract Signatures + +- Supports smart contract wallets (multi-sigs, modules, account abstraction). +- The `bytes` payload is passed to the signer's `isValidSignature(hash, bytes signature)` function. +- Implementations can encode arbitrary metadata (e.g., batched approvals, guardians). + +### EIP-6492 Counterfactual Signatures + +- Wraps an EIP-1271 signature with deployment bytecode and a detection suffix `0x6492649264926492649264926492649264926492649264926492649264926492`. +- Allows a not-yet-deployed ERC-4337 smart wallet to attest to a state. +- During verification VirtualApp simulates or deploys the wallet, then forwards the inner signature to the regular EIP-1271 flow. + +## Verification Order + +The Custody contract attempts the following strategies in order: + +1. **EIP-6492** – If the detection suffix is present, unwrap and validate as counterfactual. +2. **EIP-1271** – If the signer currently has contract code, call `isValidSignature`. +3. **ECDSA / EIP-191 / EIP-712** – Otherwise treat it as an externally owned account signature and recover the signer using the appropriate hash for the advertised format. + +Implementations should persist metadata about which scheme was used so that adjudicators and monitoring services can reproduce the expected hash locally. + +## Implementation Notes + +- `bytes[] sigs` preserves the ordering of channel participants, but each entry may come from a different signature family. +- Wallets should expose the format they used when signing to aid debugging. +- Future versions may extend this list; storing opaque bytes ensures backward compatibility. + + diff --git a/versioned_docs/version-0.5.x/protocol/architecture.mdx b/versioned_docs/version-0.5.x/protocol/architecture.mdx new file mode 100644 index 0000000..8252fe0 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/architecture.mdx @@ -0,0 +1,233 @@ +--- +sidebar_position: 3 +title: "Architecture" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Architecture + +## System Overview + +Yellow Protocol consists of two distinct layers — the **Decentralized Layer** for network consensus and settlement, and the **App Layer (VirtualApp)** for state channel operations. Together they enable scalable, secure cross-chain clearing and application hosting. + +### Two-Layer Architecture + +```mermaid +%%{init: { "flowchart": { "htmlLabels": true } }}%% +graph TB + subgraph DL["Decentralized Layer"] + direction LR + DHT["Kademlia DHT"] + BLS["BLS Clusters"] + SECURITY["Elastic Security"] + end + + subgraph AL["App Layer — VirtualApp"] + direction LR + RPC["Nitro RPC"] + CHANNELS["State Channels"] + SESSIONS["YApps / App Sessions"] + end + + subgraph OnChain["On-Chain Contracts"] + direction LR + DL_CONTRACTS["Registry, Custody, Governor"] + AL_CONTRACTS["Custody, Adjudicator"] + end + + subgraph Blockchain["Blockchains"] + CHAIN["Ethereum, Arbitrum, Base, Polygon, Linea, BNB"] + end + + DL --> AL + DL_CONTRACTS --> Blockchain + AL_CONTRACTS --> Blockchain + + style DL fill:#e1f5ff,stroke:#9ad7ff,color:#111 + style AL fill:#fff4e1,stroke:#ffd497,color:#111 + style OnChain fill:#ffe1f5,stroke:#ffbde6,color:#111 + style Blockchain fill:#f0f0f0,stroke:#c9c9c9,color:#111 +``` + +:::info Contract Convergence +Both layers currently deploy their own on-chain contracts. These are expected to be merged into a unified contract suite once testnet concludes. +::: + +The Decentralized Layer architecture (DHT topology, cluster formation, elastic security) is documented in the v1.x release. + +## App Layer Architecture + +The VirtualApp App Layer architecture consists of multiple components working together to enable state channel operations: + +```mermaid +%%{init: { "flowchart": { "htmlLabels": true } }}%% +graph TB + + %% Layered architecture with legible fills for light/dark modes + + subgraph Application["Application Layer"] + direction TB + APP["Chess, DEX, Gaming, Payments, Custom Logic"] + end + + subgraph OffChain["Off-Chain Layer
(Fast Updates)"] + direction LR + BROKER["Clearnode"] + end + + subgraph ClientLayer["Client SDK"] + direction TB + CLIENT["Client SDK"] + end + + subgraph OnChain["On-Chain Layer
(Smart Contracts)"] + direction TB + CONTRACTS["Custody, Adjudicator contracts"] + end + + subgraph Blockchain["Blockchain Layer"] + direction TB + CHAIN["Ethereum, Polygon, etc."] + end + + APP --> CLIENT + CLIENT <-->|"Communicate via RPC using NitroRPC protocol"| BROKER + CLIENT -. "Operate on-chain state channels" .-> CONTRACTS + BROKER -. "Observe events" .-> CONTRACTS + CONTRACTS --> CHAIN + + style Application fill:#e1f5ff,stroke:#9ad7ff,color:#111 + style OffChain fill:#fff4e1,stroke:#ffd497,color:#111 + style OnChain fill:#ffe1f5,stroke:#ffbde6,color:#111 + style Blockchain fill:#f0f0f0,stroke:#c9c9c9,color:#111 + + linkStyle 2 stroke:#0ea5e9,stroke-width:2px + +``` + +## Communication Patterns + +### On-Chain Channel Opening + +The channel opening process follows a coordinated sequence between client and a clearnode: + +1. Client requests channel creation from a clearnode via Nitro RPC +2. The clearnode returns a channel struct and signed initial state (signature at index 1) +3. Client signs the initial state (signature at index 0) +4. Client calls the `create(...)` method of the Custody Smart Contract on the blockchain, providing the channel and initial state with both signatures +5. Contract verifies signatures and emits `Opened` event +6. Channel becomes ACTIVE immediately +7. The clearnode monitors the `Opened` event and updates its internal state + +```mermaid +sequenceDiagram + participant Client + participant Clearnode + participant Blockchain + + Client->>Clearnode: create_channel request + Clearnode->>Client: channel struct + signed initial state (clearnode signature) + Client->>Client: Add own signature + Client->>Blockchain: create() with BOTH signatures + Blockchain->>Blockchain: Verify signatures
Set status = ACTIVE + Blockchain->>Blockchain: Emit Opened event + Blockchain-->>Clearnode: Opened event (monitored) + Clearnode->>Client: channel_update notification +``` + +:::success Cooperative Opening +Channel opening requires cooperation between both parties, ensuring mutual agreement before funds are locked. +::: + +### Off-Chain Updates + +**Off-Chain Updates**: + +1. Participants exchange signed state updates: + + - **For Payment Channels** (User ↔ Clearnode): States are exchanged directly via Nitro RPC + + - **For App Sessions** (Multi-party): State exchange is managed by the App itself (peer-to-peer). Once the state has enough signatures to satisfy quorum, a responsible party submits the signed state to the Clearnode + +2. No blockchain transactions required + +3. Latest valid state maintained off-chain + +4. Can be checkpointed on-chain at any time + + - *Current Implementation Note*: While this is the ideal design goal, the current implementation does not store the state off-chain, so checkpointing is not currently supported. This functionality is under development and will be more enforced in the next version of the protocol. + +:::tip Zero Gas Fees +Off-chain updates are instant (< 1 second) and incur zero gas fees, enabling high-frequency operations. +::: + +### On-Chain Channel Closing + +Channels can be closed in two ways: + +**Cooperative Closure**: +1. All participants negotiate and agree on the final state +2. Each participant signs the final state with `intent = FINALIZE` +3. Any participant submits the fully-signed final state to the Custody Contract via `close()` +4. Contract verifies all signatures and distributes funds according to final allocations +5. Channel status becomes FINAL + +This is the preferred closure method. It requires only 1 transaction and is gas-efficient. + +**Non-Cooperative Closure**: +1. A participant submits the latest known state to the Custody Contract via `challenge()` +2. Contract verifies signatures and sets channel status to DISPUTE +3. A challenge period begins (e.g., 24 hours), allowing the other party to respond +4. If participants decides to cooperate again, they may produce a newer valid state, and any of them can submit it via `checkpoint()`, thus stopping the challenge period and moving the channel from DISPUTE back to ACTIVE status +5. If not, after the challenge period expires, any participant calls `close()` to finalize with the latest submitted state +6. Contract distributes funds according to the final state allocations + +This mechanism resolves disputes when parties cannot cooperate. It requires a waiting period for security and is more expensive due to multiple transactions. + +```mermaid +graph LR + A[Active Channel] --> B{Parties Agree?} + B -->|Yes| C[Cooperative Close
Fast, Cheap] + B -->|No| D[Challenge-Response
Slow, Secure] + + style C fill:#90EE90,stroke:#333,color:#111 + style D fill:#FFB6C1,stroke:#333,color:#111 +``` + +## Fund Flow + +The following diagram illustrates how funds flow through the VirtualApp protocol: + +```mermaid +graph TB + A["User Wallet
(ERC-20)"] -->|deposit| B["Available
(Custody SC)"] + B -->|resize| D["Unified Balance
(Clearnode)"] + B <-->|resize| C["Channel-Locked
(Custody SC)"] + C <-->|resize| D + D -->|open/deposit| E["App Sessions
(Application)"] + E -->|withdraw / close session| D + D -->|resize / close channel| B + B -->|withdraw| A + + style A fill:#90EE90,stroke:#333,color:#111 + style B fill:#87CEEB,stroke:#333,color:#111 + style C fill:#FFD700,stroke:#333,color:#111 + style D fill:#DDA0DD,stroke:#333,color:#111 + style E fill:#FFA07A,stroke:#333,color:#111 + +``` + +**Flow Explanation**: + +1. **Deposit**: User deposits ERC-20 tokens into the **Available** balance of the Custody Contract. +2. **Resize**: Funds can be moved between **Available** balance and **Unified Balance** (managed off-chain by the clearnode). +3. **Channel Lock**: Funds can also be moved between **Available** balance and **Channel-Locked** balance via resize operations, or between **Channel-Locked** and **Unified Balance**. +4. **App Sessions**: Funds from the **Unified Balance** can be allocated to App Sessions. +5. **Release**: When app sessions close or funds are withdrawn, they return to the **Unified Balance**. +6. **Unlock/Withdraw**: Funds can be moved back to **Available** balance (via resize/close) and then withdrawn to the **User Wallet**. + +:::caution Security Guarantee +At every stage, funds remain cryptographically secured. Users can always recover their funds according to the latest valid signed state, even if the clearnode becomes unresponsive. +::: diff --git a/versioned_docs/version-0.5.x/protocol/communication-flows.mdx b/versioned_docs/version-0.5.x/protocol/communication-flows.mdx new file mode 100644 index 0000000..4cdcaa3 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/communication-flows.mdx @@ -0,0 +1,753 @@ +--- +sidebar_position: 6 +title: Cross-Layer Communication Flows +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Cross-Layer Communication Flows + +This section illustrates how the on-chain and off-chain layers interact during typical operations. Each flow shows the sequence of method calls and data exchange between Client, Clearnode, and Smart Contracts. + +:::info Flow Navigation +Jump to a specific flow: +- [Authentication Flow](#authentication-flow) - Establish session with session key delegation +- [Channel Creation Flow](#channel-creation-flow) - Open payment channel on blockchain +- [Off-Chain Transfer Flow](#off-chain-transfer-flow) - Instant transfers without gas +- [App Session Lifecycle](#app-session-lifecycle-flow) - Multi-party application flow +- [Cooperative Closure](#cooperative-closure-flow) - Fast channel closure +- [Challenge-Response Closure](#challenge-response-closure-flow) - Dispute resolution +::: + +--- + +## Authentication Flow + +### Purpose + +Establish authenticated session with session key delegation. + +### Actors + +- **Client**: User application or SDK +- **Clearnode**: Off-chain service provider + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant Client + participant Wallet as Main Wallet + participant Clearnode + + Note over Client: 1. Generate Session Keypair + Client->>Client: session_private_key = random() + Client->>Client: session_address = address(session_public_key) + + Note over Client,Clearnode: 2. auth_request (public, no signature) + Client->>Clearnode: auth_request(address, session_key, allowances, scope, expires_at) + + Note over Clearnode: 3. Generate Challenge + Clearnode->>Clearnode: Validate params, generate UUID + Clearnode->>Client: auth_challenge(challenge_message) + + Note over Client: 4. Sign Challenge with MAIN wallet (EIP-712) + Client->>Wallet: Sign Policy typed data (challenge, scope, wallet, session_key, expires_at, allowances) + Wallet-->>Client: EIP-712 signature + Client->>Clearnode: auth_verify(challenge, sig) // or auth_verify(challenge, jwt) + + Note over Clearnode: 5. Validate & Issue Session + Clearnode->>Clearnode: Recover main wallet from sig (or validate jwt) + Clearnode->>Clearnode: Create session + JWT + Clearnode->>Client: {address, session_key, jwt_token, success} + + Note over Client,Clearnode: Subsequent requests signed with session_key +``` + +### Steps + +#### Step 1: Client Generates Session Keypair + +The session key is generated entirely off-chain and the private key never leaves the client: + +```javascript +session_private_key = random() +session_public_key = derive(session_private_key) +session_address = address(session_public_key) +``` + +#### Step 2: Client → Clearnode: `auth_request` (public, no signature) + +The client sends a public registration request (no signature required): + +```javascript +Request: +{ + address: user_wallet_address + session_key: session_address + allowances: [{"asset": "usdc", "amount": "100.0"}] + scope: "transfer,app.create" + expires_at: 1762417328123 // Unix ms +} +``` + +#### Step 3: Clearnode Validates and Generates Challenge + +The clearnode performs validation: +- Validate address/session_key format, optional allowances/scope, expires_at +- Generate challenge UUID + +#### Step 4: Clearnode → Client: `auth_challenge` + +The clearnode responds with a challenge: + +```javascript +Response: +{ + challenge_message: "550e8400-e29b-41d4-a716-446655440000" +} +Signature: signed by Clearnode +``` + +#### Step 5: Client Signs Challenge (MAIN wallet, EIP-712) + +The client signs the challenge using the main wallet over the Policy typed data (includes challenge, wallet, session_key, expires_at, scope, allowances): + +```javascript +challenge_signature = signTypedData(policyTypedData, main_wallet_private_key) +``` + +#### Step 6: Client → Clearnode: `auth_verify` + +The client submits the signed challenge (or a previously issued JWT): + +```javascript +Request: +{ + challenge: "550e8400-e29b-41d4-a716-446655440000", + // alternatively: + // jwt: "" +} +Signature: EIP-712 signature by main wallet (required if jwt is absent) +``` + +#### Step 7: Clearnode Validates Challenge + +The clearnode validates: +- Signature recovers the wallet used in `auth_request` +- Challenge matches pending authentication +- Challenge not expired or reused + +#### Step 8: Clearnode → Client: `auth_verify` Response + +The clearnode confirms authentication: + +```javascript +Response: +{ + address: user_wallet_address + session_key: session_address + jwt_token: "" + success: true +} +``` + +#### Step 9: Session Established + +- All subsequent requests signed with `session_private_key` +- The clearnode enforces allowances and expiration +- No main wallet interaction required until session expires + +### Key Points + +:::success Session Security +- Session private key **NEVER** leaves the client +- Main wallet only signs once (`auth_request`) +- All subsequent operations use session key +- Allowances prevent unlimited spending +- Challenge-response prevents replay attacks +::: + +**Related Methods**: [`auth_request`](./app-layer/off-chain/authentication#step-1-auth_request), [`auth_challenge`](./app-layer/off-chain/authentication#step-2-auth_challenge), [`auth_verify`](./app-layer/off-chain/authentication#step-3-auth_verify) + +--- + +## Channel Creation Flow + +### Purpose + +Open a payment channel with zero initial balance; fund it later via `resize_channel`. + +### Actors + +- **Client**: User application or SDK +- **Clearnode**: Off-chain service provider +- **Smart Contract**: Custody Contract +- **Blockchain**: Ethereum-compatible network + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant Client + participant Clearnode + participant Blockchain + + Note over Client,Clearnode: Off-Chain Preparation + Client->>Clearnode: create_channel(chain_id, token) + + Note over Clearnode: 2. Prepare Channel + Clearnode->>Clearnode: Generate unique nonce + Clearnode->>Clearnode: Create channel config + Clearnode->>Clearnode: Create initial state (intent: INITIALIZE, version: 0, zero allocations) + Clearnode->>Clearnode: Pack & sign state + + Clearnode->>Client: {channel_id, channel, state, server_signature} + + Note over Client: 3. Validate & Sign + Client->>Client: Verify Clearnode signature + Client->>Client: Sign packed state with user key + + Note over Client,Blockchain: On-Chain Execution + Client->>Blockchain: Custody.create(channel, state, sig_user, sig_clearnode) + Blockchain->>Blockchain: Verify signatures + Blockchain->>Blockchain: Create channel (zero balance) + Blockchain->>Blockchain: Set status to OPEN/ACTIVE + Blockchain->>Blockchain: Emit Opened event + + Blockchain-->>Clearnode: Opened event (monitored) + Blockchain-->>Client: Opened event + + Note over Client,Clearnode: Channel is now ACTIVE +``` + +### Steps + +#### Step 1: Client → Clearnode: `create_channel` + +Client requests channel creation: + +```javascript +Request: +{ + chain_id: 137 // Polygon + token: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" // USDC +} +Signature: session key signature +``` + +#### Step 2: Clearnode Processes Request + +The clearnode: +- Validates token is supported on chain +- Generates unique nonce +- Selects adjudicator (SimpleConsensus for payment channels) +- Creates Channel struct +- Computes `channelId` = `keccak256(abi.encode(Channel))` +- Creates initial State with `intent: INITIALIZE`, `version: 0`, `state_data: "0x"`, zero allocations +- Packs state (`abi.encode(channelId, intent, version, data, allocations)` in Solidity terms) +- Signs packed state with clearnode's participant key + +#### Step 3: Clearnode → Client: Response + +```javascript +Response: +{ + channel: { + participants: [user_address, clearnode_address] + adjudicator: 0xSimpleConsensusAddress + challenge: 86400 + nonce: 1699123456789 + } + state: { + intent: INITIALIZE + version: 0 + data: "0x" + allocations: [ + {destination: user_address, token: usdc, amount: 0}, + {destination: clearnode_address, token: usdc, amount: 0} + ] + } + server_signature: "0xClearnodeSig..." + channel_id: "0xChannelId..." +} +``` + +:::tip Clearnode Signs First +The clearnode provides its signature **BEFORE** the user commits funds on-chain. This ensures both parties have committed before any on-chain transaction occurs. +::: + +#### Steps 4-5: Client Validates and Signs + +Client: +- Recomputes `channelId` and verifies it matches +- Recomputes packed state and verifies clearnode signature +- Signs packed state with user's participant key + +#### Step 6: Client → Blockchain: `Custody.create()` + +Client submits transaction: +```javascript +Custody.create(channel, state, userSig, serverSig) +``` + +#### Step 7: Blockchain Validates and Creates Channel + +Contract: +- Verifies user's signature is valid +- Verifies clearnode's signature is valid +- Stores channel parameters and funding state (zero balances) +- Sets channel status to `OPEN` +- Emits `Opened` event + +#### Step 8: Event Listener Detects Creation + +The clearnode's event listener: +- Detects `Opened` event +- Validates channel parameters + +#### Steps 9-10: Notifications + +The clearnode: +- Updates internal database: channel status = open (zero balance) +- Sends `channel_update` notification to client + +#### Step 11: Channel Active + +- Channel active with zero balance +- Use `resize_channel` to fund the channel + +### Key Points + +:::success Two-Phase Process +- **Off-chain preparation**: Clearnode prepares and signs channel configuration +- **On-chain execution**: User submits transaction to lock funds +- This ensures clearnode is ready to join before user risks funds +::: + +**Related Methods**: [`create_channel`](./app-layer/off-chain/channel-methods#create_channel) + +--- + +## Off-Chain Transfer Flow + +### Purpose + +Transfer funds between users instantly without blockchain transaction. + +### Actors + +- **Sender (Client A)**: Initiating user +- **Clearnode**: Off-chain service provider +- **Receiver (Client B)**: Receiving user + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant Sender as Client A + participant Clearnode + participant Receiver as Client B + + Sender->>Clearnode: transfer(destination, amount, asset) + + Note over Clearnode: 2. Validate Request + Clearnode->>Clearnode: Check A authenticated + Clearnode->>Clearnode: Check A has sufficient balance + Clearnode->>Clearnode: Check B exists (has channel) + + Note over Clearnode: 3. Update Ledger + Clearnode->>Clearnode: Debit entry (A: -50 USDC) + Clearnode->>Clearnode: Credit entry (B: +50 USDC) + Clearnode->>Clearnode: Create transaction record + + Clearnode->>Sender: Transfer confirmed ✓ + + Note over Clearnode,Receiver: 4. Notify Receiver + Clearnode->>Receiver: transfer_received event + Clearnode->>Receiver: balance_update event + + Note over Sender,Receiver: Complete: < 1 second, zero gas +``` + +### Steps + +#### Step 1: Client A → Clearnode: `transfer` + +Sender initiates transfer: + +```javascript +Request: +{ + destination: "0xClientB_Address", // or destination_user_tag: "UX123D" + allocations: [{"asset": "usdc", "amount": "50.0"}] +} +Signature: Client A's session key +``` + +#### Step 2: Clearnode Validates + +The clearnode validates: +- Client A is authenticated +- Client A has >= 50 USDC available balance +- Destination address/tag is valid (account is created if new) +- Asset "usdc" is supported + +#### Step 3: Clearnode Creates Ledger Entries + +Double-entry bookkeeping: + +**Entry 1 (Debit from Client A unified account)**: +```javascript +{ + account_id: Client A address + asset: "usdc" + credit: "0.0" + debit: "50.0" +} +``` + +**Entry 2 (Credit to Client B unified account)**: +```javascript +{ + account_id: Client B address + asset: "usdc" + credit: "50.0" + debit: "0.0" +} +``` + +#### Step 4: Clearnode Creates Transaction Record + +```javascript +{ + id: 1, + tx_type: "transfer", + from_account: Client A address, + from_account_tag: "NQKO7C", + to_account: Client B address, + to_account_tag: "UX123D", + asset: "usdc", + amount: "50.0", + created_at: "2023-05-01T12:00:00Z" +} +``` + +#### Step 5: Clearnode → Client A: Response + +```javascript +Response: +{ + transactions: [ + { + id: 1, + tx_type: "transfer", + from_account: "0xA...", + from_account_tag: "NQKO7C", + to_account: "0xB...", + to_account_tag: "UX123D", + asset: "usdc", + amount: "50.0", + created_at: "2023-05-01T12:00:00Z" + } + ] +} +``` + +#### Step 6-7: Clearnode → Clients: Notifications + +- `tr` (transfer) notification to sender/receiver with `transactions` array +- `bu` (balance update) notification reflecting new balances + +#### Step 9: Transfer Complete + +- Instant (< 1 second) +- No blockchain transaction +- Zero gas fees +- Both parties notified + +### Key Points + +:::success Instant Settlement +- **Purely off-chain**: Database transaction, no blockchain +- **Instant settlement**: < 1 second typical +- **Zero gas fees**: No on-chain transaction required +- **Double-entry bookkeeping**: Accounting accuracy guaranteed +- **Receiver account auto-created**: Destination tag/address need not have a prior balance +::: + +**Related Methods**: [`transfer`](./app-layer/off-chain/transfers#transfer) + +--- + +## App Session Lifecycle Flow + +### Purpose + +Create, update, and close a collaborative app session with multiple participants. + +### Actors + +- **Client A**: Participant 1 +- **Client B**: Participant 2 +- **Clearnode**: Off-chain service provider + +### Scenario + +Two-player chess game with 100 USDC stake each. + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant A as Client A + participant B as Client B + participant Clearnode + + Note over A,B: Create (lock funds) + A->>Clearnode: create_app_session(definition, allocations, session_data?) + B-->>Clearnode: co-sign (if non-zero allocation) + Clearnode->>Clearnode: Validate quorum, balances, allowances + Clearnode->>Clearnode: Lock allocations from unified balances + Clearnode-->>A: {app_session_id, status:"open", version:1} + Clearnode-->>B: asu/bu notifications + + Note over A,B: Update (submit_app_state) + A->>Clearnode: submit_app_state(app_session_id, intent, version, allocations, session_data?) + B-->>Clearnode: co-signs to meet quorum + Clearnode->>Clearnode: Validate intent rules, version, quorum, allowances + Clearnode->>Clearnode: Apply operate/deposit/withdraw + Clearnode-->>A: {app_session_id, status:"open", version:n} + Clearnode-->>B: asu/bu notifications + + Note over A,B: Close + A->>Clearnode: close_app_session(app_session_id, allocations, session_data?) + B-->>Clearnode: co-signs to meet quorum + Clearnode->>Clearnode: Validate sums and quorum, release to unified balances + Clearnode-->>A: {app_session_id, status:"closed", version:n+1} + Clearnode-->>B: asu/bu notifications + +``` + +### Sequence (Create → Update → Close) + +1. **Create (off-chain): `create_app_session`** + - Client signs request (all participants with non-zero allocations must sign). + - Clearnode validates protocol version (0.2/0.4), quorum, balances, allowances/session keys. + - Funds are locked from each signer’s unified balance into the app session account. + - **Response (minimal)**: `app_session_id`, `status: "open"`, `version: 1`. Full metadata is not echoed; use `get_app_sessions` to read it. + + **Example Request**: + ```json + { + "req": [1,"create_app_session",{ + "definition": { + "protocol": "NitroRPC/0.4", + "participants": ["0xA","0xB"], + "weights": [100,100], + "quorum": 200, + "challenge": 86400, + "nonce": 1699123 + }, + "allocations": [ + {"participant": "0xA","asset": "usdc","amount": "100.0"}, + {"participant": "0xB","asset": "usdc","amount": "100.0"} + ], + "session_data": "{\"game\":\"chess\"}" + },1699123456789], + "sig": ["0xUserSig","0xCoSig"] + } + ``` + +2. **State Updates (off-chain): `submit_app_state`** + - v0.4 requires `version = current+1`; v0.2 rejects `intent`/`version` and only allows a single update. + - Intents: + - `operate`: redistribute, sum must stay equal. + - `deposit`: sum must increase; depositor must sign and have available unified balance. + - `withdraw`: sum must decrease; session must have funds. + - Quorum required; session-key allowances enforced. + - **Response (minimal)**: `app_session_id`, `status: "open"`, `version` (new). No metadata echoed. + - Notifications: `asu` (app session update) + `bu` (balance update for deposit/withdraw). + + **Example Request (deposit v0.4)**: + ```json + { + "req": [2,"submit_app_state",{ + "app_session_id": "0xSession", + "intent": "deposit", + "version": 2, + "allocations": [ + {"participant": "0xA","asset": "usdc","amount": "150.0"}, + {"participant": "0xB","asset": "usdc","amount": "100.0"} + ] + },1699123456790], + "sig": ["0xUserSig","0xCoSig"] + } + ``` + +3. **Close (off-chain): `close_app_session`** + - Requires quorum signatures; final allocations must match total balances. + - **Response (minimal)**: `app_session_id`, `status: "closed"`, `version` (incremented). No metadata echoed. + - Funds are released to participants’ unified balances; notifications `asu` and `bu` are sent. + + **Example Request**: + ```json + { + "req": [3,"close_app_session",{ + "app_session_id": "0xSession", + "allocations": [ + {"participant": "0xA","asset": "usdc","amount": "180.0"}, + {"participant": "0xB","asset": "usdc","amount": "20.0"} + ] + },1699123456795], + "sig": ["0xUserSig","0xCoSig"] + } + ``` + +### Key Points + +:::info App Sessions +App sessions enable multi-party applications with custom governance rules. Funds are locked from unified balance for the duration of the session. +::: + +**Related Methods**: [`create_app_session`](./app-layer/off-chain/app-sessions#create_app_session), [`submit_app_state`](./app-layer/off-chain/app-sessions#submit_app_state), [`close_app_session`](./app-layer/off-chain/app-sessions#close_app_session) + +--- + +## Cooperative Closure Flow + +### Purpose + +Close channel when all parties agree on final state. + +### Actors + +- **Client**: User application +- **Clearnode**: Off-chain service provider +- **Smart Contract**: Custody Contract +- **Blockchain**: Ethereum-compatible network + +### Key Points + +:::success Preferred Method +Cooperative closure is **fast (1 transaction)**, **cheap (low gas)**, and **immediate (no waiting period)**. Always use this when possible. +::: + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant Client + participant Clearnode + participant Blockchain + + Client->>Clearnode: close_channel(channel_id, funds_destination) + Clearnode->>Clearnode: Validate channel open/resizing and not challenged + Clearnode->>Clearnode: Build FINALIZE state (version = current+1, data = "0x", allocations) + Clearnode-->>Client: {channel_id, state, server_signature} + Client->>Client: Verify server_signature and sign packed state + Client->>Blockchain: Custody.close(channel_id, state, userSig, serverSig) + Blockchain->>Blockchain: Verify signatures, close channel, emit event + Blockchain-->>Clearnode: Event observed + Clearnode->>Clearnode: Update DB and balances + Clearnode-->>Client: cu + bu notifications + +``` + +### Sequence + +1. **Client → Clearnode**: `close_channel(channel_id, funds_destination)` + - Authenticated request signed by the user (session key or wallet). + + **Example Request**: + ```json + { + "req": [10,"close_channel",{ + "channel_id": "0xChannel", + "funds_destination": "0xUser" + },1699123457000], + "sig": ["0xUserSig"] + } + ``` +2. **Clearnode**: validates channel exists and is `open`/`resizing`, checks challenged-channel guard, builds FINALIZE state: + - `intent: FINALIZE`, `version = current+1`, `state_data: "0x"`, allocations split between user and broker based on channel balance. + - Signs packed state (`keccak256(abi.encode(channelId, intent, version, data, allocations))`). +3. **Clearnode → Client**: response with `channel_id`, `state`, `server_signature`. +4. **Client**: verifies server signature, signs the same packed state. +5. **Client → Blockchain**: `Custody.close(channel_id, state, userSig, serverSig)` (one tx). +6. **Blockchain**: verifies both signatures, closes channel, emits `Closed/Opened`-equivalent event (implementation-specific), releases funds. +7. **Clearnode**: observes event, updates DB, sends `cu` (channel update) and `bu` (balance update) notifications. + +**Related Methods**: [`close_channel`](./app-layer/off-chain/channel-methods#close_channel) + +--- + +## Challenge-Response Closure Flow + +### Purpose + +Close channel when other party is unresponsive or disputes final state. + +### Actors + +- **Client**: User application +- **Clearnode**: Off-chain service provider (may be unresponsive) +- **Smart Contract**: Custody Contract +- **Blockchain**: Ethereum-compatible network + +### Key Points + +:::warning Challenge Period +This method requires waiting for the challenge period (typically 24 hours) to elapse. Use only when cooperative closure fails. +::: + +### Sequence Diagram + +```mermaid +sequenceDiagram + participant Client + participant Blockchain + participant Clearnode + + Note over Client: Hold latest signed state + Client->>Blockchain: Custody.challenge(channelId, state, sigs) + Blockchain->>Blockchain: Start challenge timer (challenge period) + alt Newer state posted + OtherParty->>Blockchain: Custody.checkpoint(channelId, newerState, sigs) + Blockchain->>Blockchain: Replace pending state + end + Note over Client,Blockchain: Wait for challenge period expiry + Client->>Blockchain: Custody.close(channelId, state, sigs) // after timeout if uncontested + Blockchain->>Blockchain: Finalize channel, emit event + Blockchain-->>Clearnode: Event observed when back online + Clearnode->>Clearnode: Update DB, balances + Clearnode-->>Client: cu + bu notifications +``` + +### Sequence (User-initiated, clearnode unresponsive) + +1. **Prerequisite**: User holds the latest mutually signed state (or clearnode-signed latest) for the channel. +2. **Client → Blockchain**: `Custody.challenge(channelId, state, sigs...)` + - Submits the latest signed state to start the challenge. +3. **Challenge Window**: Other party can respond with a newer valid state before timeout. +4. **If no newer state is posted**: After the challenge period, user calls `Custody.close(channelId, state, sigs...)` to finalize. +5. **Blockchain**: finalizes channel, releases funds per challenged state, emits closure event. +6. **Clearnode** (when responsive again): observes event, updates DB, sends `cu`/`bu` notifications to participants. + +**Related Methods**: On-chain `Custody.challenge()` and `Custody.close()` + +--- + +## Next Steps + +Now that you understand how all protocol layers work together: + +1. **Review Method Details**: Visit Part 2 (Off-Chain RPC Protocol) for complete method specifications +2. **Explore Reference**: See [Protocol Reference](./protocol-reference) for constants and standards +3. **Implementation Guide**: Check [Implementation Checklist](./implementation-checklist) for best practices +4. **Quick Start**: Follow the [Quick Start Guide](/docs/build/quick-start) to begin building + +:::tip Complete Flows +These flows represent the most common operations. For edge cases and error handling, consult the specific method documentation in Part 2. +::: diff --git a/versioned_docs/version-0.5.x/protocol/glossary.mdx b/versioned_docs/version-0.5.x/protocol/glossary.mdx new file mode 100644 index 0000000..063a3d0 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/glossary.mdx @@ -0,0 +1,415 @@ +--- +sidebar_position: 9 +title: Glossary +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Glossary + +Complete alphabetical reference of all protocol terms and concepts. + +:::tip Quick Reference +This glossary provides concise definitions of all VirtualApp protocol terms. For detailed explanations with examples and diagrams, see the respective sections in the documentation. +::: + +--- + +## A + +### Adjudicator + +Smart contract that validates state transitions according to application-specific rules. Each channel specifies an adjudicator that determines which state updates are valid. + +**Examples**: SimpleConsensus, RemittanceAdjudicator + +**Related**: [Data Structures](./app-layer/on-chain/data-structures), [Security Considerations](./app-layer/on-chain/security) + +--- + +### Allocation + +Specification of how funds are distributed, containing destination, token, and amount. Allocations define where funds should be sent when a channel closes or how they're distributed within an app session. + +**Structure**: +```javascript +{ + destination: address, // Recipient wallet address + token: address, // ERC-20 token contract + amount: uint256 // Amount in smallest unit +} +``` + +**Related**: [Data Structures](./app-layer/on-chain/data-structures#allocation) + +--- + +### App Session + +Off-chain channels built on top of payment channels, intended to be used by app developers to enable application-specific interactions and transactions without touching the blockchain. Previously known as Virtual Ledger Channels (VLC). + +**Features**: +- Multi-party participation +- Custom governance rules (quorum, weights) +- Fund locking from unified balance +- Application-specific state management + +**Related**: [App Sessions Methods](./app-layer/off-chain/app-sessions), [Communication Flows](./communication-flows#app-session-lifecycle-flow) + +--- + +## C + +### Challenge Period + +Duration (in seconds) that parties have to respond to a dispute. When a channel is challenged, all participants have this amount of time to submit a newer state before the challenged state becomes final. + +**Typical Values**: +- Default: 86400 seconds (24 hours) +- Minimum recommended: 3600 seconds (1 hour) +- Maximum recommended: 604800 seconds (7 days) + +**Related**: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle#closure---challenge-response) + +--- + +### Channel + +A secure communication pathway between participants that locks funds in an on-chain smart contract while enabling off-chain state updates. Channels are the foundation of the VirtualApp protocol. + +**Lifecycle**: VOID → INITIAL → ACTIVE → (DISPUTE) → FINAL + +**Related**: [On-Chain Protocol](./app-layer/on-chain/overview), [Data Structures](./app-layer/on-chain/data-structures#channel) + +--- + +### channelId + +A unique identifier for a channel, formatted as a 0x-prefixed hex string (32 bytes). Computed deterministically from the channel configuration. + +**Computation**: +```javascript +channelId = keccak256(abi.encode( + channel.participants, + channel.adjudicator, + channel.challenge, + channel.nonce +)) +``` + +**Related**: [Data Structures](./app-layer/on-chain/data-structures#channel-identifier) + +--- + +### Checkpoint + +Recording a state on-chain without entering dispute mode. Checkpointing creates an on-chain record of the current state but keeps the channel in ACTIVE status. + +**Benefits**: +- Shortens effective challenge history +- Provides on-chain proof of state +- Doesn't start challenge period + +**Related**: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle#checkpointing) + +--- + +### Clearnode + +A virtual ledger layer operated by independent node operators using the issuer's open-source software. It provides a unified ledger (through Nitro RPC) and coordinates state channels (through VirtualApp), providing chain abstraction for developers and users. + +**Responsibilities**: +- Manage off-chain RPC protocol +- Provide unified balance +- Join payment channels +- Coordinate app sessions + +**Related**: [Architecture](./architecture), [Off-Chain RPC Protocol](./app-layer/off-chain/overview) + +--- + +### Creator + +The participant at index 0 in a channel who initiates channel creation. The Creator constructs the channel configuration, prepares the initial funding state, signs it (signature at position 0), and calls the on-chain `create()` function to lock their funds and establish the channel. + +**Typically**: A user or light client opening a payment channel with a clearnode. + +**Related**: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle#creation-phase), [Protocol Reference](./protocol-reference#participant-indices) + +--- + +### Custody Contract + +The main on-chain contract implementing channel creation, joining, closure, and resizing. It is an implementation of the VirtualApp protocol. + +**Interfaces Implemented**: +- `IChannel` - Core channel operations +- `IDeposit` - Fund management +- `IChannelReader` - State queries + +**Related**: [On-Chain Overview](./app-layer/on-chain/overview), [Data Structures](./app-layer/on-chain/data-structures) + +--- + +## I + +### Intent + +In NitroRPC/0.4, specifies the type of app session state update. The intent system enables dynamic fund management within active sessions. + +**Types**: +- **OPERATE**: Redistribute existing funds (sum unchanged) +- **DEPOSIT**: Add funds to session from unified balance +- **WITHDRAW**: Remove funds from session to unified balance + +**Related**: [App Sessions](./app-layer/off-chain/app-sessions#submit_app_state) + +--- + +## L + +### Ledger Entry + +Double-entry bookkeeping record of a debit or credit. Every financial operation in a clearnode creates two ledger entries to maintain accounting accuracy. + +**Fields**: +- account_id +- asset +- credit (incoming) +- debit (outgoing) +- created_at + +**Related**: [Transfers](./app-layer/off-chain/transfers#off-chain-processing), [Query Methods](./app-layer/off-chain/queries#get_ledger_entries) + +--- + +### Ledger Transaction + +User-facing transaction record showing transfers, deposits, withdrawals, and app session operations. Provides a simplified view of financial activity. + +**Types**: +- `transfer` - Direct transfer between users +- `deposit` - Funds deposited to unified balance +- `withdrawal` - Funds withdrawn from unified balance +- `app_deposit` - Funds locked in app session +- `app_withdrawal` - Funds released from app session + +**Related**: [Query Methods](./app-layer/off-chain/queries#get_ledger_transactions) + +--- + +## M + +### Magic Number + +Constant in `state.data` signaling special states. Magic numbers enable smart contracts to identify the type of state without complex parsing. + +**Values**: +- **CHANOPEN** = 7877 (0x1EC5) - Initial funding state +- **CHANCLOSE** = 7879 (0x1EC7) - Final closing state + +**Related**: [Protocol Reference](./protocol-reference#magic-numbers), [Data Structures](./app-layer/on-chain/data-structures) + +--- + +## N + +### Nonce + +Unique number ensuring channel identifier uniqueness. Even with identical participants and configuration, different nonces create different channel IDs. + +**Typical Value**: Timestamp in milliseconds + +**Related**: [Data Structures](./app-layer/on-chain/data-structures#channel), [Channel Creation](./app-layer/off-chain/channel-methods#create_channel) + +--- + +### Nitro RPC + +The off-chain communication protocol for state channel operations. A lightweight, RPC-based RPC protocol with compact message format and signature-based authentication. + +**Versions**: +- **NitroRPC/0.2**: Legacy (basic state updates) +- **NitroRPC/0.4**: Current (intent system) + +**Related**: [Off-Chain RPC Protocol](./app-layer/off-chain/overview), [Message Format](./app-layer/off-chain/message-format) + +--- + +### VirtualApp (App Layer / YApps) + +The App Layer protocol for state channels and application hosting. Includes on-chain contracts (Custody, Adjudicator) and the off-chain Nitro RPC protocol. Also known as YApps. + +**Version**: 0.5.0 (Mainnet deployments live; not production yet) + +**Related**: [On-Chain Protocol](./app-layer/on-chain/overview), [Protocol Reference](./protocol-reference#protocol-versions) + +--- + +## P + +### Participant + +An entity (identified by a wallet address) that is part of a channel. Typically includes a Creator (user) and a Clearnode or Clearnode Operator. + +**In Payment Channels**: +- Index 0: Creator (user) +- Index 1: Clearnode + +**Related**: [Data Structures](./app-layer/on-chain/data-structures#channel), [Protocol Reference](./protocol-reference#participant-indices) + +--- + +## Q + +### Quorum + +Minimum total weight of signatures required to approve app session state updates. The quorum defines the governance threshold for multi-party applications. + +**Example**: +```javascript +participants: [Alice, Bob, Judge] +weights: [40, 40, 50] +quorum: 80 + +// Valid signature combinations: +// - Alice + Bob (40 + 40 = 80) ✓ +// - Alice + Judge (40 + 50 = 90) ✓ +// - Bob + Judge (40 + 50 = 90) ✓ +// - Alice alone (40 < 80) ✗ +``` + +**Related**: [App Sessions](./app-layer/off-chain/app-sessions#create_app_session), [Governance Examples](./app-layer/off-chain/app-sessions#governance-examples) + +--- + +## R + +### requestId + +A unique identifier for an RPC request, used for correlating requests and responses. Generated by the client for each request. + +**Type**: uint64 + +**Related**: [Message Format](./app-layer/off-chain/message-format#general-structure) + +--- + +## S + +### Session Key + +A temporary cryptographic key delegated by a user's main wallet that provides a flexible way for the user to manage security of their funds by giving specific permissions and allowances for specific apps. + +**Features**: +- Spending limits (allowances) +- Operation scope restrictions +- Expiration time +- Gasless signing (no wallet prompts) + +**Related**: [Authentication](./app-layer/off-chain/authentication), [Communication Flows](./communication-flows#authentication-flow) + +--- + +### State + +A snapshot of the channel at a point in time, including fund allocations and application-specific data. States are signed by participants to authorize transitions. + +**Components**: +- `intent` - Purpose of the state (INITIALIZE, OPERATE, RESIZE, FINALIZE) +- `version` - Incremental version number for comparison +- `data` - Application-specific data or magic number +- `allocations` - Fund distribution +- `sigs` - Participant signatures + +**Related**: [Data Structures](./app-layer/on-chain/data-structures#state) + +--- + +### packedState + +A specific encoding of a channelId, state.intent, state.version, state.data, state.allocations, used for signing and signature verification. + +**Computation**: +```javascript +packedState = abi.encode( + channelId, + state.intent, + state.version, + state.data, + state.allocations +) +``` + +**Related**: [Data Structures](./app-layer/on-chain/data-structures#packed-state) + +--- + +## U + +### Unified Balance + +An abstraction that aggregates a user's funds across multiple blockchain networks, managed by a clearnode. The unified balance provides a single view of all assets regardless of which chain they're locked on. + +**Benefits**: +- Chain abstraction +- Simplified fund management +- Cross-chain transfers without bridges +- Single balance for all operations + +**Related**: [Architecture](./architecture#fund-flow), [Transfers](./app-layer/off-chain/transfers#unified-balance-mechanics) + +--- + +## W + +### Weight + +Voting power assigned to a participant in an app session. Weights determine how much influence each participant has in governance decisions. + +**Usage**: Sum of signer weights must meet quorum for state updates to be valid. + +**Related**: [App Sessions](./app-layer/off-chain/app-sessions#create_app_session) + +--- + +## Additional Terms + +### appSessionId + +A unique identifier for an app session, formatted as a 0x-prefixed hex string (32 bytes). Used for all subsequent operations on that specific app session. + +--- + +### chainId + +A blockchain network identifier (uint64). Examples: 1 (Ethereum Mainnet), 137 (Polygon), 8453 (Base), 42161 (Arbitrum One), 10 (Optimism). + +--- + +### assetSymbol + +A lowercase string identifier for a supported asset (e.g., "usdc", "eth", "weth", "usdt", "dai", "wbtc"). Asset symbols are consistent across chains. + +--- + +### walletAddress + +A user's blockchain address (0x-prefixed hex string, 20 bytes) that identifies their account and owns funds. Used to identify participants in channels and app sessions. + +--- + +## Cross-References + +For detailed explanations of these terms with examples, diagrams, and use cases, refer to: + +- **Core Concepts**: [Terminology](./terminology) +- **On-Chain Details**: [On-Chain Protocol](./app-layer/on-chain/overview) +- **Off-Chain Details**: [Off-Chain RPC Protocol](./app-layer/off-chain/overview) +- **Implementation**: [Quick Start Guide](/docs/build/quick-start), [Implementation Checklist](./implementation-checklist) + +:::tip Using This Glossary +Press `Ctrl+F` (or `Cmd+F` on Mac) to search for specific terms. Most terms also appear as tooltips throughout the documentation for quick reference. +::: + diff --git a/versioned_docs/version-0.5.x/protocol/implementation-checklist.mdx b/versioned_docs/version-0.5.x/protocol/implementation-checklist.mdx new file mode 100644 index 0000000..c1eda5e --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/implementation-checklist.mdx @@ -0,0 +1,566 @@ +--- +sidebar_position: 10 +title: Implementation Checklist +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Implementation Checklist + +Comprehensive checklist for building a compliant VirtualApp client with security best practices. + +:::tip Progressive Implementation +You don't need to implement everything at once. Start with Core Protocol and On-Chain Integration, then add Off-Chain RPC and advanced features progressively. +::: + +--- + +## Core Protocol Support + +Foundation requirements for any VirtualApp implementation. + +### Identifier Computation + +- [ ] **Compute channelId from Channel struct** + - Hash participants, adjudicator, challenge, nonce using keccak256 + - Verify deterministic computation (same inputs = same output) + - Reference: [Data Structures](./app-layer/on-chain/data-structures#channel-identifier) + +- [ ] **Compute payload hash (packedState) from channel state** + - Compute `packedState = keccak256(abi.encode(channelId, state.intent, state.version, state.data, state.allocations))` + - Ensure proper ABI encoding + - Reference: [Data Structures](./app-layer/on-chain/data-structures#packed-state) + +### Signature Handling + +- [ ] **Generate signatures** + - Support ECDSA signatures (standard for EOA wallets) + - Encode as `bytes` format (65 bytes: r + s + v) + - For on-chain: sign raw `packedState` hash + - For off-chain RPC: sign EIP-712 typed data structures + - Reference: [Signature Standards](./protocol-reference#signature-standards) + +- [ ] **Verify signatures** + - Recover signer address from signature + - Validate signer matches expected participant + - Support EIP-1271 for smart contract wallets + - Support EIP-6492 for counterfactual contracts + - Handle EIP-191 for personal signatures where applicable + +:::caution Signature Standards +On-chain signatures use raw `packedState` hash for chain-agnostic compatibility. Off-chain RPC messages use EIP-712 typed data for user-facing signatures (e.g., authentication). Refer to [Signature Standards](./protocol-reference#signature-standards) for details. +::: + +--- + +## On-Chain Integration + +Smart contract interactions for channel lifecycle management. + +### Blockchain Connection + +- [ ] **Connect to Ethereum-compatible blockchain** + - Support multiple chains (Ethereum, Polygon, Arbitrum, Optimism, Base) + - Use Web3 provider (e.g., Infura, Alchemy) + - Handle network switching + - Implement retry logic for failed connections + +- [ ] **Load contract ABIs** + - Custody Contract ABI + - Adjudicator contract ABI (application-specific) + - ERC-20 token ABI + +### Channel Operations + +- [ ] **Create channel (`Custody.create`)** + - Verify state has `intent = INITIALIZE` (1) and `version = 0` + - Preferred: include both participant signatures to start in `ACTIVE` + - Legacy: single sig → `INITIAL`; wait for `join()` to reach `ACTIVE` + - Handle ERC-20 approvals only if depositing at creation (legacy flow) + - Reference: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle#creation-phase) + +- [ ] **Monitor activation / join** + - Subscribe to `Opened` and `Joined` events + - In legacy flow, ensure `join()` transitions `INITIAL` → `ACTIVE` + +- [ ] **Cooperative closure (`Custody.close`)** + - Build state with `intent = FINALIZE` (3), `version = current+1`, `data = "0x"` + - Require both participant signatures; submit single tx + - Confirm funds destination and allocations match expectations + - Reference: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle#cooperative-closure) + +- [ ] **Dispute / challenge** + - Persist latest fully signed state for `challenge()` + - During `DISPUTE`, accept newer state via `checkpoint()` (on-chain) if available + - After challenge timeout, finalize with `close()` using challenged state + - Reference: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle#challenge-phase) + +### Event Listening + +- [ ] **Listen to contract events** + - `Opened(channelId, channel, deposits)` - Channel created and active + - `Challenged(channelId, state, expiration)` - Dispute started (expiration = challenge period end) + - `Closed(channelId, allocations)` - Channel finalized + - `Resized(channelId)` - Channel capacity changed + +- [ ] **Process events in order** + - Maintain event log cursor/checkpoint + - Handle blockchain reorganizations + - Implement event replay for recovery + +- [ ] **Update internal state based on events** + - Sync channel status (INITIAL → ACTIVE → DISPUTE → FINAL) + - Update unified balance when channels open/close + - Notify users of status changes + +:::tip Event Recovery +Implement event recovery for when your application restarts or loses connection. Replay events from last checkpoint to current block. +::: + +--- + +## Off-Chain RPC + +RPC communication with clearnode. + +### Connection Management + +- [ ] **Establish RPC connection** + - Connect to clearnode RPC endpoint + - Handle connection timeouts + - Implement exponential backoff for reconnection + - Reference: [Off-Chain Overview](./app-layer/off-chain/overview) + +- [ ] **Implement message format** + - Compact JSON array format: `[requestId, method, params, timestamp]` + - Request wrapper: `{req: [...], sig: [...]}` + - Response wrapper: `{res: [...], sig: [...]}` + - Error format: `{res: [requestId, "error", {error: "message"}, timestamp], sig: [...]}` + - Reference: [Message Format](./app-layer/off-chain/message-format) + +- [ ] **Handle network outages gracefully** + - Detect connection loss + - Queue pending requests + - Reconnect automatically + - Resubmit queued requests after reconnection + +### Authentication + +- [ ] **Implement 3-step flow (auth_request → auth_challenge → auth_verify)** + - Generate session keypair locally; never transmit the private key + - `auth_request`: public, unsigned; send address, session_key, allowances (optional), scope (optional, not enforced), **expires_at (required, ms)** + - Store the exact parameters; response method is `auth_challenge` with `challenge_message` + - `auth_verify`: sign EIP-712 Policy typed data with **main wallet** (not session key) including challenge, wallet, session_key, expires_at, scope, allowances; or pass `jwt` to reuse without signature + - Response returns `{address, session_key, jwt_token, success}`; use session key for all subsequent private calls + - Reference: [Authentication](./app-layer/off-chain/authentication) + +- [ ] **Session key management** + - Specify allowances per asset (unrestricted if omitted); enforce spending caps on every debit + - Set `expires_at`; re-authenticate before expiry; handle “session expired” errors + - Rotate/revoke session keys as needed; avoid reusing keys across applications + +- [ ] **Request signing & verification** + - Client signs all private RPC requests with session key; validate clearnode signatures on responses + - Ensure canonical JSON serialization of `req`/`res` arrays before hashing/signing + +### Method Implementation + +- [ ] **Implement all required methods** + - **Authentication**: auth_request, auth_verify + - **Channel Management**: create_channel, close_channel, resize_channel + - **Transfers**: transfer + - **App Sessions**: create_app_session, submit_app_state, close_app_session + - **Queries**: get_config, get_assets, get_app_definition, get_channels, get_app_sessions, get_ledger_balances, get_ledger_entries, get_ledger_transactions, get_rpc_history, get_user_tag, get_session_keys, ping + - Reference: [Queries](./app-layer/off-chain/queries) + +- [ ] **Handle server notifications** + - `bu` (Balance Update) - Balance changed + - `cu` (Channel Update) - Channel status changed + - `tr` (Transfer) - Incoming/outgoing transfer + - `asu` (App Session Update) - App session state changed + - Reference: [Notifications](./app-layer/off-chain/queries#notifications) + +:::tip Method Prioritization +Start with: authentication → create_channel → transfer → get_ledger_balances. Add other methods as needed for your use case. +::: + +--- + +## State Management + +Off-chain state tracking and synchronization. + +### State Storage + +- [ ] **Store latest signed states securely** + - Save complete state struct (data, allocations, sigs) + - Include channelId and version + - Persist to durable storage (database, filesystem) + - Implement atomic updates + +- [ ] **Track state versions** + - Maintain version counter per channel and app session + - Reject states with version ≤ current version + - Increment version for each new state + +- [ ] **Implement unified balance tracking** + - Aggregate funds across all chains + - Track funds in unified account vs channel escrow vs app session accounts + - Update on channel open/close and transfers + - Reference: [Transfers](./app-layer/off-chain/transfers) + +- [ ] **Handle app session state updates** + - Verify quorum met (sum of weights ≥ quorum) + - Track locked funds per session + - Release funds on session close + - Reference: [App Sessions](./app-layer/off-chain/app-sessions) + +### State Validation + +- [ ] **Verify signatures before accepting states** + - Check all required signatures present + - Validate each signature against expected signer + - Ensure quorum met for app sessions + +- [ ] **Validate state transitions** + - For channels: verify StateIntent (INITIALIZE, RESIZE, FINALIZE) + - For app sessions: verify quorum and allocation rules + - Verify version increments correctly + - For closure: allocations valid and complete + +- [ ] **Maintain state history** + - Keep N most recent states per channel + - Useful for dispute resolution + - Implement pruning strategy for old states + +--- + +## Security + +Critical security practices for production deployments. + +### Key Management + +- [ ] **Secure key storage** + - Never log private keys + - Use secure key storage (keychain, HSM, encrypted database) + - Implement key rotation + - Separate signing keys from storage keys + +- [ ] **Implement signature verification** + - Verify all incoming signatures + - Validate signer matches expected participant + - Check signature freshness (timestamp) + +- [ ] **Never share private keys or session key private keys** + - Session keys stay on client + - Never transmit private keys over network + - Use separate keys for different purposes + +### Challenge Monitoring + +- [ ] **Monitor blockchain for channel events** + - Subscribe to all channels you participate in + - Alert on `Challenged` events + - Automated response to challenges + +- [ ] **Respond to challenges within challenge period** + - Maintain latest valid state + - Submit newer state if challenged with old state + - Set alerts for challenge expiration + +- [ ] **Implement automated challenge response** + - Detect challenges automatically + - Submit newer state without manual intervention + - Fallback to manual response if needed + +### Session Key Management + +- [ ] **Session key allowance enforcement** + - Track spending per session key + - Reject operations exceeding allowance + - Alert user when approaching limit + +- [ ] **Validate spending limits client-side** + - Check allowance before submitting operations + - Provide clear error messages + - Offer to re-authenticate with higher allowance + +### Best Practices + +- [ ] **Never sign two states with same version number** + - Maintain version counter + - Reject duplicate versions + - Use atomic version increment + +- [ ] **Keep track of latest state you've signed** + - Store all signed states + - Never sign older version + - Use for dispute resolution + +- [ ] **Set appropriate challenge periods** + - Balance security (longer) vs UX (shorter) + - Consider block time and congestion + - Minimum: 1 hour (enforced by Custody Contract `MIN_CHALLENGE_PERIOD`) + +- [ ] **Validate all inputs thoroughly** + - Check address formats + - Verify amounts are positive + - Validate asset symbols + - Sanitize user input + +- [ ] **Log all state transitions for auditing** + - Timestamp all operations + - Record signatures and signers + - Maintain audit trail + - Implement log rotation + +--- + +## Error Handling + +Robust error handling for production reliability. + +### RPC Errors + +- [ ] **Handle error responses** + - Error response format: `{res: [requestId, "error", {error: "descriptive message"}, timestamp], sig: [...]}` + - No numeric error codes; errors have descriptive messages only + - Common errors: "authentication required", "insufficient balance", "channel not found", "session expired, please re-authenticate" + - Always check if response method is `"error"` + - Reference: [Error Handling](./protocol-reference#error-handling) + +### Transaction Errors + +- [ ] **Implement retry logic for critical operations** + - Exponential backoff + - Maximum retry attempts + - Idempotent operations + +- [ ] **Handle gas estimation failures** + - Provide manual gas limit option + - Retry with higher gas limit + - Alert user to potential issues + +- [ ] **Handle transaction reverts** + - Parse revert reason + - Provide helpful error messages + - Suggest corrective actions + +--- + +## Testing + +Comprehensive testing strategy for confidence in production. + +### Unit Testing + +- [ ] **Test signature generation and verification** + - Known test vectors + - Round-trip signing + - Invalid signature rejection + +- [ ] **Test identifier computation** + - channelId determinism + - packedState (payload hash) consistency + - Known test vectors + +- [ ] **Test state validation logic** + - Version ordering + - Allocation sum validation + - StateIntent validation (INITIALIZE, RESIZE, FINALIZE for channels) + +### Integration Testing + +- [ ] **Test both cooperative and challenge closure paths** + - Cooperative close (happy path) + - Challenge initiation + - Challenge response + - Challenge timeout + +- [ ] **Test multi-chain operations** + - Open channels on different chains + - Cross-chain transfers (via unified balance) + - Chain-specific edge cases + +- [ ] **Test network reconnection** + - Simulate network interruption + - Verify automatic reconnection + - Check state synchronization + +### End-to-End Testing + +- [ ] **Test complete user journeys** + - Authentication → Channel Open → Transfer → Channel Close + - App session creation → State updates → Closure + - Error scenarios and recovery + +- [ ] **Test with real clearnodes** + - Testnet deployment + - Mainnet staging environment + - Monitor performance and errors + +--- + +## Performance Optimization + +Optimize for production workloads. + +### Efficiency + +- [ ] **Minimize blockchain queries** + - Cache contract addresses + - Batch event queries + - Use multicall for multiple reads + +- [ ] **Implement connection pooling** + - Reuse RPC connections + - Pool blockchain RPC connections + - Implement connection limits + +- [ ] **Optimize state storage** + - Index by channelId and app_session_id + - Prune old states + - Compress stored states + +### Monitoring + +- [ ] **Implement health checks** + - RPC connection status + - Blockchain connection status + - Event listener status + - Use `ping` method for clearnode health + +- [ ] **Monitor latency** + - RPC request/response time + - Transaction confirmation time + - Event processing delay + +- [ ] **Track error rates** + - Failed transactions + - RPC errors + - Signature verification failures + +--- + +## Documentation + +Documentation for maintainability. + +### Code Documentation + +- [ ] **Document adjudicator-specific requirements clearly** + - State validation rules + - Version comparison logic + - Gas cost estimates + +- [ ] **Document custom state formats** + - Application-specific data structures + - Serialization format + - Version compatibility + +### User Documentation + +- [ ] **Provide integration guide** + - Setup instructions + - Code examples + - Common patterns + +- [ ] **Document error messages** + - User-friendly descriptions + - Suggested actions + - Support contact information + +--- + +## Deployment Checklist + +Pre-production validation. + +### Pre-Production + +- [ ] **Audit smart contracts thoroughly before deployment** + - Use established auditors + - Test on testnets first + - Gradual mainnet rollout + +- [ ] **Test on testnet extensively** + - All user flows + - Error scenarios + - Performance under load + +- [ ] **Implement monitoring and alerting** + - Error rate alerts + - Performance degradation alerts + - Challenge event alerts + +### Production + +- [ ] **Use appropriate challenge periods** + - Longer for high-value channels + - Consider network congestion + - Balance security vs UX + +- [ ] **Implement proper key management** + - Hardware security modules (HSM) + - Key rotation policy + - Backup and recovery procedures + +- [ ] **Set up incident response procedures** + - On-call rotation + - Escalation procedures + - Communication plan + +--- + +## Compliance Levels + +### Minimal (User Client) + +Essential for basic client functionality: +- Core Protocol Support ✓ +- On-Chain Integration (create, close) ✓ +- Off-Chain RPC (auth, transfer, basic queries) ✓ +- Basic Security ✓ + +### Standard (Production Application) + +Add: +- Complete method implementation ✓ +- State Management ✓ +- Comprehensive Error Handling ✓ +- Testing ✓ + +### Advanced (Clearnode Implementation) + +Add: +- Server-side RPC routing and authentication ✓ +- Event-driven architecture ✓ +- Unified balance management (double-entry ledger) ✓ +- App session coordination ✓ +- High availability and fault tolerance ✓ + +--- + +## Next Steps + +1. **Start Simple**: Implement Core Protocol + Basic On-Chain integration +2. **Add RPC**: Connect to clearnode, implement authentication and basic methods +3. **Enhance Security**: Implement all security best practices +4. **Test Thoroughly**: Unit, integration, and end-to-end tests +5. **Deploy Gradually**: Testnet → Staging → Production + +:::success Ready to Build +Use this checklist as a guide throughout your implementation. Check off items as you complete them and refer back to detailed documentation for each section. +::: + +--- + +## Resources + +- **Communication Flows**: [Communication Flows](./communication-flows) +- **Reference**: [Protocol Reference](./protocol-reference) +- **Channel Lifecycle**: [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle) +- **RPC Methods**: [Queries](./app-layer/off-chain/queries) +- **Example Code**: [Integration Tests](https://github.com/layer-3/nitrolite/tree/main/integration) diff --git a/versioned_docs/version-0.5.x/protocol/introduction.mdx b/versioned_docs/version-0.5.x/protocol/introduction.mdx new file mode 100644 index 0000000..6d5bdea --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/introduction.mdx @@ -0,0 +1,81 @@ +--- +sidebar_position: 1 +title: "Introduction" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Introduction + +## What is Yellow Protocol? + +Yellow Protocol is a layered system for decentralized clearing, settlement, and application hosting across multiple blockchains. It is developed and maintained by Layer3 Fintech Ltd. and operated by independent node operators running open-source clearnode software. + +The protocol is composed of two distinct layers: + +1. **Decentralized Layer** — A peer-to-peer overlay network (the Yellow Network Protocol, or YNP) that uses a Kademlia DHT, BLS threshold signatures, and elastic security to manage accounts, route transactions, and settle assets across chains. + +2. **App Layer (VirtualApp)** — A state channel protocol (also known as YApps) that enables off-chain interactions between participants with minimal on-chain operations. It forms a unified virtual ledger for applications to escrow funds while being fully abstracted from the underlying blockchain. + +:::info On-Chain Contracts +Both layers currently deploy their own on-chain smart contracts. These are expected to be merged into a unified contract suite once testnet concludes. +::: + +```mermaid +graph TB + subgraph DL["Decentralized Layer"] + direction TB + DHT["Kademlia DHT & Routing"] + CLUSTERS["BLS Threshold Clusters"] + ELASTIC["Elastic Security & Collateral"] + LIQUIDITY["Liquidity Layer & AMM"] + DL_CHAIN["Registry, Custody, Governor"] + end + + subgraph AL["App Layer (VirtualApp)"] + direction TB + OFFCHAIN["Off-Chain Nitro RPC"] + AL_CHAIN["Custody & Adjudicator Contracts"] + APPS["YApps (Chess, DEX, Payments, ...)"] + end + + subgraph L1["Blockchains"] + CHAINS["Ethereum, Arbitrum, Base, Polygon, ..."] + end + + DL --> AL + AL --> L1 + DL_CHAIN -.-> L1 + AL_CHAIN -.-> L1 + + style DL fill:#e1f5ff,stroke:#9ad7ff,color:#111 + style AL fill:#fff4e1,stroke:#ffd497,color:#111 + style L1 fill:#f0f0f0,stroke:#c9c9c9,color:#111 +``` + +## Design Goals + +- **Scalability**: Move high-frequency operations off-chain; throughput scales with the number of users +- **Cost Efficiency**: Minimize gas fees by reducing on-chain transactions to deposits, withdrawals, and disputes +- **Security**: Value-proportional security — the cost to corrupt a cluster always exceeds the value it protects +- **Interoperability**: Support multiple blockchains and assets through a unified clearing layer +- **Developer Experience**: Provide clear, implementable specifications for both layers + +## Specification Scope + +This documentation defines the Yellow Protocol in a **programming language-agnostic manner**. Implementers can use these specifications to build compliant implementations in any language (Go, Python, Rust, JavaScript, etc.). + +| Section | Layer | Covers | +|---------|-------|--------| +| Decentralized Layer | Network | DHT topology, cluster lifecycle, elastic security, protocol lifecycle, liquidity, security analysis (available in v1.x) | +| [App Layer — On-Chain](./app-layer/on-chain/overview) | VirtualApp | Smart contracts for fund custody, dispute resolution, and settlement | +| [App Layer — Off-Chain](./app-layer/off-chain/overview) | VirtualApp | Nitro RPC protocol for state channel operations, transfers, and app sessions | + +:::caution Language Independence +Implementation-specific details are referenced but not mandated by this specification. The protocol description is abstract and can be implemented in any programming language. +::: + +## RFC 2119 Keywords + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. diff --git a/versioned_docs/version-0.5.x/protocol/protocol-reference.mdx b/versioned_docs/version-0.5.x/protocol/protocol-reference.mdx new file mode 100644 index 0000000..7f02434 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/protocol-reference.mdx @@ -0,0 +1,343 @@ +--- +sidebar_position: 8 +title: Protocol Reference +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Protocol Reference + +Quick reference guide for protocol versions, constants, standards, and specifications. + +:::info Quick Navigation +Jump to a section: +- [Protocol Versions](#protocol-versions) - VirtualApp & Nitro RPC versions +- [State Intent System](#state-intent-system) - Channel state classification +- [Participant Indices](#participant-indices) - Creator & Clearnode positions +- [Channel Status](#channel-status-state-machine) - Status transitions +- [Signature Standards](#signature-standards) - On-chain & off-chain formats +- [EIP References](#eip-references) - Ethereum standards used +- [Protocol Constants](#protocol-constants) - Core constants +::: + +--- + +## Protocol Versions + +### VirtualApp Protocol + +| Property | Value | +|----------|-------| +| **Version** | 0.5.0 | +| **Status** | Mainnet deployments live; not production yet | +| **Compatibility** | EVM-compatible chains | + +**Supported Chains**: Ethereum, Polygon, Arbitrum One, Optimism, Base, and other EVM-compatible networks. + +### Nitro RPC Protocol + +| Version | Status | Features | +|---------|--------|----------| +| **0.2** | Legacy | Basic state updates only | +| **0.4** | Current | Intent system (OPERATE, DEPOSIT, WITHDRAW) | + +:::tip Version Recommendation +**Always use NitroRPC/0.4** for new implementations. Version 0.4 adds the intent system for app sessions, enabling dynamic fund management (deposits and withdrawals) within active sessions. +::: + +**Breaking Changes**: +- NitroRPC/0.4 introduces the `intent` parameter in `submit_app_state` +- NitroRPC/0.2 sessions cannot use DEPOSIT or WITHDRAW intents +- Protocol version is set during app session creation and cannot be changed + +--- + +## State Intent System + +Channel states are classified by `state.intent` (uint8) to signal their purpose. The Solidity enum defines: + +### StateIntent Enumeration + +```solidity +enum StateIntent { + OPERATE, // 0: Normal updates (challenge/checkpoint) + INITIALIZE, // 1: Channel funding/creation + RESIZE, // 2: In-place capacity change + FINALIZE // 3: Cooperative closure +} +``` + +### Intent Usage + +| Intent | Value | When Used | Method | +|--------|-------|-----------|---------| +| `INITIALIZE` | 1 | Channel creation | `Custody.create()` | +| `RESIZE` | 2 | Channel resize | `Custody.resize()` | +| `FINALIZE` | 3 | Cooperative closure | `Custody.close()` | +| `OPERATE` | 0 | Challenge/checkpoint | `Custody.challenge()`, `Custody.checkpoint()` | + +**Example**: +```javascript +// Creation state +state.intent = 1 // INITIALIZE +state.version = 0 +state.data = "0x" // Empty for basic channels + +// Closing state +state.intent = 3 // FINALIZE +state.version = currentVersion + 1 +state.data = "0x" +``` + +:::caution Intent Validation +Smart contracts validate the `intent` field to ensure proper channel lifecycle. Incorrect intent values will cause transactions to revert. +::: + +--- + +## Participant Indices + +In a standard payment channel, participants are identified by their array index. + +### Index 0: Creator (User) + +**Role**: Creator + +**Responsibilities**: +- Initiates channel creation +- Typically the one depositing funds +- First to sign states (`state.sigs[0]`) +- Calls `Custody.create()` on-chain + +**Example**: +```javascript +channel.participants[0] = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" // User +``` + +### Index 1: Clearnode + +**Role**: Service provider + +**Responsibilities**: +- Co-signs the initial state before on-chain `create()`; there is no separate `join()` call +- Provides off-chain services (Nitro RPC, unified balance management) +- Second to sign states (`state.sigs[1]`) + +**Example**: +```javascript +channel.participants[1] = "0x123456789abcdef0123456789abcdef012345678" // Clearnode +``` + +:::warning Signature Order Critical +Signatures array order **MUST** match participants array order. Mismatched signatures will cause transaction failures. + +```javascript +state.sigs[0] = creator_signature // Must be from participants[0] +state.sigs[1] = clearnode_signature // Must be from participants[1] +``` +::: + +--- + +## Channel Status State Machine + +Channel lifecycle is governed by status transitions. + +### Status Enumeration + +```solidity +enum Status { + VOID, // 0: Channel does not exist + INITIAL, // 1: Creation in progress, awaiting all participants + ACTIVE, // 2: Fully funded and operational + DISPUTE, // 3: Challenge period active + FINAL // 4: Ready to be closed and deleted +} +``` + +### State Transition Diagram + +```mermaid +stateDiagram-v2 + [*] --> VOID + VOID --> INITIAL : create() (creator only) + VOID --> ACTIVE : create() (all sigs present) + INITIAL --> ACTIVE : join() (remaining participants) + ACTIVE --> DISPUTE : challenge() + ACTIVE --> FINAL : close() (cooperative) + DISPUTE --> ACTIVE : checkpoint() (newer state) + DISPUTE --> FINAL : close() (after timeout) + FINAL --> [*] + + note right of VOID + Channel does not exist + on blockchain + end note + + note right of INITIAL + Creator has joined, + awaiting other participants + end note + + note right of ACTIVE + Operational state, + can perform off-chain updates + end note + + note right of DISPUTE + Challenge active, + parties can submit newer states + end note + + note right of FINAL + Ready for deletion, + funds distributed + end note +``` + +### Valid Transitions + +| From | To | Trigger | Notes | +|------|----|---------|---------| +| `VOID` | `INITIAL` | `create()` (creator only) | Legacy flow; awaiting other participants | +| `VOID` | `ACTIVE` | `create()` (all sigs present) | Current flow; both participants co-sign initial state | +| `INITIAL` | `ACTIVE` | `join()` | Remaining participants join | +| `ACTIVE` | `DISPUTE` | `challenge()` | Dispute initiated | +| `ACTIVE` | `FINAL` | `close()` | Cooperative closure | +| `DISPUTE` | `ACTIVE` | `checkpoint()` | Newer state accepted | +| `DISPUTE` | `FINAL` | `close()` | Challenge timeout | + +:::tip Quick Closure +The fastest way to close a channel is **ACTIVE → FINAL** via cooperative `close()`. This skips the challenge period entirely. +::: + +--- + +## Signature Standards + +### On-Chain Signatures (Solidity) + +Used in smart contract transactions (`create`, `join`, `close`, `challenge`, `resize`). + +**Format**: Variable-length byte arrays supporting multiple signature types (since v0.3.0) + +**Structure**: +```solidity +struct State { + // ... other fields ... + bytes[] sigs; // Array of signatures from participants +} +``` + +**Supported Types**: +- **ECDSA** (65 bytes): Standard signatures from EOA wallets +- **ERC-1271**: Smart contract wallet signatures +- **ERC-6492**: Counterfactual contract signatures (not yet deployed) + +**Hash**: Raw `packedState` (no EIP-191 prefix for chain-agnostic compatibility) + +**Example**: +```javascript +packedState = keccak256(abi.encode(channelId, state.intent, state.version, state.data, state.allocations)) +signature = sign(packedState, participantPrivateKey) // Raw hash, no prefix +``` + +### Off-Chain Signatures (Nitro RPC) + +Used in RPC requests and responses over RPC. + +**Format**: 0x-prefixed hex string (typically ECDSA from session keys) + +**Typical Length**: 65 bytes for ECDSA +- `r`: 32 bytes +- `s`: 32 bytes +- `v`: 1 byte + +**Representation**: 130 hex characters + `0x` prefix + +**Example**: +```javascript +signature = "0x1234567890abcdef...xyz" // 132 characters total (ECDSA) +``` + +**Computed Over**: +```javascript +rpcHash = keccak256(JSON.stringify(req)) +signature = sign(rpcHash, sessionPrivateKey) +``` + +:::info Session Key Signatures +Off-chain RPC signatures are typically ECDSA from session keys (EOA wallets), but the protocol supports other signature types for future flexibility. +::: + +:::caution Chain-Agnostic Signatures +On-chain signatures do NOT use EIP-191 or EIP-712 prefixes to maintain chain-agnostic compatibility. This differs from typical Ethereum signing patterns. Off-chain RPC signatures (e.g., authentication) DO use EIP-712 for better wallet UX. +::: + +--- + +## EIP References + +Ethereum Improvement Proposals referenced or used by the protocol. + +### EIP-191: Signed Data Standard + +**Status**: Not used in on-chain signatures (chain-agnostic design) +**Link**: https://eips.ethereum.org/EIPS/eip-191 + +**Why not used for on-chain**: On-chain signatures are computed over raw `packedState` hash without EIP-191 prefix to maintain compatibility across different EVM chains and potential non-EVM implementations. + +### EIP-712: Typed Structured Data Hashing + +**Status**: Used for off-chain RPC authentication +**Link**: https://eips.ethereum.org/EIPS/eip-712 + +**Usage**: Authentication flow uses EIP-712 typed data for signing the Policy structure (challenge, wallet, session_key, expires_at, scope, allowances) with the main wallet. This provides better wallet UX by displaying human-readable signing data. + +### EIP-1271: Contract Signature Validation + +**Status**: Supported by adjudicators +**Link**: https://eips.ethereum.org/EIPS/eip-1271 + +**Usage**: Enables smart contract wallets to sign state updates as participants. + +### EIP-20 (ERC-20): Token Standard + +**Status**: Required for all assets +**Link**: https://eips.ethereum.org/EIPS/eip-20 + +**Usage**: All assets must be ERC-20 compliant tokens. The Custody Contract uses `transferFrom` and `transfer` methods. + +:::note Standards Compliance +While the protocol references these EIPs, implementation details may vary. Always consult the specific smart contract code for authoritative behavior. +::: + +--- + +## Protocol Constants + +The only protocol-wide constants defined in code are: + +```solidity +uint256 constant PART_NUM = 2; // Channels are always 2-party +uint256 constant CLIENT_IDX = 0; // Client/creator participant index +uint256 constant SERVER_IDX = 1; // Server/clearnode participant index +``` + +All channel arrays (participants, allocations, sigs) and state validation logic rely on these indices and fixed participant count. + +--- + +## Next Steps + +Now that you have the complete protocol reference: + +1. **Terminology**: Review [Terminology](./terminology) for all term definitions +2. **Communication Flows**: See [Communication Flows](./communication-flows) for sequence diagrams +3. **Implementation Guide**: Follow [Implementation Checklist](./implementation-checklist) to build compliant clients +4. **Channel Lifecycle**: See [Channel Lifecycle](./app-layer/on-chain/channel-lifecycle) for detailed state transitions + +:::tip Reference Updates +This reference reflects protocol version 0.5.0. For the latest updates, check the [Nitrolite repository](https://github.com/layer-3/nitrolite) or use `get_config` to query clearnode capabilities dynamically. +::: diff --git a/versioned_docs/version-0.5.x/protocol/terminology.mdx b/versioned_docs/version-0.5.x/protocol/terminology.mdx new file mode 100644 index 0000000..c493f02 --- /dev/null +++ b/versioned_docs/version-0.5.x/protocol/terminology.mdx @@ -0,0 +1,79 @@ +--- +sidebar_position: 2 +title: "Terminology" +--- + +import Tooltip from '@site/src/components/Tooltip'; +import { tooltipDefinitions } from '@site/src/constants/tooltipDefinitions'; + +# Terminology + +## Core Concepts + +**Channel**: {tooltipDefinitions.channel} + +**State**: {tooltipDefinitions.channelState} + +**Participant**: {tooltipDefinitions.participant} + +**Clearnode**: {tooltipDefinitions.clearnode} + +**Creator**: {tooltipDefinitions.creatorParticipant} + +**App Sessions**: {tooltipDefinitions.appChannel} + +**Unified Balance**: {tooltipDefinitions.unifiedBalance} + +**Session Key**: {tooltipDefinitions.sessionKey} + +## Identifiers + +**channelId**: {tooltipDefinitions.channelId} + +**packedState**: {tooltipDefinitions.packedState} + +**requestId**: A unique identifier for an RPC request, used for correlating requests and responses formatted as a 0x-prefixed hex string (32 bytes). + +**appSessionId**: {tooltipDefinitions.appSessionId} Used for all subsequent operations on that specific app session. + +**accountId**: An identifier for an account or app session within the unified ledger. Can be either a 0x-prefixed hex string or a wallet address. + +**chainId**: {tooltipDefinitions.chainId} Examples: 1 (Ethereum Mainnet), 137 (Polygon), 8453 (Base), 42161 (Arbitrum One), 10 (Optimism). + +**assetSymbol**: {tooltipDefinitions.assetSymbol} Asset symbols are consistent across chains. + +**walletAddress**: {tooltipDefinitions.walletAddress} Used to identify participants in channels and app sessions. + +**userId**: Identifies a user after authentication to the Clearnode. Currently, this is always equivalent to the user's walletAddress. + +## On-Chain Contracts + +**Custody Contract**: The main on-chain contract implementing the VirtualApp protocol. It provides the functionality to lock and unlock funds; create, close and challenge a channel; track channel state, and coordinate with adjudicators to validate state transitions on state updates. + +**Adjudicator**: A smart contract that defines the rules for validating state transitions during all channel lifecycle operations. The adjudicator's `adjudicate(...)` function is called by the Custody contract to verify whether a new state is valid based on previous states and application-specific logic. Examples include SimpleConsensus (requires both signatures) and Remittance (only sender must sign). + +## Protocol Layers + +**Decentralized Layer (YNP)**: The peer-to-peer overlay network — Kademlia DHT, BLS threshold clusters, elastic security, and the liquidity layer. Manages account state, cross-shard transfers, and settlement. + +**App Layer (VirtualApp)**: The state channel protocol (also known as YApps) — on-chain custody and adjudicator contracts, plus the off-chain Nitro RPC protocol for fast state updates. + +**Nitro RPC**: The off-chain communication protocol used by the App Layer for channel operations, transfers, and app sessions. + +## Decentralized Layer Terms + +**NodeID**: A 256-bit identity derived from on-chain randomness at registration. Each NodeID requires collateral. + +**Signing Cluster (C_sign)**: The set of *k* nodes closest to an account's key in the DHT, responsible for BLS threshold signing. + +**Replication Set (C_watch)**: A larger ring of *r* nodes that independently verify signing cluster state. Always a superset of the signing cluster. + +**Certificate**: The protocol's universal primitive for state change — a cluster-attested operation carrying a BLS threshold signature. + +**Escrow Certificate**: A certificate authorizing a withdrawal from the network to L1. + +**Dispute Certificate**: A certificate produced by the replication set to override a fraudulent withdrawal. + +:::tip Quick Reference +These terms are used throughout the protocol specification. Bookmark this page for easy reference while reading other sections. +::: diff --git a/versioned_docs/version-0.5.x/tutorials/_category_.json b/versioned_docs/version-0.5.x/tutorials/_category_.json new file mode 100644 index 0000000..422afbb --- /dev/null +++ b/versioned_docs/version-0.5.x/tutorials/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Tutorials", + "position": 5, + "link": { + "type": "doc", + "id": "tutorials/index" + } +} \ No newline at end of file diff --git a/versioned_docs/version-0.5.x/tutorials/index.md b/versioned_docs/version-0.5.x/tutorials/index.md new file mode 100644 index 0000000..f6d7e76 --- /dev/null +++ b/versioned_docs/version-0.5.x/tutorials/index.md @@ -0,0 +1,13 @@ +--- +title: Tutorials +description: Step-by-step tutorials and guides +displayed_sidebar: tutorialsSidebar +--- + +# Tutorials + +:::info Work in Progress +This section is currently under development. Step-by-step tutorials and comprehensive guides will be available soon. +::: + +Coming soon: Interactive tutorials covering various development scenarios and use cases. \ No newline at end of file diff --git a/versioned_sidebars/version-0.5.x-sidebars.json b/versioned_sidebars/version-0.5.x-sidebars.json new file mode 100644 index 0000000..8158047 --- /dev/null +++ b/versioned_sidebars/version-0.5.x-sidebars.json @@ -0,0 +1,90 @@ +{ + "learnSidebar": [ + { + "type": "doc", + "id": "learn/index", + "label": "Learn" + }, + { + "type": "category", + "label": "Introduction", + "items": [ + "learn/introduction/what-yellow-solves", + "learn/introduction/architecture-at-a-glance", + "learn/introduction/supported-chains" + ], + "collapsible": false, + "collapsed": false + }, + { + "type": "category", + "label": "Getting Started", + "items": [ + "learn/getting-started/quickstart", + "learn/getting-started/prerequisites", + "learn/getting-started/key-terms" + ], + "collapsible": false, + "collapsed": false + }, + { + "type": "category", + "label": "Core Concepts", + "items": [ + "learn/core-concepts/state-channels-vs-l1-l2", + "learn/core-concepts/app-sessions", + "learn/core-concepts/session-keys", + "learn/core-concepts/challenge-response", + "learn/core-concepts/message-envelope" + ], + "collapsible": false, + "collapsed": false + }, + { + "type": "category", + "label": "Advanced", + "items": [ + "learn/advanced/managing-session-keys" + ], + "collapsible": false, + "collapsed": false + } + ], + "buildSidebar": [ + { + "type": "autogenerated", + "dirName": "build" + } + ], + "manualsSidebar": [ + { + "type": "autogenerated", + "dirName": "manuals" + } + ], + "tutorialsSidebar": [ + { + "type": "autogenerated", + "dirName": "tutorials" + } + ], + "guidesSidebar": [ + { + "type": "autogenerated", + "dirName": "guides" + } + ], + "apiSidebar": [ + { + "type": "autogenerated", + "dirName": "api-reference" + } + ], + "protocolSidebar": [ + { + "type": "autogenerated", + "dirName": "protocol" + } + ], + "defaultSidebar": [] +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..2ec90bd --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +[ + "0.5.x" +] From cd3f2f1da35cd04d97cb123ee73ddc954e788639 Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Fri, 6 Mar 2026 20:11:54 +0530 Subject: [PATCH 2/4] feat: restore Contracts navbar link, hide on 0.5.x via client module Adds Contracts back to the navbar using a path-based link (to avoid build failures when contracts don't exist in frozen 0.5.x). A tiny client module (hideContractsOn05x.js) hides the link when the user is browsing 0.5.x docs via onRouteDidUpdate. Made-with: Cursor --- docusaurus.config.ts | 9 +++++++++ src/clientModules/hideContractsOn05x.js | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/clientModules/hideContractsOn05x.js diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 61f3d68..31b37d6 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -92,6 +92,9 @@ const config: Config = { mermaid: true, }, themes: ['@docusaurus/theme-mermaid'], + clientModules: [ + './src/clientModules/hideContractsOn05x.js', + ], plugins: [ [ 'docusaurus-lunr-search', @@ -136,6 +139,12 @@ const config: Config = { label: 'Protocol', position: 'left', }, + { + to: '/docs/contracts', + label: 'Contracts', + position: 'left', + className: 'navbar-contracts-link', + }, { type: 'doc', docId: 'manuals/index', diff --git a/src/clientModules/hideContractsOn05x.js b/src/clientModules/hideContractsOn05x.js new file mode 100644 index 0000000..2b50d8e --- /dev/null +++ b/src/clientModules/hideContractsOn05x.js @@ -0,0 +1,22 @@ +const CONTRACTS_CLASS = 'navbar-contracts-link'; +const LEGACY_PATH = '/docs/0.5.x/'; + +function toggle() { + const el = document.querySelector(`.${CONTRACTS_CLASS}`)?.closest('.navbar__item'); + if (!el) return; + el.style.display = window.location.pathname.startsWith(LEGACY_PATH) ? 'none' : ''; +} + +if (typeof window !== 'undefined') { + toggle(); + + const observer = new MutationObserver(toggle); + observer.observe(document.querySelector('#__docusaurus') || document.body, { + childList: true, + subtree: true, + }); +} + +export function onRouteDidUpdate() { + toggle(); +} From ff1889ba88e628ff3157c1e5b3d78d1b192575a8 Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Fri, 6 Mar 2026 20:17:04 +0530 Subject: [PATCH 3/4] chore: add WIP banners to 12 learn pages not yet updated for v1 Adds a warning admonition to pages carried over from 0.5.x that still contain outdated terminology, code examples, or API references. The banner reads: "This page was carried over from the v0.5.x documentation and has not yet been fully updated for v1.x." Heavy rewrites: quickstart, prerequisites, key-terms, message-envelope, managing-session-keys Moderate updates: what-yellow-solves, architecture-at-a-glance, app-sessions, session-keys, challenge-response Minor updates: supported-chains, state-channels-vs-l1-l2 Pages already correct for v1 (no banner): yellow-token, index, all protocol-flows/* Made-with: Cursor --- docs/learn/advanced/managing-session-keys.mdx | 4 ++++ docs/learn/core-concepts/app-sessions.mdx | 4 ++++ docs/learn/core-concepts/challenge-response.mdx | 4 ++++ docs/learn/core-concepts/message-envelope.mdx | 4 ++++ docs/learn/core-concepts/session-keys.mdx | 4 ++++ docs/learn/core-concepts/state-channels-vs-l1-l2.mdx | 4 ++++ docs/learn/getting-started/key-terms.mdx | 4 ++++ docs/learn/getting-started/prerequisites.mdx | 4 ++++ docs/learn/getting-started/quickstart.mdx | 4 ++++ docs/learn/introduction/architecture-at-a-glance.mdx | 4 ++++ docs/learn/introduction/supported-chains.mdx | 4 ++++ docs/learn/introduction/what-yellow-solves.mdx | 4 ++++ 12 files changed, 48 insertions(+) diff --git a/docs/learn/advanced/managing-session-keys.mdx b/docs/learn/advanced/managing-session-keys.mdx index f1f4c31..83ddc33 100644 --- a/docs/learn/advanced/managing-session-keys.mdx +++ b/docs/learn/advanced/managing-session-keys.mdx @@ -8,6 +8,10 @@ keywords: [session keys, authentication, API, create, revoke, manage] 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. +::: + # Managing Session Keys This guide covers the operational details of creating, listing, and revoking session keys via the Clearnode API. diff --git a/docs/learn/core-concepts/app-sessions.mdx b/docs/learn/core-concepts/app-sessions.mdx index 9532ee8..f457ce7 100644 --- a/docs/learn/core-concepts/app-sessions.mdx +++ b/docs/learn/core-concepts/app-sessions.mdx @@ -8,6 +8,10 @@ keywords: [app sessions, multi-party, governance, quorum, NitroRPC] 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. +::: + # App Sessions App sessions are off-chain channels built on top of the unified balance that enable multi-party applications with custom governance rules. diff --git a/docs/learn/core-concepts/challenge-response.mdx b/docs/learn/core-concepts/challenge-response.mdx index 4729658..ab33d31 100644 --- a/docs/learn/core-concepts/challenge-response.mdx +++ b/docs/learn/core-concepts/challenge-response.mdx @@ -8,6 +8,10 @@ keywords: [challenge, dispute, security, settlement, fund recovery] 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. +::: + # Challenge-Response & Disputes In this guide, you will learn how Yellow Network resolves disputes and ensures your funds are always recoverable. diff --git a/docs/learn/core-concepts/message-envelope.mdx b/docs/learn/core-concepts/message-envelope.mdx index cd812ce..6260d5f 100644 --- a/docs/learn/core-concepts/message-envelope.mdx +++ b/docs/learn/core-concepts/message-envelope.mdx @@ -8,6 +8,10 @@ keywords: [Nitro RPC, message format, WebSocket, protocol, signatures] 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. +::: + # Message Envelope (RPC Protocol) In this guide, you will learn the essentials of how messages are structured and transmitted in Yellow Network. diff --git a/docs/learn/core-concepts/session-keys.mdx b/docs/learn/core-concepts/session-keys.mdx index 7a3b861..3312ff0 100644 --- a/docs/learn/core-concepts/session-keys.mdx +++ b/docs/learn/core-concepts/session-keys.mdx @@ -8,6 +8,10 @@ keywords: [session keys, authentication, signatures, allowances, security] 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. +::: + # Session Keys Session keys are delegated keys that enable applications to perform operations on behalf of a user's wallet with specified spending limits, permissions, and expiration times. They provide a secure way to grant limited access to applications without exposing the main wallet's private key. diff --git a/docs/learn/core-concepts/state-channels-vs-l1-l2.mdx b/docs/learn/core-concepts/state-channels-vs-l1-l2.mdx index 84199eb..b5f617c 100644 --- a/docs/learn/core-concepts/state-channels-vs-l1-l2.mdx +++ b/docs/learn/core-concepts/state-channels-vs-l1-l2.mdx @@ -8,6 +8,10 @@ keywords: [state channels, L1, L2, scaling, comparison, rollups, VirtualApp] 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. +::: + # State Channels vs L1/L2 In this guide, you will learn how state channels compare to Layer 1 and Layer 2 solutions, and when each approach is the right choice. diff --git a/docs/learn/getting-started/key-terms.mdx b/docs/learn/getting-started/key-terms.mdx index 2a8221a..a5596f7 100644 --- a/docs/learn/getting-started/key-terms.mdx +++ b/docs/learn/getting-started/key-terms.mdx @@ -8,6 +8,10 @@ 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. diff --git a/docs/learn/getting-started/prerequisites.mdx b/docs/learn/getting-started/prerequisites.mdx index 0462dfd..28f64f6 100644 --- a/docs/learn/getting-started/prerequisites.mdx +++ b/docs/learn/getting-started/prerequisites.mdx @@ -8,6 +8,10 @@ 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. diff --git a/docs/learn/getting-started/quickstart.mdx b/docs/learn/getting-started/quickstart.mdx index debe91d..c03ea1d 100644 --- a/docs/learn/getting-started/quickstart.mdx +++ b/docs/learn/getting-started/quickstart.mdx @@ -3,6 +3,10 @@ title: Quickstart description: Get up and running with the Yellow Network SDK in minutes. --- +:::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. diff --git a/docs/learn/introduction/architecture-at-a-glance.mdx b/docs/learn/introduction/architecture-at-a-glance.mdx index df980db..b0a2bbb 100644 --- a/docs/learn/introduction/architecture-at-a-glance.mdx +++ b/docs/learn/introduction/architecture-at-a-glance.mdx @@ -8,6 +8,10 @@ keywords: [architecture, state channels, VirtualApp, Clearnode, smart contracts] 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. +::: + # Architecture at a Glance In this guide, you will learn how Yellow Network's three protocol layers work together to enable fast, secure, off-chain transactions. diff --git a/docs/learn/introduction/supported-chains.mdx b/docs/learn/introduction/supported-chains.mdx index 55e0560..72ac7a1 100644 --- a/docs/learn/introduction/supported-chains.mdx +++ b/docs/learn/introduction/supported-chains.mdx @@ -8,6 +8,10 @@ keywords: [supported chains, blockchains, assets, tokens, USDC, Base, Polygon, E import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +:::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. +::: + # Supported Chains & Assets This page lists all blockchains and assets currently supported on Yellow Network. diff --git a/docs/learn/introduction/what-yellow-solves.mdx b/docs/learn/introduction/what-yellow-solves.mdx index 994980d..ae0e573 100644 --- a/docs/learn/introduction/what-yellow-solves.mdx +++ b/docs/learn/introduction/what-yellow-solves.mdx @@ -8,6 +8,10 @@ keywords: [Yellow Network, state channels, blockchain scaling, off-chain, Web3] 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. +::: + # What Yellow Solves In this guide, you will learn why Yellow Network exists, what problems it addresses, and how it provides a faster, cheaper way to build Web3 applications. Yellow Network is developed and maintained by Layer3 Fintech Ltd. and operated by independent node operators running the issuer's open-source software. From 098fa5710364b397776297f6d5f32d381282b303 Mon Sep 17 00:00:00 2001 From: Maharshi Mishra Date: Fri, 6 Mar 2026 20:35:41 +0530 Subject: [PATCH 4/4] fix: correct clearnode clone path, add version badge margin - Fix running-clearnode-locally.md clone instructions in both v1.x and 0.5.x: use layer-3/nitrolite repo with cd nitrolite/clearnode - Add bottom margin to version badge (.theme-doc-version-badge) Made-with: Cursor --- .gitignore | 1 + docs/manuals/running-clearnode-locally.md | 4 ++-- src/css/custom.css | 5 +++++ .../version-0.5.x/manuals/running-clearnode-locally.md | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3dabc9c..b580350 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ node_modules npm-debug.log* yarn-debug.log* yarn-error.log* +.vercel diff --git a/docs/manuals/running-clearnode-locally.md b/docs/manuals/running-clearnode-locally.md index 42912a4..cba7efc 100644 --- a/docs/manuals/running-clearnode-locally.md +++ b/docs/manuals/running-clearnode-locally.md @@ -12,8 +12,8 @@ This manual explains how to run a Clearnode locally using Docker Compose for dev ### 1. Clone the Repository ```bash -git clone https://github.com/erc7824/nitrolite.git -cd virtualapp/clearnode +git clone https://github.com/layer-3/nitrolite.git +cd nitrolite/clearnode ``` ### 2. Configuration Setup diff --git a/src/css/custom.css b/src/css/custom.css index f3fe887..b5e9667 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -1165,6 +1165,11 @@ pre.codeBlock_bY9V[style*="background-color"], border-left: 3px solid rgba(26, 255, 0, 0.6); } +/* Version badge spacing */ +.theme-doc-version-badge { + margin-bottom: 1rem !important; +} + /* Version Switcher Styles */ .navbar-version-dropdown { margin-right: 0.5rem; diff --git a/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md b/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md index 42912a4..cba7efc 100644 --- a/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md +++ b/versioned_docs/version-0.5.x/manuals/running-clearnode-locally.md @@ -12,8 +12,8 @@ This manual explains how to run a Clearnode locally using Docker Compose for dev ### 1. Clone the Repository ```bash -git clone https://github.com/erc7824/nitrolite.git -cd virtualapp/clearnode +git clone https://github.com/layer-3/nitrolite.git +cd nitrolite/clearnode ``` ### 2. Configuration Setup