diff --git a/.dockerignore b/.dockerignore
index d3be7426b..72c49dfbb 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,7 +7,6 @@ docker-compose.yml
LICENSE
contracts/
docs/
-erc7824-docs/
sdk/ts/
test/
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4e19d7391..b93cc0637 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,4 +1,4 @@
# CODEOWNERS: https://help.github.com/articles/about-codeowners/
# Yellow Network - Research and Development
-* @erc7824/yellow-network
+* @alessio @dimast-x @nksazonov @philanton @ihsraham
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f3edff838..65d4f6ba0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,7 +8,6 @@ updates:
- package-ecosystem: "npm" # See documentation for possible values
directories:
- "/sdk/ts"
- - "/erc7824-docs"
- "/sdk/compat"
- "/test/integration"
- "/sdk/ts/examples/*app*"
diff --git a/.github/workflows/main-pr.yml b/.github/workflows/main-pr.yml
index 6a7afc431..96d4b3ab3 100644
--- a/.github/workflows/main-pr.yml
+++ b/.github/workflows/main-pr.yml
@@ -57,22 +57,3 @@ jobs:
build-args: |
VERSION=${{ steps.sha.outputs.short_sha }}
- # TODO: Enable this job if docs preview are needed (do not forget to provide GITHUB_TOKEN and GH access to receive preview URLs).
- # https://stackoverflow.com/questions/75514653/firebase-action-hosting-deploy-fails-with-requesterror-resource-not-accessible
-
- # build-and-preview-docs-firebase:
- # name: Deploy to Firebase Hosting on PR
- # if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
- # runs-on: ubuntu-latest
- # steps:
- # - uses: actions/checkout@v4
-
- # - run: npm install && npm run build
- # working-directory: erc7824-docs
-
- # - uses: FirebaseExtended/action-hosting-deploy@v0
- # with:
- # # repoToken: ${{ secrets.GITHUB_TOKEN }}
- # firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }}
- # projectId: erc7824
- # entryPoint: ./erc7824-docs
diff --git a/.github/workflows/main-push.yml b/.github/workflows/main-push.yml
index 370ce4fd9..901393272 100644
--- a/.github/workflows/main-push.yml
+++ b/.github/workflows/main-push.yml
@@ -117,19 +117,3 @@ jobs:
# ${{github.event.head_commit.message}}
# SLACK_FOOTER: 'Nitrolite CI/CD Pipeline'
- # build-and-deploy-docs-firebase:
- # name: Deploy to Firebase Hosting on merge
- # runs-on: ubuntu-latest
- # steps:
- # - uses: actions/checkout@v6
-
- # - run: npm install && npm run build
- # working-directory: erc7824-docs
-
- # - uses: FirebaseExtended/action-hosting-deploy@v0
- # with:
- # # repoToken: ${{ secrets.GITHUB_TOKEN }}
- # firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }}
- # channelId: live
- # projectId: erc7824
- # entryPoint: ./erc7824-docs
diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml
index a302e7266..55ce9da2a 100644
--- a/.github/workflows/test-go.yml
+++ b/.github/workflows/test-go.yml
@@ -36,4 +36,4 @@ jobs:
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- slug: erc7824/nitrolite
+ slug: layer-3/nitrolite
diff --git a/.github/workflows/v1-push.yml b/.github/workflows/v1-push.yml
index 532debf23..93af694f7 100644
--- a/.github/workflows/v1-push.yml
+++ b/.github/workflows/v1-push.yml
@@ -160,20 +160,3 @@ jobs:
# ⚠️ RC build or deployment was cancelled!
# ${{github.event.head_commit.message}}
# SLACK_FOOTER: 'Nitrolite CI/CD Pipeline'
-
- # build-and-deploy-docs-firebase:
- # name: Deploy to Firebase Hosting on merge
- # runs-on: ubuntu-latest
- # steps:
- # - uses: actions/checkout@v4
-
- # - run: npm install && npm run build
- # working-directory: erc7824-docs
-
- # - uses: FirebaseExtended/action-hosting-deploy@v0
- # with:
- # # repoToken: ${{ secrets.GITHUB_TOKEN }}
- # firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ERC7824 }}
- # channelId: live
- # projectId: erc7824
- # entryPoint: ./erc7824-docs
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..daa7def30
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.prettierrc
+.prettierignore
+.vscode/
+.prettierrc
+.prettierignore
+.vscode/
+.idea/
+*.swp
+.DS_Store
+contracts/foundry.toml
+package-lock.json
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..bdb00ee51
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,22 @@
+{
+ "tabWidth": 2,
+ "useTabs": false,
+ "printWidth": 120,
+ "semi": true,
+ "singleQuote": false,
+ "trailingComma": "es5",
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "overrides": [
+ {
+ "files": "*.sol",
+ "options": {
+ "printWidth": 160,
+ "tabWidth": 4,
+ "useTabs": false,
+ "bracketSpacing": false
+ }
+ }
+ ],
+ "plugins": ["prettier-plugin-solidity"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..efb5435dc
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,22 @@
+{
+ "[solidity]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true,
+ "editor.tabSize": 2,
+ "editor.insertSpaces": true,
+ "editor.detectIndentation": false
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true,
+ "editor.tabSize": 2,
+ "editor.insertSpaces": true,
+ "editor.detectIndentation": false
+ },
+ "prettier.documentSelectors": ["**/*.sol"],
+ "prettier.enable": true
+}
diff --git a/LICENSE b/LICENSE
index 36f6d597a..20b1a81cf 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025, 2026 erc7824
+Copyright (c) 2025, 2026 layer-3
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 62256e191..627a21c2b 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/layer-3/release-process/blob/master/README.md)
-[](https://codecov.io/github/erc7824/nitrolite)
-[](https://pkg.go.dev/github.com/erc7824/nitrolite)
-[](https://github.com/erc7824/nitrolite/releases)
+[](https://codecov.io/github/layer-3/nitrolite)
+[](https://pkg.go.dev/github.com/layer-3/nitrolite)
+[](https://github.com/layer-3/nitrolite/releases)
# Nitrolite: State Channel Framework
@@ -87,7 +87,7 @@ See the [Clearnode Documentation](/clearnode/README.md) for more details.
The official Go SDK for building performant backend integrations or CLI tools.
```bash
-go get github.com/erc7824/nitrolite/sdk/go
+go get github.com/layer-3/nitrolite/sdk/go
```
See [SDK Go README](/sdk/go/README.md).
@@ -96,7 +96,7 @@ See [SDK Go README](/sdk/go/README.md).
The official TypeScript SDK for web-based applications.
```bash
-npm install @erc7824/nitrolite
+npm install @layer-3/nitrolite
```
See [SDK TS README](/sdk/ts/README.md).
diff --git a/cerebro/README.md b/cerebro/README.md
index bb330feed..e1f2622ac 100644
--- a/cerebro/README.md
+++ b/cerebro/README.md
@@ -12,7 +12,7 @@ go build -o clearnode-cli
Or install directly:
```bash
-go install github.com/erc7824/nitrolite/sdk/go/examples/cli@latest
+go install github.com/layer-3/nitrolite/sdk/go/examples/cli@latest
```
## Quick Start
diff --git a/cerebro/commands.go b/cerebro/commands.go
index 822ccb14e..a3b15fbdf 100644
--- a/cerebro/commands.go
+++ b/cerebro/commands.go
@@ -5,18 +5,30 @@ import (
"crypto/ecdsa"
"crypto/rand"
"fmt"
+ "os"
"strconv"
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/sign"
- sdk "github.com/erc7824/nitrolite/sdk/go"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/sign"
+ sdk "github.com/layer-3/nitrolite/sdk/go"
+ "golang.org/x/term"
)
+// readSecure reads a line from stdin without echo. Works under go-prompt's raw mode.
+func readSecure() string {
+ bytes, err := term.ReadPassword(int(os.Stdin.Fd()))
+ fmt.Println() // newline after hidden input
+ if err != nil {
+ return ""
+ }
+ return strings.TrimSpace(string(bytes))
+}
+
// ============================================================================
// Help & Config
// ============================================================================
@@ -26,15 +38,27 @@ func (o *Operator) showHelp() {
Clearnode CLI - SDK Development Tool
=====================================
-SETUP COMMANDS
- help Display this help message
- config Display current configuration
- wallet Display wallet address
- import wallet Configure wallet (import or generate)
- import rpc Configure blockchain RPC endpoint
-
-HIGH-LEVEL OPERATIONS (Smart Client)
- token-balance Check on-chain token balance for your wallet
+CONFIGURATION
+ config Display current configuration
+ config wallet Display wallet address
+ config wallet import Import existing private key
+ config wallet generate Generate new wallet
+ config wallet export Export private key to file
+ config rpc import Configure blockchain RPC endpoint
+ config node Show node info
+ config node set-ws-url Set clearnode WebSocket URL
+ config node set-home-blockchain Set home blockchain for channels
+ config session-key Show current session key info
+ config session-key generate Generate new session key
+ config session-key import Import existing session key
+ config session-key clear Clear session key, revert to default signer
+ config session-key register-channel-key Register channel session key
+ config session-key channel-keys List active channel session keys
+ config session-key register-app-key [apps] [sessions] Register app session key
+ config session-key app-keys List active app session keys
+
+OPERATIONS
+ token-balance Check on-chain token balance
approve Approve token spending for deposits
deposit Deposit to channel (auto-create if needed)
withdraw Withdraw from channel
@@ -43,52 +67,47 @@ HIGH-LEVEL OPERATIONS (Smart Client)
close-channel Close home channel on-chain
checkpoint Submit latest state on-chain
-NODE INFORMATION (Base Client)
+QUERIES
ping Test node connection
- node info Get node configuration
chains List supported blockchains
assets [chain_id] List supported assets (optionally filter by chain)
-
-USER QUERIES (Base Client)
balances [wallet] Get user balances (defaults to configured wallet)
- transactions [wallet] Get transaction history (defaults to configured wallet)
-
-LOW-LEVEL STATE MANAGEMENT (Base Client)
- state [wallet] Get latest state (wallet defaults to configured)
- home-channel [wallet] Get home channel (wallet defaults to configured)
+ transactions [wallet] Get transaction history
+ action-allowances [wallet] Get action allowances
+ state [wallet] Get latest state
+ home-channel [wallet] Get home channel
escrow-channel Get escrow channel by ID
-LOW-LEVEL APP SESSIONS (Base Client)
- app-sessions List app sessions
+APP REGISTRY
+ app-info Show application details
+ my-apps List your registered applications
+ register-app [no-approval] Register a new application
+ app-sessions List app sessions
-SESSION KEY MANAGEMENT
- generate-session-key Generate or import session key (stores locally)
- session-key Show current session key info
- clear-session-key Clear session key, revert to default wallet signer
- create-channel-session-key Register channel session key (auto-activates if stored)
- channel-session-keys List active channel session keys
- create-app-session-key [app_ids] [session_ids] Register app session key (IDs: comma-separated)
- app-session-keys List active app session keys
+SECURITY TOKEN OPERATIONS
+ security-token approve Approve security token spending
+ security-token balance [wallet] Check escrowed security token balance
+ security-token escrow [target_address] Escrow security tokens
+ security-token initiate-withdrawal Start unlock period
+ security-token cancel-withdrawal Cancel unlock and re-lock
+ security-token withdraw Withdraw unlocked security tokens
OTHER
+ help Display this help message
exit Exit the CLI
EXAMPLES
- import wallet
- import rpc 80002 https://polygon-amoy.g.alchemy.com/v2/KEY
+ config wallet import
+ config rpc import 80002 https://polygon-amoy.g.alchemy.com/v2/KEY
+ config session-key generate
+ config session-key register-channel-key 0xabcd... 24 usdc,weth
approve 80002 usdc 1000000
deposit 80002 usdc 100
transfer 0x1234... usdc 50
- balances # Uses configured wallet
- balances 0x1234... # Query specific wallet
- state usdc # Get state for USDC
- chains
- generate-session-key # Step 1: generate/import
- create-channel-session-key 0xabcd... 24 usdc,weth # Step 2: register + activate
- create-app-session-key 0xabcd... 24 app1,app2`)
+ balances`)
}
-func (o *Operator) showConfig(ctx context.Context) {
+func (o *Operator) showConfig() {
fmt.Println("Current Configuration")
fmt.Println("=====================")
@@ -136,14 +155,15 @@ func (o *Operator) showConfig(ctx context.Context) {
}
}
- // Node info
- nodeConfig, err := o.client.GetConfig(ctx)
- if err == nil {
- fmt.Printf("\nNode Info\n")
- fmt.Printf(" Address: %s\n", nodeConfig.NodeAddress)
- fmt.Printf(" Version: %s\n", nodeConfig.NodeVersion)
- fmt.Printf(" Chains: %d\n", len(nodeConfig.Blockchains))
- }
+ // Node connection
+ fmt.Printf("Node: %s\n", o.wsURL)
+
+ fmt.Println()
+ fmt.Println("Commands:")
+ fmt.Println(" config wallet Wallet management")
+ fmt.Println(" config rpc import Configure blockchain RPC")
+ fmt.Println(" config node Node info and connection")
+ fmt.Println(" config session-key Session key management")
}
// ============================================================================
@@ -155,7 +175,7 @@ func (o *Operator) showWallet(_ context.Context) {
privateKey, err := o.store.GetPrivateKey()
if err != nil {
fmt.Println("ERROR: No wallet configured")
- fmt.Println("INFO: Use 'import wallet' to configure wallet")
+ fmt.Println("INFO: Use 'config wallet import' to configure wallet")
return
}
@@ -173,101 +193,81 @@ func (o *Operator) showWallet(_ context.Context) {
fmt.Printf("Address: %s\n", address)
}
-// ============================================================================
-// Import Commands
-// ============================================================================
-
-func (o *Operator) importWallet(_ context.Context) {
- fmt.Println("Wallet Configuration")
- fmt.Println("====================")
- fmt.Println()
- fmt.Println("Choose an option:")
- fmt.Println(" 1. Import existing private key")
- fmt.Println(" 2. Generate new wallet")
- fmt.Println()
- fmt.Print("Enter choice (1 or 2): ")
+func (o *Operator) exportWallet(exportPath string) {
+ privateKey, err := o.store.GetPrivateKey()
+ if err != nil {
+ fmt.Println("ERROR: No wallet configured")
+ return
+ }
- var choice string
- fmt.Scanln(&choice)
- choice = strings.TrimSpace(choice)
+ if err := os.WriteFile(exportPath, []byte(privateKey+"\n"), 0600); err != nil {
+ fmt.Printf("ERROR: Failed to export wallet: %v\n", err)
+ return
+ }
- var privateKey string
- var signer sign.Signer
- var err error
+ fmt.Printf("SUCCESS: Private key exported to %s\n", exportPath)
+ fmt.Println("WARNING: Keep this file secure and do not share it with anyone.")
+}
- switch choice {
- case "1":
- // Import existing key
- fmt.Println()
- fmt.Println("Import Existing Wallet")
- fmt.Print("Enter private key (with or without 0x prefix): ")
- fmt.Scanln(&privateKey)
+// ============================================================================
+// Import Commands
+// ============================================================================
- privateKey = strings.TrimSpace(privateKey)
- if privateKey == "" {
- fmt.Println("ERROR: Private key cannot be empty")
- return
- }
+func (o *Operator) importWallet() {
+ fmt.Print("Enter private key (with or without 0x prefix): ")
+ privateKey := readSecure()
+ if privateKey == "" {
+ fmt.Println("ERROR: Private key cannot be empty")
+ return
+ }
- // Validate by creating signer
- signer, err = sign.NewEthereumRawSigner(privateKey)
- if err != nil {
- fmt.Printf("ERROR: Invalid private key: %v\n", err)
- return
- }
+ signer, err := sign.NewEthereumRawSigner(privateKey)
+ if err != nil {
+ fmt.Printf("ERROR: Invalid private key: %v\n", err)
+ return
+ }
- case "2":
- // Generate new wallet
- fmt.Println()
- fmt.Println("Generate New Wallet")
- privateKey, err = generatePrivateKey()
- if err != nil {
- fmt.Printf("ERROR: Failed to generate private key: %v\n", err)
- return
- }
+ if err := o.store.SetPrivateKey(privateKey); err != nil {
+ fmt.Printf("ERROR: Failed to save private key: %v\n", err)
+ return
+ }
- signer, err = sign.NewEthereumRawSigner(privateKey)
- if err != nil {
- fmt.Printf("ERROR: Failed to create signer: %v\n", err)
- return
- }
+ fmt.Printf("SUCCESS: Wallet configured\n")
+ fmt.Printf("Address: %s\n", signer.PublicKey().Address().String())
- fmt.Println()
- fmt.Println("WARNING: Save your private key securely!")
- fmt.Println("=========================================")
- fmt.Printf("Private Key: %s\n", privateKey)
- fmt.Println("=========================================")
- fmt.Println()
- fmt.Print("Type 'I have saved my private key' to continue: ")
+ fmt.Println("Reconnecting...")
+ if err := o.reconnect(); err != nil {
+ fmt.Printf("WARNING: Failed to reconnect: %v\n", err)
+ fmt.Println("INFO: Restart the CLI to apply changes.")
+ }
+}
- var confirmation string
- fmt.Scanln(&confirmation)
- // Read the full line
- if confirmation == "" {
- fmt.Println("ERROR: You must confirm that you saved the private key")
- return
- }
+func (o *Operator) generateWallet() {
+ privateKey, err := generatePrivateKey()
+ if err != nil {
+ fmt.Printf("ERROR: Failed to generate private key: %v\n", err)
+ return
+ }
- default:
- fmt.Println("ERROR: Invalid choice")
+ signer, err := sign.NewEthereumRawSigner(privateKey)
+ if err != nil {
+ fmt.Printf("ERROR: Failed to create signer: %v\n", err)
return
}
- // Save to storage
if err := o.store.SetPrivateKey(privateKey); err != nil {
fmt.Printf("ERROR: Failed to save private key: %v\n", err)
return
}
- fmt.Printf("SUCCESS: Wallet configured successfully\n")
+ fmt.Printf("SUCCESS: New wallet generated\n")
fmt.Printf("Address: %s\n", signer.PublicKey().Address().String())
+ fmt.Println("IMPORTANT: Run 'config wallet export' to save your private key to a file.")
- if choice == "2" {
- fmt.Println()
- fmt.Println("Security Recommendations:")
- fmt.Println(" - Store your private key in a secure location")
- fmt.Println(" - Never share your private key with anyone")
- fmt.Println(" - Consider using a hardware wallet for large amounts")
+ fmt.Println("Reconnecting...")
+ if err := o.reconnect(); err != nil {
+ fmt.Printf("WARNING: Failed to reconnect: %v\n", err)
+ fmt.Println("INFO: Restart the CLI to apply changes.")
}
}
@@ -282,9 +282,14 @@ func (o *Operator) importRPC(_ context.Context, chainIDStr, rpcURL string) {
fmt.Printf("ERROR: Failed to save RPC: %v\n", err)
return
}
- // TODO: add to SDK Client dynamically
fmt.Printf("SUCCESS: RPC configured for chain %d\n", chainID)
+
+ fmt.Println("Reconnecting...")
+ if err := o.reconnect(); err != nil {
+ fmt.Printf("WARNING: Failed to reconnect: %v\n", err)
+ fmt.Println("INFO: Restart the CLI to apply changes.")
+ }
}
func (o *Operator) setHomeBlockchain(_ context.Context, asset, chainIDStr string) {
@@ -339,7 +344,7 @@ func (o *Operator) tokenBalance(ctx context.Context, chainIDStr, asset string) {
wallet := o.getImportedWalletAddress()
if wallet == "" {
- fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.")
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
return
}
@@ -483,6 +488,7 @@ func (o *Operator) nodeInfo(ctx context.Context) {
fmt.Println("Node Information")
fmt.Println("================")
+ fmt.Printf("WS URL: %s\n", o.wsURL)
fmt.Printf("Address: %s\n", config.NodeAddress)
fmt.Printf("Version: %s\n", config.NodeVersion)
fmt.Printf("Chains: %d\n", len(config.Blockchains))
@@ -504,10 +510,30 @@ func (o *Operator) nodeInfo(ctx context.Context) {
fmt.Println("\nSupported Blockchains:")
for _, bc := range config.Blockchains {
fmt.Printf(" - %s (ID: %d)\n", bc.Name, bc.ID)
- fmt.Printf(" Contract: %s\n", bc.ChannelHubAddress)
+ fmt.Printf(" Channel Hub: %s\n", bc.ChannelHubAddress)
+ if bc.LockingContractAddress != "" {
+ fmt.Printf(" Locking: %s\n", bc.LockingContractAddress)
+ }
}
}
+func (o *Operator) setWSURL(wsURL string) {
+ if err := o.store.SetWSURL(wsURL); err != nil {
+ fmt.Printf("ERROR: Failed to save WebSocket URL: %v\n", err)
+ return
+ }
+
+ o.wsURL = wsURL
+ fmt.Printf("SUCCESS: WebSocket URL set to %s\n", wsURL)
+ fmt.Println("INFO: Reconnecting...")
+ if err := o.reconnect(); err != nil {
+ fmt.Printf("ERROR: Failed to reconnect: %v\n", err)
+ fmt.Println("INFO: URL saved. Restart the CLI to connect.")
+ return
+ }
+ fmt.Println("SUCCESS: Connected to new node")
+}
+
func (o *Operator) listChains(ctx context.Context) {
chains, err := o.client.GetBlockchains(ctx)
if err != nil {
@@ -714,6 +740,90 @@ func (o *Operator) listTransactions(ctx context.Context, wallet string) {
}
}
+func (o *Operator) getActionAllowances(ctx context.Context, wallet string) {
+ allowances, err := o.client.GetActionAllowances(ctx, wallet)
+ if err != nil {
+ fmt.Printf("ERROR: Failed to get action allowances: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Action Allowances for %s\n", wallet)
+ fmt.Println("========================================")
+ if len(allowances) == 0 {
+ fmt.Println("No action allowances found")
+ return
+ }
+
+ for _, a := range allowances {
+ fmt.Printf("- %s\n", a.GatedAction)
+ fmt.Printf(" Window: %s\n", a.TimeWindow)
+ fmt.Printf(" Used: %d / %d\n", a.Used, a.Allowance)
+ remaining := uint64(0)
+ if a.Allowance > a.Used {
+ remaining = a.Allowance - a.Used
+ }
+ fmt.Printf(" Remaining: %d\n", remaining)
+ }
+}
+
+// ============================================================================
+// App Registry
+// ============================================================================
+
+func (o *Operator) getApps(ctx context.Context, appID *string, ownerWallet *string) {
+ fmt.Println("Fetching registered applications...")
+
+ apps, _, err := o.client.GetApps(ctx, &sdk.GetAppsOptions{
+ AppID: appID,
+ OwnerWallet: ownerWallet,
+ })
+ if err != nil {
+ fmt.Printf("ERROR: Failed to get apps: %v\n", err)
+ return
+ }
+
+ if len(apps) == 0 {
+ fmt.Println("No applications found.")
+ return
+ }
+
+ fmt.Printf("Found %d application(s):\n\n", len(apps))
+ for _, a := range apps {
+ fmt.Printf(" App ID: %s\n", a.App.ID)
+ fmt.Printf(" Owner: %s\n", a.App.OwnerWallet)
+ fmt.Printf(" Version: %d\n", a.App.Version)
+ if a.App.CreationApprovalNotRequired {
+ fmt.Println(" Approval: Not required")
+ } else {
+ fmt.Println(" Approval: Required")
+ }
+ if a.App.Metadata != "" {
+ fmt.Printf(" Metadata: %s\n", a.App.Metadata)
+ }
+ fmt.Printf(" Created: %s\n", a.CreatedAt.Format("2006-01-02 15:04:05"))
+ fmt.Printf(" Updated: %s\n", a.UpdatedAt.Format("2006-01-02 15:04:05"))
+ fmt.Println()
+ }
+}
+
+func (o *Operator) registerApp(ctx context.Context, appID, metadata string, creationApprovalNotRequired bool) {
+ fmt.Printf("Registering application: %s...\n", appID)
+
+ err := o.client.RegisterApp(ctx, appID, metadata, creationApprovalNotRequired)
+ if err != nil {
+ fmt.Printf("ERROR: Failed to register app: %v\n", err)
+ return
+ }
+
+ fmt.Println("SUCCESS: Application registered")
+ fmt.Printf(" App ID: %s\n", appID)
+ if creationApprovalNotRequired {
+ fmt.Println(" Approval: Not required for session creation")
+ } else {
+ fmt.Println(" Approval: Required for session creation")
+ }
+}
+
// ============================================================================
// Low-Level State Management (Base Client)
// ============================================================================
@@ -774,10 +884,10 @@ func (o *Operator) listAppSessions(ctx context.Context, wallet string) {
for _, session := range sessions {
fmt.Printf("\n- Session %s\n", session.AppSessionID)
fmt.Printf(" Version: %d\n", session.Version)
- fmt.Printf(" Nonce: %d\n", session.Nonce)
- fmt.Printf(" Quorum: %d\n", session.Quorum)
+ fmt.Printf(" Nonce: %d\n", session.AppDefinition.Nonce)
+ fmt.Printf(" Quorum: %d\n", session.AppDefinition.Quorum)
fmt.Printf(" Closed: %v\n", session.IsClosed)
- fmt.Printf(" Participants: %d\n", len(session.Participants))
+ fmt.Printf(" Participants: %d\n", len(session.AppDefinition.Participants))
fmt.Printf(" Allocations: %d\n", len(session.Allocations))
}
}
@@ -786,50 +896,34 @@ func (o *Operator) listAppSessions(ctx context.Context, wallet string) {
// Session Key Management
// ============================================================================
-func (o *Operator) generateSessionKey(_ context.Context) {
- fmt.Println("Session Key Setup")
- fmt.Println("=================")
- fmt.Println()
- fmt.Println("Choose an option:")
- fmt.Println(" 1. Generate new session key")
- fmt.Println(" 2. Import existing private key")
- fmt.Println()
- fmt.Print("Enter choice (1 or 2): ")
-
- var choice string
- fmt.Scanln(&choice)
- choice = strings.TrimSpace(choice)
+func (o *Operator) generateSessionKey() {
+ privateKeyHex, err := generatePrivateKey()
+ if err != nil {
+ fmt.Printf("ERROR: Failed to generate session key: %v\n", err)
+ return
+ }
- var privateKeyHex string
- var err error
+ o.storeSessionKey(privateKeyHex)
+}
- switch choice {
- case "1":
- privateKeyHex, err = generatePrivateKey()
- if err != nil {
- fmt.Printf("ERROR: Failed to generate session key: %v\n", err)
- return
- }
- case "2":
- fmt.Print("Enter session key private key (hex): ")
- fmt.Scanln(&privateKeyHex)
- privateKeyHex = strings.TrimSpace(privateKeyHex)
- if privateKeyHex == "" {
- fmt.Println("ERROR: Private key cannot be empty")
- return
- }
- default:
- fmt.Println("ERROR: Invalid choice")
+func (o *Operator) importSessionKey() {
+ fmt.Print("Enter session key private key (hex): ")
+ privateKeyHex := readSecure()
+ if privateKeyHex == "" {
+ fmt.Println("ERROR: Private key cannot be empty")
return
}
+ o.storeSessionKey(privateKeyHex)
+}
+
+func (o *Operator) storeSessionKey(privateKeyHex string) {
signer, err := sign.NewEthereumRawSigner(privateKeyHex)
if err != nil {
fmt.Printf("ERROR: Invalid private key: %v\n", err)
return
}
- // Store the session key private key locally (no metadata yet — will be set on registration)
if err := o.store.SetSessionKeyPrivateKey(privateKeyHex); err != nil {
fmt.Printf("ERROR: Failed to store session key: %v\n", err)
return
@@ -837,17 +931,11 @@ func (o *Operator) generateSessionKey(_ context.Context) {
address := signer.PublicKey().Address().String()
- fmt.Println()
fmt.Println("SUCCESS: Session key stored locally")
fmt.Printf(" Address: %s\n", address)
- if choice == "1" {
- fmt.Printf(" Private Key: %s\n", privateKeyHex)
- fmt.Println()
- fmt.Println("WARNING: Save the private key securely!")
- }
fmt.Println()
fmt.Println("Next step: Register it on the clearnode with:")
- fmt.Printf(" create-channel-session-key %s \n", address)
+ fmt.Printf(" config session-key register-channel-key %s \n", address)
}
func (o *Operator) showSessionKey() {
@@ -857,7 +945,7 @@ func (o *Operator) showSessionKey() {
pk, pkErr := o.store.GetSessionKeyPrivateKey()
if pkErr != nil {
fmt.Println("No session key configured")
- fmt.Println("INFO: Use 'generate-session-key' to create one.")
+ fmt.Println("INFO: Use 'config session-key generate' to create one.")
return
}
signer, sigErr := sign.NewEthereumRawSigner(pk)
@@ -871,7 +959,7 @@ func (o *Operator) showSessionKey() {
fmt.Println("Status: Stored locally (not yet registered on clearnode)")
fmt.Println()
fmt.Println("Next step: Register it with:")
- fmt.Printf(" create-channel-session-key %s \n", signer.PublicKey().Address().String())
+ fmt.Printf(" config session-key register-channel-key %s \n", signer.PublicKey().Address().String())
return
}
@@ -918,7 +1006,7 @@ func (o *Operator) createChannelSessionKey(ctx context.Context, sessionKeyAddr,
wallet := o.getImportedWalletAddress()
if wallet == "" {
- fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.")
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
return
}
@@ -1043,7 +1131,7 @@ func (o *Operator) createAppSessionKey(ctx context.Context, sessionKeyAddr, expi
wallet := o.getImportedWalletAddress()
if wallet == "" {
- fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.")
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
return
}
@@ -1122,6 +1210,145 @@ func (o *Operator) listAppSessionKeys(ctx context.Context, wallet string) {
}
}
+// ============================================================================
+// Security Token Operations
+// ============================================================================
+
+func (o *Operator) escrowSecurityTokens(ctx context.Context, chainIDStr, targetAddress, amountStr string) {
+ chainID, err := o.parseChainID(chainIDStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ amount, err := o.parseAmount(amountStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ // Default target to own wallet if not specified
+ if targetAddress == "" {
+ targetAddress = o.getImportedWalletAddress()
+ if targetAddress == "" {
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
+ return
+ }
+ fmt.Printf("INFO: Using configured wallet as target: %s\n", targetAddress)
+ }
+
+ fmt.Printf("Escrowing %s security tokens for %s on chain %d...\n", amount.String(), targetAddress, chainID)
+
+ txHash, err := o.client.EscrowSecurityTokens(ctx, targetAddress, chainID, amount)
+ if err != nil {
+ fmt.Printf("ERROR: Escrow failed: %v\n", err)
+ return
+ }
+
+ fmt.Println("SUCCESS: Security tokens escrowed")
+ fmt.Printf("Transaction Hash: %s\n", txHash)
+}
+
+func (o *Operator) initiateSecurityWithdrawal(ctx context.Context, chainIDStr string) {
+ chainID, err := o.parseChainID(chainIDStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Initiating security tokens withdrawal on chain %d...\n", chainID)
+
+ txHash, err := o.client.InitiateSecurityTokensWithdrawal(ctx, chainID)
+ if err != nil {
+ fmt.Printf("ERROR: Initiate withdrawal failed: %v\n", err)
+ return
+ }
+
+ fmt.Println("SUCCESS: Security tokens withdrawal initiated")
+ fmt.Printf("Transaction Hash: %s\n", txHash)
+}
+
+func (o *Operator) cancelSecurityWithdrawal(ctx context.Context, chainIDStr string) {
+ chainID, err := o.parseChainID(chainIDStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Cancelling security tokens withdrawal on chain %d...\n", chainID)
+
+ txHash, err := o.client.CancelSecurityTokensWithdrawal(ctx, chainID)
+ if err != nil {
+ fmt.Printf("ERROR: Cancel withdrawal failed: %v\n", err)
+ return
+ }
+
+ fmt.Println("SUCCESS: Security tokens withdrawal cancelled (re-locked)")
+ fmt.Printf("Transaction Hash: %s\n", txHash)
+}
+
+func (o *Operator) withdrawSecurityTokens(ctx context.Context, chainIDStr, destination string) {
+ chainID, err := o.parseChainID(chainIDStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Withdrawing security tokens to %s on chain %d...\n", destination, chainID)
+
+ txHash, err := o.client.WithdrawSecurityTokens(ctx, chainID, destination)
+ if err != nil {
+ fmt.Printf("ERROR: Withdraw security tokens failed: %v\n", err)
+ return
+ }
+
+ fmt.Println("SUCCESS: Security tokens withdrawn")
+ fmt.Printf("Transaction Hash: %s\n", txHash)
+}
+
+func (o *Operator) approveSecurityToken(ctx context.Context, chainIDStr, amountStr string) {
+ chainID, err := o.parseChainID(chainIDStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ amount, err := o.parseAmount(amountStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Approving %s security tokens on chain %d...\n", amount.String(), chainID)
+
+ txHash, err := o.client.ApproveSecurityToken(ctx, chainID, amount)
+ if err != nil {
+ fmt.Printf("ERROR: Approve security token failed: %v\n", err)
+ return
+ }
+
+ fmt.Println("SUCCESS: Security token spending approved")
+ fmt.Printf("Transaction Hash: %s\n", txHash)
+}
+
+func (o *Operator) securityBalance(ctx context.Context, chainIDStr, wallet string) {
+ chainID, err := o.parseChainID(chainIDStr)
+ if err != nil {
+ fmt.Printf("ERROR: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Querying security token balance for %s on chain %d...\n", wallet, chainID)
+
+ balance, err := o.client.GetLockedBalance(ctx, chainID, wallet)
+ if err != nil {
+ fmt.Printf("ERROR: Failed to get security token balance: %v\n", err)
+ return
+ }
+
+ fmt.Printf("Security token balance: %s\n", balance.String())
+}
+
// ============================================================================
// Helper Methods
// ============================================================================
diff --git a/cerebro/main.go b/cerebro/main.go
index 52f907d3a..03b7c0a74 100644
--- a/cerebro/main.go
+++ b/cerebro/main.go
@@ -12,14 +12,11 @@ import (
)
func main() {
+ const defaultWSURL = "wss://clearnode-sandbox.yellow.org/v1/ws"
+
log.SetFlags(0)
log.SetPrefix("clearnode-cli: ")
log.SetOutput(os.Stderr)
- if len(os.Args) < 2 {
- log.Fatalf("Usage: clearnode-cli \nExample: clearnode-cli wss://clearnode.example.com/ws")
- }
-
- wsURL := os.Args[1]
// Get config directory
configDir := os.Getenv("CLEARNODE_CLI_CONFIG_DIR")
@@ -42,8 +39,18 @@ func main() {
log.Fatalf("failed to initialize storage: %v", err)
}
+ // Determine WebSocket URL: CLI arg > stored > default
+ var wsURL string
+ if len(os.Args) >= 2 {
+ wsURL = os.Args[1]
+ } else if stored, err := store.GetWSURL(); err == nil {
+ wsURL = stored
+ } else {
+ wsURL = defaultWSURL
+ }
+
// Create operator
- operator, err := NewOperator(wsURL, store)
+ operator, err := NewOperator(wsURL, configDir, store)
if err != nil {
log.Fatalf("failed to create operator: %v", err)
}
diff --git a/cerebro/operator.go b/cerebro/operator.go
index 47d7bf738..8b2a08835 100644
--- a/cerebro/operator.go
+++ b/cerebro/operator.go
@@ -8,24 +8,26 @@ import (
"time"
"github.com/c-bata/go-prompt"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/sign"
- sdk "github.com/erc7824/nitrolite/sdk/go"
+ "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"
)
type Operator struct {
- wsURL string
- store *Storage
- client *sdk.Client
- exitCh chan struct{}
+ wsURL string
+ configDir string
+ store *Storage
+ client *sdk.Client
+ exitCh chan struct{}
}
-func NewOperator(wsURL string, store *Storage) (*Operator, error) {
+func NewOperator(wsURL, configDir string, store *Storage) (*Operator, error) {
op := &Operator{
- wsURL: wsURL,
- store: store,
- exitCh: make(chan struct{}),
+ wsURL: wsURL,
+ configDir: configDir,
+ store: store,
+ exitCh: make(chan struct{}),
}
if err := op.connect(); err != nil {
@@ -69,10 +71,29 @@ func (o *Operator) buildStateSigner(walletPrivateKey string) (core.ChannelSigner
}
// connect creates the SDK client with the appropriate signer.
+// If no wallet is configured, a new one is automatically generated.
func (o *Operator) connect() error {
privateKey, err := o.store.GetPrivateKey()
if err != nil {
- return fmt.Errorf("no wallet imported (use 'import wallet' first): %w", err)
+ // Auto-generate a new wallet for first-time users
+ privateKey, err = generatePrivateKey()
+ if err != nil {
+ return fmt.Errorf("failed to generate wallet: %w", err)
+ }
+ if err := o.store.SetPrivateKey(privateKey); err != nil {
+ return fmt.Errorf("failed to save generated wallet: %w", err)
+ }
+ signer, err := sign.NewEthereumRawSigner(privateKey)
+ if err != nil {
+ return fmt.Errorf("failed to create signer: %w", err)
+ }
+ fmt.Println()
+ fmt.Println("Welcome! No wallet imported. A new wallet has been generated for you.")
+ fmt.Printf("Address: %s\n", signer.PublicKey().Address().String())
+ fmt.Println()
+ fmt.Println("IMPORTANT: Run 'config wallet export' to save your private key to a file.")
+ fmt.Println("INFO: You can import a different wallet anytime with 'config wallet import'.")
+ fmt.Println()
}
stateSigner, err := o.buildStateSigner(privateKey)
@@ -144,10 +165,7 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{
// Setup
{Text: "help", Description: "Show help information"},
- {Text: "config", Description: "Show current configuration"},
- {Text: "wallet", Description: "Show wallet address"},
- {Text: "import", Description: "Import wallet or blockchain RPC"},
- {Text: "set-home-blockchain", Description: "Set home blockchain for channels"},
+ {Text: "config", Description: "Configuration (wallet, rpc, node, session-key)"},
// High-level operations
{Text: "token-balance", Description: "Check on-chain token balance"},
@@ -161,49 +179,57 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest {
// Node information
{Text: "ping", Description: "Test node connection"},
- {Text: "node", Description: "Get node information"},
{Text: "chains", Description: "List supported blockchains"},
{Text: "assets", Description: "List supported assets"},
// User queries
{Text: "balances", Description: "Get user balances"},
{Text: "transactions", Description: "Get transaction history"},
+ {Text: "action-allowances", Description: "Get action allowances"},
// State management
{Text: "state", Description: "Get latest state"},
{Text: "home-channel", Description: "Get home channel"},
{Text: "escrow-channel", Description: "Get escrow channel"},
+ // App registry
+ {Text: "app-info", Description: "Show application details"},
+ {Text: "my-apps", Description: "List your registered applications"},
+ {Text: "register-app", Description: "Register a new application"},
+
// App sessions (Base Client - Low-level)
{Text: "app-sessions", Description: "List app sessions"},
- // Session key management
- {Text: "generate-session-key", Description: "Generate or import session key (stores locally)"},
- {Text: "session-key", Description: "Show current session key info"},
- {Text: "clear-session-key", Description: "Clear session key, revert to default signer"},
- {Text: "create-channel-session-key", Description: "Register channel session key"},
- {Text: "channel-session-keys", Description: "List active channel session keys"},
- {Text: "create-app-session-key", Description: "Register app session key"},
- {Text: "app-session-keys", Description: "List active app session keys"},
+ // Security token operations
+ {Text: "security-token", Description: "Security token operations"},
- {Text: "exit", Description: "Exit the CLI"},
+{Text: "exit", Description: "Exit the CLI"},
}
}
// Second level
if len(args) < 3 {
switch args[0] {
- case "import":
+ case "config":
return []prompt.Suggest{
- {Text: "wallet", Description: "Import private key for signing"},
- {Text: "rpc", Description: "Import blockchain RPC URL"},
+ {Text: "wallet", Description: "Wallet management"},
+ {Text: "rpc", Description: "RPC management"},
+ {Text: "node", Description: "Node info and connection"},
+ {Text: "session-key", Description: "Session key management"},
}
- case "set-home-blockchain", "close-channel", "acknowledge", "checkpoint":
- // set-home-blockchain , others take
+ case "close-channel", "acknowledge", "checkpoint":
return o.getAssetSuggestions()
case "token-balance", "approve", "deposit", "withdraw":
- // token-balance , approve/deposit/withdraw
return o.getChainSuggestions()
+ case "security-token":
+ return []prompt.Suggest{
+ {Text: "approve", Description: "Approve security token spending"},
+ {Text: "balance", Description: "Check escrowed security token balance"},
+ {Text: "escrow", Description: "Escrow security tokens"},
+ {Text: "initiate-withdrawal", Description: "Start unlock period"},
+ {Text: "cancel-withdrawal", Description: "Cancel unlock and re-lock"},
+ {Text: "withdraw", Description: "Withdraw unlocked security tokens"},
+ }
case "state", "home-channel":
// state [wallet] , home-channel [wallet]
// Suggest asset first (common case), wallet can be typed manually
@@ -211,27 +237,44 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest {
case "transfer":
// transfer
return o.getWalletSuggestion()
- case "balances", "transactions":
+ case "balances", "transactions", "action-allowances":
return o.getWalletSuggestion()
case "assets":
return o.getChainSuggestions()
- case "node":
- return []prompt.Suggest{
- {Text: "info", Description: "Get node configuration"},
- }
}
}
// Third level
if len(args) < 4 {
switch args[0] {
- case "import":
- if args[1] == "rpc" {
- return o.getChainSuggestions()
+ case "config":
+ switch args[1] {
+ case "wallet":
+ return []prompt.Suggest{
+ {Text: "import", Description: "Import existing private key"},
+ {Text: "generate", Description: "Generate new wallet"},
+ {Text: "export", Description: "Export private key to file"},
+ }
+ case "rpc":
+ return []prompt.Suggest{
+ {Text: "import", Description: "Import blockchain RPC URL"},
+ }
+ case "node":
+ return []prompt.Suggest{
+ {Text: "set-ws-url", Description: "Set clearnode WebSocket URL"},
+ {Text: "set-home-blockchain", Description: "Set home blockchain for channels"},
+ }
+ case "session-key":
+ return []prompt.Suggest{
+ {Text: "generate", Description: "Generate new session key"},
+ {Text: "import", Description: "Import existing session key"},
+ {Text: "clear", Description: "Clear session key, revert to default signer"},
+ {Text: "register-channel-key", Description: "Register channel session key"},
+ {Text: "channel-keys", Description: "List active channel session keys"},
+ {Text: "register-app-key", Description: "Register app session key"},
+ {Text: "app-keys", Description: "List active app session keys"},
+ }
}
- case "set-home-blockchain":
- // set-home-blockchain
- return o.getChainSuggestions()
case "token-balance":
// token-balance
return o.getAssetSuggestions()
@@ -244,6 +287,9 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest {
case "state", "home-channel":
// state [wallet] — if wallet was explicitly provided, suggest asset
return o.getAssetSuggestions()
+ case "security-token":
+ // security-token ...
+ return o.getChainSuggestions()
case "escrow-channel":
// Escrow channel ID (no suggestion)
return nil
@@ -253,9 +299,24 @@ func (o *Operator) complete(d prompt.Document) []prompt.Suggest {
// Fourth level
if len(args) < 5 {
switch args[0] {
- case "create-channel-session-key":
- // Fourth arg is assets (comma-separated)
- return o.getAssetSuggestions()
+ case "config":
+ switch args[1] {
+ case "rpc":
+ if args[2] == "import" {
+ return o.getChainSuggestions()
+ }
+ case "node":
+ if args[2] == "set-home-blockchain" {
+ return o.getAssetSuggestions()
+ }
+ }
+ }
+ }
+
+ // Fifth level
+ if len(args) < 6 {
+ if args[0] == "config" && args[1] == "node" && args[2] == "set-home-blockchain" {
+ return o.getChainSuggestions()
}
}
@@ -275,33 +336,125 @@ func (o *Operator) Execute(s string) {
case "help":
o.showHelp()
case "config":
- o.showConfig(ctx)
- case "wallet":
- o.showWallet(ctx)
- case "import":
if len(args) < 2 {
- fmt.Println("ERROR: Usage: import ...")
+ o.showConfig()
return
}
switch args[1] {
case "wallet":
- o.importWallet(ctx)
+ if len(args) < 3 {
+ o.showWallet(ctx)
+ return
+ }
+ switch args[2] {
+ case "import":
+ o.importWallet()
+ case "generate":
+ o.generateWallet()
+ case "export":
+ if len(args) < 4 {
+ fmt.Println("ERROR: Usage: config wallet export ")
+ return
+ }
+ o.exportWallet(args[3])
+ default:
+ fmt.Printf("ERROR: Unknown wallet command: %s\n", args[2])
+ fmt.Println("Usage: config wallet [import|generate|export]")
+ }
case "rpc":
- if len(args) < 4 {
- fmt.Println("ERROR: Usage: import rpc ")
+ if len(args) < 3 {
+ fmt.Println("ERROR: Usage: config rpc import ")
return
}
- o.importRPC(ctx, args[2], args[3])
+ switch args[2] {
+ case "import":
+ if len(args) < 5 {
+ fmt.Println("ERROR: Usage: config rpc import ")
+ return
+ }
+ o.importRPC(ctx, args[3], args[4])
+ default:
+ fmt.Printf("ERROR: Unknown rpc command: %s\n", args[2])
+ fmt.Println("Usage: config rpc [import]")
+ }
+ case "node":
+ if len(args) < 3 {
+ o.nodeInfo(ctx)
+ return
+ }
+ switch args[2] {
+ case "set-ws-url":
+ if len(args) < 4 {
+ fmt.Println("ERROR: Usage: config node set-ws-url ")
+ return
+ }
+ o.setWSURL(args[3])
+ case "set-home-blockchain":
+ if len(args) < 5 {
+ fmt.Println("ERROR: Usage: config node set-home-blockchain ")
+ return
+ }
+ o.setHomeBlockchain(ctx, args[3], args[4])
+ default:
+ fmt.Printf("ERROR: Unknown node command: %s\n", args[2])
+ fmt.Println("Usage: config node [set-ws-url|set-home-blockchain]")
+ }
+ case "session-key":
+ if len(args) < 3 {
+ o.showSessionKey()
+ return
+ }
+ switch args[2] {
+ case "generate":
+ o.generateSessionKey()
+ case "import":
+ o.importSessionKey()
+ case "clear":
+ o.clearSessionKey()
+ case "register-channel-key":
+ if len(args) < 6 {
+ fmt.Println("ERROR: Usage: config session-key register-channel-key ")
+ fmt.Println("INFO: Assets are comma-separated, e.g. usdc,weth")
+ return
+ }
+ o.createChannelSessionKey(ctx, args[3], args[4], args[5])
+ case "channel-keys":
+ wallet := o.getImportedWalletAddress()
+ if wallet == "" {
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
+ return
+ }
+ o.listChannelSessionKeys(ctx, wallet)
+ case "register-app-key":
+ if len(args) < 5 {
+ fmt.Println("ERROR: Usage: config session-key register-app-key [app_ids] [session_ids]")
+ fmt.Println("INFO: IDs are comma-separated. app_ids and session_ids are optional.")
+ return
+ }
+ appIDs := ""
+ sessionIDs := ""
+ if len(args) >= 6 {
+ appIDs = args[5]
+ }
+ if len(args) >= 7 {
+ sessionIDs = args[6]
+ }
+ o.createAppSessionKey(ctx, args[3], args[4], appIDs, sessionIDs)
+ case "app-keys":
+ wallet := o.getImportedWalletAddress()
+ if wallet == "" {
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
+ return
+ }
+ o.listAppSessionKeys(ctx, wallet)
+ default:
+ fmt.Printf("ERROR: Unknown session-key command: %s\n", args[2])
+ fmt.Println("Usage: config session-key [generate|import|clear|register-channel-key|channel-keys|register-app-key|app-keys]")
+ }
default:
- fmt.Printf("ERROR: Unknown import type: %s\n", args[1])
+ fmt.Printf("ERROR: Unknown config command: %s\n", args[1])
+ fmt.Println("Usage: config [wallet|rpc|node|session-key]")
}
-
- case "set-home-blockchain":
- if len(args) < 3 {
- fmt.Println("ERROR: Usage: set-home-blockchain ")
- return
- }
- o.setHomeBlockchain(ctx, args[1], args[2])
// High-level operations
case "token-balance":
if len(args) < 3 {
@@ -355,10 +508,6 @@ func (o *Operator) Execute(s string) {
// Node information
case "ping":
o.ping(ctx)
- case "node":
- if len(args) < 2 || args[1] == "info" {
- o.nodeInfo(ctx)
- }
case "chains":
o.listChains(ctx)
case "assets":
@@ -378,7 +527,7 @@ func (o *Operator) Execute(s string) {
wallet = o.getImportedWalletAddress()
if wallet == "" {
fmt.Println("ERROR: Usage: balances ")
- fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.")
+ fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.")
return
}
fmt.Printf("INFO: Using configured wallet: %s\n", wallet)
@@ -393,7 +542,7 @@ func (o *Operator) Execute(s string) {
wallet = o.getImportedWalletAddress()
if wallet == "" {
fmt.Println("ERROR: Usage: transactions ")
- fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.")
+ fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.")
return
}
fmt.Printf("INFO: Using configured wallet: %s\n", wallet)
@@ -412,7 +561,7 @@ func (o *Operator) Execute(s string) {
wallet = o.getImportedWalletAddress()
if wallet == "" {
fmt.Println("ERROR: Usage: state ")
- fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.")
+ fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.")
return
}
asset = args[1]
@@ -434,7 +583,7 @@ func (o *Operator) Execute(s string) {
wallet = o.getImportedWalletAddress()
if wallet == "" {
fmt.Println("ERROR: Usage: home-channel ")
- fmt.Println("INFO: No wallet configured. Use 'import wallet' first or specify a wallet address.")
+ fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.")
return
}
asset = args[1]
@@ -452,54 +601,117 @@ func (o *Operator) Execute(s string) {
}
o.getEscrowChannel(ctx, args[1])
- // App sessions
- case "app-sessions":
- wallet := o.getImportedWalletAddress()
- o.listAppSessions(ctx, wallet)
-
- // Session key management
- case "generate-session-key":
- o.generateSessionKey(ctx)
- case "session-key":
- o.showSessionKey()
- case "clear-session-key":
- o.clearSessionKey()
- case "create-channel-session-key":
- if len(args) < 4 {
- fmt.Println("ERROR: Usage: create-channel-session-key ")
- fmt.Println("INFO: Assets are comma-separated, e.g. usdc,weth")
+ // App registry
+ case "app-info":
+ if len(args) < 2 {
+ fmt.Println("ERROR: Usage: app-info ")
return
}
- o.createChannelSessionKey(ctx, args[1], args[2], args[3])
- case "channel-session-keys":
+ o.getApps(ctx, &args[1], nil)
+
+ case "my-apps":
wallet := o.getImportedWalletAddress()
if wallet == "" {
- fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.")
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first.")
return
}
- o.listChannelSessionKeys(ctx, wallet)
- case "create-app-session-key":
- if len(args) < 3 {
- fmt.Println("ERROR: Usage: create-app-session-key [app_ids] [session_ids]")
- fmt.Println("INFO: IDs are comma-separated. app_ids and session_ids are optional.")
+ o.getApps(ctx, nil, &wallet)
+
+ case "register-app":
+ if len(args) < 2 {
+ fmt.Println("ERROR: Usage: register-app [no-approval]")
+ fmt.Println("INFO: Pass 'no-approval' as second arg to allow session creation without owner approval")
return
}
- appIDs := ""
- sessionIDs := ""
- if len(args) >= 4 {
- appIDs = args[3]
- }
- if len(args) >= 5 {
- sessionIDs = args[4]
+ noApproval := len(args) >= 3 && args[2] == "no-approval"
+ o.registerApp(ctx, args[1], "", noApproval)
+
+ // User action allowances
+ case "action-allowances":
+ wallet := ""
+ if len(args) >= 2 {
+ wallet = args[1]
+ } else {
+ wallet = o.getImportedWalletAddress()
+ if wallet == "" {
+ fmt.Println("ERROR: Usage: action-allowances ")
+ fmt.Println("INFO: No wallet configured. Use 'config wallet import' first or specify a wallet address.")
+ return
+ }
+ fmt.Printf("INFO: Using configured wallet: %s\n", wallet)
}
- o.createAppSessionKey(ctx, args[1], args[2], appIDs, sessionIDs)
- case "app-session-keys":
+ o.getActionAllowances(ctx, wallet)
+
+ // App sessions
+ case "app-sessions":
wallet := o.getImportedWalletAddress()
- if wallet == "" {
- fmt.Println("ERROR: No wallet configured. Use 'import wallet' first.")
+ o.listAppSessions(ctx, wallet)
+
+
+ // Security token operations
+ case "security-token":
+ if len(args) < 2 {
+ fmt.Println("ERROR: Usage: security-token ...")
+ fmt.Println("Commands: approve, balance, escrow, initiate-withdrawal, cancel-withdrawal, withdraw")
return
}
- o.listAppSessionKeys(ctx, wallet)
+ switch args[1] {
+ case "approve":
+ if len(args) < 4 {
+ fmt.Println("ERROR: Usage: security-token approve ")
+ return
+ }
+ o.approveSecurityToken(ctx, args[2], args[3])
+ case "escrow":
+ if len(args) < 4 {
+ fmt.Println("ERROR: Usage: security-token escrow [target_address] ")
+ fmt.Println("INFO: If target_address is omitted, your own wallet is used.")
+ return
+ }
+ if len(args) >= 5 {
+ o.escrowSecurityTokens(ctx, args[2], args[3], args[4])
+ } else {
+ o.escrowSecurityTokens(ctx, args[2], "", args[3])
+ }
+ case "initiate-withdrawal":
+ if len(args) < 3 {
+ fmt.Println("ERROR: Usage: security-token initiate-withdrawal ")
+ return
+ }
+ o.initiateSecurityWithdrawal(ctx, args[2])
+ case "cancel-withdrawal":
+ if len(args) < 3 {
+ fmt.Println("ERROR: Usage: security-token cancel-withdrawal ")
+ return
+ }
+ o.cancelSecurityWithdrawal(ctx, args[2])
+ case "withdraw":
+ if len(args) < 4 {
+ fmt.Println("ERROR: Usage: security-token withdraw ")
+ return
+ }
+ o.withdrawSecurityTokens(ctx, args[2], args[3])
+ case "balance":
+ if len(args) < 3 {
+ fmt.Println("ERROR: Usage: security-token balance [wallet_address]")
+ return
+ }
+ wallet := ""
+ if len(args) >= 4 {
+ wallet = args[3]
+ } else {
+ wallet = o.getImportedWalletAddress()
+ if wallet == "" {
+ fmt.Println("ERROR: No wallet configured. Use 'config wallet import' first or specify a wallet address.")
+ return
+ }
+ fmt.Printf("INFO: Using configured wallet: %s\n", wallet)
+ }
+ o.securityBalance(ctx, args[2], wallet)
+ default:
+ fmt.Printf("ERROR: Unknown security-token command: %s\n", args[1])
+ fmt.Println("Commands: approve, balance, escrow, initiate-withdrawal, cancel-withdrawal, withdraw")
+ }
case "exit":
fmt.Println("Exiting...")
diff --git a/cerebro/operator_test.go b/cerebro/operator_test.go
index d11decb39..53e338d3e 100644
--- a/cerebro/operator_test.go
+++ b/cerebro/operator_test.go
@@ -5,8 +5,8 @@ import (
"strings"
"testing"
- "github.com/erc7824/nitrolite/pkg/core"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -152,7 +152,7 @@ func TestOperator_Connect_Failure(t *testing.T) {
require.NoError(t, err)
// Attempt to connect to a non-existent server
- _, err = NewOperator("ws://localhost:12345/nonexistent", s)
+ _, err = NewOperator("ws://localhost:12345/nonexistent", t.TempDir(), s)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to connect to clearnode")
}
diff --git a/cerebro/storage.go b/cerebro/storage.go
index 1165d64f0..2939a2cbb 100644
--- a/cerebro/storage.go
+++ b/cerebro/storage.go
@@ -35,6 +35,20 @@ func NewStorage(path string) (*Storage, error) {
return &Storage{db: db}, nil
}
+func (s *Storage) SetWSURL(wsURL string) error {
+ _, err := s.db.Exec("INSERT OR REPLACE INTO config (key, value) VALUES ('ws_url', ?)", wsURL)
+ return err
+}
+
+func (s *Storage) GetWSURL() (string, error) {
+ var wsURL string
+ err := s.db.QueryRow("SELECT value FROM config WHERE key = 'ws_url'").Scan(&wsURL)
+ if err == sql.ErrNoRows {
+ return "", fmt.Errorf("no WebSocket URL configured")
+ }
+ return wsURL, err
+}
+
func (s *Storage) SetPrivateKey(privateKey string) error {
_, err := s.db.Exec("INSERT OR REPLACE INTO config (key, value) VALUES ('private_key', ?)", privateKey)
return err
diff --git a/clearnode/action_gateway/action_gateway.go b/clearnode/action_gateway/action_gateway.go
new file mode 100644
index 000000000..1eecbf519
--- /dev/null
+++ b/clearnode/action_gateway/action_gateway.go
@@ -0,0 +1,209 @@
+package action_gateway
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "slices"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/shopspring/decimal"
+ "go.yaml.in/yaml/v2"
+)
+
+const (
+ actionGatewayFileName = "action_gateway.yaml"
+ defaultTimeWindow = 24 * time.Hour
+)
+
+type ActionLimitConfig struct {
+ LevelStepTokens decimal.Decimal `yaml:"level_step_tokens"`
+ AppCost decimal.Decimal `yaml:"app_cost"`
+ ActionGates map[core.GatedAction]ActionGateConfig `yaml:"action_gates"`
+}
+
+type ActionGateConfig struct {
+ FreeActionsAllowance uint64 `yaml:"free_actions_allowance"`
+ IncreasePerLevel uint64 `yaml:"increase_per_level"`
+}
+
+type ActionGateway struct {
+ cfg ActionLimitConfig
+}
+
+func NewActionGateway(cfg ActionLimitConfig) (*ActionGateway, error) {
+ if !cfg.LevelStepTokens.IsPositive() {
+ return nil, errors.New("LevelStepTokens must be greater than zero")
+ }
+ if !cfg.AppCost.IsPositive() {
+ return nil, errors.New("AppCost must be greater than zero")
+ }
+
+ seenIDs := make(map[uint8]core.GatedAction, len(cfg.ActionGates))
+ for action := range cfg.ActionGates {
+ id := action.ID()
+ if id == 0 {
+ return nil, fmt.Errorf("unknown action_gates key: %q", action)
+ }
+ if prev, exists := seenIDs[id]; exists && prev != action {
+ return nil, fmt.Errorf("duplicate gated action id %d for %q and %q", id, prev, action)
+ }
+ seenIDs[id] = action
+ }
+
+ return &ActionGateway{
+ cfg: cfg,
+ }, nil
+}
+
+func NewActionGatewayFromYaml(configDirPath string) (*ActionGateway, error) {
+ assetsPath := filepath.Join(configDirPath, actionGatewayFileName)
+ f, err := os.Open(assetsPath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ var cfg ActionLimitConfig
+ if err := yaml.NewDecoder(f).Decode(&cfg); err != nil {
+ return nil, err
+ }
+
+ return NewActionGateway(cfg)
+}
+
+type Store interface {
+ // GetAppCount returns the total number of applications owned by a specific wallet.
+ GetAppCount(ownerWallet string) (uint64, error)
+
+ // GetTotalUserStaked returns the total staked amount for a user across all blockchains.
+ GetTotalUserStaked(wallet string) (decimal.Decimal, error)
+
+ // RecordAction inserts a new action log entry for a user.
+ RecordAction(wallet string, gatedAction core.GatedAction) error
+
+ // GetUserActionCount returns the number of actions matching the given wallet and gated action
+ // within the specified time window (counting backwards from now).
+ GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error)
+
+ // GetUserActionCounts returns a map of gated actions to their respective counts for a user within the specified time window.
+ GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error)
+}
+
+func (a *ActionGateway) AllowAction(tx Store, userAddress string, gatedAction core.GatedAction) error {
+ if _, ok := a.cfg.ActionGates[gatedAction]; !ok {
+ return nil
+ }
+
+ stakedTokens, err := tx.GetTotalUserStaked(userAddress)
+ if err != nil {
+ return fmt.Errorf("failed to get user staked amount: %w", err)
+ }
+
+ remainingStaked := stakedTokens
+ if stakedTokens.IsPositive() {
+ appCount, err := tx.GetAppCount(userAddress)
+ if err != nil {
+ return fmt.Errorf("failed to get app count: %w", err)
+ }
+
+ maintenanceCost := a.cfg.AppCost.Mul(decimal.NewFromUint64(appCount))
+ remainingStaked = stakedTokens.Sub(maintenanceCost)
+ }
+
+ allowance := a.stakedTo24hActionsAllowance(gatedAction, remainingStaked)
+
+ usedCount, err := tx.GetUserActionCount(userAddress, gatedAction, defaultTimeWindow)
+ if err != nil {
+ return fmt.Errorf("failed to get user action count: %w", err)
+ }
+
+ if usedCount >= allowance {
+ return fmt.Errorf("action %s limit reached: used %d of %d allowed in 24h", gatedAction, usedCount, allowance)
+ }
+
+ if err := tx.RecordAction(userAddress, gatedAction); err != nil {
+ return fmt.Errorf("failed to record action: %w", err)
+ }
+
+ return nil
+}
+
+func (v *ActionGateway) AllowAppRegistration(tx Store, userAddress string) error {
+ stakedTokens, err := tx.GetTotalUserStaked(userAddress)
+ if err != nil {
+ return fmt.Errorf("failed to get user staked amount: %w", err)
+ }
+ if stakedTokens.IsZero() {
+ return errors.New("cannot register an app with zero staked tokens")
+ }
+
+ appCount, err := tx.GetAppCount(userAddress)
+ if err != nil {
+ return fmt.Errorf("failed to get app count: %w", err)
+ }
+
+ maintenanceCost := v.cfg.AppCost.Mul(decimal.NewFromUint64(appCount + 1))
+ if stakedTokens.LessThan(maintenanceCost) {
+ return fmt.Errorf("not enough staked tokens to register a new app: staked %s, required %s", stakedTokens.String(), maintenanceCost.String())
+ }
+
+ return nil
+}
+
+// stakedTo24hActionsAllowance returns the number of executions allowed in 24 hours for a specific gated action.
+func (a *ActionGateway) stakedTo24hActionsAllowance(gatedAction core.GatedAction, remainingStaked decimal.Decimal) uint64 {
+ actionLinitsConfig, ok := a.cfg.ActionGates[gatedAction]
+ if !ok {
+ return 0
+ }
+ actionAllowance := uint64(actionLinitsConfig.FreeActionsAllowance)
+ if remainingStaked.IsPositive() {
+ levels := uint64(remainingStaked.Div(a.cfg.LevelStepTokens).IntPart())
+ actionAllowance += actionLinitsConfig.IncreasePerLevel * levels
+ }
+ return actionAllowance
+}
+
+// GetUserAllowances returns user allowance for every gated action.
+func (a *ActionGateway) GetUserAllowances(tx Store, userAddress string) ([]core.ActionAllowance, error) {
+ stakedTokens, err := tx.GetTotalUserStaked(userAddress)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get user staked amount: %w", err)
+ }
+
+ actionCounts, err := tx.GetUserActionCounts(userAddress, defaultTimeWindow)
+ if err != nil {
+ return nil, err
+ }
+
+ remainingStaked := stakedTokens
+ if stakedTokens.IsPositive() {
+ appCount, err := tx.GetAppCount(userAddress)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get app count: %w", err)
+ }
+
+ maintenanceCost := a.cfg.AppCost.Mul(decimal.NewFromUint64(appCount))
+ remainingStaked = stakedTokens.Sub(maintenanceCost)
+ }
+
+ timeWindowStr := defaultTimeWindow.String()
+ result := make([]core.ActionAllowance, 0, len(a.cfg.ActionGates))
+ for action := range a.cfg.ActionGates {
+ result = append(result, core.ActionAllowance{
+ GatedAction: action,
+ TimeWindow: timeWindowStr,
+ Allowance: a.stakedTo24hActionsAllowance(action, remainingStaked),
+ Used: actionCounts[action],
+ })
+ }
+
+ slices.SortFunc(result, func(a, b core.ActionAllowance) int {
+ return int(a.GatedAction.ID()) - int(b.GatedAction.ID())
+ })
+
+ return result, nil
+}
diff --git a/clearnode/action_gateway/action_gateway_test.go b/clearnode/action_gateway/action_gateway_test.go
new file mode 100644
index 000000000..dad63ad5a
--- /dev/null
+++ b/clearnode/action_gateway/action_gateway_test.go
@@ -0,0 +1,356 @@
+package action_gateway
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/shopspring/decimal"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// mockAVStore implements AVStore for unit tests.
+type mockAVStore struct {
+ totalStaked decimal.Decimal
+ stakedErr error
+ appCount uint64
+ appCountErr error
+ actionCount uint64
+ actionErr error
+ actionCounts map[core.GatedAction]uint64
+ actionCSErr error
+ recordedActions []core.GatedAction
+ recordErr error
+}
+
+func (m *mockAVStore) GetTotalUserStaked(string) (decimal.Decimal, error) {
+ return m.totalStaked, m.stakedErr
+}
+
+func (m *mockAVStore) GetAppCount(string) (uint64, error) {
+ return m.appCount, m.appCountErr
+}
+
+func (m *mockAVStore) GetUserActionCount(string, core.GatedAction, time.Duration) (uint64, error) {
+ return m.actionCount, m.actionErr
+}
+
+func (m *mockAVStore) GetUserActionCounts(string, time.Duration) (map[core.GatedAction]uint64, error) {
+ return m.actionCounts, m.actionCSErr
+}
+
+func (m *mockAVStore) RecordAction(_ string, action core.GatedAction) error {
+ m.recordedActions = append(m.recordedActions, action)
+ return m.recordErr
+}
+
+func defaultConfig() ActionLimitConfig {
+ return ActionLimitConfig{
+ LevelStepTokens: decimal.NewFromInt(100),
+ AppCost: decimal.NewFromInt(50),
+ ActionGates: map[core.GatedAction]ActionGateConfig{
+ core.GatedActionTransfer: {FreeActionsAllowance: 5, IncreasePerLevel: 10},
+ },
+ }
+}
+
+func mustNewGateway(t *testing.T, cfg ActionLimitConfig) *ActionGateway {
+ t.Helper()
+ gw, err := NewActionGateway(cfg)
+ require.NoError(t, err)
+ return gw
+}
+
+// --- NewActionGateway ---
+
+func TestNewActionGateway(t *testing.T) {
+ t.Run("valid config", func(t *testing.T) {
+ gw, err := NewActionGateway(defaultConfig())
+ require.NoError(t, err)
+ assert.NotNil(t, gw)
+ })
+
+ t.Run("zero LevelStepTokens", func(t *testing.T) {
+ cfg := defaultConfig()
+ cfg.LevelStepTokens = decimal.Zero
+ _, err := NewActionGateway(cfg)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "LevelStepTokens")
+ })
+
+ t.Run("zero AppCost", func(t *testing.T) {
+ cfg := defaultConfig()
+ cfg.AppCost = decimal.Zero
+ _, err := NewActionGateway(cfg)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "AppCost")
+ })
+}
+
+// --- stakedTo24hActionsAllowance ---
+
+func TestStakedTo24hActionsAllowance(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+
+ t.Run("unknown action returns 0", func(t *testing.T) {
+ assert.Equal(t, uint64(0), gw.stakedTo24hActionsAllowance("unknown_action", decimal.NewFromInt(1000)))
+ })
+
+ t.Run("zero staked returns free allowance only", func(t *testing.T) {
+ assert.Equal(t, uint64(5), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.Zero))
+ })
+
+ t.Run("negative staked returns free allowance only", func(t *testing.T) {
+ assert.Equal(t, uint64(5), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.NewFromInt(-100)))
+ })
+
+ t.Run("positive staked adds levels", func(t *testing.T) {
+ // 250 tokens / 100 step = 2 levels -> 5 + 2*10 = 25
+ assert.Equal(t, uint64(25), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.NewFromInt(250)))
+ })
+
+ t.Run("partial level truncated", func(t *testing.T) {
+ // 199 tokens / 100 step = 1 level -> 5 + 1*10 = 15
+ assert.Equal(t, uint64(15), gw.stakedTo24hActionsAllowance(core.GatedActionTransfer, decimal.NewFromInt(199)))
+ })
+}
+
+// --- AllowAction ---
+
+func TestAllowAction(t *testing.T) {
+ t.Run("allowed with free allowance, zero staked", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCount: 0,
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ require.NoError(t, err)
+ assert.Equal(t, []core.GatedAction{core.GatedActionTransfer}, store.recordedActions)
+ })
+
+ t.Run("allowed with staked tokens and apps", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ // 300 staked - 2 apps * 50 cost = 200 remaining -> 2 levels -> 5 + 20 = 25
+ store := &mockAVStore{
+ totalStaked: decimal.NewFromInt(300),
+ appCount: 2,
+ actionCount: 24, // under 25
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ require.NoError(t, err)
+ })
+
+ t.Run("rejected at limit", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCount: 5, // equals free allowance
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "limit reached")
+ })
+
+ t.Run("rejected over limit", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCount: 10,
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ assert.Error(t, err)
+ })
+
+ t.Run("unknown gated action returns nil without store calls", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ stakedErr: errors.New("should not be called"),
+ }
+ err := gw.AllowAction(store, "0xuser", "nonexistent")
+ require.NoError(t, err)
+ assert.Empty(t, store.recordedActions)
+ })
+
+ t.Run("staked error propagated", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{stakedErr: errors.New("db down")}
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ assert.ErrorContains(t, err, "db down")
+ })
+
+ t.Run("app count error propagated", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.NewFromInt(100),
+ appCountErr: errors.New("db down"),
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ assert.ErrorContains(t, err, "db down")
+ })
+
+ t.Run("action count error propagated", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionErr: errors.New("db down"),
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ assert.ErrorContains(t, err, "db down")
+ })
+
+ t.Run("record error propagated", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCount: 0,
+ recordErr: errors.New("write fail"),
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ assert.ErrorContains(t, err, "write fail")
+ })
+
+ t.Run("skips GetAppCount when staked is zero", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ // If GetAppCount were called it would return an error
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ appCountErr: errors.New("should not be called"),
+ actionCount: 0,
+ }
+ err := gw.AllowAction(store, "0xuser", core.GatedActionTransfer)
+ require.NoError(t, err)
+ })
+}
+
+// --- AllowAppRegistration ---
+
+func TestAllowAppRegistration(t *testing.T) {
+ t.Run("allowed with enough stake", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ // 0 existing apps, cost for 1 = 50, staked = 100
+ store := &mockAVStore{totalStaked: decimal.NewFromInt(100), appCount: 0}
+ err := gw.AllowAppRegistration(store, "0xuser")
+ require.NoError(t, err)
+ })
+
+ t.Run("allowed exact cost", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ // 1 existing app, cost for 2 = 100, staked = 100
+ store := &mockAVStore{totalStaked: decimal.NewFromInt(100), appCount: 1}
+ err := gw.AllowAppRegistration(store, "0xuser")
+ require.NoError(t, err)
+ })
+
+ t.Run("rejected not enough stake", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ // 1 existing app, cost for 2 = 100, staked = 99
+ store := &mockAVStore{totalStaked: decimal.NewFromInt(99), appCount: 1}
+ err := gw.AllowAppRegistration(store, "0xuser")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "not enough staked tokens")
+ })
+
+ t.Run("rejected zero staked", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{totalStaked: decimal.Zero}
+ err := gw.AllowAppRegistration(store, "0xuser")
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "zero staked tokens")
+ })
+}
+
+// --- GetUserAllowances ---
+
+func TestGetUserAllowances(t *testing.T) {
+ multiActionConfig := ActionLimitConfig{
+ LevelStepTokens: decimal.NewFromInt(100),
+ AppCost: decimal.NewFromInt(50),
+ ActionGates: map[core.GatedAction]ActionGateConfig{
+ core.GatedActionTransfer: {FreeActionsAllowance: 5, IncreasePerLevel: 10},
+ core.GatedActionAppSessionOperation: {FreeActionsAllowance: 20, IncreasePerLevel: 5},
+ },
+ }
+
+ t.Run("zero staked returns free allowances", func(t *testing.T) {
+ gw := mustNewGateway(t, multiActionConfig)
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCounts: map[core.GatedAction]uint64{core.GatedActionTransfer: 3},
+ }
+ result, err := gw.GetUserAllowances(store, "0xuser")
+ require.NoError(t, err)
+ require.Len(t, result, 2)
+
+ // Results should be sorted by GatedAction ID
+ assert.Equal(t, core.GatedActionTransfer, result[0].GatedAction)
+ assert.Equal(t, uint64(5), result[0].Allowance)
+ assert.Equal(t, uint64(3), result[0].Used)
+
+ assert.Equal(t, core.GatedActionAppSessionOperation, result[1].GatedAction)
+ assert.Equal(t, uint64(20), result[1].Allowance)
+ assert.Equal(t, uint64(0), result[1].Used)
+ })
+
+ t.Run("staked tokens increase allowances", func(t *testing.T) {
+ gw := mustNewGateway(t, multiActionConfig)
+ // 500 staked - 2 apps * 50 = 400 remaining -> 4 levels
+ store := &mockAVStore{
+ totalStaked: decimal.NewFromInt(500),
+ appCount: 2,
+ actionCounts: map[core.GatedAction]uint64{},
+ }
+ result, err := gw.GetUserAllowances(store, "0xuser")
+ require.NoError(t, err)
+
+ // Transfer: 5 + 4*10 = 45
+ assert.Equal(t, uint64(45), result[0].Allowance)
+ // AppSessionOperation: 20 + 4*5 = 40
+ assert.Equal(t, uint64(40), result[1].Allowance)
+ })
+
+ t.Run("maintenance exceeds staked gives free allowance", func(t *testing.T) {
+ gw := mustNewGateway(t, multiActionConfig)
+ // 50 staked - 2 apps * 50 = -50 remaining
+ store := &mockAVStore{
+ totalStaked: decimal.NewFromInt(50),
+ appCount: 2,
+ actionCounts: map[core.GatedAction]uint64{},
+ }
+ result, err := gw.GetUserAllowances(store, "0xuser")
+ require.NoError(t, err)
+
+ assert.Equal(t, uint64(5), result[0].Allowance)
+ assert.Equal(t, uint64(20), result[1].Allowance)
+ })
+
+ t.Run("time window is set", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCounts: map[core.GatedAction]uint64{},
+ }
+ result, err := gw.GetUserAllowances(store, "0xuser")
+ require.NoError(t, err)
+ assert.Equal(t, "24h0m0s", result[0].TimeWindow)
+ })
+
+ t.Run("staked error propagated", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{stakedErr: errors.New("db down")}
+ _, err := gw.GetUserAllowances(store, "0xuser")
+ assert.ErrorContains(t, err, "db down")
+ })
+
+ t.Run("action counts error propagated", func(t *testing.T) {
+ gw := mustNewGateway(t, defaultConfig())
+ store := &mockAVStore{
+ totalStaked: decimal.Zero,
+ actionCSErr: errors.New("db down"),
+ }
+ _, err := gw.GetUserAllowances(store, "0xuser")
+ assert.ErrorContains(t, err, "db down")
+ })
+}
diff --git a/clearnode/api/app_session_v1/create_app_session.go b/clearnode/api/app_session_v1/create_app_session.go
index d2e6043c8..1db8d74dd 100644
--- a/clearnode/api/app_session_v1/create_app_session.go
+++ b/clearnode/api/app_session_v1/create_app_session.go
@@ -4,9 +4,11 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// CreateAppSession creates a new application session between participants.
@@ -35,6 +37,11 @@ func (h *Handler) CreateAppSession(c *rpc.Context) {
return
}
+ if reqPayload.Definition.Application == "" {
+ c.Fail(nil, "application id is required")
+ return
+ }
+
appDef, err := unmapAppDefinitionV1(reqPayload.Definition)
if err != nil {
c.Fail(err, "invalid app definition")
@@ -101,25 +108,71 @@ func (h *Handler) CreateAppSession(c *rpc.Context) {
}
err = h.useStoreInTx(func(tx Store) error {
+ registeredApp, err := tx.GetApp(appDef.ApplicationID)
+ if err != nil {
+ return rpc.Errorf("failed to look up application: %v", err)
+ }
+
+ // App must be registered regardless of CreationApprovalNotRequired flag.
+ if registeredApp == nil {
+ return rpc.Errorf("application %s is not registered", appDef.ApplicationID)
+ }
+
+ if !registeredApp.App.CreationApprovalNotRequired {
+ if reqPayload.OwnerSig == "" {
+ return rpc.Errorf("owner_sig is required for this application")
+ }
+
+ sigBytes, err := hexutil.Decode(reqPayload.OwnerSig)
+ if err != nil {
+ return rpc.Errorf("failed to decode signature: %v", err)
+ }
+ if len(sigBytes) == 0 {
+ return rpc.Errorf("empty owner_sig after decode")
+ }
+
+ sigType := app.AppSessionSignerTypeV1(sigBytes[0])
+ appSessionSignerValidator := app.NewAppSessionKeySigValidatorV1(
+ func(sessionKeyAddr string) (string, error) {
+ return tx.GetAppSessionKeyOwner(sessionKeyAddr, appSessionID)
+ },
+ )
+ recoveredOwnerWallet, err := appSessionSignerValidator.Recover(packedRequest, sigBytes)
+ if err != nil {
+ h.metrics.IncAppSessionUpdateSigValidation(appSessionID, sigType, false)
+ return rpc.Errorf("failed to recover user wallet: %v", err)
+ }
+ h.metrics.IncAppSessionUpdateSigValidation(appSessionID, sigType, true)
+
+ if !strings.EqualFold(recoveredOwnerWallet, registeredApp.App.OwnerWallet) {
+ return rpc.Errorf("invalid owner signature: signer %s is not the app owner", recoveredOwnerWallet)
+ }
+ }
+
+ err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, core.GatedActionAppSessionCreation)
+ if err != nil {
+ return rpc.NewError(err)
+ }
+
// Create app session with 0 allocations
appSession := app.AppSessionV1{
- SessionID: appSessionID,
- Application: appDef.Application,
- Participants: appDef.Participants,
- Quorum: appDef.Quorum,
- Nonce: appDef.Nonce,
- Status: app.AppSessionStatusOpen,
- Version: 1,
- SessionData: reqPayload.SessionData,
- CreatedAt: time.Now(),
- UpdatedAt: time.Now(),
+ SessionID: appSessionID,
+ ApplicationID: appDef.ApplicationID,
+ Participants: appDef.Participants,
+ Quorum: appDef.Quorum,
+ Nonce: appDef.Nonce,
+ Status: app.AppSessionStatusOpen,
+ Version: 1,
+ SessionData: reqPayload.SessionData,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
}
if err := tx.CreateAppSession(appSession); err != nil {
return rpc.Errorf("failed to create app session: %v", err)
}
- if err := h.verifyQuorum(tx, appSessionID, appDef.Application, participantWeights, appDef.Quorum, packedRequest, reqPayload.QuorumSigs); err != nil {
+ if err := h.verifyQuorum(tx, appSessionID, appDef.ApplicationID, participantWeights, appDef.Quorum, packedRequest, reqPayload.QuorumSigs); err != nil {
return err
}
diff --git a/clearnode/api/app_session_v1/create_app_session_test.go b/clearnode/api/app_session_v1/create_app_session_test.go
index 552644b47..ccbab0721 100644
--- a/clearnode/api/app_session_v1/create_app_session_test.go
+++ b/clearnode/api/app_session_v1/create_app_session_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestCreateAppSession_Success(t *testing.T) {
@@ -29,6 +29,7 @@ func TestCreateAppSession_Success(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -44,7 +45,7 @@ func TestCreateAppSession_Success(t *testing.T) {
// Build the app.AppDefinitionV1 for signing
appDef := app.AppDefinitionV1{
- Application: "test-app",
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 1},
{WalletAddress: participant2, SignatureWeight: 1},
@@ -76,6 +77,9 @@ func TestCreateAppSession_Success(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.MatchedBy(func(session any) bool {
return true // Accept any app session for now
})).Return(nil).Once()
@@ -129,6 +133,7 @@ func TestCreateAppSession_QuorumWithMultipleSignatures(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -146,7 +151,7 @@ func TestCreateAppSession_QuorumWithMultipleSignatures(t *testing.T) {
// Build the app.AppDefinitionV1 for signing
appDef := app.AppDefinitionV1{
- Application: "test-app",
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 2},
{WalletAddress: participant2, SignatureWeight: 1},
@@ -186,6 +191,9 @@ func TestCreateAppSession_QuorumWithMultipleSignatures(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once()
// Create RPC context
@@ -227,6 +235,7 @@ func TestCreateAppSession_ZeroNonce(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -290,6 +299,7 @@ func TestCreateAppSession_QuorumExceedsTotalWeights(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -359,6 +369,7 @@ func TestCreateAppSession_NoSignatures(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -422,6 +433,7 @@ func TestCreateAppSession_SignatureFromNonParticipant(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -436,7 +448,7 @@ func TestCreateAppSession_SignatureFromNonParticipant(t *testing.T) {
// Build the app.AppDefinitionV1 for signing (with participant1 only)
appDef := app.AppDefinitionV1{
- Application: "test-app",
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 1},
},
@@ -461,6 +473,9 @@ func TestCreateAppSession_SignatureFromNonParticipant(t *testing.T) {
QuorumSigs: []string{sig},
}
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once()
// Create RPC context
@@ -500,6 +515,7 @@ func TestCreateAppSession_QuorumNotMet(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -516,7 +532,7 @@ func TestCreateAppSession_QuorumNotMet(t *testing.T) {
// Build the app.AppDefinitionV1 for signing
appDef := app.AppDefinitionV1{
- Application: "test-app",
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 1},
{WalletAddress: participant2, SignatureWeight: 1},
@@ -553,6 +569,9 @@ func TestCreateAppSession_QuorumNotMet(t *testing.T) {
},
}
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once()
// Create RPC context
@@ -592,6 +611,7 @@ func TestCreateAppSession_DuplicateSignatures(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -607,7 +627,7 @@ func TestCreateAppSession_DuplicateSignatures(t *testing.T) {
// Build the app.AppDefinitionV1 for signing
appDef := app.AppDefinitionV1{
- Application: "test-app",
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 1},
{WalletAddress: participant2, SignatureWeight: 1},
@@ -641,6 +661,9 @@ func TestCreateAppSession_DuplicateSignatures(t *testing.T) {
},
}
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once()
// Create RPC context
@@ -681,6 +704,7 @@ func TestCreateAppSession_InvalidSignatureHex(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -707,6 +731,9 @@ func TestCreateAppSession_InvalidSignatureHex(t *testing.T) {
QuorumSigs: []string{"not-valid-hex"}, // Invalid hex string
}
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once()
// Create RPC context
@@ -746,6 +773,7 @@ func TestCreateAppSession_SignatureRecoveryFailure(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -773,6 +801,9 @@ func TestCreateAppSession_SignatureRecoveryFailure(t *testing.T) {
QuorumSigs: []string{"0xa100000000"},
}
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", CreationApprovalNotRequired: true},
+ }, nil).Once()
mockStore.On("CreateAppSession", mock.Anything).Return(nil).Once()
// Create RPC context
@@ -796,3 +827,246 @@ func TestCreateAppSession_SignatureRecoveryFailure(t *testing.T) {
// Verify mocks were called
mockStore.AssertExpectations(t)
}
+
+func TestCreateAppSession_AppNotRegistered(t *testing.T) {
+ mockStore := new(MockStore)
+
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ mockSigner := NewMockSigner()
+ mockAssetStore := new(MockAssetStore)
+ mockStatePacker := new(MockStatePacker)
+
+ handler := NewHandler(
+ storeTxProvider,
+ mockAssetStore,
+ &MockActionGateway{},
+ mockSigner,
+ core.NewStateAdvancerV1(mockAssetStore),
+ mockStatePacker,
+ "0xnode",
+ metrics.NewNoopRuntimeMetricExporter(),
+ 32, 1024, 256, 16,
+ )
+
+ wallet1 := NewTestAppSessionWallet(t)
+ participant1 := wallet1.Address
+
+ appDef := app.AppDefinitionV1{
+ ApplicationID: "unregistered-app",
+ Participants: []app.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Nonce: 12345,
+ }
+ sig1 := wallet1.SignCreateRequest(t, appDef, "")
+
+ reqPayload := rpc.AppSessionsV1CreateAppSessionRequest{
+ Definition: rpc.AppDefinitionV1{
+ Application: "unregistered-app",
+ Participants: []rpc.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Nonce: "12345",
+ },
+ QuorumSigs: []string{sig1},
+ }
+
+ // GetApp returns nil (not found)
+ mockStore.On("GetApp", "unregistered-app").Return(nil, nil).Once()
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppSessionsV1CreateAppSessionMethod), payload),
+ }
+
+ handler.CreateAppSession(ctx)
+
+ assert.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "not registered")
+
+ mockStore.AssertExpectations(t)
+}
+
+func TestCreateAppSession_OwnerSigRequired(t *testing.T) {
+ mockStore := new(MockStore)
+
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ mockSigner := NewMockSigner()
+ mockAssetStore := new(MockAssetStore)
+ mockStatePacker := new(MockStatePacker)
+
+ handler := NewHandler(
+ storeTxProvider,
+ mockAssetStore,
+ &MockActionGateway{},
+ mockSigner,
+ core.NewStateAdvancerV1(mockAssetStore),
+ mockStatePacker,
+ "0xnode",
+ metrics.NewNoopRuntimeMetricExporter(),
+ 32, 1024, 256, 16,
+ )
+
+ wallet1 := NewTestAppSessionWallet(t)
+ participant1 := wallet1.Address
+
+ appDef := app.AppDefinitionV1{
+ ApplicationID: "restricted-app",
+ Participants: []app.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Nonce: 12345,
+ }
+ sig1 := wallet1.SignCreateRequest(t, appDef, "")
+
+ reqPayload := rpc.AppSessionsV1CreateAppSessionRequest{
+ Definition: rpc.AppDefinitionV1{
+ Application: "restricted-app",
+ Participants: []rpc.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Nonce: "12345",
+ },
+ QuorumSigs: []string{sig1},
+ // No OwnerSig provided
+ }
+
+ // App requires approval (CreationApprovalNotRequired = false)
+ mockStore.On("GetApp", "restricted-app").Return(&app.AppInfoV1{
+ App: app.AppV1{
+ ID: "restricted-app",
+ OwnerWallet: "0xowneraddr",
+ CreationApprovalNotRequired: false,
+ },
+ }, nil).Once()
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppSessionsV1CreateAppSessionMethod), payload),
+ }
+
+ handler.CreateAppSession(ctx)
+
+ assert.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "owner_sig is required")
+
+ mockStore.AssertExpectations(t)
+}
+
+func TestCreateAppSession_OwnerSigSuccess(t *testing.T) {
+ mockStore := new(MockStore)
+
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ mockSigner := NewMockSigner()
+ mockAssetStore := new(MockAssetStore)
+ mockStatePacker := new(MockStatePacker)
+
+ handler := NewHandler(
+ storeTxProvider,
+ mockAssetStore,
+ &MockActionGateway{},
+ mockSigner,
+ core.NewStateAdvancerV1(mockAssetStore),
+ mockStatePacker,
+ "0xnode",
+ metrics.NewNoopRuntimeMetricExporter(),
+ 32, 1024, 256, 16,
+ )
+
+ // Create participant and owner wallets
+ wallet1 := NewTestAppSessionWallet(t)
+ ownerWallet := NewTestAppSessionWallet(t)
+ participant1 := wallet1.Address
+
+ appDef := app.AppDefinitionV1{
+ ApplicationID: "restricted-app",
+ Participants: []app.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Nonce: 12345,
+ }
+ sessionData := `{"game": "poker"}`
+
+ // Participant signs for quorum
+ sig1 := wallet1.SignCreateRequest(t, appDef, sessionData)
+ // Owner signs for approval
+ ownerSig := ownerWallet.SignCreateRequest(t, appDef, sessionData)
+
+ reqPayload := rpc.AppSessionsV1CreateAppSessionRequest{
+ Definition: rpc.AppDefinitionV1{
+ Application: "restricted-app",
+ Participants: []rpc.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Nonce: "12345",
+ },
+ QuorumSigs: []string{sig1},
+ SessionData: sessionData,
+ OwnerSig: ownerSig,
+ }
+
+ // App requires approval — owner wallet matches
+ mockStore.On("GetApp", "restricted-app").Return(&app.AppInfoV1{
+ App: app.AppV1{
+ ID: "restricted-app",
+ OwnerWallet: ownerWallet.Address,
+ CreationApprovalNotRequired: false,
+ },
+ }, nil).Once()
+ mockStore.On("CreateAppSession", mock.MatchedBy(func(session any) bool {
+ return true
+ })).Return(nil).Once()
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppSessionsV1CreateAppSessionMethod), payload),
+ }
+
+ handler.CreateAppSession(ctx)
+
+ assert.NotNil(t, ctx.Response)
+
+ if respErr := ctx.Response.Error(); respErr != nil {
+ t.Fatalf("Unexpected error response: %v", respErr)
+ }
+
+ assert.Equal(t, rpc.MsgTypeResp, ctx.Response.Type)
+
+ var resp rpc.AppSessionsV1CreateAppSessionResponse
+ err = ctx.Response.Payload.Translate(&resp)
+ require.NoError(t, err)
+
+ assert.NotEmpty(t, resp.AppSessionID)
+ assert.Equal(t, "1", resp.Version)
+ assert.Equal(t, app.AppSessionStatusOpen.String(), resp.Status)
+
+ mockStore.AssertExpectations(t)
+}
diff --git a/clearnode/api/app_session_v1/get_app_definition.go b/clearnode/api/app_session_v1/get_app_definition.go
index a9706186f..849b21a05 100644
--- a/clearnode/api/app_session_v1/get_app_definition.go
+++ b/clearnode/api/app_session_v1/get_app_definition.go
@@ -3,7 +3,7 @@ package app_session_v1
import (
"strconv"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetAppDefinition retrieves the application definition for a specific app session.
@@ -36,7 +36,7 @@ func (h *Handler) GetAppDefinition(c *rpc.Context) {
}
definition = rpc.AppDefinitionV1{
- Application: session.Application,
+ Application: session.ApplicationID,
Participants: participants,
Quorum: session.Quorum,
Nonce: strconv.FormatUint(session.Nonce, 10),
diff --git a/clearnode/api/app_session_v1/get_app_definition_test.go b/clearnode/api/app_session_v1/get_app_definition_test.go
index a07105b14..8f3fb7231 100644
--- a/clearnode/api/app_session_v1/get_app_definition_test.go
+++ b/clearnode/api/app_session_v1/get_app_definition_test.go
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetAppDefinition_Success(t *testing.T) {
@@ -39,8 +39,8 @@ func TestGetAppDefinition_Success(t *testing.T) {
participant2 := "0x9876543210987654321098765432109876543210"
session := &app.AppSessionV1{
- SessionID: sessionID,
- Application: "game",
+ SessionID: sessionID,
+ ApplicationID: "game",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 1},
{WalletAddress: participant2, SignatureWeight: 1},
diff --git a/clearnode/api/app_session_v1/get_app_sessions.go b/clearnode/api/app_session_v1/get_app_sessions.go
index bdb953128..2c5d1c196 100644
--- a/clearnode/api/app_session_v1/get_app_sessions.go
+++ b/clearnode/api/app_session_v1/get_app_sessions.go
@@ -3,9 +3,9 @@ package app_session_v1
import (
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetAppSessions retrieves application sessions with optional filtering.
diff --git a/clearnode/api/app_session_v1/get_app_sessions_test.go b/clearnode/api/app_session_v1/get_app_sessions_test.go
index b953d415b..1367c81b8 100644
--- a/clearnode/api/app_session_v1/get_app_sessions_test.go
+++ b/clearnode/api/app_session_v1/get_app_sessions_test.go
@@ -10,10 +10,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetAppSessions_SuccessWithParticipant(t *testing.T) {
@@ -41,8 +41,8 @@ func TestGetAppSessions_SuccessWithParticipant(t *testing.T) {
sessions := []app.AppSessionV1{
{
- SessionID: "session1",
- Application: "game",
+ SessionID: "session1",
+ ApplicationID: "game",
Participants: []app.AppParticipantV1{
{WalletAddress: participant, SignatureWeight: 1},
{WalletAddress: participant2, SignatureWeight: 1},
@@ -56,8 +56,8 @@ func TestGetAppSessions_SuccessWithParticipant(t *testing.T) {
UpdatedAt: time.Now(),
},
{
- SessionID: "session2",
- Application: "betting",
+ SessionID: "session2",
+ ApplicationID: "betting",
Participants: []app.AppParticipantV1{
{WalletAddress: participant, SignatureWeight: 1},
},
@@ -116,17 +116,17 @@ func TestGetAppSessions_SuccessWithParticipant(t *testing.T) {
// Verify first session
assert.Equal(t, "session1", response.AppSessions[0].AppSessionID)
assert.Equal(t, "closed", response.AppSessions[0].Status)
- assert.Len(t, response.AppSessions[0].Participants, 2)
- assert.Equal(t, participant, response.AppSessions[0].Participants[0].WalletAddress)
- assert.Equal(t, uint8(2), response.AppSessions[0].Quorum)
+ assert.Len(t, response.AppSessions[0].AppDefinitionV1.Participants, 2)
+ assert.Equal(t, participant, response.AppSessions[0].AppDefinitionV1.Participants[0].WalletAddress)
+ assert.Equal(t, uint8(2), response.AppSessions[0].AppDefinitionV1.Quorum)
assert.Equal(t, "1", response.AppSessions[0].Version)
assert.NotNil(t, response.AppSessions[0].SessionData)
// Verify second session
assert.Equal(t, "session2", response.AppSessions[1].AppSessionID)
assert.Equal(t, "open", response.AppSessions[1].Status)
- assert.Len(t, response.AppSessions[1].Participants, 1)
- assert.Equal(t, uint8(1), response.AppSessions[1].Quorum)
+ assert.Len(t, response.AppSessions[1].AppDefinitionV1.Participants, 1)
+ assert.Equal(t, uint8(1), response.AppSessions[1].AppDefinitionV1.Quorum)
assert.Equal(t, "5", response.AppSessions[1].Version)
assert.Nil(t, response.AppSessions[1].SessionData)
@@ -165,8 +165,8 @@ func TestGetAppSessions_SuccessWithAppSessionID(t *testing.T) {
sessions := []app.AppSessionV1{
{
- SessionID: sessionID,
- Application: "game",
+ SessionID: sessionID,
+ ApplicationID: "game",
Participants: []app.AppParticipantV1{
{WalletAddress: participant, SignatureWeight: 1},
},
@@ -296,8 +296,8 @@ func TestGetAppSessions_WithStatusFilter(t *testing.T) {
sessions := []app.AppSessionV1{
{
- SessionID: "session1",
- Application: "game",
+ SessionID: "session1",
+ ApplicationID: "game",
Participants: []app.AppParticipantV1{
{WalletAddress: participant, SignatureWeight: 1},
},
diff --git a/clearnode/api/app_session_v1/get_last_key_states.go b/clearnode/api/app_session_v1/get_last_key_states.go
index e741b9deb..83976a1c5 100644
--- a/clearnode/api/app_session_v1/get_last_key_states.go
+++ b/clearnode/api/app_session_v1/get_last_key_states.go
@@ -1,9 +1,9 @@
package app_session_v1
import (
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetLastKeyStates retrieves the latest session key states for a user with optional filtering by session key.
diff --git a/clearnode/api/app_session_v1/handler.go b/clearnode/api/app_session_v1/handler.go
index 1f3c59c78..bfac5079b 100644
--- a/clearnode/api/app_session_v1/handler.go
+++ b/clearnode/api/app_session_v1/handler.go
@@ -7,18 +7,19 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
// Handler manages app session operations and provides RPC endpoints for app session management.
type Handler struct {
useStoreInTx StoreTxProvider
assetStore AssetStore
+ actionGateway ActionGateway
signer sign.Signer
stateAdvancer core.StateAdvancer
statePacker core.StatePacker
@@ -34,6 +35,7 @@ type Handler struct {
func NewHandler(
useStoreInTx StoreTxProvider,
assetStore AssetStore,
+ actionGateway ActionGateway,
signer sign.Signer,
stateAdvancer core.StateAdvancer,
statePacker core.StatePacker,
@@ -44,6 +46,7 @@ func NewHandler(
return &Handler{
useStoreInTx: useStoreInTx,
assetStore: assetStore,
+ actionGateway: actionGateway,
signer: signer,
stateAdvancer: stateAdvancer,
statePacker: statePacker,
diff --git a/clearnode/api/app_session_v1/interface.go b/clearnode/api/app_session_v1/interface.go
index 056a820b0..5c13464b9 100644
--- a/clearnode/api/app_session_v1/interface.go
+++ b/clearnode/api/app_session_v1/interface.go
@@ -1,13 +1,17 @@
package app_session_v1
import (
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
)
// Store defines the persistence layer interface for app session management.
type Store interface {
+ // App registry operations
+ GetApp(appID string) (*app.AppInfoV1, error)
+
// App session operations
CreateAppSession(session app.AppSessionV1) error
GetAppSession(sessionID string) (*app.AppSessionV1, error)
@@ -42,6 +46,13 @@ type Store interface {
// Channel Session key state operations
ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, metadataHash string) (bool, error)
+
+ action_gateway.Store
+}
+
+type ActionGateway interface {
+ // AllowAction checks if a user is allowed to perform a specific gated action based on their past activity and allowances.
+ AllowAction(tx action_gateway.Store, userAddress string, gatedAction core.GatedAction) error
}
// StoreTxHandler is a function that executes Store operations within a transaction.
diff --git a/clearnode/api/app_session_v1/rebalance_app_sessions.go b/clearnode/api/app_session_v1/rebalance_app_sessions.go
index 4cebc5b01..f066c6cf1 100644
--- a/clearnode/api/app_session_v1/rebalance_app_sessions.go
+++ b/clearnode/api/app_session_v1/rebalance_app_sessions.go
@@ -6,10 +6,10 @@ import (
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// RebalanceAppSessions processes multi-session rebalancing operations atomically.
@@ -103,6 +103,18 @@ func (h *Handler) RebalanceAppSessions(c *rpc.Context) {
if appSession == nil {
return rpc.Errorf("app session not found: %s", update.AppStateUpdate.AppSessionID)
}
+ registeredApp, err := tx.GetApp(appSession.ApplicationID)
+ if err != nil {
+ return rpc.Errorf("failed to look up application: %v", err)
+ }
+ if registeredApp == nil {
+ return rpc.Errorf("application %s is not registered", appSession.ApplicationID)
+ }
+
+ err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, update.AppStateUpdate.Intent.GatedAction())
+ if err != nil {
+ return rpc.NewError(err)
+ }
if len(update.QuorumSigs) > len(appSession.Participants) {
return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(update.QuorumSigs), len(appSession.Participants))
}
@@ -125,7 +137,7 @@ func (h *Handler) RebalanceAppSessions(c *rpc.Context) {
return rpc.Errorf("failed to pack app state update for session %s: %v", update.AppStateUpdate.AppSessionID, err)
}
- if err := h.verifyQuorum(tx, update.AppStateUpdate.AppSessionID, appSession.Application, participantWeights, appSession.Quorum, packedStateUpdate, update.QuorumSigs); err != nil {
+ if err := h.verifyQuorum(tx, update.AppStateUpdate.AppSessionID, appSession.ApplicationID, participantWeights, appSession.Quorum, packedStateUpdate, update.QuorumSigs); err != nil {
return rpc.Errorf("quorum verification failed for session %s: %v", update.AppStateUpdate.AppSessionID, err)
}
diff --git a/clearnode/api/app_session_v1/rebalance_app_sessions_test.go b/clearnode/api/app_session_v1/rebalance_app_sessions_test.go
index 5305b521e..aef9cb2e9 100644
--- a/clearnode/api/app_session_v1/rebalance_app_sessions_test.go
+++ b/clearnode/api/app_session_v1/rebalance_app_sessions_test.go
@@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// assertSuccess checks if the RPC context has a successful response
@@ -43,6 +43,7 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -59,8 +60,8 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) {
sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222"
session1 := &app.AppSessionV1{
- SessionID: sessionID1,
- Application: "test-app",
+ SessionID: sessionID1,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: wallet1.Address, SignatureWeight: 10},
},
@@ -71,8 +72,8 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) {
}
session2 := &app.AppSessionV1{
- SessionID: sessionID2,
- Application: "test-app",
+ SessionID: sessionID2,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: wallet2.Address, SignatureWeight: 10},
},
@@ -149,6 +150,9 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) {
}
// Mock expectations for session 1
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID1).Return(session1, nil)
mockStore.On("GetParticipantAllocations", sessionID1).Return(currentAllocations1, nil)
mockStore.On("UpdateAppSession", mock.MatchedBy(func(session app.AppSessionV1) bool {
@@ -158,6 +162,9 @@ func TestRebalanceAppSessions_Success_TwoSessions(t *testing.T) {
})).Return(nil).Once()
// Mock expectations for session 2
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID2).Return(session2, nil)
mockStore.On("GetParticipantAllocations", sessionID2).Return(currentAllocations2, nil)
mockStore.On("UpdateAppSession", mock.MatchedBy(func(session app.AppSessionV1) bool {
@@ -207,6 +214,7 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -223,8 +231,8 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) {
sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222"
session1 := &app.AppSessionV1{
- SessionID: sessionID1,
- Application: "test-app",
+ SessionID: sessionID1,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: wallet1.Address, SignatureWeight: 10},
},
@@ -234,8 +242,8 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) {
}
session2 := &app.AppSessionV1{
- SessionID: sessionID2,
- Application: "test-app",
+ SessionID: sessionID2,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: wallet2.Address, SignatureWeight: 10},
},
@@ -313,12 +321,18 @@ func TestRebalanceAppSessions_Success_MultiAsset(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID1).Return(session1, nil)
mockStore.On("GetParticipantAllocations", sessionID1).Return(currentAllocations1, nil)
mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool {
return s.SessionID == sessionID1 && s.Version == 2
})).Return(nil).Once()
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID2).Return(session2, nil)
mockStore.On("GetParticipantAllocations", sessionID2).Return(currentAllocations2, nil)
mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool {
@@ -360,6 +374,7 @@ func TestRebalanceAppSessions_Error_InsufficientSessions(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -417,6 +432,7 @@ func TestRebalanceAppSessions_Error_InvalidIntent(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -490,6 +506,7 @@ func TestRebalanceAppSessions_Error_DuplicateSession(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -565,6 +582,7 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -581,8 +599,8 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) {
sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222"
session1 := &app.AppSessionV1{
- SessionID: sessionID1,
- Application: "test-app",
+ SessionID: sessionID1,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: wallet1.Address, SignatureWeight: 10},
},
@@ -592,8 +610,8 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) {
}
session2 := &app.AppSessionV1{
- SessionID: sessionID2,
- Application: "test-app",
+ SessionID: sessionID2,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: wallet2.Address, SignatureWeight: 10},
},
@@ -664,12 +682,18 @@ func TestRebalanceAppSessions_Error_ConservationViolation(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID1).Return(session1, nil)
mockStore.On("GetParticipantAllocations", sessionID1).Return(currentAllocations1, nil)
mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool {
return s.SessionID == sessionID1 && s.Version == 2
})).Return(nil).Once()
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID2).Return(session2, nil)
mockStore.On("GetParticipantAllocations", sessionID2).Return(currentAllocations2, nil)
mockStore.On("UpdateAppSession", mock.MatchedBy(func(s app.AppSessionV1) bool {
@@ -704,6 +728,7 @@ func TestRebalanceAppSessions_Error_SessionNotFound(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -784,6 +809,7 @@ func TestRebalanceAppSessions_Error_ClosedSession(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -799,9 +825,10 @@ func TestRebalanceAppSessions_Error_ClosedSession(t *testing.T) {
sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222"
session1 := &app.AppSessionV1{
- SessionID: sessionID1,
- Status: app.AppSessionStatusClosed, // Closed
- Version: 1,
+ SessionID: sessionID1,
+ ApplicationID: "test-app",
+ Status: app.AppSessionStatusClosed, // Closed
+ Version: 1,
Participants: []app.AppParticipantV1{
{WalletAddress: wallet1.Address, SignatureWeight: 1},
{WalletAddress: wallet2.Address, SignatureWeight: 1},
@@ -844,6 +871,9 @@ func TestRebalanceAppSessions_Error_ClosedSession(t *testing.T) {
},
}
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID1).Return(session1, nil)
payload, err := rpc.NewPayload(reqPayload)
@@ -874,6 +904,7 @@ func TestRebalanceAppSessions_Error_InvalidVersion(t *testing.T) {
handler := NewHandler(
storeTxProvider,
nil,
+ &MockActionGateway{},
nil,
nil,
nil,
@@ -889,9 +920,10 @@ func TestRebalanceAppSessions_Error_InvalidVersion(t *testing.T) {
sessionID2 := "0x2222222222222222222222222222222222222222222222222222222222222222"
session1 := &app.AppSessionV1{
- SessionID: sessionID1,
- Status: app.AppSessionStatusOpen,
- Version: 5, // Current version is 5
+ SessionID: sessionID1,
+ ApplicationID: "test-app",
+ Status: app.AppSessionStatusOpen,
+ Version: 5, // Current version is 5
Participants: []app.AppParticipantV1{
{WalletAddress: wallet1.Address, SignatureWeight: 1},
{WalletAddress: wallet2.Address, SignatureWeight: 1},
@@ -934,6 +966,9 @@ func TestRebalanceAppSessions_Error_InvalidVersion(t *testing.T) {
},
}
+ mockStore.On("GetApp", mock.Anything).Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil)
mockStore.On("GetAppSession", sessionID1).Return(session1, nil)
payload, err := rpc.NewPayload(reqPayload)
diff --git a/clearnode/api/app_session_v1/submit_app_state.go b/clearnode/api/app_session_v1/submit_app_state.go
index d03707d01..e5c1290c5 100644
--- a/clearnode/api/app_session_v1/submit_app_state.go
+++ b/clearnode/api/app_session_v1/submit_app_state.go
@@ -4,10 +4,10 @@ import (
"context"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
"github.com/shopspring/decimal"
)
@@ -65,6 +65,18 @@ func (h *Handler) SubmitAppState(c *rpc.Context) {
return rpc.Errorf("app session is already closed")
}
+ registeredApp, err := tx.GetApp(appSession.ApplicationID)
+ if err != nil {
+ return rpc.Errorf("failed to look up application: %v", err)
+ }
+ if registeredApp == nil {
+ return rpc.Errorf("application %s is not registered", appSession.ApplicationID)
+ }
+ err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, appStateUpd.Intent.GatedAction())
+ if err != nil {
+ return rpc.NewError(err)
+ }
+
if len(reqPayload.QuorumSigs) > len(appSession.Participants) {
return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(reqPayload.QuorumSigs), len(appSession.Participants))
}
@@ -84,7 +96,7 @@ func (h *Handler) SubmitAppState(c *rpc.Context) {
return rpc.Errorf("failed to pack app state update: %v", err)
}
- if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.Application, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil {
+ if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.ApplicationID, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil {
return err
}
diff --git a/clearnode/api/app_session_v1/submit_app_state_test.go b/clearnode/api/app_session_v1/submit_app_state_test.go
index 0300f49d9..3638e600b 100644
--- a/clearnode/api/app_session_v1/submit_app_state_test.go
+++ b/clearnode/api/app_session_v1/submit_app_state_test.go
@@ -5,10 +5,10 @@ import (
"errors"
"testing"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -29,6 +29,7 @@ func TestSubmitAppState_OperateIntent_NoRedistribution_Success(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -43,8 +44,8 @@ func TestSubmitAppState_OperateIntent_NoRedistribution_Success(t *testing.T) {
participant2 := "0x2222222222222222222222222222222222222222"
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 5},
{WalletAddress: participant2, SignatureWeight: 5},
@@ -96,6 +97,9 @@ func TestSubmitAppState_OperateIntent_NoRedistribution_Success(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil)
@@ -140,6 +144,7 @@ func TestSubmitAppState_OperateIntent_WithRedistribution_Success(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -154,8 +159,8 @@ func TestSubmitAppState_OperateIntent_WithRedistribution_Success(t *testing.T) {
participant2 := "0x2222222222222222222222222222222222222222"
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 5},
{WalletAddress: participant2, SignatureWeight: 5},
@@ -209,6 +214,9 @@ func TestSubmitAppState_OperateIntent_WithRedistribution_Success(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil)
@@ -257,6 +265,7 @@ func TestSubmitAppState_WithdrawIntent_Success(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -270,8 +279,8 @@ func TestSubmitAppState_WithdrawIntent_Success(t *testing.T) {
participant1 := wallet1.Address
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 10},
},
@@ -313,6 +322,9 @@ func TestSubmitAppState_WithdrawIntent_Success(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil)
@@ -367,6 +379,7 @@ func TestSubmitAppState_CloseIntent_Success(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -381,8 +394,8 @@ func TestSubmitAppState_CloseIntent_Success(t *testing.T) {
participant2 := "0x2222222222222222222222222222222222222222"
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 5},
{WalletAddress: participant2, SignatureWeight: 5},
@@ -430,6 +443,9 @@ func TestSubmitAppState_CloseIntent_Success(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil)
@@ -491,6 +507,7 @@ func TestSubmitAppState_CloseIntent_AllocationMismatch_Rejected(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -504,8 +521,8 @@ func TestSubmitAppState_CloseIntent_AllocationMismatch_Rejected(t *testing.T) {
participant1 := wallet1.Address
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 10},
},
@@ -547,6 +564,9 @@ func TestSubmitAppState_CloseIntent_AllocationMismatch_Rejected(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil).Maybe()
@@ -586,6 +606,7 @@ func TestSubmitAppState_OperateIntent_MissingAllocation_Rejected(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -600,8 +621,8 @@ func TestSubmitAppState_OperateIntent_MissingAllocation_Rejected(t *testing.T) {
participant2 := "0x2222222222222222222222222222222222222222"
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 5},
{WalletAddress: participant2, SignatureWeight: 5},
@@ -651,6 +672,9 @@ func TestSubmitAppState_OperateIntent_MissingAllocation_Rejected(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil)
@@ -695,6 +719,7 @@ func TestSubmitAppState_WithdrawIntent_MissingAllocation_Rejected(t *testing.T)
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -708,8 +733,8 @@ func TestSubmitAppState_WithdrawIntent_MissingAllocation_Rejected(t *testing.T)
participant1 := wallet1.Address
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 10},
},
@@ -752,6 +777,9 @@ func TestSubmitAppState_WithdrawIntent_MissingAllocation_Rejected(t *testing.T)
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil).Maybe()
@@ -802,6 +830,7 @@ func TestSubmitAppState_DepositIntent_Rejected(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -856,6 +885,7 @@ func TestSubmitAppState_ClosedSession_Rejected(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -884,6 +914,9 @@ func TestSubmitAppState_ClosedSession_Rejected(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
// Create RPC context
@@ -922,6 +955,7 @@ func TestSubmitAppState_InvalidVersion_Rejected(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -933,9 +967,10 @@ func TestSubmitAppState_InvalidVersion_Rejected(t *testing.T) {
appSessionID := "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Status: app.AppSessionStatusOpen,
- Version: 5, // Current version is 5
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
+ Status: app.AppSessionStatusOpen,
+ Version: 5, // Current version is 5
Participants: []app.AppParticipantV1{
{WalletAddress: "0x1111111111111111111111111111111111111111", SignatureWeight: 1},
},
@@ -954,6 +989,9 @@ func TestSubmitAppState_InvalidVersion_Rejected(t *testing.T) {
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
// Create RPC context
@@ -992,6 +1030,7 @@ func TestSubmitAppState_SessionNotFound_Rejected(t *testing.T) {
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -1051,6 +1090,7 @@ func TestSubmitAppState_OperateIntent_InvalidDecimalPrecision_Rejected(t *testin
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -1064,8 +1104,8 @@ func TestSubmitAppState_OperateIntent_InvalidDecimalPrecision_Rejected(t *testin
participant1 := wallet1.Address
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 10},
},
@@ -1113,6 +1153,9 @@ func TestSubmitAppState_OperateIntent_InvalidDecimalPrecision_Rejected(t *testin
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil)
@@ -1155,6 +1198,7 @@ func TestSubmitAppState_WithdrawIntent_InvalidDecimalPrecision_Rejected(t *testi
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -1168,8 +1212,8 @@ func TestSubmitAppState_WithdrawIntent_InvalidDecimalPrecision_Rejected(t *testi
participant1 := wallet1.Address
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 10},
},
@@ -1213,6 +1257,9 @@ func TestSubmitAppState_WithdrawIntent_InvalidDecimalPrecision_Rejected(t *testi
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockAssetStore.On("GetAssetDecimals", "USDC").Return(uint8(6), nil)
@@ -1255,6 +1302,7 @@ func TestSubmitAppState_OperateIntent_RedistributeToNewParticipant_Success(t *te
handler := NewHandler(
storeTxProvider,
mockAssetStore,
+ &MockActionGateway{},
mockSigner,
core.NewStateAdvancerV1(mockAssetStore),
mockStatePacker,
@@ -1270,8 +1318,8 @@ func TestSubmitAppState_OperateIntent_RedistributeToNewParticipant_Success(t *te
participant2 := wallet2.Address
existingSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{WalletAddress: participant1, SignatureWeight: 50},
{WalletAddress: participant2, SignatureWeight: 50},
@@ -1322,6 +1370,9 @@ func TestSubmitAppState_OperateIntent_RedistributeToNewParticipant_Success(t *te
}
// Mock expectations
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingSession, nil)
mockStore.On("GetParticipantAllocations", appSessionID).Return(currentAllocations, nil)
mockStore.On("GetAppSessionBalances", appSessionID).Return(sessionBalances, nil)
diff --git a/clearnode/api/app_session_v1/submit_deposit_state.go b/clearnode/api/app_session_v1/submit_deposit_state.go
index 0097b5ff8..c273c608e 100644
--- a/clearnode/api/app_session_v1/submit_deposit_state.go
+++ b/clearnode/api/app_session_v1/submit_deposit_state.go
@@ -3,11 +3,11 @@ package app_session_v1
import (
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
"github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
"github.com/shopspring/decimal"
)
@@ -44,8 +44,48 @@ func (h *Handler) SubmitDepositState(c *rpc.Context) {
var nodeSig string
err = h.useStoreInTx(func(tx Store) error {
+ appSession, err := tx.GetAppSession(appStateUpd.AppSessionID)
+ if err != nil {
+ return rpc.Errorf("app session not found: %v", err)
+ }
+ if appSession == nil {
+ return rpc.Errorf("app session not found")
+ }
+ if len(reqPayload.QuorumSigs) > len(appSession.Participants) {
+ return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(reqPayload.QuorumSigs), len(appSession.Participants))
+ }
+ if appSession.Status == app.AppSessionStatusClosed {
+ return rpc.Errorf("app session is already closed")
+ }
+ if appStateUpd.Version != appSession.Version+1 {
+ return rpc.Errorf("invalid app session version: expected %d, got %d", appSession.Version+1, appStateUpd.Version)
+ }
+
+ if appStateUpd.Intent != app.AppStateUpdateIntentDeposit {
+ return rpc.Errorf("invalid intent: expected 'deposit', got '%s'", appStateUpd.Intent)
+ }
+
+ participantWeights := getParticipantWeights(appSession.Participants)
+
+ if len(reqPayload.QuorumSigs) == 0 {
+ return rpc.Errorf("no signatures provided")
+ }
+
+ registeredApp, err := tx.GetApp(appSession.ApplicationID)
+ if err != nil {
+ return rpc.Errorf("failed to look up application: %v", err)
+ }
+ if registeredApp == nil {
+ return rpc.Errorf("application %s is not registered", appSession.ApplicationID)
+ }
+
+ err = h.actionGateway.AllowAction(tx, registeredApp.App.OwnerWallet, appStateUpd.Intent.GatedAction())
+ if err != nil {
+ return rpc.NewError(err)
+ }
+
// Lock the user's state to prevent concurrent modifications
- _, err := tx.LockUserState(userState.UserWallet, userState.Asset)
+ _, err = tx.LockUserState(userState.UserWallet, userState.Asset)
if err != nil {
return rpc.Errorf("failed to lock user state: %v", err)
}
@@ -116,40 +156,13 @@ func (h *Handler) SubmitDepositState(c *rpc.Context) {
}
h.metrics.IncChannelStateSigValidation(sigType, true)
- appSession, err := tx.GetAppSession(appStateUpd.AppSessionID)
- if err != nil {
- return rpc.Errorf("app session not found: %v", err)
- }
- if appSession == nil {
- return rpc.Errorf("app session not found")
- }
- if len(reqPayload.QuorumSigs) > len(appSession.Participants) {
- return rpc.Errorf("quorum_sigs count (%d) exceeds participants count (%d)", len(reqPayload.QuorumSigs), len(appSession.Participants))
- }
- if appSession.Status == app.AppSessionStatusClosed {
- return rpc.Errorf("app session is already closed")
- }
- if appStateUpd.Version != appSession.Version+1 {
- return rpc.Errorf("invalid app session version: expected %d, got %d", appSession.Version+1, appStateUpd.Version)
- }
-
- if appStateUpd.Intent != app.AppStateUpdateIntentDeposit {
- return rpc.Errorf("invalid intent: expected 'deposit', got '%s'", appStateUpd.Intent)
- }
-
- participantWeights := getParticipantWeights(appSession.Participants)
-
- if len(reqPayload.QuorumSigs) == 0 {
- return rpc.Errorf("no signatures provided")
- }
-
// Pack the app state update for signature verification
packedStateUpdate, err := app.PackAppStateUpdateV1(appStateUpd)
if err != nil {
return rpc.Errorf("failed to pack app state update: %v", err)
}
- if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.Application, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil {
+ if err := h.verifyQuorum(tx, appStateUpd.AppSessionID, appSession.ApplicationID, participantWeights, appSession.Quorum, packedStateUpdate, reqPayload.QuorumSigs); err != nil {
return err
}
diff --git a/clearnode/api/app_session_v1/submit_deposit_state_test.go b/clearnode/api/app_session_v1/submit_deposit_state_test.go
index 52d53e080..539667ae9 100644
--- a/clearnode/api/app_session_v1/submit_deposit_state_test.go
+++ b/clearnode/api/app_session_v1/submit_deposit_state_test.go
@@ -13,10 +13,10 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestSubmitDepositState_Success(t *testing.T) {
@@ -29,6 +29,7 @@ func TestSubmitDepositState_Success(t *testing.T) {
handler := &Handler{
assetStore: mockAssetStore,
+ actionGateway: &MockActionGateway{},
stateAdvancer: core.NewStateAdvancerV1(mockAssetStore),
statePacker: mockStatePacker,
useStoreInTx: func(handler StoreTxHandler) error {
@@ -56,8 +57,8 @@ func TestSubmitDepositState_Success(t *testing.T) {
// Create existing app session
existingAppSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{
WalletAddress: participant1,
@@ -152,6 +153,9 @@ func TestSubmitDepositState_Success(t *testing.T) {
mockStore.On("GetLastUserState", participant1, asset, false).Return(currentUserState, nil).Once()
mockStore.On("EnsureNoOngoingStateTransitions", participant1, asset).Return(nil).Once()
mockAssetStore.On("GetAssetDecimals", asset).Return(uint8(6), nil)
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingAppSession, nil).Once()
// Mock allocations check - empty initially
@@ -234,6 +238,7 @@ func TestSubmitDepositState_InvalidTransitionType(t *testing.T) {
handler := &Handler{
assetStore: mockAssetStore,
+ actionGateway: &MockActionGateway{},
stateAdvancer: core.NewStateAdvancerV1(mockAssetStore),
statePacker: mockStatePacker,
useStoreInTx: func(handler StoreTxHandler) error {
@@ -319,8 +324,22 @@ func TestSubmitDepositState_InvalidTransitionType(t *testing.T) {
},
}
- // Mock expectations - LockUserState is called before transition type check
- mockStore.On("LockUserState", participant1, asset).Return(decimal.Zero, nil).Once()
+ // Mock expectations
+ existingAppSession := &app.AppSessionV1{
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
+ Participants: []app.AppParticipantV1{
+ {WalletAddress: participant1, SignatureWeight: 1},
+ },
+ Quorum: 1,
+ Status: app.AppSessionStatusOpen,
+ Version: 1,
+ }
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
+ mockStore.On("GetAppSession", appSessionID).Return(existingAppSession, nil).Once()
+ mockStore.On("LockUserState", participant1, asset).Return(decimal.Zero, nil).Maybe()
// Create RPC request
rpcState := toRPCState(userState)
@@ -363,6 +382,7 @@ func TestSubmitDepositState_QuorumNotMet(t *testing.T) {
handler := &Handler{
assetStore: mockAssetStore,
+ actionGateway: &MockActionGateway{},
stateAdvancer: core.NewStateAdvancerV1(mockAssetStore),
statePacker: mockStatePacker,
useStoreInTx: func(handler StoreTxHandler) error {
@@ -390,8 +410,8 @@ func TestSubmitDepositState_QuorumNotMet(t *testing.T) {
// Create existing app session with higher quorum requirement
existingAppSession := &app.AppSessionV1{
- SessionID: appSessionID,
- Application: "test-app",
+ SessionID: appSessionID,
+ ApplicationID: "test-app",
Participants: []app.AppParticipantV1{
{
WalletAddress: participant1,
@@ -477,6 +497,9 @@ func TestSubmitDepositState_QuorumNotMet(t *testing.T) {
mockStore.On("GetLastUserState", participant1, asset, false).Return(currentUserState, nil).Once()
mockStore.On("EnsureNoOngoingStateTransitions", participant1, asset).Return(nil).Once()
mockAssetStore.On("GetAssetDecimals", asset).Return(uint8(6), nil)
+ mockStore.On("GetApp", "test-app").Return(&app.AppInfoV1{
+ App: app.AppV1{ID: "test-app", OwnerWallet: "0x0000000000000000000000000000000000000001"},
+ }, nil).Maybe()
mockStore.On("GetAppSession", appSessionID).Return(existingAppSession, nil).Once()
// Create RPC request
diff --git a/clearnode/api/app_session_v1/submit_session_key_state.go b/clearnode/api/app_session_v1/submit_session_key_state.go
index 18dd1c313..c92970a93 100644
--- a/clearnode/api/app_session_v1/submit_session_key_state.go
+++ b/clearnode/api/app_session_v1/submit_session_key_state.go
@@ -7,10 +7,10 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
// SubmitSessionKeyState processes session key state submissions for registration and updates.
diff --git a/clearnode/api/app_session_v1/testing.go b/clearnode/api/app_session_v1/testing.go
index dcae3b3ff..a1a220f3f 100644
--- a/clearnode/api/app_session_v1/testing.go
+++ b/clearnode/api/app_session_v1/testing.go
@@ -3,6 +3,7 @@ package app_session_v1
import (
"strings"
"testing"
+ "time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
@@ -10,9 +11,10 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
// MockStore is a mock implementation of the Store interface
@@ -128,11 +130,55 @@ func (m *MockStore) GetAppSessionKeyOwner(sessionKey, appSessionId string) (stri
return args.String(0), args.Error(1)
}
+func (m *MockStore) GetApp(appID string) (*app.AppInfoV1, error) {
+ args := m.Called(appID)
+ if args.Get(0) == nil {
+ return nil, args.Error(1)
+ }
+ return args.Get(0).(*app.AppInfoV1), args.Error(1)
+}
+
func (m *MockStore) ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, metadataHash string) (bool, error) {
args := m.Called(wallet, sessionKey, asset, metadataHash)
return args.Bool(0), args.Error(1)
}
+func (m *MockStore) GetAppCount(ownerWallet string) (uint64, error) {
+ args := m.Called(ownerWallet)
+ return args.Get(0).(uint64), args.Error(1)
+}
+
+func (m *MockStore) GetTotalUserStaked(wallet string) (decimal.Decimal, error) {
+ args := m.Called(wallet)
+ return args.Get(0).(decimal.Decimal), args.Error(1)
+}
+
+func (m *MockStore) RecordAction(wallet string, gatedAction core.GatedAction) error {
+ args := m.Called(wallet, gatedAction)
+ return args.Error(0)
+}
+
+func (m *MockStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) {
+ args := m.Called(wallet, gatedAction, window)
+ return args.Get(0).(uint64), args.Error(1)
+}
+
+func (m *MockStore) GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) {
+ args := m.Called(userWallet, window)
+ if args.Get(0) == nil {
+ return nil, args.Error(1)
+ }
+ return args.Get(0).(map[core.GatedAction]uint64), args.Error(1)
+}
+
+type MockActionGateway struct {
+ Err error
+}
+
+func (m *MockActionGateway) AllowAction(_ action_gateway.Store, _ string, _ core.GatedAction) error {
+ return m.Err
+}
+
// MockSigValidator is a mock implementation of the SigValidator interface
type MockSigValidator struct {
mock.Mock
diff --git a/clearnode/api/app_session_v1/utils.go b/clearnode/api/app_session_v1/utils.go
index 98d5f7df0..4381b2d62 100644
--- a/clearnode/api/app_session_v1/utils.go
+++ b/clearnode/api/app_session_v1/utils.go
@@ -7,9 +7,9 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
"github.com/shopspring/decimal"
)
@@ -28,10 +28,10 @@ func unmapAppDefinitionV1(def rpc.AppDefinitionV1) (app.AppDefinitionV1, error)
}
return app.AppDefinitionV1{
- Application: def.Application,
- Participants: participants,
- Quorum: def.Quorum,
- Nonce: nonce,
+ ApplicationID: def.Application,
+ Participants: participants,
+ Quorum: def.Quorum,
+ Nonce: nonce,
}, nil
}
@@ -219,12 +219,15 @@ func mapAppSessionInfoV1(session app.AppSessionV1, allocations map[string]map[st
return rpc.AppSessionInfoV1{
AppSessionID: session.SessionID,
Status: session.Status.String(),
- Participants: participants,
- SessionData: sessionData,
- Quorum: session.Quorum,
- Version: strconv.FormatUint(session.Version, 10),
- Nonce: strconv.FormatUint(session.Nonce, 10),
- Allocations: rpcAllocations,
+ AppDefinitionV1: rpc.AppDefinitionV1{
+ Application: session.ApplicationID,
+ Participants: participants,
+ Quorum: session.Quorum,
+ Nonce: strconv.FormatUint(session.Nonce, 10),
+ },
+ SessionData: sessionData,
+ Version: strconv.FormatUint(session.Version, 10),
+ Allocations: rpcAllocations,
}
}
diff --git a/clearnode/api/apps_v1/get_apps.go b/clearnode/api/apps_v1/get_apps.go
new file mode 100644
index 000000000..3ab03b4e5
--- /dev/null
+++ b/clearnode/api/apps_v1/get_apps.go
@@ -0,0 +1,71 @@
+package apps_v1
+
+import (
+ "strconv"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+)
+
+// GetApps retrieves registered applications with optional filtering.
+func (h *Handler) GetApps(c *rpc.Context) {
+ var req rpc.AppsV1GetAppsRequest
+ if err := c.Request.Payload.Translate(&req); err != nil {
+ c.Fail(err, "failed to parse parameters")
+ return
+ }
+
+ var paginationParams core.PaginationParams
+ if req.Pagination != nil {
+ paginationParams.Offset = req.Pagination.Offset
+ paginationParams.Limit = req.Pagination.Limit
+ paginationParams.Sort = req.Pagination.Sort
+ }
+
+ apps, metadata, err := h.store.GetApps(req.AppID, req.OwnerWallet, &paginationParams)
+ if err != nil {
+ c.Fail(err, "failed to retrieve apps")
+ return
+ }
+
+ response := rpc.AppsV1GetAppsResponse{
+ Apps: make([]rpc.AppInfoV1, len(apps)),
+ Metadata: mapPaginationMetadataV1(metadata),
+ }
+
+ for i, a := range apps {
+ response.Apps[i] = mapAppInfoV1(a)
+ }
+
+ payload, err := rpc.NewPayload(response)
+ if err != nil {
+ c.Fail(err, "failed to create response")
+ return
+ }
+
+ c.Succeed(c.Request.Method, payload)
+}
+
+func mapAppInfoV1(info app.AppInfoV1) rpc.AppInfoV1 {
+ return rpc.AppInfoV1{
+ AppV1: rpc.AppV1{
+ ID: info.App.ID,
+ OwnerWallet: info.App.OwnerWallet,
+ Metadata: info.App.Metadata,
+ Version: strconv.FormatUint(info.App.Version, 10),
+ CreationApprovalNotRequired: info.App.CreationApprovalNotRequired,
+ },
+ CreatedAt: strconv.FormatInt(info.CreatedAt.Unix(), 10),
+ UpdatedAt: strconv.FormatInt(info.UpdatedAt.Unix(), 10),
+ }
+}
+
+func mapPaginationMetadataV1(meta core.PaginationMetadata) rpc.PaginationMetadataV1 {
+ return rpc.PaginationMetadataV1{
+ Page: meta.Page,
+ PerPage: meta.PerPage,
+ TotalCount: meta.TotalCount,
+ PageCount: meta.PageCount,
+ }
+}
diff --git a/clearnode/api/apps_v1/get_apps_test.go b/clearnode/api/apps_v1/get_apps_test.go
new file mode 100644
index 000000000..aaeb8ed52
--- /dev/null
+++ b/clearnode/api/apps_v1/get_apps_test.go
@@ -0,0 +1,143 @@
+package apps_v1
+
+import (
+ "context"
+ "testing"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetApps_Success(t *testing.T) {
+ mockStore := &MockStore{
+ getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) {
+ return []app.AppInfoV1{
+ {App: app.AppV1{ID: "app-1", OwnerWallet: "0x1111", Metadata: "0x00", Version: 1}},
+ {App: app.AppV1{ID: "app-2", OwnerWallet: "0x2222", Metadata: "0x00", Version: 1}},
+ }, core.PaginationMetadata{TotalCount: 2, Page: 1, PerPage: 50}, nil
+ },
+ }
+
+ handler := NewHandler(mockStore, nil, nil, 4096)
+
+ reqPayload := rpc.AppsV1GetAppsRequest{}
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload),
+ }
+
+ handler.GetApps(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+
+ var resp rpc.AppsV1GetAppsResponse
+ require.NoError(t, ctx.Response.Payload.Translate(&resp))
+ assert.Len(t, resp.Apps, 2)
+ assert.Equal(t, "app-1", resp.Apps[0].ID)
+ assert.Equal(t, "app-2", resp.Apps[1].ID)
+ assert.Equal(t, uint32(2), resp.Metadata.TotalCount)
+}
+
+func TestGetApps_EmptyResults(t *testing.T) {
+ mockStore := &MockStore{
+ getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) {
+ return []app.AppInfoV1{}, core.PaginationMetadata{TotalCount: 0}, nil
+ },
+ }
+
+ handler := NewHandler(mockStore, nil, nil, 4096)
+
+ reqPayload := rpc.AppsV1GetAppsRequest{}
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload),
+ }
+
+ handler.GetApps(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+
+ var resp rpc.AppsV1GetAppsResponse
+ require.NoError(t, ctx.Response.Payload.Translate(&resp))
+ assert.Empty(t, resp.Apps)
+ assert.Equal(t, uint32(0), resp.Metadata.TotalCount)
+}
+
+func TestGetApps_FilterByAppID(t *testing.T) {
+ mockStore := &MockStore{
+ getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) {
+ require.NotNil(t, appID)
+ assert.Equal(t, "app-1", *appID)
+ return []app.AppInfoV1{
+ {App: app.AppV1{ID: "app-1", OwnerWallet: "0x1111", Version: 1}},
+ }, core.PaginationMetadata{TotalCount: 1, Page: 1, PerPage: 50}, nil
+ },
+ }
+
+ handler := NewHandler(mockStore, nil, nil, 4096)
+
+ aid := "app-1"
+ reqPayload := rpc.AppsV1GetAppsRequest{AppID: &aid}
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload),
+ }
+
+ handler.GetApps(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+
+ var resp rpc.AppsV1GetAppsResponse
+ require.NoError(t, ctx.Response.Payload.Translate(&resp))
+ assert.Len(t, resp.Apps, 1)
+ assert.Equal(t, "app-1", resp.Apps[0].ID)
+}
+
+func TestGetApps_FilterByOwnerWallet(t *testing.T) {
+ mockStore := &MockStore{
+ getAppsFn: func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) {
+ require.NotNil(t, ownerWallet)
+ assert.Equal(t, "0x1111", *ownerWallet)
+ return []app.AppInfoV1{
+ {App: app.AppV1{ID: "app-1", OwnerWallet: "0x1111", Version: 1}},
+ }, core.PaginationMetadata{TotalCount: 1, Page: 1, PerPage: 50}, nil
+ },
+ }
+
+ handler := NewHandler(mockStore, nil, nil, 4096)
+
+ owner := "0x1111"
+ reqPayload := rpc.AppsV1GetAppsRequest{OwnerWallet: &owner}
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1GetAppsMethod), payload),
+ }
+
+ handler.GetApps(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+
+ var resp rpc.AppsV1GetAppsResponse
+ require.NoError(t, ctx.Response.Payload.Translate(&resp))
+ assert.Len(t, resp.Apps, 1)
+ assert.Equal(t, "0x1111", resp.Apps[0].OwnerWallet)
+}
diff --git a/clearnode/api/apps_v1/handler.go b/clearnode/api/apps_v1/handler.go
new file mode 100644
index 000000000..280e6e453
--- /dev/null
+++ b/clearnode/api/apps_v1/handler.go
@@ -0,0 +1,20 @@
+package apps_v1
+
+// Handler manages app registry operations and provides RPC endpoints.
+type Handler struct {
+ store Store
+ useStoreInTx StoreTxProvider
+ actionGateway ActionGateway
+
+ maxAppMetadataLen int
+}
+
+// NewHandler creates a new Handler instance with the provided dependencies.
+func NewHandler(store Store, useStoreInTx StoreTxProvider, actionGateway ActionGateway, maxAppMetadataLen int) *Handler {
+ return &Handler{
+ store: store,
+ useStoreInTx: useStoreInTx,
+ actionGateway: actionGateway,
+ maxAppMetadataLen: maxAppMetadataLen,
+ }
+}
diff --git a/clearnode/api/apps_v1/interface.go b/clearnode/api/apps_v1/interface.go
new file mode 100644
index 000000000..06c9d94c2
--- /dev/null
+++ b/clearnode/api/apps_v1/interface.go
@@ -0,0 +1,33 @@
+package apps_v1
+
+import (
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+)
+
+// StoreTxHandler is a function that executes Store operations within a transaction.
+// If the handler returns an error, the transaction is rolled back; otherwise it's committed.
+type StoreTxHandler func(Store) error
+
+// StoreTxProvider wraps Store operations in a database transaction.
+// It accepts a StoreTxHandler and manages transaction lifecycle (begin, commit, rollback).
+// Returns an error if the handler fails or the transaction cannot be committed.
+type StoreTxProvider func(StoreTxHandler) error
+
+// Store defines the persistence layer interface for user data management.
+// All methods should be implemented to work within database transactions.
+type Store interface {
+ // CreateApp registers a new application. Returns an error if the app ID already exists.
+ CreateApp(entry app.AppV1) error
+
+ // GetApps retrieves applications with optional filtering.
+ GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error)
+
+ action_gateway.Store
+}
+
+type ActionGateway interface {
+ // AllowAppRegistration checks if a user is allowed to register a new application based on their past activity and allowances.
+ AllowAppRegistration(tx action_gateway.Store, userAddress string) error
+}
diff --git a/clearnode/api/apps_v1/submit_app_version.go b/clearnode/api/apps_v1/submit_app_version.go
new file mode 100644
index 000000000..c604e76fe
--- /dev/null
+++ b/clearnode/api/apps_v1/submit_app_version.go
@@ -0,0 +1,103 @@
+package apps_v1
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/sign"
+)
+
+// SubmitAppVersion updates an entry in the app registry.
+func (h *Handler) SubmitAppVersion(c *rpc.Context) {
+ var req rpc.AppsV1SubmitAppVersionRequest
+ if err := c.Request.Payload.Translate(&req); err != nil {
+ c.Fail(err, "failed to parse parameters")
+ return
+ }
+
+ if !app.AppIDV1Regex.MatchString(req.App.ID) {
+ c.Fail(rpc.Errorf("invalid app ID: should match regex %s", app.AppIDV1Regex.String()), "")
+ return
+ }
+ if req.App.OwnerWallet == "" {
+ c.Fail(nil, "owner_wallet is required")
+ return
+ }
+ if req.OwnerSig == "" {
+ c.Fail(nil, "owner_sig is required")
+ return
+ }
+ if len(req.App.Metadata) > h.maxAppMetadataLen {
+ c.Fail(rpc.Errorf("metadata exceeds maximum length of %d characters", h.maxAppMetadataLen), "")
+ return
+ }
+
+ version, err := strconv.ParseUint(req.App.Version, 10, 64)
+ if err != nil {
+ c.Fail(err, "invalid version")
+ return
+ }
+
+ // Only creation (version 1) is supported for now
+ if version != 1 {
+ c.Fail(nil, "only version 1 (creation) is currently supported")
+ return
+ }
+
+ err = h.useStoreInTx(func(tx Store) error {
+ err := h.actionGateway.AllowAppRegistration(tx, req.App.OwnerWallet)
+ if err != nil {
+ return rpc.NewError(err)
+ }
+
+ appEntry := app.AppV1{
+ ID: strings.ToLower(req.App.ID),
+ OwnerWallet: strings.ToLower(req.App.OwnerWallet),
+ Metadata: req.App.Metadata,
+ Version: version,
+ CreationApprovalNotRequired: req.App.CreationApprovalNotRequired,
+ }
+
+ packedApp, err := app.PackAppV1(appEntry)
+ if err != nil {
+ return rpc.Errorf("failed to pack app data: %v", err)
+ }
+
+ sigBytes, err := hexutil.Decode(req.OwnerSig)
+ if err != nil {
+ return rpc.Errorf("failed to decode owner signature: %v", err)
+ }
+
+ sigValidator, err := sign.NewSigValidator(sign.TypeEthereumMsg)
+ if err != nil {
+ return rpc.Errorf("failed to create signature validator: %v", err)
+ }
+
+ if err := sigValidator.Verify(appEntry.OwnerWallet, packedApp, sigBytes); err != nil {
+ return rpc.Errorf("invalid owner signature: %v", err)
+ }
+
+ if err := tx.CreateApp(appEntry); err != nil {
+ return rpc.Errorf("failed to create app")
+ }
+
+ return nil
+ })
+ if err != nil {
+ c.Fail(err, "failed to create app")
+ return
+ }
+
+ resp := rpc.AppsV1SubmitAppVersionResponse{}
+ payload, err := rpc.NewPayload(resp)
+ if err != nil {
+ c.Fail(err, "failed to create response")
+ return
+ }
+
+ c.Succeed(c.Request.Method, payload)
+}
diff --git a/clearnode/api/apps_v1/submit_app_version_test.go b/clearnode/api/apps_v1/submit_app_version_test.go
new file mode 100644
index 000000000..5486fec86
--- /dev/null
+++ b/clearnode/api/apps_v1/submit_app_version_test.go
@@ -0,0 +1,242 @@
+package apps_v1
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/sign"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// testOwnerWallet generates a real ECDSA key pair, signs the packed app data, and returns
+// the wallet address (lowercase hex), the hex-encoded signature, and the signer.
+func testOwnerWallet(t *testing.T, appEntry app.AppV1) (address string, sig string) {
+ t.Helper()
+
+ key, err := crypto.GenerateKey()
+ require.NoError(t, err)
+
+ addr := strings.ToLower(crypto.PubkeyToAddress(key.PublicKey).Hex())
+ privKeyHex := hexutil.Encode(crypto.FromECDSA(key))
+
+ signer, err := sign.NewEthereumMsgSigner(privKeyHex)
+ require.NoError(t, err)
+
+ // Update the entry with the real address for packing
+ appEntry.OwnerWallet = addr
+ packed, err := app.PackAppV1(appEntry)
+ require.NoError(t, err)
+
+ sigBytes, err := signer.Sign(packed)
+ require.NoError(t, err)
+
+ return addr, hexutil.Encode(sigBytes)
+}
+
+func newHandlerWithDefaults(store Store) *Handler {
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(store)
+ }
+ return NewHandler(store, storeTxProvider, &MockActionGateway{}, 4096)
+}
+
+func TestSubmitAppVersion_Success(t *testing.T) {
+ appEntry := app.AppV1{
+ ID: "test-app",
+ Metadata: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ Version: 1,
+ }
+
+ addr, sig := testOwnerWallet(t, appEntry)
+
+ mockStore := &MockStore{
+ createAppFn: func(entry app.AppV1) error {
+ assert.Equal(t, "test-app", entry.ID)
+ assert.Equal(t, addr, entry.OwnerWallet)
+ return nil
+ },
+ }
+
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{}, 4096)
+
+ reqPayload := rpc.AppsV1SubmitAppVersionRequest{
+ App: rpc.AppV1{
+ ID: "test-app",
+ OwnerWallet: addr,
+ Metadata: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ Version: "1",
+ },
+ OwnerSig: sig,
+ }
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload),
+ }
+
+ handler.SubmitAppVersion(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+}
+
+func TestSubmitAppVersion_MissingOwnerWallet(t *testing.T) {
+ mockStore := &MockStore{}
+ handler := newHandlerWithDefaults(mockStore)
+
+ reqPayload := rpc.AppsV1SubmitAppVersionRequest{
+ App: rpc.AppV1{
+ ID: "test-app",
+ Metadata: "0x00",
+ Version: "1",
+ },
+ OwnerSig: "0xdeadbeef",
+ }
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload),
+ }
+
+ handler.SubmitAppVersion(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "owner_wallet")
+}
+
+func TestSubmitAppVersion_MissingOwnerSig(t *testing.T) {
+ mockStore := &MockStore{}
+ handler := newHandlerWithDefaults(mockStore)
+
+ reqPayload := rpc.AppsV1SubmitAppVersionRequest{
+ App: rpc.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x00",
+ Version: "1",
+ },
+ }
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload),
+ }
+
+ handler.SubmitAppVersion(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "owner_sig")
+}
+
+func TestSubmitAppVersion_InvalidAppID(t *testing.T) {
+ mockStore := &MockStore{}
+ handler := newHandlerWithDefaults(mockStore)
+
+ reqPayload := rpc.AppsV1SubmitAppVersionRequest{
+ App: rpc.AppV1{
+ ID: "INVALID_APP!!", // doesn't match regex
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x00",
+ Version: "1",
+ },
+ OwnerSig: "0xdeadbeef",
+ }
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload),
+ }
+
+ handler.SubmitAppVersion(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "invalid app ID")
+}
+
+func TestSubmitAppVersion_InvalidVersion(t *testing.T) {
+ mockStore := &MockStore{}
+ handler := newHandlerWithDefaults(mockStore)
+
+ reqPayload := rpc.AppsV1SubmitAppVersionRequest{
+ App: rpc.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x00",
+ Version: "2", // Only version 1 is supported
+ },
+ OwnerSig: "0xdeadbeef",
+ }
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload),
+ }
+
+ handler.SubmitAppVersion(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "version 1")
+}
+
+func TestSubmitAppVersion_InvalidSignature(t *testing.T) {
+ mockStore := &MockStore{}
+ handler := newHandlerWithDefaults(mockStore)
+
+ reqPayload := rpc.AppsV1SubmitAppVersionRequest{
+ App: rpc.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ Version: "1",
+ },
+ OwnerSig: "0x" + strings.Repeat("ab", 65), // valid hex, wrong signature
+ }
+
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, string(rpc.AppsV1SubmitAppVersionMethod), payload),
+ }
+
+ handler.SubmitAppVersion(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "invalid owner signature")
+}
diff --git a/clearnode/api/apps_v1/testing.go b/clearnode/api/apps_v1/testing.go
new file mode 100644
index 000000000..e3cf9a4cf
--- /dev/null
+++ b/clearnode/api/apps_v1/testing.go
@@ -0,0 +1,58 @@
+package apps_v1
+
+import (
+ "time"
+
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/shopspring/decimal"
+)
+
+// MockStore implements the Store interface for testing.
+type MockStore struct {
+ createAppFn func(entry app.AppV1) error
+ getAppsFn func(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error)
+}
+
+func (m *MockStore) CreateApp(entry app.AppV1) error {
+ if m.createAppFn != nil {
+ return m.createAppFn(entry)
+ }
+ return nil
+}
+
+func (m *MockStore) GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) {
+ if m.getAppsFn != nil {
+ return m.getAppsFn(appID, ownerWallet, pagination)
+ }
+ return nil, core.PaginationMetadata{}, nil
+}
+
+func (m *MockStore) GetAppCount(_ string) (uint64, error) {
+ return 0, nil
+}
+
+func (m *MockStore) GetTotalUserStaked(_ string) (decimal.Decimal, error) {
+ return decimal.Zero, nil
+}
+
+func (m *MockStore) RecordAction(_ string, _ core.GatedAction) error {
+ return nil
+}
+
+func (m *MockStore) GetUserActionCount(_ string, _ core.GatedAction, _ time.Duration) (uint64, error) {
+ return 0, nil
+}
+
+func (m *MockStore) GetUserActionCounts(_ string, _ time.Duration) (map[core.GatedAction]uint64, error) {
+ return nil, nil
+}
+
+type MockActionGateway struct {
+ Err error
+}
+
+func (m *MockActionGateway) AllowAppRegistration(_ action_gateway.Store, _ string) error {
+ return m.Err
+}
diff --git a/clearnode/api/channel_v1/get_channels.go b/clearnode/api/channel_v1/get_channels.go
index 9c751de07..f51d11043 100644
--- a/clearnode/api/channel_v1/get_channels.go
+++ b/clearnode/api/channel_v1/get_channels.go
@@ -4,8 +4,8 @@ import (
"fmt"
"strings"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func channelStatusFromString(s string) (core.ChannelStatus, error) {
diff --git a/clearnode/api/channel_v1/get_channels_test.go b/clearnode/api/channel_v1/get_channels_test.go
index aa8b1f593..05dc01c8d 100644
--- a/clearnode/api/channel_v1/get_channels_test.go
+++ b/clearnode/api/channel_v1/get_channels_test.go
@@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func newGetChannelsHandler(mockTxStore *MockStore) *Handler {
@@ -31,6 +31,7 @@ func newGetChannelsHandler(mockTxStore *MockStore) *Handler {
minChallenge: uint32(3600),
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
}
diff --git a/clearnode/api/channel_v1/get_escrow_channel.go b/clearnode/api/channel_v1/get_escrow_channel.go
index 1866508ca..351e566c4 100644
--- a/clearnode/api/channel_v1/get_escrow_channel.go
+++ b/clearnode/api/channel_v1/get_escrow_channel.go
@@ -1,8 +1,8 @@
package channel_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetEscrowChannel retrieves current on-chain escrow channel information.
diff --git a/clearnode/api/channel_v1/get_escrow_channel_test.go b/clearnode/api/channel_v1/get_escrow_channel_test.go
index 3232fe3be..16ae76e61 100644
--- a/clearnode/api/channel_v1/get_escrow_channel_test.go
+++ b/clearnode/api/channel_v1/get_escrow_channel_test.go
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetEscrowChannel_Success(t *testing.T) {
@@ -37,6 +37,7 @@ func TestGetEscrowChannel_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data
diff --git a/clearnode/api/channel_v1/get_home_channel.go b/clearnode/api/channel_v1/get_home_channel.go
index 392e44d2e..18b4d6935 100644
--- a/clearnode/api/channel_v1/get_home_channel.go
+++ b/clearnode/api/channel_v1/get_home_channel.go
@@ -1,8 +1,8 @@
package channel_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetHomeChannel retrieves current on-chain home channel information.
diff --git a/clearnode/api/channel_v1/get_home_channel_test.go b/clearnode/api/channel_v1/get_home_channel_test.go
index 9f1198cc5..aec4fb3dd 100644
--- a/clearnode/api/channel_v1/get_home_channel_test.go
+++ b/clearnode/api/channel_v1/get_home_channel_test.go
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetHomeChannel_Success(t *testing.T) {
@@ -37,6 +37,7 @@ func TestGetHomeChannel_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data
@@ -124,6 +125,7 @@ func TestGetHomeChannel_NotFound(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data
diff --git a/clearnode/api/channel_v1/get_last_key_states.go b/clearnode/api/channel_v1/get_last_key_states.go
index 37836d759..001d96008 100644
--- a/clearnode/api/channel_v1/get_last_key_states.go
+++ b/clearnode/api/channel_v1/get_last_key_states.go
@@ -1,9 +1,9 @@
package channel_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetLastKeyStates retrieves the latest channel session key states for a user with optional filtering by session key.
diff --git a/clearnode/api/channel_v1/get_latest_state.go b/clearnode/api/channel_v1/get_latest_state.go
index 0d9b2a49b..80000a38c 100644
--- a/clearnode/api/channel_v1/get_latest_state.go
+++ b/clearnode/api/channel_v1/get_latest_state.go
@@ -1,8 +1,8 @@
package channel_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetLatestState retrieves the current state of the user stored on the Node.
diff --git a/clearnode/api/channel_v1/get_latest_state_test.go b/clearnode/api/channel_v1/get_latest_state_test.go
index 837547cc3..922bca4c8 100644
--- a/clearnode/api/channel_v1/get_latest_state_test.go
+++ b/clearnode/api/channel_v1/get_latest_state_test.go
@@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetLatestState_Success(t *testing.T) {
@@ -38,6 +38,7 @@ func TestGetLatestState_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data
@@ -136,6 +137,7 @@ func TestGetLatestState_OnlySigned(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data
diff --git a/clearnode/api/channel_v1/handler.go b/clearnode/api/channel_v1/handler.go
index d9086b6b2..f5912303c 100644
--- a/clearnode/api/channel_v1/handler.go
+++ b/clearnode/api/channel_v1/handler.go
@@ -3,16 +3,17 @@ package channel_v1
import (
"context"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// Handler manages channel state transitions and provides RPC endpoints for state submission.
type Handler struct {
useStoreInTx StoreTxProvider
memoryStore MemoryStore
+ actionGateway ActionGateway
nodeSigner *core.ChannelDefaultSigner
stateAdvancer core.StateAdvancer
statePacker core.StatePacker
@@ -26,6 +27,7 @@ type Handler struct {
func NewHandler(
useStoreInTx StoreTxProvider,
memoryStore MemoryStore,
+ actionGateway ActionGateway,
nodeSigner *core.ChannelDefaultSigner,
stateAdvancer core.StateAdvancer,
statePacker core.StatePacker,
@@ -39,6 +41,7 @@ func NewHandler(
statePacker: statePacker,
useStoreInTx: useStoreInTx,
memoryStore: memoryStore,
+ actionGateway: actionGateway,
nodeSigner: nodeSigner,
nodeAddress: nodeAddress,
minChallenge: minChallenge,
diff --git a/clearnode/api/channel_v1/interface.go b/clearnode/api/channel_v1/interface.go
index 28206873c..3817e275a 100644
--- a/clearnode/api/channel_v1/interface.go
+++ b/clearnode/api/channel_v1/interface.go
@@ -1,7 +1,8 @@
package channel_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
)
@@ -78,6 +79,13 @@ type Store interface {
// exists at its latest version for the (wallet, sessionKey) pair, includes the given asset,
// and matches the metadata hash.
ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset, metadataHash string) (bool, error)
+
+ action_gateway.Store
+}
+
+type ActionGateway interface {
+ // AllowAction checks if a user is allowed to perform a specific gated action based on their past activity and allowances.
+ AllowAction(tx action_gateway.Store, userAddress string, gatedAction core.GatedAction) error
}
// SigValidator validates cryptographic signatures on state transitions.
diff --git a/clearnode/api/channel_v1/request_creation.go b/clearnode/api/channel_v1/request_creation.go
index 81a419e68..99fd79fa1 100644
--- a/clearnode/api/channel_v1/request_creation.go
+++ b/clearnode/api/channel_v1/request_creation.go
@@ -4,11 +4,11 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// RequestCreation processes channel creation requests from users.
diff --git a/clearnode/api/channel_v1/request_creation_test.go b/clearnode/api/channel_v1/request_creation_test.go
index f43810d0a..1db939081 100644
--- a/clearnode/api/channel_v1/request_creation_test.go
+++ b/clearnode/api/channel_v1/request_creation_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestRequestCreation_Success(t *testing.T) {
@@ -42,6 +42,7 @@ func TestRequestCreation_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -191,6 +192,7 @@ func TestRequestCreation_Acknowledgement_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -332,6 +334,7 @@ func TestRequestCreation_InvalidChallenge(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data
diff --git a/clearnode/api/channel_v1/submit_session_key_state.go b/clearnode/api/channel_v1/submit_session_key_state.go
index 16eb612ea..3f464d367 100644
--- a/clearnode/api/channel_v1/submit_session_key_state.go
+++ b/clearnode/api/channel_v1/submit_session_key_state.go
@@ -5,9 +5,9 @@ import (
"github.com/ethereum/go-ethereum/common"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// SubmitSessionKeyState processes channel session key state submissions for registration and updates.
diff --git a/clearnode/api/channel_v1/submit_state.go b/clearnode/api/channel_v1/submit_state.go
index 1f3758b6b..2daf5867d 100644
--- a/clearnode/api/channel_v1/submit_state.go
+++ b/clearnode/api/channel_v1/submit_state.go
@@ -1,10 +1,10 @@
package channel_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
"github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// SubmitState processes user-submitted state transitions, validates them against the current state,
@@ -30,7 +30,12 @@ func (h *Handler) SubmitState(c *rpc.Context) {
var nodeSig string
incomingTransition := incomingState.Transition
err = h.useStoreInTx(func(tx Store) error {
- _, err := tx.LockUserState(incomingState.UserWallet, incomingState.Asset)
+ err := h.actionGateway.AllowAction(tx, incomingState.UserWallet, incomingState.Transition.Type.GatedAction())
+ if err != nil {
+ return rpc.NewError(err)
+ }
+
+ _, err = tx.LockUserState(incomingState.UserWallet, incomingState.Asset)
if err != nil {
return rpc.Errorf("failed to lock user state: %v", err)
}
diff --git a/clearnode/api/channel_v1/submit_state_test.go b/clearnode/api/channel_v1/submit_state_test.go
index 73bd45ab1..75407f1d1 100644
--- a/clearnode/api/channel_v1/submit_state_test.go
+++ b/clearnode/api/channel_v1/submit_state_test.go
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestSubmitState_TransferSend_Success(t *testing.T) {
@@ -42,6 +42,7 @@ func TestSubmitState_TransferSend_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive senderWallet from a user signer key
@@ -216,6 +217,7 @@ func TestSubmitState_EscrowLock_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -371,6 +373,7 @@ func TestSubmitState_EscrowWithdraw_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -524,6 +527,7 @@ func TestSubmitState_HomeDeposit_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -655,6 +659,7 @@ func TestSubmitState_HomeWithdrawal_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -788,6 +793,7 @@ func TestSubmitState_MutualLock_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -943,6 +949,7 @@ func TestSubmitState_EscrowDeposit_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -1098,6 +1105,7 @@ func TestSubmitState_Finalize_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
@@ -1237,6 +1245,7 @@ func TestSubmitState_Acknowledgement_Success(t *testing.T) {
minChallenge: minChallenge,
metrics: metrics.NewNoopRuntimeMetricExporter(),
maxSessionKeyIDs: 256,
+ actionGateway: &MockActionGateway{},
}
// Test data - derive userWallet from a user signer key
diff --git a/clearnode/api/channel_v1/testing.go b/clearnode/api/channel_v1/testing.go
index 198f3efb0..78c3ab245 100644
--- a/clearnode/api/channel_v1/testing.go
+++ b/clearnode/api/channel_v1/testing.go
@@ -1,13 +1,16 @@
package channel_v1
import (
+ "time"
+
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/mock"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
// MockStore is a mock implementation of the Store interface
@@ -111,6 +114,34 @@ func (m *MockStore) ValidateChannelSessionKeyForAsset(wallet, sessionKey, asset,
return args.Bool(0), args.Error(1)
}
+func (m *MockStore) GetAppCount(ownerWallet string) (uint64, error) {
+ args := m.Called(ownerWallet)
+ return args.Get(0).(uint64), args.Error(1)
+}
+
+func (m *MockStore) GetTotalUserStaked(wallet string) (decimal.Decimal, error) {
+ args := m.Called(wallet)
+ return args.Get(0).(decimal.Decimal), args.Error(1)
+}
+
+func (m *MockStore) RecordAction(wallet string, gatedAction core.GatedAction) error {
+ args := m.Called(wallet, gatedAction)
+ return args.Error(0)
+}
+
+func (m *MockStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) {
+ args := m.Called(wallet, gatedAction, window)
+ return args.Get(0).(uint64), args.Error(1)
+}
+
+func (m *MockStore) GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) {
+ args := m.Called(userWallet, window)
+ if args.Get(0) == nil {
+ return nil, args.Error(1)
+ }
+ return args.Get(0).(map[core.GatedAction]uint64), args.Error(1)
+}
+
func NewMockSigner() sign.Signer {
key, _ := crypto.GenerateKey()
@@ -171,3 +202,11 @@ func (m *MockStatePacker) PackState(state core.State) ([]byte, error) {
args := m.Called(state)
return args.Get(0).([]byte), args.Error(1)
}
+
+type MockActionGateway struct {
+ Err error
+}
+
+func (m *MockActionGateway) AllowAction(_ action_gateway.Store, _ string, _ core.GatedAction) error {
+ return m.Err
+}
diff --git a/clearnode/api/channel_v1/utils.go b/clearnode/api/channel_v1/utils.go
index 7346365fa..d7e85bb72 100644
--- a/clearnode/api/channel_v1/utils.go
+++ b/clearnode/api/channel_v1/utils.go
@@ -8,8 +8,8 @@ import (
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func toCoreState(state rpc.StateV1) (core.State, error) {
diff --git a/clearnode/api/metric_store.go b/clearnode/api/metric_store.go
index b9a84117c..2ea3e7601 100644
--- a/clearnode/api/metric_store.go
+++ b/clearnode/api/metric_store.go
@@ -1,10 +1,10 @@
package api
import (
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/clearnode/store/database"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/clearnode/store/database"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// metricStore wraps a DatabaseStore to buffer metric callbacks during a DB transaction.
@@ -40,7 +40,7 @@ func (s *metricStore) UpdateAppSession(session app.AppSessionV1) error {
return err
}
s.callbacks = append(s.callbacks, func() {
- s.m.IncAppStateUpdate(session.Application)
+ s.m.IncAppStateUpdate(session.ApplicationID)
})
return nil
}
diff --git a/clearnode/api/node_v1/get_assets.go b/clearnode/api/node_v1/get_assets.go
index 392fb6ea3..290af89d6 100644
--- a/clearnode/api/node_v1/get_assets.go
+++ b/clearnode/api/node_v1/get_assets.go
@@ -3,7 +3,7 @@ package node_v1
import (
"strconv"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetAssets retrieves the current assets of the Node.
diff --git a/clearnode/api/node_v1/get_assets_test.go b/clearnode/api/node_v1/get_assets_test.go
index 4e5495116..caae73999 100644
--- a/clearnode/api/node_v1/get_assets_test.go
+++ b/clearnode/api/node_v1/get_assets_test.go
@@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetAssets_Success(t *testing.T) {
diff --git a/clearnode/api/node_v1/get_config.go b/clearnode/api/node_v1/get_config.go
index 91c80011f..050333c82 100644
--- a/clearnode/api/node_v1/get_config.go
+++ b/clearnode/api/node_v1/get_config.go
@@ -1,8 +1,8 @@
package node_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetConfig retrieves the current configuration of the Node.
diff --git a/clearnode/api/node_v1/get_config_test.go b/clearnode/api/node_v1/get_config_test.go
index dacae7233..c113a4a1d 100644
--- a/clearnode/api/node_v1/get_config_test.go
+++ b/clearnode/api/node_v1/get_config_test.go
@@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetConfig_Success(t *testing.T) {
diff --git a/clearnode/api/node_v1/interface.go b/clearnode/api/node_v1/interface.go
index 140da7077..34f3f7074 100644
--- a/clearnode/api/node_v1/interface.go
+++ b/clearnode/api/node_v1/interface.go
@@ -1,7 +1,7 @@
package node_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// MemoryStore defines an in-memory data store interface for retrieving
diff --git a/clearnode/api/node_v1/ping.go b/clearnode/api/node_v1/ping.go
index 1a0a70e94..8de7c358e 100644
--- a/clearnode/api/node_v1/ping.go
+++ b/clearnode/api/node_v1/ping.go
@@ -1,7 +1,7 @@
package node_v1
import (
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// Ping handles the ping request and responds with the ping method.
diff --git a/clearnode/api/node_v1/testing.go b/clearnode/api/node_v1/testing.go
index f02577d70..cf114d19f 100644
--- a/clearnode/api/node_v1/testing.go
+++ b/clearnode/api/node_v1/testing.go
@@ -3,7 +3,7 @@ package node_v1
import (
"github.com/stretchr/testify/mock"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// MockMemoryStore is a mock implementation of the MemoryStore interface
diff --git a/clearnode/api/node_v1/utils.go b/clearnode/api/node_v1/utils.go
index 2d3a3800d..378dd5545 100644
--- a/clearnode/api/node_v1/utils.go
+++ b/clearnode/api/node_v1/utils.go
@@ -4,15 +4,16 @@ import (
"fmt"
"strconv"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func mapBlockchainV1(blockchain core.Blockchain) rpc.BlockchainInfoV1 {
return rpc.BlockchainInfoV1{
- Name: blockchain.Name,
- BlockchainID: strconv.FormatUint(blockchain.ID, 10),
- ChannelHubAddress: blockchain.ChannelHubAddress,
+ Name: blockchain.Name,
+ BlockchainID: strconv.FormatUint(blockchain.ID, 10),
+ ChannelHubAddress: blockchain.ChannelHubAddress,
+ LockingContractAddress: blockchain.LockingContractAddress,
}
}
diff --git a/clearnode/api/rate_limits.go b/clearnode/api/rate_limits.go
new file mode 100644
index 000000000..55041e2c4
--- /dev/null
+++ b/clearnode/api/rate_limits.go
@@ -0,0 +1,51 @@
+package api
+
+import (
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/rpc"
+)
+
+const (
+ // rateLimitStorageKey is the key used to store the token bucket in connection storage.
+ rateLimitStorageKey = "rate_limiter"
+)
+
+// tokenBucket holds the mutable state for per-connection rate limiting.
+type tokenBucket struct {
+ tokens float64
+ last time.Time
+}
+
+// RateLimitMiddleware enforces per-connection rate limiting using a token bucket algorithm.
+// It stores the token bucket in the connection's Storage for persistence across requests.
+func (r *RPCRouter) RateLimitMiddleware(c *rpc.Context) {
+ bucket := &tokenBucket{
+ tokens: r.rateLimitBurst,
+ last: time.Now().Add(-time.Second),
+ }
+ if val, ok := c.Storage.Get(rateLimitStorageKey); ok {
+ if b, ok := val.(*tokenBucket); ok {
+ bucket = b
+ }
+ }
+
+ now := time.Now()
+ elapsed := now.Sub(bucket.last).Seconds()
+ bucket.last = now
+
+ // Refill tokens based on elapsed time
+ bucket.tokens += elapsed * r.rateLimitPerSec
+ if bucket.tokens > r.rateLimitBurst {
+ bucket.tokens = r.rateLimitBurst
+ }
+
+ if bucket.tokens < 1 {
+ c.Fail(nil, "rate limit exceeded")
+ return
+ }
+ bucket.tokens--
+ c.Storage.Set(rateLimitStorageKey, bucket)
+
+ c.Next()
+}
diff --git a/clearnode/api/rate_limits_test.go b/clearnode/api/rate_limits_test.go
new file mode 100644
index 000000000..45cb7c299
--- /dev/null
+++ b/clearnode/api/rate_limits_test.go
@@ -0,0 +1,161 @@
+package api
+
+import (
+ "testing"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRateLimitMiddleware(t *testing.T) {
+ t.Parallel()
+
+ newTestRouter := func(ratePerSec, burst float64) *RPCRouter {
+ return &RPCRouter{
+ rateLimitPerSec: ratePerSec,
+ rateLimitBurst: burst,
+ }
+ }
+
+ newTestContext := func(storage *rpc.SafeStorage, requestID uint64) *rpc.Context {
+ return &rpc.Context{
+ Storage: storage,
+ Request: rpc.Message{RequestID: requestID},
+ }
+ }
+
+ isRateLimited := func(ctx *rpc.Context) bool {
+ err := ctx.Response.Error()
+ return err != nil && err.Error() == "rate limit exceeded"
+ }
+
+ t.Run("allows requests within burst limit", func(t *testing.T) {
+ t.Parallel()
+
+ router := newTestRouter(10, 5)
+ storage := rpc.NewSafeStorage()
+
+ var allowed int
+ for i := 0; i < 5; i++ {
+ ctx := newTestContext(storage, uint64(i))
+ router.RateLimitMiddleware(ctx)
+ if !isRateLimited(ctx) {
+ allowed++
+ }
+ }
+
+ assert.Equal(t, 5, allowed, "all requests within burst should be allowed")
+ })
+
+ t.Run("blocks requests exceeding burst", func(t *testing.T) {
+ t.Parallel()
+
+ router := newTestRouter(10, 3)
+ storage := rpc.NewSafeStorage()
+
+ var allowed, rateLimited int
+ for i := 0; i < 5; i++ {
+ ctx := newTestContext(storage, uint64(i))
+ router.RateLimitMiddleware(ctx)
+ if isRateLimited(ctx) {
+ rateLimited++
+ } else {
+ allowed++
+ }
+ }
+
+ assert.Equal(t, 3, allowed, "only burst amount of requests should be allowed")
+ assert.Equal(t, 2, rateLimited, "excess requests should be rate limited")
+ })
+
+ t.Run("returns rate limit error message", func(t *testing.T) {
+ t.Parallel()
+
+ router := newTestRouter(10, 1)
+ storage := rpc.NewSafeStorage()
+
+ // First request - allowed
+ ctx1 := newTestContext(storage, 1)
+ router.RateLimitMiddleware(ctx1)
+ require.False(t, isRateLimited(ctx1), "first request should be allowed")
+
+ // Second request - should be rate limited
+ ctx2 := newTestContext(storage, 2)
+ router.RateLimitMiddleware(ctx2)
+
+ require.True(t, isRateLimited(ctx2), "second request should be rate limited")
+ assert.Equal(t, "rate limit exceeded", ctx2.Response.Error().Error())
+ })
+
+ t.Run("tokens refill over time", func(t *testing.T) {
+ t.Parallel()
+
+ router := newTestRouter(100, 2) // 100 tokens per second = 1 token per 10ms
+ storage := rpc.NewSafeStorage()
+
+ // Exhaust the bucket
+ for i := 0; i < 2; i++ {
+ ctx := newTestContext(storage, uint64(i))
+ router.RateLimitMiddleware(ctx)
+ require.False(t, isRateLimited(ctx), "initial requests should be allowed")
+ }
+
+ // This should be rate limited
+ ctx := newTestContext(storage, 100)
+ router.RateLimitMiddleware(ctx)
+ require.True(t, isRateLimited(ctx), "should be rate limited after exhausting bucket")
+
+ // Wait for tokens to refill (need 1 token, at 100/sec = 10ms per token)
+ time.Sleep(15 * time.Millisecond)
+
+ // Now it should work
+ ctx = newTestContext(storage, 101)
+ router.RateLimitMiddleware(ctx)
+ assert.False(t, isRateLimited(ctx), "should be allowed after refill")
+ })
+
+ t.Run("separate storage has separate buckets", func(t *testing.T) {
+ t.Parallel()
+
+ router := newTestRouter(10, 2)
+ storage1 := rpc.NewSafeStorage()
+ storage2 := rpc.NewSafeStorage()
+
+ // Exhaust storage1's bucket
+ for i := 0; i < 2; i++ {
+ ctx := newTestContext(storage1, uint64(i))
+ router.RateLimitMiddleware(ctx)
+ }
+
+ // storage1 should be rate limited
+ ctx1 := newTestContext(storage1, 100)
+ router.RateLimitMiddleware(ctx1)
+ assert.True(t, isRateLimited(ctx1), "storage1 should be rate limited")
+
+ // storage2 should still have its own bucket
+ ctx2 := newTestContext(storage2, 200)
+ router.RateLimitMiddleware(ctx2)
+ assert.False(t, isRateLimited(ctx2), "storage2 should have its own bucket")
+ })
+
+ t.Run("bucket persists in storage", func(t *testing.T) {
+ t.Parallel()
+
+ router := newTestRouter(10, 5)
+ storage := rpc.NewSafeStorage()
+
+ // Make one request
+ ctx := newTestContext(storage, 1)
+ router.RateLimitMiddleware(ctx)
+
+ // Check bucket is stored
+ val, ok := storage.Get(rateLimitStorageKey)
+ require.True(t, ok, "bucket should be stored")
+
+ bucket, ok := val.(*tokenBucket)
+ require.True(t, ok, "stored value should be a tokenBucket")
+ assert.Less(t, bucket.tokens, 5.0, "tokens should have been consumed")
+ })
+}
diff --git a/clearnode/api/rpc_router.go b/clearnode/api/rpc_router.go
index 220b55fea..73a4e2274 100644
--- a/clearnode/api/rpc_router.go
+++ b/clearnode/api/rpc_router.go
@@ -3,43 +3,64 @@ package api
import (
"time"
- "github.com/erc7824/nitrolite/clearnode/api/app_session_v1"
- "github.com/erc7824/nitrolite/clearnode/api/channel_v1"
- "github.com/erc7824/nitrolite/clearnode/api/node_v1"
- "github.com/erc7824/nitrolite/clearnode/api/user_v1"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/clearnode/store/database"
- "github.com/erc7824/nitrolite/clearnode/store/memory"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/clearnode/api/app_session_v1"
+ "github.com/layer-3/nitrolite/clearnode/api/apps_v1"
+ "github.com/layer-3/nitrolite/clearnode/api/channel_v1"
+ "github.com/layer-3/nitrolite/clearnode/api/node_v1"
+ "github.com/layer-3/nitrolite/clearnode/api/user_v1"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/clearnode/store/database"
+ "github.com/layer-3/nitrolite/clearnode/store/memory"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
type RPCRouter struct {
Node rpc.Node
lg log.Logger
runtimeMetrics metrics.RuntimeMetricExporter
+
+ rateLimitPerSec float64
+ rateLimitBurst float64
+}
+
+type RPCRouterConfig struct {
+ NodeVersion string
+ MinChallenge uint32
+
+ MaxParticipants int
+ MaxSessionDataLen int
+ MaxAppMetadataLen int
+ MaxRebalanceSignedUpdates int
+ MaxSessionKeyIDs int
+
+ RateLimitPerSec float64
+ RateLimitBurst float64
}
func NewRPCRouter(
- nodeVersion string,
- minChallenge uint32,
+ cfg RPCRouterConfig,
node rpc.Node,
signer sign.Signer,
dbStore database.DatabaseStore,
memoryStore memory.MemoryStore,
+ actionGateway *action_gateway.ActionGateway,
runtimeMetrics metrics.RuntimeMetricExporter,
logger log.Logger,
- maxParticipants, maxSessionDataLen, maxRebalanceSignedUpdates, maxSessionKeyIDs int,
) *RPCRouter {
r := &RPCRouter{
- Node: node,
- lg: logger.WithName("rpc-router"),
- runtimeMetrics: runtimeMetrics,
+ Node: node,
+ lg: logger.WithName("rpc-router"),
+ runtimeMetrics: runtimeMetrics,
+ rateLimitPerSec: cfg.RateLimitPerSec,
+ rateLimitBurst: cfg.RateLimitBurst,
}
r.Node.Use(r.ObservabilityMiddleware)
+ r.Node.Use(r.RateLimitMiddleware)
// Transaction wrapper helpers for each store type.
// wrapWithMetrics executes fn inside a DB transaction with a metricStore wrapper,
@@ -61,6 +82,12 @@ func NewRPCRouter(
useAppSessionV1StoreInTx := func(h app_session_v1.StoreTxHandler) error {
return wrapWithMetrics(func(ms *metricStore) error { return h(ms) })
}
+ useAppV1StoreInTx := func(h apps_v1.StoreTxHandler) error {
+ return wrapWithMetrics(func(ms *metricStore) error { return h(ms) })
+ }
+ useUserV1StoreInTx := func(h user_v1.StoreTxHandler) error {
+ return wrapWithMetrics(func(ms *metricStore) error { return h(ms) })
+ }
nodeAddress := signer.PublicKey().Address().String()
@@ -72,11 +99,12 @@ func NewRPCRouter(
panic("failed to create channel wallet signer: " + err.Error())
}
- channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, minChallenge, runtimeMetrics, maxSessionKeyIDs)
- appSessionV1Handler := app_session_v1.NewHandler(useAppSessionV1StoreInTx, memoryStore, signer, stateAdvancer, statePacker, nodeAddress, runtimeMetrics,
- maxParticipants, maxSessionDataLen, maxSessionKeyIDs, maxRebalanceSignedUpdates)
- nodeV1Handler := node_v1.NewHandler(memoryStore, nodeAddress, nodeVersion)
- userV1Handler := user_v1.NewHandler(dbStore)
+ channelV1Handler := channel_v1.NewHandler(useChannelV1StoreInTx, memoryStore, actionGateway, nodeChannelSigner, stateAdvancer, statePacker, nodeAddress, cfg.MinChallenge, runtimeMetrics, cfg.MaxSessionKeyIDs)
+ appSessionV1Handler := app_session_v1.NewHandler(useAppSessionV1StoreInTx, memoryStore, actionGateway, signer, stateAdvancer, statePacker, nodeAddress, runtimeMetrics,
+ cfg.MaxParticipants, cfg.MaxSessionDataLen, cfg.MaxSessionKeyIDs, cfg.MaxRebalanceSignedUpdates)
+ appsV1Handler := apps_v1.NewHandler(dbStore, useAppV1StoreInTx, actionGateway, cfg.MaxAppMetadataLen)
+ nodeV1Handler := node_v1.NewHandler(memoryStore, nodeAddress, cfg.NodeVersion)
+ userV1Handler := user_v1.NewHandler(dbStore, useUserV1StoreInTx, actionGateway)
appSessionV1Group := r.Node.NewGroup(rpc.AppSessionsV1Group.String())
appSessionV1Group.Handle(rpc.AppSessionsV1SubmitDepositStateMethod.String(), appSessionV1Handler.SubmitDepositState)
@@ -86,7 +114,7 @@ func NewRPCRouter(
appSessionV1Group.Handle(rpc.AppSessionsV1GetAppSessionsMethod.String(), appSessionV1Handler.GetAppSessions)
appSessionV1Group.Handle(rpc.AppSessionsV1SubmitSessionKeyStateMethod.String(), appSessionV1Handler.SubmitSessionKeyState)
appSessionV1Group.Handle(rpc.AppSessionsV1GetLastKeyStatesMethod.String(), appSessionV1Handler.GetLastKeyStates)
- if maxRebalanceSignedUpdates >= 2 {
+ if cfg.MaxRebalanceSignedUpdates >= 2 {
appSessionV1Group.Handle(rpc.AppSessionsV1RebalanceAppSessionsMethod.String(), appSessionV1Handler.RebalanceAppSessions)
}
@@ -105,9 +133,14 @@ func NewRPCRouter(
nodeV1Group.Handle(rpc.NodeV1GetAssetsMethod.String(), nodeV1Handler.GetAssets)
nodeV1Group.Handle(rpc.NodeV1GetConfigMethod.String(), nodeV1Handler.GetConfig)
+ appsV1Group := r.Node.NewGroup(rpc.AppsV1Group.String())
+ appsV1Group.Handle(rpc.AppsV1GetAppsMethod.String(), appsV1Handler.GetApps)
+ appsV1Group.Handle(rpc.AppsV1SubmitAppVersionMethod.String(), appsV1Handler.SubmitAppVersion)
+
userV1Group := r.Node.NewGroup(rpc.UserV1Group.String())
userV1Group.Handle(rpc.UserV1GetBalancesMethod.String(), userV1Handler.GetBalances)
userV1Group.Handle(rpc.UserV1GetTransactionsMethod.String(), userV1Handler.GetTransactions)
+ userV1Group.Handle(rpc.UserV1GetActionAllowancesMethod.String(), userV1Handler.GetActionAllowances)
return r
}
diff --git a/clearnode/api/user_v1/get_action_allowances.go b/clearnode/api/user_v1/get_action_allowances.go
new file mode 100644
index 000000000..5fa193a33
--- /dev/null
+++ b/clearnode/api/user_v1/get_action_allowances.go
@@ -0,0 +1,59 @@
+package user_v1
+
+import (
+ "strconv"
+
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+)
+
+// GetActionAllowances retrieves the action allowances for a user.
+func (h *Handler) GetActionAllowances(c *rpc.Context) {
+ var req rpc.UserV1GetActionAllowancesRequest
+ if err := c.Request.Payload.Translate(&req); err != nil {
+ c.Fail(err, "failed to parse parameters")
+ return
+ }
+
+ if req.Wallet == "" {
+ c.Fail(nil, "wallet is required")
+ return
+ }
+
+ var allowances []core.ActionAllowance
+ err := h.useStoreInTx(func(tx Store) error {
+ var err error
+ allowances, err = h.actionGateway.GetUserAllowances(h.store, req.Wallet)
+ if err != nil {
+ return rpc.Errorf("failed to retrieve action allowances: %w", err)
+ }
+
+ return nil
+ })
+ if err != nil {
+ c.Fail(err, "failed to retrieve action allowances")
+ return
+ }
+
+ rpcAllowances := make([]rpc.ActionAllowanceV1, len(allowances))
+ for i, a := range allowances {
+ rpcAllowances[i] = rpc.ActionAllowanceV1{
+ GatedAction: a.GatedAction,
+ TimeWindow: a.TimeWindow,
+ Allowance: strconv.FormatUint(a.Allowance, 10),
+ Used: strconv.FormatUint(a.Used, 10),
+ }
+ }
+
+ response := rpc.UserV1GetActionAllowancesResponse{
+ Allowances: rpcAllowances,
+ }
+
+ payload, err := rpc.NewPayload(response)
+ if err != nil {
+ c.Fail(err, "failed to create response")
+ return
+ }
+
+ c.Succeed(c.Request.Method, payload)
+}
diff --git a/clearnode/api/user_v1/get_action_allowances_test.go b/clearnode/api/user_v1/get_action_allowances_test.go
new file mode 100644
index 000000000..37cbcfac3
--- /dev/null
+++ b/clearnode/api/user_v1/get_action_allowances_test.go
@@ -0,0 +1,145 @@
+package user_v1
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+)
+
+func TestGetActionAllowances_Success(t *testing.T) {
+ mockStore := new(MockStore)
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ handler := NewHandler(
+ mockStore,
+ storeTxProvider,
+ &MockActionGateway{
+ Allowances: []core.ActionAllowance{
+ {GatedAction: core.GatedActionTransfer, TimeWindow: "24h", Allowance: 100, Used: 5},
+ {GatedAction: core.GatedActionAppSessionCreation, TimeWindow: "24h", Allowance: 50, Used: 0},
+ },
+ },
+ )
+
+ reqPayload := rpc.UserV1GetActionAllowancesRequest{
+ Wallet: "0x1234567890123456789012345678901234567890",
+ }
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload),
+ }
+
+ handler.GetActionAllowances(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+
+ var response rpc.UserV1GetActionAllowancesResponse
+ err = ctx.Response.Payload.Translate(&response)
+ require.NoError(t, err)
+
+ assert.Len(t, response.Allowances, 2)
+ assert.Equal(t, core.GatedActionTransfer, response.Allowances[0].GatedAction)
+ assert.Equal(t, "24h", response.Allowances[0].TimeWindow)
+ assert.Equal(t, "100", response.Allowances[0].Allowance)
+ assert.Equal(t, "5", response.Allowances[0].Used)
+ assert.Equal(t, core.GatedActionAppSessionCreation, response.Allowances[1].GatedAction)
+ assert.Equal(t, "50", response.Allowances[1].Allowance)
+ assert.Equal(t, "0", response.Allowances[1].Used)
+}
+
+func TestGetActionAllowances_EmptyResult(t *testing.T) {
+ mockStore := new(MockStore)
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{})
+
+ reqPayload := rpc.UserV1GetActionAllowancesRequest{
+ Wallet: "0x1234567890123456789012345678901234567890",
+ }
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload),
+ }
+
+ handler.GetActionAllowances(ctx)
+
+ require.NotNil(t, ctx.Response)
+ require.NoError(t, ctx.Response.Error())
+
+ var response rpc.UserV1GetActionAllowancesResponse
+ err = ctx.Response.Payload.Translate(&response)
+ require.NoError(t, err)
+
+ assert.Empty(t, response.Allowances)
+}
+
+func TestGetActionAllowances_MissingWallet(t *testing.T) {
+ mockStore := new(MockStore)
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{})
+
+ reqPayload := rpc.UserV1GetActionAllowancesRequest{}
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload),
+ }
+
+ handler.GetActionAllowances(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "wallet is required")
+}
+
+func TestGetActionAllowances_GatewayError(t *testing.T) {
+ mockStore := new(MockStore)
+ storeTxProvider := func(fn StoreTxHandler) error {
+ return fn(mockStore)
+ }
+
+ handler := NewHandler(mockStore, storeTxProvider, &MockActionGateway{
+ Err: fmt.Errorf("gateway failure"),
+ })
+
+ reqPayload := rpc.UserV1GetActionAllowancesRequest{
+ Wallet: "0x1234567890123456789012345678901234567890",
+ }
+ payload, err := rpc.NewPayload(reqPayload)
+ require.NoError(t, err)
+
+ ctx := &rpc.Context{
+ Context: context.Background(),
+ Request: rpc.NewRequest(1, "user.v1.get_action_allowances", payload),
+ }
+
+ handler.GetActionAllowances(ctx)
+
+ require.NotNil(t, ctx.Response)
+ err = ctx.Response.Error()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to retrieve action allowances")
+}
diff --git a/clearnode/api/user_v1/get_balances.go b/clearnode/api/user_v1/get_balances.go
index b85d9a9c7..44efc99f3 100644
--- a/clearnode/api/user_v1/get_balances.go
+++ b/clearnode/api/user_v1/get_balances.go
@@ -1,7 +1,7 @@
package user_v1
import (
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetBalances retrieves the balances of the user.
diff --git a/clearnode/api/user_v1/get_balances_test.go b/clearnode/api/user_v1/get_balances_test.go
index 4a8613eee..c07b08ccb 100644
--- a/clearnode/api/user_v1/get_balances_test.go
+++ b/clearnode/api/user_v1/get_balances_test.go
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetBalances_Success(t *testing.T) {
diff --git a/clearnode/api/user_v1/get_transactions.go b/clearnode/api/user_v1/get_transactions.go
index 75b690002..9c7c1bd59 100644
--- a/clearnode/api/user_v1/get_transactions.go
+++ b/clearnode/api/user_v1/get_transactions.go
@@ -1,8 +1,8 @@
package user_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// GetTransactions retrieves transaction history for a user with optional filters.
diff --git a/clearnode/api/user_v1/get_transactions_test.go b/clearnode/api/user_v1/get_transactions_test.go
index 83b76d8c7..ed41441fa 100644
--- a/clearnode/api/user_v1/get_transactions_test.go
+++ b/clearnode/api/user_v1/get_transactions_test.go
@@ -9,8 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func TestGetTransactions_Success(t *testing.T) {
diff --git a/clearnode/api/user_v1/handler.go b/clearnode/api/user_v1/handler.go
index 2ebdb7c52..d0d68d10a 100644
--- a/clearnode/api/user_v1/handler.go
+++ b/clearnode/api/user_v1/handler.go
@@ -2,14 +2,20 @@ package user_v1
// Handler manages user data operations and provides RPC endpoints.
type Handler struct {
- store Store
+ store Store
+ useStoreInTx StoreTxProvider
+ actionGateway ActionGateway
}
// NewHandler creates a new Handler instance with the provided dependencies.
func NewHandler(
store Store,
+ useStoreInTx StoreTxProvider,
+ actionGateway ActionGateway,
) *Handler {
return &Handler{
- store: store,
+ store: store,
+ useStoreInTx: useStoreInTx,
+ actionGateway: actionGateway,
}
}
diff --git a/clearnode/api/user_v1/interface.go b/clearnode/api/user_v1/interface.go
index f8e68edde..06f085a06 100644
--- a/clearnode/api/user_v1/interface.go
+++ b/clearnode/api/user_v1/interface.go
@@ -1,9 +1,19 @@
package user_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/core"
)
+// StoreTxHandler is a function that executes Store operations within a transaction.
+// If the handler returns an error, the transaction is rolled back; otherwise it's committed.
+type StoreTxHandler func(Store) error
+
+// StoreTxProvider wraps Store operations in a database transaction.
+// It accepts a StoreTxHandler and manages transaction lifecycle (begin, commit, rollback).
+// Returns an error if the handler fails or the transaction cannot be committed.
+type StoreTxProvider func(StoreTxHandler) error
+
// Store defines the persistence layer interface for user data management.
// All methods should be implemented to work within database transactions.
type Store interface {
@@ -17,4 +27,11 @@ type Store interface {
FromTime *uint64,
ToTime *uint64,
Paginate *core.PaginationParams) ([]core.Transaction, core.PaginationMetadata, error)
+
+ action_gateway.Store
+}
+
+type ActionGateway interface {
+ // GetUserAllowances retrieves the action allowances for a user, which define what actions the user is permitted to perform.
+ GetUserAllowances(tx action_gateway.Store, userAddress string) ([]core.ActionAllowance, error)
}
diff --git a/clearnode/api/user_v1/testing.go b/clearnode/api/user_v1/testing.go
index 98079450b..be69f2c72 100644
--- a/clearnode/api/user_v1/testing.go
+++ b/clearnode/api/user_v1/testing.go
@@ -1,9 +1,13 @@
package user_v1
import (
+ "time"
+
+ "github.com/shopspring/decimal"
"github.com/stretchr/testify/mock"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// MockStore is a mock implementation of the Store interface
@@ -30,3 +34,32 @@ func (m *MockStore) GetUserTransactions(Wallet string, Asset *string, TxType *co
}
return args.Get(0).([]core.Transaction), metadata, args.Error(2)
}
+
+func (m *MockStore) GetAppCount(_ string) (uint64, error) {
+ return 0, nil
+}
+
+func (m *MockStore) GetTotalUserStaked(_ string) (decimal.Decimal, error) {
+ return decimal.Zero, nil
+}
+
+func (m *MockStore) RecordAction(_ string, _ core.GatedAction) error {
+ return nil
+}
+
+func (m *MockStore) GetUserActionCount(_ string, _ core.GatedAction, _ time.Duration) (uint64, error) {
+ return 0, nil
+}
+
+func (m *MockStore) GetUserActionCounts(_ string, _ time.Duration) (map[core.GatedAction]uint64, error) {
+ return nil, nil
+}
+
+type MockActionGateway struct {
+ Allowances []core.ActionAllowance
+ Err error
+}
+
+func (m *MockActionGateway) GetUserAllowances(_ action_gateway.Store, _ string) ([]core.ActionAllowance, error) {
+ return m.Allowances, m.Err
+}
diff --git a/clearnode/api/user_v1/utils.go b/clearnode/api/user_v1/utils.go
index 226186dbd..c8a5f0930 100644
--- a/clearnode/api/user_v1/utils.go
+++ b/clearnode/api/user_v1/utils.go
@@ -1,8 +1,8 @@
package user_v1
import (
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
func mapTransactionV1(tx core.Transaction) rpc.TransactionV1 {
diff --git a/clearnode/api/utils.go b/clearnode/api/utils.go
index 4aa3d4b84..dfbc54469 100644
--- a/clearnode/api/utils.go
+++ b/clearnode/api/utils.go
@@ -1,6 +1,6 @@
package api
-import "github.com/erc7824/nitrolite/pkg/rpc"
+import "github.com/layer-3/nitrolite/pkg/rpc"
func getMethodPath(c *rpc.Context) string {
switch c.Request.Method {
diff --git a/clearnode/blockchain_worker.go b/clearnode/blockchain_worker.go
index 2c7c518d0..e63b4d9aa 100644
--- a/clearnode/blockchain_worker.go
+++ b/clearnode/blockchain_worker.go
@@ -6,9 +6,9 @@ import (
"sync"
"time"
- "github.com/erc7824/nitrolite/clearnode/store/database"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/clearnode/store/database"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
)
type BlockchainWorkerStore interface {
@@ -38,13 +38,13 @@ const (
type BlockchainWorker struct {
blockchainID uint64
- client core.Client
+ client core.BlockchainClient
store BlockchainWorkerStore
logger log.Logger
metrics MetricsExporter
}
-func NewBlockchainWorker(blockchainID uint64, client core.Client, store BlockchainWorkerStore, logger log.Logger, m MetricsExporter) *BlockchainWorker {
+func NewBlockchainWorker(blockchainID uint64, client core.BlockchainClient, store BlockchainWorkerStore, logger log.Logger, m MetricsExporter) *BlockchainWorker {
return &BlockchainWorker{
blockchainID: blockchainID,
client: client,
diff --git a/clearnode/chart/README.md b/clearnode/chart/README.md
index 1656ae503..7ee043e87 100644
--- a/clearnode/chart/README.md
+++ b/clearnode/chart/README.md
@@ -17,7 +17,7 @@ Clearnode Helm chart
To install the chart with the release name `my-release`:
```bash
-helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main
+helm install my-release git+https://github.com/layer-3/clearnode@chart?ref=main
```
The command deploys Clearnode on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
@@ -54,7 +54,7 @@ helm delete my-release
| config.secretEnvs | object | `{}` | Additional environment variables to be stored in a secret |
| extraLabels | object | `{}` | Additional labels to add to all resources |
| fullnameOverride | string | `""` | Override the full name |
-| image.repository | string | `"ghcr.io/erc7824/clearnode"` | Docker image repository |
+| image.repository | string | `"ghcr.io/layer-3/clearnode"` | Docker image repository |
| image.tag | string | `"0.0.1"` | Docker image tag |
| imagePullSecret | string | `""` | Image pull secret name |
| metrics.enabled | bool | `true` | Enable Prometheus metrics |
@@ -114,7 +114,7 @@ For managing sensitive values like API keys and credentials, you can use `helm-s
3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix:
```bash
- helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \
+ helm upgrade --install my-release git+https://github.com/layer-3/clearnode@chart?ref=main \
-f values.yaml \
-f secrets://secrets.yaml
```
diff --git a/clearnode/chart/README.md.gotmpl b/clearnode/chart/README.md.gotmpl
index 96c24668b..352b5f66e 100644
--- a/clearnode/chart/README.md.gotmpl
+++ b/clearnode/chart/README.md.gotmpl
@@ -17,7 +17,7 @@
To install the chart with the release name `my-release`:
```bash
-helm install my-release git+https://github.com/erc7824/clearnode@chart?ref=main
+helm install my-release git+https://github.com/layer-3/clearnode@chart?ref=main
```
The command deploys Clearnode on the Kubernetes cluster with default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
@@ -61,7 +61,7 @@ For managing sensitive values like API keys and credentials, you can use `helm-s
3. When deploying or upgrading, reference your secrets file with the `secrets://` prefix:
```bash
- helm upgrade --install my-release git+https://github.com/erc7824/clearnode@chart?ref=main \
+ helm upgrade --install my-release git+https://github.com/layer-3/clearnode@chart?ref=main \
-f values.yaml \
-f secrets://secrets.yaml
```
diff --git a/clearnode/chart/config/prod/action_gateway.yaml b/clearnode/chart/config/prod/action_gateway.yaml
new file mode 100644
index 000000000..67ac1e8bc
--- /dev/null
+++ b/clearnode/chart/config/prod/action_gateway.yaml
@@ -0,0 +1,19 @@
+# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml
+level_step_tokens: "100"
+app_cost: "200"
+action_gates:
+ transfer:
+ free_actions_allowance: 10
+ increase_per_level: 5
+ app_session_creation:
+ free_actions_allowance: 5
+ increase_per_level: 2
+ app_session_operation:
+ free_actions_allowance: 20
+ increase_per_level: 10
+ app_session_deposit:
+ free_actions_allowance: 15
+ increase_per_level: 7
+ app_session_withdrawal:
+ free_actions_allowance: 15
+ increase_per_level: 7
diff --git a/clearnode/chart/config/prod/clearnode.yaml b/clearnode/chart/config/prod/clearnode.yaml
index 351caf01f..10a5a9f10 100644
--- a/clearnode/chart/config/prod/clearnode.yaml
+++ b/clearnode/chart/config/prod/clearnode.yaml
@@ -12,7 +12,7 @@ config:
MSG_EXPIRY_TIME: "60"
image:
- repository: ghcr.io/erc7824/nitrolite/clearnode
+ repository: ghcr.io/layer-3/nitrolite/clearnode
tag: 0.3.1-rc.18
service:
diff --git a/clearnode/chart/config/sandbox-v1/action_gateway.yaml b/clearnode/chart/config/sandbox-v1/action_gateway.yaml
new file mode 100644
index 000000000..277b774ec
--- /dev/null
+++ b/clearnode/chart/config/sandbox-v1/action_gateway.yaml
@@ -0,0 +1,19 @@
+# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml
+level_step_tokens: "200"
+app_cost: "2000"
+action_gates:
+ transfer:
+ free_actions_allowance: 10
+ increase_per_level: 5
+ app_session_creation:
+ free_actions_allowance: 50
+ increase_per_level: 5
+ app_session_operation:
+ free_actions_allowance: 100
+ increase_per_level: 15
+ app_session_deposit:
+ free_actions_allowance: 50
+ increase_per_level: 5
+ app_session_withdrawal:
+ free_actions_allowance: 50
+ increase_per_level: 10
diff --git a/clearnode/chart/config/sandbox-v1/assets.yaml b/clearnode/chart/config/sandbox-v1/assets.yaml
new file mode 100644
index 000000000..20a78ce7c
--- /dev/null
+++ b/clearnode/chart/config/sandbox-v1/assets.yaml
@@ -0,0 +1,47 @@
+assets:
+ - name: "Yellow USD"
+ symbol: "yusd"
+ decimals: 6
+ suggested_blockchain_id: 11155111
+ tokens:
+ - blockchain_id: 11155111
+ address: "0xD3E8Eb01Ae895262f187c4aAe936eC5c0665bbf8"
+ decimals: 6
+ - blockchain_id: 80002
+ address: "0x0827b6aAA03475A8BF59Ee1A2bD76903DDFbaDB6"
+ decimals: 8
+ - name: "BNB"
+ symbol: "bnb"
+ decimals: 18
+ suggested_blockchain_id: 11155111
+ tokens:
+ - blockchain_id: 11155111
+ address: "0x719a00F9e8b831335F156337cEF7dC48986b2e84"
+ decimals: 18
+ - blockchain_id: 80002
+ address: "0x9d8193e5655a36FFB9CD7D88D31c91d2650896D0"
+ decimals: 18
+ - name: "Ether"
+ symbol: "eth"
+ decimals: 18
+ suggested_blockchain_id: 11155111
+ tokens:
+ - blockchain_id: 11155111
+ address: "0x0000000000000000000000000000000000000000"
+ decimals: 18
+ - name: "POL"
+ symbol: "pol"
+ decimals: 18
+ suggested_blockchain_id: 80002
+ tokens:
+ - blockchain_id: 80002
+ address: "0x0000000000000000000000000000000000000000"
+ decimals: 18
+ - name: "Yellow"
+ symbol: "yellow"
+ decimals: 18
+ suggested_blockchain_id: 11155111
+ tokens:
+ - blockchain_id: 11155111
+ address: "0x236eB848C95b231299B4AA9f56c73D6893462720"
+ decimals: 18
diff --git a/clearnode/chart/config/sandbox-v1/blockchains.yaml b/clearnode/chart/config/sandbox-v1/blockchains.yaml
new file mode 100644
index 000000000..e7615d224
--- /dev/null
+++ b/clearnode/chart/config/sandbox-v1/blockchains.yaml
@@ -0,0 +1,12 @@
+blockchains:
+- name: "ethereum_sepolia"
+ id: 11155111
+ channel_hub_address: "0xb7bE0E2007dDF320d680942cb9e008F986E74F83"
+ channel_hub_sig_validators:
+ 1: "0x2aC63456d78Cf2E2FDAf45cbed45b5d29907f4ac"
+ locking_contract_address: "0x9B3D4dA5A37857F17648CC4d78Bbae0A681C02c6"
+- name: "polygon_amoy"
+ id: 80002
+ channel_hub_address: "0x55D6f0A0322606447fbc612Cf58014Faed65aF9D"
+ channel_hub_sig_validators:
+ 1: "0x87825ACa5f4B9c3dc8B5aa3352724eDF5135D892"
diff --git a/clearnode/chart/config/sandbox-v1/clearnode.yaml b/clearnode/chart/config/sandbox-v1/clearnode.yaml
new file mode 100644
index 000000000..5dcd61f4f
--- /dev/null
+++ b/clearnode/chart/config/sandbox-v1/clearnode.yaml
@@ -0,0 +1,73 @@
+config:
+ args: ["clearnode"]
+ logLevel: info
+ database:
+ driver: postgres
+ host: pgbouncer
+ port: 5432
+ name: clearnode_sandbox_v1
+ user: clearnode_sandbox_v1_admin
+ gcpSaSecret: gcp-kms-signer-sa
+ envSecret: clearnode-secret-env
+ extraEnvs:
+ CLEARNODE_DATABASE_MAX_OPEN_CONNS: "10"
+ CLEARNODE_DATABASE_MAX_IDLE_CONNS: "2"
+ CLEARNODE_DATABASE_CONN_MAX_LIFETIME_SEC: "3600"
+ CLEARNODE_DATABASE_CONN_MAX_IDLE_TIME_SEC: "600"
+ CLEARNODE_SIGNER_TYPE: "gcp-kms"
+ CLEARNODE_GCP_KMS_KEY_NAME: "projects/ynet-stage/locations/europe-central2/keyRings/clearnode-signers-eu/cryptoKeys/sandbox-v1-a/cryptoKeyVersions/1"
+ CLEARNODE_MAX_PARTICIPANTS: "32"
+ CLEARNODE_MAX_SESSION_DATA_LEN: "1024"
+ CLEARNODE_MAX_SIGNED_UPDATES: "0"
+ CLEARNODE_MAX_SESSION_KEY_IDS: "256"
+
+image:
+ repository: ghcr.io/layer-3/nitrolite/clearnode
+ tag: v1.2.0
+
+service:
+ http:
+ enabled: true
+ port: 7824
+ path: /v1/ws
+
+metrics:
+ enabled: true
+ podmonitoring:
+ enabled: true
+ port: 4242
+ endpoint: "/metrics"
+
+resources:
+ limits:
+ cpu: 2000m
+ memory: 2Gi
+ ephemeral-storage: 256Mi
+ requests:
+ cpu: 2000m
+ memory: 2Gi
+ ephemeral-storage: 256Mi
+
+autoscaling:
+ enabled: false
+
+networking:
+ externalHostname: clearnode-sandbox.yellow.org
+ tlsClusterIssuer: zerossl-prod
+ gateway:
+ enabled: false
+ ingress:
+ enabled: true
+ className: nginx
+ tls:
+ enabled: true
+ annotations:
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
+ nginx.ingress.kubernetes.io/proxy-buffering: "off"
+
+imagePullSecret: ghcr-pull
+
+stressTest:
+ enabled: false
diff --git a/clearnode/chart/config/sandbox/action_gateway.yaml b/clearnode/chart/config/sandbox/action_gateway.yaml
new file mode 100644
index 000000000..67ac1e8bc
--- /dev/null
+++ b/clearnode/chart/config/sandbox/action_gateway.yaml
@@ -0,0 +1,19 @@
+# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml
+level_step_tokens: "100"
+app_cost: "200"
+action_gates:
+ transfer:
+ free_actions_allowance: 10
+ increase_per_level: 5
+ app_session_creation:
+ free_actions_allowance: 5
+ increase_per_level: 2
+ app_session_operation:
+ free_actions_allowance: 20
+ increase_per_level: 10
+ app_session_deposit:
+ free_actions_allowance: 15
+ increase_per_level: 7
+ app_session_withdrawal:
+ free_actions_allowance: 15
+ increase_per_level: 7
diff --git a/clearnode/chart/config/sandbox/clearnode.yaml b/clearnode/chart/config/sandbox/clearnode.yaml
index 808785df9..420ccec06 100644
--- a/clearnode/chart/config/sandbox/clearnode.yaml
+++ b/clearnode/chart/config/sandbox/clearnode.yaml
@@ -12,7 +12,7 @@ config:
MSG_EXPIRY_TIME: "60"
image:
- repository: ghcr.io/erc7824/nitrolite/clearnode
+ repository: ghcr.io/layer-3/nitrolite/clearnode
tag: 6069d30
service:
diff --git a/clearnode/chart/config/uat/assets.yaml b/clearnode/chart/config/uat/assets.yaml
deleted file mode 100644
index a58b93106..000000000
--- a/clearnode/chart/config/uat/assets.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-assets:
- - name: "USD Coin"
- symbol: "usdc"
- tokens:
- - blockchain_id: 137
- address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"
- decimals: 6
- - name: "ytest.USD"
- symbol: "ytest.usd"
- tokens:
- - blockchain_id: 11155111
- address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
- decimals: 6
- - name: "wETH"
- symbol: "weth"
- tokens:
- - blockchain_id: 137
- address: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"
- decimals: 18
diff --git a/clearnode/chart/config/uat/blockchains.yaml b/clearnode/chart/config/uat/blockchains.yaml
deleted file mode 100644
index daba54318..000000000
--- a/clearnode/chart/config/uat/blockchains.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-default_contract_addresses:
- custody: "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6"
- adjudicator: "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C"
-
-blockchains:
-- name: polygon
- id: 137
- contract_addresses:
- balance_checker: "0x2352c63A83f9Fd126af8676146721Fa00924d7e4"
-- name: ethereum_sepolia
- id: 11155111
- contract_addresses:
- balance_checker: "0xBfbCed302deD369855fc5f7668356e123ca4B329"
diff --git a/clearnode/chart/config/uat/clearnode.yaml b/clearnode/chart/config/uat/clearnode.yaml
deleted file mode 100644
index 8ab3f821f..000000000
--- a/clearnode/chart/config/uat/clearnode.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-config:
- args: ["clearnode"]
- logLevel: debug
- database:
- driver: postgres
- host: postgresql.core
- port: 5432
- name: clearnet_uat
- user: clearnet_uat_admin
- envSecret: ""
- extraEnvs:
- MSG_EXPIRY_TIME: "60"
-
-image:
- repository: ghcr.io/erc7824/nitrolite/clearnode
- tag: c9c4d4c
-
-service:
- http:
- enabled: true
- port: 8000
- path: /
-
-metrics:
- enabled: true
- podmonitoring:
- enabled: true
- port: 4242
- endpoint: "/metrics"
-
-resources:
- limits:
- cpu: 100m
- memory: 256Mi
- ephemeral-storage: 100Mi
- requests:
- cpu: 100m
- memory: 256Mi
- ephemeral-storage: 100Mi
-
-autoscaling:
- enabled: false
-
-networking:
- externalHostname: canarynet.yellow.com
- tlsClusterIssuer: zerossl-prod
- gateway:
- enabled: false
- ingress:
- enabled: true
- className: nginx
- tls:
- enabled: true
-
-imagePullSecret: ghcr-pull
diff --git a/clearnode/chart/config/uat/secrets.yaml b/clearnode/chart/config/uat/secrets.yaml
deleted file mode 100644
index 33534c8d2..000000000
--- a/clearnode/chart/config/uat/secrets.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-config:
- database:
- password: ref+tfstategs://terraform-state-deploy/gke-uat-postgresql-admin/default.tfstate/output.postgresql_user_passwords["clearnet_uat_admin"]
- secretEnvs:
- BROKER_PRIVATE_KEY: ref+gcpsecrets://ynet-stage/clearnet-uat-broker-private-key?version=latest
- POLYGON_BLOCKCHAIN_RPC: ref+gcpsecrets://ynet-stage/clearnet-uat-polygon-blockchain-rpc?version=latest
- ETHEREUM_SEPOLIA_BLOCKCHAIN_RPC: ref+gcpsecrets://ynet-stage/clearnet-uat-eth-sepolia-blockchain-rpc?version=latest
diff --git a/clearnode/chart/config/v1-rc/action_gateway.yaml b/clearnode/chart/config/v1-rc/action_gateway.yaml
new file mode 100644
index 000000000..67ac1e8bc
--- /dev/null
+++ b/clearnode/chart/config/v1-rc/action_gateway.yaml
@@ -0,0 +1,19 @@
+# yaml-language-server: $schema=../../../config/schemas/action_gateway_schema.yaml
+level_step_tokens: "100"
+app_cost: "200"
+action_gates:
+ transfer:
+ free_actions_allowance: 10
+ increase_per_level: 5
+ app_session_creation:
+ free_actions_allowance: 5
+ increase_per_level: 2
+ app_session_operation:
+ free_actions_allowance: 20
+ increase_per_level: 10
+ app_session_deposit:
+ free_actions_allowance: 15
+ increase_per_level: 7
+ app_session_withdrawal:
+ free_actions_allowance: 15
+ increase_per_level: 7
diff --git a/clearnode/chart/config/v1-rc/assets.yaml b/clearnode/chart/config/v1-rc/assets.yaml
index 2290bd4c1..80c7f299e 100644
--- a/clearnode/chart/config/v1-rc/assets.yaml
+++ b/clearnode/chart/config/v1-rc/assets.yaml
@@ -23,4 +23,11 @@ assets:
- blockchain_id: 11155111
address: "0x0000000000000000000000000000000000000000"
decimals: 18
-
+ - name: "Yellow"
+ symbol: "yellow"
+ decimals: 18
+ suggested_blockchain_id: 11155111
+ tokens:
+ - blockchain_id: 11155111
+ address: "0xB1aA0ac73B5E648a57db2d9342f11c471FcC85F1"
+ decimals: 18
diff --git a/clearnode/chart/config/v1-rc/blockchains.yaml b/clearnode/chart/config/v1-rc/blockchains.yaml
index f338ed26d..2c63d8745 100644
--- a/clearnode/chart/config/v1-rc/blockchains.yaml
+++ b/clearnode/chart/config/v1-rc/blockchains.yaml
@@ -1,7 +1,7 @@
-default_contract_addresses:
- channel_hub: "0x09ffB9eA86F42e3e3B5E34650311d7E595dFB769"
-
blockchains:
- name: "ethereum_sepolia"
id: 11155111
channel_hub_address: "0x09ffB9eA86F42e3e3B5E34650311d7E595dFB769"
+ channel_hub_sig_validators:
+ 1: "0xae5f5f520d0edb5c2ead31381f6d1d1fc2a7c36b"
+ locking_contract_address: "0x89A969AE9D7e695DF948Ba744e72A97769C5C1ef"
diff --git a/clearnode/chart/config/v1-rc/clearnode.yaml b/clearnode/chart/config/v1-rc/clearnode.yaml
index 3a21f8b52..b92af869c 100644
--- a/clearnode/chart/config/v1-rc/clearnode.yaml
+++ b/clearnode/chart/config/v1-rc/clearnode.yaml
@@ -22,7 +22,7 @@ config:
CLEARNODE_MAX_SESSION_KEY_IDS: "256"
image:
- repository: ghcr.io/erc7824/nitrolite/clearnode
+ repository: ghcr.io/layer-3/nitrolite/clearnode
tag: v1.0.0-rc.0
service:
diff --git a/clearnode/chart/templates/configmap.yaml b/clearnode/chart/templates/configmap.yaml
index fa64af12d..ff5a9d6f5 100644
--- a/clearnode/chart/templates/configmap.yaml
+++ b/clearnode/chart/templates/configmap.yaml
@@ -13,3 +13,5 @@ data:
{{ .Values.config.blockchains | indent 4 }}
assets.yaml: |-
{{ .Values.config.assets | indent 4 }}
+ action_gateway.yaml: |-
+{{ .Values.config.actionGateway | indent 4 }}
diff --git a/clearnode/chart/templates/debug-deployment.yaml b/clearnode/chart/templates/debug-deployment.yaml
new file mode 100644
index 000000000..7fa273de8
--- /dev/null
+++ b/clearnode/chart/templates/debug-deployment.yaml
@@ -0,0 +1,65 @@
+{{- if .Values.debug.enabled }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "clearnode.common.fullname" . }}-debug
+ labels:
+ {{- include "clearnode.common.labels" . | nindent 4 }}
+ app.kubernetes.io/component: debug
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ {{- include "clearnode.common.selectorLabels" . | nindent 6 }}
+ app.kubernetes.io/component: debug
+ template:
+ metadata:
+ labels:
+ {{- include "clearnode.common.selectorLabels" . | nindent 8 }}
+ app.kubernetes.io/component: debug
+ spec:
+ {{- with .Values.serviceAccount }}
+ serviceAccountName: {{ . }}
+ {{- end }}
+ containers:
+ - name: debug
+ image: {{ include "clearnode.component.image" .Values.image }}
+ imagePullPolicy: IfNotPresent
+ command: ["sleep", "infinity"]
+ env:
+ {{- include "clearnode.common.env" . | nindent 12 }}
+ {{- if or .Values.config.secretEnvs .Values.config.envSecret }}
+ envFrom:
+ {{- if .Values.config.envSecret }}
+ - secretRef:
+ name: {{ .Values.config.envSecret }}
+ {{- end }}
+ {{- if .Values.config.secretEnvs }}
+ - secretRef:
+ name: {{ include "clearnode.common.fullname" . }}-secret-env
+ {{- end }}
+ {{- end }}
+ volumeMounts:
+ - name: config
+ mountPath: /app/config
+ {{- if .Values.config.gcpSaSecret }}
+ - name: gcp-sa
+ mountPath: /etc/gcp
+ readOnly: true
+ {{- end }}
+ resources:
+ {{- toYaml .Values.debug.resources | nindent 12 }}
+ volumes:
+ - name: config
+ configMap:
+ name: {{ include "clearnode.common.fullname" . }}-config
+ {{- if .Values.config.gcpSaSecret }}
+ - name: gcp-sa
+ secret:
+ secretName: {{ .Values.config.gcpSaSecret }}
+ {{- end }}
+ {{- include "clearnode.common.imagePullSecrets" . | nindent 6 }}
+ {{- include "clearnode.common.nodeSelectorLabels" . | nindent 6 }}
+ {{- include "clearnode.common.affinity" . | nindent 6 }}
+ {{- include "clearnode.common.tolerations" . | nindent 6 }}
+{{- end }}
diff --git a/clearnode/chart/templates/deployment.yaml b/clearnode/chart/templates/full-deployment.yaml
similarity index 98%
rename from clearnode/chart/templates/deployment.yaml
rename to clearnode/chart/templates/full-deployment.yaml
index 10928e5eb..26dae6772 100644
--- a/clearnode/chart/templates/deployment.yaml
+++ b/clearnode/chart/templates/full-deployment.yaml
@@ -4,6 +4,7 @@ metadata:
name: {{ include "clearnode.common.fullname" . }}
labels:
{{- include "clearnode.common.labels" . | nindent 4 }}
+ app.kubernetes.io/component: server
spec:
{{- include "clearnode.component.replicaCount" . | nindent 2 }}
strategy:
diff --git a/clearnode/chart/templates/helpers/_ingress.tpl b/clearnode/chart/templates/helpers/_ingress.tpl
index e6116e714..3fb3d2ede 100644
--- a/clearnode/chart/templates/helpers/_ingress.tpl
+++ b/clearnode/chart/templates/helpers/_ingress.tpl
@@ -26,6 +26,7 @@ nginx.ingress.kubernetes.io/ssl-redirect: "true"
{{- if .Values.networking.ingress.grpc }}
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
{{- end }}
+nginx.ingress.kubernetes.io/rewrite-target: /ws
{{- with .Values.networking.ingress.annotations }}
{{ toYaml . }}
{{- end }}
diff --git a/clearnode/chart/values.yaml b/clearnode/chart/values.yaml
index f4e303c2c..4ba7a8b51 100644
--- a/clearnode/chart/values.yaml
+++ b/clearnode/chart/values.yaml
@@ -37,13 +37,15 @@ config:
blockchains: ""
# -- Assets configuration
assets: ""
+ # -- Action Gateway configuration
+ actionGateway: ""
# -- Number of replicas
replicaCount: 1
image:
# -- Docker image repository
- repository: ghcr.io/erc7824/nitrolite/clearnode
+ repository: ghcr.io/layer-3/nitrolite/clearnode
# -- Docker image tag
tag: v1.0.0-rc.0
@@ -53,8 +55,8 @@ service:
enabled: true
# -- HTTP service port
port: 7824
- # -- HTTP service path
- path: /
+ # -- HTTP service path (used by ingress)
+ path: /ws
metrics:
# -- Enable Prometheus metrics
@@ -150,6 +152,18 @@ affinity: {}
# -- Additional labels to add to all resources
extraLabels: {}
+debug:
+ # -- Enable debug deployment (idle container for exec debugging)
+ enabled: false
+ # -- Resource requests for debug container
+ resources:
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ limits:
+ cpu: 100m
+ memory: 128Mi
+
stressTest:
# -- Enable stress test pods (helm test)
enabled: false
diff --git a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql
index f84923a28..fd7ba50a3 100644
--- a/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql
+++ b/clearnode/config/migrations/postgres/20251222000000_initial_schema.sql
@@ -87,16 +87,30 @@ CREATE TABLE transactions (
);
CREATE INDEX idx_transactions_type ON transactions(tx_type);
+CREATE INDEX idx_transactions_type_created ON transactions(tx_type, created_at);
CREATE INDEX idx_transactions_from_account ON transactions(from_account);
CREATE INDEX idx_transactions_to_account ON transactions(to_account);
CREATE INDEX idx_transactions_from_to_type ON transactions(from_account, to_account, tx_type);
CREATE INDEX idx_transactions_from_comp ON transactions(from_account, asset_symbol, created_at DESC);
CREATE INDEX idx_transactions_to_comp ON transactions(to_account, asset_symbol, created_at DESC);
+-- Application registry
+CREATE TABLE apps_v1 (
+ id VARCHAR(66) PRIMARY KEY,
+ owner_wallet CHAR(42) NOT NULL,
+ metadata TEXT NOT NULL,
+ version NUMERIC(20,0) NOT NULL DEFAULT 1,
+ creation_approval_not_required BOOLEAN NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_apps_v1_owner_wallet ON apps_v1(owner_wallet);
+
-- App Sessions table: Application sessions
CREATE TABLE app_sessions_v1 (
id CHAR(66) PRIMARY KEY,
- application VARCHAR NOT NULL,
+ application_id VARCHAR NOT NULL,
nonce NUMERIC(20,0) NOT NULL,
session_data TEXT NOT NULL,
quorum SMALLINT NOT NULL DEFAULT 100,
@@ -106,7 +120,7 @@ CREATE TABLE app_sessions_v1 (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-CREATE INDEX idx_app_sessions_v1_application ON app_sessions_v1(application);
+CREATE INDEX idx_app_sessions_v1_application ON app_sessions_v1(application_id);
CREATE INDEX idx_app_sessions_v1_status ON app_sessions_v1(status);
-- App Session Participants table: Participants in application sessions
@@ -122,7 +136,7 @@ CREATE INDEX idx_app_session_participants_v1_wallet ON app_session_participants_
-- App Ledger table: Internal ledger entries for application sessions
CREATE TABLE app_ledger_v1 (
- id CHAR(36) PRIMARY KEY, -- UUID
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id CHAR(66) NOT NULL, -- Session ID
asset_symbol VARCHAR(20) NOT NULL,
wallet CHAR(42) NOT NULL,
@@ -246,7 +260,55 @@ CREATE TABLE user_balances (
CREATE INDEX idx_user_balances_user_wallet ON user_balances(user_wallet);
+-- User staked table: Stores staked amounts per user per blockchain
+CREATE TABLE user_staked_v1 (
+ user_wallet CHAR(42) NOT NULL,
+ blockchain_id NUMERIC(20,0) NOT NULL,
+ amount NUMERIC(78, 18) NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (user_wallet, blockchain_id)
+);
+
+CREATE INDEX idx_user_staked_v1_user_wallet ON user_staked_v1(user_wallet);
+
+-- Action log table: Records user actions for rate limiting and auditing
+CREATE TABLE action_log_v1 (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_wallet CHAR(42) NOT NULL,
+ gated_action SMALLINT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_action_log_v1_wallet_gated_action_created ON action_log_v1(user_wallet, gated_action, created_at DESC);
+
+-- Lifespan metrics table: Stores accumulated metric counters with labels
+CREATE TABLE lifespan_metrics (
+ id VARCHAR(66) PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ labels JSONB,
+ value NUMERIC(78, 18) NOT NULL,
+ last_timestamp TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_lifespan_metrics_name_last_ts ON lifespan_metrics(name, last_timestamp DESC);
+
+-- Indexes for metric-gathering queries that filter on updated_at
+CREATE INDEX idx_channels_updated_at ON channels(updated_at);
+CREATE INDEX idx_app_sessions_v1_updated_at ON app_sessions_v1(updated_at);
+CREATE INDEX idx_user_balances_updated_at ON user_balances(updated_at);
+
-- +goose Down
+DROP INDEX IF EXISTS idx_user_balances_updated_at;
+DROP INDEX IF EXISTS idx_app_sessions_v1_updated_at;
+DROP INDEX IF EXISTS idx_channels_updated_at;
+DROP INDEX IF EXISTS idx_lifespan_metrics_name_last_ts;
+DROP TABLE IF EXISTS lifespan_metrics;
+DROP INDEX IF EXISTS idx_action_log_v1_wallet_gated_action_created;
+DROP TABLE IF EXISTS action_log_v1;
+DROP INDEX IF EXISTS idx_user_staked_v1_user_wallet;
+DROP TABLE IF EXISTS user_staked_v1;
DROP INDEX IF EXISTS idx_user_balances_user_wallet;
DROP TABLE IF EXISTS user_balances;
DROP INDEX IF EXISTS idx_channel_session_key_assets_v1_asset;
@@ -275,11 +337,14 @@ DROP TABLE IF EXISTS app_session_participants_v1;
DROP INDEX IF EXISTS idx_app_sessions_v1_status;
DROP INDEX IF EXISTS idx_app_sessions_v1_application;
DROP TABLE IF EXISTS app_sessions_v1;
+DROP INDEX IF EXISTS idx_apps_v1_owner_wallet;
+DROP TABLE IF EXISTS apps_v1;
DROP INDEX IF EXISTS idx_transactions_to_comp;
DROP INDEX IF EXISTS idx_transactions_from_comp;
DROP INDEX IF EXISTS idx_transactions_from_to_type;
DROP INDEX IF EXISTS idx_transactions_to_account;
DROP INDEX IF EXISTS idx_transactions_from_account;
+DROP INDEX IF EXISTS idx_transactions_type_created;
DROP INDEX IF EXISTS idx_transactions_type;
DROP TABLE IF EXISTS transactions;
DROP INDEX IF EXISTS idx_channel_states_escrow_channel_id;
diff --git a/clearnode/config/schemas/action_gateway_schema.yaml b/clearnode/config/schemas/action_gateway_schema.yaml
new file mode 100644
index 000000000..4f4997a67
--- /dev/null
+++ b/clearnode/config/schemas/action_gateway_schema.yaml
@@ -0,0 +1,31 @@
+$schema: "http://json-schema.org"
+type: object
+required:
+ - level_step_tokens
+ - app_cost
+ - action_gates
+properties:
+ level_step_tokens:
+ type: string
+ pattern: "^(?:[1-9][0-9]*(\\.[0-9]+)?|0\\.[0-9]*[1-9][0-9]*)$"
+ description: "Decimal amount for level step tokens"
+ app_cost:
+ type: string
+ pattern: "^(?:[1-9][0-9]*(\\.[0-9]+)?|0\\.[0-9]*[1-9][0-9]*)$"
+ action_gates:
+ type: object
+ additionalProperties:
+ $ref: "#/definitions/ActionGateConfig"
+definitions:
+ ActionGateConfig:
+ type: object
+ required:
+ - free_actions_allowance
+ - increase_per_level
+ properties:
+ free_actions_allowance:
+ type: integer
+ minimum: 0
+ increase_per_level:
+ type: integer
+ minimum: 0
diff --git a/clearnode/event_handlers/interface.go b/clearnode/event_handlers/interface.go
index 40167fb07..7b4e011ac 100644
--- a/clearnode/event_handlers/interface.go
+++ b/clearnode/event_handlers/interface.go
@@ -1,7 +1,8 @@
package event_handlers
import (
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/shopspring/decimal"
)
// StoreTxHandler is a function that executes Store operations within a transaction.
@@ -49,4 +50,7 @@ type Store interface {
// ScheduleFinalizeEscrowWithdrawal schedules a checkpoint for an escrow withdrawal operation.
// This queues the state to be submitted on-chain to finalize an escrow withdrawal.
ScheduleFinalizeEscrowWithdrawal(stateID string, chainID uint64) error
+
+ // UpdateUserStaked updates the total staked amount for a user on a specific blockchain.
+ UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error
}
diff --git a/clearnode/event_handlers/service.go b/clearnode/event_handlers/service.go
index 46e87d6b8..ff3dbb7b1 100644
--- a/clearnode/event_handlers/service.go
+++ b/clearnode/event_handlers/service.go
@@ -4,11 +4,12 @@ import (
"context"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
)
-var _ core.BlockchainEventHandler = &EventHandlerService{}
+var _ core.ChannelHubEventHandler = &EventHandlerService{}
+var _ core.LockingContractEventHandler = &EventHandlerService{}
// EventHandlerService processes blockchain events and updates the local database state accordingly.
// It handles events from both home channels (user state channels) and escrow channels (temporary lock channels).
@@ -445,3 +446,16 @@ func (s *EventHandlerService) HandleEscrowWithdrawalFinalized(ctx context.Contex
return nil
})
}
+
+func (s *EventHandlerService) HandleUserLockedBalanceUpdated(ctx context.Context, event *core.UserLockedBalanceUpdatedEvent) error {
+ logger := log.FromContext(ctx)
+ return s.useStoreInTx(func(tx Store) error {
+ err := tx.UpdateUserStaked(event.UserAddress, event.BlockchainID, event.Balance)
+ if err != nil {
+ return err
+ }
+
+ logger.Info("handled UserLockedBalanceUpdatedEvent event", "userWallet", event.UserAddress, "blockchainID", event.BlockchainID, "balance", event.Balance)
+ return nil
+ })
+}
diff --git a/clearnode/event_handlers/service_test.go b/clearnode/event_handlers/service_test.go
index 2c3fed384..e5e9afb1c 100644
--- a/clearnode/event_handlers/service_test.go
+++ b/clearnode/event_handlers/service_test.go
@@ -2,14 +2,16 @@ package event_handlers
import (
"context"
+ "errors"
"testing"
"time"
+ "github.com/shopspring/decimal"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
)
func TestHandleHomeChannelCreated_Success(t *testing.T) {
@@ -515,3 +517,70 @@ func TestHandleEscrowWithdrawalFinalized_Success(t *testing.T) {
require.NoError(t, err)
mockStore.AssertExpectations(t)
}
+
+func TestHandleUserLockedBalanceUpdated_Success(t *testing.T) {
+ // Setup
+ mockStore := new(MockStore)
+ ctx := log.SetContextLogger(context.Background(), log.NewNoopLogger())
+
+ service := &EventHandlerService{
+ useStoreInTx: func(handler StoreTxHandler) error {
+ return handler(mockStore)
+ },
+ }
+
+ // Test data
+ userWallet := "0x1234567890123456789012345678901234567890"
+ blockchainID := uint64(1)
+ balance := decimal.NewFromInt(1000)
+
+ event := &core.UserLockedBalanceUpdatedEvent{
+ UserAddress: userWallet,
+ BlockchainID: blockchainID,
+ Balance: balance,
+ }
+
+ // Mock expectations
+ mockStore.On("UpdateUserStaked", userWallet, blockchainID, balance).Return(nil)
+
+ // Execute
+ err := service.HandleUserLockedBalanceUpdated(ctx, event)
+
+ // Assert
+ require.NoError(t, err)
+ mockStore.AssertExpectations(t)
+}
+
+func TestHandleUserLockedBalanceUpdated_StoreError(t *testing.T) {
+ // Setup
+ mockStore := new(MockStore)
+ ctx := log.SetContextLogger(context.Background(), log.NewNoopLogger())
+
+ service := &EventHandlerService{
+ useStoreInTx: func(handler StoreTxHandler) error {
+ return handler(mockStore)
+ },
+ }
+
+ // Test data
+ userWallet := "0x1234567890123456789012345678901234567890"
+ blockchainID := uint64(1)
+ balance := decimal.NewFromInt(500)
+
+ event := &core.UserLockedBalanceUpdatedEvent{
+ UserAddress: userWallet,
+ BlockchainID: blockchainID,
+ Balance: balance,
+ }
+
+ // Mock expectations
+ mockStore.On("UpdateUserStaked", userWallet, blockchainID, balance).Return(errors.New("db error"))
+
+ // Execute
+ err := service.HandleUserLockedBalanceUpdated(ctx, event)
+
+ // Assert
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "db error")
+ mockStore.AssertExpectations(t)
+}
diff --git a/clearnode/event_handlers/testing.go b/clearnode/event_handlers/testing.go
index 6a87572ce..aab414c32 100644
--- a/clearnode/event_handlers/testing.go
+++ b/clearnode/event_handlers/testing.go
@@ -1,9 +1,10 @@
package event_handlers
import (
+ "github.com/shopspring/decimal"
"github.com/stretchr/testify/mock"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// MockStore is a mock implementation of the Store interface for testing
@@ -67,3 +68,9 @@ func (m *MockStore) ScheduleFinalizeEscrowWithdrawal(stateID string, chainID uin
args := m.Called(stateID, chainID)
return args.Error(0)
}
+
+// UpdateUserStaked mocks updating the total staked amount for a user
+func (m *MockStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error {
+ args := m.Called(wallet, blockchainID, amount)
+ return args.Error(0)
+}
diff --git a/clearnode/main.go b/clearnode/main.go
index d34fdad40..3980fb3b1 100644
--- a/clearnode/main.go
+++ b/clearnode/main.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "fmt"
"net/http"
"os"
"os/signal"
@@ -12,13 +13,14 @@ import (
"github.com/ethereum/go-ethereum/ethclient"
"github.com/prometheus/client_golang/prometheus/promhttp"
- "github.com/erc7824/nitrolite/clearnode/api"
- "github.com/erc7824/nitrolite/clearnode/event_handlers"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/clearnode/store/database"
- "github.com/erc7824/nitrolite/clearnode/stress"
- "github.com/erc7824/nitrolite/pkg/blockchain/evm"
- "github.com/erc7824/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/clearnode/api"
+ "github.com/layer-3/nitrolite/clearnode/event_handlers"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/clearnode/store/database"
+ "github.com/layer-3/nitrolite/clearnode/stress"
+ "github.com/layer-3/nitrolite/pkg/blockchain/evm"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
)
func main() {
@@ -26,14 +28,30 @@ func main() {
os.Exit(stress.Run(os.Args[2:]))
}
+ if len(os.Args) > 1 && os.Args[1] == "operator" {
+ runOperatorCommand(os.Args[2:])
+ return
+ }
+
bb := InitBackbone()
logger := bb.Logger
- ctx := context.Background()
+ rootCtx := context.Background()
+ blockchainCtx, cancelBlockchain := context.WithCancel(rootCtx)
+ ctx := rootCtx
vl := bb.ValidationLimits
- api.NewRPCRouter(bb.NodeVersion, bb.ChannelMinChallengeDuration,
- bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.RuntimeMetrics, bb.Logger,
- vl.MaxParticipants, vl.MaxSessionDataLen, vl.MaxSignedUpdates, vl.MaxSessionKeyIDs)
+ rpcRouterCfg := api.RPCRouterConfig{
+ NodeVersion: bb.NodeVersion,
+ MinChallenge: bb.ChannelMinChallengeDuration,
+ MaxParticipants: vl.MaxParticipants,
+ MaxSessionDataLen: vl.MaxSessionDataLen,
+ MaxAppMetadataLen: vl.MaxAppMetadataLen,
+ MaxRebalanceSignedUpdates: vl.MaxSignedUpdates,
+ MaxSessionKeyIDs: vl.MaxSessionKeyIDs,
+ RateLimitPerSec: bb.RateLimitPerSec,
+ RateLimitBurst: bb.RateLimitBurst,
+ }
+ api.NewRPCRouter(rpcRouterCfg, bb.RpcNode, bb.StateSigner, bb.DbStore, bb.MemoryStore, bb.ActionGateway, bb.RuntimeMetrics, bb.Logger)
rpcListenAddr := ":7824"
rpcListenEndpoint := "/ws"
@@ -69,37 +87,71 @@ func main() {
if err != nil {
logger.Fatal("failed to connect to EVM Node")
}
- reactor := evm.NewReactor(b.ID, eventHandlerService, bb.DbStore.StoreContractEvent)
- reactor.SetOnEventProcessed(bb.RuntimeMetrics.IncBlockchainEvent)
- l := evm.NewListener(common.HexToAddress(b.ChannelHubAddress), client, b.ID, b.BlockStep, logger, reactor.HandleEvent, bb.DbStore.GetLatestEvent)
- l.Listen(ctx, func(err error) {
+
+ if b.ChannelHubAddress != "" {
+ // For the node itself, the node address is the signer's address
+ nodeAddress := bb.StateSigner.PublicKey().Address().String()
+
+ clientOpts := []evm.ClientOption{
+ evm.ClientBalanceCheck{RequireBalanceCheck: false},
+ evm.ClientAllowanceCheck{RequireAllowanceCheck: false},
+ }
+
+ blockchainClient, err := evm.NewBlockchainClient(common.HexToAddress(b.ChannelHubAddress), client, bb.TxSigner, b.ID, nodeAddress, bb.MemoryStore, clientOpts...)
if err != nil {
- logger.Fatal("blockchain listener stopped", "error", err, "blockchainID", b.ID)
+ logger.Fatal("failed to create EVM client")
}
- })
- // For the node itself, the node address is the signer's address
- nodeAddress := bb.StateSigner.PublicKey().Address().String()
+ sigValidators, err := bb.MemoryStore.GetChannelSigValidators(b.ID)
+ if err != nil {
+ logger.Fatal("failed to get channel signature validators from memory store", "error", err, "blockchainID", b.ID)
+ }
- clientOpts := []evm.ClientOption{
- evm.ClientBalanceCheck{RequireBalanceCheck: false},
- evm.ClientAllowanceCheck{RequireAllowanceCheck: false},
- }
+ if err := ensureSigValidatorsRegistered(blockchainClient, sigValidators, true); err != nil {
+ logger.Fatal("failed to ensure signature validators are registered", "error", err, "blockchainID", b.ID)
+ }
- blockchainClient, err := evm.NewClient(common.HexToAddress(b.ChannelHubAddress), client, bb.TxSigner, b.ID, nodeAddress, bb.MemoryStore, clientOpts...)
- if err != nil {
- logger.Fatal("failed to create EVM client")
+ reactor := evm.NewChannelHubReactor(b.ID, eventHandlerService, bb.DbStore.StoreContractEvent)
+ reactor.SetOnEventProcessed(bb.RuntimeMetrics.IncBlockchainEvent)
+ l := evm.NewListener(common.HexToAddress(b.ChannelHubAddress), client, b.ID, b.BlockStep, logger, reactor.HandleEvent, bb.DbStore.GetLatestEvent)
+ l.Listen(blockchainCtx, func(err error) {
+ if err != nil {
+ logger.Fatal("blockchain listener stopped", "error", err, "blockchainID", b.ID)
+ }
+ })
+
+ worker := NewBlockchainWorker(b.ID, blockchainClient, bb.DbStore, logger, bb.RuntimeMetrics)
+ worker.Start(blockchainCtx, func(err error) {
+ if err != nil {
+ logger.Fatal("blockchain worker stopped", "error", err, "blockchainID", b.ID)
+ }
+ })
+ } else {
+ logger.Info("channel hub address is not configured for blockchain", "blockchainID", b.ID)
}
- worker := NewBlockchainWorker(b.ID, blockchainClient, bb.DbStore, logger, bb.RuntimeMetrics)
- worker.Start(ctx, func(err error) {
+ if b.LockingContractAddress != "" {
+ appRegistryClient, err := evm.NewLockingClient(common.HexToAddress(b.LockingContractAddress), client, b.ID)
+ if err != nil {
+ logger.Fatal("failed to create locking client", "error", err, "blockchainID", b.ID)
+ }
+
+ reactor, err := evm.NewLockingContractReactor(b.ID, eventHandlerService, appRegistryClient.GetTokenDecimals, bb.DbStore.StoreContractEvent)
if err != nil {
- logger.Fatal("blockchain worker stopped", "error", err, "blockchainID", b.ID)
+ logger.Fatal("failed to create app registry reactor", "error", err, "blockchainID", b.ID)
}
- })
+
+ reactor.SetOnEventProcessed(bb.RuntimeMetrics.IncBlockchainEvent)
+ l := evm.NewListener(common.HexToAddress(b.LockingContractAddress), client, b.ID, b.BlockStep, logger, reactor.HandleEvent, bb.DbStore.GetLatestEvent)
+ l.Listen(blockchainCtx, func(err error) {
+ if err != nil {
+ logger.Fatal("blockchain listener stopped", "error", err, "blockchainID", b.ID)
+ }
+ })
+ }
}
- go runStoreMetricsExporter(ctx, 10*time.Second, bb.DbStore, bb.StoreMetrics, logger)
+ go runStoreMetricsExporter(ctx, 30*time.Second, bb.DbStore, bb.StoreMetrics, logger)
metricsListenAddr := ":4242"
metricsEndpoint := "/metrics"
@@ -149,21 +201,115 @@ func main() {
logger.Error("failed to shut down RPC server", "error", err)
}
+ logger.Info("stopping blockchain listeners and workers")
+ cancelBlockchain()
+
// Close backbone resources
if err := bb.Close(); err != nil {
logger.Error("failed to close backbone resources", "error", err)
}
- // TODO: gracefully stop blockchain listeners and workers
logger.Info("shutdown complete")
}
+func runOperatorCommand(args []string) {
+ if len(args) == 0 {
+ fmt.Println("Usage: clearnode operator ")
+ fmt.Println("Commands:")
+ fmt.Println(" address Print the operator address")
+ fmt.Println(" register-validators Register signature validators on-chain")
+ os.Exit(1)
+ }
+
+ switch args[0] {
+ case "address":
+ runOperatorAddress()
+ case "register-validators":
+ runRegisterValidators()
+ default:
+ fmt.Printf("Unknown operator command: %s\n", args[0])
+ os.Exit(1)
+ }
+}
+
+func runOperatorAddress() {
+ bb := InitBackbone()
+ defer bb.Close()
+
+ fmt.Println(bb.StateSigner.PublicKey().Address().String())
+ time.Sleep(5 * time.Second)
+}
+
+func runRegisterValidators() {
+ bb := InitBackbone()
+ defer bb.Close()
+ logger := bb.Logger
+
+ blockchains, err := bb.MemoryStore.GetBlockchains()
+ if err != nil {
+ logger.Fatal("failed to get blockchains from memory store", "error", err)
+ }
+
+ for _, b := range blockchains {
+ if b.ChannelHubAddress == "" {
+ continue
+ }
+
+ rpcURL, ok := bb.BlockchainRPCs[b.ID]
+ if !ok {
+ logger.Fatal("no RPC URL configured for blockchain", "blockchainID", b.ID)
+ }
+
+ client, err := ethclient.Dial(rpcURL)
+ if err != nil {
+ logger.Fatal("failed to connect to EVM Node", "blockchainID", b.ID)
+ }
+
+ nodeAddress := bb.StateSigner.PublicKey().Address().String()
+ clientOpts := []evm.ClientOption{
+ evm.ClientBalanceCheck{RequireBalanceCheck: false},
+ evm.ClientAllowanceCheck{RequireAllowanceCheck: false},
+ }
+
+ blockchainClient, err := evm.NewBlockchainClient(common.HexToAddress(b.ChannelHubAddress), client, bb.TxSigner, b.ID, nodeAddress, bb.MemoryStore, clientOpts...)
+ if err != nil {
+ logger.Fatal("failed to create EVM client", "blockchainID", b.ID)
+ }
+
+ sigValidators, err := bb.MemoryStore.GetChannelSigValidators(b.ID)
+ if err != nil {
+ logger.Fatal("failed to get channel signature validators from memory store", "error", err, "blockchainID", b.ID)
+ }
+
+ if err := ensureSigValidatorsRegistered(blockchainClient, sigValidators, false); err != nil {
+ logger.Fatal("failed to register signature validators", "error", err, "blockchainID", b.ID)
+ }
+
+ logger.Info("signature validators registered successfully", "blockchainID", b.ID)
+ }
+
+ logger.Info("all signature validators registered")
+}
+
+func ensureSigValidatorsRegistered(client core.BlockchainClient, validators map[uint8]string, checkOnly bool) error {
+ for id, addr := range validators {
+ if err := client.EnsureSigValidatorRegistered(id, addr, checkOnly); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
func runStoreMetricsExporter(
ctx context.Context,
fetchInterval time.Duration,
store interface {
- CountAppSessionsByStatus() ([]database.AppSessionCount, error)
- CountChannelsByStatus() ([]database.ChannelCount, error)
+ GetChannelsCountByLabels() ([]database.ChannelCount, error)
+ GetAppSessionsCountByLabels() ([]database.AppSessionCount, error)
+ GetTotalValueLocked() ([]database.TotalValueLocked, error)
+ CountActiveUsers(window time.Duration) ([]database.ActiveCountByLabel, error)
+ CountActiveAppSessions(window time.Duration) ([]database.ActiveCountByLabel, error)
},
metricExported metrics.StoreMetricExporter, logger log.Logger) {
logger = logger.WithName("store-metrics")
@@ -173,21 +319,62 @@ func runStoreMetricsExporter(
for {
select {
case <-ticker.C:
- if counts, err := store.CountAppSessionsByStatus(); err != nil {
- logger.Error("failed to count app sessions", "error", err)
+ timeSpans := []struct {
+ label string
+ duration time.Duration
+ }{
+ {"day", 24 * time.Hour},
+ {"week", 7 * 24 * time.Hour},
+ {"month", 30 * 24 * time.Hour},
+ }
+
+ channelCounts, err := store.GetChannelsCountByLabels()
+ if err != nil {
+ logger.Error("failed to get channel counts by labels", "error", err)
+ } else {
+ for _, c := range channelCounts {
+ metricExported.SetChannels(c.Asset, c.Status, c.Count)
+ }
+ }
+
+ appSessionCounts, err := store.GetAppSessionsCountByLabels()
+ if err != nil {
+ logger.Error("failed to get app sessions counts by labels", "error", err)
} else {
- for _, c := range counts {
+ for _, c := range appSessionCounts {
metricExported.SetAppSessions(c.Application, c.Status, c.Count)
}
}
- if counts, err := store.CountChannelsByStatus(); err != nil {
- logger.Error("failed to count channels", "error", err)
+ tvlCounts, err := store.GetTotalValueLocked()
+ if err != nil {
+ logger.Error("failed to get total value locked", "error", err)
} else {
- for _, c := range counts {
- metricExported.SetChannels(c.Asset, c.Status, c.Count)
+ for _, c := range tvlCounts {
+ metricExported.SetTotalValueLocked(c.Domain, c.Asset, c.Value.InexactFloat64())
+ }
+ }
+
+ for _, tw := range timeSpans {
+ if counts, err := store.CountActiveUsers(tw.duration); err != nil {
+ logger.Error("failed to count active users", "timeframe", tw.label, "error", err)
+ } else {
+ for _, c := range counts {
+ metricExported.SetActiveUsers(c.Label, tw.label, c.Count)
+ }
+ }
+ }
+
+ for _, tw := range timeSpans {
+ if counts, err := store.CountActiveAppSessions(tw.duration); err != nil {
+ logger.Error("failed to count active app sessions", "timeframe", tw.label, "error", err)
+ } else {
+ for _, c := range counts {
+ metricExported.SetActiveAppSessions(c.Label, tw.label, c.Count)
+ }
}
}
+
case <-ctx.Done():
logger.Info("stopping store metrics exporter")
return
diff --git a/clearnode/metrics/exporter.go b/clearnode/metrics/exporter.go
index d6dd0e313..3148ae142 100644
--- a/clearnode/metrics/exporter.go
+++ b/clearnode/metrics/exporter.go
@@ -8,9 +8,9 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
const (
@@ -24,28 +24,49 @@ var (
)
type storeMetricExporter struct {
- appSessionsTotal *prometheus.GaugeVec
- channelsTotal *prometheus.GaugeVec
+ appSessionsTotal *prometheus.GaugeVec
+ channelsTotal *prometheus.GaugeVec
+ usersActive *prometheus.GaugeVec
+ appSessionsActive *prometheus.GaugeVec
+ totalValueLocked *prometheus.GaugeVec
}
func NewStoreMetricExporter(reg prometheus.Registerer) (StoreMetricExporter, error) {
m := &storeMetricExporter{
appSessionsTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricNamespace,
- Name: "app_sessions_active",
+ Name: "app_sessions_total",
Help: "Current total number of app sessions",
}, []string{"application", "status"}),
channelsTotal: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: MetricNamespace,
- Name: "channels_active",
+ Name: "channels_total",
Help: "Current total number of channels",
}, []string{"asset", "status"}),
+ usersActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: MetricNamespace,
+ Name: "users_active",
+ Help: "Current total active users",
+ }, []string{"asset", "timespan"}),
+ appSessionsActive: prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: MetricNamespace,
+ Name: "app_sessions_active",
+ Help: "Current total active app sessions",
+ }, []string{"application", "timespan"}),
+ totalValueLocked: prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: MetricNamespace,
+ Name: "total_value_locked",
+ Help: "Total value locked by domain and asset",
+ }, []string{"domain", "asset"}),
}
if reg != nil {
reg.MustRegister(
m.appSessionsTotal,
m.channelsTotal,
+ m.usersActive,
+ m.appSessionsActive,
+ m.totalValueLocked,
)
} else {
return nil, fmt.Errorf("prometheus registerer not provided")
@@ -62,6 +83,18 @@ func (m *storeMetricExporter) SetChannels(asset string, status core.ChannelStatu
m.channelsTotal.WithLabelValues(asset, status.String()).Set(float64(count))
}
+func (m *storeMetricExporter) SetActiveUsers(asset, timeSpanLabel string, count uint64) {
+ m.usersActive.WithLabelValues(asset, timeSpanLabel).Set(float64(count))
+}
+
+func (m *storeMetricExporter) SetActiveAppSessions(applicationID, timeSpanLabel string, count uint64) {
+ m.appSessionsActive.WithLabelValues(applicationID, timeSpanLabel).Set(float64(count))
+}
+
+func (m *storeMetricExporter) SetTotalValueLocked(domain, asset string, value float64) {
+ m.totalValueLocked.WithLabelValues(domain, asset).Set(value)
+}
+
// runtimeMetricExporter is the concrete implementation of the Metrics interface.
type runtimeMetricExporter struct {
// Shared Metrics (Cross-Package)
diff --git a/clearnode/metrics/interface.go b/clearnode/metrics/interface.go
index c103e8ba2..db0df3f06 100644
--- a/clearnode/metrics/interface.go
+++ b/clearnode/metrics/interface.go
@@ -5,9 +5,9 @@ import (
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/rpc"
)
// RuntimeMetricExporter defines the interface for recording runtime metrics across various components of the system.
@@ -60,4 +60,7 @@ func (noopRuntimeMetricExporter) IncBlockchainEvent(uint64, bool) {}
type StoreMetricExporter interface {
SetAppSessions(applicationID string, status app.AppSessionStatus, count uint64)
SetChannels(asset string, status core.ChannelStatus, count uint64)
+ SetActiveUsers(asset, timeSpanLabel string, count uint64)
+ SetActiveAppSessions(applicationID, timeSpanLabel string, count uint64)
+ SetTotalValueLocked(domain, asset string, value float64)
}
diff --git a/clearnode/runtime.go b/clearnode/runtime.go
index d58ec8189..c65733b59 100644
--- a/clearnode/runtime.go
+++ b/clearnode/runtime.go
@@ -15,15 +15,16 @@ import (
"github.com/joho/godotenv"
"github.com/prometheus/client_golang/prometheus"
- "github.com/erc7824/nitrolite/clearnode/metrics"
- "github.com/erc7824/nitrolite/clearnode/store/database"
- "github.com/erc7824/nitrolite/clearnode/store/memory"
- "github.com/erc7824/nitrolite/pkg/blockchain/evm"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/log"
- "github.com/erc7824/nitrolite/pkg/rpc"
- "github.com/erc7824/nitrolite/pkg/sign"
- "github.com/erc7824/nitrolite/pkg/sign/kms/gcp"
+ "github.com/layer-3/nitrolite/clearnode/action_gateway"
+ "github.com/layer-3/nitrolite/clearnode/metrics"
+ "github.com/layer-3/nitrolite/clearnode/store/database"
+ "github.com/layer-3/nitrolite/clearnode/store/memory"
+ "github.com/layer-3/nitrolite/pkg/blockchain/evm"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/log"
+ "github.com/layer-3/nitrolite/pkg/rpc"
+ "github.com/layer-3/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/pkg/sign/kms/gcp"
)
//go:embed config/migrations/*/*.sql
@@ -36,9 +37,12 @@ type Backbone struct {
ChannelMinChallengeDuration uint32
BlockchainRPCs map[uint64]string
ValidationLimits ValidationLimits
+ RateLimitPerSec float64
+ RateLimitBurst float64
DbStore database.DatabaseStore
MemoryStore memory.MemoryStore
+ ActionGateway *action_gateway.ActionGateway
RpcNode rpc.Node
StateSigner sign.Signer
TxSigner sign.Signer
@@ -66,12 +70,17 @@ type Config struct {
SignerKey string `yaml:"signer_key" env:"CLEARNODE_SIGNER_KEY"` // required when signer_type=key
GCPKMSKeyName string `yaml:"gcp_kms_key_name" env:"CLEARNODE_GCP_KMS_KEY_NAME"` // required when signer_type=gcp-kms
ValidationLimits ValidationLimits `yaml:"validation_limits"`
+ RateLimitPerSec float64 `yaml:"rate_limit_per_sec" env:"CLEARNODE_RATE_LIMIT_PER_SEC" env-default:"10"`
+ RateLimitBurst float64 `yaml:"rate_limit_burst" env:"CLEARNODE_RATE_LIMIT_BURST" env-default:"20"`
+ WsProcessBufferSize int `yaml:"ws_process_buffer_size" env:"CLEARNODE_WS_PROCESS_BUFFER_SIZE" env-default:"64"`
+ WsWriteBufferSize int `yaml:"ws_write_buffer_size" env:"CLEARNODE_WS_WRITE_BUFFER_SIZE" env-default:"64"`
}
// ValidationLimits defines configurable upper bounds for dynamic-length request fields.
type ValidationLimits struct {
MaxParticipants int `yaml:"max_participants" env:"CLEARNODE_MAX_PARTICIPANTS" env-default:"32"`
MaxSessionDataLen int `yaml:"max_session_data_len" env:"CLEARNODE_MAX_SESSION_DATA_LEN" env-default:"1024"`
+ MaxAppMetadataLen int `yaml:"max_app_metadata_len" env:"CLEARNODE_MAX_APP_METADATA_LEN" env-default:"1024"`
MaxSessionKeyIDs int `yaml:"max_session_key_ids" env:"CLEARNODE_MAX_SESSION_KEY_IDS" env-default:"256"`
MaxSignedUpdates int `yaml:"max_signed_updates" env:"CLEARNODE_MAX_SIGNED_UPDATES" env-default:"0"`
}
@@ -130,6 +139,15 @@ func InitBackbone() *Backbone {
logger.Fatal("failed to load blockchains", "error", err)
}
+ // ------------------------------------------------
+ // Action Gateway
+ // ------------------------------------------------
+
+ actionGateway, err := action_gateway.NewActionGatewayFromYaml(configDirPath)
+ if err != nil {
+ logger.Fatal("failed to initialize action gateway", "error", err)
+ }
+
// ------------------------------------------------
// Signer
// ------------------------------------------------
@@ -189,8 +207,10 @@ func InitBackbone() *Backbone {
// ------------------------------------------------
rpcNode, err := rpc.NewWebsocketNode(rpc.WebsocketNodeConfig{
- Logger: logger,
- ObserveConnections: runtimeMetrics.SetRPCConnections,
+ Logger: logger,
+ ObserveConnections: runtimeMetrics.SetRPCConnections,
+ WsConnProcessBufferSize: conf.WsProcessBufferSize,
+ WsConnWriteBufferSize: conf.WsWriteBufferSize,
})
if err != nil {
logger.Fatal("failed to initialize RPC node", "error", err)
@@ -232,9 +252,12 @@ func InitBackbone() *Backbone {
ChannelMinChallengeDuration: conf.ChannelMinChallengeDuration,
BlockchainRPCs: blockchainRPCs,
ValidationLimits: conf.ValidationLimits,
+ RateLimitPerSec: conf.RateLimitPerSec,
+ RateLimitBurst: conf.RateLimitBurst,
DbStore: dbStore,
MemoryStore: memoryStore,
+ ActionGateway: actionGateway,
RpcNode: rpcNode,
StateSigner: stateSigner,
TxSigner: txSigner,
diff --git a/clearnode/runtime_test.go b/clearnode/runtime_test.go
index 0fd342827..2b66d9917 100644
--- a/clearnode/runtime_test.go
+++ b/clearnode/runtime_test.go
@@ -6,7 +6,7 @@ import (
"github.com/ethereum/go-ethereum/common"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
)
func TestCheckChannelHubVersion_Manual(t *testing.T) {
diff --git a/clearnode/store/database/action_log.go b/clearnode/store/database/action_log.go
new file mode 100644
index 000000000..d6e331cad
--- /dev/null
+++ b/clearnode/store/database/action_log.go
@@ -0,0 +1,91 @@
+package database
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/layer-3/nitrolite/pkg/core"
+)
+
+type ActionLogEntryV1 struct {
+ ID uuid.UUID `gorm:"type:char(36);primaryKey"`
+ UserWallet string `gorm:"column:user_wallet;not null"`
+ GatedAction uint8 `gorm:"column:gated_action;not null"`
+ CreatedAt time.Time
+}
+
+func (ActionLogEntryV1) TableName() string {
+ return "action_log_v1"
+}
+
+// RecordAction inserts a new action log entry for a user.
+func (s *DBStore) RecordAction(wallet string, gatedAction core.GatedAction) error {
+ if gatedAction.ID() == 0 {
+ return fmt.Errorf("invalid gated action ID")
+ }
+
+ wallet = strings.ToLower(wallet)
+
+ entry := ActionLogEntryV1{
+ ID: uuid.New(),
+ UserWallet: wallet,
+ GatedAction: gatedAction.ID(),
+ CreatedAt: time.Now(),
+ }
+
+ if err := s.db.Create(&entry).Error; err != nil {
+ return fmt.Errorf("failed to record action log entry: %w", err)
+ }
+
+ return nil
+}
+
+// GetUserActionCount returns the number of actions matching the given wallet and gated action
+// within the specified time window (counting backwards from now).
+func (s *DBStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) {
+ wallet = strings.ToLower(wallet)
+ since := time.Now().Add(-window)
+
+ query := s.db.Model(&ActionLogEntryV1{}).
+ Where("user_wallet = ? AND gated_action = ? AND created_at >= ?", wallet, gatedAction.ID(), since)
+
+ var count int64
+ if err := query.Count(&count).Error; err != nil {
+ return 0, fmt.Errorf("failed to get user action count: %w", err)
+ }
+
+ return uint64(count), nil
+}
+
+func (s *DBStore) GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error) {
+ userWallet = strings.ToLower(userWallet)
+ since := time.Now().Add(-window)
+
+ query := s.db.Model(&ActionLogEntryV1{}).
+ Select("gated_action, COUNT(id) as count").
+ Where("user_wallet = ? AND created_at >= ?", userWallet, since).
+ Group("gated_action")
+
+ type Result struct {
+ GatedAction uint8
+ Count int64
+ }
+
+ var results []Result
+ if err := query.Scan(&results).Error; err != nil {
+ return nil, fmt.Errorf("failed to get user action counts: %w", err)
+ }
+
+ counts := make(map[core.GatedAction]uint64)
+ for _, r := range results {
+ action, ok := core.GatedActionFromID(r.GatedAction)
+ if !ok {
+ continue
+ }
+ counts[action] = uint64(r.Count)
+ }
+
+ return counts, nil
+}
diff --git a/clearnode/store/database/action_log_test.go b/clearnode/store/database/action_log_test.go
new file mode 100644
index 000000000..a0a5cf262
--- /dev/null
+++ b/clearnode/store/database/action_log_test.go
@@ -0,0 +1,249 @@
+package database
+
+import (
+ "testing"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRecordAction(t *testing.T) {
+ t.Run("records action successfully", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ err := store.RecordAction("0xUser123", core.GatedActionTransfer)
+ require.NoError(t, err)
+
+ count, err := store.GetUserActionCount("0xuser123", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), count)
+ })
+
+ t.Run("records multiple actions", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+
+ count, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(3), count)
+ })
+
+ t.Run("normalizes wallet to lowercase", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, store.RecordAction("0xABCDEF", core.GatedActionTransfer))
+
+ count, err := store.GetUserActionCount("0xabcdef", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), count)
+ })
+}
+
+func TestGetUserActionCount(t *testing.T) {
+ t.Run("returns zero for no actions", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ count, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(0), count)
+ })
+
+ t.Run("filters by gated action", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionAppSessionOperation))
+
+ transferCount, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(2), transferCount)
+
+ opCount, err := store.GetUserActionCount("0xuser", core.GatedActionAppSessionOperation, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), opCount)
+ })
+
+ t.Run("filters by wallet", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, store.RecordAction("0xuser1", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser1", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser2", core.GatedActionTransfer))
+
+ count, err := store.GetUserActionCount("0xuser1", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(2), count)
+
+ count, err = store.GetUserActionCount("0xuser2", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), count)
+ })
+
+ t.Run("respects time window", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ oldEntry := ActionLogEntryV1{
+ ID: [16]byte{1},
+ UserWallet: "0xuser",
+ GatedAction: core.GatedActionTransfer.ID(),
+ CreatedAt: time.Now().Add(-2 * time.Hour),
+ }
+ require.NoError(t, db.Create(&oldEntry).Error)
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+
+ count, err := store.GetUserActionCount("0xuser", core.GatedActionTransfer, time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), count)
+
+ count, err = store.GetUserActionCount("0xuser", core.GatedActionTransfer, 3*time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(2), count)
+ })
+}
+
+func TestGetUserActionCounts(t *testing.T) {
+ t.Run("returns empty map for no actions", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ counts, err := store.GetUserActionCounts("0xuser", time.Hour)
+ require.NoError(t, err)
+ assert.Empty(t, counts)
+ })
+
+ t.Run("groups by gated action", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionAppSessionOperation))
+
+ counts, err := store.GetUserActionCounts("0xuser", time.Hour)
+ require.NoError(t, err)
+
+ assert.Equal(t, uint64(2), counts[core.GatedActionTransfer])
+ assert.Equal(t, uint64(1), counts[core.GatedActionAppSessionOperation])
+ })
+
+ t.Run("filters by wallet", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, store.RecordAction("0xuser1", core.GatedActionTransfer))
+ require.NoError(t, store.RecordAction("0xuser2", core.GatedActionTransfer))
+
+ counts, err := store.GetUserActionCounts("0xuser1", time.Hour)
+ require.NoError(t, err)
+ assert.Len(t, counts, 1)
+ })
+
+ t.Run("respects time window", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ oldEntry := ActionLogEntryV1{
+ ID: [16]byte{1},
+ UserWallet: "0xuser",
+ GatedAction: core.GatedActionTransfer.ID(),
+ CreatedAt: time.Now().Add(-2 * time.Hour),
+ }
+ require.NoError(t, db.Create(&oldEntry).Error)
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+
+ counts, err := store.GetUserActionCounts("0xuser", time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), counts[core.GatedActionTransfer])
+
+ counts, err = store.GetUserActionCounts("0xuser", 3*time.Hour)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(2), counts[core.GatedActionTransfer])
+ })
+
+ t.Run("skips unknown gated action IDs", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ // Insert an entry with an unknown gated action ID directly
+ entry := ActionLogEntryV1{
+ ID: [16]byte{99},
+ UserWallet: "0xuser",
+ GatedAction: 255, // unknown ID
+ CreatedAt: time.Now(),
+ }
+ require.NoError(t, db.Create(&entry).Error)
+ require.NoError(t, store.RecordAction("0xuser", core.GatedActionTransfer))
+
+ counts, err := store.GetUserActionCounts("0xuser", time.Hour)
+ require.NoError(t, err)
+ assert.Len(t, counts, 1)
+ assert.Equal(t, uint64(1), counts[core.GatedActionTransfer])
+ })
+}
+
+func TestGetAppCount(t *testing.T) {
+ t.Run("returns zero for no apps", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ count, err := store.GetAppCount("0xowner")
+ require.NoError(t, err)
+ assert.Equal(t, uint64(0), count)
+ })
+
+ t.Run("counts apps for owner", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, db.Create(&AppV1{ID: "app1", OwnerWallet: "0xowner1", Metadata: "{}"}).Error)
+ require.NoError(t, db.Create(&AppV1{ID: "app2", OwnerWallet: "0xowner1", Metadata: "{}"}).Error)
+ require.NoError(t, db.Create(&AppV1{ID: "app3", OwnerWallet: "0xowner2", Metadata: "{}"}).Error)
+
+ count, err := store.GetAppCount("0xowner1")
+ require.NoError(t, err)
+ assert.Equal(t, uint64(2), count)
+
+ count, err = store.GetAppCount("0xowner2")
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), count)
+ })
+
+ t.Run("normalizes wallet to lowercase", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := NewDBStore(db)
+
+ require.NoError(t, db.Create(&AppV1{ID: "app1", OwnerWallet: "0xabcdef", Metadata: "{}"}).Error)
+
+ count, err := store.GetAppCount("0xABCDEF")
+ require.NoError(t, err)
+ assert.Equal(t, uint64(1), count)
+ })
+}
diff --git a/clearnode/store/database/app.go b/clearnode/store/database/app.go
new file mode 100644
index 000000000..5df3757f8
--- /dev/null
+++ b/clearnode/store/database/app.go
@@ -0,0 +1,118 @@
+package database
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "gorm.io/gorm"
+)
+
+// AppV1 represents an application registry entry in the database.
+type AppV1 struct {
+ ID string `gorm:"primaryKey"`
+ OwnerWallet string `gorm:"column:owner_wallet;not null"`
+ Metadata string `gorm:"column:metadata;type:text;not null"`
+ Version uint64 `gorm:"column:version;default:1"`
+ CreationApprovalNotRequired bool `gorm:"column:creation_approval_not_required"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+func (AppV1) TableName() string {
+ return "apps_v1"
+}
+
+// CreateApp registers a new application. Returns an error if the app ID already exists.
+func (s *DBStore) CreateApp(entry app.AppV1) error {
+ dbApp := AppV1{
+ ID: strings.ToLower(entry.ID),
+ OwnerWallet: strings.ToLower(entry.OwnerWallet),
+ Metadata: entry.Metadata,
+ Version: entry.Version,
+ CreationApprovalNotRequired: entry.CreationApprovalNotRequired,
+ }
+
+ if err := s.db.Create(&dbApp).Error; err != nil {
+ return fmt.Errorf("failed to create app: %w", err)
+ }
+
+ return nil
+}
+
+// GetApp retrieves a single application by ID. Returns nil if not found.
+func (s *DBStore) GetApp(appID string) (*app.AppInfoV1, error) {
+ var dbApp AppV1
+ err := s.db.Where("id = ?", strings.ToLower(appID)).First(&dbApp).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("failed to get app: %w", err)
+ }
+
+ result := databaseAppToCore(&dbApp)
+ return &result, nil
+}
+
+// GetApps retrieves applications with optional filtering by app ID, owner wallet, and pagination.
+func (s *DBStore) GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error) {
+ query := s.db.Model(&AppV1{})
+
+ if appID != nil && *appID != "" {
+ query = query.Where("id = ?", strings.ToLower(*appID))
+ }
+
+ if ownerWallet != nil && *ownerWallet != "" {
+ query = query.Where("owner_wallet = ?", strings.ToLower(*ownerWallet))
+ }
+
+ var totalCount int64
+ if err := query.Count(&totalCount).Error; err != nil {
+ return nil, core.PaginationMetadata{}, fmt.Errorf("failed to count apps: %w", err)
+ }
+
+ offset, limit := pagination.GetOffsetAndLimit(DefaultLimit, MaxLimit)
+
+ query = query.Order("created_at DESC").Offset(int(offset)).Limit(int(limit))
+
+ var dbApps []AppV1
+ if err := query.Find(&dbApps).Error; err != nil {
+ return nil, core.PaginationMetadata{}, fmt.Errorf("failed to get apps: %w", err)
+ }
+
+ apps := make([]app.AppInfoV1, len(dbApps))
+ for i, dbApp := range dbApps {
+ apps[i] = databaseAppToCore(&dbApp)
+ }
+
+ metadata := calculatePaginationMetadata(totalCount, offset, limit)
+
+ return apps, metadata, nil
+}
+
+func databaseAppToCore(dbApp *AppV1) app.AppInfoV1 {
+ return app.AppInfoV1{
+ App: app.AppV1{
+ ID: dbApp.ID,
+ OwnerWallet: dbApp.OwnerWallet,
+ Metadata: dbApp.Metadata,
+ Version: dbApp.Version,
+ CreationApprovalNotRequired: dbApp.CreationApprovalNotRequired,
+ },
+ CreatedAt: dbApp.CreatedAt,
+ UpdatedAt: dbApp.UpdatedAt,
+ }
+}
+
+func (s *DBStore) GetAppCount(ownerWallet string) (uint64, error) {
+ var count int64
+ err := s.db.Model(&AppV1{}).Where("owner_wallet = ?", strings.ToLower(ownerWallet)).Count(&count).Error
+ if err != nil {
+ return 0, fmt.Errorf("failed to count apps: %w", err)
+ }
+
+ return uint64(count), nil
+}
diff --git a/clearnode/store/database/app_ledger_test.go b/clearnode/store/database/app_ledger_test.go
index ac4cc7fca..afc318d7d 100644
--- a/clearnode/store/database/app_ledger_test.go
+++ b/clearnode/store/database/app_ledger_test.go
@@ -3,7 +3,7 @@ package database
import (
"testing"
- "github.com/erc7824/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/app"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -204,9 +204,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
// Create app session with participant
session := app.AppSessionV1{
- SessionID: "session123",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session123",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -242,9 +242,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
// Create app session with participants
session := app.AppSessionV1{
- SessionID: "session456",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session456",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -286,9 +286,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
// Create app session with participant
session := app.AppSessionV1{
- SessionID: "session789",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session789",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -326,9 +326,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
// Create app session with participant but no ledger entries
session := app.AppSessionV1{
- SessionID: "session100",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session100",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -366,9 +366,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
// Create app session with one participant
session := app.AppSessionV1{
- SessionID: "session200",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session200",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -406,9 +406,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
// Create two app sessions with the same participant
session1 := app.AppSessionV1{
- SessionID: "session300",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session300",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -423,9 +423,9 @@ func TestDBStore_GetParticipantAllocations(t *testing.T) {
require.NoError(t, store.CreateAppSession(session1))
session2 := app.AppSessionV1{
- SessionID: "session400",
- Application: "poker",
- Nonce: 2,
+ SessionID: "session400",
+ ApplicationID: "poker",
+ Nonce: 2,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
diff --git a/clearnode/store/database/app_session.go b/clearnode/store/database/app_session.go
index ef93afd9f..14c946cf4 100644
--- a/clearnode/store/database/app_session.go
+++ b/clearnode/store/database/app_session.go
@@ -5,23 +5,23 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
"gorm.io/gorm"
)
// AppSessionV1 represents a virtual payment application session between participants
type AppSessionV1 struct {
- ID string `gorm:"primaryKey"`
- Application string `gorm:"column:application;not null"`
- Nonce uint64 `gorm:"column:nonce;not null"`
- Participants []AppParticipantV1 `gorm:"foreignKey:AppSessionID;references:ID"`
- SessionData string `gorm:"column:session_data;type:text;not null"`
- Quorum uint8 `gorm:"column:quorum;default:100"`
- Version uint64 `gorm:"column:version;default:1"`
- Status app.AppSessionStatus `gorm:"column:status;not null"`
- CreatedAt time.Time
- UpdatedAt time.Time
+ ID string `gorm:"primaryKey"`
+ ApplicationID string `gorm:"column:application_id;not null"`
+ Nonce uint64 `gorm:"column:nonce;not null"`
+ Participants []AppParticipantV1 `gorm:"foreignKey:AppSessionID;references:ID"`
+ SessionData string `gorm:"column:session_data;type:text;not null"`
+ Quorum uint8 `gorm:"column:quorum;default:100"`
+ Version uint64 `gorm:"column:version;default:1"`
+ Status app.AppSessionStatus `gorm:"column:status;not null"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
}
func (AppSessionV1) TableName() string {
@@ -51,16 +51,16 @@ func (s *DBStore) CreateAppSession(session app.AppSessionV1) error {
}
dbSession := AppSessionV1{
- ID: strings.ToLower(session.SessionID),
- Application: session.Application,
- Nonce: session.Nonce,
- Participants: participants,
- SessionData: session.SessionData,
- Quorum: session.Quorum,
- Version: session.Version,
- Status: session.Status,
- CreatedAt: session.CreatedAt,
- UpdatedAt: session.UpdatedAt,
+ ID: strings.ToLower(session.SessionID),
+ ApplicationID: strings.ToLower(session.ApplicationID),
+ Nonce: session.Nonce,
+ Participants: participants,
+ SessionData: session.SessionData,
+ Quorum: session.Quorum,
+ Version: session.Version,
+ Status: session.Status,
+ CreatedAt: session.CreatedAt,
+ UpdatedAt: session.UpdatedAt,
}
if err := s.db.Create(&dbSession).Error; err != nil {
@@ -133,26 +133,6 @@ func (s *DBStore) GetAppSessions(appSessionID *string, participant *string, stat
return sessions, metadata, nil
}
-// AppSessionCount holds the result of a COUNT() GROUP BY query on app sessions.
-type AppSessionCount struct {
- Application string `gorm:"column:application"`
- Status app.AppSessionStatus `gorm:"column:status"`
- Count uint64 `gorm:"column:count"`
-}
-
-// CountAppSessionsByStatus returns app session counts grouped by (application, status).
-func (s *DBStore) CountAppSessionsByStatus() ([]AppSessionCount, error) {
- var results []AppSessionCount
- err := s.db.Model(&AppSessionV1{}).
- Select("application, status, COUNT(id) as count").
- Group("application, status").
- Find(&results).Error
- if err != nil {
- return nil, fmt.Errorf("failed to count app sessions: %w", err)
- }
- return results, nil
-}
-
// UpdateAppSession updates existing session data with optimistic locking.
func (s *DBStore) UpdateAppSession(session app.AppSessionV1) error {
return s.db.Transaction(func(tx *gorm.DB) error {
diff --git a/clearnode/store/database/app_session_key_state.go b/clearnode/store/database/app_session_key_state.go
index 2733f2fc9..76bcdd597 100644
--- a/clearnode/store/database/app_session_key_state.go
+++ b/clearnode/store/database/app_session_key_state.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/app"
"gorm.io/gorm"
)
@@ -194,7 +194,7 @@ func (s *DBStore) GetAppSessionKeyOwner(sessionKey, appSessionId string) (string
appSessionId = strings.ToLower(appSessionId)
// Subquery to get the application ID from the app session
- appSubQuery := s.db.Model(&AppSessionV1{}).Select("application").Where("id = ?", appSessionId)
+ appSubQuery := s.db.Model(&AppSessionV1{}).Select("application_id").Where("id = ?", appSessionId)
maxVersionSubQ := s.db.Model(&AppSessionKeyStateV1{}).
Select("MAX(version)").
diff --git a/clearnode/store/database/app_session_key_state_test.go b/clearnode/store/database/app_session_key_state_test.go
index 2818e1507..823f76049 100644
--- a/clearnode/store/database/app_session_key_state_test.go
+++ b/clearnode/store/database/app_session_key_state_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/app"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -852,9 +852,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) {
// Create an app session that the session key references
appSession := app.AppSessionV1{
- SessionID: testSess1,
- Application: "poker",
- Nonce: 1,
+ SessionID: testSess1,
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{WalletAddress: testUser1, SignatureWeight: 100},
},
@@ -891,9 +891,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) {
// Create an app session with a specific application
appSession := app.AppSessionV1{
- SessionID: testSess1,
- Application: testApp1,
- Nonce: 1,
+ SessionID: testSess1,
+ ApplicationID: testApp1,
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{WalletAddress: testUser1, SignatureWeight: 100},
},
@@ -941,9 +941,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) {
// Create an app session
appSession := app.AppSessionV1{
- SessionID: testSess1,
- Application: "poker",
- Nonce: 1,
+ SessionID: testSess1,
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{WalletAddress: testUser1, SignatureWeight: 100},
},
@@ -980,9 +980,9 @@ func TestDBStore_GetAppSessionKeyOwner(t *testing.T) {
// Create an app session
appSession := app.AppSessionV1{
- SessionID: testSess1,
- Application: "poker",
- Nonce: 1,
+ SessionID: testSess1,
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{WalletAddress: testUser1, SignatureWeight: 100},
},
diff --git a/clearnode/store/database/app_session_test.go b/clearnode/store/database/app_session_test.go
index c0b9437d7..0d8746b92 100644
--- a/clearnode/store/database/app_session_test.go
+++ b/clearnode/store/database/app_session_test.go
@@ -4,8 +4,8 @@ import (
"testing"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -28,9 +28,9 @@ func TestDBStore_CreateAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "session123",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session123",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -54,7 +54,7 @@ func TestDBStore_CreateAppSession(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "session123", dbSession.ID)
- assert.Equal(t, "poker", dbSession.Application)
+ assert.Equal(t, "poker", dbSession.ApplicationID)
assert.Equal(t, uint64(1), dbSession.Nonce)
assert.Equal(t, `{"state": "active"}`, dbSession.SessionData)
assert.Equal(t, uint8(100), dbSession.Quorum)
@@ -72,9 +72,9 @@ func TestDBStore_CreateAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "session456",
- Application: "chess",
- Nonce: 1,
+ SessionID: "session456",
+ ApplicationID: "chess",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -118,9 +118,9 @@ func TestDBStore_CreateAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "session789",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session789",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -152,9 +152,9 @@ func TestDBStore_GetAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "session123",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session123",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -176,7 +176,7 @@ func TestDBStore_GetAppSession(t *testing.T) {
require.NotNil(t, result)
assert.Equal(t, "session123", result.SessionID)
- assert.Equal(t, "poker", result.Application)
+ assert.Equal(t, "poker", result.ApplicationID)
assert.Equal(t, uint64(1), result.Nonce)
assert.Equal(t, `{"state": "active"}`, result.SessionData)
assert.Equal(t, uint8(100), result.Quorum)
@@ -207,9 +207,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
// Create multiple sessions
session1 := app.AppSessionV1{
- SessionID: "session1",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session1",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -225,9 +225,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
}
session2 := app.AppSessionV1{
- SessionID: "session2",
- Application: "chess",
- Nonce: 1,
+ SessionID: "session2",
+ ApplicationID: "chess",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser456",
@@ -264,9 +264,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
store := NewDBStore(db)
session1 := app.AppSessionV1{
- SessionID: "session1",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session1",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -282,9 +282,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
}
session2 := app.AppSessionV1{
- SessionID: "session2",
- Application: "chess",
- Nonce: 1,
+ SessionID: "session2",
+ ApplicationID: "chess",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser456",
@@ -319,9 +319,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
store := NewDBStore(db)
session1 := app.AppSessionV1{
- SessionID: "session1",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session1",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -337,9 +337,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
}
session2 := app.AppSessionV1{
- SessionID: "session2",
- Application: "chess",
- Nonce: 1,
+ SessionID: "session2",
+ ApplicationID: "chess",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser456",
@@ -374,9 +374,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
store := NewDBStore(db)
session1 := app.AppSessionV1{
- SessionID: "session1",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session1",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -392,9 +392,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
}
session2 := app.AppSessionV1{
- SessionID: "session2",
- Application: "chess",
- Nonce: 1,
+ SessionID: "session2",
+ ApplicationID: "chess",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser456",
@@ -432,9 +432,9 @@ func TestDBStore_GetAppSessions(t *testing.T) {
for i := 1; i <= 3; i++ {
sessionID := "session" + string(rune(i+'0'))
session := app.AppSessionV1{
- SessionID: sessionID,
- Application: "poker",
- Nonce: uint64(i),
+ SessionID: sessionID,
+ ApplicationID: "poker",
+ Nonce: uint64(i),
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -500,9 +500,9 @@ func TestDBStore_UpdateAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "session123",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session123",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -544,9 +544,9 @@ func TestDBStore_UpdateAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "nonexistent",
- Application: "poker",
- Nonce: 1,
+ SessionID: "nonexistent",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
@@ -573,9 +573,9 @@ func TestDBStore_UpdateAppSession(t *testing.T) {
store := NewDBStore(db)
session := app.AppSessionV1{
- SessionID: "session456",
- Application: "poker",
- Nonce: 1,
+ SessionID: "session456",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0xuser123",
diff --git a/clearnode/store/database/app_test.go b/clearnode/store/database/app_test.go
new file mode 100644
index 000000000..2293ddada
--- /dev/null
+++ b/clearnode/store/database/app_test.go
@@ -0,0 +1,317 @@
+package database
+
+import (
+ "testing"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAppV1_TableName(t *testing.T) {
+ a := AppV1{}
+ assert.Equal(t, "apps_v1", a.TableName())
+}
+
+func TestDBStore_CreateApp(t *testing.T) {
+ t.Run("Success", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ entry := app.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0xabcdef",
+ Version: 1,
+ CreationApprovalNotRequired: true,
+ }
+
+ err := store.CreateApp(entry)
+ require.NoError(t, err)
+
+ // Verify app was created
+ var dbApp AppV1
+ err = db.Where("id = ?", "test-app").First(&dbApp).Error
+ require.NoError(t, err)
+
+ assert.Equal(t, "test-app", dbApp.ID)
+ assert.Equal(t, "0x1111111111111111111111111111111111111111", dbApp.OwnerWallet)
+ assert.Equal(t, "0xabcdef", dbApp.Metadata)
+ assert.Equal(t, uint64(1), dbApp.Version)
+ assert.True(t, dbApp.CreationApprovalNotRequired)
+ })
+
+ t.Run("Duplicate ID error", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ entry := app.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0xabcdef",
+ Version: 1,
+ }
+
+ err := store.CreateApp(entry)
+ require.NoError(t, err)
+
+ // Try to create again with same ID
+ err = store.CreateApp(entry)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "failed to create app")
+ })
+
+ t.Run("Stores lowercase ID and wallet", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ entry := app.AppV1{
+ ID: "My-App",
+ OwnerWallet: "0xABCD1234567890ABCDEF1234567890ABCDEF1234",
+ Metadata: "0x00",
+ Version: 1,
+ }
+
+ err := store.CreateApp(entry)
+ require.NoError(t, err)
+
+ var dbApp AppV1
+ err = db.Where("id = ?", "my-app").First(&dbApp).Error
+ require.NoError(t, err)
+
+ assert.Equal(t, "my-app", dbApp.ID)
+ assert.Equal(t, "0xabcd1234567890abcdef1234567890abcdef1234", dbApp.OwnerWallet)
+ })
+}
+
+func TestDBStore_GetApp(t *testing.T) {
+ t.Run("Found", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ entry := app.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0xabcdef",
+ Version: 1,
+ CreationApprovalNotRequired: true,
+ }
+ require.NoError(t, store.CreateApp(entry))
+
+ result, err := store.GetApp("test-app")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+
+ assert.Equal(t, "test-app", result.App.ID)
+ assert.Equal(t, "0x1111111111111111111111111111111111111111", result.App.OwnerWallet)
+ assert.Equal(t, "0xabcdef", result.App.Metadata)
+ assert.Equal(t, uint64(1), result.App.Version)
+ assert.True(t, result.App.CreationApprovalNotRequired)
+ })
+
+ t.Run("Not found returns nil", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ result, err := store.GetApp("nonexistent")
+ require.NoError(t, err)
+ assert.Nil(t, result)
+ })
+
+ t.Run("Case-insensitive lookup", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ entry := app.AppV1{
+ ID: "test-app",
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x00",
+ Version: 1,
+ }
+ require.NoError(t, store.CreateApp(entry))
+
+ // Look up with different casing
+ result, err := store.GetApp("Test-App")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ assert.Equal(t, "test-app", result.App.ID)
+ })
+}
+
+func TestDBStore_GetApps(t *testing.T) {
+ t.Run("No filter", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-1", OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x01", Version: 1,
+ }))
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-2", OwnerWallet: "0x2222222222222222222222222222222222222222",
+ Metadata: "0x02", Version: 1,
+ }))
+
+ // Small delay to ensure different created_at times
+ time.Sleep(10 * time.Millisecond)
+
+ pagination := &core.PaginationParams{}
+ apps, metadata, err := store.GetApps(nil, nil, pagination)
+ require.NoError(t, err)
+
+ assert.Len(t, apps, 2)
+ assert.Equal(t, uint32(2), metadata.TotalCount)
+ })
+
+ t.Run("Filter by appID", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-1", OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x01", Version: 1,
+ }))
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-2", OwnerWallet: "0x2222222222222222222222222222222222222222",
+ Metadata: "0x02", Version: 1,
+ }))
+
+ appID := "app-1"
+ pagination := &core.PaginationParams{}
+ apps, metadata, err := store.GetApps(&appID, nil, pagination)
+ require.NoError(t, err)
+
+ assert.Len(t, apps, 1)
+ assert.Equal(t, uint32(1), metadata.TotalCount)
+ assert.Equal(t, "app-1", apps[0].App.ID)
+ })
+
+ t.Run("Filter by ownerWallet", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ owner := "0x1111111111111111111111111111111111111111"
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-1", OwnerWallet: owner, Metadata: "0x01", Version: 1,
+ }))
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-2", OwnerWallet: "0x2222222222222222222222222222222222222222",
+ Metadata: "0x02", Version: 1,
+ }))
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-3", OwnerWallet: owner, Metadata: "0x03", Version: 1,
+ }))
+
+ pagination := &core.PaginationParams{}
+ apps, metadata, err := store.GetApps(nil, &owner, pagination)
+ require.NoError(t, err)
+
+ assert.Len(t, apps, 2)
+ assert.Equal(t, uint32(2), metadata.TotalCount)
+ for _, a := range apps {
+ assert.Equal(t, owner, a.App.OwnerWallet)
+ }
+ })
+
+ t.Run("Combined filters", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ owner := "0x1111111111111111111111111111111111111111"
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-1", OwnerWallet: owner, Metadata: "0x01", Version: 1,
+ }))
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-2", OwnerWallet: owner, Metadata: "0x02", Version: 1,
+ }))
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-3", OwnerWallet: "0x2222222222222222222222222222222222222222",
+ Metadata: "0x03", Version: 1,
+ }))
+
+ appID := "app-1"
+ pagination := &core.PaginationParams{}
+ apps, metadata, err := store.GetApps(&appID, &owner, pagination)
+ require.NoError(t, err)
+
+ assert.Len(t, apps, 1)
+ assert.Equal(t, uint32(1), metadata.TotalCount)
+ assert.Equal(t, "app-1", apps[0].App.ID)
+ assert.Equal(t, owner, apps[0].App.OwnerWallet)
+ })
+
+ t.Run("Pagination", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ for i := 1; i <= 3; i++ {
+ require.NoError(t, store.CreateApp(app.AppV1{
+ ID: "app-" + string(rune(i+'0')),
+ OwnerWallet: "0x1111111111111111111111111111111111111111",
+ Metadata: "0x00",
+ Version: 1,
+ }))
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ limit := uint32(2)
+ offset := uint32(0)
+ pagination := &core.PaginationParams{Limit: &limit, Offset: &offset}
+
+ apps, metadata, err := store.GetApps(nil, nil, pagination)
+ require.NoError(t, err)
+
+ assert.Len(t, apps, 2)
+ assert.Equal(t, uint32(3), metadata.TotalCount)
+ assert.Equal(t, uint32(1), metadata.Page)
+ assert.Equal(t, uint32(2), metadata.PerPage)
+
+ // Second page
+ offset = 2
+ pagination.Offset = &offset
+ apps, metadata, err = store.GetApps(nil, nil, pagination)
+ require.NoError(t, err)
+
+ assert.Len(t, apps, 1)
+ assert.Equal(t, uint32(2), metadata.Page)
+ })
+
+ t.Run("Empty results", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+
+ store := NewDBStore(db)
+
+ appID := "nonexistent"
+ pagination := &core.PaginationParams{}
+ apps, metadata, err := store.GetApps(&appID, nil, pagination)
+ require.NoError(t, err)
+
+ assert.Empty(t, apps)
+ assert.Equal(t, uint32(0), metadata.TotalCount)
+ })
+}
diff --git a/clearnode/store/database/blockchain_action_test.go b/clearnode/store/database/blockchain_action_test.go
index a1a09cff1..c94e24be7 100644
--- a/clearnode/store/database/blockchain_action_test.go
+++ b/clearnode/store/database/blockchain_action_test.go
@@ -3,8 +3,8 @@ package database
import (
"testing"
- "github.com/erc7824/nitrolite/pkg/core"
"github.com/ethereum/go-ethereum/common"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/clearnode/store/database/channel.go b/clearnode/store/database/channel.go
index a407ea7c8..3979fa95a 100644
--- a/clearnode/store/database/channel.go
+++ b/clearnode/store/database/channel.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"gorm.io/gorm"
)
@@ -152,27 +152,6 @@ func (s *DBStore) GetUserChannels(wallet string, status *core.ChannelStatus, ass
return channels, uint32(totalCount), nil
}
-// ChannelCount holds the result of a COUNT() GROUP BY query on channels.
-type ChannelCount struct {
- Asset string `gorm:"column:asset"`
- Status core.ChannelStatus `gorm:"column:status"`
- Count uint64 `gorm:"column:count"`
-}
-
-// CountChannelsByStatus returns channel counts grouped by (asset, status).
-func (s *DBStore) CountChannelsByStatus() ([]ChannelCount, error) {
- var results []ChannelCount
- err := s.db.Raw(`
- SELECT asset, status, COUNT(*) as count
- FROM channels
- GROUP BY asset, status
- `).Scan(&results).Error
- if err != nil {
- return nil, fmt.Errorf("failed to count channels: %w", err)
- }
- return results, nil
-}
-
// UpdateChannel persists changes to a channel's metadata (status, version, etc).
func (s *DBStore) UpdateChannel(channel core.Channel) error {
updates := map[string]interface{}{
diff --git a/clearnode/store/database/channel_session_key_state.go b/clearnode/store/database/channel_session_key_state.go
index 6eb23e179..aa41159a7 100644
--- a/clearnode/store/database/channel_session_key_state.go
+++ b/clearnode/store/database/channel_session_key_state.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"gorm.io/gorm"
)
diff --git a/clearnode/store/database/channel_session_key_state_test.go b/clearnode/store/database/channel_session_key_state_test.go
index a6f670105..4263cc63d 100644
--- a/clearnode/store/database/channel_session_key_state_test.go
+++ b/clearnode/store/database/channel_session_key_state_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/clearnode/store/database/channel_test.go b/clearnode/store/database/channel_test.go
index 2e13dbe6d..0be5d2559 100644
--- a/clearnode/store/database/channel_test.go
+++ b/clearnode/store/database/channel_test.go
@@ -5,7 +5,7 @@ import (
"testing"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/clearnode/store/database/contract_event.go b/clearnode/store/database/contract_event.go
index b11465ece..955feb58d 100644
--- a/clearnode/store/database/contract_event.go
+++ b/clearnode/store/database/contract_event.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"gorm.io/gorm"
)
diff --git a/clearnode/store/database/contract_event_test.go b/clearnode/store/database/contract_event_test.go
index 090dd17f4..72e054d30 100644
--- a/clearnode/store/database/contract_event_test.go
+++ b/clearnode/store/database/contract_event_test.go
@@ -3,7 +3,7 @@ package database
import (
"testing"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/clearnode/store/database/database.go b/clearnode/store/database/database.go
index 4ba74d4d6..bb01c3189 100644
--- a/clearnode/store/database/database.go
+++ b/clearnode/store/database/database.go
@@ -67,6 +67,7 @@ func connectToPostgresql(cnf DatabaseConfig, embedMigrations embed.FS) (*gorm.DB
}
dial := postgres.Open(dsn)
+ // TODO: don't print SQL in logs
db, err := gorm.Open(dial, &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: cnf.Schema + ".", // schema name
@@ -213,7 +214,7 @@ func migratePostgres(cnf DatabaseConfig, embedMigrations embed.FS) error {
}
func migrateSqlite(db *gorm.DB) error {
- if err := db.AutoMigrate(&AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}); err != nil {
+ if err := db.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}, &LifespanMetric{}); err != nil {
return err
}
return nil
diff --git a/clearnode/store/database/db_store.go b/clearnode/store/database/db_store.go
index 21fe52a91..c87ae2331 100644
--- a/clearnode/store/database/db_store.go
+++ b/clearnode/store/database/db_store.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"gorm.io/gorm/clause"
diff --git a/clearnode/store/database/db_store_test.go b/clearnode/store/database/db_store_test.go
index 3a819ded0..e5b113d10 100644
--- a/clearnode/store/database/db_store_test.go
+++ b/clearnode/store/database/db_store_test.go
@@ -3,7 +3,7 @@ package database
import (
"testing"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/clearnode/store/database/interface.go b/clearnode/store/database/interface.go
index d6b5fc7b8..06bb4cbba 100644
--- a/clearnode/store/database/interface.go
+++ b/clearnode/store/database/interface.go
@@ -1,8 +1,10 @@
package database
import (
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
)
@@ -113,6 +115,20 @@ type DatabaseStore interface {
// GetStateByID retrieves a state by its deterministic ID.
GetStateByID(stateID string) (*core.State, error)
+ // --- App Registry Operations ---
+
+ // CreateApp registers a new application. Returns an error if the app ID already exists.
+ CreateApp(entry app.AppV1) error
+
+ // GetApp retrieves a single application by ID. Returns nil if not found.
+ GetApp(appID string) (*app.AppInfoV1, error)
+
+ // GetApps retrieves applications with optional filtering by app ID, owner wallet, and pagination.
+ GetApps(appID *string, ownerWallet *string, pagination *core.PaginationParams) ([]app.AppInfoV1, core.PaginationMetadata, error)
+
+ // GetAppCount returns the total number of applications owned by a specific wallet.
+ GetAppCount(ownerWallet string) (uint64, error)
+
// --- App Session Operations ---
// CreateAppSession initializes a new application session.
@@ -176,11 +192,45 @@ type DatabaseStore interface {
// --- Metric Aggregation ---
- // CountAppSessionsByStatus returns app session counts grouped by (application, status).
- CountAppSessionsByStatus() ([]AppSessionCount, error)
+ // CountActiveUsers returns distinct user counts per asset within the given window.
+ CountActiveUsers(window time.Duration) ([]ActiveCountByLabel, error)
+
+ // CountActiveAppSessions returns app session counts per application within the given window.
+ CountActiveAppSessions(window time.Duration) ([]ActiveCountByLabel, error)
+
+ // --- Lifespan Metric Operations ---
+
+ // GetLifetimeMetricLastTimestamp returns the most recent last_timestamp among all metrics with the given name.
+ GetLifetimeMetricLastTimestamp(name string) (time.Time, error)
+
+ // GetAppSessionsCountByLabels computes app session count deltas, upserts as lifespan metrics, and returns updated totals.
+ GetAppSessionsCountByLabels() ([]AppSessionCount, error)
+
+ // GetChannelsCountByLabels computes channel count deltas, upserts as lifespan metrics, and returns updated totals.
+ GetChannelsCountByLabels() ([]ChannelCount, error)
+
+ // GetTotalValueLocked computes TVL deltas by domain (channels, app_sessions) and asset, upserts as lifespan metrics, and returns updated totals.
+ GetTotalValueLocked() ([]TotalValueLocked, error)
+
+ // --- User Staked Operations ---
+
+ // UpdateUserStaked upserts the staked amount for a user on a specific blockchain.
+ UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error
+
+ // GetTotalUserStaked returns the total staked amount for a user across all blockchains.
+ GetTotalUserStaked(wallet string) (decimal.Decimal, error)
+
+ // --- Action Log Operations ---
+
+ // RecordAction inserts a new action log entry for a user.
+ RecordAction(wallet string, gatedAction core.GatedAction) error
+
+ // GetUserActionCount returns the number of actions matching the given wallet, method, and path
+ // within the specified time window.
+ GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error)
- // CountChannelsByStatus returns channel counts grouped by (asset, status).
- CountChannelsByStatus() ([]ChannelCount, error)
+ // GetUserActionCounts returns a map of gated actions to their respective counts for a user within the specified time window.
+ GetUserActionCounts(userWallet string, window time.Duration) (map[core.GatedAction]uint64, error)
// --- Contract Event Operations ---
diff --git a/clearnode/store/database/lifespan_metric.go b/clearnode/store/database/lifespan_metric.go
new file mode 100644
index 000000000..db3498d9d
--- /dev/null
+++ b/clearnode/store/database/lifespan_metric.go
@@ -0,0 +1,439 @@
+package database
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/shopspring/decimal"
+ "gorm.io/datatypes"
+ "gorm.io/gorm"
+)
+
+type LifespanMetric struct {
+ ID string `gorm:"column:id;primaryKey;size:66"`
+ Name string `gorm:"column:name;not null"`
+ Labels datatypes.JSON `gorm:"column:labels;type:text"`
+ Value decimal.Decimal `gorm:"column:value;type:varchar(78);not null"`
+ LastTimestamp time.Time `gorm:"column:last_timestamp;not null"`
+ UpdatedAt time.Time
+}
+
+func (LifespanMetric) TableName() string {
+ return "lifespan_metrics"
+}
+
+// ChannelCount holds the result of a COUNT() GROUP BY query on channels.
+type ChannelCount struct {
+ Asset string `gorm:"column:asset"`
+ Status core.ChannelStatus `gorm:"column:status"`
+ Count uint64 `gorm:"column:count"`
+ LastUpdated time.Time `gorm:"column:last_updated"`
+}
+
+// GetChannelsCountByLabels computes channel count deltas since last processed timestamp,
+// upserts them as lifespan metrics, and returns the updated totals.
+func (s *DBStore) GetChannelsCountByLabels() ([]ChannelCount, error) {
+ metricName := "channels_total"
+
+ lastProcessedTimestamp, err := s.GetLifetimeMetricLastTimestamp(metricName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get last processed timestamp: %w", err)
+ }
+
+ var deltas []ChannelCount
+ err = s.db.Raw(`
+ SELECT asset,
+ status AS status,
+ COUNT(channel_id)::bigint AS count,
+ MAX(updated_at) AS last_updated
+ FROM channels
+ WHERE updated_at > ?
+ GROUP BY asset, status
+ `, lastProcessedTimestamp).Scan(&deltas).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to compute channel deltas: %w", err)
+ }
+
+ if len(deltas) > 0 {
+ now := time.Now()
+ valuesSQL := make([]string, 0, len(deltas))
+ args := make([]any, 0, len(deltas)*6)
+
+ for i, d := range deltas {
+ labelsMap := map[string]string{
+ "asset": d.Asset,
+ "status": d.Status.String(),
+ }
+ labelsJSON, err := json.Marshal(labelsMap)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal labels for asset=%s status=%s: %w", d.Asset, d.Status, err)
+ }
+
+ id, err := getMetricID(metricName, "asset", d.Asset, "status", d.Status.String())
+ if err != nil {
+ return nil, fmt.Errorf("failed to compute metric ID for asset=%s status=%s: %w", d.Asset, d.Status, err)
+ }
+
+ deltaValue := decimal.NewFromUint64(d.Count)
+ base := i * 6
+ valuesSQL = append(valuesSQL,
+ fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)",
+ base+1, base+2, base+3, base+4, base+5, base+6,
+ ),
+ )
+
+ args = append(args,
+ id, // $1
+ metricName, // $2
+ datatypes.JSON(labelsJSON), // $3
+ deltaValue, // $4
+ d.LastUpdated, // $5
+ now, // $6
+ )
+ }
+
+ upsertSQL := fmt.Sprintf(`
+ INSERT INTO lifespan_metrics (id, name, labels, value, last_timestamp, updated_at)
+ VALUES %s
+ ON CONFLICT (id) DO UPDATE
+ SET
+ value = lifespan_metrics.value + EXCLUDED.value,
+ last_timestamp = GREATEST(lifespan_metrics.last_timestamp, EXCLUDED.last_timestamp),
+ updated_at = now()
+ `, strings.Join(valuesSQL, ","))
+
+ if err := s.db.Exec(upsertSQL, args...).Error; err != nil {
+ return nil, fmt.Errorf("failed to upsert lifespan metrics: %w", err)
+ }
+ }
+
+ var results []ChannelCount
+ err = s.db.Raw(`
+ SELECT labels->>'asset' AS asset,
+ labels->>'status' AS status,
+ value::bigint AS count,
+ last_timestamp AS last_updated
+ FROM lifespan_metrics
+ WHERE name = ?
+ `, metricName).Scan(&results).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to read lifespan metrics: %w", err)
+ }
+
+ return results, nil
+}
+
+// AppSessionCount holds the result of a COUNT() GROUP BY query on app sessions.
+type AppSessionCount struct {
+ Application string `gorm:"column:application_id"`
+ Status app.AppSessionStatus `gorm:"column:status"`
+ Count uint64 `gorm:"column:count"`
+ LastUpdated time.Time `gorm:"column:last_updated"`
+}
+
+// GetAppSessionsCountByLabels computes app session count deltas since last processed timestamp,
+// upserts them as lifespan metrics, and returns the updated totals.
+func (s *DBStore) GetAppSessionsCountByLabels() ([]AppSessionCount, error) {
+ metricName := "app_sessions_total"
+
+ lastProcessedTimestamp, err := s.GetLifetimeMetricLastTimestamp(metricName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get last processed timestamp: %w", err)
+ }
+
+ // 2) Compute deltas since lastProcessedTimestamp.
+ var deltas []AppSessionCount
+ err = s.db.Raw(`
+ SELECT application_id,
+ status AS status,
+ COUNT(id)::bigint AS count,
+ MAX(updated_at) AS last_updated
+ FROM app_sessions_v1
+ WHERE updated_at > ?
+ GROUP BY application_id, status
+ `, lastProcessedTimestamp).Scan(&deltas).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to compute app sessions deltas: %w", err)
+ }
+
+ if len(deltas) > 0 {
+ now := time.Now()
+ valuesSQL := make([]string, 0, len(deltas))
+ args := make([]any, 0, len(deltas)*6)
+
+ for i, d := range deltas {
+ labelsMap := map[string]string{
+ "application_id": d.Application,
+ "status": d.Status.String(),
+ }
+ labelsJSON, err := json.Marshal(labelsMap)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal labels for application_id=%s status=%s: %w", d.Application, d.Status, err)
+ }
+
+ id, err := getMetricID(metricName, "application_id", d.Application, "status", d.Status.String())
+ if err != nil {
+ return nil, fmt.Errorf("failed to compute metric ID for application_id=%s status=%s: %w", d.Application, d.Status, err)
+ }
+
+ deltaValue := decimal.NewFromUint64(d.Count)
+ base := i * 6
+ valuesSQL = append(valuesSQL,
+ fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)",
+ base+1, base+2, base+3, base+4, base+5, base+6,
+ ),
+ )
+
+ args = append(args,
+ id, // $1
+ metricName, // $2
+ datatypes.JSON(labelsJSON), // $3
+ deltaValue, // $4
+ d.LastUpdated, // $5
+ now, // $6
+ )
+ }
+
+ upsertSQL := fmt.Sprintf(`
+ INSERT INTO lifespan_metrics (id, name, labels, value, last_timestamp, updated_at)
+ VALUES %s
+ ON CONFLICT (id) DO UPDATE
+ SET
+ value = lifespan_metrics.value + EXCLUDED.value,
+ last_timestamp = GREATEST(lifespan_metrics.last_timestamp, EXCLUDED.last_timestamp),
+ updated_at = now()
+ `, strings.Join(valuesSQL, ","))
+
+ if err := s.db.Exec(upsertSQL, args...).Error; err != nil {
+ return nil, fmt.Errorf("failed to upsert lifespan metrics: %w", err)
+ }
+ }
+
+ var results []AppSessionCount
+ err = s.db.Raw(`
+ SELECT labels->>'application_id' AS application_id,
+ labels->>'status' AS status,
+ value::bigint AS count,
+ last_timestamp AS last_updated
+ FROM lifespan_metrics
+ WHERE name = ?
+ `, metricName).Scan(&results).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to read lifespan metrics: %w", err)
+ }
+
+ return results, nil
+}
+
+// TotalValueLocked holds the total value locked for a given asset, along with the last update timestamp.
+type TotalValueLocked struct {
+ Asset string `gorm:"column:asset"`
+ Domain string `gorm:"column:domain"`
+ Value decimal.Decimal `gorm:"column:value"`
+ LastUpdated time.Time `gorm:"column:last_updated"`
+}
+
+func (s *DBStore) GetTotalValueLocked() ([]TotalValueLocked, error) {
+ metricName := "total_value_locked"
+
+ lastProcessedTimestamp, err := s.GetLifetimeMetricLastTimestamp(metricName)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get last processed timestamp: %w", err)
+ }
+
+ // Compute net TVL deltas since lastProcessedTimestamp:
+ // - channels: deposits (tx_type=10) minus withdrawals (tx_type=11)
+ // - app_sessions: commits (tx_type=40) minus releases (tx_type=41)
+ var deltas []TotalValueLocked
+ err = s.db.Raw(`
+ SELECT domain, asset_symbol AS asset, SUM(net) AS value, MAX(created_at) AS last_updated
+ FROM (
+ SELECT 'channels' AS domain, asset_symbol,
+ CASE WHEN tx_type = ? THEN amount ELSE -amount END AS net,
+ created_at
+ FROM transactions
+ WHERE tx_type IN (?, ?) AND created_at > ?
+ UNION ALL
+ SELECT 'app_sessions' AS domain, asset_symbol,
+ CASE WHEN tx_type = ? THEN amount ELSE -amount END AS net,
+ created_at
+ FROM transactions
+ WHERE tx_type IN (?, ?) AND created_at > ?
+ ) t
+ GROUP BY domain, asset_symbol
+ `,
+ core.TransactionTypeHomeDeposit, core.TransactionTypeHomeDeposit, core.TransactionTypeHomeWithdrawal, lastProcessedTimestamp,
+ core.TransactionTypeCommit, core.TransactionTypeCommit, core.TransactionTypeRelease, lastProcessedTimestamp,
+ ).Scan(&deltas).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to compute TVL deltas: %w", err)
+ }
+
+ if len(deltas) > 0 {
+ now := time.Now()
+ valuesSQL := make([]string, 0, len(deltas))
+ args := make([]any, 0, len(deltas)*6)
+
+ for i, d := range deltas {
+ labelsMap := map[string]string{
+ "domain": d.Domain,
+ "asset": d.Asset,
+ }
+ labelsJSON, err := json.Marshal(labelsMap)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal labels for domain=%s asset=%s: %w", d.Domain, d.Asset, err)
+ }
+
+ id, err := getMetricID(metricName, "domain", d.Domain, "asset", d.Asset)
+ if err != nil {
+ return nil, fmt.Errorf("failed to compute metric ID for domain=%s asset=%s: %w", d.Domain, d.Asset, err)
+ }
+
+ base := i * 6
+ valuesSQL = append(valuesSQL,
+ fmt.Sprintf("($%d,$%d,$%d,$%d,$%d,$%d)",
+ base+1, base+2, base+3, base+4, base+5, base+6,
+ ),
+ )
+
+ args = append(args,
+ id, // $1
+ metricName, // $2
+ datatypes.JSON(labelsJSON), // $3
+ d.Value, // $4
+ d.LastUpdated, // $5
+ now, // $6
+ )
+ }
+
+ upsertSQL := fmt.Sprintf(`
+ INSERT INTO lifespan_metrics (id, name, labels, value, last_timestamp, updated_at)
+ VALUES %s
+ ON CONFLICT (id) DO UPDATE
+ SET
+ value = lifespan_metrics.value + EXCLUDED.value,
+ last_timestamp = GREATEST(lifespan_metrics.last_timestamp, EXCLUDED.last_timestamp),
+ updated_at = now()
+ `, strings.Join(valuesSQL, ","))
+
+ if err := s.db.Exec(upsertSQL, args...).Error; err != nil {
+ return nil, fmt.Errorf("failed to upsert lifespan metrics: %w", err)
+ }
+ }
+
+ var results []TotalValueLocked
+ err = s.db.Raw(`
+ SELECT labels->>'domain' AS domain,
+ labels->>'asset' AS asset,
+ value,
+ last_timestamp AS last_updated
+ FROM lifespan_metrics
+ WHERE name = ?
+ `, metricName).Scan(&results).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to read lifespan metrics: %w", err)
+ }
+
+ return results, nil
+}
+
+// CountActiveUsers returns the number of distinct users who had channel state updates
+// within the given duration, grouped by asset. If asset is empty, counts across all assets.
+// ActiveCountByLabel holds a count grouped by a label (asset or application_id).
+type ActiveCountByLabel struct {
+ Label string `gorm:"column:label"`
+ Count uint64 `gorm:"column:count"`
+}
+
+// CountActiveUsers returns distinct user counts per asset and an "all" aggregate
+// for users with channel state updates within the given window.
+func (s *DBStore) CountActiveUsers(window time.Duration) ([]ActiveCountByLabel, error) {
+ since := time.Now().Add(-window)
+
+ var results []ActiveCountByLabel
+ err := s.db.Raw(`
+ SELECT asset AS label, COUNT(DISTINCT user_wallet) AS count
+ FROM user_balances
+ WHERE updated_at > ?
+ GROUP BY asset
+ `, since).Scan(&results).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to count active users: %w", err)
+ }
+
+ // "ALL" aggregate: distinct users across all assets.
+ var total uint64
+ err = s.db.Model(&UserBalance{}).
+ Select("COUNT(DISTINCT user_wallet)").
+ Where("updated_at > ?", since).
+ Scan(&total).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to count total active users: %w", err)
+ }
+
+ results = append(results, ActiveCountByLabel{Label: "ALL", Count: total})
+ return results, nil
+}
+
+// CountActiveAppSessions returns app session counts per application within the given window.
+func (s *DBStore) CountActiveAppSessions(window time.Duration) ([]ActiveCountByLabel, error) {
+ since := time.Now().Add(-window)
+
+ var results []ActiveCountByLabel
+ err := s.db.Raw(`
+ SELECT application_id AS label, COUNT(id) AS count
+ FROM app_sessions_v1
+ WHERE updated_at > ?
+ GROUP BY application_id
+ `, since).Scan(&results).Error
+ if err != nil {
+ return nil, fmt.Errorf("failed to count active app sessions: %w", err)
+ }
+
+ return results, nil
+}
+
+// GetLifetimeMetricLastTimestamp returns the most recent last_timestamp among all metrics with the given name.
+func (s *DBStore) GetLifetimeMetricLastTimestamp(name string) (time.Time, error) {
+ var metric LifespanMetric
+ err := s.db.Where("name = ?", name).
+ Order("last_timestamp DESC").
+ First(&metric).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return time.Time{}, nil
+ }
+ return time.Time{}, fmt.Errorf("failed to get metric last timestamp: %w", err)
+ }
+
+ return metric.LastTimestamp, nil
+}
+
+func getMetricID(name string, labels ...string) (string, error) {
+ var labelsArray = []string{}
+ labelsArray = append(labelsArray, labels...)
+
+ stringTy, _ := abi.NewType("string", "", nil)
+ stringSliceTy, _ := abi.NewType("string[]", "", nil)
+ args := abi.Arguments{
+ {Type: stringTy}, // name
+ {Type: stringSliceTy}, // labels array
+ }
+
+ packed, err := args.Pack(
+ name,
+ labelsArray,
+ )
+ if err != nil {
+ return "", fmt.Errorf("failed to pack app session request: %w", err)
+ }
+
+ return hexutil.Encode(crypto.Keccak256(packed)), nil
+}
diff --git a/clearnode/store/database/lifespan_metric_test.go b/clearnode/store/database/lifespan_metric_test.go
new file mode 100644
index 000000000..2ca2521b3
--- /dev/null
+++ b/clearnode/store/database/lifespan_metric_test.go
@@ -0,0 +1,243 @@
+package database
+
+import (
+ "testing"
+ "time"
+
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/shopspring/decimal"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGetMetricID(t *testing.T) {
+ t.Run("deterministic ID", func(t *testing.T) {
+ id1, err := getMetricID("test_metric", "key1", "val1")
+ require.NoError(t, err)
+
+ id2, err := getMetricID("test_metric", "key1", "val1")
+ require.NoError(t, err)
+
+ assert.Equal(t, id1, id2)
+ })
+
+ t.Run("different labels produce different IDs", func(t *testing.T) {
+ id1, err := getMetricID("test_metric", "key1", "val1")
+ require.NoError(t, err)
+
+ id2, err := getMetricID("test_metric", "key1", "val2")
+ require.NoError(t, err)
+
+ assert.NotEqual(t, id1, id2)
+ })
+
+ t.Run("different names produce different IDs", func(t *testing.T) {
+ id1, err := getMetricID("metric_a")
+ require.NoError(t, err)
+
+ id2, err := getMetricID("metric_b")
+ require.NoError(t, err)
+
+ assert.NotEqual(t, id1, id2)
+ })
+
+ t.Run("no labels", func(t *testing.T) {
+ id, err := getMetricID("metric_no_labels")
+ require.NoError(t, err)
+ assert.NotEmpty(t, id)
+ })
+
+ t.Run("ID starts with 0x", func(t *testing.T) {
+ id, err := getMetricID("test")
+ require.NoError(t, err)
+ assert.Equal(t, "0x", id[:2])
+ })
+}
+
+func TestGetLifetimeMetricLastTimestamp(t *testing.T) {
+ t.Run("no metrics returns zero time", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ ts, err := store.GetLifetimeMetricLastTimestamp("nonexistent")
+ require.NoError(t, err)
+ assert.True(t, ts.IsZero())
+ })
+
+ t.Run("returns most recent timestamp", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ ts1 := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
+ ts2 := time.Now().Add(-1 * time.Hour).Truncate(time.Second)
+ ts3 := time.Now().Truncate(time.Second)
+
+ db.Create(&LifespanMetric{ID: "id-a", Name: "my_metric", Value: decimal.NewFromInt(1), LastTimestamp: ts1})
+ db.Create(&LifespanMetric{ID: "id-b", Name: "my_metric", Value: decimal.NewFromInt(2), LastTimestamp: ts3})
+ db.Create(&LifespanMetric{ID: "id-c", Name: "my_metric", Value: decimal.NewFromInt(3), LastTimestamp: ts2})
+
+ latest, err := store.GetLifetimeMetricLastTimestamp("my_metric")
+ require.NoError(t, err)
+ assert.Equal(t, ts3.UTC(), latest.UTC())
+ })
+
+ t.Run("scoped to metric name", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ tsOld := time.Now().Add(-time.Hour).Truncate(time.Second)
+ tsNew := time.Now().Truncate(time.Second)
+
+ db.Create(&LifespanMetric{ID: "id-1", Name: "metric_a", Value: decimal.NewFromInt(1), LastTimestamp: tsOld})
+ db.Create(&LifespanMetric{ID: "id-2", Name: "metric_b", Value: decimal.NewFromInt(1), LastTimestamp: tsNew})
+
+ latest, err := store.GetLifetimeMetricLastTimestamp("metric_a")
+ require.NoError(t, err)
+ assert.Equal(t, tsOld.UTC(), latest.UTC())
+ })
+}
+
+func TestCountActiveUsers(t *testing.T) {
+ t.Run("no data returns only ALL with zero", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ results, err := store.CountActiveUsers(24 * time.Hour)
+ require.NoError(t, err)
+
+ require.Len(t, results, 1)
+ assert.Equal(t, "ALL", results[0].Label)
+ assert.Equal(t, uint64(0), results[0].Count)
+ })
+
+ t.Run("counts distinct users per asset", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ now := time.Now()
+ db.Create(&UserBalance{UserWallet: "0xuser1", Asset: "USDC", Balance: decimal.NewFromInt(100), UpdatedAt: now})
+ db.Create(&UserBalance{UserWallet: "0xuser2", Asset: "USDC", Balance: decimal.NewFromInt(200), UpdatedAt: now})
+ db.Create(&UserBalance{UserWallet: "0xuser1", Asset: "ETH", Balance: decimal.NewFromInt(50), UpdatedAt: now})
+
+ results, err := store.CountActiveUsers(24 * time.Hour)
+ require.NoError(t, err)
+
+ require.Len(t, results, 3)
+
+ countByLabel := make(map[string]uint64)
+ for _, r := range results {
+ countByLabel[r.Label] = r.Count
+ }
+
+ assert.Equal(t, uint64(2), countByLabel["USDC"])
+ assert.Equal(t, uint64(1), countByLabel["ETH"])
+ assert.Equal(t, uint64(2), countByLabel["ALL"])
+ })
+
+ t.Run("respects time window", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ old := time.Now().Add(-48 * time.Hour)
+ recent := time.Now()
+ db.Create(&UserBalance{UserWallet: "0xold", Asset: "USDC", Balance: decimal.NewFromInt(100), UpdatedAt: old})
+ db.Create(&UserBalance{UserWallet: "0xnew", Asset: "USDC", Balance: decimal.NewFromInt(200), UpdatedAt: recent})
+
+ results, err := store.CountActiveUsers(24 * time.Hour)
+ require.NoError(t, err)
+
+ countByLabel := make(map[string]uint64)
+ for _, r := range results {
+ countByLabel[r.Label] = r.Count
+ }
+
+ assert.Equal(t, uint64(1), countByLabel["USDC"])
+ assert.Equal(t, uint64(1), countByLabel["ALL"])
+ })
+}
+
+func TestCountActiveAppSessions(t *testing.T) {
+ t.Run("no data returns empty", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ results, err := store.CountActiveAppSessions(24 * time.Hour)
+ require.NoError(t, err)
+ assert.Empty(t, results)
+ })
+
+ t.Run("counts sessions per application", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ now := time.Now()
+ db.Create(&AppSessionV1{ID: "s1", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now})
+ db.Create(&AppSessionV1{ID: "s2", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 2, UpdatedAt: now})
+ db.Create(&AppSessionV1{ID: "s3", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now})
+
+ results, err := store.CountActiveAppSessions(24 * time.Hour)
+ require.NoError(t, err)
+
+ countByLabel := make(map[string]uint64)
+ for _, r := range results {
+ countByLabel[r.Label] = r.Count
+ }
+
+ assert.Equal(t, uint64(2), countByLabel["app1"])
+ assert.Equal(t, uint64(1), countByLabel["app2"])
+ })
+
+ t.Run("respects time window", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ old := time.Now().Add(-48 * time.Hour)
+ recent := time.Now()
+ db.Create(&AppSessionV1{ID: "s1", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: old})
+ db.Create(&AppSessionV1{ID: "s2", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 2, UpdatedAt: recent})
+
+ results, err := store.CountActiveAppSessions(24 * time.Hour)
+ require.NoError(t, err)
+
+ countByLabel := make(map[string]uint64)
+ for _, r := range results {
+ countByLabel[r.Label] = r.Count
+ }
+
+ assert.Equal(t, uint64(1), countByLabel["app1"])
+ })
+
+ t.Run("multiple applications with mixed statuses", func(t *testing.T) {
+ db, cleanup := SetupTestDB(t)
+ defer cleanup()
+ store := &DBStore{db: db}
+
+ now := time.Now()
+ db.Create(&AppSessionV1{ID: "s1", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now})
+ db.Create(&AppSessionV1{ID: "s2", ApplicationID: "app1", SessionData: "{}", Status: app.AppSessionStatusClosed, Nonce: 2, UpdatedAt: now})
+ db.Create(&AppSessionV1{ID: "s3", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 1, UpdatedAt: now})
+ db.Create(&AppSessionV1{ID: "s4", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusOpen, Nonce: 2, UpdatedAt: now})
+ db.Create(&AppSessionV1{ID: "s5", ApplicationID: "app2", SessionData: "{}", Status: app.AppSessionStatusClosed, Nonce: 3, UpdatedAt: now})
+
+ results, err := store.CountActiveAppSessions(24 * time.Hour)
+ require.NoError(t, err)
+
+ countByLabel := make(map[string]uint64)
+ for _, r := range results {
+ countByLabel[r.Label] = r.Count
+ }
+
+ // CountActiveAppSessions counts all sessions regardless of status
+ assert.Equal(t, uint64(2), countByLabel["app1"])
+ assert.Equal(t, uint64(3), countByLabel["app2"])
+ })
+}
diff --git a/clearnode/store/database/state.go b/clearnode/store/database/state.go
index 9f62911ab..b4191a7e1 100644
--- a/clearnode/store/database/state.go
+++ b/clearnode/store/database/state.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
diff --git a/clearnode/store/database/state_test.go b/clearnode/store/database/state_test.go
index aab572bd7..08e6e5ddc 100644
--- a/clearnode/store/database/state_test.go
+++ b/clearnode/store/database/state_test.go
@@ -3,7 +3,7 @@ package database
import (
"testing"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/clearnode/store/database/test/postgres_integration_test.go b/clearnode/store/database/test/postgres_integration_test.go
index 59a44a316..fb6a01f42 100644
--- a/clearnode/store/database/test/postgres_integration_test.go
+++ b/clearnode/store/database/test/postgres_integration_test.go
@@ -6,9 +6,9 @@ import (
"testing"
"time"
- "github.com/erc7824/nitrolite/clearnode/store/database"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/clearnode/store/database"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -254,9 +254,9 @@ func TestPostgres_AppSessionOperations(t *testing.T) {
t.Run("Create and retrieve app session", func(t *testing.T) {
session := app.AppSessionV1{
- SessionID: "0x7234567890123456789012345678901234567890123456789012345678901234",
- Application: "poker",
- Nonce: 1,
+ SessionID: "0x7234567890123456789012345678901234567890123456789012345678901234",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0x7334567890123456789012345678901234567890",
@@ -283,7 +283,7 @@ func TestPostgres_AppSessionOperations(t *testing.T) {
require.NotNil(t, retrieved)
assert.Equal(t, session.SessionID, retrieved.SessionID)
- assert.Equal(t, session.Application, retrieved.Application)
+ assert.Equal(t, session.ApplicationID, retrieved.ApplicationID)
assert.Len(t, retrieved.Participants, 2)
})
@@ -291,9 +291,9 @@ func TestPostgres_AppSessionOperations(t *testing.T) {
participant := "0x7534567890123456789012345678901234567890"
session := app.AppSessionV1{
- SessionID: "0x7634567890123456789012345678901234567890123456789012345678901234",
- Application: "poker",
- Nonce: 1,
+ SessionID: "0x7634567890123456789012345678901234567890123456789012345678901234",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: participant,
@@ -319,9 +319,9 @@ func TestPostgres_AppSessionOperations(t *testing.T) {
t.Run("Update app session", func(t *testing.T) {
session := app.AppSessionV1{
- SessionID: "0x7734567890123456789012345678901234567890123456789012345678901234",
- Application: "poker",
- Nonce: 1,
+ SessionID: "0x7734567890123456789012345678901234567890123456789012345678901234",
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{
WalletAddress: "0x7834567890123456789012345678901234567890",
@@ -380,9 +380,9 @@ func TestPostgres_AppLedgerOperations(t *testing.T) {
// Create session with participants
session := app.AppSessionV1{
- SessionID: sessionID,
- Application: "poker",
- Nonce: 1,
+ SessionID: sessionID,
+ ApplicationID: "poker",
+ Nonce: 1,
Participants: []app.AppParticipantV1{
{WalletAddress: wallet1, SignatureWeight: 50},
{WalletAddress: wallet2, SignatureWeight: 50},
diff --git a/clearnode/store/database/testing.go b/clearnode/store/database/testing.go
index ffc7cd669..1f54bc26a 100644
--- a/clearnode/store/database/testing.go
+++ b/clearnode/store/database/testing.go
@@ -54,7 +54,7 @@ func setupTestSqlite(t testing.TB) *gorm.DB {
t.Fatalf("Failed to open SQLite database: %v", err)
}
- err = database.AutoMigrate(&AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{})
+ err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &AppSessionV1{}, &AppParticipantV1{}, &BlockchainAction{}, &Channel{}, &ContractEvent{}, &State{}, &Transaction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}, &LifespanMetric{})
if err != nil {
t.Fatalf("Failed to run migrations: %v", err)
}
@@ -99,7 +99,7 @@ func setupTestPostgres(ctx context.Context, t testing.TB) (*gorm.DB, testcontain
t.Fatalf("Failed to open PostgreSQL database: %v", err)
}
- err = database.AutoMigrate(&AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{})
+ err = database.AutoMigrate(&AppV1{}, &AppLedgerEntryV1{}, &Channel{}, &AppSessionV1{}, &ContractEvent{}, &Transaction{}, &BlockchainAction{}, &AppSessionKeyStateV1{}, &AppSessionKeyApplicationV1{}, &AppSessionKeyAppSessionIDV1{}, &ChannelSessionKeyStateV1{}, &ChannelSessionKeyAssetV1{}, &UserBalance{}, &UserStakedV1{}, &ActionLogEntryV1{}, &LifespanMetric{})
if err != nil {
t.Fatalf("Failed to run migrations: %v", err)
}
diff --git a/clearnode/store/database/transaction.go b/clearnode/store/database/transaction.go
index fa74ffb77..dbd1c8daa 100644
--- a/clearnode/store/database/transaction.go
+++ b/clearnode/store/database/transaction.go
@@ -5,7 +5,7 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
)
diff --git a/clearnode/store/database/transaction_test.go b/clearnode/store/database/transaction_test.go
index 1f3c68109..f0a604f93 100644
--- a/clearnode/store/database/transaction_test.go
+++ b/clearnode/store/database/transaction_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/clearnode/store/database/user_staked.go b/clearnode/store/database/user_staked.go
new file mode 100644
index 000000000..01db0fe18
--- /dev/null
+++ b/clearnode/store/database/user_staked.go
@@ -0,0 +1,75 @@
+package database
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/shopspring/decimal"
+ "gorm.io/gorm/clause"
+)
+
+type UserStakedV1 struct {
+ UserWallet string `gorm:"column:user_wallet;primaryKey;not null"`
+ BlockchainID uint64 `gorm:"column:blockchain_id;primaryKey;not null"`
+ Amount decimal.Decimal `gorm:"column:amount;type:varchar(78);not null"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+func (UserStakedV1) TableName() string {
+ return "user_staked_v1"
+}
+
+// UpdateUserStaked upserts the staked amount for a user on a specific blockchain.
+func (s *DBStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error {
+ wallet = strings.ToLower(wallet)
+
+ if wallet == "" {
+ return fmt.Errorf("wallet address must not be empty")
+ }
+ if blockchainID == 0 {
+ return fmt.Errorf("blockchain ID must not be zero")
+ }
+ if amount.IsNegative() {
+ return fmt.Errorf("staked amount must not be negative")
+ }
+
+ now := time.Now()
+
+ record := UserStakedV1{
+ UserWallet: wallet,
+ BlockchainID: blockchainID,
+ Amount: amount,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ err := s.db.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "user_wallet"}, {Name: "blockchain_id"}},
+ DoUpdates: clause.AssignmentColumns([]string{"amount", "updated_at"}),
+ }).Create(&record).Error
+ if err != nil {
+ return fmt.Errorf("failed to update user staked amount: %w", err)
+ }
+
+ return nil
+}
+
+// GetTotalUserStaked returns the total staked amount for a user across all blockchains.
+func (s *DBStore) GetTotalUserStaked(wallet string) (decimal.Decimal, error) {
+ wallet = strings.ToLower(wallet)
+
+ var result struct {
+ Total decimal.Decimal `gorm:"column:total"`
+ }
+ err := s.db.Model(&UserStakedV1{}).
+ Where("user_wallet = ?", wallet).
+ Select("COALESCE(SUM(amount), 0) AS total").
+ Scan(&result).Error
+ if err != nil {
+ return decimal.Zero, fmt.Errorf("failed to get user staked total: %w", err)
+ }
+
+ return result.Total, nil
+}
diff --git a/clearnode/store/database/utils.go b/clearnode/store/database/utils.go
index 696de008b..5f7af224e 100644
--- a/clearnode/store/database/utils.go
+++ b/clearnode/store/database/utils.go
@@ -5,8 +5,8 @@ import (
"strings"
"time"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// databaseChannelToCore converts database.Channel to core.Channel
@@ -38,16 +38,16 @@ func databaseAppSessionToCore(dbSession *AppSessionV1) *app.AppSessionV1 {
}
return &app.AppSessionV1{
- SessionID: dbSession.ID,
- Application: dbSession.Application,
- Participants: participants,
- Quorum: dbSession.Quorum,
- Nonce: dbSession.Nonce,
- Status: dbSession.Status,
- Version: dbSession.Version,
- SessionData: dbSession.SessionData,
- CreatedAt: dbSession.CreatedAt,
- UpdatedAt: dbSession.UpdatedAt,
+ SessionID: dbSession.ID,
+ ApplicationID: dbSession.ApplicationID,
+ Participants: participants,
+ Quorum: dbSession.Quorum,
+ Nonce: dbSession.Nonce,
+ Status: dbSession.Status,
+ Version: dbSession.Version,
+ SessionData: dbSession.SessionData,
+ CreatedAt: dbSession.CreatedAt,
+ UpdatedAt: dbSession.UpdatedAt,
}
}
diff --git a/clearnode/store/memory/blockchain_config.go b/clearnode/store/memory/blockchain_config.go
index 361907249..27e89f688 100644
--- a/clearnode/store/memory/blockchain_config.go
+++ b/clearnode/store/memory/blockchain_config.go
@@ -6,6 +6,7 @@ import (
"path/filepath"
"regexp"
+ "github.com/layer-3/nitrolite/pkg/core"
"gopkg.in/yaml.v3"
)
@@ -23,12 +24,7 @@ var (
// It contains default contract addresses that apply to all blockchains unless overridden,
// and a list of individual blockchain configurations.
type BlockchainsConfig struct {
- DefaultContractAddresses DefaultContractAddresses `yaml:"default_contract_addresses"`
- Blockchains []BlockchainConfig `yaml:"blockchains"`
-}
-
-type DefaultContractAddresses struct {
- ChannelHub string `yaml:"channel_hub,omitempty"`
+ Blockchains []BlockchainConfig `yaml:"blockchains"`
}
// BlockchainConfig represents configuration for a single blockchain.
@@ -45,6 +41,10 @@ type BlockchainConfig struct {
BlockStep uint64 `yaml:"block_step"`
// ChannelHubAddress is the address of the ChannelHub contract on this blockchain
ChannelHubAddress string `yaml:"channel_hub_address"`
+ // ChannelHubSigValidators maps validator IDs to the addresses of signature validators for the ChannelHub contract on this blockchain
+ ChannelHubSigValidators map[uint8]string `yaml:"channel_hub_sig_validators"`
+ // LockingContractAddress is the address of the locking contract on this blockchain
+ LockingContractAddress string `yaml:"locking_contract_address"`
}
// LoadEnabledBlockchains loads and validates blockchain configurations from a YAML file.
@@ -77,10 +77,6 @@ func LoadEnabledBlockchains(configDirPath string) (map[uint64]BlockchainConfig,
}
func verifyBlockchainsConfig(cfg *BlockchainsConfig) error {
- if addr := cfg.DefaultContractAddresses.ChannelHub; !contractAddressRegex.MatchString(addr) && addr != "" {
- return fmt.Errorf("invalid default channel hub address '%s'", addr)
- }
-
for i, bc := range cfg.Blockchains {
if bc.Disabled {
continue
@@ -90,19 +86,33 @@ func verifyBlockchainsConfig(cfg *BlockchainsConfig) error {
return fmt.Errorf("invalid blockchain name '%s', should match snake_case format", bc.Name)
}
- if bc.ChannelHubAddress == "" {
- if cfg.DefaultContractAddresses.ChannelHub == "" {
- return fmt.Errorf("missing default and blockchain-specific channel hub address for blockchain '%s'", bc.Name)
- } else {
- cfg.Blockchains[i].ChannelHubAddress = cfg.DefaultContractAddresses.ChannelHub
- }
- } else if !contractAddressRegex.MatchString(bc.ChannelHubAddress) {
+ if bc.ChannelHubAddress == "" && bc.LockingContractAddress == "" {
+ return fmt.Errorf("blockchain '%s' must specify at least one of channel_hub_address or locking_contract_address", bc.Name)
+ }
+
+ if bc.ChannelHubAddress != "" && !contractAddressRegex.MatchString(bc.ChannelHubAddress) {
return fmt.Errorf("invalid channel hub address '%s' for blockchain '%s'", bc.ChannelHubAddress, bc.Name)
}
+ if bc.LockingContractAddress != "" && !contractAddressRegex.MatchString(bc.LockingContractAddress) {
+ return fmt.Errorf("invalid locking contract address '%s' for blockchain '%s'", bc.LockingContractAddress, bc.Name)
+ }
+
if bc.BlockStep == 0 {
cfg.Blockchains[i].BlockStep = defaultBlockStep
}
+
+ if bc.ChannelHubAddress != "" && len(core.ChannelSignerTypes) > 1 {
+ for _, channelSignerType := range core.ChannelSignerTypes[1:] {
+ validatorAddress, ok := bc.ChannelHubSigValidators[uint8(channelSignerType)]
+ if !ok {
+ return fmt.Errorf("blockchain '%s' must specify a signature validator address for channel signer type %d", bc.Name, channelSignerType)
+ }
+ if !contractAddressRegex.MatchString(validatorAddress) {
+ return fmt.Errorf("invalid signature validator address '%s' for channel signer type %d on blockchain '%s'", validatorAddress, channelSignerType, bc.Name)
+ }
+ }
+ }
}
return nil
diff --git a/clearnode/store/memory/blockchain_config_test.go b/clearnode/store/memory/blockchain_config_test.go
index 4a67ea2ba..60dcb579b 100644
--- a/clearnode/store/memory/blockchain_config_test.go
+++ b/clearnode/store/memory/blockchain_config_test.go
@@ -17,20 +17,18 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) {
{
name: "valid config",
cfg: BlockchainsConfig{
- DefaultContractAddresses: DefaultContractAddresses{
- ChannelHub: "0x0000000000000000000000000000000000000001",
- },
-
Blockchains: []BlockchainConfig{
{
- ID: 1,
- Name: "ethereum",
- ChannelHubAddress: "0x1111111111111111111111111111111111111111",
- BlockStep: 10,
+ ID: 1,
+ Name: "ethereum",
+ ChannelHubAddress: "0x1111111111111111111111111111111111111111",
+ BlockStep: 10,
+ ChannelHubSigValidators: map[uint8]string{1: "0x3333333333333333333333333333333333333333"},
},
{
- ID: 11155111,
- Name: "ethereum_sepolia",
+ ID: 11155111,
+ Name: "ethereum_sepolia",
+ LockingContractAddress: "0x2222222222222222222222222222222222222222",
},
},
},
@@ -48,7 +46,7 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) {
sepoliaCfg := blockchains[1]
assert.Equal(t, "ethereum_sepolia", sepoliaCfg.Name)
assert.Equal(t, uint64(11155111), sepoliaCfg.ID)
- assert.Equal(t, "0x0000000000000000000000000000000000000001", sepoliaCfg.ChannelHubAddress)
+ assert.Equal(t, "0x2222222222222222222222222222222222222222", sepoliaCfg.LockingContractAddress)
assert.False(t, sepoliaCfg.Disabled)
assert.Equal(t, defaultBlockStep, sepoliaCfg.BlockStep)
},
@@ -80,14 +78,13 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) {
{
name: "disabled blockchain",
cfg: BlockchainsConfig{
- DefaultContractAddresses: DefaultContractAddresses{
- ChannelHub: "0x0000000000000000000000000000000000000001",
- },
Blockchains: []BlockchainConfig{
{
- ID: 1,
- Name: "ethereum",
- Disabled: false,
+ ID: 1,
+ Name: "ethereum",
+ Disabled: false,
+ ChannelHubAddress: "0x1111111111111111111111111111111111111111",
+ ChannelHubSigValidators: map[uint8]string{1: "0x3333333333333333333333333333333333333333"},
},
{
ID: 11155111,
@@ -109,28 +106,6 @@ func TestBlockchainConfig_verifyVariables(t *testing.T) {
assert.Equal(t, uint64(11155111), sepoliaCfg.ID)
},
},
- {
- name: "invalid default channel hub address",
- cfg: BlockchainsConfig{
- DefaultContractAddresses: DefaultContractAddresses{
- ChannelHub: "0x0000s00000000000000000000000000000000001",
- },
- },
- expectedErrorStr: "invalid default channel hub address '0x0000s00000000000000000000000000000000001'",
- },
- {
- name: "missing channel hub address",
- cfg: BlockchainsConfig{
- Blockchains: []BlockchainConfig{
- {
- ID: 1,
- Name: "ethereum",
- ChannelHubAddress: "",
- },
- },
- },
- expectedErrorStr: "missing default and blockchain-specific channel hub address for blockchain 'ethereum'",
- },
{
name: "invalid channel hub address",
cfg: BlockchainsConfig{
diff --git a/clearnode/store/memory/interface.go b/clearnode/store/memory/interface.go
index be05d9f67..5f0aef35d 100644
--- a/clearnode/store/memory/interface.go
+++ b/clearnode/store/memory/interface.go
@@ -1,7 +1,7 @@
package memory
import (
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
)
// MemoryStore defines an in-memory data store interface for retrieving
@@ -14,6 +14,9 @@ type MemoryStore interface {
// If blockchainID is provided, filters assets to only include tokens on that blockchain.
GetAssets(blockchainID *uint64) ([]core.Asset, error)
+ // GetChannelSigValidators retrieves the channel signature validators for a specific blockchain.
+ GetChannelSigValidators(blockchainID uint64) (map[uint8]string, error)
+
// GetTokenAddress retrieves the token address for a given asset on a specific blockchain.
GetTokenAddress(asset string, blockchainID uint64) (string, error)
diff --git a/clearnode/store/memory/memory_store.go b/clearnode/store/memory/memory_store.go
index d4e27c9a1..18b572e86 100644
--- a/clearnode/store/memory/memory_store.go
+++ b/clearnode/store/memory/memory_store.go
@@ -5,31 +5,38 @@ import (
"slices"
"strings"
- "github.com/erc7824/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/core"
)
type MemoryStoreV1 struct {
- blockchains []core.Blockchain
- assets []core.Asset
- supportedAssets map[string]map[uint64]string // map[asset]map[blockchain_id]string
- tokenDecimals map[uint64]map[string]uint8 // map[blockchain_id]map[token_address]decimals
- assetDecimals map[string]uint8 // map[asset]decimals
+ blockchains []core.Blockchain
+ assets []core.Asset
+ channelSigValidators map[uint64]map[uint8]string // map[blockchain_id]map[validator_id]validator_address
+ supportedAssets map[string]map[uint64]string // map[asset]map[blockchain_id]string
+ tokenDecimals map[uint64]map[string]uint8 // map[blockchain_id]map[token_address]decimals
+ assetDecimals map[string]uint8 // map[asset]decimals
}
func NewMemoryStoreV1(assetsConfig AssetsConfig, blockchainsConfig map[uint64]BlockchainConfig) (MemoryStore, error) {
supportedBlockchainIDs := make(map[uint64]struct{})
blockchains := make([]core.Blockchain, 0, len(blockchainsConfig))
+ channelSigValidators := make(map[uint64]map[uint8]string)
for _, bc := range blockchainsConfig {
if bc.Disabled {
continue
}
- supportedBlockchainIDs[bc.ID] = struct{}{}
+
+ if bc.ChannelHubAddress != "" {
+ supportedBlockchainIDs[bc.ID] = struct{}{}
+ channelSigValidators[bc.ID] = bc.ChannelHubSigValidators
+ }
blockchains = append(blockchains, core.Blockchain{
- ID: bc.ID,
- Name: bc.Name,
- ChannelHubAddress: bc.ChannelHubAddress,
- BlockStep: bc.BlockStep,
+ ID: bc.ID,
+ Name: bc.Name,
+ ChannelHubAddress: bc.ChannelHubAddress,
+ LockingContractAddress: bc.LockingContractAddress,
+ BlockStep: bc.BlockStep,
})
}
slices.SortFunc(blockchains, func(a, b core.Blockchain) int {
@@ -120,11 +127,12 @@ func NewMemoryStoreV1(assetsConfig AssetsConfig, blockchainsConfig map[uint64]Bl
})
return &MemoryStoreV1{
- blockchains: blockchains,
- assets: assets,
- supportedAssets: supportedAssets,
- tokenDecimals: tokenDecimals,
- assetDecimals: assetDecimals,
+ blockchains: blockchains,
+ assets: assets,
+ channelSigValidators: channelSigValidators,
+ supportedAssets: supportedAssets,
+ tokenDecimals: tokenDecimals,
+ assetDecimals: assetDecimals,
}, nil
}
@@ -171,6 +179,14 @@ func (ms *MemoryStoreV1) GetAssets(blockchainID *uint64) ([]core.Asset, error) {
return filteredAssets, nil
}
+func (ms *MemoryStoreV1) GetChannelSigValidators(blockchainID uint64) (map[uint8]string, error) {
+ channelSigValidators, ok := ms.channelSigValidators[blockchainID]
+ if !ok {
+ return nil, fmt.Errorf("blockchain with ID '%d' is not supported", blockchainID)
+ }
+ return channelSigValidators, nil
+}
+
func (ms *MemoryStoreV1) GetTokenAddress(asset string, blockchainID uint64) (string, error) {
tokensOnchain, ok := ms.supportedAssets[asset]
if !ok {
diff --git a/clearnode/stress/app_session.go b/clearnode/stress/app_session.go
index f622e6f87..085515445 100644
--- a/clearnode/stress/app_session.go
+++ b/clearnode/stress/app_session.go
@@ -13,8 +13,8 @@ import (
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/app"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/pkg/app"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
type pipe struct {
@@ -72,10 +72,10 @@ func preGenerateSigs(p *pipe, numParticipants, numOperates int, nonce uint64, as
}
definition := app.AppDefinitionV1{
- Application: "stress-test",
- Participants: participants,
- Quorum: uint8(numParticipants),
- Nonce: nonce,
+ ApplicationID: "stress-test",
+ Participants: participants,
+ Quorum: uint8(numParticipants),
+ Nonce: nonce,
}
sessionID, err := app.GenerateAppSessionIDV1(definition)
diff --git a/clearnode/stress/config.go b/clearnode/stress/config.go
index 6a1c64833..d9241baee 100644
--- a/clearnode/stress/config.go
+++ b/clearnode/stress/config.go
@@ -10,7 +10,7 @@ import (
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ilyakaznacheev/cleanenv"
- "github.com/erc7824/nitrolite/pkg/sign"
+ "github.com/layer-3/nitrolite/pkg/sign"
)
// Config holds all stress test settings, read from environment variables.
diff --git a/clearnode/stress/methods.go b/clearnode/stress/methods.go
index 11ea73f25..6aca3e709 100644
--- a/clearnode/stress/methods.go
+++ b/clearnode/stress/methods.go
@@ -5,8 +5,8 @@ import (
"fmt"
"strconv"
- "github.com/erc7824/nitrolite/pkg/core"
- sdk "github.com/erc7824/nitrolite/sdk/go"
+ "github.com/layer-3/nitrolite/pkg/core"
+ sdk "github.com/layer-3/nitrolite/sdk/go"
)
// MethodRegistry returns all available stress test methods.
diff --git a/clearnode/stress/pool.go b/clearnode/stress/pool.go
index cd70071d9..a84050f39 100644
--- a/clearnode/stress/pool.go
+++ b/clearnode/stress/pool.go
@@ -4,9 +4,9 @@ import (
"fmt"
"time"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/sign"
- sdk "github.com/erc7824/nitrolite/sdk/go"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/sign"
+ sdk "github.com/layer-3/nitrolite/sdk/go"
)
// CreateClientPool opens up to n WebSocket connections to the clearnode.
diff --git a/clearnode/stress/runner.go b/clearnode/stress/runner.go
index 457900b8d..7bc26ed1b 100644
--- a/clearnode/stress/runner.go
+++ b/clearnode/stress/runner.go
@@ -7,7 +7,7 @@ import (
"sync/atomic"
"time"
- sdk "github.com/erc7824/nitrolite/sdk/go"
+ sdk "github.com/layer-3/nitrolite/sdk/go"
)
// RunTest executes totalReqs calls of fn distributed across the client pool.
diff --git a/clearnode/stress/transfer.go b/clearnode/stress/transfer.go
index 97df7ae57..d75467d07 100644
--- a/clearnode/stress/transfer.go
+++ b/clearnode/stress/transfer.go
@@ -13,9 +13,9 @@ import (
"github.com/shopspring/decimal"
- "github.com/erc7824/nitrolite/pkg/core"
- "github.com/erc7824/nitrolite/pkg/sign"
- sdk "github.com/erc7824/nitrolite/sdk/go"
+ "github.com/layer-3/nitrolite/pkg/core"
+ "github.com/layer-3/nitrolite/pkg/sign"
+ sdk "github.com/layer-3/nitrolite/sdk/go"
)
type wallet struct {
diff --git a/clearnode/stress/types.go b/clearnode/stress/types.go
index 9891b36e7..912b1e663 100644
--- a/clearnode/stress/types.go
+++ b/clearnode/stress/types.go
@@ -4,7 +4,7 @@ import (
"context"
"time"
- sdk "github.com/erc7824/nitrolite/sdk/go"
+ sdk "github.com/layer-3/nitrolite/sdk/go"
)
// Runner is the unified signature for executing a stress test.
diff --git a/contracts/README.md b/contracts/README.md
index 9265b4558..681c2a8d6 100644
--- a/contracts/README.md
+++ b/contracts/README.md
@@ -4,10 +4,10 @@
Foundry consists of:
-- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
-- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
-- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
-- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
+- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
+- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
+- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
+- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
## Documentation
@@ -64,3 +64,57 @@ $ forge --help
$ anvil --help
$ cast --help
```
+
+## Parametric Tokens
+
+Nitrolite supports tokens with additional parameters (e.g., mintTime) through the `IParametricToken` interface. These tokens maintain separate balances per sub-account to preserve parameter integrity. `ParametricToken` contract provides implementation of a token with both mutable and immutable parameters.
+
+### How It Works
+
+When a token is marked parametric, the ChannelHub contract:
+
+1. Converts its own account to Super account on the token
+2. Creates a new sub-account for each channel at channel creation time
+3. Stores the sub-account ID in channel metadata
+
+All subsequent deposits, withdrawals, and transfers for that channel automatically use the correct sub-account.
+
+### Important: Channel Must Exist First
+
+For parametric tokens, funds **cannot be deposited before channel creation**. The workflow is:
+
+1. **Create channel** → ChannelHub creates a sub-account and returns channel ID
+2. **Deposit** → Funds go to the channel's sub-account
+3. **Transfer/Withdraw** → Funds move from/to the sub-account
+
+Depositing a parametric token without an existing channel leads to token lock and requires reclaim.
+
+### Enabling Parametric Token Support
+
+The vault contract owner must perform two steps:
+
+```solidity
+// Step 1: Mark token as parametric
+channelHub.setParametricToken(tokenAddress, true);
+
+// Step 2: Convert ChannelHub to Super account on the token
+IParametricToken(tokenAddress).convertToSuper(address(channelHub));
+```
+
+After this, channel creation and deposits work through the standard NitroliteClient API - no additional user action required.
+
+### Low-Level Access
+
+For advanced use cases, the `IParametricToken` interface exposes direct sub-account operations:
+
+- `transferToSub()` - Transfer from normal account to a vault sub-account
+
+- `transferFromSub()` - Transfer from a vault sub-account to normal account
+
+- `transferBetweenSubs()` - Transfer between sub-accounts of the same super account (including vault)
+
+These are intended for custom integrations and use `subId` for sub-account identification; standard channel operations handle sub-accounts automatically.
+
+### Standard ERC20 Tokens
+
+For non-parametric tokens (USDC, ETH, etc.), the `isParametricToken` flag is disabled by default and no sub-accounts are created.
diff --git a/contracts/broadcast/DeployChannelHub.s.sol/11155111/run-1772892720931.json b/contracts/broadcast/DeployChannelHub.s.sol/11155111/run-1772892720931.json
new file mode 100644
index 000000000..5af57197a
--- /dev/null
+++ b/contracts/broadcast/DeployChannelHub.s.sol/11155111/run-1772892720931.json
@@ -0,0 +1,188 @@
+{
+ "transactions": [
+ {
+ "hash": "0x3df2187dc8a50ef62abfeb377318888493042770315492070c4708584dfbf572",
+ "transactionType": "CREATE2",
+ "contractName": "ChannelEngine.channelhub",
+ "contractAddress": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x1bb70c",
+ "input": "0x0000000000000000000000000000000000000000000000000000000000000000608080604052346019576116f0908161001e823930815050f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63a8b4483c14610025575f80fd5b604060031936011261122d5760043567ffffffffffffffff811161122d5760a0600319823603011261122d5760a0820182811067ffffffffffffffff821117611299576040528060040135600681101561122d578252602481013567ffffffffffffffff811161122d5761009f90600436918401016113e1565b602083019081526040830192604483013584526100c96084606083019460648101358652016112ec565b6080820190815260243567ffffffffffffffff811161122d576100f09036906004016113e1565b6100f8611498565b50606081019367ffffffffffffffff855151164603610dc45767ffffffffffffffff82511681519067ffffffffffffffff82511610908115611261575b5015610a085784516040810190601260ff83511611611239574667ffffffffffffffff825116146110ff575b505060208201928351600a8110156103585760041480156110eb575b80156110d7575b80156110c3575b80156110af575b801561109b575b1561105e576080830167ffffffffffffffff815151161561103657515167ffffffffffffffff16461461100e575b6101dc865160a06060820151910151906114e6565b6101f1875160c06080820151910151906114f3565b5f8112610fe65761020190611526565b03610fbe578451600681101561035857600214610f7f575b50610222611498565b5061023c608086510151608060608451015101519061150e565b9061025660c08751015160c060608451015101519061150e565b9351600a81101561035857600281036104b65750509050610275611498565b928051600681101561035857159081156104a0575b811561048a575b8115610475575b501561044d575f8113156104255782526020820152600160408201525f6060820152925b6102d96102d1608086019260018452516115a7565b8551906114f3565b926102ea60208601948551906114f3565b5f81126103fd5760a08601938451156103ab575b50508351905f821361036c575b50506040519284518452516020840152604084015193600685101561035857606067ffffffffffffffff9160c09660408701520151166060840152511515608083015251151560a0820152f35b634e487b7160e01b5f52602160045260245ffd5b610377905191611526565b11610383575f8061030b565b7f2e3b1ec0000000000000000000000000000000000000000000000000000000005f5260045ffd5b6103c36103c9915160a06060820151910151906114e6565b91611526565b036103d5575f806102fe565b7f8f9003ee000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fae0bb491000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610298565b8091505160068110156103585760021490610291565b809150516006811015610358576001149061028a565b6003810361055657505090506104ca611498565b92805160068110156103585715908115610540575b811561052a575b8115610515575b501561044d575f8112156104255782526020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f6104ed565b80915051600681101561035857600214906104e6565b80915051600681101561035857600114906104df565b8061061f5750509050610567611498565b92805160068110156103585715908115610609575b81156105f3575b81156105de575b501561044d576104255760a0835101516105b6576020820152600160408201525f6060820152926102bc565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61058a565b8091505160068110156103585760021490610583565b809150516006811015610358576001149061057c565b600181036107185750509050610633611498565b928051600681101561035857600114908115610702575b81156106ed575b501561044d5761066c845160a06060820151910151906114e6565b8651106106c55761068a82610685836106858a516115a7565b6114f3565b5f81126103fd5761069f60a0865101516115a7565b136103fd5782526020820152600360408201525f6060820152600160a0820152926102bc565b7f7fa0800f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610651565b809150516006811015610358576002149061064a565b6004810361083857505061072a611498565b938051600681101561035857600114908115610822575b811561080d575b501561044d57610425576080016060815101519081156107e55761077a855160ff604060a0830151920151169061161e565b61078c60ff604084510151168461161e565b036105b65760806107a091510151916115a7565b036107bd576020820152600160408201525f6060820152926102bc565b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610748565b8091505160068110156103585760021490610741565b909391929060058103610a83575061084e611498565b948051600681101561035857600114908115610a6d575b8115610a58575b501561044d5761087f60208551016114d9565b600a81101561035857600403610a305767ffffffffffffffff81511667ffffffffffffffff6108b1818751511661155b565b1603610a0857608001916060835101516107e55760a0835101516105b65760a0865101516105b657610425576109e05760606080835101510151906080815101516108fb836115a7565b036107bd575160c00151610916610911836115a7565b61157b565b036109b8576060845101519060608084510151015182039182116109a45760ff6040608061094e61095b9584848b510151169061161e565b955101510151169061161e565b0361097c575f81525f6020820152600160408201525f6060820152926102bc565b7f733d14c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fd916ea0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f7dcd8ffd000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61086c565b8091505160068110156103585760021490610865565b9193909160068103610b3f57505090610a9a611498565b938051600681101561035857600114908115610b29575b8115610b14575b501561044d576104255760a0845101516105b6576080016080815101516107bd576060815101516107e55760c0610af360a0835101516115a7565b91510151036109b8576020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f610ab8565b8091505160068110156103585760021490610ab1565b60078103610bd957505090610b52611498565b938051600681101561035857600114908115610bc3575b8115610bae575b501561044d576104255760a0845101516105b6576080016060815101516107e55760a0815101516105b657516107a060c0608083015192015161157b565b9050516006811015610358576004145f610b70565b8091505160068110156103585760021490610b69565b60088103610e1557505090610bec611498565b938051600681101561035857158015610e01575b15610ce5575050608001805160600151915081156107e55760a0815101516105b6576060845101516107e557610c44845160ff604060a0830151920151169061161e565b610c5660ff604084510151168461161e565b03610cbd57610c8d9060ff6040610c82610c7c8851848460c0830151920151169061165b565b956115a7565b92510151169061165b565b036109b8576080825101516107bd57610caa60a0835101516115a7565b6020820152600460408201525b926102bc565b7f7b208b9d000000000000000000000000000000000000000000000000000000005f5260045ffd5b8051600681101561035857600114908115610dec575b501561044d574667ffffffffffffffff8651511603610dc457610425576060845101519081156107e55760a0855101516105b657608001906060825101516107e557610d55825160ff604060a0830151920151169061161e565b610d6760ff604088510151168361161e565b03610cbd57610d9f610d90610d8a845160ff604060c0830151920151169061165b565b926115a7565b60ff604088510151169061165b565b036109b85751608001516107bd576020820152600160408201525f6060820152610cb7565b7f67525583000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576002145f610cfb565b508051600681101561035857600514610c00565b600903610f5757610e24611498565b948051600681101561035857600403610ed957504667ffffffffffffffff8751511603610dc457610e5860208251016114d9565b600a81101561035857600803610a305767ffffffffffffffff82511667ffffffffffffffff610e8a818451511661155b565b1603610a0857606080915101510151606086510151036107e55760a0855101516105b6576080016060815101516107e5575160a001516105b657610425576109e05760016040820152926102bc565b919250508051600681101561035857600114908115610f42575b501561044d576060845101516107e55760a0845101516105b657608001606081510151156107e5575160a001516105b6576020820152600560408201525f6060820152600160a0820152610cb7565b9050516006811015610358576002145f610ef3565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b5167ffffffffffffffff164211610f96575f610219565b7ff06506c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff019de0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f114a9df4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f26c21ae4000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff60808401515116156101c7577f4c7b586e000000000000000000000000000000000000000000000000000000005f5260045ffd5b508351600a81101561035857600914610199565b508351600a81101561035857600814610192565b508351600a8110156103585760071461018b565b508351600a81101561035857600614610184565b508351600a8110156103585760051461017d565b6020015173ffffffffffffffffffffffffffffffffffffffff168061115b575060ff601291511603611133575b5f80610161565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f92816111f7575b506111c2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461112c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011611231575b81611213602093836112c9565b8101031261122d575160ff8116810361122d57915f611195565b5f80fd5b3d9150611206565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b60608101515167ffffffffffffffff1615915081611281575b505f610135565b67ffffffffffffffff9150608001515116155f61127a565b634e487b7160e01b5f52604160045260245ffd5b60e0810190811067ffffffffffffffff82111761129957604052565b90601f601f19910116810190811067ffffffffffffffff82111761129957604052565b359067ffffffffffffffff8216820361122d57565b91908260e091031261122d57604051611319816112ad565b8092611324816112ec565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361122d576020830152604081013560ff8116810361122d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561122d5780359067ffffffffffffffff821161129957604051926113c06020601f19601f86011601856112c9565b8284526020838301011161122d57815f926020809301838601378301015290565b91906102608382031261122d57604051906113fb826112ad565b8193611406816112ec565b83526020810135600a81101561122d576020840152604081013560408401526114328260608301611301565b6060840152611445826101408301611301565b608084015261022081013567ffffffffffffffff811161122d578261146b91830161138b565b60a08401526102408101359167ffffffffffffffff831161122d5760c092611493920161138b565b910152565b6040519060c0820182811067ffffffffffffffff821117611299576040525f60a0838281528260208201528260408201528260608201528260808201520152565b51600a8110156103585790565b919082018092116109a457565b9190915f83820193841291129080158216911516176109a457565b81810392915f1380158285131691841216176109a457565b5f81126115305790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b67ffffffffffffffff60019116019067ffffffffffffffff82116109a457565b7f800000000000000000000000000000000000000000000000000000000000000081146109a4575f0390565b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81116115d15790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116109a457565b60ff16604d81116109a457600a0a90565b9060ff811660128111611239576012146116575761163e611643916115fc565b61160d565b908181029181830414901517156109a45790565b5090565b9060ff811660128111611239576012146116575761163e61167b916115fc565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166109a45781830514901517156109a4579056fea264697066735822122036f6b0f3261f4d84fa391cd2e29d848110238f6d49d373a5912f2304cae9c86d64736f6c634300081e0033",
+ "nonce": "0x28",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x9712dcbc9f46d075bb90ab9d5cbbdf30195810bb050150b302cb3aaaf0e71bc0",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowWithdrawalEngine.channelhub",
+ "contractAddress": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x124792",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610edc908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8062ea54e714610118576324063eba1461002e575f80fd5b60206003193601126101145760043567ffffffffffffffff81116101145761005a903690600401610c2a565b610062610ced565b90516004811015610100575f19016100d857600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff82116100c45767ffffffffffffffff6100c0921660608201525f608082015260405191829182610ca2565b0390f35b634e487b7160e01b5f52601160045260245ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b5f80fd5b60406003193601126101145760043567ffffffffffffffff811161011457610144903690600401610c2a565b60243567ffffffffffffffff811161011457610164903690600401610b73565b61016c610ced565b5081516004811015610100576003146109dc5767ffffffffffffffff461660608201908067ffffffffffffffff83515116146109b457608083019067ffffffffffffffff825151160361098c5767ffffffffffffffff835116156107a25780516040810190601260ff83511611610964574667ffffffffffffffff8251161461082e575b5050805160a0606082015191015181018091116100c45761021c825160c0608082015191015190610d17565b5f81126108065761022c90610d5e565b036107de57610239610ced565b5060208301928351600a811015610100576006810361052657505061025c610ced565b9184516004811015610100576104fe576060825101516104d6576080825101516104ae5781519160c060a084015193015161029684610d93565b03610486576102c360ff60406102b88551838360608301519201511690610e0a565b935101511684610e0a565b1161045e575160a00151610436576102da90610d93565b60208201526001604082015260016080820152915b825115801590610429575b15610401578251906103126020850192835190610d17565b928051600a81101561010057600603610366575050510361033e576100c0905b60405191829182610ca2565b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092915051600a81101561010057600714610387575b50506100c090610332565b8251036103d95760406103a261039d8451610d32565b610d5e565b910151036103b157818061037c565b7fd9132288000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b50602083015115156102fa565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f06b4cdae000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b90929060070361077a57610538610ced565b92855160048110156101005760011480156107ca575b156100d85767ffffffffffffffff9051166020860190600167ffffffffffffffff835151160167ffffffffffffffff81116100c45767ffffffffffffffff16036107a257602081510151600a811015610100577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0161077a5760a06080825101510151926060815101516104d6576080815101516105f36105ee86610d93565b610d32565b036104ae5760a081510151610436575160c0015161061084610d93565b036107025760608251015160608083510151015111156107525760608082510151015160608351015181039081116100c4576106559060ff6040855101511690610e0a565b61066b60ff604060808551015101511685610e0a565b0361072a5760c08251015160c06060835101510151905f82820392128183128116918313901516176100c4575f81121561070257604060806106cb6106c56106d89660ff856106ba8298610d32565b925101511690610e47565b96610d93565b9351015101511690610e47565b03610486576106ed6105ee6040850151610d93565b8152600360408201525f6080820152916102ef565b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fffda345d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f25e3e1b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156101005760021461054e565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061088a575060ff601291511603610862575b84806101f0565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610926575b506108f1577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461085b577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d60201161095c575b8161094260209383610a50565b81010312610114575160ff811681036101145791876108c4565b3d9150610935565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff821117610a2057604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117610a2057604052565b90601f601f19910116810190811067ffffffffffffffff821117610a2057604052565b359067ffffffffffffffff8216820361011457565b359073ffffffffffffffffffffffffffffffffffffffff8216820361011457565b91908260e091031261011457604051610ac181610a04565b8092610acc81610a73565b8252610ada60208201610a88565b6020830152604081013560ff811681036101145760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f820112156101145780359067ffffffffffffffff8211610a205760405192610b526020601f19601f8601160185610a50565b8284526020838301011161011457815f926020809301838601378301015290565b9190610260838203126101145760405190610b8d82610a04565b8193610b9881610a73565b83526020810135600a81101561011457602084015260408101356040840152610bc48260608301610aa9565b6060840152610bd7826101408301610aa9565b608084015261022081013567ffffffffffffffff81116101145782610bfd918301610b1d565b60a08401526102408101359167ffffffffffffffff83116101145760c092610c259201610b1d565b910152565b91909160a0818403126101145760405190610c4482610a34565b81938135600481101561011457835260208201359067ffffffffffffffff82116101145782610c7c60809492610c2594869401610b73565b602086015260408101356040860152610c9760608201610a73565b606086015201610a88565b91909160a0810192805182526020810151602083015260408101516004811015610100576080918291604085015267ffffffffffffffff606082015116606085015201511515910152565b60405190610cfa82610a34565b5f6080838281528260208201528260408201528260608201520152565b9190915f83820193841291129080158216911516176100c457565b7f800000000000000000000000000000000000000000000000000000000000000081146100c4575f0390565b5f8112610d685790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610dbd5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116100c457565b60ff16604d81116100c457600a0a90565b9060ff81166012811161096457601214610e4357610e2a610e2f91610de8565b610df9565b908181029181830414901517156100c45790565b5090565b9060ff81166012811161096457601214610e4357610e2a610e6791610de8565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166100c45781830514901517156100c4579056fea264697066735822122073585d1c2949228993d38506ffc5f542f9ffb1c023c1893a2f5522e50227b27564736f6c634300081e0033",
+ "nonce": "0x29",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x6e81a9f20bb7b3370a15b6402271a9f8e7eae63184e733c80273c497a4187983",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowDepositEngine.channelhub",
+ "contractAddress": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x11ad7a",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610e55908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80636666e4c0146109095763bbc42f341461002f575f80fd5b604060031936011261085d5760043567ffffffffffffffff811161085d5761005b903690600401610bf4565b60243567ffffffffffffffff811161085d5761007b903690600401610b3d565b610083610cd9565b5081516004811015610343576003146108e15767ffffffffffffffff46169060608101918067ffffffffffffffff84515116146108b957608082019067ffffffffffffffff82515116036108915767ffffffffffffffff8251161561067b5780516040810190601260ff83511611610869574667ffffffffffffffff8251161461072f575b5050805160a06060820151910151810180911161038c57610134825160c0608082015191015190610d09565b5f81126107075761014490610d50565b036106df57610151610cd9565b5060208201928351600a81101561034357600481036104685750909150610176610cd9565b918451600481101561034357610440578051916080606084015193015161019c84610d85565b036104185760a0825101516103f05760c0825101516103c85760ff60406101d26101dd9351838360a08301519201511690610dda565b935101511683610dda565b036103a0576101eb90610d85565b815260016040820152612a3067ffffffffffffffff42160167ffffffffffffffff811161038c5767ffffffffffffffff166060820152600160a0820152915b82511580159061037f575b1561035757825161024c6020850191825190610d09565b928051600a811015610343576004036102a65750505081510361027e5761027a905b60405191829182610c7a565b0390f35b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9290919251600a811015610343576005146102c8575b50505061027a9061026e565b81510361031b576102e36102de60409251610d24565b610d50565b910151036102f3575f80806102bc565b7fb09443e7000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b5060208301511515610235565b634e487b7160e01b5f52601160045260245ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f76ac27ca000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b60050361065357610477610cd9565b92855160048110156103435760011480156106cb575b156106a35767ffffffffffffffff905116916020860192600167ffffffffffffffff855151160167ffffffffffffffff811161038c5767ffffffffffffffff160361067b57602083510151600a811015610343576003190161065357606060808451015101519060808151015161050383610d85565b036104185760c08151015161051f61051a84610d85565b610d24565b036103c85760608151015161062b575160a001516103f057606082510151606080855101510151810390811161038c576105656105799160ff6040865101511690610dda565b9160ff604060808751015101511690610dda565b036106035760a0815101516103f057606060808092510151925101510151908181035f831282808312821692139015161761038c57036105db576105c361051a6040850151610d85565b6020820152600360408201525f60a08201529161022a565b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fff0edb30000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156103435760021461048d565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061078b575060ff601291511603610763575b5f80610108565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610827575b506107f2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461075c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011610861575b8161084360209383610a25565b8101031261085d575160ff8116810361085d57915f6107c5565b5f80fd5b3d9150610836565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b602060031936011261085d5760043567ffffffffffffffff811161085d57610935903690600401610bf4565b61093d610cd9565b9080516004811015610343575f19016106a3576060015167ffffffffffffffff164210156109b157600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff821161038c5767ffffffffffffffff61027a921660808201525f60a082015260405191829182610c7a565b7f2b39d042000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff8211176109f557604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176109f557604052565b90601f601f19910116810190811067ffffffffffffffff8211176109f557604052565b359067ffffffffffffffff8216820361085d57565b91908260e091031261085d57604051610a75816109d9565b8092610a8081610a48565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361085d576020830152604081013560ff8116810361085d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561085d5780359067ffffffffffffffff82116109f55760405192610b1c6020601f19601f8601160185610a25565b8284526020838301011161085d57815f926020809301838601378301015290565b91906102608382031261085d5760405190610b57826109d9565b8193610b6281610a48565b83526020810135600a81101561085d57602084015260408101356040840152610b8e8260608301610a5d565b6060840152610ba1826101408301610a5d565b608084015261022081013567ffffffffffffffff811161085d5782610bc7918301610ae7565b60a08401526102408101359167ffffffffffffffff831161085d5760c092610bef9201610ae7565b910152565b91909160c08184031261085d5760405190610c0e82610a09565b81938135600481101561085d57835260208201359167ffffffffffffffff831161085d57610c4260a0939284938301610b3d565b602085015260408101356040850152610c5d60608201610a48565b6060850152610c6e60808201610a48565b60808501520135910152565b91909160c08101928051825260208101516020830152604081015160048110156103435760a0918291604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff608082015116608085015201511515910152565b60405190610ce682610a09565b5f60a0838281528260208201528260408201528260608201528260808201520152565b9190915f838201938412911290801582169115161761038c57565b7f8000000000000000000000000000000000000000000000000000000000000000811461038c575f0390565b5f8112610d5a5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610daf5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b9060ff16601281116108695760128114610e1b5760120360ff811161038c5760ff16604d811161038c57600a0a9081810291818304149015171561038c5790565b509056fea2646970667358221220fc0a93f7abd0c8aae0f4edd1fab1eef03232af831542ee9ea9f3dcf8d76c3da064736f6c634300081e0033",
+ "nonce": "0x2a",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x5e58e1f709d9ded21112c24523733b843486bf6ae775ffd10d86118a5c947cfe",
+ "transactionType": "CREATE",
+ "contractName": "ECDSAValidator.channelhub",
+ "contractAddress": "0x735eb1026afba78b602da39c6b59eaba95753686",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x880d5",
+ "value": "0x0",
+ "input": "0x608080604052346015576106d6908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63600109bb14610024575f80fd5b346100cc5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100cc5760243567ffffffffffffffff81116100cc576100739036906004016100d0565b9060443567ffffffffffffffff81116100cc576100949036906004016100d0565b6064359173ffffffffffffffffffffffffffffffffffffffff831683036100cc576020946100c4946004356101a0565b604051908152f35b5f80fd5b9181601f840112156100cc5782359167ffffffffffffffff83116100cc57602083818601950101116100cc57565b90601f601f19910116810190811067ffffffffffffffff82111761012157604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161012157601f01601f191660200190565b9291926101768261014e565b9161018460405193846100fe565b8294818452818301116100cc578281602093845f960137010152565b929091949383156102635773ffffffffffffffffffffffffffffffffffffffff85161561023b5761022060806101de6102279561022d99369161016a565b95601f19601f6020604051998a94828601526040808601528051918291826060880152018686015e5f858286010152011681010301601f1981018652856100fe565b369161016a565b9061028b565b1561023757600190565b5f90565b7f4501a919000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe1b97cf8000000000000000000000000000000000000000000000000000000005f5260045ffd5b91825192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156104cc575b806d04ee2d6d415b85acef8100000000600a9210156104b1575b662386f26fc1000081101561049d575b6305f5e10081101561048c575b61271081101561047d575b606481101561046f575b1015610465575b6001850190600a602161033461031e8561014e565b9461032c60405196876100fe565b80865261014e565b97601f19602086019901368a378401015b5f1901917f30313233343536373839616263646566000000000000000000000000000000008282061a83530490811561038057600a90610345565b505073ffffffffffffffffffffffffffffffffffffffff5f9361040c86610415946020610404869b603a604051938492818401967f19457468657265756d205369676e6564204d6573736167653a0a00000000000088525180918486015e83018281019d8e528c8051928391019e8f905e01015f815203601f1981018352826100fe565b5190206104f4565b9094919461052e565b169416841461045c5773ffffffffffffffffffffffffffffffffffffffff9261044d92610444925190206104f4565b9092919261052e565b1614610457575f90565b600190565b50505050600190565b9360010193610309565b606460029104960195610302565b612710600491049601956102f8565b6305f5e100600891049601956102ed565b662386f26fc10000601091049601956102e0565b6d04ee2d6d415b85acef8100000000602091049601956102d0565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f01000000000000000081046102b6565b81519190604183036105245761051d9250602082015190606060408401519301515f1a90610606565b9192909190565b50505f9160029190565b60048110156105d95780610540575050565b60018103610570577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b600281036105a457507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6003146105ae5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610695579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa1561068a575f5173ffffffffffffffffffffffffffffffffffffffff81161561068057905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fea2646970667358221220c8e32dfe4c3317faffb02d4b02fddbb5e01dbc789e117442dd5ec08557786de764736f6c634300081e0033",
+ "nonce": "0x2b",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x6e0b716f9bdb40d3aadbfa2544bf5ec11b39f431736bd19569ade187cb0b7396",
+ "transactionType": "CREATE",
+ "contractName": "ChannelHub",
+ "contractAddress": "0xb7be0e2007ddf320d680942cb9e008f986e74f83",
+ "function": null,
+ "arguments": [
+ "0x735EB1026aFbA78B602dA39C6B59EABa95753686"
+ ],
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x6a626e",
+ "value": "0x0",
+ "input": "0x60a0346100aa57601f61608238819003918201601f19168301916001600160401b038311848410176100ae578084926020946040528339810103126100aa57516001600160a01b0381168082036100aa5760017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00551561009b57608052604051615fbf90816100c382396080518181816111420152613f180152f35b63e6c4247b60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806316b390b11461024457806317536c061461023f578063187576d81461023a5780633115f6301461023557806338a66be21461023057806341b660ef1461022b57806347de477a146102265780635326919814610221578063587675e81461021c5780635a0745b4146102175780635b9acbf9146102125780635dc46a741461020d5780636840dbd2146102085780636898234b146102035780636af820bd146101fe57806371a47141146101f9578063735181f0146101f457806382d3e15d146101ef5780638d0b12a5146101ea57806394191051146101e55780639691b468146101e0578063a5c82680146101db578063b00b6fd6146101d6578063b25a1d38146101d1578063beed9d5f146101cc578063c74a2d10146101c7578063d888ccae146101c2578063dc23f29e146101bd578063dd73d494146101b8578063e617208c146101b3578063ecf3d7e8146101ae578063f4ac51f5146101a9578063f766f8d6146101a4578063ff5bc09e1461019f5763ffa1ad741461019a575f80fd5b612650565b612639565b61249a565b61241f565b61230d565b61226e565b6120f0565b611ef5565b611db5565b611b71565b6119d6565b6116c4565b61165b565b6114ba565b611379565b61135c565b6111cb565b6111ae565b611171565b61112d565b611112565b611026565b61100f565b610fc8565b610fa6565b610f8a565b610f44565b610cfc565b610b13565b610850565b6107ea565b61065e565b6105d8565b6104ca565b6102cb565b9181601f840112156102775782359167ffffffffffffffff8311610277576020838186019501011161027757565b5f80fd5b60643590600282101561027757565b90606060031983011261027757600435916024359067ffffffffffffffff8211610277576102ba91600401610249565b909160443560028110156102775790565b34610277576102d93661028a565b6103986102f1859493945f52600260205260405f2090565b9283546102ff81151561266b565b61035a600286019461032a61031b87546001600160a01b031690565b948560038a019a8b5492613eff565b9591600160068b019a019661034a88546001600160a01b039060081c1690565b926103548c6128a0565b8861405d565b60c061036588614161565b604051809581927f6666e4c000000000000000000000000000000000000000000000000000000000835260048301612a25565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577fba075bd445233f7cad862c72f0343b3503aad9c8e704a2295f122b82abf8e80196610436956080955f9461044b575b50836104146104066104279697546001600160a01b039060081c1690565b92546001600160a01b031690565b9254936104208a6128a0565b908c61428d565b015167ffffffffffffffff1690565b9061044660405192839283612a41565b0390a2005b6104279450946104146104786104069760c03d60c011610481575b6104708183612707565b810190612950565b955050946103e8565b503d610466565b612a36565b6001600160a01b0381160361027757565b6003196060910112610277576004356104b68161048d565b906024356104c38161048d565b9060443590565b6001600160a01b036104db3661049e565b92909116906104eb821515612b9e565b6104f6831515612bcd565b815f52600660205261051c8160405f20906001600160a01b03165f5260205260405f2090565b80549184830180931161059a577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7926001600160a01b03925561055d615810565b61056885823361458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00556040519485521692602090a3005b612bfc565b60206040818301928281528451809452019201905f5b8181106105c25750505090565b82518452602093840193909201916001016105b5565b34610277576020600319360112610277576001600160a01b036004356105fd8161048d565b165f52600160205260405f206040519081602082549182815201915f5260205f20905f5b818110610648576106448561063881870382612707565b6040519182918261059f565b0390f35b8254845260209093019260019283019201610621565b3461027757602060031936011261027757600354600480549190355f5b828410806107e1575b156107d4576106b06106a2610698866131a0565b90549060031b1c90565b5f52600260205260405f2090565b6001810160036106c1825460ff1690565b6106ca81611c00565b146107c2576106d882615b4d565b1561077e57915f8261076961077595600561076f96019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b6001600160a01b03165f52600660205260405f2090565b92015460401c6001600160a01b031690565b6001600160a01b03165f5260205260405f2090565b918254612c10565b9055600360ff19825416179055565b556139c6565b936139c6565b915b919261067b565b505092905061078d9150600455565b8061079457005b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1005b5050926107ce906139c6565b91610777565b92905061078d9150600455565b50818110610684565b34610277575f600319360112610277576020604051620186a08152f35b90816102609103126102775790565b90600319820160e081126102775760c0136102775760049160c4359067ffffffffffffffff82116102775761084d91600401610807565b90565b61085936610816565b906020820191600261086a84612c27565b61087381611c0f565b148015610af8575b8015610ada575b61088b90612c31565b61091a6108a061089b3685612c79565b614780565b916108aa8461483e565b60208401906108b882612ced565b956108d760408701976108ca89612ced565b608089013591858961494b565b60c0826108ff6108f86108ec6107148c612ced565b61073d60808501612ced565b54886149c5565b6040519687928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af493841561048857610a156001600160a01b0394610a2d936109967fb00e209e275d0e1892f1982b34d3f545d1628aebd95322d7ce3585c558f638b498610a1b955f91610aab575b50610985368d612c79565b61098f368a61301c565b908c614b52565b6109c2896109bd6109a685612ced565b6001600160a01b03165f52600160205260405f2090565b615bde565b5060026109ce82612c27565b6109d781611c0f565b03610a325750877f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f4177869620669660405180610a0d89826130ca565b0390a2612ced565b97612ced565b918360405194859416981696836130db565b0390a4005b610a3d600391612c27565b610a4681611c0f565b03610a7b57877f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf98660405180610a0d89826130ca565b877f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc60405180610a0d89826130ca565b610acd915060c03d60c011610ad3575b610ac58183612707565b810190612cf7565b5f61097a565b503d610abb565b5061088b610ae784612c27565b610af081611c0f565b159050610882565b506003610b0484612c27565b610b0d81611c0f565b1461087b565b610b1c36610816565b90610b3d6004610b2e60208501612c27565b610b3781611c0f565b14612c31565b610b4a61089b3683612c79565b9160208201610b5881612ced565b90610b7960408501926080610b6c85612ced565b960135958691868961494b565b610b8b610b858461316b565b86614c6c565b93610b9586614c9c565b15610bdd57505050610bd881610bcc7f471c4ebe4e57d25ef7117e141caac31c6b98f067b8098a7a7bbd38f637c2f9809386614cf9565b604051918291826130ca565b0390a3005b610c259060c085610bf18897959697614161565b60405194859283927fbbc42f3400000000000000000000000000000000000000000000000000000000845260048401613175565b038173728904e52308213ba61c90ef49f34c18fbda9e115af48015610488577fede7867afa7cdb9c443667efd8244d98bf9df1dce68e60dc94dca6605125ca7695610bd895610c9a945f93610ca3575b50610c82610c8891612ced565b91612ced565b91610c93368761301c565b8a8a61428d565b610bcc846131ef565b610c88919350610cc4610c829160c03d60c011610481576104708183612707565b939150610c75565b90604060031983011261027757600435916024359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610d0a36610ccc565b610d1b6009610b2e60208401612c27565b610d376001610d31845f525f60205260405f2090565b0161323d565b610dfd610d4e60208301516001600160a01b031690565b9161071460c060408301610d7a610d6c82516001600160a01b031690565b608086015190888a8c61494b565b610de2610ddb610dc4610d8d368b61301c565b9586946101408c018d8d610da08361316b565b67ffffffffffffffff1646149d8e610eb7575b50505050516001600160a01b031690565b6060840151602001516001600160a01b031661073d565b54896149c5565b6040519586928392632a2d120f60e21b8452600484016132c9565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857610e2f935f93610e96575b5086614b52565b15610e65576104467f9a6f675cc94b83b55f1ecc0876affd4332a30c92e6faa2aca0199b1b6df922c391604051918291826130ca565b6104467f7b20773c41402791c5f18914dbbeacad38b1ebcc4c55d8eb3bfe0a4cde26c82691604051918291826130ca565b610eb091935060c03d60c011610ad357610ac58183612707565b915f610e28565b610edb610f1092610ecc610f15963690612f3c565b60608d01526060369101612f3c565b60808b0152610ee86132b5565b60a08b0152610ef56132b5565b8b8b01526001600160a01b03165f52600160205260405f2090565b615c88565b505f8d8d82610db3565b600319604091011261027757600435610f378161048d565b9060243561084d8161048d565b34610277576020610f816001600160a01b03610f5f36610f1f565b91165f526006835260405f20906001600160a01b03165f5260205260405f2090565b54604051908152f35b34610277575f600319360112610277576020604051612a308152f35b3461027757604060031936011261027757610644610638602435600435613373565b610fda610fd436610ccc565b9061342d565b005b60606003198201126102775760043591602435916044359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610fda61102036610fdc565b9161378a565b34610277576020600319360112610277576001600160a01b0360043561104b8161048d565b165f52600160205261105f60405f20615b05565b5f905f5b81518110156110ff5761109161108a61107c838561335f565b515f525f60205260405f2090565b5460ff1690565b61109a816121c1565b600381141590816110ea575b506110b4575b600101611063565b916110c78184600193106110cf576139c6565b9290506110ac565b6110d9858561335f565b516110e4828661335f565b526139c6565b600591506110f7816121c1565b14155f6110a6565b506106449181526040519182918261059f565b34610277575f60031936011261027757602060405160408152f35b34610277575f600319360112610277576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610277576020610f816001600160a01b0361118c36610f1f565b91165f526008835260405f20906001600160a01b03165f5260205260405f2090565b34610277575f600319360112610277576020600454604051908152f35b34610277576112556111dc3661028a565b929391906111f2855f52600560205260405f2090565b9182549261120184151561266b565b600281019060a061122261121c84546001600160a01b031690565b8a615053565b604051809881927f24063eba000000000000000000000000000000000000000000000000000000008352600483016139d4565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4958615610488575f9661132b575b50600181015460081c6001600160a01b0316968792546112a2906001600160a01b031690565b809581956003850154976112b7928992613eff565b9a9190946006019a6112c88c6128a0565b956112d3968b61405d565b846112dd876128a0565b6112e795896150bb565b6060015167ffffffffffffffff166040519182916113059183612a41565b037fb8568a1f475f3c76759a620e08a653d28348c5c09e2e0bc91d533339801fefd891a2005b61134e91965060a03d60a011611355575b6113468183612707565b8101906136bc565b945f61127c565b503d61133c565b34610277575f600319360112610277576020604051620151808152f35b61143661138536610ccc565b6113a661139760208395949501612c27565b6113a081611c0f565b15612c31565b6113bc6001610d31855f525f60205260405f2090565b9060c08161141b6114146108ec6107146113e060208901516001600160a01b031690565b6114078b8a60408101938960806113fe87516001600160a01b031690565b9301519361494b565b516001600160a01b031690565b54876149c5565b6040519586928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc9361044693610bcc925f92611499575b50611492368561301c565b9087614b52565b6114b391925060c03d60c011610ad357610ac58183612707565b905f611487565b34610277576114c836610816565b906114da6006610b2e60208501612c27565b6114e761089b3683612c79565b91602082016114f581612ced565b9061150960408501926080610b6c85612ced565b611515610b858461316b565b9361151f86614c9c565b1561155657505050610bd881610bcc7f587faad1bcd589ce902468251883e1976a645af8563c773eed7356d78433210c9386614cf9565b6115a59060a08561157261156c87989697612ced565b89615053565b60405194859283927eea54e700000000000000000000000000000000000000000000000000000000845260048401613773565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af48015610488577f17eb0a6bd5a0de45d1029ce3444941070e149df35b22176fc439f930f73c09f795610bd895610bcc945f93611614575b50610c8261160291612ced565b9161160d368761301c565b8a8a6150bb565b611602919350611635610c829160a03d60a011611355576113468183612707565b9391506115f5565b6024359060ff8216820361027757565b359060ff8216820361027757565b34610277576040600319360112610277576001600160a01b036116a96004356116838161048d565b8261168c61163d565b91165f52600760205260405f209060ff165f5260205260405f2090565b541660405180916001600160a01b0360208301911682520390f35b60806003193601126102775760043560243567ffffffffffffffff8111610277576116f3903690600401610807565b60443567ffffffffffffffff811161027757611713903690600401610249565b919061171d61027b565b9061172f855f525f60205260405f2090565b9161173c6001840161323d565b9161176661174b855460ff1690565b611754816121c1565b600181149081156119c2575b506139e5565b86611773600586016128a0565b916117b46117808861316b565b67ffffffffffffffff6117ab61179e875167ffffffffffffffff1690565b67ffffffffffffffff1690565b91161015613a14565b60208501516001600160a01b0316976117d760408701516001600160a01b031690565b9367ffffffffffffffff6117ff61179e6117f08c61316b565b935167ffffffffffffffff1690565b9116116118c3575b94611867889795857f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a9b6118616118859760149d61185561187c996118959c6118b49f60808c015192613eff565b9391949092369061301c565b9061405d565b845460ff191660021785555163ffffffff1690565b63ffffffff1690565b67ffffffffffffffff4216613a43565b9301805467ffffffffffffffff191667ffffffffffffffff8516179055565b61044660405192839283613a65565b909296959397946118df61190a9389888a60808601519361494b565b60c08761141b6119036108ec8c6001600160a01b03165f52600660205260405f2090565b548d6149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4938415610488577f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a99896118b4986118618e8c61185560149f976118679861187c9b61198a6118959f6118859f5f916119a3575b508d611983368961301c565b9089615443565b9a9f5050995050509750509b5095509597985050611807565b6119bc915060c03d60c011610ad357610ac58183612707565b5f611977565b600491506119cf816121c1565b145f611760565b34610277576080600319360112610277576004356119f38161048d565b6119fb61163d565b90604435611a088161048d565b60643567ffffffffffffffff811161027757611b4a6001600160a01b0392611b22611a6396611b07611b02611a4289973690600401610249565b60ff85169a91611afc90611a578d1515613a8d565b8b89169d8e1515612b9e565b611abf8785611ab9611aad611aad611aa085611a90866001600160a01b03165f52600760205260405f2090565b9060ff165f5260205260405f2090565b546001600160a01b031690565b6001600160a01b031690565b15613abc565b6040805160ff891660208201526001600160a01b038b169181019190915246606080830191909152815292611af5608085612707565b3691612fcb565b9061564c565b613b01565b611a90856001600160a01b03165f52600760205260405f2090565b906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b167f2366b94a706a0cfc2dca2fe8be9410b6fba2db75e3e9d3f03b3c2fb0b051efad5f80a4005b611b91611b7d36610ccc565b6113a66003610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf9869361044693610bcc925f926114995750611492368561301c565b634e487b7160e01b5f52602160045260245ffd5b60041115611c0a57565b611bec565b600a1115611c0a57565b90600a821015611c0a5752565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b61084d9167ffffffffffffffff8251168152611c6f60208301516020830190611c19565b60408201516040820152611cdd6060830151606083019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b608082810151805167ffffffffffffffff1661014084015260208101516001600160a01b0316610160840152604081015160ff1661018084015260608101516101a0840152908101516101c083015260a08101516101e083015260c0015161020082015260c0611d5f60a0840151610260610220850152610260840190611c26565b92015190610240818403910152611c26565b929367ffffffffffffffff60c09561084d98979482948752611d9281611c00565b602087015216604085015216606083015260808201528160a08201520190611c4b565b3461027757602060031936011261027757600435611dd1613b66565b505f52600260205260405f20611de561272a565b9080548252610644600182015491611e31611e21611e038560ff1690565b94611e12602088019687613baa565b60081c6001600160a01b031690565b6001600160a01b03166040860152565b611e58611e4860028301546001600160a01b031690565b6001600160a01b03166060860152565b60038101546080850152600481015467ffffffffffffffff811660a086019081529490611e90905b60401c67ffffffffffffffff1690565b67ffffffffffffffff1660c0820190815291611ee46117f0611ec0600660058501549460e08701958652016128a0565b93610100810194855251965197611ed689611c00565b5167ffffffffffffffff1690565b905191519260405196879687611d71565b3461027757611f0336610816565b611f146008610b2e60208401612c27565b80611f89611f2561089b3686612c79565b936020810160c0611f3582612ced565b91611f546040850193611f4785612ced565b608087013591898c61494b565b610de2610ddb610dc4610714611f6a368b61301c565b9687958d8a611f7882614c9c565b9d8e15612052575b50505050612ced565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857611fc6935f9361202d575b50611fc0903690612c79565b86614b52565b15611ffc576104467f3142fb397e715d80415dff7b527bf1c451def4675da6e1199ee1b4588e3f630a91604051918291826130ca565b6104467f26afbcb9eb52c21f42eb9cfe8f263718ffb65afbf84abe8ad8cce2acfb2242b891604051918291826130ca565b611fc091935061204b9060c03d60c011610ad357610ac58183612707565b9290611fb4565b6120aa936120876109a6926120696109bd9561483e565b8c606061207a366101408501612f3c565b9101526060369101612f3c565b60808c01526120946132b5565b60a08c01526120a16132b5565b8c8c0152612ced565b505f8d8a8e611f80565b9160a09367ffffffffffffffff9161084d97969385526120d381611c00565b602085015216604083015260608201528160808201520190611c4b565b346102775760206003193601126102775760043561210c613b66565b505f52600560205260405f2061212061273c565b908054825261064460018201549161213e611e21611e038560ff1690565b612155611e4860028301546001600160a01b031690565b60038101546080850152600481015467ffffffffffffffff1667ffffffffffffffff1660a08501908152936121b061219b600660058501549460c08501958652016128a0565b9160e0810192835251945195611ed687611c00565b9151905191604051958695866120b4565b60061115611c0a57565b906006821015611c0a5752565b9192612250610120946121f285612263959a99989a6121cb565b602085019060a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b61014060e0840152610140830190611c4b565b946101008201520152565b34610277576020600319360112610277576004355f60a0604051612291816126ae565b82815282602082015282604082015282606082015282608082015201526122b6613b66565b505f525f6020526122c960405f20613bc2565b80516122d4816121c1565b61064460208301519260408101519060606122fd61179e608084015167ffffffffffffffff1690565b91015191604051958695866121d8565b346102775761231b3661049e565b90916123316001600160a01b0382161515612b9e565b61233c821515612bcd565b335f5260066020526123628360405f20906001600160a01b03165f5260205260405f2090565b54908282106123f75782820391821161059a578383916123b7936123b18361239b336001600160a01b03165f52600660205260405f2090565b906001600160a01b03165f5260205260405f2090565b55614d9d565b7fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb6001600160a01b0360405193169280610bd83394829190602083019252565b7ff4d678b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b61243f61242b36610ccc565b6113a66002610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f417786962066969361044693610bcc925f926114995750611492368561301c565b34610277576124a836610f1f565b6124b0615810565b6001600160a01b038116916124c6831515612b9e565b6001600160a01b036124ed8261239b336001600160a01b03165f52600860205260405f2090565b54916124fa831515612bcd565b5f61251a8261239b336001600160a01b03165f52600860205260405f2090565b55169181836125945761253d915f808080858a5af1612537613c20565b50613c4f565b60405190815233907f7b8d70738154be94a9a068a6d2f5dd8cfc65c52855859dc8f47de1ff185f8b5590602090a4610fda60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b50506040517fa9059cbb000000000000000000000000000000000000000000000000000000005f52836004528160245260205f60448180875af160015f511481161561261a575b60409190915261253d577f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b03821660045260245ffd5b6001811516612630573d15843b151516166125db565b503d5f823e3d90fd5b3461027757610fda61264a36610fdc565b91613c90565b34610277575f60031936011261027757602060405160018152f35b1561267257565b7fc60f1e78000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176126ca57604052565b61269a565b60e0810190811067ffffffffffffffff8211176126ca57604052565b60a0810190811067ffffffffffffffff8211176126ca57604052565b90601f601f19910116810190811067ffffffffffffffff8211176126ca57604052565b6040519061273a61012083612707565b565b6040519061273a61010083612707565b6040519061273a60e083612707565b90604051612768816126cf565b60c0600482946127a660ff825467ffffffffffffffff811687526001600160a01b03808260401c1616602088015260e01c16604086019060ff169052565b6001810154606085015260028101546080850152600381015460a08501520154910152565b90600182811c921680156127f9575b60208310146127e557565b634e487b7160e01b5f52602260045260245ffd5b91607f16916127da565b5f9291815491612812836127cb565b8083529260018116908115612867575060011461282e57505050565b5f9081526020812093945091925b83831061284d575060209250010190565b60018160209294939454838587010152019101919061283c565b9050602094955060ff1991509291921683830152151560051b010190565b9061273a6128999260405193848092612803565b0383612707565b906040516128ad816126cf565b809260ff815467ffffffffffffffff8116845260401c1690600a821015611c0a57600d61291f9160c0936020860152600181015460408601526128f26002820161275b565b60608601526129036007820161275b565b6080860152612914600c8201612885565b60a086015201612885565b910152565b5190600482101561027757565b67ffffffffffffffff81160361027757565b5190811515820361027757565b908160c0910312610277576129b860a06040519261296d846126ae565b805184526020810151602085015261298760408201612924565b6040850152606081015161299a81612931565b606085015260808101516129ad81612931565b608085015201612943565b60a082015290565b9081516129cc81611c00565b815260a0806129ea602085015160c0602086015260c0850190611c4b565b936040810151604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff6080820151166080850152015191015290565b90602061084d9281815201906129c0565b6040513d5f823e3d90fd5b92916020612b8d61273a9360408752612a75815467ffffffffffffffff811660408a015260ff60608a019160401c16611c19565b60018101546080880152600281015467ffffffffffffffff811660a0890152604081901c6001600160a01b031660c089015260e090811c60ff16908801526003810154610100880152600481015461012088015260058101546101408801526006810154610160880152600781015467ffffffffffffffff8116610180890152604081901c6001600160a01b03166101a089015260e01c60ff166101c088015260088101546101e08801526009810154610200880152600a810154610220880152600b81015461024088015261026080880152600d612b5b6102a08901600c8401612803565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0898403016102808a015201612803565b94019067ffffffffffffffff169052565b15612ba557565b7fe6c4247b000000000000000000000000000000000000000000000000000000005f5260045ffd5b15612bd457565b7f69640e72000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b9190820180921161059a57565b600a111561027757565b3561084d81612c1d565b15612c3857565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b63ffffffff81160361027757565b359061273a82612931565b91908260c091031261027757604051612c91816126ae565b60a08082948035612ca181612c60565b84526020810135612cb18161048d565b60208501526040810135612cc48161048d565b60408501526060810135612cd781612931565b6060850152608081013560808501520135910152565b3561084d8161048d565b908160c09103126102775760405190612d0f826126ae565b805182526020810151602083015260408101516006811015610277576129b89160a09160408501526060810151612d4581612931565b60608501526129ad60808201612943565b90612d628183516121cb565b608067ffffffffffffffff81612d87602086015160a0602087015260a0860190611c4b565b94604081015160408601526060810151606086015201511691015290565b359061273a82612c1d565b60c0809167ffffffffffffffff8135612dc881612931565b1684526001600160a01b036020820135612de18161048d565b16602085015260ff612df56040830161164d565b166040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b9035601e198236030181121561027757016020813591019167ffffffffffffffff821161027757813603831361027757565b601f8260209493601f1993818652868601375f8582860101520116010190565b61084d9167ffffffffffffffff8235612e8a81612931565b168152612ea86020830135612e9e81612c1d565b6020830190611c19565b60408201356040820152612ec26060820160608401612db0565b612ed461014082016101408401612db0565b612f08612efc612ee8610220850185612e20565b610260610220860152610260850191612e52565b92610240810190612e20565b91610240818503910152612e52565b9091612f2e61084d93604084526040840190612d56565b916020818403910152612e72565b91908260e091031261027757604051612f54816126cf565b60c08082948035612f6481612931565b84526020810135612f748161048d565b6020850152612f856040820161164d565b6040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b67ffffffffffffffff81116126ca57601f01601f191660200190565b929192612fd782612faf565b91612fe56040519384612707565b829481845281830111610277578281602093845f960137010152565b9080601f830112156102775781602061084d93359101612fcb565b919091610260818403126102775761303261274c565b9261303c82612c6e565b845261304a60208301612da5565b6020850152604082013560408501526130668160608401612f3c565b6060850152613079816101408401612f3c565b608085015261022082013567ffffffffffffffff8111610277578161309f918401613001565b60a085015261024082013567ffffffffffffffff8111610277576130c39201613001565b60c0830152565b90602061084d928181520190612e72565b60e09060a061084d949363ffffffff81356130f581612c60565b1683526001600160a01b03602082013561310e8161048d565b1660208401526001600160a01b03604082013561312a8161048d565b16604084015267ffffffffffffffff606082013561314781612931565b16606084015260808101356080840152013560a08201528160c08201520190612e72565b3561084d81612931565b9091612f2e61084d936040845260408401906129c0565b634e487b7160e01b5f52603260045260245ffd5b6003548110156131b85760035f5260205f2001905f90565b61318c565b80548210156131b8575f5260205f2001905f90565b916131eb9183549060031b91821b915f19901b19161790565b9055565b600354680100000000000000008110156126ca57600181016003556003548110156131b85760035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b0155565b9060405161324a816126ae565b60a0600382946001600160a01b03815463ffffffff8116865260201c1660208501526132a467ffffffffffffffff60018301546001600160a01b03808216166040880152851c16606086019067ffffffffffffffff169052565b600281015460808501520154910152565b604051906132c4602083612707565b5f8252565b90916132e061084d93604084526040840190612d56565b916020818403910152611c4b565b67ffffffffffffffff81116126ca5760051b60200190565b60405190613315602083612707565b5f808352366020840137565b9061332b826132ee565b6133386040519182612707565b828152601f1961334882946132ee565b0190602036910137565b9190820391821161059a57565b80518210156131b85760209160051b010190565b9190600354908084029380850482149015171561059a57818410156133f75783019081841161059a578082116133ef575b506133b76133b28483613352565b613321565b92805b8281106133c657505050565b806133d56106986001936131a0565b6133e86133e28584613352565b8861335f565b52016133ba565b90505f6133a4565b5050905061084d613306565b906006811015611c0a5760ff60ff198354169116179055565b90602061084d928181520190611c4b565b9061343f825f525f60205260405f2090565b61344b6001820161323d565b91613457825460ff1690565b9184613465600583016128a0565b91604086019261347c84516001600160a01b031690565b91600261349360208a01516001600160a01b031690565b9761349d816121c1565b1480613654575b6135995750506134e16108f86108ec6107146134fc9661140760c0978a978c898f6080906134d96001610b2e60208601612c27565b01519361494b565b6040519384928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4801561048857610f10613573946109a688937f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a898613566965f92613578575b5061355f368961301c565b9086614b52565b50604051918291826130ca565b0390a2565b61359291925060c03d60c011610ad357610ac58183612707565b905f613554565b7f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a89750613573969195506136479450610f10926135fe6014836135e66109a695600360ff19825416179055565b5f60138201550167ffffffffffffffff198154169055565b606087016136278151606061361d60208301516001600160a01b031690565b9101519086614d9d565b5160a061363e60208301516001600160a01b031690565b91015191614d9d565b506040519182918261341c565b5061366d61179e601483015467ffffffffffffffff1690565b42116134a4565b1561367b57565b7fdb1ea1ac000000000000000000000000000000000000000000000000000000005f5260045ffd5b906136ad81611c00565b60ff60ff198354169116179055565b908160a0910312610277576137116080604051926136d9846126eb565b80518452602081015160208501526136f360408201612924565b6040850152606081015161370681612931565b606085015201612943565b608082015290565b90815161372581611c00565b815260806001600160a01b038161374b602086015160a0602087015260a0860190611c4b565b946040810151604086015267ffffffffffffffff606082015116606086015201511691015290565b9091612f2e61084d93604084526040840190613719565b9161379483614c9c565b613959576137aa825f52600560205260405f2090565b6137b684825414613674565b60018101918254916137d2836001600160a01b039060081c1690565b9360026137f26137eb828501546001600160a01b031690565b9560ff1690565b6137fb81611c00565b1480613939575b6138bf5750600361383b9161381e6007610b2e60208701612c27565b019261382e84548287868b61494b565b60a0836115728389615053565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4908115610488577f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d19561389995610bcc945f9461389e575b50549261160d368761301c565b0390a3565b6138b891945060a03d60a011611355576113468183612707565b925f61388c565b7f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d1945090613899936138fb610bcc93600360ff19825416179055565b613933600d60058401935f855495556139226004820167ffffffffffffffff198154169055565b015460401c6001600160a01b031690565b90614d9d565b5061395261179e600484015467ffffffffffffffff1690565b4211613802565b6138997f6d0cf3d243d63f08f50db493a8af34b27d4e3bc9ec4098e82700abfeffe2d49891610bcc613992865f525f60205260405f2090565b6139be60026139af60018401546001600160a01b039060201c1690565b9201546001600160a01b031690565b908388615025565b5f19811461059a5760010190565b90602061084d928181520190613719565b156139ec57565b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613a1b57565b7f7d957361000000000000000000000000000000000000000000000000000000005f5260045ffd5b9067ffffffffffffffff8091169116019067ffffffffffffffff821161059a57565b9067ffffffffffffffff613a86602092959495604085526040850190612e72565b9416910152565b15613a9457565b7f06ee4dcd000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613ac5575050565b906001600160a01b0360ff927f0bcc40f3000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b15613b0857565b7fc1606c2f000000000000000000000000000000000000000000000000000000005f5260045ffd5b60405190613b3d826126cf565b5f60c0838281528260208201528260408201528260608201528260808201528260a08201520152565b60405190613b73826126cf565b606060c0835f81525f60208201525f6040820152613b8f613b30565b83820152613b9b613b30565b60808201528260a08201520152565b613bb382611c00565b52565b6006821015611c0a5752565b90604051613bcf816126eb565b608067ffffffffffffffff60148395613bec60ff82541686613bb6565b613bf86001820161323d565b6020860152613c09600582016128a0565b604086015260138101546060860152015416910152565b3d15613c4a573d90613c3182612faf565b91613c3f6040519384612707565b82523d5f602084013e565b606090565b15613c58575050565b6001600160a01b03907fa5b05eec000000000000000000000000000000000000000000000000000000005f521660045260245260445ffd5b91613c9a83614c9c565b613e8157613cb0825f52600260205260405f2090565b613cbc84825414613674565b6001810191825491613cd8836001600160a01b039060081c1690565b936002613cf16137eb828501546001600160a01b031690565b613cfa81611c00565b1480613e5e575b613de457506003613d6591613d1d6005610b2e60208701612c27565b0192613d2d84548287868b61494b565b60c083610bf1613d5e613d51856001600160a01b03165f52600660205260405f2090565b61073d6101608501612ced565b5489614206565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e9561389995610bcc945f94613dc3575b505492610c93368761301c565b613ddd91945060c03d60c011610481576104708183612707565b925f613db6565b7f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e94509061389993613e20610bcc93600360ff19825416179055565b613933600d60058401935f85549555613922600482017fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff8154169055565b506004820154613e7a9060401c67ffffffffffffffff1661179e565b4211613d01565b6138997f32e24720f56fd5a7f4cb219d7ff3278ae95196e79c85b5801395894a6f53466c91610bcc613992865f525f60205260405f2090565b15613ec3575050565b906001600160a01b0360ff927f577f5940000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b93929190918215613fd157843560f81c9081613f4c57507f000000000000000000000000000000000000000000000000000000000000000094600101925f19019150613f489050565b9091565b600180915f97939594975060ff86161c1603613fa957613f9d83613f8b611aa0613f4896611a908a6001600160a01b03165f52600760205260405f2090565b966001600160a01b0388161515613eba565b600101915f1990910190565b7f1a9073b4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fac241e11000000000000000000000000000000000000000000000000000000005f5260045ffd5b805191908290602001825e015f815290565b60021115611c0a57565b90816020910312610277575190565b939260609361404f6001600160a01b0394613a86949998998852608060208901526080880190611c26565b918683036040880152612e52565b906001600160a01b03929560209761409195996140c861407f61410d95615887565b6140ba604051998a928e840190613ff9565b7f6368616c6c656e67650000000000000000000000000000000000000000000000815260090190565b03601f198101895288612707565b6140d18161400b565b61415a57505b604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b0392165afa80156104885761273a915f9161412b575b501515613b01565b61414d915060203d602011614153575b6141458183612707565b810190614015565b5f614123565b503d61413b565b90506140d7565b5f6040519161416f836126ae565b81835261420260208401614181613b66565b81526141f46040860191858352611e806004606089019288845260808a01958987526141bc60a08c01998b8b525f52600260205260405f2090565b9160ff6001840154166141ce81611c00565b8c526141dc600684016128a0565b90526005820154905201549167ffffffffffffffff83165b67ffffffffffffffff169052565b5290565b9060405191614214836126ae565b5f835261420260208401614226613b66565b81526141f460408601915f8352611e80600460608901925f845260808a01955f87526141bc60a08c01995f8b525f52600260205260405f2090565b7f8000000000000000000000000000000000000000000000000000000000000000811461059a575f0390565b6020939291614329919796976142ab815f52600260205260405f2090565b976040860180516142bb81611c00565b6142c481611c00565b61450a575b5089888660a08901956142dc8751151590565b6144f6575b5050505050506142fc606085015167ffffffffffffffff1690565b67ffffffffffffffff81166144cb575b50608084015167ffffffffffffffff168061447c575b5051151590565b1561446357608001518201516001600160a01b031680935b8251905f82131561442357614363915061435b8451615ad0565b928391614527565b61437260058601918254612c10565b90555b0180515f8113156143d15750916143b060059261239b6143986143c69651615ad0565b966001600160a01b03165f52600660205260405f2090565b6143bb858254613352565b905501918254612c10565b90555b61273a61467e565b9290505f83126143e5575b505050506143c9565b61440260059261239b6143986143fd61441897614261565b615ad0565b61440d858254612c10565b905501918254613352565b90555f8080806143dc565b5f8212614433575b505050614375565b6144426143fd61444a93614261565b928391614d9d565b61445960058601918254613352565b9055825f8061442b565b50600d84015460401c6001600160a01b03168093614341565b6144c59060048901907fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff6fffffffffffffffff000000000000000083549260401b169116179055565b5f614322565b6144f090600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f61430c565b6144ff956159a2565b5f80898886836142e1565b614521905161451881611c00565b60018b016136a3565b5f6142c9565b9061453a9291614535615810565b61458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b1561456757565b7fd2ade556000000000000000000000000000000000000000000000000000000005f5260045ffd5b908215614679576001600160a01b0316918215801561466a576145b3823414614560565b156145bd57505050565b6001600160a01b03604051927f23b872dd000000000000000000000000000000000000000000000000000000005f52166004523060245260445260205f60648180865af160015f5114811615614654575b6040919091525f606052156146205750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b031660045260245ffd5b6001811516612630573d15833b1515161661460e565b6146743415614560565b6145b3565b505050565b6003546004545f5b82821080614776575b1561476b576146a36106a2610698846131a0565b6001810160036146b4825460ff1690565b6146bd81611c00565b14614759576146cb82615b4d565b1561471657915f8261076961470d95600561470796019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b916139c6565b915b9190614686565b5050915061472390600455565b8061472b5750565b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1565b505090614765906139c6565b9161470f565b915061472390600455565b506040811061468f565b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f01000000000000000000000000000000000000000000000000000000000000009160405161482760208201809360a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b60c0815261483660e082612707565b519020161790565b602081013561484c8161048d565b6001600160a01b03811690614862821515612b9e565b6040830135906148718261048d565b6148986001600160a01b0383169261488a841515612b9e565b6148938361048d565b61048d565b6148a18161048d565b5081146148ed575063ffffffff6201518091356148bd81612c60565b16106148c557565b7f0596b15b000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fabfa558d000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b903590601e1981360301821215610277570180359067ffffffffffffffff82116102775760200191813603831361027757565b929161273a9461497c61498b92614971838761496b610220890189614918565b90613eff565b90878a949394615b81565b8361496b610240850185614918565b92909194615b81565b604051906149a1826126eb565b5f6080838281526149b0613b66565b60208201528260408201528260608201520152565b90601467ffffffffffffffff916149da614994565b935f525f60205260405f20906149f460ff83541686613bb6565b614a00600583016128a0565b6020860152601382015460408601526060850152015416608082015290565b9060a060039163ffffffff8151167fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000008554161784556001600160a01b036020820151167fffffffffffffffff0000000000000000000000000000000000000000ffffffff77ffffffffffffffffffffffffffffffffffffffff0000000086549260201b169116178455614b4160018501614aef614ac660408501516001600160a01b031690565b82906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b606083015181547fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff1660a09190911b7bffffffffffffffff000000000000000000000000000000000000000016179055565b608081015160028501550151910155565b92614b8e81614bde9460a094614b6f885f525f60205260405f2090565b97614b7b895460ff1690565b614b84816121c1565b15614c5a57615443565b604081018051614b9d816121c1565b614ba6816121c1565b151580614c2f575b614c15575b50601484018054606083015167ffffffffffffffff9081169116819003614bed575b50500151151590565b614be55750565b60135f910155565b614c0e919067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f80614bd5565b614c299051614c23816121c1565b85613403565b5f614bb3565b50845460ff16815190614c41826121c1565b614c4a826121c1565b614c53816121c1565b1415614bae565b614c678260018b01614a1f565b615443565b9067ffffffffffffffff604051916020830193845216604082015260408152614c96606082612707565b51902090565b805f525f60205260ff60405f2054166006811015611c0a578015908115614ce5575b50614ce0575f525f60205267ffffffffffffffff600760405f20015416461490565b505f90565b60059150614cf2816121c1565b145f614cbe565b90614d3b91614d146001610d31835f525f60205260405f2090565b60c0836108ff614d346108ec61071460408701516001600160a01b031690565b54856149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af49283156104885761273a945f94614d78575b50614d7290369061301c565b91614b52565b614d72919450614d969060c03d60c011610ad357610ac58183612707565b9390614d66565b9061453a9291614dab615810565b9190918115614679576001600160a01b0383169283614e4f576001600160a01b038216925f8080808488620186a0f1614de2613c20565b5015614def575050505050565b614e326138999261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614e3d828254612c10565b90556040519081529081906020820190565b6040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481885afa908115610488575f91615006575b506040517fa9059cbb00000000000000000000000000000000000000000000000000000000602082019081526001600160a01b0385166024830152604480830187905282525f91829190614ee7606482612707565b51908286620186a0f190614ef9613c20565b506040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526020816024818a5afa9182156104885786915f93614fe5575b5083614fda575b83614fc6575b50505015614f5b575b50505050565b81614fa46001600160a01b039261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614faf858254612c10565b90556040519384521691602090a35f808080614f55565b614fd1929350613352565b145f8481614f4c565b818110159350614f46565b614fff91935060203d602011614153576141458183612707565b915f614f3f565b61501f915060203d602011614153576141458183612707565b5f614e92565b909192614d14614d3b946150456001610d31865f525f60205260405f2090565b92608084015191868661494b565b906001600160a01b0390615065614994565b925f52600560205267ffffffffffffffff600460405f2060ff60018201541661508d81611c00565b865261509b600682016128a0565b602087015260058101546040870152015416606084015216608082015290565b6020939291614329919796976150d9815f52600560205260405f2090565b976040860180516150e981611c00565b6150f281611c00565b615179575b50898886608089019561510a8751151590565b615165575b50505050505061512a606085015167ffffffffffffffff1690565b67ffffffffffffffff8116615140575051151590565b6144c590600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b61516e95615d2e565b5f808988868361510f565b615187905161451881611c00565b5f6150f7565b90600a811015611c0a577fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff68ff000000000000000083549260401b169116179055565b9060c060049161520367ffffffffffffffff825116859067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015184546040808401517fffffff000000000000000000000000000000000000000000ffffffffffffffff90921692901b7bffffffffffffffffffffffffffffffffffffffff0000000000000000169190911760e09190911b7cff0000000000000000000000000000000000000000000000000000000016178455606081015160018501556080810151600285015560a081015160038501550151910155565b601f82116152b357505050565b5f5260205f20906020601f840160051c830193106152eb575b601f0160051c01905b8181106152e0575050565b5f81556001016152d5565b90915081906152cc565b919091825167ffffffffffffffff81116126ca5761531d8161531784546127cb565b846152a6565b6020601f82116001146153585781906131eb9394955f9261534d575b50508160011b915f199060031b1c19161790565b015190505f80615339565b601f1982169061536b845f5260205f2090565b915f5b8181106153a55750958360019596971061538d575b505050811b019055565b01515f1960f88460031b161c191690555f8080615383565b9192602060018192868b01518155019401920161536e565b8151815467ffffffffffffffff191667ffffffffffffffff91909116178155602082015191600a831015611c0a5760c0600d916153fd61273a958561518d565b604081015160018501556154186060820151600286016151d0565b6154296080820151600786016151d0565b61543a60a0820151600c86016152f5565b015191016152f5565b6154596060919493945f525f60205260405f2090565b936154676080850151151590565b61563a575b01916154bf60a06154886020865101516001600160a01b031690565b9280515f81136155fc575b506020810180515f81136155b4575b5081515f8112615573575b50515f8112615528575b500151151590565b8061551a575b6154d6575b5050505061273a61467e565b61550f9261550260a0926154f6604060139601516001600160a01b031690565b90848451015191614d9d565b5101519201918254613352565b90555f8080806154ca565b5060a08351015115156154c5565b6143fd61553491614261565b61554f8561239b61071460408a01516001600160a01b031690565b61555a828254612c10565b905561556b60138901918254613352565b90555f6154b7565b6143fd61557f91614261565b61559d818761559860208b01516001600160a01b031690565b614d9d565b6155ac60138a01918254613352565b90555f6154ad565b6155bd90615ad0565b6155d88661239b61071460408b01516001600160a01b031690565b6155e3828254613352565b90556155f460138a01918254612c10565b90555f6154a2565b61560590615ad0565b615623818661561e60208a01516001600160a01b031690565b614527565b61563260138901918254612c10565b90555f615493565b61564781600587016153bd565b61546c565b805192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156157e8575b806d04ee2d6d415b85acef8100000000600a9210156157cc575b662386f26fc100008110156157b7575b6305f5e1008110156157a5575b612710811015615795575b6064811015615786575b101561577b575b61571260216156da60018801615ddf565b968701015b5f1901917f3031323334353637383961626364656600000000000000000000000000000000600a82061a8353600a900490565b90811561572257615712906156df565b50506001600160a01b036157478461573b858498615d73565b60208151910120615dc9565b9116931683146157735761576591816020611aad9351910120615dc9565b1461576e575f90565b600190565b505050600190565b6001909401936156c9565b600290606490049601956156c2565b60049061271090049601956156b8565b6008906305f5e10090049601956156ad565b601090662386f26fc1000090049601956156a0565b6020906d04ee2d6d415b85acef81000000009004960195615690565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008104615676565b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00541461585f5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff815116906020810151600a811015611c0a576159308260406159919401516158cf60806060840151930151946040519760208901526040880190611c19565b6060860152608085019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b805167ffffffffffffffff1661016084015260208101516001600160a01b0316610180840152604081015160ff166101a084015260608101516101c084015260808101516101e084015260a081015161020084015260c00151610220830152565b610220815261084d61024082612707565b93909291935f52600260205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015192600a841015611c0a57615a5660c0615aa093615a0e615acc9760039a61518d565b60408101516007890155615a29606082015160088a016151d0565b615a3a6080820151600d8a016151d0565b615a4b60a082015160128a016152f5565b0151601387016152f5565b60018501907fffffffffffffffffffffff0000000000000000000000000000000000000000ff74ffffffffffffffffffffffffffffffffffffffff0083549260081b169116179055565b60028301906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b0155565b5f8112615ada5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b90604051918281549182825260208201905f5260205f20925f5b818110615b3457505061273a92500383612707565b8454835260019485019487945060209093019201615b1f565b67ffffffffffffffff6004820154164210159081615b69575090565b600180925060ff91015416615b7d81611c00565b1490565b6001600160a01b039061410d615ba7615ba26020989599969799369061301c565b615887565b93604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b6001810190825f528160205260405f2054155f14615c46578054680100000000000000008110156126ca57615c33615c1d8260018794018555846131bd565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b5050505f90565b80548015615c74575f190190615c6382826131bd565b8154905f199060031b1b1916905555565b634e487b7160e01b5f52603160045260245ffd5b6001810191805f528260205260405f2054928315155f14615d26575f19840184811161059a5783545f1981019490851161059a575f958583615ce397615cd69503615ce9575b505050615c4d565b905f5260205260405f2090565b55600190565b615d0f615d0991615d00610698615d1d95886131bd565b928391876131bd565b906131d2565b85905f5260205260405f2090565b555f8080615cce565b505050505f90565b93909291935f52600560205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b61273a90615dbb615db594936040519586937f19457468657265756d205369676e6564204d6573736167653a0a0000000000006020860152603a850190613ff9565b90613ff9565b03601f198101845283612707565b61084d91615dd691615e06565b90929192615e40565b90615de982612faf565b615df66040519182612707565b828152601f196133488294612faf565b8151919060418303615e3657615e2f9250602082015190606060408401519301515f1a90615f07565b9192909190565b50505f9160029190565b615e4981611c00565b80615e52575050565b615e5b81611c00565b60018103615e8b577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b615e9481611c00565b60028103615ec857507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b80615ed4600392611c00565b14615edc5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411615f7e579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610488575f516001600160a01b03811615615f7457905f905f90565b505f906001905f90565b5050505f916003919056fea2646970667358221220a1d82448e7e3f611b69660be3f5dc6e070ca23bb9e11aefc6fc6b7622d1cdc4e64736f6c634300081e0033000000000000000000000000735eb1026afba78b602da39c6b59eaba95753686",
+ "nonce": "0x2c",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ }
+ ],
+ "receipts": [
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x9fc18a",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x3df2187dc8a50ef62abfeb377318888493042770315492070c4708584dfbf572",
+ "transactionIndex": "0x66",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0x1410b0",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0xacfd78",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x9712dcbc9f46d075bb90ab9d5cbbdf30195810bb050150b302cb3aaaf0e71bc0",
+ "transactionIndex": "0x67",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0xd3bee",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0xb9c9d6",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x6e81a9f20bb7b3370a15b6402271a9f8e7eae63184e733c80273c497a4187983",
+ "transactionIndex": "0x68",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0xccc5e",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0xc05453",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x5e58e1f709d9ded21112c24523733b843486bf6ae775ffd10d86118a5c947cfe",
+ "transactionIndex": "0x69",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0x68a7d",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0x735eb1026afba78b602da39c6b59eaba95753686"
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x11229e3",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x6e0b716f9bdb40d3aadbfa2544bf5ec11b39f431736bd19569ade187cb0b7396",
+ "transactionIndex": "0x6a",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0x51d590",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0xb7be0e2007ddf320d680942cb9e008f986e74f83"
+ }
+ ],
+ "libraries": [
+ "src/ChannelEngine.sol:ChannelEngine:0x78D150fdA6fa6739C18014B347c7c7C45C58e148",
+ "src/EscrowDepositEngine.sol:EscrowDepositEngine:0x728904E52308213bA61C90EF49F34c18Fbda9E11",
+ "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine:0x893F2D45fDFFe2D4297a5C1D5732EDce4849eE82"
+ ],
+ "pending": [],
+ "returns": {},
+ "timestamp": 1772892720931,
+ "chain": 11155111,
+ "commit": "fd394085"
+}
\ No newline at end of file
diff --git a/contracts/broadcast/DeployChannelHub.s.sol/11155111/run-latest.json b/contracts/broadcast/DeployChannelHub.s.sol/11155111/run-latest.json
new file mode 100644
index 000000000..5af57197a
--- /dev/null
+++ b/contracts/broadcast/DeployChannelHub.s.sol/11155111/run-latest.json
@@ -0,0 +1,188 @@
+{
+ "transactions": [
+ {
+ "hash": "0x3df2187dc8a50ef62abfeb377318888493042770315492070c4708584dfbf572",
+ "transactionType": "CREATE2",
+ "contractName": "ChannelEngine.channelhub",
+ "contractAddress": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x1bb70c",
+ "input": "0x0000000000000000000000000000000000000000000000000000000000000000608080604052346019576116f0908161001e823930815050f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63a8b4483c14610025575f80fd5b604060031936011261122d5760043567ffffffffffffffff811161122d5760a0600319823603011261122d5760a0820182811067ffffffffffffffff821117611299576040528060040135600681101561122d578252602481013567ffffffffffffffff811161122d5761009f90600436918401016113e1565b602083019081526040830192604483013584526100c96084606083019460648101358652016112ec565b6080820190815260243567ffffffffffffffff811161122d576100f09036906004016113e1565b6100f8611498565b50606081019367ffffffffffffffff855151164603610dc45767ffffffffffffffff82511681519067ffffffffffffffff82511610908115611261575b5015610a085784516040810190601260ff83511611611239574667ffffffffffffffff825116146110ff575b505060208201928351600a8110156103585760041480156110eb575b80156110d7575b80156110c3575b80156110af575b801561109b575b1561105e576080830167ffffffffffffffff815151161561103657515167ffffffffffffffff16461461100e575b6101dc865160a06060820151910151906114e6565b6101f1875160c06080820151910151906114f3565b5f8112610fe65761020190611526565b03610fbe578451600681101561035857600214610f7f575b50610222611498565b5061023c608086510151608060608451015101519061150e565b9061025660c08751015160c060608451015101519061150e565b9351600a81101561035857600281036104b65750509050610275611498565b928051600681101561035857159081156104a0575b811561048a575b8115610475575b501561044d575f8113156104255782526020820152600160408201525f6060820152925b6102d96102d1608086019260018452516115a7565b8551906114f3565b926102ea60208601948551906114f3565b5f81126103fd5760a08601938451156103ab575b50508351905f821361036c575b50506040519284518452516020840152604084015193600685101561035857606067ffffffffffffffff9160c09660408701520151166060840152511515608083015251151560a0820152f35b634e487b7160e01b5f52602160045260245ffd5b610377905191611526565b11610383575f8061030b565b7f2e3b1ec0000000000000000000000000000000000000000000000000000000005f5260045ffd5b6103c36103c9915160a06060820151910151906114e6565b91611526565b036103d5575f806102fe565b7f8f9003ee000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fae0bb491000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610298565b8091505160068110156103585760021490610291565b809150516006811015610358576001149061028a565b6003810361055657505090506104ca611498565b92805160068110156103585715908115610540575b811561052a575b8115610515575b501561044d575f8112156104255782526020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f6104ed565b80915051600681101561035857600214906104e6565b80915051600681101561035857600114906104df565b8061061f5750509050610567611498565b92805160068110156103585715908115610609575b81156105f3575b81156105de575b501561044d576104255760a0835101516105b6576020820152600160408201525f6060820152926102bc565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61058a565b8091505160068110156103585760021490610583565b809150516006811015610358576001149061057c565b600181036107185750509050610633611498565b928051600681101561035857600114908115610702575b81156106ed575b501561044d5761066c845160a06060820151910151906114e6565b8651106106c55761068a82610685836106858a516115a7565b6114f3565b5f81126103fd5761069f60a0865101516115a7565b136103fd5782526020820152600360408201525f6060820152600160a0820152926102bc565b7f7fa0800f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610651565b809150516006811015610358576002149061064a565b6004810361083857505061072a611498565b938051600681101561035857600114908115610822575b811561080d575b501561044d57610425576080016060815101519081156107e55761077a855160ff604060a0830151920151169061161e565b61078c60ff604084510151168461161e565b036105b65760806107a091510151916115a7565b036107bd576020820152600160408201525f6060820152926102bc565b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610748565b8091505160068110156103585760021490610741565b909391929060058103610a83575061084e611498565b948051600681101561035857600114908115610a6d575b8115610a58575b501561044d5761087f60208551016114d9565b600a81101561035857600403610a305767ffffffffffffffff81511667ffffffffffffffff6108b1818751511661155b565b1603610a0857608001916060835101516107e55760a0835101516105b65760a0865101516105b657610425576109e05760606080835101510151906080815101516108fb836115a7565b036107bd575160c00151610916610911836115a7565b61157b565b036109b8576060845101519060608084510151015182039182116109a45760ff6040608061094e61095b9584848b510151169061161e565b955101510151169061161e565b0361097c575f81525f6020820152600160408201525f6060820152926102bc565b7f733d14c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fd916ea0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f7dcd8ffd000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61086c565b8091505160068110156103585760021490610865565b9193909160068103610b3f57505090610a9a611498565b938051600681101561035857600114908115610b29575b8115610b14575b501561044d576104255760a0845101516105b6576080016080815101516107bd576060815101516107e55760c0610af360a0835101516115a7565b91510151036109b8576020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f610ab8565b8091505160068110156103585760021490610ab1565b60078103610bd957505090610b52611498565b938051600681101561035857600114908115610bc3575b8115610bae575b501561044d576104255760a0845101516105b6576080016060815101516107e55760a0815101516105b657516107a060c0608083015192015161157b565b9050516006811015610358576004145f610b70565b8091505160068110156103585760021490610b69565b60088103610e1557505090610bec611498565b938051600681101561035857158015610e01575b15610ce5575050608001805160600151915081156107e55760a0815101516105b6576060845101516107e557610c44845160ff604060a0830151920151169061161e565b610c5660ff604084510151168461161e565b03610cbd57610c8d9060ff6040610c82610c7c8851848460c0830151920151169061165b565b956115a7565b92510151169061165b565b036109b8576080825101516107bd57610caa60a0835101516115a7565b6020820152600460408201525b926102bc565b7f7b208b9d000000000000000000000000000000000000000000000000000000005f5260045ffd5b8051600681101561035857600114908115610dec575b501561044d574667ffffffffffffffff8651511603610dc457610425576060845101519081156107e55760a0855101516105b657608001906060825101516107e557610d55825160ff604060a0830151920151169061161e565b610d6760ff604088510151168361161e565b03610cbd57610d9f610d90610d8a845160ff604060c0830151920151169061165b565b926115a7565b60ff604088510151169061165b565b036109b85751608001516107bd576020820152600160408201525f6060820152610cb7565b7f67525583000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576002145f610cfb565b508051600681101561035857600514610c00565b600903610f5757610e24611498565b948051600681101561035857600403610ed957504667ffffffffffffffff8751511603610dc457610e5860208251016114d9565b600a81101561035857600803610a305767ffffffffffffffff82511667ffffffffffffffff610e8a818451511661155b565b1603610a0857606080915101510151606086510151036107e55760a0855101516105b6576080016060815101516107e5575160a001516105b657610425576109e05760016040820152926102bc565b919250508051600681101561035857600114908115610f42575b501561044d576060845101516107e55760a0845101516105b657608001606081510151156107e5575160a001516105b6576020820152600560408201525f6060820152600160a0820152610cb7565b9050516006811015610358576002145f610ef3565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b5167ffffffffffffffff164211610f96575f610219565b7ff06506c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff019de0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f114a9df4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f26c21ae4000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff60808401515116156101c7577f4c7b586e000000000000000000000000000000000000000000000000000000005f5260045ffd5b508351600a81101561035857600914610199565b508351600a81101561035857600814610192565b508351600a8110156103585760071461018b565b508351600a81101561035857600614610184565b508351600a8110156103585760051461017d565b6020015173ffffffffffffffffffffffffffffffffffffffff168061115b575060ff601291511603611133575b5f80610161565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f92816111f7575b506111c2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461112c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011611231575b81611213602093836112c9565b8101031261122d575160ff8116810361122d57915f611195565b5f80fd5b3d9150611206565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b60608101515167ffffffffffffffff1615915081611281575b505f610135565b67ffffffffffffffff9150608001515116155f61127a565b634e487b7160e01b5f52604160045260245ffd5b60e0810190811067ffffffffffffffff82111761129957604052565b90601f601f19910116810190811067ffffffffffffffff82111761129957604052565b359067ffffffffffffffff8216820361122d57565b91908260e091031261122d57604051611319816112ad565b8092611324816112ec565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361122d576020830152604081013560ff8116810361122d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561122d5780359067ffffffffffffffff821161129957604051926113c06020601f19601f86011601856112c9565b8284526020838301011161122d57815f926020809301838601378301015290565b91906102608382031261122d57604051906113fb826112ad565b8193611406816112ec565b83526020810135600a81101561122d576020840152604081013560408401526114328260608301611301565b6060840152611445826101408301611301565b608084015261022081013567ffffffffffffffff811161122d578261146b91830161138b565b60a08401526102408101359167ffffffffffffffff831161122d5760c092611493920161138b565b910152565b6040519060c0820182811067ffffffffffffffff821117611299576040525f60a0838281528260208201528260408201528260608201528260808201520152565b51600a8110156103585790565b919082018092116109a457565b9190915f83820193841291129080158216911516176109a457565b81810392915f1380158285131691841216176109a457565b5f81126115305790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b67ffffffffffffffff60019116019067ffffffffffffffff82116109a457565b7f800000000000000000000000000000000000000000000000000000000000000081146109a4575f0390565b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81116115d15790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116109a457565b60ff16604d81116109a457600a0a90565b9060ff811660128111611239576012146116575761163e611643916115fc565b61160d565b908181029181830414901517156109a45790565b5090565b9060ff811660128111611239576012146116575761163e61167b916115fc565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166109a45781830514901517156109a4579056fea264697066735822122036f6b0f3261f4d84fa391cd2e29d848110238f6d49d373a5912f2304cae9c86d64736f6c634300081e0033",
+ "nonce": "0x28",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x9712dcbc9f46d075bb90ab9d5cbbdf30195810bb050150b302cb3aaaf0e71bc0",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowWithdrawalEngine.channelhub",
+ "contractAddress": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x124792",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610edc908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8062ea54e714610118576324063eba1461002e575f80fd5b60206003193601126101145760043567ffffffffffffffff81116101145761005a903690600401610c2a565b610062610ced565b90516004811015610100575f19016100d857600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff82116100c45767ffffffffffffffff6100c0921660608201525f608082015260405191829182610ca2565b0390f35b634e487b7160e01b5f52601160045260245ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b5f80fd5b60406003193601126101145760043567ffffffffffffffff811161011457610144903690600401610c2a565b60243567ffffffffffffffff811161011457610164903690600401610b73565b61016c610ced565b5081516004811015610100576003146109dc5767ffffffffffffffff461660608201908067ffffffffffffffff83515116146109b457608083019067ffffffffffffffff825151160361098c5767ffffffffffffffff835116156107a25780516040810190601260ff83511611610964574667ffffffffffffffff8251161461082e575b5050805160a0606082015191015181018091116100c45761021c825160c0608082015191015190610d17565b5f81126108065761022c90610d5e565b036107de57610239610ced565b5060208301928351600a811015610100576006810361052657505061025c610ced565b9184516004811015610100576104fe576060825101516104d6576080825101516104ae5781519160c060a084015193015161029684610d93565b03610486576102c360ff60406102b88551838360608301519201511690610e0a565b935101511684610e0a565b1161045e575160a00151610436576102da90610d93565b60208201526001604082015260016080820152915b825115801590610429575b15610401578251906103126020850192835190610d17565b928051600a81101561010057600603610366575050510361033e576100c0905b60405191829182610ca2565b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092915051600a81101561010057600714610387575b50506100c090610332565b8251036103d95760406103a261039d8451610d32565b610d5e565b910151036103b157818061037c565b7fd9132288000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b50602083015115156102fa565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f06b4cdae000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b90929060070361077a57610538610ced565b92855160048110156101005760011480156107ca575b156100d85767ffffffffffffffff9051166020860190600167ffffffffffffffff835151160167ffffffffffffffff81116100c45767ffffffffffffffff16036107a257602081510151600a811015610100577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0161077a5760a06080825101510151926060815101516104d6576080815101516105f36105ee86610d93565b610d32565b036104ae5760a081510151610436575160c0015161061084610d93565b036107025760608251015160608083510151015111156107525760608082510151015160608351015181039081116100c4576106559060ff6040855101511690610e0a565b61066b60ff604060808551015101511685610e0a565b0361072a5760c08251015160c06060835101510151905f82820392128183128116918313901516176100c4575f81121561070257604060806106cb6106c56106d89660ff856106ba8298610d32565b925101511690610e47565b96610d93565b9351015101511690610e47565b03610486576106ed6105ee6040850151610d93565b8152600360408201525f6080820152916102ef565b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fffda345d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f25e3e1b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156101005760021461054e565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061088a575060ff601291511603610862575b84806101f0565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610926575b506108f1577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461085b577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d60201161095c575b8161094260209383610a50565b81010312610114575160ff811681036101145791876108c4565b3d9150610935565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff821117610a2057604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117610a2057604052565b90601f601f19910116810190811067ffffffffffffffff821117610a2057604052565b359067ffffffffffffffff8216820361011457565b359073ffffffffffffffffffffffffffffffffffffffff8216820361011457565b91908260e091031261011457604051610ac181610a04565b8092610acc81610a73565b8252610ada60208201610a88565b6020830152604081013560ff811681036101145760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f820112156101145780359067ffffffffffffffff8211610a205760405192610b526020601f19601f8601160185610a50565b8284526020838301011161011457815f926020809301838601378301015290565b9190610260838203126101145760405190610b8d82610a04565b8193610b9881610a73565b83526020810135600a81101561011457602084015260408101356040840152610bc48260608301610aa9565b6060840152610bd7826101408301610aa9565b608084015261022081013567ffffffffffffffff81116101145782610bfd918301610b1d565b60a08401526102408101359167ffffffffffffffff83116101145760c092610c259201610b1d565b910152565b91909160a0818403126101145760405190610c4482610a34565b81938135600481101561011457835260208201359067ffffffffffffffff82116101145782610c7c60809492610c2594869401610b73565b602086015260408101356040860152610c9760608201610a73565b606086015201610a88565b91909160a0810192805182526020810151602083015260408101516004811015610100576080918291604085015267ffffffffffffffff606082015116606085015201511515910152565b60405190610cfa82610a34565b5f6080838281528260208201528260408201528260608201520152565b9190915f83820193841291129080158216911516176100c457565b7f800000000000000000000000000000000000000000000000000000000000000081146100c4575f0390565b5f8112610d685790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610dbd5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116100c457565b60ff16604d81116100c457600a0a90565b9060ff81166012811161096457601214610e4357610e2a610e2f91610de8565b610df9565b908181029181830414901517156100c45790565b5090565b9060ff81166012811161096457601214610e4357610e2a610e6791610de8565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166100c45781830514901517156100c4579056fea264697066735822122073585d1c2949228993d38506ffc5f542f9ffb1c023c1893a2f5522e50227b27564736f6c634300081e0033",
+ "nonce": "0x29",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x6e81a9f20bb7b3370a15b6402271a9f8e7eae63184e733c80273c497a4187983",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowDepositEngine.channelhub",
+ "contractAddress": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x11ad7a",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610e55908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80636666e4c0146109095763bbc42f341461002f575f80fd5b604060031936011261085d5760043567ffffffffffffffff811161085d5761005b903690600401610bf4565b60243567ffffffffffffffff811161085d5761007b903690600401610b3d565b610083610cd9565b5081516004811015610343576003146108e15767ffffffffffffffff46169060608101918067ffffffffffffffff84515116146108b957608082019067ffffffffffffffff82515116036108915767ffffffffffffffff8251161561067b5780516040810190601260ff83511611610869574667ffffffffffffffff8251161461072f575b5050805160a06060820151910151810180911161038c57610134825160c0608082015191015190610d09565b5f81126107075761014490610d50565b036106df57610151610cd9565b5060208201928351600a81101561034357600481036104685750909150610176610cd9565b918451600481101561034357610440578051916080606084015193015161019c84610d85565b036104185760a0825101516103f05760c0825101516103c85760ff60406101d26101dd9351838360a08301519201511690610dda565b935101511683610dda565b036103a0576101eb90610d85565b815260016040820152612a3067ffffffffffffffff42160167ffffffffffffffff811161038c5767ffffffffffffffff166060820152600160a0820152915b82511580159061037f575b1561035757825161024c6020850191825190610d09565b928051600a811015610343576004036102a65750505081510361027e5761027a905b60405191829182610c7a565b0390f35b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9290919251600a811015610343576005146102c8575b50505061027a9061026e565b81510361031b576102e36102de60409251610d24565b610d50565b910151036102f3575f80806102bc565b7fb09443e7000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b5060208301511515610235565b634e487b7160e01b5f52601160045260245ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f76ac27ca000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b60050361065357610477610cd9565b92855160048110156103435760011480156106cb575b156106a35767ffffffffffffffff905116916020860192600167ffffffffffffffff855151160167ffffffffffffffff811161038c5767ffffffffffffffff160361067b57602083510151600a811015610343576003190161065357606060808451015101519060808151015161050383610d85565b036104185760c08151015161051f61051a84610d85565b610d24565b036103c85760608151015161062b575160a001516103f057606082510151606080855101510151810390811161038c576105656105799160ff6040865101511690610dda565b9160ff604060808751015101511690610dda565b036106035760a0815101516103f057606060808092510151925101510151908181035f831282808312821692139015161761038c57036105db576105c361051a6040850151610d85565b6020820152600360408201525f60a08201529161022a565b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fff0edb30000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156103435760021461048d565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061078b575060ff601291511603610763575b5f80610108565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610827575b506107f2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461075c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011610861575b8161084360209383610a25565b8101031261085d575160ff8116810361085d57915f6107c5565b5f80fd5b3d9150610836565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b602060031936011261085d5760043567ffffffffffffffff811161085d57610935903690600401610bf4565b61093d610cd9565b9080516004811015610343575f19016106a3576060015167ffffffffffffffff164210156109b157600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff821161038c5767ffffffffffffffff61027a921660808201525f60a082015260405191829182610c7a565b7f2b39d042000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff8211176109f557604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176109f557604052565b90601f601f19910116810190811067ffffffffffffffff8211176109f557604052565b359067ffffffffffffffff8216820361085d57565b91908260e091031261085d57604051610a75816109d9565b8092610a8081610a48565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361085d576020830152604081013560ff8116810361085d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561085d5780359067ffffffffffffffff82116109f55760405192610b1c6020601f19601f8601160185610a25565b8284526020838301011161085d57815f926020809301838601378301015290565b91906102608382031261085d5760405190610b57826109d9565b8193610b6281610a48565b83526020810135600a81101561085d57602084015260408101356040840152610b8e8260608301610a5d565b6060840152610ba1826101408301610a5d565b608084015261022081013567ffffffffffffffff811161085d5782610bc7918301610ae7565b60a08401526102408101359167ffffffffffffffff831161085d5760c092610bef9201610ae7565b910152565b91909160c08184031261085d5760405190610c0e82610a09565b81938135600481101561085d57835260208201359167ffffffffffffffff831161085d57610c4260a0939284938301610b3d565b602085015260408101356040850152610c5d60608201610a48565b6060850152610c6e60808201610a48565b60808501520135910152565b91909160c08101928051825260208101516020830152604081015160048110156103435760a0918291604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff608082015116608085015201511515910152565b60405190610ce682610a09565b5f60a0838281528260208201528260408201528260608201528260808201520152565b9190915f838201938412911290801582169115161761038c57565b7f8000000000000000000000000000000000000000000000000000000000000000811461038c575f0390565b5f8112610d5a5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610daf5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b9060ff16601281116108695760128114610e1b5760120360ff811161038c5760ff16604d811161038c57600a0a9081810291818304149015171561038c5790565b509056fea2646970667358221220fc0a93f7abd0c8aae0f4edd1fab1eef03232af831542ee9ea9f3dcf8d76c3da064736f6c634300081e0033",
+ "nonce": "0x2a",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x5e58e1f709d9ded21112c24523733b843486bf6ae775ffd10d86118a5c947cfe",
+ "transactionType": "CREATE",
+ "contractName": "ECDSAValidator.channelhub",
+ "contractAddress": "0x735eb1026afba78b602da39c6b59eaba95753686",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x880d5",
+ "value": "0x0",
+ "input": "0x608080604052346015576106d6908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63600109bb14610024575f80fd5b346100cc5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100cc5760243567ffffffffffffffff81116100cc576100739036906004016100d0565b9060443567ffffffffffffffff81116100cc576100949036906004016100d0565b6064359173ffffffffffffffffffffffffffffffffffffffff831683036100cc576020946100c4946004356101a0565b604051908152f35b5f80fd5b9181601f840112156100cc5782359167ffffffffffffffff83116100cc57602083818601950101116100cc57565b90601f601f19910116810190811067ffffffffffffffff82111761012157604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161012157601f01601f191660200190565b9291926101768261014e565b9161018460405193846100fe565b8294818452818301116100cc578281602093845f960137010152565b929091949383156102635773ffffffffffffffffffffffffffffffffffffffff85161561023b5761022060806101de6102279561022d99369161016a565b95601f19601f6020604051998a94828601526040808601528051918291826060880152018686015e5f858286010152011681010301601f1981018652856100fe565b369161016a565b9061028b565b1561023757600190565b5f90565b7f4501a919000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe1b97cf8000000000000000000000000000000000000000000000000000000005f5260045ffd5b91825192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156104cc575b806d04ee2d6d415b85acef8100000000600a9210156104b1575b662386f26fc1000081101561049d575b6305f5e10081101561048c575b61271081101561047d575b606481101561046f575b1015610465575b6001850190600a602161033461031e8561014e565b9461032c60405196876100fe565b80865261014e565b97601f19602086019901368a378401015b5f1901917f30313233343536373839616263646566000000000000000000000000000000008282061a83530490811561038057600a90610345565b505073ffffffffffffffffffffffffffffffffffffffff5f9361040c86610415946020610404869b603a604051938492818401967f19457468657265756d205369676e6564204d6573736167653a0a00000000000088525180918486015e83018281019d8e528c8051928391019e8f905e01015f815203601f1981018352826100fe565b5190206104f4565b9094919461052e565b169416841461045c5773ffffffffffffffffffffffffffffffffffffffff9261044d92610444925190206104f4565b9092919261052e565b1614610457575f90565b600190565b50505050600190565b9360010193610309565b606460029104960195610302565b612710600491049601956102f8565b6305f5e100600891049601956102ed565b662386f26fc10000601091049601956102e0565b6d04ee2d6d415b85acef8100000000602091049601956102d0565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f01000000000000000081046102b6565b81519190604183036105245761051d9250602082015190606060408401519301515f1a90610606565b9192909190565b50505f9160029190565b60048110156105d95780610540575050565b60018103610570577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b600281036105a457507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6003146105ae5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610695579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa1561068a575f5173ffffffffffffffffffffffffffffffffffffffff81161561068057905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fea2646970667358221220c8e32dfe4c3317faffb02d4b02fddbb5e01dbc789e117442dd5ec08557786de764736f6c634300081e0033",
+ "nonce": "0x2b",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x6e0b716f9bdb40d3aadbfa2544bf5ec11b39f431736bd19569ade187cb0b7396",
+ "transactionType": "CREATE",
+ "contractName": "ChannelHub",
+ "contractAddress": "0xb7be0e2007ddf320d680942cb9e008f986e74f83",
+ "function": null,
+ "arguments": [
+ "0x735EB1026aFbA78B602dA39C6B59EABa95753686"
+ ],
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x6a626e",
+ "value": "0x0",
+ "input": "0x60a0346100aa57601f61608238819003918201601f19168301916001600160401b038311848410176100ae578084926020946040528339810103126100aa57516001600160a01b0381168082036100aa5760017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00551561009b57608052604051615fbf90816100c382396080518181816111420152613f180152f35b63e6c4247b60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806316b390b11461024457806317536c061461023f578063187576d81461023a5780633115f6301461023557806338a66be21461023057806341b660ef1461022b57806347de477a146102265780635326919814610221578063587675e81461021c5780635a0745b4146102175780635b9acbf9146102125780635dc46a741461020d5780636840dbd2146102085780636898234b146102035780636af820bd146101fe57806371a47141146101f9578063735181f0146101f457806382d3e15d146101ef5780638d0b12a5146101ea57806394191051146101e55780639691b468146101e0578063a5c82680146101db578063b00b6fd6146101d6578063b25a1d38146101d1578063beed9d5f146101cc578063c74a2d10146101c7578063d888ccae146101c2578063dc23f29e146101bd578063dd73d494146101b8578063e617208c146101b3578063ecf3d7e8146101ae578063f4ac51f5146101a9578063f766f8d6146101a4578063ff5bc09e1461019f5763ffa1ad741461019a575f80fd5b612650565b612639565b61249a565b61241f565b61230d565b61226e565b6120f0565b611ef5565b611db5565b611b71565b6119d6565b6116c4565b61165b565b6114ba565b611379565b61135c565b6111cb565b6111ae565b611171565b61112d565b611112565b611026565b61100f565b610fc8565b610fa6565b610f8a565b610f44565b610cfc565b610b13565b610850565b6107ea565b61065e565b6105d8565b6104ca565b6102cb565b9181601f840112156102775782359167ffffffffffffffff8311610277576020838186019501011161027757565b5f80fd5b60643590600282101561027757565b90606060031983011261027757600435916024359067ffffffffffffffff8211610277576102ba91600401610249565b909160443560028110156102775790565b34610277576102d93661028a565b6103986102f1859493945f52600260205260405f2090565b9283546102ff81151561266b565b61035a600286019461032a61031b87546001600160a01b031690565b948560038a019a8b5492613eff565b9591600160068b019a019661034a88546001600160a01b039060081c1690565b926103548c6128a0565b8861405d565b60c061036588614161565b604051809581927f6666e4c000000000000000000000000000000000000000000000000000000000835260048301612a25565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577fba075bd445233f7cad862c72f0343b3503aad9c8e704a2295f122b82abf8e80196610436956080955f9461044b575b50836104146104066104279697546001600160a01b039060081c1690565b92546001600160a01b031690565b9254936104208a6128a0565b908c61428d565b015167ffffffffffffffff1690565b9061044660405192839283612a41565b0390a2005b6104279450946104146104786104069760c03d60c011610481575b6104708183612707565b810190612950565b955050946103e8565b503d610466565b612a36565b6001600160a01b0381160361027757565b6003196060910112610277576004356104b68161048d565b906024356104c38161048d565b9060443590565b6001600160a01b036104db3661049e565b92909116906104eb821515612b9e565b6104f6831515612bcd565b815f52600660205261051c8160405f20906001600160a01b03165f5260205260405f2090565b80549184830180931161059a577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7926001600160a01b03925561055d615810565b61056885823361458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00556040519485521692602090a3005b612bfc565b60206040818301928281528451809452019201905f5b8181106105c25750505090565b82518452602093840193909201916001016105b5565b34610277576020600319360112610277576001600160a01b036004356105fd8161048d565b165f52600160205260405f206040519081602082549182815201915f5260205f20905f5b818110610648576106448561063881870382612707565b6040519182918261059f565b0390f35b8254845260209093019260019283019201610621565b3461027757602060031936011261027757600354600480549190355f5b828410806107e1575b156107d4576106b06106a2610698866131a0565b90549060031b1c90565b5f52600260205260405f2090565b6001810160036106c1825460ff1690565b6106ca81611c00565b146107c2576106d882615b4d565b1561077e57915f8261076961077595600561076f96019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b6001600160a01b03165f52600660205260405f2090565b92015460401c6001600160a01b031690565b6001600160a01b03165f5260205260405f2090565b918254612c10565b9055600360ff19825416179055565b556139c6565b936139c6565b915b919261067b565b505092905061078d9150600455565b8061079457005b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1005b5050926107ce906139c6565b91610777565b92905061078d9150600455565b50818110610684565b34610277575f600319360112610277576020604051620186a08152f35b90816102609103126102775790565b90600319820160e081126102775760c0136102775760049160c4359067ffffffffffffffff82116102775761084d91600401610807565b90565b61085936610816565b906020820191600261086a84612c27565b61087381611c0f565b148015610af8575b8015610ada575b61088b90612c31565b61091a6108a061089b3685612c79565b614780565b916108aa8461483e565b60208401906108b882612ced565b956108d760408701976108ca89612ced565b608089013591858961494b565b60c0826108ff6108f86108ec6107148c612ced565b61073d60808501612ced565b54886149c5565b6040519687928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af493841561048857610a156001600160a01b0394610a2d936109967fb00e209e275d0e1892f1982b34d3f545d1628aebd95322d7ce3585c558f638b498610a1b955f91610aab575b50610985368d612c79565b61098f368a61301c565b908c614b52565b6109c2896109bd6109a685612ced565b6001600160a01b03165f52600160205260405f2090565b615bde565b5060026109ce82612c27565b6109d781611c0f565b03610a325750877f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f4177869620669660405180610a0d89826130ca565b0390a2612ced565b97612ced565b918360405194859416981696836130db565b0390a4005b610a3d600391612c27565b610a4681611c0f565b03610a7b57877f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf98660405180610a0d89826130ca565b877f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc60405180610a0d89826130ca565b610acd915060c03d60c011610ad3575b610ac58183612707565b810190612cf7565b5f61097a565b503d610abb565b5061088b610ae784612c27565b610af081611c0f565b159050610882565b506003610b0484612c27565b610b0d81611c0f565b1461087b565b610b1c36610816565b90610b3d6004610b2e60208501612c27565b610b3781611c0f565b14612c31565b610b4a61089b3683612c79565b9160208201610b5881612ced565b90610b7960408501926080610b6c85612ced565b960135958691868961494b565b610b8b610b858461316b565b86614c6c565b93610b9586614c9c565b15610bdd57505050610bd881610bcc7f471c4ebe4e57d25ef7117e141caac31c6b98f067b8098a7a7bbd38f637c2f9809386614cf9565b604051918291826130ca565b0390a3005b610c259060c085610bf18897959697614161565b60405194859283927fbbc42f3400000000000000000000000000000000000000000000000000000000845260048401613175565b038173728904e52308213ba61c90ef49f34c18fbda9e115af48015610488577fede7867afa7cdb9c443667efd8244d98bf9df1dce68e60dc94dca6605125ca7695610bd895610c9a945f93610ca3575b50610c82610c8891612ced565b91612ced565b91610c93368761301c565b8a8a61428d565b610bcc846131ef565b610c88919350610cc4610c829160c03d60c011610481576104708183612707565b939150610c75565b90604060031983011261027757600435916024359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610d0a36610ccc565b610d1b6009610b2e60208401612c27565b610d376001610d31845f525f60205260405f2090565b0161323d565b610dfd610d4e60208301516001600160a01b031690565b9161071460c060408301610d7a610d6c82516001600160a01b031690565b608086015190888a8c61494b565b610de2610ddb610dc4610d8d368b61301c565b9586946101408c018d8d610da08361316b565b67ffffffffffffffff1646149d8e610eb7575b50505050516001600160a01b031690565b6060840151602001516001600160a01b031661073d565b54896149c5565b6040519586928392632a2d120f60e21b8452600484016132c9565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857610e2f935f93610e96575b5086614b52565b15610e65576104467f9a6f675cc94b83b55f1ecc0876affd4332a30c92e6faa2aca0199b1b6df922c391604051918291826130ca565b6104467f7b20773c41402791c5f18914dbbeacad38b1ebcc4c55d8eb3bfe0a4cde26c82691604051918291826130ca565b610eb091935060c03d60c011610ad357610ac58183612707565b915f610e28565b610edb610f1092610ecc610f15963690612f3c565b60608d01526060369101612f3c565b60808b0152610ee86132b5565b60a08b0152610ef56132b5565b8b8b01526001600160a01b03165f52600160205260405f2090565b615c88565b505f8d8d82610db3565b600319604091011261027757600435610f378161048d565b9060243561084d8161048d565b34610277576020610f816001600160a01b03610f5f36610f1f565b91165f526006835260405f20906001600160a01b03165f5260205260405f2090565b54604051908152f35b34610277575f600319360112610277576020604051612a308152f35b3461027757604060031936011261027757610644610638602435600435613373565b610fda610fd436610ccc565b9061342d565b005b60606003198201126102775760043591602435916044359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610fda61102036610fdc565b9161378a565b34610277576020600319360112610277576001600160a01b0360043561104b8161048d565b165f52600160205261105f60405f20615b05565b5f905f5b81518110156110ff5761109161108a61107c838561335f565b515f525f60205260405f2090565b5460ff1690565b61109a816121c1565b600381141590816110ea575b506110b4575b600101611063565b916110c78184600193106110cf576139c6565b9290506110ac565b6110d9858561335f565b516110e4828661335f565b526139c6565b600591506110f7816121c1565b14155f6110a6565b506106449181526040519182918261059f565b34610277575f60031936011261027757602060405160408152f35b34610277575f600319360112610277576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610277576020610f816001600160a01b0361118c36610f1f565b91165f526008835260405f20906001600160a01b03165f5260205260405f2090565b34610277575f600319360112610277576020600454604051908152f35b34610277576112556111dc3661028a565b929391906111f2855f52600560205260405f2090565b9182549261120184151561266b565b600281019060a061122261121c84546001600160a01b031690565b8a615053565b604051809881927f24063eba000000000000000000000000000000000000000000000000000000008352600483016139d4565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4958615610488575f9661132b575b50600181015460081c6001600160a01b0316968792546112a2906001600160a01b031690565b809581956003850154976112b7928992613eff565b9a9190946006019a6112c88c6128a0565b956112d3968b61405d565b846112dd876128a0565b6112e795896150bb565b6060015167ffffffffffffffff166040519182916113059183612a41565b037fb8568a1f475f3c76759a620e08a653d28348c5c09e2e0bc91d533339801fefd891a2005b61134e91965060a03d60a011611355575b6113468183612707565b8101906136bc565b945f61127c565b503d61133c565b34610277575f600319360112610277576020604051620151808152f35b61143661138536610ccc565b6113a661139760208395949501612c27565b6113a081611c0f565b15612c31565b6113bc6001610d31855f525f60205260405f2090565b9060c08161141b6114146108ec6107146113e060208901516001600160a01b031690565b6114078b8a60408101938960806113fe87516001600160a01b031690565b9301519361494b565b516001600160a01b031690565b54876149c5565b6040519586928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc9361044693610bcc925f92611499575b50611492368561301c565b9087614b52565b6114b391925060c03d60c011610ad357610ac58183612707565b905f611487565b34610277576114c836610816565b906114da6006610b2e60208501612c27565b6114e761089b3683612c79565b91602082016114f581612ced565b9061150960408501926080610b6c85612ced565b611515610b858461316b565b9361151f86614c9c565b1561155657505050610bd881610bcc7f587faad1bcd589ce902468251883e1976a645af8563c773eed7356d78433210c9386614cf9565b6115a59060a08561157261156c87989697612ced565b89615053565b60405194859283927eea54e700000000000000000000000000000000000000000000000000000000845260048401613773565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af48015610488577f17eb0a6bd5a0de45d1029ce3444941070e149df35b22176fc439f930f73c09f795610bd895610bcc945f93611614575b50610c8261160291612ced565b9161160d368761301c565b8a8a6150bb565b611602919350611635610c829160a03d60a011611355576113468183612707565b9391506115f5565b6024359060ff8216820361027757565b359060ff8216820361027757565b34610277576040600319360112610277576001600160a01b036116a96004356116838161048d565b8261168c61163d565b91165f52600760205260405f209060ff165f5260205260405f2090565b541660405180916001600160a01b0360208301911682520390f35b60806003193601126102775760043560243567ffffffffffffffff8111610277576116f3903690600401610807565b60443567ffffffffffffffff811161027757611713903690600401610249565b919061171d61027b565b9061172f855f525f60205260405f2090565b9161173c6001840161323d565b9161176661174b855460ff1690565b611754816121c1565b600181149081156119c2575b506139e5565b86611773600586016128a0565b916117b46117808861316b565b67ffffffffffffffff6117ab61179e875167ffffffffffffffff1690565b67ffffffffffffffff1690565b91161015613a14565b60208501516001600160a01b0316976117d760408701516001600160a01b031690565b9367ffffffffffffffff6117ff61179e6117f08c61316b565b935167ffffffffffffffff1690565b9116116118c3575b94611867889795857f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a9b6118616118859760149d61185561187c996118959c6118b49f60808c015192613eff565b9391949092369061301c565b9061405d565b845460ff191660021785555163ffffffff1690565b63ffffffff1690565b67ffffffffffffffff4216613a43565b9301805467ffffffffffffffff191667ffffffffffffffff8516179055565b61044660405192839283613a65565b909296959397946118df61190a9389888a60808601519361494b565b60c08761141b6119036108ec8c6001600160a01b03165f52600660205260405f2090565b548d6149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4938415610488577f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a99896118b4986118618e8c61185560149f976118679861187c9b61198a6118959f6118859f5f916119a3575b508d611983368961301c565b9089615443565b9a9f5050995050509750509b5095509597985050611807565b6119bc915060c03d60c011610ad357610ac58183612707565b5f611977565b600491506119cf816121c1565b145f611760565b34610277576080600319360112610277576004356119f38161048d565b6119fb61163d565b90604435611a088161048d565b60643567ffffffffffffffff811161027757611b4a6001600160a01b0392611b22611a6396611b07611b02611a4289973690600401610249565b60ff85169a91611afc90611a578d1515613a8d565b8b89169d8e1515612b9e565b611abf8785611ab9611aad611aad611aa085611a90866001600160a01b03165f52600760205260405f2090565b9060ff165f5260205260405f2090565b546001600160a01b031690565b6001600160a01b031690565b15613abc565b6040805160ff891660208201526001600160a01b038b169181019190915246606080830191909152815292611af5608085612707565b3691612fcb565b9061564c565b613b01565b611a90856001600160a01b03165f52600760205260405f2090565b906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b167f2366b94a706a0cfc2dca2fe8be9410b6fba2db75e3e9d3f03b3c2fb0b051efad5f80a4005b611b91611b7d36610ccc565b6113a66003610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf9869361044693610bcc925f926114995750611492368561301c565b634e487b7160e01b5f52602160045260245ffd5b60041115611c0a57565b611bec565b600a1115611c0a57565b90600a821015611c0a5752565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b61084d9167ffffffffffffffff8251168152611c6f60208301516020830190611c19565b60408201516040820152611cdd6060830151606083019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b608082810151805167ffffffffffffffff1661014084015260208101516001600160a01b0316610160840152604081015160ff1661018084015260608101516101a0840152908101516101c083015260a08101516101e083015260c0015161020082015260c0611d5f60a0840151610260610220850152610260840190611c26565b92015190610240818403910152611c26565b929367ffffffffffffffff60c09561084d98979482948752611d9281611c00565b602087015216604085015216606083015260808201528160a08201520190611c4b565b3461027757602060031936011261027757600435611dd1613b66565b505f52600260205260405f20611de561272a565b9080548252610644600182015491611e31611e21611e038560ff1690565b94611e12602088019687613baa565b60081c6001600160a01b031690565b6001600160a01b03166040860152565b611e58611e4860028301546001600160a01b031690565b6001600160a01b03166060860152565b60038101546080850152600481015467ffffffffffffffff811660a086019081529490611e90905b60401c67ffffffffffffffff1690565b67ffffffffffffffff1660c0820190815291611ee46117f0611ec0600660058501549460e08701958652016128a0565b93610100810194855251965197611ed689611c00565b5167ffffffffffffffff1690565b905191519260405196879687611d71565b3461027757611f0336610816565b611f146008610b2e60208401612c27565b80611f89611f2561089b3686612c79565b936020810160c0611f3582612ced565b91611f546040850193611f4785612ced565b608087013591898c61494b565b610de2610ddb610dc4610714611f6a368b61301c565b9687958d8a611f7882614c9c565b9d8e15612052575b50505050612ced565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857611fc6935f9361202d575b50611fc0903690612c79565b86614b52565b15611ffc576104467f3142fb397e715d80415dff7b527bf1c451def4675da6e1199ee1b4588e3f630a91604051918291826130ca565b6104467f26afbcb9eb52c21f42eb9cfe8f263718ffb65afbf84abe8ad8cce2acfb2242b891604051918291826130ca565b611fc091935061204b9060c03d60c011610ad357610ac58183612707565b9290611fb4565b6120aa936120876109a6926120696109bd9561483e565b8c606061207a366101408501612f3c565b9101526060369101612f3c565b60808c01526120946132b5565b60a08c01526120a16132b5565b8c8c0152612ced565b505f8d8a8e611f80565b9160a09367ffffffffffffffff9161084d97969385526120d381611c00565b602085015216604083015260608201528160808201520190611c4b565b346102775760206003193601126102775760043561210c613b66565b505f52600560205260405f2061212061273c565b908054825261064460018201549161213e611e21611e038560ff1690565b612155611e4860028301546001600160a01b031690565b60038101546080850152600481015467ffffffffffffffff1667ffffffffffffffff1660a08501908152936121b061219b600660058501549460c08501958652016128a0565b9160e0810192835251945195611ed687611c00565b9151905191604051958695866120b4565b60061115611c0a57565b906006821015611c0a5752565b9192612250610120946121f285612263959a99989a6121cb565b602085019060a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b61014060e0840152610140830190611c4b565b946101008201520152565b34610277576020600319360112610277576004355f60a0604051612291816126ae565b82815282602082015282604082015282606082015282608082015201526122b6613b66565b505f525f6020526122c960405f20613bc2565b80516122d4816121c1565b61064460208301519260408101519060606122fd61179e608084015167ffffffffffffffff1690565b91015191604051958695866121d8565b346102775761231b3661049e565b90916123316001600160a01b0382161515612b9e565b61233c821515612bcd565b335f5260066020526123628360405f20906001600160a01b03165f5260205260405f2090565b54908282106123f75782820391821161059a578383916123b7936123b18361239b336001600160a01b03165f52600660205260405f2090565b906001600160a01b03165f5260205260405f2090565b55614d9d565b7fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb6001600160a01b0360405193169280610bd83394829190602083019252565b7ff4d678b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b61243f61242b36610ccc565b6113a66002610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f417786962066969361044693610bcc925f926114995750611492368561301c565b34610277576124a836610f1f565b6124b0615810565b6001600160a01b038116916124c6831515612b9e565b6001600160a01b036124ed8261239b336001600160a01b03165f52600860205260405f2090565b54916124fa831515612bcd565b5f61251a8261239b336001600160a01b03165f52600860205260405f2090565b55169181836125945761253d915f808080858a5af1612537613c20565b50613c4f565b60405190815233907f7b8d70738154be94a9a068a6d2f5dd8cfc65c52855859dc8f47de1ff185f8b5590602090a4610fda60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b50506040517fa9059cbb000000000000000000000000000000000000000000000000000000005f52836004528160245260205f60448180875af160015f511481161561261a575b60409190915261253d577f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b03821660045260245ffd5b6001811516612630573d15843b151516166125db565b503d5f823e3d90fd5b3461027757610fda61264a36610fdc565b91613c90565b34610277575f60031936011261027757602060405160018152f35b1561267257565b7fc60f1e78000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176126ca57604052565b61269a565b60e0810190811067ffffffffffffffff8211176126ca57604052565b60a0810190811067ffffffffffffffff8211176126ca57604052565b90601f601f19910116810190811067ffffffffffffffff8211176126ca57604052565b6040519061273a61012083612707565b565b6040519061273a61010083612707565b6040519061273a60e083612707565b90604051612768816126cf565b60c0600482946127a660ff825467ffffffffffffffff811687526001600160a01b03808260401c1616602088015260e01c16604086019060ff169052565b6001810154606085015260028101546080850152600381015460a08501520154910152565b90600182811c921680156127f9575b60208310146127e557565b634e487b7160e01b5f52602260045260245ffd5b91607f16916127da565b5f9291815491612812836127cb565b8083529260018116908115612867575060011461282e57505050565b5f9081526020812093945091925b83831061284d575060209250010190565b60018160209294939454838587010152019101919061283c565b9050602094955060ff1991509291921683830152151560051b010190565b9061273a6128999260405193848092612803565b0383612707565b906040516128ad816126cf565b809260ff815467ffffffffffffffff8116845260401c1690600a821015611c0a57600d61291f9160c0936020860152600181015460408601526128f26002820161275b565b60608601526129036007820161275b565b6080860152612914600c8201612885565b60a086015201612885565b910152565b5190600482101561027757565b67ffffffffffffffff81160361027757565b5190811515820361027757565b908160c0910312610277576129b860a06040519261296d846126ae565b805184526020810151602085015261298760408201612924565b6040850152606081015161299a81612931565b606085015260808101516129ad81612931565b608085015201612943565b60a082015290565b9081516129cc81611c00565b815260a0806129ea602085015160c0602086015260c0850190611c4b565b936040810151604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff6080820151166080850152015191015290565b90602061084d9281815201906129c0565b6040513d5f823e3d90fd5b92916020612b8d61273a9360408752612a75815467ffffffffffffffff811660408a015260ff60608a019160401c16611c19565b60018101546080880152600281015467ffffffffffffffff811660a0890152604081901c6001600160a01b031660c089015260e090811c60ff16908801526003810154610100880152600481015461012088015260058101546101408801526006810154610160880152600781015467ffffffffffffffff8116610180890152604081901c6001600160a01b03166101a089015260e01c60ff166101c088015260088101546101e08801526009810154610200880152600a810154610220880152600b81015461024088015261026080880152600d612b5b6102a08901600c8401612803565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0898403016102808a015201612803565b94019067ffffffffffffffff169052565b15612ba557565b7fe6c4247b000000000000000000000000000000000000000000000000000000005f5260045ffd5b15612bd457565b7f69640e72000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b9190820180921161059a57565b600a111561027757565b3561084d81612c1d565b15612c3857565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b63ffffffff81160361027757565b359061273a82612931565b91908260c091031261027757604051612c91816126ae565b60a08082948035612ca181612c60565b84526020810135612cb18161048d565b60208501526040810135612cc48161048d565b60408501526060810135612cd781612931565b6060850152608081013560808501520135910152565b3561084d8161048d565b908160c09103126102775760405190612d0f826126ae565b805182526020810151602083015260408101516006811015610277576129b89160a09160408501526060810151612d4581612931565b60608501526129ad60808201612943565b90612d628183516121cb565b608067ffffffffffffffff81612d87602086015160a0602087015260a0860190611c4b565b94604081015160408601526060810151606086015201511691015290565b359061273a82612c1d565b60c0809167ffffffffffffffff8135612dc881612931565b1684526001600160a01b036020820135612de18161048d565b16602085015260ff612df56040830161164d565b166040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b9035601e198236030181121561027757016020813591019167ffffffffffffffff821161027757813603831361027757565b601f8260209493601f1993818652868601375f8582860101520116010190565b61084d9167ffffffffffffffff8235612e8a81612931565b168152612ea86020830135612e9e81612c1d565b6020830190611c19565b60408201356040820152612ec26060820160608401612db0565b612ed461014082016101408401612db0565b612f08612efc612ee8610220850185612e20565b610260610220860152610260850191612e52565b92610240810190612e20565b91610240818503910152612e52565b9091612f2e61084d93604084526040840190612d56565b916020818403910152612e72565b91908260e091031261027757604051612f54816126cf565b60c08082948035612f6481612931565b84526020810135612f748161048d565b6020850152612f856040820161164d565b6040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b67ffffffffffffffff81116126ca57601f01601f191660200190565b929192612fd782612faf565b91612fe56040519384612707565b829481845281830111610277578281602093845f960137010152565b9080601f830112156102775781602061084d93359101612fcb565b919091610260818403126102775761303261274c565b9261303c82612c6e565b845261304a60208301612da5565b6020850152604082013560408501526130668160608401612f3c565b6060850152613079816101408401612f3c565b608085015261022082013567ffffffffffffffff8111610277578161309f918401613001565b60a085015261024082013567ffffffffffffffff8111610277576130c39201613001565b60c0830152565b90602061084d928181520190612e72565b60e09060a061084d949363ffffffff81356130f581612c60565b1683526001600160a01b03602082013561310e8161048d565b1660208401526001600160a01b03604082013561312a8161048d565b16604084015267ffffffffffffffff606082013561314781612931565b16606084015260808101356080840152013560a08201528160c08201520190612e72565b3561084d81612931565b9091612f2e61084d936040845260408401906129c0565b634e487b7160e01b5f52603260045260245ffd5b6003548110156131b85760035f5260205f2001905f90565b61318c565b80548210156131b8575f5260205f2001905f90565b916131eb9183549060031b91821b915f19901b19161790565b9055565b600354680100000000000000008110156126ca57600181016003556003548110156131b85760035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b0155565b9060405161324a816126ae565b60a0600382946001600160a01b03815463ffffffff8116865260201c1660208501526132a467ffffffffffffffff60018301546001600160a01b03808216166040880152851c16606086019067ffffffffffffffff169052565b600281015460808501520154910152565b604051906132c4602083612707565b5f8252565b90916132e061084d93604084526040840190612d56565b916020818403910152611c4b565b67ffffffffffffffff81116126ca5760051b60200190565b60405190613315602083612707565b5f808352366020840137565b9061332b826132ee565b6133386040519182612707565b828152601f1961334882946132ee565b0190602036910137565b9190820391821161059a57565b80518210156131b85760209160051b010190565b9190600354908084029380850482149015171561059a57818410156133f75783019081841161059a578082116133ef575b506133b76133b28483613352565b613321565b92805b8281106133c657505050565b806133d56106986001936131a0565b6133e86133e28584613352565b8861335f565b52016133ba565b90505f6133a4565b5050905061084d613306565b906006811015611c0a5760ff60ff198354169116179055565b90602061084d928181520190611c4b565b9061343f825f525f60205260405f2090565b61344b6001820161323d565b91613457825460ff1690565b9184613465600583016128a0565b91604086019261347c84516001600160a01b031690565b91600261349360208a01516001600160a01b031690565b9761349d816121c1565b1480613654575b6135995750506134e16108f86108ec6107146134fc9661140760c0978a978c898f6080906134d96001610b2e60208601612c27565b01519361494b565b6040519384928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4801561048857610f10613573946109a688937f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a898613566965f92613578575b5061355f368961301c565b9086614b52565b50604051918291826130ca565b0390a2565b61359291925060c03d60c011610ad357610ac58183612707565b905f613554565b7f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a89750613573969195506136479450610f10926135fe6014836135e66109a695600360ff19825416179055565b5f60138201550167ffffffffffffffff198154169055565b606087016136278151606061361d60208301516001600160a01b031690565b9101519086614d9d565b5160a061363e60208301516001600160a01b031690565b91015191614d9d565b506040519182918261341c565b5061366d61179e601483015467ffffffffffffffff1690565b42116134a4565b1561367b57565b7fdb1ea1ac000000000000000000000000000000000000000000000000000000005f5260045ffd5b906136ad81611c00565b60ff60ff198354169116179055565b908160a0910312610277576137116080604051926136d9846126eb565b80518452602081015160208501526136f360408201612924565b6040850152606081015161370681612931565b606085015201612943565b608082015290565b90815161372581611c00565b815260806001600160a01b038161374b602086015160a0602087015260a0860190611c4b565b946040810151604086015267ffffffffffffffff606082015116606086015201511691015290565b9091612f2e61084d93604084526040840190613719565b9161379483614c9c565b613959576137aa825f52600560205260405f2090565b6137b684825414613674565b60018101918254916137d2836001600160a01b039060081c1690565b9360026137f26137eb828501546001600160a01b031690565b9560ff1690565b6137fb81611c00565b1480613939575b6138bf5750600361383b9161381e6007610b2e60208701612c27565b019261382e84548287868b61494b565b60a0836115728389615053565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4908115610488577f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d19561389995610bcc945f9461389e575b50549261160d368761301c565b0390a3565b6138b891945060a03d60a011611355576113468183612707565b925f61388c565b7f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d1945090613899936138fb610bcc93600360ff19825416179055565b613933600d60058401935f855495556139226004820167ffffffffffffffff198154169055565b015460401c6001600160a01b031690565b90614d9d565b5061395261179e600484015467ffffffffffffffff1690565b4211613802565b6138997f6d0cf3d243d63f08f50db493a8af34b27d4e3bc9ec4098e82700abfeffe2d49891610bcc613992865f525f60205260405f2090565b6139be60026139af60018401546001600160a01b039060201c1690565b9201546001600160a01b031690565b908388615025565b5f19811461059a5760010190565b90602061084d928181520190613719565b156139ec57565b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613a1b57565b7f7d957361000000000000000000000000000000000000000000000000000000005f5260045ffd5b9067ffffffffffffffff8091169116019067ffffffffffffffff821161059a57565b9067ffffffffffffffff613a86602092959495604085526040850190612e72565b9416910152565b15613a9457565b7f06ee4dcd000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613ac5575050565b906001600160a01b0360ff927f0bcc40f3000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b15613b0857565b7fc1606c2f000000000000000000000000000000000000000000000000000000005f5260045ffd5b60405190613b3d826126cf565b5f60c0838281528260208201528260408201528260608201528260808201528260a08201520152565b60405190613b73826126cf565b606060c0835f81525f60208201525f6040820152613b8f613b30565b83820152613b9b613b30565b60808201528260a08201520152565b613bb382611c00565b52565b6006821015611c0a5752565b90604051613bcf816126eb565b608067ffffffffffffffff60148395613bec60ff82541686613bb6565b613bf86001820161323d565b6020860152613c09600582016128a0565b604086015260138101546060860152015416910152565b3d15613c4a573d90613c3182612faf565b91613c3f6040519384612707565b82523d5f602084013e565b606090565b15613c58575050565b6001600160a01b03907fa5b05eec000000000000000000000000000000000000000000000000000000005f521660045260245260445ffd5b91613c9a83614c9c565b613e8157613cb0825f52600260205260405f2090565b613cbc84825414613674565b6001810191825491613cd8836001600160a01b039060081c1690565b936002613cf16137eb828501546001600160a01b031690565b613cfa81611c00565b1480613e5e575b613de457506003613d6591613d1d6005610b2e60208701612c27565b0192613d2d84548287868b61494b565b60c083610bf1613d5e613d51856001600160a01b03165f52600660205260405f2090565b61073d6101608501612ced565b5489614206565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e9561389995610bcc945f94613dc3575b505492610c93368761301c565b613ddd91945060c03d60c011610481576104708183612707565b925f613db6565b7f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e94509061389993613e20610bcc93600360ff19825416179055565b613933600d60058401935f85549555613922600482017fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff8154169055565b506004820154613e7a9060401c67ffffffffffffffff1661179e565b4211613d01565b6138997f32e24720f56fd5a7f4cb219d7ff3278ae95196e79c85b5801395894a6f53466c91610bcc613992865f525f60205260405f2090565b15613ec3575050565b906001600160a01b0360ff927f577f5940000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b93929190918215613fd157843560f81c9081613f4c57507f000000000000000000000000000000000000000000000000000000000000000094600101925f19019150613f489050565b9091565b600180915f97939594975060ff86161c1603613fa957613f9d83613f8b611aa0613f4896611a908a6001600160a01b03165f52600760205260405f2090565b966001600160a01b0388161515613eba565b600101915f1990910190565b7f1a9073b4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fac241e11000000000000000000000000000000000000000000000000000000005f5260045ffd5b805191908290602001825e015f815290565b60021115611c0a57565b90816020910312610277575190565b939260609361404f6001600160a01b0394613a86949998998852608060208901526080880190611c26565b918683036040880152612e52565b906001600160a01b03929560209761409195996140c861407f61410d95615887565b6140ba604051998a928e840190613ff9565b7f6368616c6c656e67650000000000000000000000000000000000000000000000815260090190565b03601f198101895288612707565b6140d18161400b565b61415a57505b604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b0392165afa80156104885761273a915f9161412b575b501515613b01565b61414d915060203d602011614153575b6141458183612707565b810190614015565b5f614123565b503d61413b565b90506140d7565b5f6040519161416f836126ae565b81835261420260208401614181613b66565b81526141f46040860191858352611e806004606089019288845260808a01958987526141bc60a08c01998b8b525f52600260205260405f2090565b9160ff6001840154166141ce81611c00565b8c526141dc600684016128a0565b90526005820154905201549167ffffffffffffffff83165b67ffffffffffffffff169052565b5290565b9060405191614214836126ae565b5f835261420260208401614226613b66565b81526141f460408601915f8352611e80600460608901925f845260808a01955f87526141bc60a08c01995f8b525f52600260205260405f2090565b7f8000000000000000000000000000000000000000000000000000000000000000811461059a575f0390565b6020939291614329919796976142ab815f52600260205260405f2090565b976040860180516142bb81611c00565b6142c481611c00565b61450a575b5089888660a08901956142dc8751151590565b6144f6575b5050505050506142fc606085015167ffffffffffffffff1690565b67ffffffffffffffff81166144cb575b50608084015167ffffffffffffffff168061447c575b5051151590565b1561446357608001518201516001600160a01b031680935b8251905f82131561442357614363915061435b8451615ad0565b928391614527565b61437260058601918254612c10565b90555b0180515f8113156143d15750916143b060059261239b6143986143c69651615ad0565b966001600160a01b03165f52600660205260405f2090565b6143bb858254613352565b905501918254612c10565b90555b61273a61467e565b9290505f83126143e5575b505050506143c9565b61440260059261239b6143986143fd61441897614261565b615ad0565b61440d858254612c10565b905501918254613352565b90555f8080806143dc565b5f8212614433575b505050614375565b6144426143fd61444a93614261565b928391614d9d565b61445960058601918254613352565b9055825f8061442b565b50600d84015460401c6001600160a01b03168093614341565b6144c59060048901907fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff6fffffffffffffffff000000000000000083549260401b169116179055565b5f614322565b6144f090600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f61430c565b6144ff956159a2565b5f80898886836142e1565b614521905161451881611c00565b60018b016136a3565b5f6142c9565b9061453a9291614535615810565b61458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b1561456757565b7fd2ade556000000000000000000000000000000000000000000000000000000005f5260045ffd5b908215614679576001600160a01b0316918215801561466a576145b3823414614560565b156145bd57505050565b6001600160a01b03604051927f23b872dd000000000000000000000000000000000000000000000000000000005f52166004523060245260445260205f60648180865af160015f5114811615614654575b6040919091525f606052156146205750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b031660045260245ffd5b6001811516612630573d15833b1515161661460e565b6146743415614560565b6145b3565b505050565b6003546004545f5b82821080614776575b1561476b576146a36106a2610698846131a0565b6001810160036146b4825460ff1690565b6146bd81611c00565b14614759576146cb82615b4d565b1561471657915f8261076961470d95600561470796019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b916139c6565b915b9190614686565b5050915061472390600455565b8061472b5750565b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1565b505090614765906139c6565b9161470f565b915061472390600455565b506040811061468f565b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f01000000000000000000000000000000000000000000000000000000000000009160405161482760208201809360a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b60c0815261483660e082612707565b519020161790565b602081013561484c8161048d565b6001600160a01b03811690614862821515612b9e565b6040830135906148718261048d565b6148986001600160a01b0383169261488a841515612b9e565b6148938361048d565b61048d565b6148a18161048d565b5081146148ed575063ffffffff6201518091356148bd81612c60565b16106148c557565b7f0596b15b000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fabfa558d000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b903590601e1981360301821215610277570180359067ffffffffffffffff82116102775760200191813603831361027757565b929161273a9461497c61498b92614971838761496b610220890189614918565b90613eff565b90878a949394615b81565b8361496b610240850185614918565b92909194615b81565b604051906149a1826126eb565b5f6080838281526149b0613b66565b60208201528260408201528260608201520152565b90601467ffffffffffffffff916149da614994565b935f525f60205260405f20906149f460ff83541686613bb6565b614a00600583016128a0565b6020860152601382015460408601526060850152015416608082015290565b9060a060039163ffffffff8151167fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000008554161784556001600160a01b036020820151167fffffffffffffffff0000000000000000000000000000000000000000ffffffff77ffffffffffffffffffffffffffffffffffffffff0000000086549260201b169116178455614b4160018501614aef614ac660408501516001600160a01b031690565b82906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b606083015181547fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff1660a09190911b7bffffffffffffffff000000000000000000000000000000000000000016179055565b608081015160028501550151910155565b92614b8e81614bde9460a094614b6f885f525f60205260405f2090565b97614b7b895460ff1690565b614b84816121c1565b15614c5a57615443565b604081018051614b9d816121c1565b614ba6816121c1565b151580614c2f575b614c15575b50601484018054606083015167ffffffffffffffff9081169116819003614bed575b50500151151590565b614be55750565b60135f910155565b614c0e919067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f80614bd5565b614c299051614c23816121c1565b85613403565b5f614bb3565b50845460ff16815190614c41826121c1565b614c4a826121c1565b614c53816121c1565b1415614bae565b614c678260018b01614a1f565b615443565b9067ffffffffffffffff604051916020830193845216604082015260408152614c96606082612707565b51902090565b805f525f60205260ff60405f2054166006811015611c0a578015908115614ce5575b50614ce0575f525f60205267ffffffffffffffff600760405f20015416461490565b505f90565b60059150614cf2816121c1565b145f614cbe565b90614d3b91614d146001610d31835f525f60205260405f2090565b60c0836108ff614d346108ec61071460408701516001600160a01b031690565b54856149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af49283156104885761273a945f94614d78575b50614d7290369061301c565b91614b52565b614d72919450614d969060c03d60c011610ad357610ac58183612707565b9390614d66565b9061453a9291614dab615810565b9190918115614679576001600160a01b0383169283614e4f576001600160a01b038216925f8080808488620186a0f1614de2613c20565b5015614def575050505050565b614e326138999261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614e3d828254612c10565b90556040519081529081906020820190565b6040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481885afa908115610488575f91615006575b506040517fa9059cbb00000000000000000000000000000000000000000000000000000000602082019081526001600160a01b0385166024830152604480830187905282525f91829190614ee7606482612707565b51908286620186a0f190614ef9613c20565b506040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526020816024818a5afa9182156104885786915f93614fe5575b5083614fda575b83614fc6575b50505015614f5b575b50505050565b81614fa46001600160a01b039261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614faf858254612c10565b90556040519384521691602090a35f808080614f55565b614fd1929350613352565b145f8481614f4c565b818110159350614f46565b614fff91935060203d602011614153576141458183612707565b915f614f3f565b61501f915060203d602011614153576141458183612707565b5f614e92565b909192614d14614d3b946150456001610d31865f525f60205260405f2090565b92608084015191868661494b565b906001600160a01b0390615065614994565b925f52600560205267ffffffffffffffff600460405f2060ff60018201541661508d81611c00565b865261509b600682016128a0565b602087015260058101546040870152015416606084015216608082015290565b6020939291614329919796976150d9815f52600560205260405f2090565b976040860180516150e981611c00565b6150f281611c00565b615179575b50898886608089019561510a8751151590565b615165575b50505050505061512a606085015167ffffffffffffffff1690565b67ffffffffffffffff8116615140575051151590565b6144c590600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b61516e95615d2e565b5f808988868361510f565b615187905161451881611c00565b5f6150f7565b90600a811015611c0a577fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff68ff000000000000000083549260401b169116179055565b9060c060049161520367ffffffffffffffff825116859067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015184546040808401517fffffff000000000000000000000000000000000000000000ffffffffffffffff90921692901b7bffffffffffffffffffffffffffffffffffffffff0000000000000000169190911760e09190911b7cff0000000000000000000000000000000000000000000000000000000016178455606081015160018501556080810151600285015560a081015160038501550151910155565b601f82116152b357505050565b5f5260205f20906020601f840160051c830193106152eb575b601f0160051c01905b8181106152e0575050565b5f81556001016152d5565b90915081906152cc565b919091825167ffffffffffffffff81116126ca5761531d8161531784546127cb565b846152a6565b6020601f82116001146153585781906131eb9394955f9261534d575b50508160011b915f199060031b1c19161790565b015190505f80615339565b601f1982169061536b845f5260205f2090565b915f5b8181106153a55750958360019596971061538d575b505050811b019055565b01515f1960f88460031b161c191690555f8080615383565b9192602060018192868b01518155019401920161536e565b8151815467ffffffffffffffff191667ffffffffffffffff91909116178155602082015191600a831015611c0a5760c0600d916153fd61273a958561518d565b604081015160018501556154186060820151600286016151d0565b6154296080820151600786016151d0565b61543a60a0820151600c86016152f5565b015191016152f5565b6154596060919493945f525f60205260405f2090565b936154676080850151151590565b61563a575b01916154bf60a06154886020865101516001600160a01b031690565b9280515f81136155fc575b506020810180515f81136155b4575b5081515f8112615573575b50515f8112615528575b500151151590565b8061551a575b6154d6575b5050505061273a61467e565b61550f9261550260a0926154f6604060139601516001600160a01b031690565b90848451015191614d9d565b5101519201918254613352565b90555f8080806154ca565b5060a08351015115156154c5565b6143fd61553491614261565b61554f8561239b61071460408a01516001600160a01b031690565b61555a828254612c10565b905561556b60138901918254613352565b90555f6154b7565b6143fd61557f91614261565b61559d818761559860208b01516001600160a01b031690565b614d9d565b6155ac60138a01918254613352565b90555f6154ad565b6155bd90615ad0565b6155d88661239b61071460408b01516001600160a01b031690565b6155e3828254613352565b90556155f460138a01918254612c10565b90555f6154a2565b61560590615ad0565b615623818661561e60208a01516001600160a01b031690565b614527565b61563260138901918254612c10565b90555f615493565b61564781600587016153bd565b61546c565b805192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156157e8575b806d04ee2d6d415b85acef8100000000600a9210156157cc575b662386f26fc100008110156157b7575b6305f5e1008110156157a5575b612710811015615795575b6064811015615786575b101561577b575b61571260216156da60018801615ddf565b968701015b5f1901917f3031323334353637383961626364656600000000000000000000000000000000600a82061a8353600a900490565b90811561572257615712906156df565b50506001600160a01b036157478461573b858498615d73565b60208151910120615dc9565b9116931683146157735761576591816020611aad9351910120615dc9565b1461576e575f90565b600190565b505050600190565b6001909401936156c9565b600290606490049601956156c2565b60049061271090049601956156b8565b6008906305f5e10090049601956156ad565b601090662386f26fc1000090049601956156a0565b6020906d04ee2d6d415b85acef81000000009004960195615690565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008104615676565b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00541461585f5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff815116906020810151600a811015611c0a576159308260406159919401516158cf60806060840151930151946040519760208901526040880190611c19565b6060860152608085019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b805167ffffffffffffffff1661016084015260208101516001600160a01b0316610180840152604081015160ff166101a084015260608101516101c084015260808101516101e084015260a081015161020084015260c00151610220830152565b610220815261084d61024082612707565b93909291935f52600260205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015192600a841015611c0a57615a5660c0615aa093615a0e615acc9760039a61518d565b60408101516007890155615a29606082015160088a016151d0565b615a3a6080820151600d8a016151d0565b615a4b60a082015160128a016152f5565b0151601387016152f5565b60018501907fffffffffffffffffffffff0000000000000000000000000000000000000000ff74ffffffffffffffffffffffffffffffffffffffff0083549260081b169116179055565b60028301906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b0155565b5f8112615ada5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b90604051918281549182825260208201905f5260205f20925f5b818110615b3457505061273a92500383612707565b8454835260019485019487945060209093019201615b1f565b67ffffffffffffffff6004820154164210159081615b69575090565b600180925060ff91015416615b7d81611c00565b1490565b6001600160a01b039061410d615ba7615ba26020989599969799369061301c565b615887565b93604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b6001810190825f528160205260405f2054155f14615c46578054680100000000000000008110156126ca57615c33615c1d8260018794018555846131bd565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b5050505f90565b80548015615c74575f190190615c6382826131bd565b8154905f199060031b1b1916905555565b634e487b7160e01b5f52603160045260245ffd5b6001810191805f528260205260405f2054928315155f14615d26575f19840184811161059a5783545f1981019490851161059a575f958583615ce397615cd69503615ce9575b505050615c4d565b905f5260205260405f2090565b55600190565b615d0f615d0991615d00610698615d1d95886131bd565b928391876131bd565b906131d2565b85905f5260205260405f2090565b555f8080615cce565b505050505f90565b93909291935f52600560205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b61273a90615dbb615db594936040519586937f19457468657265756d205369676e6564204d6573736167653a0a0000000000006020860152603a850190613ff9565b90613ff9565b03601f198101845283612707565b61084d91615dd691615e06565b90929192615e40565b90615de982612faf565b615df66040519182612707565b828152601f196133488294612faf565b8151919060418303615e3657615e2f9250602082015190606060408401519301515f1a90615f07565b9192909190565b50505f9160029190565b615e4981611c00565b80615e52575050565b615e5b81611c00565b60018103615e8b577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b615e9481611c00565b60028103615ec857507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b80615ed4600392611c00565b14615edc5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411615f7e579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610488575f516001600160a01b03811615615f7457905f905f90565b505f906001905f90565b5050505f916003919056fea2646970667358221220a1d82448e7e3f611b69660be3f5dc6e070ca23bb9e11aefc6fc6b7622d1cdc4e64736f6c634300081e0033000000000000000000000000735eb1026afba78b602da39c6b59eaba95753686",
+ "nonce": "0x2c",
+ "chainId": "0xaa36a7"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ }
+ ],
+ "receipts": [
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x9fc18a",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x3df2187dc8a50ef62abfeb377318888493042770315492070c4708584dfbf572",
+ "transactionIndex": "0x66",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0x1410b0",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0xacfd78",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x9712dcbc9f46d075bb90ab9d5cbbdf30195810bb050150b302cb3aaaf0e71bc0",
+ "transactionIndex": "0x67",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0xd3bee",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0xb9c9d6",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x6e81a9f20bb7b3370a15b6402271a9f8e7eae63184e733c80273c497a4187983",
+ "transactionIndex": "0x68",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0xccc5e",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0xc05453",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x5e58e1f709d9ded21112c24523733b843486bf6ae775ffd10d86118a5c947cfe",
+ "transactionIndex": "0x69",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0x68a7d",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0x735eb1026afba78b602da39c6b59eaba95753686"
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x11229e3",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "type": "0x2",
+ "transactionHash": "0x6e0b716f9bdb40d3aadbfa2544bf5ec11b39f431736bd19569ade187cb0b7396",
+ "transactionIndex": "0x6a",
+ "blockHash": "0x6e814235abb6e3aa00d957143ca4c04c16879792d6232a9d367b2d793b23c018",
+ "blockNumber": "0x9ebc3c",
+ "gasUsed": "0x51d590",
+ "effectiveGasPrice": "0x135b23",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0xb7be0e2007ddf320d680942cb9e008f986e74f83"
+ }
+ ],
+ "libraries": [
+ "src/ChannelEngine.sol:ChannelEngine:0x78D150fdA6fa6739C18014B347c7c7C45C58e148",
+ "src/EscrowDepositEngine.sol:EscrowDepositEngine:0x728904E52308213bA61C90EF49F34c18Fbda9E11",
+ "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine:0x893F2D45fDFFe2D4297a5C1D5732EDce4849eE82"
+ ],
+ "pending": [],
+ "returns": {},
+ "timestamp": 1772892720931,
+ "chain": 11155111,
+ "commit": "fd394085"
+}
\ No newline at end of file
diff --git a/contracts/broadcast/DeployChannelHub.s.sol/80002/run-1772893715802.json b/contracts/broadcast/DeployChannelHub.s.sol/80002/run-1772893715802.json
new file mode 100644
index 000000000..7434592c5
--- /dev/null
+++ b/contracts/broadcast/DeployChannelHub.s.sol/80002/run-1772893715802.json
@@ -0,0 +1,278 @@
+{
+ "transactions": [
+ {
+ "hash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "transactionType": "CREATE2",
+ "contractName": "ChannelEngine.channelhub",
+ "contractAddress": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x1bb70c",
+ "input": "0x0000000000000000000000000000000000000000000000000000000000000000608080604052346019576116f0908161001e823930815050f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63a8b4483c14610025575f80fd5b604060031936011261122d5760043567ffffffffffffffff811161122d5760a0600319823603011261122d5760a0820182811067ffffffffffffffff821117611299576040528060040135600681101561122d578252602481013567ffffffffffffffff811161122d5761009f90600436918401016113e1565b602083019081526040830192604483013584526100c96084606083019460648101358652016112ec565b6080820190815260243567ffffffffffffffff811161122d576100f09036906004016113e1565b6100f8611498565b50606081019367ffffffffffffffff855151164603610dc45767ffffffffffffffff82511681519067ffffffffffffffff82511610908115611261575b5015610a085784516040810190601260ff83511611611239574667ffffffffffffffff825116146110ff575b505060208201928351600a8110156103585760041480156110eb575b80156110d7575b80156110c3575b80156110af575b801561109b575b1561105e576080830167ffffffffffffffff815151161561103657515167ffffffffffffffff16461461100e575b6101dc865160a06060820151910151906114e6565b6101f1875160c06080820151910151906114f3565b5f8112610fe65761020190611526565b03610fbe578451600681101561035857600214610f7f575b50610222611498565b5061023c608086510151608060608451015101519061150e565b9061025660c08751015160c060608451015101519061150e565b9351600a81101561035857600281036104b65750509050610275611498565b928051600681101561035857159081156104a0575b811561048a575b8115610475575b501561044d575f8113156104255782526020820152600160408201525f6060820152925b6102d96102d1608086019260018452516115a7565b8551906114f3565b926102ea60208601948551906114f3565b5f81126103fd5760a08601938451156103ab575b50508351905f821361036c575b50506040519284518452516020840152604084015193600685101561035857606067ffffffffffffffff9160c09660408701520151166060840152511515608083015251151560a0820152f35b634e487b7160e01b5f52602160045260245ffd5b610377905191611526565b11610383575f8061030b565b7f2e3b1ec0000000000000000000000000000000000000000000000000000000005f5260045ffd5b6103c36103c9915160a06060820151910151906114e6565b91611526565b036103d5575f806102fe565b7f8f9003ee000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fae0bb491000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610298565b8091505160068110156103585760021490610291565b809150516006811015610358576001149061028a565b6003810361055657505090506104ca611498565b92805160068110156103585715908115610540575b811561052a575b8115610515575b501561044d575f8112156104255782526020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f6104ed565b80915051600681101561035857600214906104e6565b80915051600681101561035857600114906104df565b8061061f5750509050610567611498565b92805160068110156103585715908115610609575b81156105f3575b81156105de575b501561044d576104255760a0835101516105b6576020820152600160408201525f6060820152926102bc565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61058a565b8091505160068110156103585760021490610583565b809150516006811015610358576001149061057c565b600181036107185750509050610633611498565b928051600681101561035857600114908115610702575b81156106ed575b501561044d5761066c845160a06060820151910151906114e6565b8651106106c55761068a82610685836106858a516115a7565b6114f3565b5f81126103fd5761069f60a0865101516115a7565b136103fd5782526020820152600360408201525f6060820152600160a0820152926102bc565b7f7fa0800f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610651565b809150516006811015610358576002149061064a565b6004810361083857505061072a611498565b938051600681101561035857600114908115610822575b811561080d575b501561044d57610425576080016060815101519081156107e55761077a855160ff604060a0830151920151169061161e565b61078c60ff604084510151168461161e565b036105b65760806107a091510151916115a7565b036107bd576020820152600160408201525f6060820152926102bc565b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610748565b8091505160068110156103585760021490610741565b909391929060058103610a83575061084e611498565b948051600681101561035857600114908115610a6d575b8115610a58575b501561044d5761087f60208551016114d9565b600a81101561035857600403610a305767ffffffffffffffff81511667ffffffffffffffff6108b1818751511661155b565b1603610a0857608001916060835101516107e55760a0835101516105b65760a0865101516105b657610425576109e05760606080835101510151906080815101516108fb836115a7565b036107bd575160c00151610916610911836115a7565b61157b565b036109b8576060845101519060608084510151015182039182116109a45760ff6040608061094e61095b9584848b510151169061161e565b955101510151169061161e565b0361097c575f81525f6020820152600160408201525f6060820152926102bc565b7f733d14c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fd916ea0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f7dcd8ffd000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61086c565b8091505160068110156103585760021490610865565b9193909160068103610b3f57505090610a9a611498565b938051600681101561035857600114908115610b29575b8115610b14575b501561044d576104255760a0845101516105b6576080016080815101516107bd576060815101516107e55760c0610af360a0835101516115a7565b91510151036109b8576020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f610ab8565b8091505160068110156103585760021490610ab1565b60078103610bd957505090610b52611498565b938051600681101561035857600114908115610bc3575b8115610bae575b501561044d576104255760a0845101516105b6576080016060815101516107e55760a0815101516105b657516107a060c0608083015192015161157b565b9050516006811015610358576004145f610b70565b8091505160068110156103585760021490610b69565b60088103610e1557505090610bec611498565b938051600681101561035857158015610e01575b15610ce5575050608001805160600151915081156107e55760a0815101516105b6576060845101516107e557610c44845160ff604060a0830151920151169061161e565b610c5660ff604084510151168461161e565b03610cbd57610c8d9060ff6040610c82610c7c8851848460c0830151920151169061165b565b956115a7565b92510151169061165b565b036109b8576080825101516107bd57610caa60a0835101516115a7565b6020820152600460408201525b926102bc565b7f7b208b9d000000000000000000000000000000000000000000000000000000005f5260045ffd5b8051600681101561035857600114908115610dec575b501561044d574667ffffffffffffffff8651511603610dc457610425576060845101519081156107e55760a0855101516105b657608001906060825101516107e557610d55825160ff604060a0830151920151169061161e565b610d6760ff604088510151168361161e565b03610cbd57610d9f610d90610d8a845160ff604060c0830151920151169061165b565b926115a7565b60ff604088510151169061165b565b036109b85751608001516107bd576020820152600160408201525f6060820152610cb7565b7f67525583000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576002145f610cfb565b508051600681101561035857600514610c00565b600903610f5757610e24611498565b948051600681101561035857600403610ed957504667ffffffffffffffff8751511603610dc457610e5860208251016114d9565b600a81101561035857600803610a305767ffffffffffffffff82511667ffffffffffffffff610e8a818451511661155b565b1603610a0857606080915101510151606086510151036107e55760a0855101516105b6576080016060815101516107e5575160a001516105b657610425576109e05760016040820152926102bc565b919250508051600681101561035857600114908115610f42575b501561044d576060845101516107e55760a0845101516105b657608001606081510151156107e5575160a001516105b6576020820152600560408201525f6060820152600160a0820152610cb7565b9050516006811015610358576002145f610ef3565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b5167ffffffffffffffff164211610f96575f610219565b7ff06506c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff019de0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f114a9df4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f26c21ae4000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff60808401515116156101c7577f4c7b586e000000000000000000000000000000000000000000000000000000005f5260045ffd5b508351600a81101561035857600914610199565b508351600a81101561035857600814610192565b508351600a8110156103585760071461018b565b508351600a81101561035857600614610184565b508351600a8110156103585760051461017d565b6020015173ffffffffffffffffffffffffffffffffffffffff168061115b575060ff601291511603611133575b5f80610161565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f92816111f7575b506111c2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461112c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011611231575b81611213602093836112c9565b8101031261122d575160ff8116810361122d57915f611195565b5f80fd5b3d9150611206565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b60608101515167ffffffffffffffff1615915081611281575b505f610135565b67ffffffffffffffff9150608001515116155f61127a565b634e487b7160e01b5f52604160045260245ffd5b60e0810190811067ffffffffffffffff82111761129957604052565b90601f601f19910116810190811067ffffffffffffffff82111761129957604052565b359067ffffffffffffffff8216820361122d57565b91908260e091031261122d57604051611319816112ad565b8092611324816112ec565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361122d576020830152604081013560ff8116810361122d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561122d5780359067ffffffffffffffff821161129957604051926113c06020601f19601f86011601856112c9565b8284526020838301011161122d57815f926020809301838601378301015290565b91906102608382031261122d57604051906113fb826112ad565b8193611406816112ec565b83526020810135600a81101561122d576020840152604081013560408401526114328260608301611301565b6060840152611445826101408301611301565b608084015261022081013567ffffffffffffffff811161122d578261146b91830161138b565b60a08401526102408101359167ffffffffffffffff831161122d5760c092611493920161138b565b910152565b6040519060c0820182811067ffffffffffffffff821117611299576040525f60a0838281528260208201528260408201528260608201528260808201520152565b51600a8110156103585790565b919082018092116109a457565b9190915f83820193841291129080158216911516176109a457565b81810392915f1380158285131691841216176109a457565b5f81126115305790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b67ffffffffffffffff60019116019067ffffffffffffffff82116109a457565b7f800000000000000000000000000000000000000000000000000000000000000081146109a4575f0390565b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81116115d15790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116109a457565b60ff16604d81116109a457600a0a90565b9060ff811660128111611239576012146116575761163e611643916115fc565b61160d565b908181029181830414901517156109a45790565b5090565b9060ff811660128111611239576012146116575761163e61167b916115fc565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166109a45781830514901517156109a4579056fea264697066735822122036f6b0f3261f4d84fa391cd2e29d848110238f6d49d373a5912f2304cae9c86d64736f6c634300081e0033",
+ "nonce": "0x2",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowWithdrawalEngine.channelhub",
+ "contractAddress": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x124792",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610edc908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8062ea54e714610118576324063eba1461002e575f80fd5b60206003193601126101145760043567ffffffffffffffff81116101145761005a903690600401610c2a565b610062610ced565b90516004811015610100575f19016100d857600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff82116100c45767ffffffffffffffff6100c0921660608201525f608082015260405191829182610ca2565b0390f35b634e487b7160e01b5f52601160045260245ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b5f80fd5b60406003193601126101145760043567ffffffffffffffff811161011457610144903690600401610c2a565b60243567ffffffffffffffff811161011457610164903690600401610b73565b61016c610ced565b5081516004811015610100576003146109dc5767ffffffffffffffff461660608201908067ffffffffffffffff83515116146109b457608083019067ffffffffffffffff825151160361098c5767ffffffffffffffff835116156107a25780516040810190601260ff83511611610964574667ffffffffffffffff8251161461082e575b5050805160a0606082015191015181018091116100c45761021c825160c0608082015191015190610d17565b5f81126108065761022c90610d5e565b036107de57610239610ced565b5060208301928351600a811015610100576006810361052657505061025c610ced565b9184516004811015610100576104fe576060825101516104d6576080825101516104ae5781519160c060a084015193015161029684610d93565b03610486576102c360ff60406102b88551838360608301519201511690610e0a565b935101511684610e0a565b1161045e575160a00151610436576102da90610d93565b60208201526001604082015260016080820152915b825115801590610429575b15610401578251906103126020850192835190610d17565b928051600a81101561010057600603610366575050510361033e576100c0905b60405191829182610ca2565b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092915051600a81101561010057600714610387575b50506100c090610332565b8251036103d95760406103a261039d8451610d32565b610d5e565b910151036103b157818061037c565b7fd9132288000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b50602083015115156102fa565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f06b4cdae000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b90929060070361077a57610538610ced565b92855160048110156101005760011480156107ca575b156100d85767ffffffffffffffff9051166020860190600167ffffffffffffffff835151160167ffffffffffffffff81116100c45767ffffffffffffffff16036107a257602081510151600a811015610100577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0161077a5760a06080825101510151926060815101516104d6576080815101516105f36105ee86610d93565b610d32565b036104ae5760a081510151610436575160c0015161061084610d93565b036107025760608251015160608083510151015111156107525760608082510151015160608351015181039081116100c4576106559060ff6040855101511690610e0a565b61066b60ff604060808551015101511685610e0a565b0361072a5760c08251015160c06060835101510151905f82820392128183128116918313901516176100c4575f81121561070257604060806106cb6106c56106d89660ff856106ba8298610d32565b925101511690610e47565b96610d93565b9351015101511690610e47565b03610486576106ed6105ee6040850151610d93565b8152600360408201525f6080820152916102ef565b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fffda345d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f25e3e1b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156101005760021461054e565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061088a575060ff601291511603610862575b84806101f0565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610926575b506108f1577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461085b577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d60201161095c575b8161094260209383610a50565b81010312610114575160ff811681036101145791876108c4565b3d9150610935565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff821117610a2057604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117610a2057604052565b90601f601f19910116810190811067ffffffffffffffff821117610a2057604052565b359067ffffffffffffffff8216820361011457565b359073ffffffffffffffffffffffffffffffffffffffff8216820361011457565b91908260e091031261011457604051610ac181610a04565b8092610acc81610a73565b8252610ada60208201610a88565b6020830152604081013560ff811681036101145760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f820112156101145780359067ffffffffffffffff8211610a205760405192610b526020601f19601f8601160185610a50565b8284526020838301011161011457815f926020809301838601378301015290565b9190610260838203126101145760405190610b8d82610a04565b8193610b9881610a73565b83526020810135600a81101561011457602084015260408101356040840152610bc48260608301610aa9565b6060840152610bd7826101408301610aa9565b608084015261022081013567ffffffffffffffff81116101145782610bfd918301610b1d565b60a08401526102408101359167ffffffffffffffff83116101145760c092610c259201610b1d565b910152565b91909160a0818403126101145760405190610c4482610a34565b81938135600481101561011457835260208201359067ffffffffffffffff82116101145782610c7c60809492610c2594869401610b73565b602086015260408101356040860152610c9760608201610a73565b606086015201610a88565b91909160a0810192805182526020810151602083015260408101516004811015610100576080918291604085015267ffffffffffffffff606082015116606085015201511515910152565b60405190610cfa82610a34565b5f6080838281528260208201528260408201528260608201520152565b9190915f83820193841291129080158216911516176100c457565b7f800000000000000000000000000000000000000000000000000000000000000081146100c4575f0390565b5f8112610d685790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610dbd5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116100c457565b60ff16604d81116100c457600a0a90565b9060ff81166012811161096457601214610e4357610e2a610e2f91610de8565b610df9565b908181029181830414901517156100c45790565b5090565b9060ff81166012811161096457601214610e4357610e2a610e6791610de8565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166100c45781830514901517156100c4579056fea264697066735822122073585d1c2949228993d38506ffc5f542f9ffb1c023c1893a2f5522e50227b27564736f6c634300081e0033",
+ "nonce": "0x3",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowDepositEngine.channelhub",
+ "contractAddress": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x11ad7a",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610e55908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80636666e4c0146109095763bbc42f341461002f575f80fd5b604060031936011261085d5760043567ffffffffffffffff811161085d5761005b903690600401610bf4565b60243567ffffffffffffffff811161085d5761007b903690600401610b3d565b610083610cd9565b5081516004811015610343576003146108e15767ffffffffffffffff46169060608101918067ffffffffffffffff84515116146108b957608082019067ffffffffffffffff82515116036108915767ffffffffffffffff8251161561067b5780516040810190601260ff83511611610869574667ffffffffffffffff8251161461072f575b5050805160a06060820151910151810180911161038c57610134825160c0608082015191015190610d09565b5f81126107075761014490610d50565b036106df57610151610cd9565b5060208201928351600a81101561034357600481036104685750909150610176610cd9565b918451600481101561034357610440578051916080606084015193015161019c84610d85565b036104185760a0825101516103f05760c0825101516103c85760ff60406101d26101dd9351838360a08301519201511690610dda565b935101511683610dda565b036103a0576101eb90610d85565b815260016040820152612a3067ffffffffffffffff42160167ffffffffffffffff811161038c5767ffffffffffffffff166060820152600160a0820152915b82511580159061037f575b1561035757825161024c6020850191825190610d09565b928051600a811015610343576004036102a65750505081510361027e5761027a905b60405191829182610c7a565b0390f35b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9290919251600a811015610343576005146102c8575b50505061027a9061026e565b81510361031b576102e36102de60409251610d24565b610d50565b910151036102f3575f80806102bc565b7fb09443e7000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b5060208301511515610235565b634e487b7160e01b5f52601160045260245ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f76ac27ca000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b60050361065357610477610cd9565b92855160048110156103435760011480156106cb575b156106a35767ffffffffffffffff905116916020860192600167ffffffffffffffff855151160167ffffffffffffffff811161038c5767ffffffffffffffff160361067b57602083510151600a811015610343576003190161065357606060808451015101519060808151015161050383610d85565b036104185760c08151015161051f61051a84610d85565b610d24565b036103c85760608151015161062b575160a001516103f057606082510151606080855101510151810390811161038c576105656105799160ff6040865101511690610dda565b9160ff604060808751015101511690610dda565b036106035760a0815101516103f057606060808092510151925101510151908181035f831282808312821692139015161761038c57036105db576105c361051a6040850151610d85565b6020820152600360408201525f60a08201529161022a565b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fff0edb30000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156103435760021461048d565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061078b575060ff601291511603610763575b5f80610108565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610827575b506107f2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461075c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011610861575b8161084360209383610a25565b8101031261085d575160ff8116810361085d57915f6107c5565b5f80fd5b3d9150610836565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b602060031936011261085d5760043567ffffffffffffffff811161085d57610935903690600401610bf4565b61093d610cd9565b9080516004811015610343575f19016106a3576060015167ffffffffffffffff164210156109b157600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff821161038c5767ffffffffffffffff61027a921660808201525f60a082015260405191829182610c7a565b7f2b39d042000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff8211176109f557604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176109f557604052565b90601f601f19910116810190811067ffffffffffffffff8211176109f557604052565b359067ffffffffffffffff8216820361085d57565b91908260e091031261085d57604051610a75816109d9565b8092610a8081610a48565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361085d576020830152604081013560ff8116810361085d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561085d5780359067ffffffffffffffff82116109f55760405192610b1c6020601f19601f8601160185610a25565b8284526020838301011161085d57815f926020809301838601378301015290565b91906102608382031261085d5760405190610b57826109d9565b8193610b6281610a48565b83526020810135600a81101561085d57602084015260408101356040840152610b8e8260608301610a5d565b6060840152610ba1826101408301610a5d565b608084015261022081013567ffffffffffffffff811161085d5782610bc7918301610ae7565b60a08401526102408101359167ffffffffffffffff831161085d5760c092610bef9201610ae7565b910152565b91909160c08184031261085d5760405190610c0e82610a09565b81938135600481101561085d57835260208201359167ffffffffffffffff831161085d57610c4260a0939284938301610b3d565b602085015260408101356040850152610c5d60608201610a48565b6060850152610c6e60808201610a48565b60808501520135910152565b91909160c08101928051825260208101516020830152604081015160048110156103435760a0918291604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff608082015116608085015201511515910152565b60405190610ce682610a09565b5f60a0838281528260208201528260408201528260608201528260808201520152565b9190915f838201938412911290801582169115161761038c57565b7f8000000000000000000000000000000000000000000000000000000000000000811461038c575f0390565b5f8112610d5a5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610daf5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b9060ff16601281116108695760128114610e1b5760120360ff811161038c5760ff16604d811161038c57600a0a9081810291818304149015171561038c5790565b509056fea2646970667358221220fc0a93f7abd0c8aae0f4edd1fab1eef03232af831542ee9ea9f3dcf8d76c3da064736f6c634300081e0033",
+ "nonce": "0x4",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "transactionType": "CREATE",
+ "contractName": "ECDSAValidator.channelhub",
+ "contractAddress": "0x2a35728cadd8076dfd424fc3e20974a3cd03bfa5",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x880d5",
+ "value": "0x0",
+ "input": "0x608080604052346015576106d6908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63600109bb14610024575f80fd5b346100cc5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100cc5760243567ffffffffffffffff81116100cc576100739036906004016100d0565b9060443567ffffffffffffffff81116100cc576100949036906004016100d0565b6064359173ffffffffffffffffffffffffffffffffffffffff831683036100cc576020946100c4946004356101a0565b604051908152f35b5f80fd5b9181601f840112156100cc5782359167ffffffffffffffff83116100cc57602083818601950101116100cc57565b90601f601f19910116810190811067ffffffffffffffff82111761012157604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161012157601f01601f191660200190565b9291926101768261014e565b9161018460405193846100fe565b8294818452818301116100cc578281602093845f960137010152565b929091949383156102635773ffffffffffffffffffffffffffffffffffffffff85161561023b5761022060806101de6102279561022d99369161016a565b95601f19601f6020604051998a94828601526040808601528051918291826060880152018686015e5f858286010152011681010301601f1981018652856100fe565b369161016a565b9061028b565b1561023757600190565b5f90565b7f4501a919000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe1b97cf8000000000000000000000000000000000000000000000000000000005f5260045ffd5b91825192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156104cc575b806d04ee2d6d415b85acef8100000000600a9210156104b1575b662386f26fc1000081101561049d575b6305f5e10081101561048c575b61271081101561047d575b606481101561046f575b1015610465575b6001850190600a602161033461031e8561014e565b9461032c60405196876100fe565b80865261014e565b97601f19602086019901368a378401015b5f1901917f30313233343536373839616263646566000000000000000000000000000000008282061a83530490811561038057600a90610345565b505073ffffffffffffffffffffffffffffffffffffffff5f9361040c86610415946020610404869b603a604051938492818401967f19457468657265756d205369676e6564204d6573736167653a0a00000000000088525180918486015e83018281019d8e528c8051928391019e8f905e01015f815203601f1981018352826100fe565b5190206104f4565b9094919461052e565b169416841461045c5773ffffffffffffffffffffffffffffffffffffffff9261044d92610444925190206104f4565b9092919261052e565b1614610457575f90565b600190565b50505050600190565b9360010193610309565b606460029104960195610302565b612710600491049601956102f8565b6305f5e100600891049601956102ed565b662386f26fc10000601091049601956102e0565b6d04ee2d6d415b85acef8100000000602091049601956102d0565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f01000000000000000081046102b6565b81519190604183036105245761051d9250602082015190606060408401519301515f1a90610606565b9192909190565b50505f9160029190565b60048110156105d95780610540575050565b60018103610570577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b600281036105a457507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6003146105ae5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610695579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa1561068a575f5173ffffffffffffffffffffffffffffffffffffffff81161561068057905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fea2646970667358221220c8e32dfe4c3317faffb02d4b02fddbb5e01dbc789e117442dd5ec08557786de764736f6c634300081e0033",
+ "nonce": "0x5",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "transactionType": "CREATE",
+ "contractName": "ChannelHub",
+ "contractAddress": "0x55d6f0a0322606447fbc612cf58014faed65af9d",
+ "function": null,
+ "arguments": [
+ "0x2A35728CADd8076dfD424fC3e20974A3CD03bFa5"
+ ],
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x6a626e",
+ "value": "0x0",
+ "input": "0x60a0346100aa57601f61608238819003918201601f19168301916001600160401b038311848410176100ae578084926020946040528339810103126100aa57516001600160a01b0381168082036100aa5760017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00551561009b57608052604051615fbf90816100c382396080518181816111420152613f180152f35b63e6c4247b60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806316b390b11461024457806317536c061461023f578063187576d81461023a5780633115f6301461023557806338a66be21461023057806341b660ef1461022b57806347de477a146102265780635326919814610221578063587675e81461021c5780635a0745b4146102175780635b9acbf9146102125780635dc46a741461020d5780636840dbd2146102085780636898234b146102035780636af820bd146101fe57806371a47141146101f9578063735181f0146101f457806382d3e15d146101ef5780638d0b12a5146101ea57806394191051146101e55780639691b468146101e0578063a5c82680146101db578063b00b6fd6146101d6578063b25a1d38146101d1578063beed9d5f146101cc578063c74a2d10146101c7578063d888ccae146101c2578063dc23f29e146101bd578063dd73d494146101b8578063e617208c146101b3578063ecf3d7e8146101ae578063f4ac51f5146101a9578063f766f8d6146101a4578063ff5bc09e1461019f5763ffa1ad741461019a575f80fd5b612650565b612639565b61249a565b61241f565b61230d565b61226e565b6120f0565b611ef5565b611db5565b611b71565b6119d6565b6116c4565b61165b565b6114ba565b611379565b61135c565b6111cb565b6111ae565b611171565b61112d565b611112565b611026565b61100f565b610fc8565b610fa6565b610f8a565b610f44565b610cfc565b610b13565b610850565b6107ea565b61065e565b6105d8565b6104ca565b6102cb565b9181601f840112156102775782359167ffffffffffffffff8311610277576020838186019501011161027757565b5f80fd5b60643590600282101561027757565b90606060031983011261027757600435916024359067ffffffffffffffff8211610277576102ba91600401610249565b909160443560028110156102775790565b34610277576102d93661028a565b6103986102f1859493945f52600260205260405f2090565b9283546102ff81151561266b565b61035a600286019461032a61031b87546001600160a01b031690565b948560038a019a8b5492613eff565b9591600160068b019a019661034a88546001600160a01b039060081c1690565b926103548c6128a0565b8861405d565b60c061036588614161565b604051809581927f6666e4c000000000000000000000000000000000000000000000000000000000835260048301612a25565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577fba075bd445233f7cad862c72f0343b3503aad9c8e704a2295f122b82abf8e80196610436956080955f9461044b575b50836104146104066104279697546001600160a01b039060081c1690565b92546001600160a01b031690565b9254936104208a6128a0565b908c61428d565b015167ffffffffffffffff1690565b9061044660405192839283612a41565b0390a2005b6104279450946104146104786104069760c03d60c011610481575b6104708183612707565b810190612950565b955050946103e8565b503d610466565b612a36565b6001600160a01b0381160361027757565b6003196060910112610277576004356104b68161048d565b906024356104c38161048d565b9060443590565b6001600160a01b036104db3661049e565b92909116906104eb821515612b9e565b6104f6831515612bcd565b815f52600660205261051c8160405f20906001600160a01b03165f5260205260405f2090565b80549184830180931161059a577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7926001600160a01b03925561055d615810565b61056885823361458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00556040519485521692602090a3005b612bfc565b60206040818301928281528451809452019201905f5b8181106105c25750505090565b82518452602093840193909201916001016105b5565b34610277576020600319360112610277576001600160a01b036004356105fd8161048d565b165f52600160205260405f206040519081602082549182815201915f5260205f20905f5b818110610648576106448561063881870382612707565b6040519182918261059f565b0390f35b8254845260209093019260019283019201610621565b3461027757602060031936011261027757600354600480549190355f5b828410806107e1575b156107d4576106b06106a2610698866131a0565b90549060031b1c90565b5f52600260205260405f2090565b6001810160036106c1825460ff1690565b6106ca81611c00565b146107c2576106d882615b4d565b1561077e57915f8261076961077595600561076f96019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b6001600160a01b03165f52600660205260405f2090565b92015460401c6001600160a01b031690565b6001600160a01b03165f5260205260405f2090565b918254612c10565b9055600360ff19825416179055565b556139c6565b936139c6565b915b919261067b565b505092905061078d9150600455565b8061079457005b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1005b5050926107ce906139c6565b91610777565b92905061078d9150600455565b50818110610684565b34610277575f600319360112610277576020604051620186a08152f35b90816102609103126102775790565b90600319820160e081126102775760c0136102775760049160c4359067ffffffffffffffff82116102775761084d91600401610807565b90565b61085936610816565b906020820191600261086a84612c27565b61087381611c0f565b148015610af8575b8015610ada575b61088b90612c31565b61091a6108a061089b3685612c79565b614780565b916108aa8461483e565b60208401906108b882612ced565b956108d760408701976108ca89612ced565b608089013591858961494b565b60c0826108ff6108f86108ec6107148c612ced565b61073d60808501612ced565b54886149c5565b6040519687928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af493841561048857610a156001600160a01b0394610a2d936109967fb00e209e275d0e1892f1982b34d3f545d1628aebd95322d7ce3585c558f638b498610a1b955f91610aab575b50610985368d612c79565b61098f368a61301c565b908c614b52565b6109c2896109bd6109a685612ced565b6001600160a01b03165f52600160205260405f2090565b615bde565b5060026109ce82612c27565b6109d781611c0f565b03610a325750877f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f4177869620669660405180610a0d89826130ca565b0390a2612ced565b97612ced565b918360405194859416981696836130db565b0390a4005b610a3d600391612c27565b610a4681611c0f565b03610a7b57877f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf98660405180610a0d89826130ca565b877f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc60405180610a0d89826130ca565b610acd915060c03d60c011610ad3575b610ac58183612707565b810190612cf7565b5f61097a565b503d610abb565b5061088b610ae784612c27565b610af081611c0f565b159050610882565b506003610b0484612c27565b610b0d81611c0f565b1461087b565b610b1c36610816565b90610b3d6004610b2e60208501612c27565b610b3781611c0f565b14612c31565b610b4a61089b3683612c79565b9160208201610b5881612ced565b90610b7960408501926080610b6c85612ced565b960135958691868961494b565b610b8b610b858461316b565b86614c6c565b93610b9586614c9c565b15610bdd57505050610bd881610bcc7f471c4ebe4e57d25ef7117e141caac31c6b98f067b8098a7a7bbd38f637c2f9809386614cf9565b604051918291826130ca565b0390a3005b610c259060c085610bf18897959697614161565b60405194859283927fbbc42f3400000000000000000000000000000000000000000000000000000000845260048401613175565b038173728904e52308213ba61c90ef49f34c18fbda9e115af48015610488577fede7867afa7cdb9c443667efd8244d98bf9df1dce68e60dc94dca6605125ca7695610bd895610c9a945f93610ca3575b50610c82610c8891612ced565b91612ced565b91610c93368761301c565b8a8a61428d565b610bcc846131ef565b610c88919350610cc4610c829160c03d60c011610481576104708183612707565b939150610c75565b90604060031983011261027757600435916024359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610d0a36610ccc565b610d1b6009610b2e60208401612c27565b610d376001610d31845f525f60205260405f2090565b0161323d565b610dfd610d4e60208301516001600160a01b031690565b9161071460c060408301610d7a610d6c82516001600160a01b031690565b608086015190888a8c61494b565b610de2610ddb610dc4610d8d368b61301c565b9586946101408c018d8d610da08361316b565b67ffffffffffffffff1646149d8e610eb7575b50505050516001600160a01b031690565b6060840151602001516001600160a01b031661073d565b54896149c5565b6040519586928392632a2d120f60e21b8452600484016132c9565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857610e2f935f93610e96575b5086614b52565b15610e65576104467f9a6f675cc94b83b55f1ecc0876affd4332a30c92e6faa2aca0199b1b6df922c391604051918291826130ca565b6104467f7b20773c41402791c5f18914dbbeacad38b1ebcc4c55d8eb3bfe0a4cde26c82691604051918291826130ca565b610eb091935060c03d60c011610ad357610ac58183612707565b915f610e28565b610edb610f1092610ecc610f15963690612f3c565b60608d01526060369101612f3c565b60808b0152610ee86132b5565b60a08b0152610ef56132b5565b8b8b01526001600160a01b03165f52600160205260405f2090565b615c88565b505f8d8d82610db3565b600319604091011261027757600435610f378161048d565b9060243561084d8161048d565b34610277576020610f816001600160a01b03610f5f36610f1f565b91165f526006835260405f20906001600160a01b03165f5260205260405f2090565b54604051908152f35b34610277575f600319360112610277576020604051612a308152f35b3461027757604060031936011261027757610644610638602435600435613373565b610fda610fd436610ccc565b9061342d565b005b60606003198201126102775760043591602435916044359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610fda61102036610fdc565b9161378a565b34610277576020600319360112610277576001600160a01b0360043561104b8161048d565b165f52600160205261105f60405f20615b05565b5f905f5b81518110156110ff5761109161108a61107c838561335f565b515f525f60205260405f2090565b5460ff1690565b61109a816121c1565b600381141590816110ea575b506110b4575b600101611063565b916110c78184600193106110cf576139c6565b9290506110ac565b6110d9858561335f565b516110e4828661335f565b526139c6565b600591506110f7816121c1565b14155f6110a6565b506106449181526040519182918261059f565b34610277575f60031936011261027757602060405160408152f35b34610277575f600319360112610277576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610277576020610f816001600160a01b0361118c36610f1f565b91165f526008835260405f20906001600160a01b03165f5260205260405f2090565b34610277575f600319360112610277576020600454604051908152f35b34610277576112556111dc3661028a565b929391906111f2855f52600560205260405f2090565b9182549261120184151561266b565b600281019060a061122261121c84546001600160a01b031690565b8a615053565b604051809881927f24063eba000000000000000000000000000000000000000000000000000000008352600483016139d4565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4958615610488575f9661132b575b50600181015460081c6001600160a01b0316968792546112a2906001600160a01b031690565b809581956003850154976112b7928992613eff565b9a9190946006019a6112c88c6128a0565b956112d3968b61405d565b846112dd876128a0565b6112e795896150bb565b6060015167ffffffffffffffff166040519182916113059183612a41565b037fb8568a1f475f3c76759a620e08a653d28348c5c09e2e0bc91d533339801fefd891a2005b61134e91965060a03d60a011611355575b6113468183612707565b8101906136bc565b945f61127c565b503d61133c565b34610277575f600319360112610277576020604051620151808152f35b61143661138536610ccc565b6113a661139760208395949501612c27565b6113a081611c0f565b15612c31565b6113bc6001610d31855f525f60205260405f2090565b9060c08161141b6114146108ec6107146113e060208901516001600160a01b031690565b6114078b8a60408101938960806113fe87516001600160a01b031690565b9301519361494b565b516001600160a01b031690565b54876149c5565b6040519586928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc9361044693610bcc925f92611499575b50611492368561301c565b9087614b52565b6114b391925060c03d60c011610ad357610ac58183612707565b905f611487565b34610277576114c836610816565b906114da6006610b2e60208501612c27565b6114e761089b3683612c79565b91602082016114f581612ced565b9061150960408501926080610b6c85612ced565b611515610b858461316b565b9361151f86614c9c565b1561155657505050610bd881610bcc7f587faad1bcd589ce902468251883e1976a645af8563c773eed7356d78433210c9386614cf9565b6115a59060a08561157261156c87989697612ced565b89615053565b60405194859283927eea54e700000000000000000000000000000000000000000000000000000000845260048401613773565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af48015610488577f17eb0a6bd5a0de45d1029ce3444941070e149df35b22176fc439f930f73c09f795610bd895610bcc945f93611614575b50610c8261160291612ced565b9161160d368761301c565b8a8a6150bb565b611602919350611635610c829160a03d60a011611355576113468183612707565b9391506115f5565b6024359060ff8216820361027757565b359060ff8216820361027757565b34610277576040600319360112610277576001600160a01b036116a96004356116838161048d565b8261168c61163d565b91165f52600760205260405f209060ff165f5260205260405f2090565b541660405180916001600160a01b0360208301911682520390f35b60806003193601126102775760043560243567ffffffffffffffff8111610277576116f3903690600401610807565b60443567ffffffffffffffff811161027757611713903690600401610249565b919061171d61027b565b9061172f855f525f60205260405f2090565b9161173c6001840161323d565b9161176661174b855460ff1690565b611754816121c1565b600181149081156119c2575b506139e5565b86611773600586016128a0565b916117b46117808861316b565b67ffffffffffffffff6117ab61179e875167ffffffffffffffff1690565b67ffffffffffffffff1690565b91161015613a14565b60208501516001600160a01b0316976117d760408701516001600160a01b031690565b9367ffffffffffffffff6117ff61179e6117f08c61316b565b935167ffffffffffffffff1690565b9116116118c3575b94611867889795857f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a9b6118616118859760149d61185561187c996118959c6118b49f60808c015192613eff565b9391949092369061301c565b9061405d565b845460ff191660021785555163ffffffff1690565b63ffffffff1690565b67ffffffffffffffff4216613a43565b9301805467ffffffffffffffff191667ffffffffffffffff8516179055565b61044660405192839283613a65565b909296959397946118df61190a9389888a60808601519361494b565b60c08761141b6119036108ec8c6001600160a01b03165f52600660205260405f2090565b548d6149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4938415610488577f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a99896118b4986118618e8c61185560149f976118679861187c9b61198a6118959f6118859f5f916119a3575b508d611983368961301c565b9089615443565b9a9f5050995050509750509b5095509597985050611807565b6119bc915060c03d60c011610ad357610ac58183612707565b5f611977565b600491506119cf816121c1565b145f611760565b34610277576080600319360112610277576004356119f38161048d565b6119fb61163d565b90604435611a088161048d565b60643567ffffffffffffffff811161027757611b4a6001600160a01b0392611b22611a6396611b07611b02611a4289973690600401610249565b60ff85169a91611afc90611a578d1515613a8d565b8b89169d8e1515612b9e565b611abf8785611ab9611aad611aad611aa085611a90866001600160a01b03165f52600760205260405f2090565b9060ff165f5260205260405f2090565b546001600160a01b031690565b6001600160a01b031690565b15613abc565b6040805160ff891660208201526001600160a01b038b169181019190915246606080830191909152815292611af5608085612707565b3691612fcb565b9061564c565b613b01565b611a90856001600160a01b03165f52600760205260405f2090565b906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b167f2366b94a706a0cfc2dca2fe8be9410b6fba2db75e3e9d3f03b3c2fb0b051efad5f80a4005b611b91611b7d36610ccc565b6113a66003610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf9869361044693610bcc925f926114995750611492368561301c565b634e487b7160e01b5f52602160045260245ffd5b60041115611c0a57565b611bec565b600a1115611c0a57565b90600a821015611c0a5752565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b61084d9167ffffffffffffffff8251168152611c6f60208301516020830190611c19565b60408201516040820152611cdd6060830151606083019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b608082810151805167ffffffffffffffff1661014084015260208101516001600160a01b0316610160840152604081015160ff1661018084015260608101516101a0840152908101516101c083015260a08101516101e083015260c0015161020082015260c0611d5f60a0840151610260610220850152610260840190611c26565b92015190610240818403910152611c26565b929367ffffffffffffffff60c09561084d98979482948752611d9281611c00565b602087015216604085015216606083015260808201528160a08201520190611c4b565b3461027757602060031936011261027757600435611dd1613b66565b505f52600260205260405f20611de561272a565b9080548252610644600182015491611e31611e21611e038560ff1690565b94611e12602088019687613baa565b60081c6001600160a01b031690565b6001600160a01b03166040860152565b611e58611e4860028301546001600160a01b031690565b6001600160a01b03166060860152565b60038101546080850152600481015467ffffffffffffffff811660a086019081529490611e90905b60401c67ffffffffffffffff1690565b67ffffffffffffffff1660c0820190815291611ee46117f0611ec0600660058501549460e08701958652016128a0565b93610100810194855251965197611ed689611c00565b5167ffffffffffffffff1690565b905191519260405196879687611d71565b3461027757611f0336610816565b611f146008610b2e60208401612c27565b80611f89611f2561089b3686612c79565b936020810160c0611f3582612ced565b91611f546040850193611f4785612ced565b608087013591898c61494b565b610de2610ddb610dc4610714611f6a368b61301c565b9687958d8a611f7882614c9c565b9d8e15612052575b50505050612ced565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857611fc6935f9361202d575b50611fc0903690612c79565b86614b52565b15611ffc576104467f3142fb397e715d80415dff7b527bf1c451def4675da6e1199ee1b4588e3f630a91604051918291826130ca565b6104467f26afbcb9eb52c21f42eb9cfe8f263718ffb65afbf84abe8ad8cce2acfb2242b891604051918291826130ca565b611fc091935061204b9060c03d60c011610ad357610ac58183612707565b9290611fb4565b6120aa936120876109a6926120696109bd9561483e565b8c606061207a366101408501612f3c565b9101526060369101612f3c565b60808c01526120946132b5565b60a08c01526120a16132b5565b8c8c0152612ced565b505f8d8a8e611f80565b9160a09367ffffffffffffffff9161084d97969385526120d381611c00565b602085015216604083015260608201528160808201520190611c4b565b346102775760206003193601126102775760043561210c613b66565b505f52600560205260405f2061212061273c565b908054825261064460018201549161213e611e21611e038560ff1690565b612155611e4860028301546001600160a01b031690565b60038101546080850152600481015467ffffffffffffffff1667ffffffffffffffff1660a08501908152936121b061219b600660058501549460c08501958652016128a0565b9160e0810192835251945195611ed687611c00565b9151905191604051958695866120b4565b60061115611c0a57565b906006821015611c0a5752565b9192612250610120946121f285612263959a99989a6121cb565b602085019060a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b61014060e0840152610140830190611c4b565b946101008201520152565b34610277576020600319360112610277576004355f60a0604051612291816126ae565b82815282602082015282604082015282606082015282608082015201526122b6613b66565b505f525f6020526122c960405f20613bc2565b80516122d4816121c1565b61064460208301519260408101519060606122fd61179e608084015167ffffffffffffffff1690565b91015191604051958695866121d8565b346102775761231b3661049e565b90916123316001600160a01b0382161515612b9e565b61233c821515612bcd565b335f5260066020526123628360405f20906001600160a01b03165f5260205260405f2090565b54908282106123f75782820391821161059a578383916123b7936123b18361239b336001600160a01b03165f52600660205260405f2090565b906001600160a01b03165f5260205260405f2090565b55614d9d565b7fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb6001600160a01b0360405193169280610bd83394829190602083019252565b7ff4d678b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b61243f61242b36610ccc565b6113a66002610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f417786962066969361044693610bcc925f926114995750611492368561301c565b34610277576124a836610f1f565b6124b0615810565b6001600160a01b038116916124c6831515612b9e565b6001600160a01b036124ed8261239b336001600160a01b03165f52600860205260405f2090565b54916124fa831515612bcd565b5f61251a8261239b336001600160a01b03165f52600860205260405f2090565b55169181836125945761253d915f808080858a5af1612537613c20565b50613c4f565b60405190815233907f7b8d70738154be94a9a068a6d2f5dd8cfc65c52855859dc8f47de1ff185f8b5590602090a4610fda60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b50506040517fa9059cbb000000000000000000000000000000000000000000000000000000005f52836004528160245260205f60448180875af160015f511481161561261a575b60409190915261253d577f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b03821660045260245ffd5b6001811516612630573d15843b151516166125db565b503d5f823e3d90fd5b3461027757610fda61264a36610fdc565b91613c90565b34610277575f60031936011261027757602060405160018152f35b1561267257565b7fc60f1e78000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176126ca57604052565b61269a565b60e0810190811067ffffffffffffffff8211176126ca57604052565b60a0810190811067ffffffffffffffff8211176126ca57604052565b90601f601f19910116810190811067ffffffffffffffff8211176126ca57604052565b6040519061273a61012083612707565b565b6040519061273a61010083612707565b6040519061273a60e083612707565b90604051612768816126cf565b60c0600482946127a660ff825467ffffffffffffffff811687526001600160a01b03808260401c1616602088015260e01c16604086019060ff169052565b6001810154606085015260028101546080850152600381015460a08501520154910152565b90600182811c921680156127f9575b60208310146127e557565b634e487b7160e01b5f52602260045260245ffd5b91607f16916127da565b5f9291815491612812836127cb565b8083529260018116908115612867575060011461282e57505050565b5f9081526020812093945091925b83831061284d575060209250010190565b60018160209294939454838587010152019101919061283c565b9050602094955060ff1991509291921683830152151560051b010190565b9061273a6128999260405193848092612803565b0383612707565b906040516128ad816126cf565b809260ff815467ffffffffffffffff8116845260401c1690600a821015611c0a57600d61291f9160c0936020860152600181015460408601526128f26002820161275b565b60608601526129036007820161275b565b6080860152612914600c8201612885565b60a086015201612885565b910152565b5190600482101561027757565b67ffffffffffffffff81160361027757565b5190811515820361027757565b908160c0910312610277576129b860a06040519261296d846126ae565b805184526020810151602085015261298760408201612924565b6040850152606081015161299a81612931565b606085015260808101516129ad81612931565b608085015201612943565b60a082015290565b9081516129cc81611c00565b815260a0806129ea602085015160c0602086015260c0850190611c4b565b936040810151604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff6080820151166080850152015191015290565b90602061084d9281815201906129c0565b6040513d5f823e3d90fd5b92916020612b8d61273a9360408752612a75815467ffffffffffffffff811660408a015260ff60608a019160401c16611c19565b60018101546080880152600281015467ffffffffffffffff811660a0890152604081901c6001600160a01b031660c089015260e090811c60ff16908801526003810154610100880152600481015461012088015260058101546101408801526006810154610160880152600781015467ffffffffffffffff8116610180890152604081901c6001600160a01b03166101a089015260e01c60ff166101c088015260088101546101e08801526009810154610200880152600a810154610220880152600b81015461024088015261026080880152600d612b5b6102a08901600c8401612803565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0898403016102808a015201612803565b94019067ffffffffffffffff169052565b15612ba557565b7fe6c4247b000000000000000000000000000000000000000000000000000000005f5260045ffd5b15612bd457565b7f69640e72000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b9190820180921161059a57565b600a111561027757565b3561084d81612c1d565b15612c3857565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b63ffffffff81160361027757565b359061273a82612931565b91908260c091031261027757604051612c91816126ae565b60a08082948035612ca181612c60565b84526020810135612cb18161048d565b60208501526040810135612cc48161048d565b60408501526060810135612cd781612931565b6060850152608081013560808501520135910152565b3561084d8161048d565b908160c09103126102775760405190612d0f826126ae565b805182526020810151602083015260408101516006811015610277576129b89160a09160408501526060810151612d4581612931565b60608501526129ad60808201612943565b90612d628183516121cb565b608067ffffffffffffffff81612d87602086015160a0602087015260a0860190611c4b565b94604081015160408601526060810151606086015201511691015290565b359061273a82612c1d565b60c0809167ffffffffffffffff8135612dc881612931565b1684526001600160a01b036020820135612de18161048d565b16602085015260ff612df56040830161164d565b166040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b9035601e198236030181121561027757016020813591019167ffffffffffffffff821161027757813603831361027757565b601f8260209493601f1993818652868601375f8582860101520116010190565b61084d9167ffffffffffffffff8235612e8a81612931565b168152612ea86020830135612e9e81612c1d565b6020830190611c19565b60408201356040820152612ec26060820160608401612db0565b612ed461014082016101408401612db0565b612f08612efc612ee8610220850185612e20565b610260610220860152610260850191612e52565b92610240810190612e20565b91610240818503910152612e52565b9091612f2e61084d93604084526040840190612d56565b916020818403910152612e72565b91908260e091031261027757604051612f54816126cf565b60c08082948035612f6481612931565b84526020810135612f748161048d565b6020850152612f856040820161164d565b6040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b67ffffffffffffffff81116126ca57601f01601f191660200190565b929192612fd782612faf565b91612fe56040519384612707565b829481845281830111610277578281602093845f960137010152565b9080601f830112156102775781602061084d93359101612fcb565b919091610260818403126102775761303261274c565b9261303c82612c6e565b845261304a60208301612da5565b6020850152604082013560408501526130668160608401612f3c565b6060850152613079816101408401612f3c565b608085015261022082013567ffffffffffffffff8111610277578161309f918401613001565b60a085015261024082013567ffffffffffffffff8111610277576130c39201613001565b60c0830152565b90602061084d928181520190612e72565b60e09060a061084d949363ffffffff81356130f581612c60565b1683526001600160a01b03602082013561310e8161048d565b1660208401526001600160a01b03604082013561312a8161048d565b16604084015267ffffffffffffffff606082013561314781612931565b16606084015260808101356080840152013560a08201528160c08201520190612e72565b3561084d81612931565b9091612f2e61084d936040845260408401906129c0565b634e487b7160e01b5f52603260045260245ffd5b6003548110156131b85760035f5260205f2001905f90565b61318c565b80548210156131b8575f5260205f2001905f90565b916131eb9183549060031b91821b915f19901b19161790565b9055565b600354680100000000000000008110156126ca57600181016003556003548110156131b85760035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b0155565b9060405161324a816126ae565b60a0600382946001600160a01b03815463ffffffff8116865260201c1660208501526132a467ffffffffffffffff60018301546001600160a01b03808216166040880152851c16606086019067ffffffffffffffff169052565b600281015460808501520154910152565b604051906132c4602083612707565b5f8252565b90916132e061084d93604084526040840190612d56565b916020818403910152611c4b565b67ffffffffffffffff81116126ca5760051b60200190565b60405190613315602083612707565b5f808352366020840137565b9061332b826132ee565b6133386040519182612707565b828152601f1961334882946132ee565b0190602036910137565b9190820391821161059a57565b80518210156131b85760209160051b010190565b9190600354908084029380850482149015171561059a57818410156133f75783019081841161059a578082116133ef575b506133b76133b28483613352565b613321565b92805b8281106133c657505050565b806133d56106986001936131a0565b6133e86133e28584613352565b8861335f565b52016133ba565b90505f6133a4565b5050905061084d613306565b906006811015611c0a5760ff60ff198354169116179055565b90602061084d928181520190611c4b565b9061343f825f525f60205260405f2090565b61344b6001820161323d565b91613457825460ff1690565b9184613465600583016128a0565b91604086019261347c84516001600160a01b031690565b91600261349360208a01516001600160a01b031690565b9761349d816121c1565b1480613654575b6135995750506134e16108f86108ec6107146134fc9661140760c0978a978c898f6080906134d96001610b2e60208601612c27565b01519361494b565b6040519384928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4801561048857610f10613573946109a688937f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a898613566965f92613578575b5061355f368961301c565b9086614b52565b50604051918291826130ca565b0390a2565b61359291925060c03d60c011610ad357610ac58183612707565b905f613554565b7f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a89750613573969195506136479450610f10926135fe6014836135e66109a695600360ff19825416179055565b5f60138201550167ffffffffffffffff198154169055565b606087016136278151606061361d60208301516001600160a01b031690565b9101519086614d9d565b5160a061363e60208301516001600160a01b031690565b91015191614d9d565b506040519182918261341c565b5061366d61179e601483015467ffffffffffffffff1690565b42116134a4565b1561367b57565b7fdb1ea1ac000000000000000000000000000000000000000000000000000000005f5260045ffd5b906136ad81611c00565b60ff60ff198354169116179055565b908160a0910312610277576137116080604051926136d9846126eb565b80518452602081015160208501526136f360408201612924565b6040850152606081015161370681612931565b606085015201612943565b608082015290565b90815161372581611c00565b815260806001600160a01b038161374b602086015160a0602087015260a0860190611c4b565b946040810151604086015267ffffffffffffffff606082015116606086015201511691015290565b9091612f2e61084d93604084526040840190613719565b9161379483614c9c565b613959576137aa825f52600560205260405f2090565b6137b684825414613674565b60018101918254916137d2836001600160a01b039060081c1690565b9360026137f26137eb828501546001600160a01b031690565b9560ff1690565b6137fb81611c00565b1480613939575b6138bf5750600361383b9161381e6007610b2e60208701612c27565b019261382e84548287868b61494b565b60a0836115728389615053565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4908115610488577f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d19561389995610bcc945f9461389e575b50549261160d368761301c565b0390a3565b6138b891945060a03d60a011611355576113468183612707565b925f61388c565b7f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d1945090613899936138fb610bcc93600360ff19825416179055565b613933600d60058401935f855495556139226004820167ffffffffffffffff198154169055565b015460401c6001600160a01b031690565b90614d9d565b5061395261179e600484015467ffffffffffffffff1690565b4211613802565b6138997f6d0cf3d243d63f08f50db493a8af34b27d4e3bc9ec4098e82700abfeffe2d49891610bcc613992865f525f60205260405f2090565b6139be60026139af60018401546001600160a01b039060201c1690565b9201546001600160a01b031690565b908388615025565b5f19811461059a5760010190565b90602061084d928181520190613719565b156139ec57565b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613a1b57565b7f7d957361000000000000000000000000000000000000000000000000000000005f5260045ffd5b9067ffffffffffffffff8091169116019067ffffffffffffffff821161059a57565b9067ffffffffffffffff613a86602092959495604085526040850190612e72565b9416910152565b15613a9457565b7f06ee4dcd000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613ac5575050565b906001600160a01b0360ff927f0bcc40f3000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b15613b0857565b7fc1606c2f000000000000000000000000000000000000000000000000000000005f5260045ffd5b60405190613b3d826126cf565b5f60c0838281528260208201528260408201528260608201528260808201528260a08201520152565b60405190613b73826126cf565b606060c0835f81525f60208201525f6040820152613b8f613b30565b83820152613b9b613b30565b60808201528260a08201520152565b613bb382611c00565b52565b6006821015611c0a5752565b90604051613bcf816126eb565b608067ffffffffffffffff60148395613bec60ff82541686613bb6565b613bf86001820161323d565b6020860152613c09600582016128a0565b604086015260138101546060860152015416910152565b3d15613c4a573d90613c3182612faf565b91613c3f6040519384612707565b82523d5f602084013e565b606090565b15613c58575050565b6001600160a01b03907fa5b05eec000000000000000000000000000000000000000000000000000000005f521660045260245260445ffd5b91613c9a83614c9c565b613e8157613cb0825f52600260205260405f2090565b613cbc84825414613674565b6001810191825491613cd8836001600160a01b039060081c1690565b936002613cf16137eb828501546001600160a01b031690565b613cfa81611c00565b1480613e5e575b613de457506003613d6591613d1d6005610b2e60208701612c27565b0192613d2d84548287868b61494b565b60c083610bf1613d5e613d51856001600160a01b03165f52600660205260405f2090565b61073d6101608501612ced565b5489614206565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e9561389995610bcc945f94613dc3575b505492610c93368761301c565b613ddd91945060c03d60c011610481576104708183612707565b925f613db6565b7f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e94509061389993613e20610bcc93600360ff19825416179055565b613933600d60058401935f85549555613922600482017fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff8154169055565b506004820154613e7a9060401c67ffffffffffffffff1661179e565b4211613d01565b6138997f32e24720f56fd5a7f4cb219d7ff3278ae95196e79c85b5801395894a6f53466c91610bcc613992865f525f60205260405f2090565b15613ec3575050565b906001600160a01b0360ff927f577f5940000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b93929190918215613fd157843560f81c9081613f4c57507f000000000000000000000000000000000000000000000000000000000000000094600101925f19019150613f489050565b9091565b600180915f97939594975060ff86161c1603613fa957613f9d83613f8b611aa0613f4896611a908a6001600160a01b03165f52600760205260405f2090565b966001600160a01b0388161515613eba565b600101915f1990910190565b7f1a9073b4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fac241e11000000000000000000000000000000000000000000000000000000005f5260045ffd5b805191908290602001825e015f815290565b60021115611c0a57565b90816020910312610277575190565b939260609361404f6001600160a01b0394613a86949998998852608060208901526080880190611c26565b918683036040880152612e52565b906001600160a01b03929560209761409195996140c861407f61410d95615887565b6140ba604051998a928e840190613ff9565b7f6368616c6c656e67650000000000000000000000000000000000000000000000815260090190565b03601f198101895288612707565b6140d18161400b565b61415a57505b604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b0392165afa80156104885761273a915f9161412b575b501515613b01565b61414d915060203d602011614153575b6141458183612707565b810190614015565b5f614123565b503d61413b565b90506140d7565b5f6040519161416f836126ae565b81835261420260208401614181613b66565b81526141f46040860191858352611e806004606089019288845260808a01958987526141bc60a08c01998b8b525f52600260205260405f2090565b9160ff6001840154166141ce81611c00565b8c526141dc600684016128a0565b90526005820154905201549167ffffffffffffffff83165b67ffffffffffffffff169052565b5290565b9060405191614214836126ae565b5f835261420260208401614226613b66565b81526141f460408601915f8352611e80600460608901925f845260808a01955f87526141bc60a08c01995f8b525f52600260205260405f2090565b7f8000000000000000000000000000000000000000000000000000000000000000811461059a575f0390565b6020939291614329919796976142ab815f52600260205260405f2090565b976040860180516142bb81611c00565b6142c481611c00565b61450a575b5089888660a08901956142dc8751151590565b6144f6575b5050505050506142fc606085015167ffffffffffffffff1690565b67ffffffffffffffff81166144cb575b50608084015167ffffffffffffffff168061447c575b5051151590565b1561446357608001518201516001600160a01b031680935b8251905f82131561442357614363915061435b8451615ad0565b928391614527565b61437260058601918254612c10565b90555b0180515f8113156143d15750916143b060059261239b6143986143c69651615ad0565b966001600160a01b03165f52600660205260405f2090565b6143bb858254613352565b905501918254612c10565b90555b61273a61467e565b9290505f83126143e5575b505050506143c9565b61440260059261239b6143986143fd61441897614261565b615ad0565b61440d858254612c10565b905501918254613352565b90555f8080806143dc565b5f8212614433575b505050614375565b6144426143fd61444a93614261565b928391614d9d565b61445960058601918254613352565b9055825f8061442b565b50600d84015460401c6001600160a01b03168093614341565b6144c59060048901907fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff6fffffffffffffffff000000000000000083549260401b169116179055565b5f614322565b6144f090600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f61430c565b6144ff956159a2565b5f80898886836142e1565b614521905161451881611c00565b60018b016136a3565b5f6142c9565b9061453a9291614535615810565b61458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b1561456757565b7fd2ade556000000000000000000000000000000000000000000000000000000005f5260045ffd5b908215614679576001600160a01b0316918215801561466a576145b3823414614560565b156145bd57505050565b6001600160a01b03604051927f23b872dd000000000000000000000000000000000000000000000000000000005f52166004523060245260445260205f60648180865af160015f5114811615614654575b6040919091525f606052156146205750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b031660045260245ffd5b6001811516612630573d15833b1515161661460e565b6146743415614560565b6145b3565b505050565b6003546004545f5b82821080614776575b1561476b576146a36106a2610698846131a0565b6001810160036146b4825460ff1690565b6146bd81611c00565b14614759576146cb82615b4d565b1561471657915f8261076961470d95600561470796019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b916139c6565b915b9190614686565b5050915061472390600455565b8061472b5750565b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1565b505090614765906139c6565b9161470f565b915061472390600455565b506040811061468f565b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f01000000000000000000000000000000000000000000000000000000000000009160405161482760208201809360a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b60c0815261483660e082612707565b519020161790565b602081013561484c8161048d565b6001600160a01b03811690614862821515612b9e565b6040830135906148718261048d565b6148986001600160a01b0383169261488a841515612b9e565b6148938361048d565b61048d565b6148a18161048d565b5081146148ed575063ffffffff6201518091356148bd81612c60565b16106148c557565b7f0596b15b000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fabfa558d000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b903590601e1981360301821215610277570180359067ffffffffffffffff82116102775760200191813603831361027757565b929161273a9461497c61498b92614971838761496b610220890189614918565b90613eff565b90878a949394615b81565b8361496b610240850185614918565b92909194615b81565b604051906149a1826126eb565b5f6080838281526149b0613b66565b60208201528260408201528260608201520152565b90601467ffffffffffffffff916149da614994565b935f525f60205260405f20906149f460ff83541686613bb6565b614a00600583016128a0565b6020860152601382015460408601526060850152015416608082015290565b9060a060039163ffffffff8151167fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000008554161784556001600160a01b036020820151167fffffffffffffffff0000000000000000000000000000000000000000ffffffff77ffffffffffffffffffffffffffffffffffffffff0000000086549260201b169116178455614b4160018501614aef614ac660408501516001600160a01b031690565b82906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b606083015181547fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff1660a09190911b7bffffffffffffffff000000000000000000000000000000000000000016179055565b608081015160028501550151910155565b92614b8e81614bde9460a094614b6f885f525f60205260405f2090565b97614b7b895460ff1690565b614b84816121c1565b15614c5a57615443565b604081018051614b9d816121c1565b614ba6816121c1565b151580614c2f575b614c15575b50601484018054606083015167ffffffffffffffff9081169116819003614bed575b50500151151590565b614be55750565b60135f910155565b614c0e919067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f80614bd5565b614c299051614c23816121c1565b85613403565b5f614bb3565b50845460ff16815190614c41826121c1565b614c4a826121c1565b614c53816121c1565b1415614bae565b614c678260018b01614a1f565b615443565b9067ffffffffffffffff604051916020830193845216604082015260408152614c96606082612707565b51902090565b805f525f60205260ff60405f2054166006811015611c0a578015908115614ce5575b50614ce0575f525f60205267ffffffffffffffff600760405f20015416461490565b505f90565b60059150614cf2816121c1565b145f614cbe565b90614d3b91614d146001610d31835f525f60205260405f2090565b60c0836108ff614d346108ec61071460408701516001600160a01b031690565b54856149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af49283156104885761273a945f94614d78575b50614d7290369061301c565b91614b52565b614d72919450614d969060c03d60c011610ad357610ac58183612707565b9390614d66565b9061453a9291614dab615810565b9190918115614679576001600160a01b0383169283614e4f576001600160a01b038216925f8080808488620186a0f1614de2613c20565b5015614def575050505050565b614e326138999261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614e3d828254612c10565b90556040519081529081906020820190565b6040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481885afa908115610488575f91615006575b506040517fa9059cbb00000000000000000000000000000000000000000000000000000000602082019081526001600160a01b0385166024830152604480830187905282525f91829190614ee7606482612707565b51908286620186a0f190614ef9613c20565b506040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526020816024818a5afa9182156104885786915f93614fe5575b5083614fda575b83614fc6575b50505015614f5b575b50505050565b81614fa46001600160a01b039261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614faf858254612c10565b90556040519384521691602090a35f808080614f55565b614fd1929350613352565b145f8481614f4c565b818110159350614f46565b614fff91935060203d602011614153576141458183612707565b915f614f3f565b61501f915060203d602011614153576141458183612707565b5f614e92565b909192614d14614d3b946150456001610d31865f525f60205260405f2090565b92608084015191868661494b565b906001600160a01b0390615065614994565b925f52600560205267ffffffffffffffff600460405f2060ff60018201541661508d81611c00565b865261509b600682016128a0565b602087015260058101546040870152015416606084015216608082015290565b6020939291614329919796976150d9815f52600560205260405f2090565b976040860180516150e981611c00565b6150f281611c00565b615179575b50898886608089019561510a8751151590565b615165575b50505050505061512a606085015167ffffffffffffffff1690565b67ffffffffffffffff8116615140575051151590565b6144c590600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b61516e95615d2e565b5f808988868361510f565b615187905161451881611c00565b5f6150f7565b90600a811015611c0a577fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff68ff000000000000000083549260401b169116179055565b9060c060049161520367ffffffffffffffff825116859067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015184546040808401517fffffff000000000000000000000000000000000000000000ffffffffffffffff90921692901b7bffffffffffffffffffffffffffffffffffffffff0000000000000000169190911760e09190911b7cff0000000000000000000000000000000000000000000000000000000016178455606081015160018501556080810151600285015560a081015160038501550151910155565b601f82116152b357505050565b5f5260205f20906020601f840160051c830193106152eb575b601f0160051c01905b8181106152e0575050565b5f81556001016152d5565b90915081906152cc565b919091825167ffffffffffffffff81116126ca5761531d8161531784546127cb565b846152a6565b6020601f82116001146153585781906131eb9394955f9261534d575b50508160011b915f199060031b1c19161790565b015190505f80615339565b601f1982169061536b845f5260205f2090565b915f5b8181106153a55750958360019596971061538d575b505050811b019055565b01515f1960f88460031b161c191690555f8080615383565b9192602060018192868b01518155019401920161536e565b8151815467ffffffffffffffff191667ffffffffffffffff91909116178155602082015191600a831015611c0a5760c0600d916153fd61273a958561518d565b604081015160018501556154186060820151600286016151d0565b6154296080820151600786016151d0565b61543a60a0820151600c86016152f5565b015191016152f5565b6154596060919493945f525f60205260405f2090565b936154676080850151151590565b61563a575b01916154bf60a06154886020865101516001600160a01b031690565b9280515f81136155fc575b506020810180515f81136155b4575b5081515f8112615573575b50515f8112615528575b500151151590565b8061551a575b6154d6575b5050505061273a61467e565b61550f9261550260a0926154f6604060139601516001600160a01b031690565b90848451015191614d9d565b5101519201918254613352565b90555f8080806154ca565b5060a08351015115156154c5565b6143fd61553491614261565b61554f8561239b61071460408a01516001600160a01b031690565b61555a828254612c10565b905561556b60138901918254613352565b90555f6154b7565b6143fd61557f91614261565b61559d818761559860208b01516001600160a01b031690565b614d9d565b6155ac60138a01918254613352565b90555f6154ad565b6155bd90615ad0565b6155d88661239b61071460408b01516001600160a01b031690565b6155e3828254613352565b90556155f460138a01918254612c10565b90555f6154a2565b61560590615ad0565b615623818661561e60208a01516001600160a01b031690565b614527565b61563260138901918254612c10565b90555f615493565b61564781600587016153bd565b61546c565b805192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156157e8575b806d04ee2d6d415b85acef8100000000600a9210156157cc575b662386f26fc100008110156157b7575b6305f5e1008110156157a5575b612710811015615795575b6064811015615786575b101561577b575b61571260216156da60018801615ddf565b968701015b5f1901917f3031323334353637383961626364656600000000000000000000000000000000600a82061a8353600a900490565b90811561572257615712906156df565b50506001600160a01b036157478461573b858498615d73565b60208151910120615dc9565b9116931683146157735761576591816020611aad9351910120615dc9565b1461576e575f90565b600190565b505050600190565b6001909401936156c9565b600290606490049601956156c2565b60049061271090049601956156b8565b6008906305f5e10090049601956156ad565b601090662386f26fc1000090049601956156a0565b6020906d04ee2d6d415b85acef81000000009004960195615690565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008104615676565b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00541461585f5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff815116906020810151600a811015611c0a576159308260406159919401516158cf60806060840151930151946040519760208901526040880190611c19565b6060860152608085019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b805167ffffffffffffffff1661016084015260208101516001600160a01b0316610180840152604081015160ff166101a084015260608101516101c084015260808101516101e084015260a081015161020084015260c00151610220830152565b610220815261084d61024082612707565b93909291935f52600260205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015192600a841015611c0a57615a5660c0615aa093615a0e615acc9760039a61518d565b60408101516007890155615a29606082015160088a016151d0565b615a3a6080820151600d8a016151d0565b615a4b60a082015160128a016152f5565b0151601387016152f5565b60018501907fffffffffffffffffffffff0000000000000000000000000000000000000000ff74ffffffffffffffffffffffffffffffffffffffff0083549260081b169116179055565b60028301906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b0155565b5f8112615ada5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b90604051918281549182825260208201905f5260205f20925f5b818110615b3457505061273a92500383612707565b8454835260019485019487945060209093019201615b1f565b67ffffffffffffffff6004820154164210159081615b69575090565b600180925060ff91015416615b7d81611c00565b1490565b6001600160a01b039061410d615ba7615ba26020989599969799369061301c565b615887565b93604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b6001810190825f528160205260405f2054155f14615c46578054680100000000000000008110156126ca57615c33615c1d8260018794018555846131bd565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b5050505f90565b80548015615c74575f190190615c6382826131bd565b8154905f199060031b1b1916905555565b634e487b7160e01b5f52603160045260245ffd5b6001810191805f528260205260405f2054928315155f14615d26575f19840184811161059a5783545f1981019490851161059a575f958583615ce397615cd69503615ce9575b505050615c4d565b905f5260205260405f2090565b55600190565b615d0f615d0991615d00610698615d1d95886131bd565b928391876131bd565b906131d2565b85905f5260205260405f2090565b555f8080615cce565b505050505f90565b93909291935f52600560205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b61273a90615dbb615db594936040519586937f19457468657265756d205369676e6564204d6573736167653a0a0000000000006020860152603a850190613ff9565b90613ff9565b03601f198101845283612707565b61084d91615dd691615e06565b90929192615e40565b90615de982612faf565b615df66040519182612707565b828152601f196133488294612faf565b8151919060418303615e3657615e2f9250602082015190606060408401519301515f1a90615f07565b9192909190565b50505f9160029190565b615e4981611c00565b80615e52575050565b615e5b81611c00565b60018103615e8b577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b615e9481611c00565b60028103615ec857507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b80615ed4600392611c00565b14615edc5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411615f7e579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610488575f516001600160a01b03811615615f7457905f905f90565b505f906001905f90565b5050505f916003919056fea2646970667358221220a1d82448e7e3f611b69660be3f5dc6e070ca23bb9e11aefc6fc6b7622d1cdc4e64736f6c634300081e00330000000000000000000000002a35728cadd8076dfd424fc3e20974a3cd03bfa5",
+ "nonce": "0x6",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ }
+ ],
+ "receipts": [
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x1410b0",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000008e12cca7c1e150000000000000000000000000000000000000000000000000a467417de9a33bd1000000000000000000000000000000000000000000002b297badcd163bced3e7000000000000000000000000000000000000000000000000a3d92eb141e15a81000000000000000000000000000000000000000000002b297c3bdfe2e390b537",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "transactionIndex": "0x0",
+ "logIndex": "0x0",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "transactionIndex": "0x0",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0x1410b0",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x214c9e",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000005db48e1b848b52000000000000000000000000000000000000000000000000a3d92eb13cf13f31000000000000000000000000000000000000000000002b297c3bdfe2e390b537000000000000000000000000000000000000000000000000a37b7a23216cb3df000000000000000000000000000000000000000000002b297c999470ff154089",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "transactionIndex": "0x1",
+ "logIndex": "0x1",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "transactionIndex": "0x1",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0xd3bee",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x2e18fc",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000005a9ea056b694e2000000000000000000000000000000000000000000000000a37b7a231e2af44d000000000000000000000000000000000000000000002b297c999470ff154089000000000000000000000000000000000000000000000000a320db82c7745f6b000000000000000000000000000000000000000000002b297cf4331155cbd56b",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "transactionIndex": "0x2",
+ "logIndex": "0x2",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "transactionIndex": "0x2",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0xccc5e",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x34a379",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002e505f361a7163000000000000000000000000000000000000000000000000a320db82c44e1449000000000000000000000000000000000000000000002b297cf4331155cbd56b000000000000000000000000000000000000000000000000a2f28b238e33a2e6000000000000000000000000000000000000000000002b297d2283708be646ce",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "transactionIndex": "0x3",
+ "logIndex": "0x3",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "transactionIndex": "0x3",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0x68a7d",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0x2a35728cadd8076dfd424fc3e20974a3cd03bfa5"
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x867909",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000002436f597d48d070000000000000000000000000000000000000000000000000a2f28b238c978e23000000000000000000000000000000000000000000002b297d2283708be646ce000000000000000000000000000000000000000000000000a0af1bca0f4ebdb3000000000000000000000000000000000000000000002b297f65f2ca092f173e",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "transactionIndex": "0x4",
+ "logIndex": "0x4",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "transactionIndex": "0x4",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0x51d590",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0x55d6f0a0322606447fbc612cf58014faed65af9d"
+ }
+ ],
+ "libraries": [
+ "src/ChannelEngine.sol:ChannelEngine:0x78D150fdA6fa6739C18014B347c7c7C45C58e148",
+ "src/EscrowDepositEngine.sol:EscrowDepositEngine:0x728904E52308213bA61C90EF49F34c18Fbda9E11",
+ "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine:0x893F2D45fDFFe2D4297a5C1D5732EDce4849eE82"
+ ],
+ "pending": [],
+ "returns": {},
+ "timestamp": 1772893715802,
+ "chain": 80002,
+ "commit": "4b4244f1"
+}
\ No newline at end of file
diff --git a/contracts/broadcast/DeployChannelHub.s.sol/80002/run-latest.json b/contracts/broadcast/DeployChannelHub.s.sol/80002/run-latest.json
new file mode 100644
index 000000000..7434592c5
--- /dev/null
+++ b/contracts/broadcast/DeployChannelHub.s.sol/80002/run-latest.json
@@ -0,0 +1,278 @@
+{
+ "transactions": [
+ {
+ "hash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "transactionType": "CREATE2",
+ "contractName": "ChannelEngine.channelhub",
+ "contractAddress": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x1bb70c",
+ "input": "0x0000000000000000000000000000000000000000000000000000000000000000608080604052346019576116f0908161001e823930815050f35b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c63a8b4483c14610025575f80fd5b604060031936011261122d5760043567ffffffffffffffff811161122d5760a0600319823603011261122d5760a0820182811067ffffffffffffffff821117611299576040528060040135600681101561122d578252602481013567ffffffffffffffff811161122d5761009f90600436918401016113e1565b602083019081526040830192604483013584526100c96084606083019460648101358652016112ec565b6080820190815260243567ffffffffffffffff811161122d576100f09036906004016113e1565b6100f8611498565b50606081019367ffffffffffffffff855151164603610dc45767ffffffffffffffff82511681519067ffffffffffffffff82511610908115611261575b5015610a085784516040810190601260ff83511611611239574667ffffffffffffffff825116146110ff575b505060208201928351600a8110156103585760041480156110eb575b80156110d7575b80156110c3575b80156110af575b801561109b575b1561105e576080830167ffffffffffffffff815151161561103657515167ffffffffffffffff16461461100e575b6101dc865160a06060820151910151906114e6565b6101f1875160c06080820151910151906114f3565b5f8112610fe65761020190611526565b03610fbe578451600681101561035857600214610f7f575b50610222611498565b5061023c608086510151608060608451015101519061150e565b9061025660c08751015160c060608451015101519061150e565b9351600a81101561035857600281036104b65750509050610275611498565b928051600681101561035857159081156104a0575b811561048a575b8115610475575b501561044d575f8113156104255782526020820152600160408201525f6060820152925b6102d96102d1608086019260018452516115a7565b8551906114f3565b926102ea60208601948551906114f3565b5f81126103fd5760a08601938451156103ab575b50508351905f821361036c575b50506040519284518452516020840152604084015193600685101561035857606067ffffffffffffffff9160c09660408701520151166060840152511515608083015251151560a0820152f35b634e487b7160e01b5f52602160045260245ffd5b610377905191611526565b11610383575f8061030b565b7f2e3b1ec0000000000000000000000000000000000000000000000000000000005f5260045ffd5b6103c36103c9915160a06060820151910151906114e6565b91611526565b036103d5575f806102fe565b7f8f9003ee000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fae0bb491000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610298565b8091505160068110156103585760021490610291565b809150516006811015610358576001149061028a565b6003810361055657505090506104ca611498565b92805160068110156103585715908115610540575b811561052a575b8115610515575b501561044d575f8112156104255782526020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f6104ed565b80915051600681101561035857600214906104e6565b80915051600681101561035857600114906104df565b8061061f5750509050610567611498565b92805160068110156103585715908115610609575b81156105f3575b81156105de575b501561044d576104255760a0835101516105b6576020820152600160408201525f6060820152926102bc565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61058a565b8091505160068110156103585760021490610583565b809150516006811015610358576001149061057c565b600181036107185750509050610633611498565b928051600681101561035857600114908115610702575b81156106ed575b501561044d5761066c845160a06060820151910151906114e6565b8651106106c55761068a82610685836106858a516115a7565b6114f3565b5f81126103fd5761069f60a0865101516115a7565b136103fd5782526020820152600360408201525f6060820152600160a0820152926102bc565b7f7fa0800f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610651565b809150516006811015610358576002149061064a565b6004810361083857505061072a611498565b938051600681101561035857600114908115610822575b811561080d575b501561044d57610425576080016060815101519081156107e55761077a855160ff604060a0830151920151169061161e565b61078c60ff604084510151168461161e565b036105b65760806107a091510151916115a7565b036107bd576020820152600160408201525f6060820152926102bc565b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f610748565b8091505160068110156103585760021490610741565b909391929060058103610a83575061084e611498565b948051600681101561035857600114908115610a6d575b8115610a58575b501561044d5761087f60208551016114d9565b600a81101561035857600403610a305767ffffffffffffffff81511667ffffffffffffffff6108b1818751511661155b565b1603610a0857608001916060835101516107e55760a0835101516105b65760a0865101516105b657610425576109e05760606080835101510151906080815101516108fb836115a7565b036107bd575160c00151610916610911836115a7565b61157b565b036109b8576060845101519060608084510151015182039182116109a45760ff6040608061094e61095b9584848b510151169061161e565b955101510151169061161e565b0361097c575f81525f6020820152600160408201525f6060820152926102bc565b7f733d14c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fd916ea0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f7dcd8ffd000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576004145f61086c565b8091505160068110156103585760021490610865565b9193909160068103610b3f57505090610a9a611498565b938051600681101561035857600114908115610b29575b8115610b14575b501561044d576104255760a0845101516105b6576080016080815101516107bd576060815101516107e55760c0610af360a0835101516115a7565b91510151036109b8576020820152600160408201525f6060820152926102bc565b9050516006811015610358576004145f610ab8565b8091505160068110156103585760021490610ab1565b60078103610bd957505090610b52611498565b938051600681101561035857600114908115610bc3575b8115610bae575b501561044d576104255760a0845101516105b6576080016060815101516107e55760a0815101516105b657516107a060c0608083015192015161157b565b9050516006811015610358576004145f610b70565b8091505160068110156103585760021490610b69565b60088103610e1557505090610bec611498565b938051600681101561035857158015610e01575b15610ce5575050608001805160600151915081156107e55760a0815101516105b6576060845101516107e557610c44845160ff604060a0830151920151169061161e565b610c5660ff604084510151168461161e565b03610cbd57610c8d9060ff6040610c82610c7c8851848460c0830151920151169061165b565b956115a7565b92510151169061165b565b036109b8576080825101516107bd57610caa60a0835101516115a7565b6020820152600460408201525b926102bc565b7f7b208b9d000000000000000000000000000000000000000000000000000000005f5260045ffd5b8051600681101561035857600114908115610dec575b501561044d574667ffffffffffffffff8651511603610dc457610425576060845101519081156107e55760a0855101516105b657608001906060825101516107e557610d55825160ff604060a0830151920151169061161e565b610d6760ff604088510151168361161e565b03610cbd57610d9f610d90610d8a845160ff604060c0830151920151169061165b565b926115a7565b60ff604088510151169061165b565b036109b85751608001516107bd576020820152600160408201525f6060820152610cb7565b7f67525583000000000000000000000000000000000000000000000000000000005f5260045ffd5b9050516006811015610358576002145f610cfb565b508051600681101561035857600514610c00565b600903610f5757610e24611498565b948051600681101561035857600403610ed957504667ffffffffffffffff8751511603610dc457610e5860208251016114d9565b600a81101561035857600803610a305767ffffffffffffffff82511667ffffffffffffffff610e8a818451511661155b565b1603610a0857606080915101510151606086510151036107e55760a0855101516105b6576080016060815101516107e5575160a001516105b657610425576109e05760016040820152926102bc565b919250508051600681101561035857600114908115610f42575b501561044d576060845101516107e55760a0845101516105b657608001606081510151156107e5575160a001516105b6576020820152600560408201525f6060820152600160a0820152610cb7565b9050516006811015610358576002145f610ef3565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b5167ffffffffffffffff164211610f96575f610219565b7ff06506c5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7ff019de0e000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f114a9df4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f26c21ae4000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff60808401515116156101c7577f4c7b586e000000000000000000000000000000000000000000000000000000005f5260045ffd5b508351600a81101561035857600914610199565b508351600a81101561035857600814610192565b508351600a8110156103585760071461018b565b508351600a81101561035857600614610184565b508351600a8110156103585760051461017d565b6020015173ffffffffffffffffffffffffffffffffffffffff168061115b575060ff601291511603611133575b5f80610161565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f92816111f7575b506111c2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461112c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011611231575b81611213602093836112c9565b8101031261122d575160ff8116810361122d57915f611195565b5f80fd5b3d9150611206565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b60608101515167ffffffffffffffff1615915081611281575b505f610135565b67ffffffffffffffff9150608001515116155f61127a565b634e487b7160e01b5f52604160045260245ffd5b60e0810190811067ffffffffffffffff82111761129957604052565b90601f601f19910116810190811067ffffffffffffffff82111761129957604052565b359067ffffffffffffffff8216820361122d57565b91908260e091031261122d57604051611319816112ad565b8092611324816112ec565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361122d576020830152604081013560ff8116810361122d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561122d5780359067ffffffffffffffff821161129957604051926113c06020601f19601f86011601856112c9565b8284526020838301011161122d57815f926020809301838601378301015290565b91906102608382031261122d57604051906113fb826112ad565b8193611406816112ec565b83526020810135600a81101561122d576020840152604081013560408401526114328260608301611301565b6060840152611445826101408301611301565b608084015261022081013567ffffffffffffffff811161122d578261146b91830161138b565b60a08401526102408101359167ffffffffffffffff831161122d5760c092611493920161138b565b910152565b6040519060c0820182811067ffffffffffffffff821117611299576040525f60a0838281528260208201528260408201528260608201528260808201520152565b51600a8110156103585790565b919082018092116109a457565b9190915f83820193841291129080158216911516176109a457565b81810392915f1380158285131691841216176109a457565b5f81126115305790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b67ffffffffffffffff60019116019067ffffffffffffffff82116109a457565b7f800000000000000000000000000000000000000000000000000000000000000081146109a4575f0390565b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81116115d15790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116109a457565b60ff16604d81116109a457600a0a90565b9060ff811660128111611239576012146116575761163e611643916115fc565b61160d565b908181029181830414901517156109a45790565b5090565b9060ff811660128111611239576012146116575761163e61167b916115fc565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166109a45781830514901517156109a4579056fea264697066735822122036f6b0f3261f4d84fa391cd2e29d848110238f6d49d373a5912f2304cae9c86d64736f6c634300081e0033",
+ "nonce": "0x2",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowWithdrawalEngine.channelhub",
+ "contractAddress": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x124792",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610edc908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8062ea54e714610118576324063eba1461002e575f80fd5b60206003193601126101145760043567ffffffffffffffff81116101145761005a903690600401610c2a565b610062610ced565b90516004811015610100575f19016100d857600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff82116100c45767ffffffffffffffff6100c0921660608201525f608082015260405191829182610ca2565b0390f35b634e487b7160e01b5f52601160045260245ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b5f80fd5b60406003193601126101145760043567ffffffffffffffff811161011457610144903690600401610c2a565b60243567ffffffffffffffff811161011457610164903690600401610b73565b61016c610ced565b5081516004811015610100576003146109dc5767ffffffffffffffff461660608201908067ffffffffffffffff83515116146109b457608083019067ffffffffffffffff825151160361098c5767ffffffffffffffff835116156107a25780516040810190601260ff83511611610964574667ffffffffffffffff8251161461082e575b5050805160a0606082015191015181018091116100c45761021c825160c0608082015191015190610d17565b5f81126108065761022c90610d5e565b036107de57610239610ced565b5060208301928351600a811015610100576006810361052657505061025c610ced565b9184516004811015610100576104fe576060825101516104d6576080825101516104ae5781519160c060a084015193015161029684610d93565b03610486576102c360ff60406102b88551838360608301519201511690610e0a565b935101511684610e0a565b1161045e575160a00151610436576102da90610d93565b60208201526001604082015260016080820152915b825115801590610429575b15610401578251906103126020850192835190610d17565b928051600a81101561010057600603610366575050510361033e576100c0905b60405191829182610ca2565b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092915051600a81101561010057600714610387575b50506100c090610332565b8251036103d95760406103a261039d8451610d32565b610d5e565b910151036103b157818061037c565b7fd9132288000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b50602083015115156102fa565b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f06b4cdae000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f4c66f955000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b90929060070361077a57610538610ced565b92855160048110156101005760011480156107ca575b156100d85767ffffffffffffffff9051166020860190600167ffffffffffffffff835151160167ffffffffffffffff81116100c45767ffffffffffffffff16036107a257602081510151600a811015610100577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0161077a5760a06080825101510151926060815101516104d6576080815101516105f36105ee86610d93565b610d32565b036104ae5760a081510151610436575160c0015161061084610d93565b036107025760608251015160608083510151015111156107525760608082510151015160608351015181039081116100c4576106559060ff6040855101511690610e0a565b61066b60ff604060808551015101511685610e0a565b0361072a5760c08251015160c06060835101510151905f82820392128183128116918313901516176100c4575f81121561070257604060806106cb6106c56106d89660ff856106ba8298610d32565b925101511690610e47565b96610d93565b9351015101511690610e47565b03610486576106ed6105ee6040850151610d93565b8152600360408201525f6080820152916102ef565b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fffda345d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f25e3e1b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156101005760021461054e565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061088a575060ff601291511603610862575b84806101f0565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610926575b506108f1577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461085b577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d60201161095c575b8161094260209383610a50565b81010312610114575160ff811681036101145791876108c4565b3d9150610935565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff821117610a2057604052565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff821117610a2057604052565b90601f601f19910116810190811067ffffffffffffffff821117610a2057604052565b359067ffffffffffffffff8216820361011457565b359073ffffffffffffffffffffffffffffffffffffffff8216820361011457565b91908260e091031261011457604051610ac181610a04565b8092610acc81610a73565b8252610ada60208201610a88565b6020830152604081013560ff811681036101145760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f820112156101145780359067ffffffffffffffff8211610a205760405192610b526020601f19601f8601160185610a50565b8284526020838301011161011457815f926020809301838601378301015290565b9190610260838203126101145760405190610b8d82610a04565b8193610b9881610a73565b83526020810135600a81101561011457602084015260408101356040840152610bc48260608301610aa9565b6060840152610bd7826101408301610aa9565b608084015261022081013567ffffffffffffffff81116101145782610bfd918301610b1d565b60a08401526102408101359167ffffffffffffffff83116101145760c092610c259201610b1d565b910152565b91909160a0818403126101145760405190610c4482610a34565b81938135600481101561011457835260208201359067ffffffffffffffff82116101145782610c7c60809492610c2594869401610b73565b602086015260408101356040860152610c9760608201610a73565b606086015201610a88565b91909160a0810192805182526020810151602083015260408101516004811015610100576080918291604085015267ffffffffffffffff606082015116606085015201511515910152565b60405190610cfa82610a34565b5f6080838281528260208201528260408201528260608201520152565b9190915f83820193841291129080158216911516176100c457565b7f800000000000000000000000000000000000000000000000000000000000000081146100c4575f0390565b5f8112610d685790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610dbd5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b60ff166012039060ff82116100c457565b60ff16604d81116100c457600a0a90565b9060ff81166012811161096457601214610e4357610e2a610e2f91610de8565b610df9565b908181029181830414901517156100c45790565b5090565b9060ff81166012811161096457601214610e4357610e2a610e6791610de8565b90818102917f800000000000000000000000000000000000000000000000000000000000000081145f8312166100c45781830514901517156100c4579056fea264697066735822122073585d1c2949228993d38506ffc5f542f9ffb1c023c1893a2f5522e50227b27564736f6c634300081e0033",
+ "nonce": "0x3",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "transactionType": "CREATE2",
+ "contractName": "EscrowDepositEngine.channelhub",
+ "contractAddress": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "gas": "0x11ad7a",
+ "input": "0x000000000000000000000000000000000000000000000000000000000000000060808060405234601957610e55908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80636666e4c0146109095763bbc42f341461002f575f80fd5b604060031936011261085d5760043567ffffffffffffffff811161085d5761005b903690600401610bf4565b60243567ffffffffffffffff811161085d5761007b903690600401610b3d565b610083610cd9565b5081516004811015610343576003146108e15767ffffffffffffffff46169060608101918067ffffffffffffffff84515116146108b957608082019067ffffffffffffffff82515116036108915767ffffffffffffffff8251161561067b5780516040810190601260ff83511611610869574667ffffffffffffffff8251161461072f575b5050805160a06060820151910151810180911161038c57610134825160c0608082015191015190610d09565b5f81126107075761014490610d50565b036106df57610151610cd9565b5060208201928351600a81101561034357600481036104685750909150610176610cd9565b918451600481101561034357610440578051916080606084015193015161019c84610d85565b036104185760a0825101516103f05760c0825101516103c85760ff60406101d26101dd9351838360a08301519201511690610dda565b935101511683610dda565b036103a0576101eb90610d85565b815260016040820152612a3067ffffffffffffffff42160167ffffffffffffffff811161038c5767ffffffffffffffff166060820152600160a0820152915b82511580159061037f575b1561035757825161024c6020850191825190610d09565b928051600a811015610343576004036102a65750505081510361027e5761027a905b60405191829182610c7a565b0390f35b7f8041118f000000000000000000000000000000000000000000000000000000005f5260045ffd5b9290919251600a811015610343576005146102c8575b50505061027a9061026e565b81510361031b576102e36102de60409251610d24565b610d50565b910151036102f3575f80806102bc565b7fb09443e7000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f8b8380f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52602160045260245ffd5b7f17bc734e000000000000000000000000000000000000000000000000000000005f5260045ffd5b5060208301511515610235565b634e487b7160e01b5f52601160045260245ffd5b7fe19f88d5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f0c18740d000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa5eabfa5000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f76ac27ca000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fed778779000000000000000000000000000000000000000000000000000000005f5260045ffd5b60050361065357610477610cd9565b92855160048110156103435760011480156106cb575b156106a35767ffffffffffffffff905116916020860192600167ffffffffffffffff855151160167ffffffffffffffff811161038c5767ffffffffffffffff160361067b57602083510151600a811015610343576003190161065357606060808451015101519060808151015161050383610d85565b036104185760c08151015161051f61051a84610d85565b610d24565b036103c85760608151015161062b575160a001516103f057606082510151606080855101510151810390811161038c576105656105799160ff6040865101511690610dda565b9160ff604060808751015101511690610dda565b036106035760a0815101516103f057606060808092510151925101510151908181035f831282808312821692139015161761038c57036105db576105c361051a6040850151610d85565b6020820152600360408201525f60a08201529161022a565b7f1180da8f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fff0edb30000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f2c0a0276000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f07646e49000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f392ebf28000000000000000000000000000000000000000000000000000000005f5260045ffd5b50855160048110156103435760021461048d565b7f92ad5c75000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe6af4070000000000000000000000000000000000000000000000000000000005f5260045ffd5b6020015173ffffffffffffffffffffffffffffffffffffffff168061078b575060ff601291511603610763575b5f80610108565b7f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b906020600492604051938480927f313ce5670000000000000000000000000000000000000000000000000000000082525afa5f9281610827575b506107f2577f6afa2af9000000000000000000000000000000000000000000000000000000005f5260045ffd5b60ff8091511691161461075c577f5a8dbaed000000000000000000000000000000000000000000000000000000005f5260045ffd5b9092506020813d602011610861575b8161084360209383610a25565b8101031261085d575160ff8116810361085d57915f6107c5565b5f80fd5b3d9150610836565b7fb016c3f4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f9ba78e55000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f21e65f65000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f69155645000000000000000000000000000000000000000000000000000000005f5260045ffd5b602060031936011261085d5760043567ffffffffffffffff811161085d57610935903690600401610bf4565b61093d610cd9565b9080516004811015610343575f19016106a3576060015167ffffffffffffffff164210156109b157600260408201526201518067ffffffffffffffff4216019067ffffffffffffffff821161038c5767ffffffffffffffff61027a921660808201525f60a082015260405191829182610c7a565b7f2b39d042000000000000000000000000000000000000000000000000000000005f5260045ffd5b60e0810190811067ffffffffffffffff8211176109f557604052565b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176109f557604052565b90601f601f19910116810190811067ffffffffffffffff8211176109f557604052565b359067ffffffffffffffff8216820361085d57565b91908260e091031261085d57604051610a75816109d9565b8092610a8081610a48565b8252602081013573ffffffffffffffffffffffffffffffffffffffff8116810361085d576020830152604081013560ff8116810361085d5760c09182916040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b81601f8201121561085d5780359067ffffffffffffffff82116109f55760405192610b1c6020601f19601f8601160185610a25565b8284526020838301011161085d57815f926020809301838601378301015290565b91906102608382031261085d5760405190610b57826109d9565b8193610b6281610a48565b83526020810135600a81101561085d57602084015260408101356040840152610b8e8260608301610a5d565b6060840152610ba1826101408301610a5d565b608084015261022081013567ffffffffffffffff811161085d5782610bc7918301610ae7565b60a08401526102408101359167ffffffffffffffff831161085d5760c092610bef9201610ae7565b910152565b91909160c08184031261085d5760405190610c0e82610a09565b81938135600481101561085d57835260208201359167ffffffffffffffff831161085d57610c4260a0939284938301610b3d565b602085015260408101356040850152610c5d60608201610a48565b6060850152610c6e60808201610a48565b60808501520135910152565b91909160c08101928051825260208101516020830152604081015160048110156103435760a0918291604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff608082015116608085015201511515910152565b60405190610ce682610a09565b5f60a0838281528260208201528260408201528260608201528260808201520152565b9190915f838201938412911290801582169115161761038c57565b7f8000000000000000000000000000000000000000000000000000000000000000811461038c575f0390565b5f8112610d5a5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8111610daf5790565b7f24775e06000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b9060ff16601281116108695760128114610e1b5760120360ff811161038c5760ff16604d811161038c57600a0a9081810291818304149015171561038c5790565b509056fea2646970667358221220fc0a93f7abd0c8aae0f4edd1fab1eef03232af831542ee9ea9f3dcf8d76c3da064736f6c634300081e0033",
+ "nonce": "0x4",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "transactionType": "CREATE",
+ "contractName": "ECDSAValidator.channelhub",
+ "contractAddress": "0x2a35728cadd8076dfd424fc3e20974a3cd03bfa5",
+ "function": null,
+ "arguments": null,
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x880d5",
+ "value": "0x0",
+ "input": "0x608080604052346015576106d6908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c63600109bb14610024575f80fd5b346100cc5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100cc5760243567ffffffffffffffff81116100cc576100739036906004016100d0565b9060443567ffffffffffffffff81116100cc576100949036906004016100d0565b6064359173ffffffffffffffffffffffffffffffffffffffff831683036100cc576020946100c4946004356101a0565b604051908152f35b5f80fd5b9181601f840112156100cc5782359167ffffffffffffffff83116100cc57602083818601950101116100cc57565b90601f601f19910116810190811067ffffffffffffffff82111761012157604052565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161012157601f01601f191660200190565b9291926101768261014e565b9161018460405193846100fe565b8294818452818301116100cc578281602093845f960137010152565b929091949383156102635773ffffffffffffffffffffffffffffffffffffffff85161561023b5761022060806101de6102279561022d99369161016a565b95601f19601f6020604051998a94828601526040808601528051918291826060880152018686015e5f858286010152011681010301601f1981018652856100fe565b369161016a565b9061028b565b1561023757600190565b5f90565b7f4501a919000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fe1b97cf8000000000000000000000000000000000000000000000000000000005f5260045ffd5b91825192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156104cc575b806d04ee2d6d415b85acef8100000000600a9210156104b1575b662386f26fc1000081101561049d575b6305f5e10081101561048c575b61271081101561047d575b606481101561046f575b1015610465575b6001850190600a602161033461031e8561014e565b9461032c60405196876100fe565b80865261014e565b97601f19602086019901368a378401015b5f1901917f30313233343536373839616263646566000000000000000000000000000000008282061a83530490811561038057600a90610345565b505073ffffffffffffffffffffffffffffffffffffffff5f9361040c86610415946020610404869b603a604051938492818401967f19457468657265756d205369676e6564204d6573736167653a0a00000000000088525180918486015e83018281019d8e528c8051928391019e8f905e01015f815203601f1981018352826100fe565b5190206104f4565b9094919461052e565b169416841461045c5773ffffffffffffffffffffffffffffffffffffffff9261044d92610444925190206104f4565b9092919261052e565b1614610457575f90565b600190565b50505050600190565b9360010193610309565b606460029104960195610302565b612710600491049601956102f8565b6305f5e100600891049601956102ed565b662386f26fc10000601091049601956102e0565b6d04ee2d6d415b85acef8100000000602091049601956102d0565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f01000000000000000081046102b6565b81519190604183036105245761051d9250602082015190606060408401519301515f1a90610606565b9192909190565b50505f9160029190565b60048110156105d95780610540575050565b60018103610570577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b600281036105a457507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6003146105ae5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411610695579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa1561068a575f5173ffffffffffffffffffffffffffffffffffffffff81161561068057905f905f90565b505f906001905f90565b6040513d5f823e3d90fd5b5050505f916003919056fea2646970667358221220c8e32dfe4c3317faffb02d4b02fddbb5e01dbc789e117442dd5ec08557786de764736f6c634300081e0033",
+ "nonce": "0x5",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ },
+ {
+ "hash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "transactionType": "CREATE",
+ "contractName": "ChannelHub",
+ "contractAddress": "0x55d6f0a0322606447fbc612cf58014faed65af9d",
+ "function": null,
+ "arguments": [
+ "0x2A35728CADd8076dfD424fC3e20974A3CD03bFa5"
+ ],
+ "transaction": {
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "gas": "0x6a626e",
+ "value": "0x0",
+ "input": "0x60a0346100aa57601f61608238819003918201601f19168301916001600160401b038311848410176100ae578084926020946040528339810103126100aa57516001600160a01b0381168082036100aa5760017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00551561009b57608052604051615fbf90816100c382396080518181816111420152613f180152f35b63e6c4247b60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60806040526004361015610011575f80fd5b5f3560e01c806316b390b11461024457806317536c061461023f578063187576d81461023a5780633115f6301461023557806338a66be21461023057806341b660ef1461022b57806347de477a146102265780635326919814610221578063587675e81461021c5780635a0745b4146102175780635b9acbf9146102125780635dc46a741461020d5780636840dbd2146102085780636898234b146102035780636af820bd146101fe57806371a47141146101f9578063735181f0146101f457806382d3e15d146101ef5780638d0b12a5146101ea57806394191051146101e55780639691b468146101e0578063a5c82680146101db578063b00b6fd6146101d6578063b25a1d38146101d1578063beed9d5f146101cc578063c74a2d10146101c7578063d888ccae146101c2578063dc23f29e146101bd578063dd73d494146101b8578063e617208c146101b3578063ecf3d7e8146101ae578063f4ac51f5146101a9578063f766f8d6146101a4578063ff5bc09e1461019f5763ffa1ad741461019a575f80fd5b612650565b612639565b61249a565b61241f565b61230d565b61226e565b6120f0565b611ef5565b611db5565b611b71565b6119d6565b6116c4565b61165b565b6114ba565b611379565b61135c565b6111cb565b6111ae565b611171565b61112d565b611112565b611026565b61100f565b610fc8565b610fa6565b610f8a565b610f44565b610cfc565b610b13565b610850565b6107ea565b61065e565b6105d8565b6104ca565b6102cb565b9181601f840112156102775782359167ffffffffffffffff8311610277576020838186019501011161027757565b5f80fd5b60643590600282101561027757565b90606060031983011261027757600435916024359067ffffffffffffffff8211610277576102ba91600401610249565b909160443560028110156102775790565b34610277576102d93661028a565b6103986102f1859493945f52600260205260405f2090565b9283546102ff81151561266b565b61035a600286019461032a61031b87546001600160a01b031690565b948560038a019a8b5492613eff565b9591600160068b019a019661034a88546001600160a01b039060081c1690565b926103548c6128a0565b8861405d565b60c061036588614161565b604051809581927f6666e4c000000000000000000000000000000000000000000000000000000000835260048301612a25565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577fba075bd445233f7cad862c72f0343b3503aad9c8e704a2295f122b82abf8e80196610436956080955f9461044b575b50836104146104066104279697546001600160a01b039060081c1690565b92546001600160a01b031690565b9254936104208a6128a0565b908c61428d565b015167ffffffffffffffff1690565b9061044660405192839283612a41565b0390a2005b6104279450946104146104786104069760c03d60c011610481575b6104708183612707565b810190612950565b955050946103e8565b503d610466565b612a36565b6001600160a01b0381160361027757565b6003196060910112610277576004356104b68161048d565b906024356104c38161048d565b9060443590565b6001600160a01b036104db3661049e565b92909116906104eb821515612b9e565b6104f6831515612bcd565b815f52600660205261051c8160405f20906001600160a01b03165f5260205260405f2090565b80549184830180931161059a577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7926001600160a01b03925561055d615810565b61056885823361458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00556040519485521692602090a3005b612bfc565b60206040818301928281528451809452019201905f5b8181106105c25750505090565b82518452602093840193909201916001016105b5565b34610277576020600319360112610277576001600160a01b036004356105fd8161048d565b165f52600160205260405f206040519081602082549182815201915f5260205f20905f5b818110610648576106448561063881870382612707565b6040519182918261059f565b0390f35b8254845260209093019260019283019201610621565b3461027757602060031936011261027757600354600480549190355f5b828410806107e1575b156107d4576106b06106a2610698866131a0565b90549060031b1c90565b5f52600260205260405f2090565b6001810160036106c1825460ff1690565b6106ca81611c00565b146107c2576106d882615b4d565b1561077e57915f8261076961077595600561076f96019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b6001600160a01b03165f52600660205260405f2090565b92015460401c6001600160a01b031690565b6001600160a01b03165f5260205260405f2090565b918254612c10565b9055600360ff19825416179055565b556139c6565b936139c6565b915b919261067b565b505092905061078d9150600455565b8061079457005b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1005b5050926107ce906139c6565b91610777565b92905061078d9150600455565b50818110610684565b34610277575f600319360112610277576020604051620186a08152f35b90816102609103126102775790565b90600319820160e081126102775760c0136102775760049160c4359067ffffffffffffffff82116102775761084d91600401610807565b90565b61085936610816565b906020820191600261086a84612c27565b61087381611c0f565b148015610af8575b8015610ada575b61088b90612c31565b61091a6108a061089b3685612c79565b614780565b916108aa8461483e565b60208401906108b882612ced565b956108d760408701976108ca89612ced565b608089013591858961494b565b60c0826108ff6108f86108ec6107148c612ced565b61073d60808501612ced565b54886149c5565b6040519687928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af493841561048857610a156001600160a01b0394610a2d936109967fb00e209e275d0e1892f1982b34d3f545d1628aebd95322d7ce3585c558f638b498610a1b955f91610aab575b50610985368d612c79565b61098f368a61301c565b908c614b52565b6109c2896109bd6109a685612ced565b6001600160a01b03165f52600160205260405f2090565b615bde565b5060026109ce82612c27565b6109d781611c0f565b03610a325750877f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f4177869620669660405180610a0d89826130ca565b0390a2612ced565b97612ced565b918360405194859416981696836130db565b0390a4005b610a3d600391612c27565b610a4681611c0f565b03610a7b57877f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf98660405180610a0d89826130ca565b877f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc60405180610a0d89826130ca565b610acd915060c03d60c011610ad3575b610ac58183612707565b810190612cf7565b5f61097a565b503d610abb565b5061088b610ae784612c27565b610af081611c0f565b159050610882565b506003610b0484612c27565b610b0d81611c0f565b1461087b565b610b1c36610816565b90610b3d6004610b2e60208501612c27565b610b3781611c0f565b14612c31565b610b4a61089b3683612c79565b9160208201610b5881612ced565b90610b7960408501926080610b6c85612ced565b960135958691868961494b565b610b8b610b858461316b565b86614c6c565b93610b9586614c9c565b15610bdd57505050610bd881610bcc7f471c4ebe4e57d25ef7117e141caac31c6b98f067b8098a7a7bbd38f637c2f9809386614cf9565b604051918291826130ca565b0390a3005b610c259060c085610bf18897959697614161565b60405194859283927fbbc42f3400000000000000000000000000000000000000000000000000000000845260048401613175565b038173728904e52308213ba61c90ef49f34c18fbda9e115af48015610488577fede7867afa7cdb9c443667efd8244d98bf9df1dce68e60dc94dca6605125ca7695610bd895610c9a945f93610ca3575b50610c82610c8891612ced565b91612ced565b91610c93368761301c565b8a8a61428d565b610bcc846131ef565b610c88919350610cc4610c829160c03d60c011610481576104708183612707565b939150610c75565b90604060031983011261027757600435916024359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610d0a36610ccc565b610d1b6009610b2e60208401612c27565b610d376001610d31845f525f60205260405f2090565b0161323d565b610dfd610d4e60208301516001600160a01b031690565b9161071460c060408301610d7a610d6c82516001600160a01b031690565b608086015190888a8c61494b565b610de2610ddb610dc4610d8d368b61301c565b9586946101408c018d8d610da08361316b565b67ffffffffffffffff1646149d8e610eb7575b50505050516001600160a01b031690565b6060840151602001516001600160a01b031661073d565b54896149c5565b6040519586928392632a2d120f60e21b8452600484016132c9565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857610e2f935f93610e96575b5086614b52565b15610e65576104467f9a6f675cc94b83b55f1ecc0876affd4332a30c92e6faa2aca0199b1b6df922c391604051918291826130ca565b6104467f7b20773c41402791c5f18914dbbeacad38b1ebcc4c55d8eb3bfe0a4cde26c82691604051918291826130ca565b610eb091935060c03d60c011610ad357610ac58183612707565b915f610e28565b610edb610f1092610ecc610f15963690612f3c565b60608d01526060369101612f3c565b60808b0152610ee86132b5565b60a08b0152610ef56132b5565b8b8b01526001600160a01b03165f52600160205260405f2090565b615c88565b505f8d8d82610db3565b600319604091011261027757600435610f378161048d565b9060243561084d8161048d565b34610277576020610f816001600160a01b03610f5f36610f1f565b91165f526006835260405f20906001600160a01b03165f5260205260405f2090565b54604051908152f35b34610277575f600319360112610277576020604051612a308152f35b3461027757604060031936011261027757610644610638602435600435613373565b610fda610fd436610ccc565b9061342d565b005b60606003198201126102775760043591602435916044359067ffffffffffffffff82116102775761084d91600401610807565b3461027757610fda61102036610fdc565b9161378a565b34610277576020600319360112610277576001600160a01b0360043561104b8161048d565b165f52600160205261105f60405f20615b05565b5f905f5b81518110156110ff5761109161108a61107c838561335f565b515f525f60205260405f2090565b5460ff1690565b61109a816121c1565b600381141590816110ea575b506110b4575b600101611063565b916110c78184600193106110cf576139c6565b9290506110ac565b6110d9858561335f565b516110e4828661335f565b526139c6565b600591506110f7816121c1565b14155f6110a6565b506106449181526040519182918261059f565b34610277575f60031936011261027757602060405160408152f35b34610277575f600319360112610277576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610277576020610f816001600160a01b0361118c36610f1f565b91165f526008835260405f20906001600160a01b03165f5260205260405f2090565b34610277575f600319360112610277576020600454604051908152f35b34610277576112556111dc3661028a565b929391906111f2855f52600560205260405f2090565b9182549261120184151561266b565b600281019060a061122261121c84546001600160a01b031690565b8a615053565b604051809881927f24063eba000000000000000000000000000000000000000000000000000000008352600483016139d4565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4958615610488575f9661132b575b50600181015460081c6001600160a01b0316968792546112a2906001600160a01b031690565b809581956003850154976112b7928992613eff565b9a9190946006019a6112c88c6128a0565b956112d3968b61405d565b846112dd876128a0565b6112e795896150bb565b6060015167ffffffffffffffff166040519182916113059183612a41565b037fb8568a1f475f3c76759a620e08a653d28348c5c09e2e0bc91d533339801fefd891a2005b61134e91965060a03d60a011611355575b6113468183612707565b8101906136bc565b945f61127c565b503d61133c565b34610277575f600319360112610277576020604051620151808152f35b61143661138536610ccc565b6113a661139760208395949501612c27565b6113a081611c0f565b15612c31565b6113bc6001610d31855f525f60205260405f2090565b9060c08161141b6114146108ec6107146113e060208901516001600160a01b031690565b6114078b8a60408101938960806113fe87516001600160a01b031690565b9301519361494b565b516001600160a01b031690565b54876149c5565b6040519586928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f567044ba1cdd4671ac3979c114241e1e3b56c9e9051f63f2f234f7a2795019cc9361044693610bcc925f92611499575b50611492368561301c565b9087614b52565b6114b391925060c03d60c011610ad357610ac58183612707565b905f611487565b34610277576114c836610816565b906114da6006610b2e60208501612c27565b6114e761089b3683612c79565b91602082016114f581612ced565b9061150960408501926080610b6c85612ced565b611515610b858461316b565b9361151f86614c9c565b1561155657505050610bd881610bcc7f587faad1bcd589ce902468251883e1976a645af8563c773eed7356d78433210c9386614cf9565b6115a59060a08561157261156c87989697612ced565b89615053565b60405194859283927eea54e700000000000000000000000000000000000000000000000000000000845260048401613773565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af48015610488577f17eb0a6bd5a0de45d1029ce3444941070e149df35b22176fc439f930f73c09f795610bd895610bcc945f93611614575b50610c8261160291612ced565b9161160d368761301c565b8a8a6150bb565b611602919350611635610c829160a03d60a011611355576113468183612707565b9391506115f5565b6024359060ff8216820361027757565b359060ff8216820361027757565b34610277576040600319360112610277576001600160a01b036116a96004356116838161048d565b8261168c61163d565b91165f52600760205260405f209060ff165f5260205260405f2090565b541660405180916001600160a01b0360208301911682520390f35b60806003193601126102775760043560243567ffffffffffffffff8111610277576116f3903690600401610807565b60443567ffffffffffffffff811161027757611713903690600401610249565b919061171d61027b565b9061172f855f525f60205260405f2090565b9161173c6001840161323d565b9161176661174b855460ff1690565b611754816121c1565b600181149081156119c2575b506139e5565b86611773600586016128a0565b916117b46117808861316b565b67ffffffffffffffff6117ab61179e875167ffffffffffffffff1690565b67ffffffffffffffff1690565b91161015613a14565b60208501516001600160a01b0316976117d760408701516001600160a01b031690565b9367ffffffffffffffff6117ff61179e6117f08c61316b565b935167ffffffffffffffff1690565b9116116118c3575b94611867889795857f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a9b6118616118859760149d61185561187c996118959c6118b49f60808c015192613eff565b9391949092369061301c565b9061405d565b845460ff191660021785555163ffffffff1690565b63ffffffff1690565b67ffffffffffffffff4216613a43565b9301805467ffffffffffffffff191667ffffffffffffffff8516179055565b61044660405192839283613a65565b909296959397946118df61190a9389888a60808601519361494b565b60c08761141b6119036108ec8c6001600160a01b03165f52600660205260405f2090565b548d6149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4938415610488577f07b9206d5a6026d3bd2a8f9a9b79f6fa4bfbd6a016975829fbaf07488019f28a99896118b4986118618e8c61185560149f976118679861187c9b61198a6118959f6118859f5f916119a3575b508d611983368961301c565b9089615443565b9a9f5050995050509750509b5095509597985050611807565b6119bc915060c03d60c011610ad357610ac58183612707565b5f611977565b600491506119cf816121c1565b145f611760565b34610277576080600319360112610277576004356119f38161048d565b6119fb61163d565b90604435611a088161048d565b60643567ffffffffffffffff811161027757611b4a6001600160a01b0392611b22611a6396611b07611b02611a4289973690600401610249565b60ff85169a91611afc90611a578d1515613a8d565b8b89169d8e1515612b9e565b611abf8785611ab9611aad611aad611aa085611a90866001600160a01b03165f52600760205260405f2090565b9060ff165f5260205260405f2090565b546001600160a01b031690565b6001600160a01b031690565b15613abc565b6040805160ff891660208201526001600160a01b038b169181019190915246606080830191909152815292611af5608085612707565b3691612fcb565b9061564c565b613b01565b611a90856001600160a01b03165f52600760205260405f2090565b906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b167f2366b94a706a0cfc2dca2fe8be9410b6fba2db75e3e9d3f03b3c2fb0b051efad5f80a4005b611b91611b7d36610ccc565b6113a66003610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f188e0ade7d115cc397426774adb960ae3e8c83e72f0a6cad4b7085e1d60bf9869361044693610bcc925f926114995750611492368561301c565b634e487b7160e01b5f52602160045260245ffd5b60041115611c0a57565b611bec565b600a1115611c0a57565b90600a821015611c0a5752565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b61084d9167ffffffffffffffff8251168152611c6f60208301516020830190611c19565b60408201516040820152611cdd6060830151606083019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b608082810151805167ffffffffffffffff1661014084015260208101516001600160a01b0316610160840152604081015160ff1661018084015260608101516101a0840152908101516101c083015260a08101516101e083015260c0015161020082015260c0611d5f60a0840151610260610220850152610260840190611c26565b92015190610240818403910152611c26565b929367ffffffffffffffff60c09561084d98979482948752611d9281611c00565b602087015216604085015216606083015260808201528160a08201520190611c4b565b3461027757602060031936011261027757600435611dd1613b66565b505f52600260205260405f20611de561272a565b9080548252610644600182015491611e31611e21611e038560ff1690565b94611e12602088019687613baa565b60081c6001600160a01b031690565b6001600160a01b03166040860152565b611e58611e4860028301546001600160a01b031690565b6001600160a01b03166060860152565b60038101546080850152600481015467ffffffffffffffff811660a086019081529490611e90905b60401c67ffffffffffffffff1690565b67ffffffffffffffff1660c0820190815291611ee46117f0611ec0600660058501549460e08701958652016128a0565b93610100810194855251965197611ed689611c00565b5167ffffffffffffffff1690565b905191519260405196879687611d71565b3461027757611f0336610816565b611f146008610b2e60208401612c27565b80611f89611f2561089b3686612c79565b936020810160c0611f3582612ced565b91611f546040850193611f4785612ced565b608087013591898c61494b565b610de2610ddb610dc4610714611f6a368b61301c565b9687958d8a611f7882614c9c565b9d8e15612052575b50505050612ced565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af491821561048857611fc6935f9361202d575b50611fc0903690612c79565b86614b52565b15611ffc576104467f3142fb397e715d80415dff7b527bf1c451def4675da6e1199ee1b4588e3f630a91604051918291826130ca565b6104467f26afbcb9eb52c21f42eb9cfe8f263718ffb65afbf84abe8ad8cce2acfb2242b891604051918291826130ca565b611fc091935061204b9060c03d60c011610ad357610ac58183612707565b9290611fb4565b6120aa936120876109a6926120696109bd9561483e565b8c606061207a366101408501612f3c565b9101526060369101612f3c565b60808c01526120946132b5565b60a08c01526120a16132b5565b8c8c0152612ced565b505f8d8a8e611f80565b9160a09367ffffffffffffffff9161084d97969385526120d381611c00565b602085015216604083015260608201528160808201520190611c4b565b346102775760206003193601126102775760043561210c613b66565b505f52600560205260405f2061212061273c565b908054825261064460018201549161213e611e21611e038560ff1690565b612155611e4860028301546001600160a01b031690565b60038101546080850152600481015467ffffffffffffffff1667ffffffffffffffff1660a08501908152936121b061219b600660058501549460c08501958652016128a0565b9160e0810192835251945195611ed687611c00565b9151905191604051958695866120b4565b60061115611c0a57565b906006821015611c0a5752565b9192612250610120946121f285612263959a99989a6121cb565b602085019060a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b61014060e0840152610140830190611c4b565b946101008201520152565b34610277576020600319360112610277576004355f60a0604051612291816126ae565b82815282602082015282604082015282606082015282608082015201526122b6613b66565b505f525f6020526122c960405f20613bc2565b80516122d4816121c1565b61064460208301519260408101519060606122fd61179e608084015167ffffffffffffffff1690565b91015191604051958695866121d8565b346102775761231b3661049e565b90916123316001600160a01b0382161515612b9e565b61233c821515612bcd565b335f5260066020526123628360405f20906001600160a01b03165f5260205260405f2090565b54908282106123f75782820391821161059a578383916123b7936123b18361239b336001600160a01b03165f52600660205260405f2090565b906001600160a01b03165f5260205260405f2090565b55614d9d565b7fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb6001600160a01b0360405193169280610bd83394829190602083019252565b7ff4d678b8000000000000000000000000000000000000000000000000000000005f5260045ffd5b61243f61242b36610ccc565b6113a66002610b2e60208496959601612c27565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4928315610488577f6085f5128b19e0d3cc37524413de47259383f0f75265d5d66f417786962066969361044693610bcc925f926114995750611492368561301c565b34610277576124a836610f1f565b6124b0615810565b6001600160a01b038116916124c6831515612b9e565b6001600160a01b036124ed8261239b336001600160a01b03165f52600860205260405f2090565b54916124fa831515612bcd565b5f61251a8261239b336001600160a01b03165f52600860205260405f2090565b55169181836125945761253d915f808080858a5af1612537613c20565b50613c4f565b60405190815233907f7b8d70738154be94a9a068a6d2f5dd8cfc65c52855859dc8f47de1ff185f8b5590602090a4610fda60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b50506040517fa9059cbb000000000000000000000000000000000000000000000000000000005f52836004528160245260205f60448180875af160015f511481161561261a575b60409190915261253d577f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b03821660045260245ffd5b6001811516612630573d15843b151516166125db565b503d5f823e3d90fd5b3461027757610fda61264a36610fdc565b91613c90565b34610277575f60031936011261027757602060405160018152f35b1561267257565b7fc60f1e78000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52604160045260245ffd5b60c0810190811067ffffffffffffffff8211176126ca57604052565b61269a565b60e0810190811067ffffffffffffffff8211176126ca57604052565b60a0810190811067ffffffffffffffff8211176126ca57604052565b90601f601f19910116810190811067ffffffffffffffff8211176126ca57604052565b6040519061273a61012083612707565b565b6040519061273a61010083612707565b6040519061273a60e083612707565b90604051612768816126cf565b60c0600482946127a660ff825467ffffffffffffffff811687526001600160a01b03808260401c1616602088015260e01c16604086019060ff169052565b6001810154606085015260028101546080850152600381015460a08501520154910152565b90600182811c921680156127f9575b60208310146127e557565b634e487b7160e01b5f52602260045260245ffd5b91607f16916127da565b5f9291815491612812836127cb565b8083529260018116908115612867575060011461282e57505050565b5f9081526020812093945091925b83831061284d575060209250010190565b60018160209294939454838587010152019101919061283c565b9050602094955060ff1991509291921683830152151560051b010190565b9061273a6128999260405193848092612803565b0383612707565b906040516128ad816126cf565b809260ff815467ffffffffffffffff8116845260401c1690600a821015611c0a57600d61291f9160c0936020860152600181015460408601526128f26002820161275b565b60608601526129036007820161275b565b6080860152612914600c8201612885565b60a086015201612885565b910152565b5190600482101561027757565b67ffffffffffffffff81160361027757565b5190811515820361027757565b908160c0910312610277576129b860a06040519261296d846126ae565b805184526020810151602085015261298760408201612924565b6040850152606081015161299a81612931565b606085015260808101516129ad81612931565b608085015201612943565b60a082015290565b9081516129cc81611c00565b815260a0806129ea602085015160c0602086015260c0850190611c4b565b936040810151604085015267ffffffffffffffff606082015116606085015267ffffffffffffffff6080820151166080850152015191015290565b90602061084d9281815201906129c0565b6040513d5f823e3d90fd5b92916020612b8d61273a9360408752612a75815467ffffffffffffffff811660408a015260ff60608a019160401c16611c19565b60018101546080880152600281015467ffffffffffffffff811660a0890152604081901c6001600160a01b031660c089015260e090811c60ff16908801526003810154610100880152600481015461012088015260058101546101408801526006810154610160880152600781015467ffffffffffffffff8116610180890152604081901c6001600160a01b03166101a089015260e01c60ff166101c088015260088101546101e08801526009810154610200880152600a810154610220880152600b81015461024088015261026080880152600d612b5b6102a08901600c8401612803565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0898403016102808a015201612803565b94019067ffffffffffffffff169052565b15612ba557565b7fe6c4247b000000000000000000000000000000000000000000000000000000005f5260045ffd5b15612bd457565b7f69640e72000000000000000000000000000000000000000000000000000000005f5260045ffd5b634e487b7160e01b5f52601160045260245ffd5b9190820180921161059a57565b600a111561027757565b3561084d81612c1d565b15612c3857565b7fc898513c000000000000000000000000000000000000000000000000000000005f5260045ffd5b63ffffffff81160361027757565b359061273a82612931565b91908260c091031261027757604051612c91816126ae565b60a08082948035612ca181612c60565b84526020810135612cb18161048d565b60208501526040810135612cc48161048d565b60408501526060810135612cd781612931565b6060850152608081013560808501520135910152565b3561084d8161048d565b908160c09103126102775760405190612d0f826126ae565b805182526020810151602083015260408101516006811015610277576129b89160a09160408501526060810151612d4581612931565b60608501526129ad60808201612943565b90612d628183516121cb565b608067ffffffffffffffff81612d87602086015160a0602087015260a0860190611c4b565b94604081015160408601526060810151606086015201511691015290565b359061273a82612c1d565b60c0809167ffffffffffffffff8135612dc881612931565b1684526001600160a01b036020820135612de18161048d565b16602085015260ff612df56040830161164d565b166040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b9035601e198236030181121561027757016020813591019167ffffffffffffffff821161027757813603831361027757565b601f8260209493601f1993818652868601375f8582860101520116010190565b61084d9167ffffffffffffffff8235612e8a81612931565b168152612ea86020830135612e9e81612c1d565b6020830190611c19565b60408201356040820152612ec26060820160608401612db0565b612ed461014082016101408401612db0565b612f08612efc612ee8610220850185612e20565b610260610220860152610260850191612e52565b92610240810190612e20565b91610240818503910152612e52565b9091612f2e61084d93604084526040840190612d56565b916020818403910152612e72565b91908260e091031261027757604051612f54816126cf565b60c08082948035612f6481612931565b84526020810135612f748161048d565b6020850152612f856040820161164d565b6040850152606081013560608501526080810135608085015260a081013560a08501520135910152565b67ffffffffffffffff81116126ca57601f01601f191660200190565b929192612fd782612faf565b91612fe56040519384612707565b829481845281830111610277578281602093845f960137010152565b9080601f830112156102775781602061084d93359101612fcb565b919091610260818403126102775761303261274c565b9261303c82612c6e565b845261304a60208301612da5565b6020850152604082013560408501526130668160608401612f3c565b6060850152613079816101408401612f3c565b608085015261022082013567ffffffffffffffff8111610277578161309f918401613001565b60a085015261024082013567ffffffffffffffff8111610277576130c39201613001565b60c0830152565b90602061084d928181520190612e72565b60e09060a061084d949363ffffffff81356130f581612c60565b1683526001600160a01b03602082013561310e8161048d565b1660208401526001600160a01b03604082013561312a8161048d565b16604084015267ffffffffffffffff606082013561314781612931565b16606084015260808101356080840152013560a08201528160c08201520190612e72565b3561084d81612931565b9091612f2e61084d936040845260408401906129c0565b634e487b7160e01b5f52603260045260245ffd5b6003548110156131b85760035f5260205f2001905f90565b61318c565b80548210156131b8575f5260205f2001905f90565b916131eb9183549060031b91821b915f19901b19161790565b9055565b600354680100000000000000008110156126ca57600181016003556003548110156131b85760035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b0155565b9060405161324a816126ae565b60a0600382946001600160a01b03815463ffffffff8116865260201c1660208501526132a467ffffffffffffffff60018301546001600160a01b03808216166040880152851c16606086019067ffffffffffffffff169052565b600281015460808501520154910152565b604051906132c4602083612707565b5f8252565b90916132e061084d93604084526040840190612d56565b916020818403910152611c4b565b67ffffffffffffffff81116126ca5760051b60200190565b60405190613315602083612707565b5f808352366020840137565b9061332b826132ee565b6133386040519182612707565b828152601f1961334882946132ee565b0190602036910137565b9190820391821161059a57565b80518210156131b85760209160051b010190565b9190600354908084029380850482149015171561059a57818410156133f75783019081841161059a578082116133ef575b506133b76133b28483613352565b613321565b92805b8281106133c657505050565b806133d56106986001936131a0565b6133e86133e28584613352565b8861335f565b52016133ba565b90505f6133a4565b5050905061084d613306565b906006811015611c0a5760ff60ff198354169116179055565b90602061084d928181520190611c4b565b9061343f825f525f60205260405f2090565b61344b6001820161323d565b91613457825460ff1690565b9184613465600583016128a0565b91604086019261347c84516001600160a01b031690565b91600261349360208a01516001600160a01b031690565b9761349d816121c1565b1480613654575b6135995750506134e16108f86108ec6107146134fc9661140760c0978a978c898f6080906134d96001610b2e60208601612c27565b01519361494b565b6040519384928392632a2d120f60e21b845260048401612f17565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af4801561048857610f10613573946109a688937f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a898613566965f92613578575b5061355f368961301c565b9086614b52565b50604051918291826130ca565b0390a2565b61359291925060c03d60c011610ad357610ac58183612707565b905f613554565b7f04cd8c68bf83e7bc531ca5a5d75c34e36513c2acf81e07e6470ba79e29da13a89750613573969195506136479450610f10926135fe6014836135e66109a695600360ff19825416179055565b5f60138201550167ffffffffffffffff198154169055565b606087016136278151606061361d60208301516001600160a01b031690565b9101519086614d9d565b5160a061363e60208301516001600160a01b031690565b91015191614d9d565b506040519182918261341c565b5061366d61179e601483015467ffffffffffffffff1690565b42116134a4565b1561367b57565b7fdb1ea1ac000000000000000000000000000000000000000000000000000000005f5260045ffd5b906136ad81611c00565b60ff60ff198354169116179055565b908160a0910312610277576137116080604051926136d9846126eb565b80518452602081015160208501526136f360408201612924565b6040850152606081015161370681612931565b606085015201612943565b608082015290565b90815161372581611c00565b815260806001600160a01b038161374b602086015160a0602087015260a0860190611c4b565b946040810151604086015267ffffffffffffffff606082015116606086015201511691015290565b9091612f2e61084d93604084526040840190613719565b9161379483614c9c565b613959576137aa825f52600560205260405f2090565b6137b684825414613674565b60018101918254916137d2836001600160a01b039060081c1690565b9360026137f26137eb828501546001600160a01b031690565b9560ff1690565b6137fb81611c00565b1480613939575b6138bf5750600361383b9161381e6007610b2e60208701612c27565b019261382e84548287868b61494b565b60a0836115728389615053565b038173893f2d45fdffe2d4297a5c1d5732edce4849ee825af4908115610488577f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d19561389995610bcc945f9461389e575b50549261160d368761301c565b0390a3565b6138b891945060a03d60a011611355576113468183612707565b925f61388c565b7f2fdac1380dbe23ae259b6871582b7f33e34461547f400bdd20d74991250317d1945090613899936138fb610bcc93600360ff19825416179055565b613933600d60058401935f855495556139226004820167ffffffffffffffff198154169055565b015460401c6001600160a01b031690565b90614d9d565b5061395261179e600484015467ffffffffffffffff1690565b4211613802565b6138997f6d0cf3d243d63f08f50db493a8af34b27d4e3bc9ec4098e82700abfeffe2d49891610bcc613992865f525f60205260405f2090565b6139be60026139af60018401546001600160a01b039060201c1690565b9201546001600160a01b031690565b908388615025565b5f19811461059a5760010190565b90602061084d928181520190613719565b156139ec57565b7ff2056b18000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613a1b57565b7f7d957361000000000000000000000000000000000000000000000000000000005f5260045ffd5b9067ffffffffffffffff8091169116019067ffffffffffffffff821161059a57565b9067ffffffffffffffff613a86602092959495604085526040850190612e72565b9416910152565b15613a9457565b7f06ee4dcd000000000000000000000000000000000000000000000000000000005f5260045ffd5b15613ac5575050565b906001600160a01b0360ff927f0bcc40f3000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b15613b0857565b7fc1606c2f000000000000000000000000000000000000000000000000000000005f5260045ffd5b60405190613b3d826126cf565b5f60c0838281528260208201528260408201528260608201528260808201528260a08201520152565b60405190613b73826126cf565b606060c0835f81525f60208201525f6040820152613b8f613b30565b83820152613b9b613b30565b60808201528260a08201520152565b613bb382611c00565b52565b6006821015611c0a5752565b90604051613bcf816126eb565b608067ffffffffffffffff60148395613bec60ff82541686613bb6565b613bf86001820161323d565b6020860152613c09600582016128a0565b604086015260138101546060860152015416910152565b3d15613c4a573d90613c3182612faf565b91613c3f6040519384612707565b82523d5f602084013e565b606090565b15613c58575050565b6001600160a01b03907fa5b05eec000000000000000000000000000000000000000000000000000000005f521660045260245260445ffd5b91613c9a83614c9c565b613e8157613cb0825f52600260205260405f2090565b613cbc84825414613674565b6001810191825491613cd8836001600160a01b039060081c1690565b936002613cf16137eb828501546001600160a01b031690565b613cfa81611c00565b1480613e5e575b613de457506003613d6591613d1d6005610b2e60208701612c27565b0192613d2d84548287868b61494b565b60c083610bf1613d5e613d51856001600160a01b03165f52600660205260405f2090565b61073d6101608501612ced565b5489614206565b038173728904e52308213ba61c90ef49f34c18fbda9e115af4908115610488577f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e9561389995610bcc945f94613dc3575b505492610c93368761301c565b613ddd91945060c03d60c011610481576104708183612707565b925f613db6565b7f1b92e8ef67d8a7c0d29c99efcd180a5e0d98d60ac41d52abbbb5950882c78e4e94509061389993613e20610bcc93600360ff19825416179055565b613933600d60058401935f85549555613922600482017fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff8154169055565b506004820154613e7a9060401c67ffffffffffffffff1661179e565b4211613d01565b6138997f32e24720f56fd5a7f4cb219d7ff3278ae95196e79c85b5801395894a6f53466c91610bcc613992865f525f60205260405f2090565b15613ec3575050565b906001600160a01b0360ff927f577f5940000000000000000000000000000000000000000000000000000000005f52166004521660245260445ffd5b93929190918215613fd157843560f81c9081613f4c57507f000000000000000000000000000000000000000000000000000000000000000094600101925f19019150613f489050565b9091565b600180915f97939594975060ff86161c1603613fa957613f9d83613f8b611aa0613f4896611a908a6001600160a01b03165f52600760205260405f2090565b966001600160a01b0388161515613eba565b600101915f1990910190565b7f1a9073b4000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fac241e11000000000000000000000000000000000000000000000000000000005f5260045ffd5b805191908290602001825e015f815290565b60021115611c0a57565b90816020910312610277575190565b939260609361404f6001600160a01b0394613a86949998998852608060208901526080880190611c26565b918683036040880152612e52565b906001600160a01b03929560209761409195996140c861407f61410d95615887565b6140ba604051998a928e840190613ff9565b7f6368616c6c656e67650000000000000000000000000000000000000000000000815260090190565b03601f198101895288612707565b6140d18161400b565b61415a57505b604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b0392165afa80156104885761273a915f9161412b575b501515613b01565b61414d915060203d602011614153575b6141458183612707565b810190614015565b5f614123565b503d61413b565b90506140d7565b5f6040519161416f836126ae565b81835261420260208401614181613b66565b81526141f46040860191858352611e806004606089019288845260808a01958987526141bc60a08c01998b8b525f52600260205260405f2090565b9160ff6001840154166141ce81611c00565b8c526141dc600684016128a0565b90526005820154905201549167ffffffffffffffff83165b67ffffffffffffffff169052565b5290565b9060405191614214836126ae565b5f835261420260208401614226613b66565b81526141f460408601915f8352611e80600460608901925f845260808a01955f87526141bc60a08c01995f8b525f52600260205260405f2090565b7f8000000000000000000000000000000000000000000000000000000000000000811461059a575f0390565b6020939291614329919796976142ab815f52600260205260405f2090565b976040860180516142bb81611c00565b6142c481611c00565b61450a575b5089888660a08901956142dc8751151590565b6144f6575b5050505050506142fc606085015167ffffffffffffffff1690565b67ffffffffffffffff81166144cb575b50608084015167ffffffffffffffff168061447c575b5051151590565b1561446357608001518201516001600160a01b031680935b8251905f82131561442357614363915061435b8451615ad0565b928391614527565b61437260058601918254612c10565b90555b0180515f8113156143d15750916143b060059261239b6143986143c69651615ad0565b966001600160a01b03165f52600660205260405f2090565b6143bb858254613352565b905501918254612c10565b90555b61273a61467e565b9290505f83126143e5575b505050506143c9565b61440260059261239b6143986143fd61441897614261565b615ad0565b61440d858254612c10565b905501918254613352565b90555f8080806143dc565b5f8212614433575b505050614375565b6144426143fd61444a93614261565b928391614d9d565b61445960058601918254613352565b9055825f8061442b565b50600d84015460401c6001600160a01b03168093614341565b6144c59060048901907fffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff6fffffffffffffffff000000000000000083549260401b169116179055565b5f614322565b6144f090600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f61430c565b6144ff956159a2565b5f80898886836142e1565b614521905161451881611c00565b60018b016136a3565b5f6142c9565b9061453a9291614535615810565b61458f565b60017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b1561456757565b7fd2ade556000000000000000000000000000000000000000000000000000000005f5260045ffd5b908215614679576001600160a01b0316918215801561466a576145b3823414614560565b156145bd57505050565b6001600160a01b03604051927f23b872dd000000000000000000000000000000000000000000000000000000005f52166004523060245260445260205f60648180865af160015f5114811615614654575b6040919091525f606052156146205750565b7f5274afe7000000000000000000000000000000000000000000000000000000005f526001600160a01b031660045260245ffd5b6001811516612630573d15833b1515161661460e565b6146743415614560565b6145b3565b505050565b6003546004545f5b82821080614776575b1561476b576146a36106a2610698846131a0565b6001810160036146b4825460ff1690565b6146bd81611c00565b14614759576146cb82615b4d565b1561471657915f8261076961470d95600561470796019261075a61075285549261073d600d61072b61071460028501546001600160a01b031690565b916139c6565b915b9190614686565b5050915061472390600455565b8061472b5750565b6040519081527f61815f4b11c6ea4e14a2e448a010bed8efdc3e53a15efbf183d16a31085cd14590602090a1565b505090614765906139c6565b9161470f565b915061472390600455565b506040811061468f565b7effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f01000000000000000000000000000000000000000000000000000000000000009160405161482760208201809360a0809163ffffffff81511684526001600160a01b0360208201511660208501526001600160a01b03604082015116604085015267ffffffffffffffff6060820151166060850152608081015160808501520151910152565b60c0815261483660e082612707565b519020161790565b602081013561484c8161048d565b6001600160a01b03811690614862821515612b9e565b6040830135906148718261048d565b6148986001600160a01b0383169261488a841515612b9e565b6148938361048d565b61048d565b6148a18161048d565b5081146148ed575063ffffffff6201518091356148bd81612c60565b16106148c557565b7f0596b15b000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fabfa558d000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b903590601e1981360301821215610277570180359067ffffffffffffffff82116102775760200191813603831361027757565b929161273a9461497c61498b92614971838761496b610220890189614918565b90613eff565b90878a949394615b81565b8361496b610240850185614918565b92909194615b81565b604051906149a1826126eb565b5f6080838281526149b0613b66565b60208201528260408201528260608201520152565b90601467ffffffffffffffff916149da614994565b935f525f60205260405f20906149f460ff83541686613bb6565b614a00600583016128a0565b6020860152601382015460408601526060850152015416608082015290565b9060a060039163ffffffff8151167fffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000008554161784556001600160a01b036020820151167fffffffffffffffff0000000000000000000000000000000000000000ffffffff77ffffffffffffffffffffffffffffffffffffffff0000000086549260201b169116178455614b4160018501614aef614ac660408501516001600160a01b031690565b82906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b606083015181547fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff1660a09190911b7bffffffffffffffff000000000000000000000000000000000000000016179055565b608081015160028501550151910155565b92614b8e81614bde9460a094614b6f885f525f60205260405f2090565b97614b7b895460ff1690565b614b84816121c1565b15614c5a57615443565b604081018051614b9d816121c1565b614ba6816121c1565b151580614c2f575b614c15575b50601484018054606083015167ffffffffffffffff9081169116819003614bed575b50500151151590565b614be55750565b60135f910155565b614c0e919067ffffffffffffffff1667ffffffffffffffff19825416179055565b5f80614bd5565b614c299051614c23816121c1565b85613403565b5f614bb3565b50845460ff16815190614c41826121c1565b614c4a826121c1565b614c53816121c1565b1415614bae565b614c678260018b01614a1f565b615443565b9067ffffffffffffffff604051916020830193845216604082015260408152614c96606082612707565b51902090565b805f525f60205260ff60405f2054166006811015611c0a578015908115614ce5575b50614ce0575f525f60205267ffffffffffffffff600760405f20015416461490565b505f90565b60059150614cf2816121c1565b145f614cbe565b90614d3b91614d146001610d31835f525f60205260405f2090565b60c0836108ff614d346108ec61071460408701516001600160a01b031690565b54856149c5565b03817378d150fda6fa6739c18014b347c7c7c45c58e1485af49283156104885761273a945f94614d78575b50614d7290369061301c565b91614b52565b614d72919450614d969060c03d60c011610ad357610ac58183612707565b9390614d66565b9061453a9291614dab615810565b9190918115614679576001600160a01b0383169283614e4f576001600160a01b038216925f8080808488620186a0f1614de2613c20565b5015614def575050505050565b614e326138999261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614e3d828254612c10565b90556040519081529081906020820190565b6040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481885afa908115610488575f91615006575b506040517fa9059cbb00000000000000000000000000000000000000000000000000000000602082019081526001600160a01b0385166024830152604480830187905282525f91829190614ee7606482612707565b51908286620186a0f190614ef9613c20565b506040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526020816024818a5afa9182156104885786915f93614fe5575b5083614fda575b83614fc6575b50505015614f5b575b50505050565b81614fa46001600160a01b039261239b7fbf182be802245e8ed88e4b8d3e4344c0863dd2a70334f089fd07265389306fcf956001600160a01b03165f52600860205260405f2090565b614faf858254612c10565b90556040519384521691602090a35f808080614f55565b614fd1929350613352565b145f8481614f4c565b818110159350614f46565b614fff91935060203d602011614153576141458183612707565b915f614f3f565b61501f915060203d602011614153576141458183612707565b5f614e92565b909192614d14614d3b946150456001610d31865f525f60205260405f2090565b92608084015191868661494b565b906001600160a01b0390615065614994565b925f52600560205267ffffffffffffffff600460405f2060ff60018201541661508d81611c00565b865261509b600682016128a0565b602087015260058101546040870152015416606084015216608082015290565b6020939291614329919796976150d9815f52600560205260405f2090565b976040860180516150e981611c00565b6150f281611c00565b615179575b50898886608089019561510a8751151590565b615165575b50505050505061512a606085015167ffffffffffffffff1690565b67ffffffffffffffff8116615140575051151590565b6144c590600489019067ffffffffffffffff1667ffffffffffffffff19825416179055565b61516e95615d2e565b5f808988868361510f565b615187905161451881611c00565b5f6150f7565b90600a811015611c0a577fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff68ff000000000000000083549260401b169116179055565b9060c060049161520367ffffffffffffffff825116859067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015184546040808401517fffffff000000000000000000000000000000000000000000ffffffffffffffff90921692901b7bffffffffffffffffffffffffffffffffffffffff0000000000000000169190911760e09190911b7cff0000000000000000000000000000000000000000000000000000000016178455606081015160018501556080810151600285015560a081015160038501550151910155565b601f82116152b357505050565b5f5260205f20906020601f840160051c830193106152eb575b601f0160051c01905b8181106152e0575050565b5f81556001016152d5565b90915081906152cc565b919091825167ffffffffffffffff81116126ca5761531d8161531784546127cb565b846152a6565b6020601f82116001146153585781906131eb9394955f9261534d575b50508160011b915f199060031b1c19161790565b015190505f80615339565b601f1982169061536b845f5260205f2090565b915f5b8181106153a55750958360019596971061538d575b505050811b019055565b01515f1960f88460031b161c191690555f8080615383565b9192602060018192868b01518155019401920161536e565b8151815467ffffffffffffffff191667ffffffffffffffff91909116178155602082015191600a831015611c0a5760c0600d916153fd61273a958561518d565b604081015160018501556154186060820151600286016151d0565b6154296080820151600786016151d0565b61543a60a0820151600c86016152f5565b015191016152f5565b6154596060919493945f525f60205260405f2090565b936154676080850151151590565b61563a575b01916154bf60a06154886020865101516001600160a01b031690565b9280515f81136155fc575b506020810180515f81136155b4575b5081515f8112615573575b50515f8112615528575b500151151590565b8061551a575b6154d6575b5050505061273a61467e565b61550f9261550260a0926154f6604060139601516001600160a01b031690565b90848451015191614d9d565b5101519201918254613352565b90555f8080806154ca565b5060a08351015115156154c5565b6143fd61553491614261565b61554f8561239b61071460408a01516001600160a01b031690565b61555a828254612c10565b905561556b60138901918254613352565b90555f6154b7565b6143fd61557f91614261565b61559d818761559860208b01516001600160a01b031690565b614d9d565b6155ac60138a01918254613352565b90555f6154ad565b6155bd90615ad0565b6155d88661239b61071460408b01516001600160a01b031690565b6155e3828254613352565b90556155f460138a01918254612c10565b90555f6154a2565b61560590615ad0565b615623818661561e60208a01516001600160a01b031690565b614527565b61563260138901918254612c10565b90555f615493565b61564781600587016153bd565b61546c565b805192835f947a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008210156157e8575b806d04ee2d6d415b85acef8100000000600a9210156157cc575b662386f26fc100008110156157b7575b6305f5e1008110156157a5575b612710811015615795575b6064811015615786575b101561577b575b61571260216156da60018801615ddf565b968701015b5f1901917f3031323334353637383961626364656600000000000000000000000000000000600a82061a8353600a900490565b90811561572257615712906156df565b50506001600160a01b036157478461573b858498615d73565b60208151910120615dc9565b9116931683146157735761576591816020611aad9351910120615dc9565b1461576e575f90565b600190565b505050600190565b6001909401936156c9565b600290606490049601956156c2565b60049061271090049601956156b8565b6008906305f5e10090049601956156ad565b601090662386f26fc1000090049601956156a0565b6020906d04ee2d6d415b85acef81000000009004960195615690565b50604094507a184f03e93ff9f4daa797ed6e38ed64bf6a1f0100000000000000008104615676565b60027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00541461585f5760027f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f0055565b7f3ee5aeb5000000000000000000000000000000000000000000000000000000005f5260045ffd5b67ffffffffffffffff815116906020810151600a811015611c0a576159308260406159919401516158cf60806060840151930151946040519760208901526040880190611c19565b6060860152608085019060c0809167ffffffffffffffff81511684526001600160a01b03602082015116602085015260ff6040820151166040850152606081015160608501526080810151608085015260a081015160a08501520151910152565b805167ffffffffffffffff1661016084015260208101516001600160a01b0316610180840152604081015160ff166101a084015260608101516101c084015260808101516101e084015260a081015161020084015260c00151610220830152565b610220815261084d61024082612707565b93909291935f52600260205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b602081015192600a841015611c0a57615a5660c0615aa093615a0e615acc9760039a61518d565b60408101516007890155615a29606082015160088a016151d0565b615a3a6080820151600d8a016151d0565b615a4b60a082015160128a016152f5565b0151601387016152f5565b60018501907fffffffffffffffffffffff0000000000000000000000000000000000000000ff74ffffffffffffffffffffffffffffffffffffffff0083549260081b169116179055565b60028301906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b0155565b5f8112615ada5790565b7fa8ce4432000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b90604051918281549182825260208201905f5260205f20925f5b818110615b3457505061273a92500383612707565b8454835260019485019487945060209093019201615b1f565b67ffffffffffffffff6004820154164210159081615b69575090565b600180925060ff91015416615b7d81611c00565b1490565b6001600160a01b039061410d615ba7615ba26020989599969799369061301c565b615887565b93604051988997889687957f600109bb00000000000000000000000000000000000000000000000000000000875260048701614024565b6001810190825f528160205260405f2054155f14615c46578054680100000000000000008110156126ca57615c33615c1d8260018794018555846131bd565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b5050505f90565b80548015615c74575f190190615c6382826131bd565b8154905f199060031b1b1916905555565b634e487b7160e01b5f52603160045260245ffd5b6001810191805f528260205260405f2054928315155f14615d26575f19840184811161059a5783545f1981019490851161059a575f958583615ce397615cd69503615ce9575b505050615c4d565b905f5260205260405f2090565b55600190565b615d0f615d0991615d00610698615d1d95886131bd565b928391876131bd565b906131d2565b85905f5260205260405f2090565b555f8080615cce565b505050505f90565b93909291935f52600560205260405f2092835560068301936159e767ffffffffffffffff825116869067ffffffffffffffff1667ffffffffffffffff19825416179055565b61273a90615dbb615db594936040519586937f19457468657265756d205369676e6564204d6573736167653a0a0000000000006020860152603a850190613ff9565b90613ff9565b03601f198101845283612707565b61084d91615dd691615e06565b90929192615e40565b90615de982612faf565b615df66040519182612707565b828152601f196133488294612faf565b8151919060418303615e3657615e2f9250602082015190606060408401519301515f1a90615f07565b9192909190565b50505f9160029190565b615e4981611c00565b80615e52575050565b615e5b81611c00565b60018103615e8b577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b615e9481611c00565b60028103615ec857507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b80615ed4600392611c00565b14615edc5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411615f7e579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610488575f516001600160a01b03811615615f7457905f905f90565b505f906001905f90565b5050505f916003919056fea2646970667358221220a1d82448e7e3f611b69660be3f5dc6e070ca23bb9e11aefc6fc6b7622d1cdc4e64736f6c634300081e00330000000000000000000000002a35728cadd8076dfd424fc3e20974a3cd03bfa5",
+ "nonce": "0x6",
+ "chainId": "0x13882"
+ },
+ "additionalContracts": [],
+ "isFixedGasLimit": false
+ }
+ ],
+ "receipts": [
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x1410b0",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000008e12cca7c1e150000000000000000000000000000000000000000000000000a467417de9a33bd1000000000000000000000000000000000000000000002b297badcd163bced3e7000000000000000000000000000000000000000000000000a3d92eb141e15a81000000000000000000000000000000000000000000002b297c3bdfe2e390b537",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "transactionIndex": "0x0",
+ "logIndex": "0x0",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "transactionIndex": "0x0",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0x1410b0",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x214c9e",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000005db48e1b848b52000000000000000000000000000000000000000000000000a3d92eb13cf13f31000000000000000000000000000000000000000000002b297c3bdfe2e390b537000000000000000000000000000000000000000000000000a37b7a23216cb3df000000000000000000000000000000000000000000002b297c999470ff154089",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "transactionIndex": "0x1",
+ "logIndex": "0x1",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "transactionIndex": "0x1",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0xd3bee",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x2e18fc",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000005a9ea056b694e2000000000000000000000000000000000000000000000000a37b7a231e2af44d000000000000000000000000000000000000000000002b297c999470ff154089000000000000000000000000000000000000000000000000a320db82c7745f6b000000000000000000000000000000000000000000002b297cf4331155cbd56b",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "transactionIndex": "0x2",
+ "logIndex": "0x2",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "transactionIndex": "0x2",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0xccc5e",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
+ "contractAddress": null
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x34a379",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002e505f361a7163000000000000000000000000000000000000000000000000a320db82c44e1449000000000000000000000000000000000000000000002b297cf4331155cbd56b000000000000000000000000000000000000000000000000a2f28b238e33a2e6000000000000000000000000000000000000000000002b297d2283708be646ce",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "transactionIndex": "0x3",
+ "logIndex": "0x3",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "transactionIndex": "0x3",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0x68a7d",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0x2a35728cadd8076dfd424fc3e20974a3cd03bfa5"
+ },
+ {
+ "status": "0x1",
+ "cumulativeGasUsed": "0x867909",
+ "logs": [
+ {
+ "address": "0x0000000000000000000000000000000000001010",
+ "topics": [
+ "0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
+ "0x0000000000000000000000000000000000000000000000000000000000001010",
+ "0x000000000000000000000000f8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "0x0000000000000000000000007ee41d8a25641000661b1ef5e6ae8a00400466b0"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000002436f597d48d070000000000000000000000000000000000000000000000000a2f28b238c978e23000000000000000000000000000000000000000000002b297d2283708be646ce000000000000000000000000000000000000000000000000a0af1bca0f4ebdb3000000000000000000000000000000000000000000002b297f65f2ca092f173e",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "blockTimestamp": "0x69ac3613",
+ "transactionHash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "transactionIndex": "0x4",
+ "logIndex": "0x4",
+ "removed": false
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000800000000000000000000100000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000020000000400000000000000000000000200000000000000200000000000000000000040000000000000000000000000000000000000004000000000000000000001000000000000000000400000000000100000000000000000000000000000000000000000000000000000000000000000000000100000",
+ "type": "0x2",
+ "transactionHash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "transactionIndex": "0x4",
+ "blockHash": "0x5b8deca35a551794826ebcca7605fce43a993be6996101ac202c51bdc1dc1a56",
+ "blockNumber": "0x2144623",
+ "gasUsed": "0x51d590",
+ "effectiveGasPrice": "0x714a1d19e",
+ "from": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "to": null,
+ "contractAddress": "0x55d6f0a0322606447fbc612cf58014faed65af9d"
+ }
+ ],
+ "libraries": [
+ "src/ChannelEngine.sol:ChannelEngine:0x78D150fdA6fa6739C18014B347c7c7C45C58e148",
+ "src/EscrowDepositEngine.sol:EscrowDepositEngine:0x728904E52308213bA61C90EF49F34c18Fbda9E11",
+ "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine:0x893F2D45fDFFe2D4297a5C1D5732EDce4849eE82"
+ ],
+ "pending": [],
+ "returns": {},
+ "timestamp": 1772893715802,
+ "chain": 80002,
+ "commit": "4b4244f1"
+}
\ No newline at end of file
diff --git a/contracts/deployments/11155111/ChannelEngine.sol_ChannelEngine/2026-03-07T14-12-00.json b/contracts/deployments/11155111/ChannelEngine.sol_ChannelEngine/2026-03-07T14-12-00.json
new file mode 100644
index 000000000..57fb626de
--- /dev/null
+++ b/contracts/deployments/11155111/ChannelEngine.sol_ChannelEngine/2026-03-07T14-12-00.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "transactionHash": "0x3df2187dc8a50ef62abfeb377318888493042770315492070c4708584dfbf572",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772892720,
+ "chainId": 11155111,
+ "contractPath": "src/ChannelEngine.sol:ChannelEngine",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/11155111/ChannelHub.sol_ChannelHub/2026-03-07T14-12-00.json b/contracts/deployments/11155111/ChannelHub.sol_ChannelHub/2026-03-07T14-12-00.json
new file mode 100644
index 000000000..98c8bed71
--- /dev/null
+++ b/contracts/deployments/11155111/ChannelHub.sol_ChannelHub/2026-03-07T14-12-00.json
@@ -0,0 +1,18 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0xb7bE0E2007dDF320d680942cb9e008F986E74F83",
+ "transactionHash": "0x6e0b716f9bdb40d3aadbfa2544bf5ec11b39f431736bd19569ade187cb0b7396",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772892720,
+ "chainId": 11155111,
+ "contractPath": "src/ChannelHub.sol:ChannelHub",
+ "constructorArgs": [
+ "0x735EB1026aFbA78B602dA39C6B59EABa95753686"
+ ],
+ "libraries": {
+ "src/ChannelEngine.sol:ChannelEngine": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "src/EscrowDepositEngine.sol:EscrowDepositEngine": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82"
+ },
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/11155111/ECDSAValidator.sol_ECDSAValidator/2026-03-07T14-12-00.json b/contracts/deployments/11155111/ECDSAValidator.sol_ECDSAValidator/2026-03-07T14-12-00.json
new file mode 100644
index 000000000..a06da0154
--- /dev/null
+++ b/contracts/deployments/11155111/ECDSAValidator.sol_ECDSAValidator/2026-03-07T14-12-00.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x735EB1026aFbA78B602dA39C6B59EABa95753686",
+ "transactionHash": "0x5e58e1f709d9ded21112c24523733b843486bf6ae775ffd10d86118a5c947cfe",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772892720,
+ "chainId": 11155111,
+ "contractPath": "src/sigValidators/ECDSAValidator.sol:ECDSAValidator",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/11155111/EscrowDepositEngine.sol_EscrowDepositEngine/2026-03-07T14-12-00.json b/contracts/deployments/11155111/EscrowDepositEngine.sol_EscrowDepositEngine/2026-03-07T14-12-00.json
new file mode 100644
index 000000000..63dff18e4
--- /dev/null
+++ b/contracts/deployments/11155111/EscrowDepositEngine.sol_EscrowDepositEngine/2026-03-07T14-12-00.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "transactionHash": "0x6e81a9f20bb7b3370a15b6402271a9f8e7eae63184e733c80273c497a4187983",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772892720,
+ "chainId": 11155111,
+ "contractPath": "src/EscrowDepositEngine.sol:EscrowDepositEngine",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/11155111/EscrowWithdrawalEngine.sol_EscrowWithdrawalEngine/2026-03-07T14-12-00.json b/contracts/deployments/11155111/EscrowWithdrawalEngine.sol_EscrowWithdrawalEngine/2026-03-07T14-12-00.json
new file mode 100644
index 000000000..caccab516
--- /dev/null
+++ b/contracts/deployments/11155111/EscrowWithdrawalEngine.sol_EscrowWithdrawalEngine/2026-03-07T14-12-00.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82",
+ "transactionHash": "0x9712dcbc9f46d075bb90ab9d5cbbdf30195810bb050150b302cb3aaaf0e71bc0",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772892720,
+ "chainId": 11155111,
+ "contractPath": "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-05T12-51-13.json b/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-05T12-51-13.json
new file mode 100644
index 000000000..f7c065d34
--- /dev/null
+++ b/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-05T12-51-13.json
@@ -0,0 +1,17 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0xB1aA0ac73B5E648a57db2d9342f11c471FcC85F1",
+ "transactionHash": "0xd0fe4560dec28164525e91b32484e0a2804b1727a4660c0e7581613e55c67b84",
+ "commit": "7e9af291fd864bc6d6d2b7b1295710781ac7c288",
+ "timestamp": 1772715073,
+ "chainId": 11155111,
+ "contractPath": "./src/PremintERC20.sol:PremintERC20",
+ "constructorArgs": [
+ "Yellow",
+ "YELLOW",
+ "18",
+ "0xd29995d8511Fe2dc1031F2650f950Adf4ECceBAD",
+ "10000000000000000000000000000"
+ ],
+ "comment": ""
+}
diff --git a/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-07T13-37-37.json b/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-07T13-37-37.json
new file mode 100644
index 000000000..67dd29de9
--- /dev/null
+++ b/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-07T13-37-37.json
@@ -0,0 +1,17 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0xD3E8Eb01Ae895262f187c4aAe936eC5c0665bbf8",
+ "transactionHash": "0xd58f896bd20194a337cf6c7e64e2da1344b9b05bf9bdf7e3891fb197f424cd06",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772890657,
+ "chainId": 11155111,
+ "contractPath": "./src/PremintERC20.sol:PremintERC20",
+ "constructorArgs": [
+ "Yellow USD",
+ "YUSD",
+ "6",
+ "0xd29995d8511Fe2dc1031F2650f950Adf4ECceBAD",
+ "10000000000000000000000000000"
+ ],
+ "comment": ""
+}
diff --git a/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-07T13-52-25.json b/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-07T13-52-25.json
new file mode 100644
index 000000000..c91d63b54
--- /dev/null
+++ b/contracts/deployments/11155111/PremintERC20.sol_PremintERC20/2026-03-07T13-52-25.json
@@ -0,0 +1,17 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0x719a00F9e8b831335F156337cEF7dC48986b2e84",
+ "transactionHash": "0x1ec139129b2bb3192f1827834b5fddeab6ea32f35d52b92074d4025d200ce90f",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772891545,
+ "chainId": 11155111,
+ "contractPath": "./src/PremintERC20.sol:PremintERC20",
+ "constructorArgs": [
+ "BNB",
+ "BNB",
+ "18",
+ "0xd29995d8511Fe2dc1031F2650f950Adf4ECceBAD",
+ "1000000000000"
+ ],
+ "comment": ""
+}
diff --git a/contracts/deployments/11155111/SessionKeyValidator.sol_SessionKeyValidator/2026-03-07T14-18-38.json b/contracts/deployments/11155111/SessionKeyValidator.sol_SessionKeyValidator/2026-03-07T14-18-38.json
new file mode 100644
index 000000000..1dc2448a9
--- /dev/null
+++ b/contracts/deployments/11155111/SessionKeyValidator.sol_SessionKeyValidator/2026-03-07T14-18-38.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0x2aC63456d78Cf2E2FDAf45cbed45b5d29907f4ac",
+ "transactionHash": "0xc60132f8c0d6776ab20861ff4c3137a3e23e4b5719cedda4bde866f435024488",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772893118,
+ "chainId": 11155111,
+ "contractPath": "./src/sigValidators/SessionKeyValidator.sol:SessionKeyValidator",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/80002/ChannelEngine.sol_ChannelEngine/2026-03-07T14-28-35.json b/contracts/deployments/80002/ChannelEngine.sol_ChannelEngine/2026-03-07T14-28-35.json
new file mode 100644
index 000000000..07566ce76
--- /dev/null
+++ b/contracts/deployments/80002/ChannelEngine.sol_ChannelEngine/2026-03-07T14-28-35.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "transactionHash": "0x0dd22ca5c6e28580d5cd04bc74d1df7d6612757840f54d0bcae73a4424db0213",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772893715,
+ "chainId": 80002,
+ "contractPath": "src/ChannelEngine.sol:ChannelEngine",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/80002/ChannelHub.sol_ChannelHub/2026-03-07T14-28-35.json b/contracts/deployments/80002/ChannelHub.sol_ChannelHub/2026-03-07T14-28-35.json
new file mode 100644
index 000000000..ea85dbb4d
--- /dev/null
+++ b/contracts/deployments/80002/ChannelHub.sol_ChannelHub/2026-03-07T14-28-35.json
@@ -0,0 +1,18 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x55D6f0A0322606447fbc612Cf58014Faed65aF9D",
+ "transactionHash": "0x566204df2ef25ad1f33d2b4abde5de21dd12b8865a8166780b4a6e588073355e",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772893715,
+ "chainId": 80002,
+ "contractPath": "src/ChannelHub.sol:ChannelHub",
+ "constructorArgs": [
+ "0x2A35728CADd8076dfD424fC3e20974A3CD03bFa5"
+ ],
+ "libraries": {
+ "src/ChannelEngine.sol:ChannelEngine": "0x78d150fda6fa6739c18014b347c7c7c45c58e148",
+ "src/EscrowDepositEngine.sol:EscrowDepositEngine": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82"
+ },
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/80002/ECDSAValidator.sol_ECDSAValidator/2026-03-07T14-28-35.json b/contracts/deployments/80002/ECDSAValidator.sol_ECDSAValidator/2026-03-07T14-28-35.json
new file mode 100644
index 000000000..919323887
--- /dev/null
+++ b/contracts/deployments/80002/ECDSAValidator.sol_ECDSAValidator/2026-03-07T14-28-35.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x2A35728CADd8076dfD424fC3e20974A3CD03bFa5",
+ "transactionHash": "0x5f3ea2e02ec5b886970dd186c7f06ed18e105477627f3759174a677323e2c735",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772893715,
+ "chainId": 80002,
+ "contractPath": "src/sigValidators/ECDSAValidator.sol:ECDSAValidator",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/80002/EscrowDepositEngine.sol_EscrowDepositEngine/2026-03-07T14-28-35.json b/contracts/deployments/80002/EscrowDepositEngine.sol_EscrowDepositEngine/2026-03-07T14-28-35.json
new file mode 100644
index 000000000..264024e04
--- /dev/null
+++ b/contracts/deployments/80002/EscrowDepositEngine.sol_EscrowDepositEngine/2026-03-07T14-28-35.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x728904e52308213ba61c90ef49f34c18fbda9e11",
+ "transactionHash": "0x51c15e27975ec5e84bc358c5df10f0c247451091d86e75cb8a81e80f1739bfe6",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772893715,
+ "chainId": 80002,
+ "contractPath": "src/EscrowDepositEngine.sol:EscrowDepositEngine",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/80002/EscrowWithdrawalEngine.sol_EscrowWithdrawalEngine/2026-03-07T14-28-35.json b/contracts/deployments/80002/EscrowWithdrawalEngine.sol_EscrowWithdrawalEngine/2026-03-07T14-28-35.json
new file mode 100644
index 000000000..d73d580b5
--- /dev/null
+++ b/contracts/deployments/80002/EscrowWithdrawalEngine.sol_EscrowWithdrawalEngine/2026-03-07T14-28-35.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xf8bedb0aba14833e95f29e760487c3d34bc4ec64",
+ "deployedTo": "0x893f2d45fdffe2d4297a5c1d5732edce4849ee82",
+ "transactionHash": "0x2e9b6ee81c54ea9035cb524b77ad6fa6bfe4340f4abacb482a5b280b94031d74",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772893715,
+ "chainId": 80002,
+ "contractPath": "src/EscrowWithdrawalEngine.sol:EscrowWithdrawalEngine",
+ "constructorArgs": [],
+ "comment": "clearnet-sandbox-1"
+}
diff --git a/contracts/deployments/80002/PremintERC20.sol_PremintERC20/2026-03-07T13-49-56.json b/contracts/deployments/80002/PremintERC20.sol_PremintERC20/2026-03-07T13-49-56.json
new file mode 100644
index 000000000..da5e9657c
--- /dev/null
+++ b/contracts/deployments/80002/PremintERC20.sol_PremintERC20/2026-03-07T13-49-56.json
@@ -0,0 +1,17 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0x0827b6aAA03475A8BF59Ee1A2bD76903DDFbaDB6",
+ "transactionHash": "0x3be3284aab87ed63c1a37041aa6d804b4141cbcb754133376be09adc2f729c95",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772891396,
+ "chainId": 80002,
+ "contractPath": "./src/PremintERC20.sol:PremintERC20",
+ "constructorArgs": [
+ "Yellow USD",
+ "YUSD",
+ "8",
+ "0xd29995d8511Fe2dc1031F2650f950Adf4ECceBAD",
+ "1000000000000000000000000000000"
+ ],
+ "comment": ""
+}
diff --git a/contracts/deployments/80002/PremintERC20.sol_PremintERC20/2026-03-07T13-53-02.json b/contracts/deployments/80002/PremintERC20.sol_PremintERC20/2026-03-07T13-53-02.json
new file mode 100644
index 000000000..7827d7f9b
--- /dev/null
+++ b/contracts/deployments/80002/PremintERC20.sol_PremintERC20/2026-03-07T13-53-02.json
@@ -0,0 +1,17 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0x9d8193e5655a36FFB9CD7D88D31c91d2650896D0",
+ "transactionHash": "0x459e4a275cc3a4acea78eed1017f30cc5c2c5ee240b0ba1365ca8f5c963c9180",
+ "commit": "fd39408567c4793fa5872f853bf875a45f12a4e9",
+ "timestamp": 1772891582,
+ "chainId": 80002,
+ "contractPath": "./src/PremintERC20.sol:PremintERC20",
+ "constructorArgs": [
+ "BNB",
+ "BNB",
+ "18",
+ "0xd29995d8511Fe2dc1031F2650f950Adf4ECceBAD",
+ "1000000000000"
+ ],
+ "comment": ""
+}
diff --git a/contracts/deployments/80002/SessionKeyValidator.sol_SessionKeyValidator/2026-03-07T14-29-19.json b/contracts/deployments/80002/SessionKeyValidator.sol_SessionKeyValidator/2026-03-07T14-29-19.json
new file mode 100644
index 000000000..074f477ae
--- /dev/null
+++ b/contracts/deployments/80002/SessionKeyValidator.sol_SessionKeyValidator/2026-03-07T14-29-19.json
@@ -0,0 +1,11 @@
+{
+ "deployer": "0xF8bedb0aBa14833e95F29e760487C3d34Bc4Ec64",
+ "deployedTo": "0x87825ACa5f4B9c3dc8B5aa3352724eDF5135D892",
+ "transactionHash": "0xb093a4bda220b56c44fb0cd62ec98938433830cb8262db0ec37d7b3571514cb3",
+ "commit": "4b4244f1df8ac6aa87a848174e29eabc7c1c3ede",
+ "timestamp": 1772893759,
+ "chainId": 80002,
+ "contractPath": "./src/sigValidators/SessionKeyValidator.sol:SessionKeyValidator",
+ "constructorArgs": [],
+ "comment": ""
+}
diff --git a/contracts/foundry.lock b/contracts/foundry.lock
new file mode 100644
index 000000000..5424e6004
--- /dev/null
+++ b/contracts/foundry.lock
@@ -0,0 +1,11 @@
+{
+ "lib/forge-std": {
+ "tag": {
+ "name": "v1.15.0",
+ "rev": "0844d7e1fc5e60d77b68e469bff60265f236c398"
+ }
+ },
+ "lib/openzeppelin-contracts": {
+ "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565"
+ }
+}
\ No newline at end of file
diff --git a/contracts/foundry.toml b/contracts/foundry.toml
index 700f9701c..455fe6f90 100644
--- a/contracts/foundry.toml
+++ b/contracts/foundry.toml
@@ -11,10 +11,10 @@ optimizer_runs = 1_000_000
# special compiler profile for ChannelHub to prevent code size overflow
additional_compiler_profiles = [
- { name = "channelhub", optimizer_runs = 4_000 }
+ { name = "channelhub", optimizer_runs = 750 }
]
# compile ChannelHub with lower optimizer runs to stay within size limits
compilation_restrictions = [
- { paths = "src/ChannelHub.sol", optimizer_runs = 4_000 }
+ { paths = "src/ChannelHub.sol", optimizer_runs = 750 }
]
diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std
index 1801b0541..0844d7e1f 160000
--- a/contracts/lib/forge-std
+++ b/contracts/lib/forge-std
@@ -1 +1 @@
-Subproject commit 1801b0541f4fda118a10798fd3486bb7051c5dd6
+Subproject commit 0844d7e1fc5e60d77b68e469bff60265f236c398
diff --git a/contracts/script/DeployChannelHub.s.sol b/contracts/script/DeployChannelHub.s.sol
new file mode 100644
index 000000000..1d12e14f8
--- /dev/null
+++ b/contracts/script/DeployChannelHub.s.sol
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.30;
+
+import {Script} from "forge-std/Script.sol";
+import {console} from "forge-std/console.sol";
+
+import {ChannelHub} from "../src/ChannelHub.sol";
+import {ISignatureValidator} from "../src/interfaces/ISignatureValidator.sol";
+import {ECDSAValidator} from "../src/sigValidators/ECDSAValidator.sol";
+
+/**
+ * @title DeployChannelHub
+ * @notice Forge script to deploy engine libraries and ChannelHub
+ * @dev Foundry automatically deploys unlinked libraries (ChannelEngine,
+ * EscrowDepositEngine, EscrowWithdrawalEngine) before ChannelHub in the
+ * broadcast batch. Their addresses appear in the broadcast JSON output.
+ *
+ * Usage:
+ * DEFAULT_VALIDATOR_ADDR= Address of an already-deployed ISignatureValidator.
+ * Leave unset to deploy a fresh ECDSAValidator.
+ *
+ * forge script script/DeployChannelHub.s.sol:DeployChannelHub \
+ * --rpc-url \
+ * --private-key \
+ * --broadcast \
+ * [-vvvv]
+ *
+ */
+contract DeployChannelHub is Script {
+ function run() external {
+ // Optional: reuse an existing validator or deploy a fresh ECDSAValidator
+ address defaultValidatorAddr = vm.envOr("DEFAULT_VALIDATOR_ADDR", address(0));
+ run(defaultValidatorAddr);
+ }
+
+ function run(address defaultValidatorAddr) public {
+ // msg.sender is set by Foundry to the address derived from --private-key
+ address deployer = msg.sender;
+
+ console.log("=== Deploy ChannelHub ===");
+ console.log("Deployer: ", deployer);
+ console.log("Chain ID: ", block.chainid);
+
+ // ----------------------------------------------------------------
+ // Predict addresses for informational logging.
+ // NOTE: Unlinked libraries (ChannelEngine, EscrowDepositEngine,
+ // EscrowWithdrawalEngine) are deployed by Foundry via the CREATE2
+ // deployer (0x4e59b44847b379578588920ca78fbf26c0b4956c) and do NOT
+ // consume the deployer's CREATE nonce. Their exact addresses appear
+ // in the broadcast JSON after the run.
+ // ----------------------------------------------------------------
+ uint64 nonce = vm.getNonce(deployer);
+
+ bool deployValidator = defaultValidatorAddr == address(0);
+ if (deployValidator) {
+ console.log("ECDSAValidator: ", vm.computeCreateAddress(deployer, nonce));
+ nonce++;
+ } else {
+ console.log("DefaultValidator: ", defaultValidatorAddr);
+ }
+
+ // Libraries are deployed via the CREATE2 deployer; they do not affect
+ // the deployer's nonce sequence, so ChannelHub lands at nonce `nonce`.
+ console.log(
+ "ChannelEngine/EscrowDepositEng/EscrowWithdrawEng: deployed via CREATE2 deployer (see broadcast JSON)"
+ );
+ console.log("ChannelHub: ", vm.computeCreateAddress(deployer, nonce));
+
+ vm.startBroadcast();
+
+ // 1. Deploy default signature validator if not provided
+ if (deployValidator) {
+ ECDSAValidator ecdsaValidator = new ECDSAValidator();
+ defaultValidatorAddr = address(ecdsaValidator);
+ console.log("Deployed ECDSAValidator:", defaultValidatorAddr);
+ }
+
+ // 2. Deploy ChannelHub.
+ // Foundry detects unlinked library references (ChannelEngine,
+ // EscrowDepositEngine, EscrowWithdrawalEngine) and inserts their
+ // deployment transactions before this one in the broadcast batch.
+ require(
+ defaultValidatorAddr.code.length > 0,
+ "DeployChannelHub: DEFAULT_VALIDATOR_ADDR has no code - must be a deployed contract"
+ );
+ ChannelHub hub = new ChannelHub(ISignatureValidator(defaultValidatorAddr));
+
+ vm.stopBroadcast();
+
+ // ----------------------------------------------------------------
+ // Summary
+ // ----------------------------------------------------------------
+ console.log("");
+ console.log("=== Deployment complete ===");
+ console.log("DefaultSigValidator:", defaultValidatorAddr);
+ console.log("ChannelHub: ", address(hub));
+ console.log("(Library addresses are logged in the broadcast JSON)");
+ }
+}
diff --git a/contracts/src/ChannelEngine.sol b/contracts/src/ChannelEngine.sol
index a2ee66179..2bb804a13 100644
--- a/contracts/src/ChannelEngine.sol
+++ b/contracts/src/ChannelEngine.sol
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.30;
+pragma solidity ^0.8.30;
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {ChannelStatus, State, StateIntent} from "./interfaces/Types.sol";
@@ -26,6 +26,7 @@ library ChannelEngine {
error IncorrectPreviousStateIntent();
error IncorrectChannelStatus();
error IncorrectStateVersion();
+ error ChallengeExpired();
error IncorrectUserAllocation();
error IncorrectNodeAllocation();
@@ -53,18 +54,18 @@ library ChannelEngine {
uint256 lockedFunds;
uint256 nodeAvailableFunds;
uint64 challengeExpiry;
+ bool isParametricToken;
+ uint48 channelSubId;
}
struct TransitionEffects {
// Fund movements (positive = pull/lock, negative = push/release)
int256 userFundsDelta; // Funds to pull from user (>0) or push to user (<0)
int256 nodeFundsDelta; // Funds to lock from node vault (>0) or release (<0)
-
// State updates
ChannelStatus newStatus;
uint64 newChallengeExpiry;
bool updateLastState;
- bool clearDispute;
bool closeChannel;
}
@@ -100,6 +101,12 @@ library ChannelEngine {
// homeLedger always represents current chain
require(candidate.homeLedger.chainId == block.chainid, IncorrectHomeChainId());
require(candidate.version > ctx.prevState.version || Utils.isEmpty(ctx.prevState), IncorrectStateVersion());
+ if (ctx.isParametricToken) {
+ require(
+ Utils.isEmpty(ctx.prevState) || candidate.homeLedger.token == ctx.prevState.homeLedger.token,
+ "Parametric token cannot change during channel lifetime"
+ );
+ }
// Validate token decimals for homeLedger
Utils.validateTokenDecimals(candidate.homeLedger);
@@ -124,6 +131,11 @@ library ChannelEngine {
require(netFlowsSum >= 0, NegativeNetFlowSum());
require(allocsSum == netFlowsSum.toUint256(), IncorrectAllocationSum());
+
+ // If channel is DISPUTED, check that challenge hasn't expired
+ if (ctx.status == ChannelStatus.DISPUTED) {
+ require(block.timestamp <= ctx.challengeExpiry, ChallengeExpired());
+ }
}
// ========== Internal: Phase 2 - Intent-Specific Calculation ==========
@@ -183,7 +195,7 @@ library ChannelEngine {
effects.userFundsDelta = userNfDelta; // Pull deposit from user
effects.nodeFundsDelta = nodeNfDelta; // May lock more from node or release
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -205,7 +217,7 @@ library ChannelEngine {
effects.userFundsDelta = userNfDelta; // Negative = push to user
effects.nodeFundsDelta = nodeNfDelta;
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -228,7 +240,7 @@ library ChannelEngine {
// Calculate effects
effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -259,6 +271,7 @@ library ChannelEngine {
effects.userFundsDelta = userNfDelta;
effects.nodeFundsDelta = nodeNfDelta;
effects.newStatus = ChannelStatus.CLOSED;
+ effects.newChallengeExpiry = 0;
effects.closeChannel = true;
return effects;
@@ -294,7 +307,7 @@ library ChannelEngine {
// Calculate effects
effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -313,10 +326,6 @@ library ChannelEngine {
|| ctx.status == ChannelStatus.MIGRATING_IN,
IncorrectChannelStatus()
);
- require(candidate.homeLedger.nodeAllocation == 0, IncorrectNodeAllocation());
- // nothing changes from initiate escrow deposit state
- require(userNfDelta == 0, IncorrectUserNetFlowDelta());
- require(nodeNfDelta == 0, IncorrectNodeNetFlowDelta());
// Check home - non-home state consistency
require(ctx.prevState.intent == StateIntent.INITIATE_ESCROW_DEPOSIT, IncorrectPreviousStateIntent());
@@ -324,6 +333,11 @@ library ChannelEngine {
require(candidate.nonHomeLedger.userAllocation == 0, IncorrectUserAllocation());
require(candidate.nonHomeLedger.nodeAllocation == 0, IncorrectNodeAllocation());
+ require(candidate.homeLedger.nodeAllocation == 0, IncorrectNodeAllocation());
+ // nothing changes from initiate escrow deposit state
+ require(userNfDelta == 0, IncorrectUserNetFlowDelta());
+ require(nodeNfDelta == 0, IncorrectNodeNetFlowDelta());
+
uint256 depositAmount = ctx.prevState.nonHomeLedger.userAllocation;
require(candidate.nonHomeLedger.userNetFlow == depositAmount.toInt256(), IncorrectUserNetFlow());
require(candidate.nonHomeLedger.nodeNetFlow == -depositAmount.toInt256(), IncorrectNodeNetFlow());
@@ -339,7 +353,7 @@ library ChannelEngine {
effects.userFundsDelta = 0;
effects.nodeFundsDelta = 0;
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -372,7 +386,7 @@ library ChannelEngine {
// Calculate effects - no immediate fund movement
effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -404,7 +418,7 @@ library ChannelEngine {
// Calculate effects
effects.nodeFundsDelta = nodeNfDelta; // Only node balance adjustments
effects.newStatus = ChannelStatus.OPERATING;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
return effects;
}
@@ -469,7 +483,8 @@ library ChannelEngine {
// Calculate effects - may adjust node vault based on net flow delta
effects.nodeFundsDelta = nodeNfDelta;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newStatus = ChannelStatus.OPERATING;
+ effects.newChallengeExpiry = 0;
} else {
revert IncorrectChannelStatus();
}
@@ -523,7 +538,7 @@ library ChannelEngine {
// Calculate effects - release all currently locked funds to node vault
effects.nodeFundsDelta = nodeNfDelta;
effects.newStatus = ChannelStatus.MIGRATED_OUT;
- effects.clearDispute = (ctx.status == ChannelStatus.DISPUTED);
+ effects.newChallengeExpiry = 0;
effects.closeChannel = true;
} else {
revert IncorrectChannelStatus();
diff --git a/contracts/src/ChannelHub.sol b/contracts/src/ChannelHub.sol
index 9621cff22..3deac68a9 100644
--- a/contracts/src/ChannelHub.sol
+++ b/contracts/src/ChannelHub.sol
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.30;
+pragma solidity ^0.8.30;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
@@ -9,6 +10,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
+import {IParametricToken} from "./interfaces/IParametricToken.sol";
import {IVault} from "./interfaces/IVault.sol";
import {ISignatureValidator, ValidationResult, VALIDATION_FAILURE} from "./interfaces/ISignatureValidator.sol";
import {
@@ -32,7 +34,7 @@ import {EcdsaSignatureUtils} from "./sigValidators/EcdsaSignatureUtils.sol";
* @notice Main contract implementing the Nitrolite state channel protocol (single-chain operations)
* @dev Uses unified transition pattern with ChannelEngine library for validation
*/
-contract ChannelHub is IVault, ReentrancyGuard {
+contract ChannelHub is IVault, ReentrancyGuard, Ownable {
using EnumerableSet for EnumerableSet.Bytes32Set;
using SafeERC20 for IERC20;
using SafeCast for int256;
@@ -73,8 +75,17 @@ contract ChannelHub is IVault, ReentrancyGuard {
event MigrationInFinalized(bytes32 indexed channelId, State state);
event ValidatorRegistered(address indexed node, uint8 indexed validatorId, ISignatureValidator indexed validator);
- event TransferFailed(address indexed recipient, address indexed token, uint256 amount);
- event FundsClaimed(address indexed account, address indexed token, address indexed destination, uint256 amount);
+ event TransferFailed(uint48 indexed fromSubId, address indexed recipient, address indexed token, uint256 amount);
+ event FundsClaimed(
+ address indexed account,
+ address indexed token,
+ uint48 subId,
+ address indexed destination,
+ uint256 amount
+ );
+ event NodeBalanceUpdated(address indexed node, address indexed token, uint48 indexed subId, uint256 amount);
+
+ event ParametricTokenIsSet(address indexed token, bool isParametric);
error InvalidAddress();
error IncorrectAmount();
@@ -94,7 +105,8 @@ contract ChannelHub is IVault, ReentrancyGuard {
error IncorrectStateIntent();
error IncorrectChannelStatus();
error ChallengerVersionTooLow();
- error OnlyNonHomeEscrowsCanBeChallenged();
+ error NoChannelIdFoundForEscrow();
+ error IncorrectChannelId();
struct ChannelMeta {
ChannelStatus status;
@@ -102,6 +114,7 @@ contract ChannelHub is IVault, ReentrancyGuard {
State lastState;
uint256 lockedFunds;
uint64 challengeExpireAt;
+ uint48 subId;
}
struct EscrowDepositMeta {
@@ -158,29 +171,34 @@ contract ChannelHub is IVault, ReentrancyGuard {
mapping(bytes32 escrowId => EscrowWithdrawalMeta meta) internal _escrowWithdrawals;
+ mapping(address token => bool) public isParametricToken;
+
mapping(address node => mapping(address token => uint256 balance)) internal _nodeBalances;
+ mapping(address node => mapping(address token => mapping(uint48 subId => uint256 balance)))
+ internal _nodeSubBalances;
// Validator ID 0x00 is reserved for DEFAULT_SIG_VALIDATOR
// Validator IDs 0x01-0xFF are available for node-registered validators
- mapping(address node => mapping(uint8 validatorId => ISignatureValidator validator)) internal
- _nodeValidatorRegistry;
+ mapping(address node => mapping(uint8 validatorId => ISignatureValidator validator))
+ internal _nodeValidatorRegistry;
// Reclaim balances for failed outbound transfers
// Accumulates funds when transfers fail (blacklists, hooks, gas depletion)
// Users can claim these funds later via claimFunds()
mapping(address account => mapping(address token => uint256 amount)) internal _reclaims;
+ mapping(address account => mapping(address token => mapping(uint48 subId => uint256 amount))) internal _subReclaims;
// ========== Constructor ==========
- constructor(ISignatureValidator _defaultSigValidator) {
+ constructor(ISignatureValidator _defaultSigValidator) Ownable(msg.sender) {
require(address(_defaultSigValidator) != address(0), InvalidAddress());
DEFAULT_SIG_VALIDATOR = _defaultSigValidator;
}
// ========== Getters ==========
- function getAccountBalance(address node, address token) external view returns (uint256) {
- return _nodeBalances[node][token];
+ function getAccountBalance(address node, address token, uint48 subId) external view returns (uint256) {
+ return !isParametricToken[token] ? _nodeBalances[node][token] : _nodeSubBalances[node][token][subId];
}
function getNodeValidator(address node, uint8 validatorId) external view returns (ISignatureValidator) {
@@ -191,6 +209,10 @@ contract ChannelHub is IVault, ReentrancyGuard {
return _userChannels[user].values();
}
+ function getChannelSubId(bytes32 channelId) external view returns (uint48) {
+ return _channels[channelId].subId;
+ }
+
// Filter only non-closed and non-migrated-out channels
function getOpenChannels(address user) external view returns (bytes32[] memory openChannels) {
openChannels = _userChannels[user].values();
@@ -211,7 +233,9 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
}
- function getChannelData(bytes32 channelId)
+ function getChannelData(
+ bytes32 channelId
+ )
external
view
returns (
@@ -230,7 +254,9 @@ contract ChannelHub is IVault, ReentrancyGuard {
lockedFunds = meta.lockedFunds;
}
- function getEscrowDepositData(bytes32 escrowId)
+ function getEscrowDepositData(
+ bytes32 escrowId
+ )
external
view
returns (
@@ -251,7 +277,9 @@ contract ChannelHub is IVault, ReentrancyGuard {
initState = meta.initState;
}
- function getEscrowWithdrawalData(bytes32 escrowId)
+ function getEscrowWithdrawalData(
+ bytes32 escrowId
+ )
external
view
returns (
@@ -274,31 +302,57 @@ contract ChannelHub is IVault, ReentrancyGuard {
return _reclaims[account][token];
}
+ function getSubReclaimBalance(address account, address token, uint48 subId) external view returns (uint256) {
+ return _subReclaims[account][token][subId];
+ }
+
+ // ========= Setters =========
+
+ function setParametricToken(address token, bool isParametric) external onlyOwner {
+ isParametricToken[token] = isParametric;
+ // Optional: emit event
+ emit ParametricTokenIsSet(token, isParametric);
+ }
+
// ========= IVault ==========
- function depositToVault(address node, address token, uint256 amount) external payable {
+ function depositToVault(address node, address token, uint48 subId, uint256 amount) external payable {
require(node != address(0), InvalidAddress());
require(amount > 0, IncorrectAmount());
- _nodeBalances[node][token] += amount;
+ uint256 nodeBalance = _getNodeBalance(node, token, subId);
+ uint256 updatedBalance = nodeBalance + amount;
+ if (!isParametricToken[token]) {
+ _nodeBalances[node][token] = updatedBalance;
+ } else {
+ _nodeSubBalances[node][token][subId] = updatedBalance;
+ }
- _pullFunds(msg.sender, token, amount);
+ _pullFunds(msg.sender, subId, token, amount);
- emit Deposited(node, token, amount);
+ emit Deposited(node, token, subId, amount);
+ emit NodeBalanceUpdated(node, token, subId, updatedBalance);
}
- function withdrawFromVault(address to, address token, uint256 amount) external {
+ function withdrawFromVault(address to, address token, uint48 subId, uint256 amount) external {
require(to != address(0), InvalidAddress());
require(amount > 0, IncorrectAmount());
- uint256 currentBalance = _nodeBalances[msg.sender][token];
- require(currentBalance >= amount, InsufficientBalance());
+ address node = msg.sender;
- _nodeBalances[msg.sender][token] = currentBalance - amount;
+ uint256 nodeBalance = _getNodeBalance(node, token, subId);
+ require(nodeBalance >= amount, InsufficientBalance());
+ uint256 updatedBalance = nodeBalance - amount;
+ if (!isParametricToken[token]) {
+ _nodeBalances[node][token] = updatedBalance;
+ } else {
+ _nodeSubBalances[node][token][subId] = updatedBalance;
+ }
- _pushFunds(to, token, amount);
+ _pushFunds(subId, to, token, amount);
- emit Withdrawn(msg.sender, token, amount);
+ emit Withdrawn(node, token, subId, amount);
+ emit NodeBalanceUpdated(node, token, subId, updatedBalance);
}
/**
@@ -307,24 +361,39 @@ contract ChannelHub is IVault, ReentrancyGuard {
* @param token The token address (address(0) for native ETH)
* @param destination The destination address to send funds to (can differ from msg.sender for blacklisted users)
*/
- function claimFunds(address token, address destination) external nonReentrant {
+ function claimFunds(address token, uint48 subId, address destination) external nonReentrant {
require(destination != address(0), InvalidAddress());
address account = msg.sender;
- uint256 amount = _reclaims[account][token];
+ uint256 amount = 0;
+
+ if (!isParametricToken[token]) {
+ amount = _reclaims[account][token];
+ } else {
+ amount = _subReclaims[account][token][subId];
+ }
+
require(amount > 0, IncorrectAmount());
- _reclaims[account][token] = 0;
+ if (!isParametricToken[token]) {
+ _reclaims[account][token] = 0;
+ } else {
+ _subReclaims[account][token][subId] = 0;
+ }
// Transfer without gas limit or reclaim logic (user controls gas, accepts responsibility)
if (token == address(0)) {
- (bool success,) = payable(destination).call{value: amount}("");
+ (bool success, ) = payable(destination).call{value: amount}("");
require(success, NativeTransferFailed(destination, amount));
} else {
- IERC20(token).safeTransfer(destination, amount);
+ if (!isParametricToken[token]) {
+ IERC20(token).safeTransfer(destination, amount);
+ } else {
+ IParametricToken(token).transferFromSub(subId, destination, amount);
+ }
}
- emit FundsClaimed(account, token, destination, amount);
+ emit FundsClaimed(account, token, subId, destination, amount);
}
// ========= Escrow Deposit Purge ==========
@@ -386,11 +455,16 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
// Only INITIALIZED escrows can be purged; CHALLENGED escrows require manual finalization
if (_isEscrowDepositUnlockable(meta)) {
- _nodeBalances[meta.node][meta.initState.nonHomeLedger.token] += meta.lockedAmount;
+ uint256 updatedBalance =
+ _nodeBalances[meta.node][meta.initState.nonHomeLedger.token] + meta.lockedAmount;
+ _nodeBalances[meta.node][meta.initState.nonHomeLedger.token] = updatedBalance;
+
meta.status = EscrowStatus.FINALIZED;
meta.lockedAmount = 0;
purgedCount++;
escrowHeadTemp++;
+
+ emit NodeBalanceUpdated(meta.node, meta.initState.nonHomeLedger.token, 0, updatedBalance);
} else {
break;
}
@@ -449,18 +523,37 @@ contract ChannelHub is IVault, ReentrancyGuard {
// to create a channel and perform initial operation simultaneously
function createChannel(ChannelDefinition calldata def, State calldata initState) external payable {
require(
- initState.intent == StateIntent.DEPOSIT || initState.intent == StateIntent.WITHDRAW
- || initState.intent == StateIntent.OPERATE,
+ initState.intent == StateIntent.DEPOSIT ||
+ initState.intent == StateIntent.WITHDRAW ||
+ initState.intent == StateIntent.OPERATE,
IncorrectStateIntent()
);
bytes32 channelId = Utils.getChannelId(def, VERSION);
+ address token = initState.homeLedger.token;
+
+ // Determine subId based on token type
+ uint48 subId = 0;
+ if (isParametricToken[token]) {
+ IParametricToken parametricToken = IParametricToken(token);
+
+ if (parametricToken.accountType(address(this)) != IParametricToken.AccountType.Super) {
+ parametricToken.convertToSuper(address(this));
+ }
+
+ subId = parametricToken.createSubAccount(address(this));
+ }
+
+ _channels[channelId].subId = subId;
+
_requireValidDefinition(def);
_validateSignatures(channelId, initState, def.user, def.node, def.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][initState.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(
+ channelId,
+ _nodeBalances[def.node][initState.homeLedger.token]
+ );
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, initState);
_applyEffects(channelId, def, initState, effects);
@@ -478,15 +571,25 @@ contract ChannelHub is IVault, ReentrancyGuard {
emit ChannelCreated(channelId, def.user, def.node, def, initState);
}
+ function _getNodeBalance(address node, address token, uint48 subId) internal view returns (uint256) {
+ if (isParametricToken[token]) {
+ return _nodeSubBalances[node][token][subId];
+ } else {
+ return _nodeBalances[node][token];
+ }
+ }
+
function depositToChannel(bytes32 channelId, State calldata candidate) public payable {
require(candidate.intent == StateIntent.DEPOSIT, IncorrectStateIntent());
ChannelMeta storage meta = _channels[channelId];
ChannelDefinition memory def = meta.definition;
+
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+
_validateSignatures(channelId, candidate, def.user, def.node, def.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][candidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(channelId, nodeBalance);
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyEffects(channelId, def, candidate, effects);
@@ -499,10 +602,12 @@ contract ChannelHub is IVault, ReentrancyGuard {
ChannelMeta storage meta = _channels[channelId];
ChannelDefinition memory def = meta.definition;
+
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+
_validateSignatures(channelId, candidate, def.user, def.node, def.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][candidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(channelId, nodeBalance);
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyEffects(channelId, def, candidate, effects);
@@ -515,10 +620,12 @@ contract ChannelHub is IVault, ReentrancyGuard {
ChannelMeta storage meta = _channels[channelId];
ChannelDefinition memory def = meta.definition;
+
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+
_validateSignatures(channelId, candidate, def.user, def.node, def.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][candidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(channelId, nodeBalance);
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyEffects(channelId, def, candidate, effects);
@@ -546,19 +653,22 @@ contract ChannelHub is IVault, ReentrancyGuard {
// If version is higher, process the new state
if (candidate.version > prevState.version) {
- require(candidate.intent == StateIntent.OPERATE, IncorrectStateIntent());
_validateSignatures(channelId, candidate, user, node, def.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[node][candidate.homeLedger.token]);
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(channelId, nodeBalance);
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyTransitionEffects(channelId, def, candidate, effects);
}
// else: challenging with same version, state already processed
- (ISignatureValidator validator, bytes calldata sigData) =
- _extractValidator(challengerSig, node, def.approvedSignatureValidators);
+ (ISignatureValidator validator, bytes calldata sigData) = _extractValidator(
+ challengerSig,
+ node,
+ def.approvedSignatureValidators
+ );
_validateChallengerSignature(channelId, candidate, sigData, validator, user, node, challengerIdx);
meta.status = ChannelStatus.DISPUTED;
@@ -569,8 +679,6 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
function closeChannel(bytes32 channelId, State calldata candidate) external payable {
- require(candidate.intent == StateIntent.CLOSE, IncorrectStateIntent());
-
ChannelMeta storage meta = _channels[channelId];
ChannelDefinition memory def = meta.definition;
ChannelStatus status = meta.status;
@@ -580,13 +688,13 @@ contract ChannelHub is IVault, ReentrancyGuard {
address user = def.user;
// Path 1: Unilateral closure after challenge timeout
- if (status == ChannelStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) {
+ if (status == ChannelStatus.DISPUTED && meta.challengeExpireAt < block.timestamp) {
meta.status = ChannelStatus.CLOSED;
meta.lockedFunds = 0;
meta.challengeExpireAt = 0;
- _pushFunds(user, prevState.homeLedger.token, prevState.homeLedger.userAllocation);
- _pushFunds(node, prevState.homeLedger.token, prevState.homeLedger.nodeAllocation);
+ _pushFunds(meta.subId, user, prevState.homeLedger.token, prevState.homeLedger.userAllocation);
+ _pushFunds(meta.subId, node, prevState.homeLedger.token, prevState.homeLedger.nodeAllocation);
_userChannels[user].remove(channelId);
@@ -595,10 +703,12 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
// Path 2: Cooperative closure with signed CLOSE state
+ require(candidate.intent == StateIntent.CLOSE, IncorrectStateIntent());
_validateSignatures(channelId, candidate, user, node, def.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][candidate.homeLedger.token]);
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(channelId, nodeBalance);
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyEffects(channelId, def, candidate, effects);
@@ -623,11 +733,19 @@ contract ChannelHub is IVault, ReentrancyGuard {
} else {
// NON-HOME CHAIN: Create escrow record - recover addresses from signatures
EscrowDepositEngine.TransitionContext memory ctx = _buildEscrowDepositContext(escrowId, 0);
- EscrowDepositEngine.TransitionEffects memory effects =
- EscrowDepositEngine.validateTransition(ctx, candidate);
+ EscrowDepositEngine.TransitionEffects memory effects = EscrowDepositEngine.validateTransition(
+ ctx,
+ candidate
+ );
_applyEscrowDepositEffects(
- escrowId, channelId, candidate, effects, def.user, def.node, def.approvedSignatureValidators
+ escrowId,
+ channelId,
+ candidate,
+ effects,
+ def.user,
+ def.node,
+ def.approvedSignatureValidators
);
_escrowDepositIds.push(escrowId);
@@ -635,69 +753,103 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
}
- function challengeEscrowDeposit(bytes32 escrowId, bytes calldata challengerSig, ParticipantIndex challengerIdx)
- external
- {
+ function challengeEscrowDeposit(
+ bytes32 escrowId,
+ bytes calldata challengerSig,
+ ParticipantIndex challengerIdx
+ ) external {
EscrowDepositMeta storage meta = _escrowDeposits[escrowId];
- require(!_isHomeChain(meta.channelId), OnlyNonHomeEscrowsCanBeChallenged());
-
bytes32 channelId = meta.channelId;
- (ISignatureValidator validator, bytes calldata sigData) =
- _extractValidator(challengerSig, meta.node, meta.approvedSignatureValidators);
- _validateChallengerSignature(channelId, meta.initState, sigData, validator, meta.user, meta.node, challengerIdx);
+ require(channelId != bytes32(0), NoChannelIdFoundForEscrow());
+
+ (ISignatureValidator validator, bytes calldata sigData) = _extractValidator(
+ challengerSig,
+ meta.node,
+ meta.approvedSignatureValidators
+ );
+ _validateChallengerSignature(
+ channelId,
+ meta.initState,
+ sigData,
+ validator,
+ meta.user,
+ meta.node,
+ challengerIdx
+ );
EscrowDepositEngine.TransitionContext memory ctx = _buildEscrowDepositContext(escrowId, 0);
EscrowDepositEngine.TransitionEffects memory effects = EscrowDepositEngine.validateChallenge(ctx);
_applyEscrowDepositEffects(
- escrowId, channelId, meta.initState, effects, meta.user, meta.node, meta.approvedSignatureValidators
+ escrowId,
+ channelId,
+ meta.initState,
+ effects,
+ meta.user,
+ meta.node,
+ meta.approvedSignatureValidators
);
emit EscrowDepositChallenged(escrowId, meta.initState, effects.newChallengeExpiry);
}
- function finalizeEscrowDeposit(bytes32 escrowId, State calldata candidate) external {
+ function finalizeEscrowDeposit(bytes32 channelId, bytes32 escrowId, State calldata candidate) external {
+ if (_isHomeChain(channelId)) {
+ // HOME CHAIN: Get user/node from channel definition
+ ChannelMeta storage channelMeta = _channels[channelId];
+ _processHomeChainEscrowFinalize(
+ channelId,
+ candidate,
+ channelMeta.definition.user,
+ channelMeta.definition.node
+ );
+ emit EscrowDepositFinalizedOnHome(escrowId, channelId, candidate);
+ return;
+ }
+
+ // NON-HOME CHAIN: Use escrow metadata
EscrowDepositMeta storage meta = _escrowDeposits[escrowId];
+ require(meta.channelId == channelId, IncorrectChannelId()); // Validate consistency
address user = meta.user;
address node = meta.node;
-
- bool isHomeChain = _isHomeChain(meta.channelId);
EscrowStatus status = meta.status;
- if (!isHomeChain && status == EscrowStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) {
+ if (status == EscrowStatus.DISPUTED && meta.challengeExpireAt < block.timestamp) {
// NON-HOME CHAIN: Unilateral finalization after challenge timeout
meta.status = EscrowStatus.FINALIZED;
uint256 lockedAmount = meta.lockedAmount;
meta.lockedAmount = 0;
meta.challengeExpireAt = 0;
- _pushFunds(node, meta.initState.nonHomeLedger.token, lockedAmount);
+ // Release to user as "deposit exchange" has not been signed yet (it is the "finalizeEscrowDeposit" state)
+ _pushFunds(0, user, meta.initState.nonHomeLedger.token, lockedAmount);
- emit EscrowDepositFinalized(escrowId, meta.channelId, candidate);
+ emit EscrowDepositFinalized(escrowId, channelId, candidate);
return;
}
require(candidate.intent == StateIntent.FINALIZE_ESCROW_DEPOSIT, IncorrectStateIntent());
- if (_isHomeChain(meta.channelId)) {
- _processHomeChainEscrowFinalize(meta.channelId, candidate, user, node);
- emit EscrowDepositFinalizedOnHome(escrowId, meta.channelId, candidate);
- return;
- } else {
- // NON-HOME CHAIN: Update via EscrowDepositEngine
- _validateSignatures(meta.channelId, candidate, user, node, meta.approvedSignatureValidators);
+ // NON-HOME CHAIN: Update via EscrowDepositEngine
+ _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators);
- EscrowDepositEngine.TransitionContext memory ctx =
- _buildEscrowDepositContext(escrowId, _nodeBalances[node][candidate.nonHomeLedger.token]);
- EscrowDepositEngine.TransitionEffects memory effects =
- EscrowDepositEngine.validateTransition(ctx, candidate);
+ EscrowDepositEngine.TransitionContext memory ctx = _buildEscrowDepositContext(
+ escrowId,
+ _nodeBalances[node][candidate.nonHomeLedger.token]
+ );
+ EscrowDepositEngine.TransitionEffects memory effects = EscrowDepositEngine.validateTransition(ctx, candidate);
- _applyEscrowDepositEffects(
- escrowId, meta.channelId, candidate, effects, user, node, meta.approvedSignatureValidators
- );
+ _applyEscrowDepositEffects(
+ escrowId,
+ channelId,
+ candidate,
+ effects,
+ user,
+ node,
+ meta.approvedSignatureValidators
+ );
- emit EscrowDepositFinalized(escrowId, meta.channelId, candidate);
- }
+ emit EscrowDepositFinalized(escrowId, channelId, candidate);
}
function initiateEscrowWithdrawal(ChannelDefinition calldata def, State calldata candidate) external {
@@ -709,87 +861,126 @@ contract ChannelHub is IVault, ReentrancyGuard {
bytes32 escrowId = Utils.getEscrowId(channelId, candidate.version);
if (_isHomeChain(channelId)) {
+ // HOME CHAIN: Process through channel state, no escrow metadata
_processHomeChainEscrowInitiate(channelId, candidate);
emit EscrowWithdrawalInitiatedOnHome(escrowId, channelId, candidate);
} else {
// NON-HOME CHAIN
EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, def.node);
- EscrowWithdrawalEngine.TransitionEffects memory effects =
- EscrowWithdrawalEngine.validateTransition(ctx, candidate);
+ EscrowWithdrawalEngine.TransitionEffects memory effects = EscrowWithdrawalEngine.validateTransition(
+ ctx,
+ candidate
+ );
_applyEscrowWithdrawalEffects(
- escrowId, channelId, candidate, effects, def.user, def.node, def.approvedSignatureValidators
+ escrowId,
+ channelId,
+ candidate,
+ effects,
+ def.user,
+ def.node,
+ def.approvedSignatureValidators
);
emit EscrowWithdrawalInitiated(escrowId, channelId, candidate);
}
}
- function challengeEscrowWithdrawal(bytes32 escrowId, bytes calldata challengerSig, ParticipantIndex challengerIdx)
- external
- {
+ function challengeEscrowWithdrawal(
+ bytes32 escrowId,
+ bytes calldata challengerSig,
+ ParticipantIndex challengerIdx
+ ) external {
EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId];
- require(!_isHomeChain(meta.channelId), OnlyNonHomeEscrowsCanBeChallenged());
+ bytes32 channelId = meta.channelId;
+ require(channelId != bytes32(0), NoChannelIdFoundForEscrow());
EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, meta.node);
EscrowWithdrawalEngine.TransitionEffects memory effects = EscrowWithdrawalEngine.validateChallenge(ctx);
// Validate challenger signature
- bytes32 channelId = meta.channelId;
address user = meta.user;
address node = meta.node;
- (ISignatureValidator validator, bytes calldata sigData) =
- _extractValidator(challengerSig, node, meta.approvedSignatureValidators);
+ (ISignatureValidator validator, bytes calldata sigData) = _extractValidator(
+ challengerSig,
+ node,
+ meta.approvedSignatureValidators
+ );
_validateChallengerSignature(channelId, meta.initState, sigData, validator, user, node, challengerIdx);
_applyEscrowWithdrawalEffects(
- escrowId, channelId, meta.initState, effects, user, node, meta.approvedSignatureValidators
+ escrowId,
+ channelId,
+ meta.initState,
+ effects,
+ user,
+ node,
+ meta.approvedSignatureValidators
);
emit EscrowWithdrawalChallenged(escrowId, meta.initState, effects.newChallengeExpiry);
}
- function finalizeEscrowWithdrawal(bytes32 escrowId, State calldata candidate) external {
+ function finalizeEscrowWithdrawal(bytes32 channelId, bytes32 escrowId, State calldata candidate) external {
+ if (_isHomeChain(channelId)) {
+ // HOME CHAIN: Get user/node from channel definition
+ ChannelMeta storage channelMeta = _channels[channelId];
+ _processHomeChainEscrowFinalize(
+ channelId,
+ candidate,
+ channelMeta.definition.user,
+ channelMeta.definition.node
+ );
+ emit EscrowWithdrawalFinalizedOnHome(escrowId, channelId, candidate);
+ return;
+ }
+
+ // NON-HOME CHAIN: Use escrow metadata
EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId];
- bytes32 channelId = meta.channelId;
+ require(meta.channelId == channelId, IncorrectChannelId()); // Validate consistency
address user = meta.user;
address node = meta.node;
-
- bool isHomeChain = _isHomeChain(channelId);
EscrowStatus status = meta.status;
- if (!isHomeChain && status == EscrowStatus.DISPUTED && block.timestamp > meta.challengeExpireAt) {
+ if (status == EscrowStatus.DISPUTED && meta.challengeExpireAt < block.timestamp) {
// NON-HOME CHAIN: Unilateral finalization after challenge timeout
meta.status = EscrowStatus.FINALIZED;
uint256 lockedAmount = meta.lockedAmount;
meta.lockedAmount = 0;
meta.challengeExpireAt = 0;
- _pushFunds(node, meta.initState.nonHomeLedger.token, lockedAmount);
+ // Release locked amount back to node as "withdrawal exchange" has not been signed yet (it is the "finalizeEscrowWithdrawal" state)
+ address withdrawalToken = meta.initState.nonHomeLedger.token;
+ uint256 updatedWithdrawalBalance = _nodeBalances[node][withdrawalToken] + lockedAmount;
+ _nodeBalances[node][withdrawalToken] = updatedWithdrawalBalance;
- emit EscrowWithdrawalFinalized(escrowId, meta.channelId, candidate);
+ emit NodeBalanceUpdated(node, withdrawalToken, 0, updatedWithdrawalBalance);
+ emit EscrowWithdrawalFinalized(escrowId, channelId, candidate);
return;
}
require(candidate.intent == StateIntent.FINALIZE_ESCROW_WITHDRAWAL, IncorrectStateIntent());
- if (_isHomeChain(channelId)) {
- _processHomeChainEscrowFinalize(channelId, candidate, user, node);
- emit EscrowWithdrawalFinalizedOnHome(escrowId, channelId, candidate);
- } else {
- // Non-Home chain: Update via EscrowWithdrawalEngine
- _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators);
+ // NON-HOME CHAIN: Update via EscrowWithdrawalEngine
+ _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators);
- EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, node);
- EscrowWithdrawalEngine.TransitionEffects memory effects =
- EscrowWithdrawalEngine.validateTransition(ctx, candidate);
+ EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, node);
+ EscrowWithdrawalEngine.TransitionEffects memory effects = EscrowWithdrawalEngine.validateTransition(
+ ctx,
+ candidate
+ );
- _applyEscrowWithdrawalEffects(
- escrowId, channelId, candidate, effects, user, node, meta.approvedSignatureValidators
- );
+ _applyEscrowWithdrawalEffects(
+ escrowId,
+ channelId,
+ candidate,
+ effects,
+ user,
+ node,
+ meta.approvedSignatureValidators
+ );
- emit EscrowWithdrawalFinalized(escrowId, channelId, candidate);
- }
+ emit EscrowWithdrawalFinalized(escrowId, channelId, candidate);
}
function initiateMigration(ChannelDefinition calldata def, State calldata candidate) external {
@@ -814,8 +1005,10 @@ contract ChannelHub is IVault, ReentrancyGuard {
_userChannels[def.user].add(channelId);
}
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][targetCandidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(
+ channelId,
+ _nodeBalances[def.node][targetCandidate.homeLedger.token]
+ );
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, targetCandidate);
_applyEffects(channelId, def, targetCandidate, effects);
@@ -850,8 +1043,10 @@ contract ChannelHub is IVault, ReentrancyGuard {
_userChannels[user].remove(channelId);
}
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[def.node][targetCandidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(
+ channelId,
+ _nodeBalances[def.node][targetCandidate.homeLedger.token]
+ );
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, targetCandidate);
_applyEffects(channelId, def, targetCandidate, effects);
@@ -871,11 +1066,17 @@ contract ChannelHub is IVault, ReentrancyGuard {
address node,
uint256 approvedSignatureValidators
) internal view {
- (ISignatureValidator userValidator, bytes calldata userSigData) =
- _extractValidator(state.userSig, node, approvedSignatureValidators);
+ (ISignatureValidator userValidator, bytes calldata userSigData) = _extractValidator(
+ state.userSig,
+ node,
+ approvedSignatureValidators
+ );
_validateSignature(channelId, state, userSigData, user, userValidator);
- (ISignatureValidator nodeValidator, bytes calldata nodeSigData) =
- _extractValidator(state.nodeSig, node, approvedSignatureValidators);
+ (ISignatureValidator nodeValidator, bytes calldata nodeSigData) = _extractValidator(
+ state.nodeSig,
+ node,
+ approvedSignatureValidators
+ );
_validateSignature(channelId, state, nodeSigData, node, nodeValidator);
}
@@ -899,11 +1100,11 @@ contract ChannelHub is IVault, ReentrancyGuard {
require(ValidationResult.unwrap(result) != ValidationResult.unwrap(VALIDATION_FAILURE), IncorrectSignature());
}
- function _extractValidator(bytes calldata signature, address node, uint256 approvedSignatureValidators)
- internal
- view
- returns (ISignatureValidator validator, bytes calldata sigData)
- {
+ function _extractValidator(
+ bytes calldata signature,
+ address node,
+ uint256 approvedSignatureValidators
+ ) internal view returns (ISignatureValidator validator, bytes calldata sigData) {
require(signature.length > 0, EmptySignature());
uint8 validatorId = uint8(signature[0]);
@@ -958,33 +1159,39 @@ contract ChannelHub is IVault, ReentrancyGuard {
ChannelMeta storage meta = _channels[channelId];
ChannelDefinition memory metaDef = meta.definition;
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[metaDef.node][candidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(
+ channelId,
+ _nodeBalances[metaDef.node][candidate.homeLedger.token]
+ );
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyEffects(channelId, metaDef, candidate, effects);
}
/// @dev Process HOME CHAIN path for escrow finalize operations
- function _processHomeChainEscrowFinalize(bytes32 channelId, State calldata candidate, address user, address node)
- internal
- {
+ function _processHomeChainEscrowFinalize(
+ bytes32 channelId,
+ State calldata candidate,
+ address user,
+ address node
+ ) internal {
ChannelMeta storage channelMeta = _channels[channelId];
ChannelDefinition memory channelDef = channelMeta.definition;
_validateSignatures(channelId, candidate, user, node, channelDef.approvedSignatureValidators);
- ChannelEngine.TransitionContext memory ctx =
- _buildChannelContext(channelId, _nodeBalances[channelDef.node][candidate.homeLedger.token]);
+ ChannelEngine.TransitionContext memory ctx = _buildChannelContext(
+ channelId,
+ _nodeBalances[channelDef.node][candidate.homeLedger.token]
+ );
ChannelEngine.TransitionEffects memory effects = ChannelEngine.validateTransition(ctx, candidate);
_applyEffects(channelId, channelDef, candidate, effects);
}
- function _buildChannelContext(bytes32 channelId, uint256 nodeBalance)
- internal
- view
- returns (ChannelEngine.TransitionContext memory ctx)
- {
+ function _buildChannelContext(
+ bytes32 channelId,
+ uint256 nodeBalance
+ ) internal view returns (ChannelEngine.TransitionContext memory ctx) {
ChannelMeta storage meta = _channels[channelId];
ctx.status = meta.status;
@@ -993,14 +1200,17 @@ contract ChannelHub is IVault, ReentrancyGuard {
ctx.nodeAvailableFunds = nodeBalance;
ctx.challengeExpiry = meta.challengeExpireAt;
+ address token = meta.lastState.homeLedger.token;
+ ctx.isParametricToken = isParametricToken[token];
+ ctx.channelSubId = meta.subId;
+
return ctx;
}
- function _buildEscrowDepositContext(bytes32 escrowId, uint256 nodeAvailableFunds)
- internal
- view
- returns (EscrowDepositEngine.TransitionContext memory ctx)
- {
+ function _buildEscrowDepositContext(
+ bytes32 escrowId,
+ uint256 nodeAvailableFunds
+ ) internal view returns (EscrowDepositEngine.TransitionContext memory ctx) {
EscrowDepositMeta storage meta = _escrowDeposits[escrowId];
ctx.status = meta.status;
@@ -1013,11 +1223,10 @@ contract ChannelHub is IVault, ReentrancyGuard {
return ctx;
}
- function _buildEscrowWithdrawalContext(bytes32 escrowId, address node)
- internal
- view
- returns (EscrowWithdrawalEngine.TransitionContext memory ctx)
- {
+ function _buildEscrowWithdrawalContext(
+ bytes32 escrowId,
+ address node
+ ) internal view returns (EscrowWithdrawalEngine.TransitionContext memory ctx) {
EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId];
ctx.status = meta.status;
@@ -1047,14 +1256,12 @@ contract ChannelHub is IVault, ReentrancyGuard {
meta.status = effects.newStatus;
}
- if (effects.clearDispute) {
- meta.status = ChannelStatus.OPERATING;
- meta.challengeExpireAt = 0;
+ if (meta.challengeExpireAt != effects.newChallengeExpiry) {
+ meta.challengeExpireAt = effects.newChallengeExpiry;
}
if (effects.closeChannel) {
meta.lockedFunds = 0;
- meta.challengeExpireAt = 0;
}
}
@@ -1075,32 +1282,50 @@ contract ChannelHub is IVault, ReentrancyGuard {
// Process POSITIVE deltas first (additions to lockedFunds) to prevent underflow
if (effects.userFundsDelta > 0) {
uint256 amount = effects.userFundsDelta.toUint256();
- _pullFunds(def.user, token, amount);
+ _pullFunds(def.user, meta.subId, token, amount);
meta.lockedFunds += amount;
}
if (effects.nodeFundsDelta > 0) {
uint256 amount = effects.nodeFundsDelta.toUint256();
- _nodeBalances[def.node][token] -= amount;
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+ uint256 updatedBalance = nodeBalance - amount;
+ if (!isParametricToken[token]) {
+ _nodeBalances[def.node][token] = updatedBalance;
+ } else {
+ _nodeSubBalances[def.node][token][meta.subId] = updatedBalance;
+ }
+
meta.lockedFunds += amount;
+
+ emit NodeBalanceUpdated(def.node, token, meta.subId, updatedBalance);
}
// Then process NEGATIVE deltas (subtractions from lockedFunds)
if (effects.userFundsDelta < 0) {
uint256 amount = (-effects.userFundsDelta).toUint256();
- _pushFunds(def.user, token, amount);
+ _pushFunds(meta.subId, def.user, token, amount);
meta.lockedFunds -= amount;
}
if (effects.nodeFundsDelta < 0) {
uint256 amount = (-effects.nodeFundsDelta).toUint256();
- _nodeBalances[def.node][token] += amount;
+ uint256 nodeBalance = _getNodeBalance(def.node, candidate.homeLedger.token, meta.subId);
+ uint256 updatedBalance = nodeBalance + amount;
+ if (!isParametricToken[token]) {
+ _nodeBalances[def.node][token] = updatedBalance;
+ } else {
+ _nodeSubBalances[def.node][token][meta.subId] = updatedBalance;
+ }
+
meta.lockedFunds -= amount;
+
+ emit NodeBalanceUpdated(def.node, token, meta.subId, updatedBalance);
}
// Special handling for CLOSE: push nodeAllocation directly to node address
if (effects.closeChannel && candidate.homeLedger.nodeAllocation > 0) {
- _pushFunds(def.node, token, candidate.homeLedger.nodeAllocation);
+ _pushFunds(meta.subId, def.node, token, candidate.homeLedger.nodeAllocation);
meta.lockedFunds -= candidate.homeLedger.nodeAllocation;
}
@@ -1124,11 +1349,7 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
if (effects.updateInitState) {
- meta.initState = candidate;
- meta.channelId = channelId;
- meta.user = user;
- meta.node = node;
- meta.approvedSignatureValidators = approvedSignatureValidators;
+ _initEscrowDepositMetadata(escrowId, channelId, candidate, user, node, approvedSignatureValidators);
}
if (effects.newUnlockAt > 0) {
@@ -1145,23 +1366,27 @@ contract ChannelHub is IVault, ReentrancyGuard {
// Handle user funds (positive = pull from user)
if (effects.userFundsDelta > 0) {
uint256 amount = effects.userFundsDelta.toUint256();
- _pullFunds(user, token, amount);
+ _pullFunds(user, 0, token, amount);
meta.lockedAmount += amount;
} else if (effects.userFundsDelta < 0) {
uint256 amount = (-effects.userFundsDelta).toUint256();
- _pushFunds(user, token, amount);
+ _pushFunds(0, user, token, amount);
meta.lockedAmount -= amount;
}
// Handle node funds (positive = pull from node vault, negative = release to vault)
if (effects.nodeFundsDelta > 0) {
uint256 amount = effects.nodeFundsDelta.toUint256();
- _nodeBalances[node][token] -= amount;
+ uint256 updatedBalance = _nodeBalances[node][token] - amount;
+ _nodeBalances[node][token] = updatedBalance;
meta.lockedAmount += amount;
+ emit NodeBalanceUpdated(node, token, 0, updatedBalance);
} else if (effects.nodeFundsDelta < 0) {
uint256 amount = (-effects.nodeFundsDelta).toUint256();
- _nodeBalances[node][token] += amount;
+ uint256 updatedBalance = _nodeBalances[node][token] + amount;
+ _nodeBalances[node][token] = updatedBalance;
meta.lockedAmount -= amount;
+ emit NodeBalanceUpdated(node, token, 0, updatedBalance);
}
// NOTE: purge escrow deposits to unlock unutilized node liquidity
@@ -1184,11 +1409,7 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
if (effects.updateInitState) {
- meta.initState = candidate;
- meta.channelId = channelId;
- meta.user = user;
- meta.node = node;
- meta.approvedSignatureValidators = approvedSignatureValidators;
+ _initEscrowWithdrawalMetadata(escrowId, channelId, candidate, user, node, approvedSignatureValidators);
}
if (effects.newChallengeExpiry > 0) {
@@ -1201,29 +1422,65 @@ contract ChannelHub is IVault, ReentrancyGuard {
// Handle user funds (negative = push to user)
if (effects.userFundsDelta > 0) {
uint256 amount = effects.userFundsDelta.toUint256();
- _pullFunds(user, token, amount);
+ _pullFunds(user, 0, token, amount);
meta.lockedAmount += amount;
} else if (effects.userFundsDelta < 0) {
uint256 amount = (-effects.userFundsDelta).toUint256();
- _pushFunds(user, token, amount);
+ _pushFunds(0, user, token, amount);
meta.lockedAmount -= amount;
}
// Handle node funds (positive = pull from node vault, negative = release to vault)
if (effects.nodeFundsDelta > 0) {
uint256 amount = effects.nodeFundsDelta.toUint256();
- _nodeBalances[node][token] -= amount;
+ uint256 updatedBalance = _nodeBalances[node][token] - amount;
+ _nodeBalances[node][token] = updatedBalance;
meta.lockedAmount += amount;
+ emit NodeBalanceUpdated(node, token, 0, updatedBalance);
} else if (effects.nodeFundsDelta < 0) {
uint256 amount = (-effects.nodeFundsDelta).toUint256();
- _nodeBalances[node][token] += amount;
+ uint256 updatedBalance = _nodeBalances[node][token] + amount;
+ _nodeBalances[node][token] = updatedBalance;
meta.lockedAmount -= amount;
+ emit NodeBalanceUpdated(node, token, 0, updatedBalance);
}
// NOTE: purge escrow deposits to unlock unutilized node liquidity
_purgeEscrowDeposits();
}
+ function _initEscrowDepositMetadata(
+ bytes32 escrowId,
+ bytes32 channelId,
+ State memory candidate,
+ address user,
+ address node,
+ uint256 approvedSignatureValidators
+ ) internal {
+ EscrowDepositMeta storage meta = _escrowDeposits[escrowId];
+ meta.channelId = channelId;
+ meta.initState = candidate;
+ meta.user = user;
+ meta.node = node;
+ meta.approvedSignatureValidators = approvedSignatureValidators;
+ }
+
+ function _initEscrowWithdrawalMetadata(
+ bytes32 escrowId,
+ bytes32 channelId,
+ State memory candidate,
+ address user,
+ address node,
+ uint256 approvedSignatureValidators
+ ) internal {
+ EscrowWithdrawalMeta storage meta = _escrowWithdrawals[escrowId];
+ meta.channelId = channelId;
+ meta.initState = candidate;
+ meta.user = user;
+ meta.node = node;
+ meta.approvedSignatureValidators = approvedSignatureValidators;
+ }
+
function _requireValidDefinition(ChannelDefinition calldata def) internal pure {
require(def.user != address(0), InvalidAddress());
require(def.node != address(0), InvalidAddress());
@@ -1240,7 +1497,7 @@ contract ChannelHub is IVault, ReentrancyGuard {
return _channels[channelId].lastState.homeLedger.chainId == block.chainid;
}
- function _pullFunds(address from, address token, uint256 amount) internal nonReentrant {
+ function _pullFunds(address from, uint48 toSubId, address token, uint256 amount) internal nonReentrant {
if (amount == 0) return;
if (token == address(0)) {
@@ -1250,38 +1507,71 @@ contract ChannelHub is IVault, ReentrancyGuard {
}
if (token != address(0)) {
- IERC20(token).safeTransferFrom(from, address(this), amount);
+ if (!isParametricToken[token]) {
+ // Non-parametric token
+ IERC20(token).safeTransferFrom(from, address(this), amount);
+ } else {
+ // Parametric token with sub-account
+ IParametricToken(token).approvedTransferToSub(from, address(this), toSubId, amount);
+ }
}
}
- function _pushFunds(address to, address token, uint256 amount) internal nonReentrant {
+ function _pushFunds(uint48 fromSubId, address to, address token, uint256 amount) internal nonReentrant {
if (amount == 0) return;
if (token == address(0)) {
// Native token: limit gas to prevent depletion attacks
- (bool success,) = payable(to).call{value: amount, gas: TRANSFER_GAS_LIMIT}("");
+ (bool success, ) = payable(to).call{value: amount, gas: TRANSFER_GAS_LIMIT}("");
if (!success) {
- _reclaims[to][token] += amount;
- emit TransferFailed(to, token, amount);
+ if (!isParametricToken[token]) {
+ _reclaims[to][token] += amount;
+ } else {
+ _subReclaims[to][token][fromSubId] += amount;
+ }
+ emit TransferFailed(fromSubId, to, token, amount);
return;
}
} else {
- // ERC20: Use balance-checking approach for maximum robustness
- uint256 balanceBefore = IERC20(token).balanceOf(address(this));
+ if (!isParametricToken[token]) {
+ // ERC20: Use balance-checking approach for maximum robustness
+ uint256 balanceBefore = IERC20(token).balanceOf(address(this));
+
+ // limit gas to prevent depletion attacks
+ (bool success, ) = address(token).call{gas: TRANSFER_GAS_LIMIT}(
+ abi.encodeCall(IERC20.transfer, (to, amount))
+ );
+
+ uint256 balanceAfter = IERC20(token).balanceOf(address(this));
+
+ // Success criteria: call succeeded AND sufficient balance AND balance decreased by exactly the expected amount
+ // Check balanceBefore >= amount first to prevent underflow revert
+ bool transferSucceeded = success && balanceBefore >= amount && balanceAfter == balanceBefore - amount;
+
+ if (!transferSucceeded) {
+ _reclaims[to][token] += amount;
+ emit TransferFailed(fromSubId, to, token, amount);
+ }
+ } else {
+ // ERC20: Use balance-checking approach for maximum robustness
+ uint256 subBalanceBefore = IParametricToken(token).balanceOfSub(address(this), fromSubId);
- // limit gas to prevent depletion attacks
- (bool success,) =
- address(token).call{gas: TRANSFER_GAS_LIMIT}(abi.encodeCall(IERC20.transfer, (to, amount)));
+ // limit gas to prevent depletion attacks
+ (bool success, ) = address(token).call{gas: TRANSFER_GAS_LIMIT}(
+ abi.encodeCall(IParametricToken.transferFromSub, (fromSubId, to, amount))
+ );
- uint256 balanceAfter = IERC20(token).balanceOf(address(this));
+ uint256 subBalanceAfter = IParametricToken(token).balanceOfSub(address(this), fromSubId);
- // Success criteria: call succeeded AND sufficient balance AND balance decreased by exactly the expected amount
- // Check balanceBefore >= amount first to prevent underflow revert
- bool transferSucceeded = success && balanceBefore >= amount && balanceAfter == balanceBefore - amount;
+ // Success criteria: call succeeded AND sufficient balance AND balance decreased by exactly the expected amount
+ // Check balanceBefore >= amount first to prevent underflow revert
+ bool transferSucceeded =
+ success && subBalanceBefore >= amount && subBalanceAfter == subBalanceBefore - amount;
- if (!transferSucceeded) {
- _reclaims[to][token] += amount;
- emit TransferFailed(to, token, amount);
+ if (!transferSucceeded) {
+ _subReclaims[to][token][fromSubId] += amount;
+ emit TransferFailed(fromSubId, to, token, amount);
+ }
}
}
}
diff --git a/contracts/src/EscrowDepositEngine.sol b/contracts/src/EscrowDepositEngine.sol
index cb3ca756e..4d4d4d88f 100644
--- a/contracts/src/EscrowDepositEngine.sol
+++ b/contracts/src/EscrowDepositEngine.sol
@@ -23,6 +23,7 @@ library EscrowDepositEngine {
error IncorrectEscrowStatus();
error EscrowAlreadyExists();
error EscrowAlreadyFinalized();
+ error ChallengeExpired();
error IncorrectUserAllocation();
error UserAllocationAndNetFlowMismatch();
@@ -128,6 +129,11 @@ library EscrowDepositEngine {
require(netFlowsSum >= 0, NegativeNetFlowSum());
require(allocsSum == netFlowsSum.toUint256(), InvalidAllocationSum());
+
+ // If channel is DISPUTED, check that challenge hasn't expired
+ if (ctx.status == EscrowStatus.DISPUTED) {
+ require(block.timestamp <= ctx.challengeExpiry, ChallengeExpired());
+ }
}
// ========== Internal: Phase 2 - Intent-Specific Calculation ==========
diff --git a/contracts/src/EscrowWithdrawalEngine.sol b/contracts/src/EscrowWithdrawalEngine.sol
index b32cee4ab..fa9ffc5d6 100644
--- a/contracts/src/EscrowWithdrawalEngine.sol
+++ b/contracts/src/EscrowWithdrawalEngine.sol
@@ -22,6 +22,7 @@ library EscrowWithdrawalEngine {
error IncorrectEscrowStatus();
error EscrowAlreadyExists();
error EscrowAlreadyFinalized();
+ error ChallengeExpired();
error IncorrectHomeChain();
error IncorrectNonHomeChain();
@@ -124,6 +125,11 @@ library EscrowWithdrawalEngine {
require(netFlowsSum >= 0, NegativeNetFlowSum());
require(allocsSum == netFlowsSum.toUint256(), InvalidAllocationSum());
+
+ // If channel is DISPUTED, check that challenge hasn't expired
+ if (ctx.status == EscrowStatus.DISPUTED) {
+ require(block.timestamp <= ctx.challengeExpiry, ChallengeExpired());
+ }
}
// ========== Internal: Phase 2 - Intent-Specific Calculation ==========
diff --git a/contracts/src/ParametricToken.sol b/contracts/src/ParametricToken.sol
new file mode 100644
index 000000000..f20af60c0
--- /dev/null
+++ b/contracts/src/ParametricToken.sol
@@ -0,0 +1,713 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.30;
+
+import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+import "./interfaces/IParametricToken.sol";
+import "forge-std/console.sol";
+
+contract ParametricToken is ERC20, IParametricToken {
+ uint8 public constant NUMBER_OF_PARAMETERS = 1;
+
+ struct Account {
+ AccountType accountType;
+ uint256 balance;
+ uint64[NUMBER_OF_PARAMETERS] parameters;
+ }
+
+ struct ParamConfig {
+ bytes32 name;
+ uint8 decimals;
+ bool isMutable;
+ }
+
+ struct SubAccount {
+ uint256 balance;
+ uint64[NUMBER_OF_PARAMETERS] parameters;
+ }
+
+ struct SuperAccount {
+ SubAccount[] subs;
+ uint48 subsCount;
+ }
+
+ struct Allowance {
+ uint256 total;
+ uint256 sub;
+ uint48 subId;
+ }
+
+ ParamConfig[NUMBER_OF_PARAMETERS] public PARAM_CONFIG;
+ uint64 constant IMMUTABLE_PARAMETER = 1;
+
+ mapping(address => Account) private _accounts;
+ mapping(address => SuperAccount) private _supers;
+ mapping(address => mapping(address => Allowance)) private _allowances;
+
+ uint64[NUMBER_OF_PARAMETERS] private _parametersInit;
+
+ modifier onlyNormal(address account) {
+ require(_accounts[account].accountType == AccountType.Normal, "Not a normal account");
+ _;
+ }
+
+ modifier onlySuper(address account) {
+ require(_accounts[account].accountType == AccountType.Super, "Not a super account");
+ _;
+ }
+
+ modifier onlyValidSub(address account, uint48 subId) {
+ require(_accounts[account].accountType == AccountType.Super, "Not a super account");
+ require(subId < _supers[account].subsCount, "Sub-account doesn't exist");
+ _;
+ }
+
+ constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {
+ PARAM_CONFIG = [ParamConfig({name: bytes32("myParam"), decimals: 0, isMutable: true})];
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ _parametersInit[i] = 0;
+ }
+ }
+
+ // ========== ERC20 Overrides ==========
+
+ function transfer(address to, uint256 amount) public override(ERC20, IERC20) returns (bool) {
+ address from = _msgSender();
+
+ Account storage fromAcc = _accounts[from];
+ Account storage toAcc = _accounts[to];
+
+ require(_noParamsConflict(from, 0, to, 0), "Conflict of parameters");
+
+ if (fromAcc.accountType == AccountType.Normal && toAcc.accountType == AccountType.Normal) {
+ // Normal transfer
+ bool success = super.transfer(to, amount);
+ if (success) {
+ fromAcc.balance -= amount;
+ toAcc.balance += amount;
+ }
+ return success;
+ }
+
+ revert("Standard transfer not allowed for super accounts");
+ }
+
+ function transferFrom(address from, address to, uint256 amount) public override(ERC20, IERC20) returns (bool) {
+ Account storage fromAcc = _accounts[from];
+ Account storage toAcc = _accounts[to];
+
+ require(_noParamsConflict(from, 0, to, 0), "Conflict of parameters");
+
+ if (fromAcc.accountType == AccountType.Normal && toAcc.accountType == AccountType.Normal) {
+ // Check allowance
+ uint256 allowed = _allowances[from][_msgSender()].total;
+ require(allowed >= amount, "Insufficient allowance");
+
+ bool success = super.transferFrom(from, to, amount);
+ if (success) {
+ fromAcc.balance -= amount;
+ toAcc.balance += amount;
+ _allowances[from][_msgSender()].total -= amount;
+ }
+ return success;
+ }
+
+ revert("TransferFrom not allowed for super accounts");
+ }
+
+ function allowance(address owner, address spender) public view override(ERC20, IERC20) returns (uint256) {
+ return _allowances[owner][spender].total;
+ }
+
+ function approve(address spender, uint256 amount) public override(ERC20, IERC20) returns (bool) {
+ address owner = _msgSender();
+
+ _allowances[owner][spender].total = amount;
+
+ if (_accounts[owner].accountType == AccountType.Super && _allowances[owner][spender].sub > amount) {
+ _allowances[owner][spender].sub = amount;
+ }
+
+ super.approve(spender, amount);
+
+ emit Approval(owner, spender, amount);
+ return true;
+ }
+
+ // ========== Account Queries ==========
+
+ function name() public view override(ERC20) returns (string memory) {
+ return super.name();
+ }
+
+ function symbol() public view override(ERC20) returns (string memory) {
+ return super.symbol();
+ }
+
+ function decimals() public view override(ERC20) returns (uint8) {
+ return super.decimals();
+ }
+
+ function totalSupply() public view override(ERC20, IERC20) returns (uint256) {
+ return super.totalSupply();
+ }
+
+ function balanceOf(address account) public view override(ERC20, IERC20) returns (uint256) {
+ return _accounts[account].balance;
+ }
+
+ // ========== Account Management ==========
+
+ function accountType(address account) external view returns (AccountType) {
+ return _accounts[account].accountType;
+ }
+
+ function convertToSuper(address account) external onlyNormal(account) returns (bool) {
+ require(_msgSender() == account, "Only owner can convert");
+
+ Account storage acc = _accounts[account];
+
+ // Convert to super account
+ acc.accountType = AccountType.Super;
+
+ // Create subId 0 with current balance
+ _supers[account].subs.push(SubAccount({balance: acc.balance, parameters: acc.parameters}));
+ _supers[account].subsCount = 1;
+
+ // Clear normal parameters
+ acc.parameters = _parametersInit;
+
+ emit AccountConvertedToSuper(account);
+ emit SubAccountCreated(account, 0);
+
+ return true;
+ }
+
+ function createSubAccount(address account) external onlySuper(account) returns (uint48) {
+ require(_msgSender() == account, "Only owner can create");
+
+ SuperAccount storage acc = _supers[account];
+
+ acc.subs.push(SubAccount({balance: 0, parameters: _parametersInit}));
+ acc.subsCount = uint48(acc.subs.length);
+ uint48 newSubId = acc.subsCount - 1;
+
+ emit SubAccountCreated(account, newSubId);
+
+ return newSubId;
+ }
+
+ // ========== Sub-account Queries ==========
+
+ function balanceOfSub(address account, uint48 subId) external view onlySuper(account) returns (uint256) {
+ require(subId < _supers[account].subsCount, "Sub-account doesn't exist");
+ return _supers[account].subs[subId].balance;
+ }
+
+ function subsCountOf(address account) external view onlySuper(account) returns (uint48) {
+ return _supers[account].subsCount;
+ }
+
+ function numberOfParameters() external pure returns (uint8) {
+ return NUMBER_OF_PARAMETERS;
+ }
+
+ function parameterOf(uint8 paramIndex, address account) external view onlyNormal(account) returns (uint64) {
+ require(paramIndex < NUMBER_OF_PARAMETERS, "Index exceeds number of parameters");
+ return _accounts[account].parameters[paramIndex];
+ }
+
+ function parameterOfSub(
+ uint8 paramIndex,
+ address account,
+ uint48 subId
+ ) external view onlyValidSub(account, subId) returns (uint64) {
+ require(paramIndex < NUMBER_OF_PARAMETERS, "Index exceeds number of parameters");
+ return _supers[account].subs[subId].parameters[paramIndex];
+ }
+
+ // ========== Allowances ==========
+
+ function allowanceForSub(
+ address owner,
+ uint48 subId,
+ address spender
+ ) external view onlyValidSub(owner, subId) returns (uint256) {
+ Allowance storage al = _allowances[owner][spender];
+ if (al.subId == subId) {
+ return al.sub;
+ }
+ return al.total - al.sub;
+ }
+
+ function approveForSub(uint48 ownerSubId, address spender, uint256 amount) external returns (bool) {
+ address owner = _msgSender();
+ Account storage acc = _accounts[owner];
+ require(acc.accountType == AccountType.Super, "Not a super account");
+ require(ownerSubId < _supers[owner].subsCount, "Sub-account doesn't exist");
+
+ Allowance storage al = _allowances[owner][spender];
+
+ al.subId = ownerSubId;
+ al.sub = amount;
+
+ // Adjust total if needed
+ if (amount > al.total) {
+ al.total = amount; // Total becomes at least the sub-amount
+ }
+
+ emit ApprovalForSub(owner, ownerSubId, spender, amount);
+
+ return true;
+ }
+
+ // Helper to check and consume allowance for a specific subId
+ function _sufficientAllowanceForSub(
+ address owner,
+ address spender,
+ uint48 fromSubId,
+ uint256 amount
+ ) internal view onlyValidSub(owner, fromSubId) returns (bool) {
+ Allowance storage al = _allowances[owner][spender];
+ return fromSubId == al.subId ? al.sub >= amount : al.total - al.sub >= amount;
+ }
+
+ function _consumeAllowanceForSub(
+ address owner,
+ address spender,
+ uint48 fromSubId,
+ uint256 amount
+ ) internal onlyValidSub(owner, fromSubId) {
+ Allowance storage al = _allowances[owner][spender];
+
+ if (fromSubId == al.subId) {
+ // Use sub-allowance first
+ require(al.sub >= amount, "Insufficient sub-allowance");
+ al.sub -= amount;
+ al.total -= amount;
+ } else {
+ // Use from remaining total allowance (total - sub)
+ uint256 remaining = al.total - al.sub;
+ require(remaining >= amount, "Insufficient allowance for this subId");
+ al.total -= amount;
+ }
+ }
+
+ function _noParamsConflict(address from, uint48 fromSubId, address to, uint48 toSubId) private view returns (bool) {
+ uint64[NUMBER_OF_PARAMETERS] memory fromParams;
+ uint64[NUMBER_OF_PARAMETERS] memory toParams;
+
+ if (_accounts[from].balance == 0 || _accounts[to].balance == 0) return true;
+
+ if (_accounts[from].accountType == AccountType.Normal) {
+ fromParams = _accounts[from].parameters;
+ } else {
+ require(fromSubId < _supers[from].subsCount, "Subaccount doesn't exist");
+ fromParams = _supers[from].subs[fromSubId].parameters;
+ }
+
+ if (_accounts[to].accountType == AccountType.Normal) {
+ toParams = _accounts[to].parameters;
+ } else {
+ require(toSubId < _supers[to].subsCount, "Subaccount doesn't exist");
+ toParams = _supers[to].subs[toSubId].parameters;
+ }
+
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (!PARAM_CONFIG[i].isMutable && fromParams[i] != toParams[i]) return false;
+ }
+
+ return true;
+ }
+
+ function _weightedAverage(
+ uint64 param1,
+ uint256 amount1,
+ uint64 param2,
+ uint256 amount2
+ ) private pure returns (uint64) {
+ require(amount1 > 0 && amount2 > 0, "Invalid amounts");
+ uint256 sumProduct = uint256(param1) * amount1 + uint256(param2) * amount2;
+ uint256 sum = amount1 + amount2;
+
+ return uint64(sumProduct / sum);
+ }
+
+ // ========== Transfers ==========
+
+ function transferToSub(
+ address toSuper,
+ uint48 toSubId,
+ uint256 amount
+ ) external onlyValidSub(toSuper, toSubId) returns (bool) {
+ address from = _msgSender();
+ Account storage fromAcc = _accounts[from];
+ Account storage toAcc = _accounts[toSuper];
+
+ require(amount > 0, "Void amount");
+ require(fromAcc.accountType == AccountType.Normal, "Sender must be normal");
+
+ require(_noParamsConflict(from, 0, toSuper, toSubId), "Conflict of parameters");
+ require(fromAcc.balance >= amount, "Insufficient balance");
+ fromAcc.balance -= amount;
+
+ SubAccount storage toSubAcc = _supers[toSuper].subs[toSubId];
+ uint256 oldSubBalance = toSubAcc.balance;
+ toSubAcc.balance += amount;
+ toAcc.balance += amount;
+
+ // Update toSubAcc parameters
+ if (oldSubBalance > 0) {
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ toSubAcc.parameters[i] = _weightedAverage(
+ toSubAcc.parameters[i],
+ oldSubBalance,
+ fromAcc.parameters[i],
+ amount
+ );
+ }
+ }
+ } else {
+ toSubAcc.parameters = fromAcc.parameters;
+ }
+
+ // Update fromAcc parameters
+ if (fromAcc.balance == 0) fromAcc.parameters = _parametersInit;
+
+ emit TransferToSub(from, toSuper, toSubId, amount);
+ emit Transfer(from, toSuper, amount);
+
+ return true;
+ }
+
+ function transferFromSub(uint48 fromSubId, address to, uint256 amount) external returns (bool) {
+ address fromSuper = _msgSender();
+ Account storage fromAcc = _accounts[fromSuper];
+ Account storage toAcc = _accounts[to];
+
+ require(amount > 0, "Void amount");
+ require(fromAcc.accountType == AccountType.Super, "Not a super account");
+ SuperAccount storage fromSuperAcc = _supers[fromSuper];
+ require(toAcc.accountType == AccountType.Normal, "Recipient must be normal");
+ require(fromSubId < fromSuperAcc.subsCount, "Sub-account doesn't exist");
+
+ require(_noParamsConflict(fromSuper, fromSubId, to, 0), "Conflict of parameters");
+ require(fromSuperAcc.subs[fromSubId].balance >= amount, "Insufficient balance");
+ fromSuperAcc.subs[fromSubId].balance -= amount;
+ fromAcc.balance -= amount;
+
+ uint256 oldToBalance = toAcc.balance;
+ toAcc.balance += amount;
+
+ // Update toAcc parameters
+ if (oldToBalance > 0) {
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ toAcc.parameters[i] = _weightedAverage(
+ toAcc.parameters[i],
+ oldToBalance,
+ fromSuperAcc.subs[fromSubId].parameters[i],
+ amount
+ );
+ }
+ }
+ } else {
+ toAcc.parameters = fromSuperAcc.subs[fromSubId].parameters;
+ }
+
+ // Update fromAcc parameters
+ if (fromAcc.balance == 0) fromSuperAcc.subs[fromSubId].parameters = _parametersInit;
+
+ emit TransferFromSub(fromSuper, fromSubId, to, amount);
+ emit Transfer(fromSuper, to, amount);
+
+ return true;
+ }
+
+ function transferBetweenSubs(uint48 fromSubId, uint48 toSubId, uint256 amount) external returns (bool) {
+ address superAccount = _msgSender();
+ Account storage acc = _accounts[superAccount];
+
+ require(acc.accountType == AccountType.Super, "Not a super account");
+ SuperAccount storage superAcc = _supers[superAccount];
+
+ require(fromSubId < superAcc.subsCount && toSubId < superAcc.subsCount, "Sub-account doesn't exist");
+ require(_noParamsConflict(superAccount, fromSubId, superAccount, toSubId), "Conflict of parameters");
+
+ require(superAcc.subs[fromSubId].balance >= amount, "Insufficient balance");
+
+ // Update fromSub
+ superAcc.subs[fromSubId].balance -= amount;
+
+ // Update toSub
+ uint256 oldSubBalance = superAcc.subs[toSubId].balance;
+ superAcc.subs[toSubId].balance += amount;
+
+ // Update toSubId parameters
+ if (oldSubBalance > 0) {
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ superAcc.subs[toSubId].parameters[i] = _weightedAverage(
+ superAcc.subs[toSubId].parameters[i],
+ oldSubBalance,
+ superAcc.subs[fromSubId].parameters[i],
+ amount
+ );
+ }
+ }
+ } else {
+ superAcc.subs[toSubId].parameters = superAcc.subs[fromSubId].parameters;
+ }
+
+ // Update fromSubId parameters
+ if (superAcc.subs[fromSubId].balance == 0) superAcc.subs[fromSubId].parameters = _parametersInit;
+
+ emit TransferBetweenSubs(superAccount, fromSubId, toSubId, amount);
+
+ return true;
+ }
+
+ // ========== Approved Transfers ==========
+
+ function approvedTransferToSub(
+ address from,
+ address toSuper,
+ uint48 toSubId,
+ uint256 amount
+ ) external onlyValidSub(toSuper, toSubId) returns (bool) {
+ address spender = _msgSender();
+
+ // Execute transfer
+ Account storage fromAcc = _accounts[from];
+ Allowance storage al = _allowances[from][spender];
+
+ require(amount > 0, "Void amount");
+ require(fromAcc.accountType == AccountType.Normal, "From must be normal");
+ require(_noParamsConflict(from, 0, toSuper, toSubId), "Conflict of parameters");
+ require(fromAcc.balance >= amount, "Insufficient balance");
+ require(al.total >= amount, "Insufficient allowance");
+ fromAcc.balance -= amount;
+
+ SubAccount storage toSubAcc = _supers[toSuper].subs[toSubId];
+ uint256 oldSubBalance = toSubAcc.balance;
+ toSubAcc.balance += amount;
+ _accounts[toSuper].balance += amount;
+
+ // Update toSubAcc parameters
+ if (oldSubBalance > 0) {
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ toSubAcc.parameters[i] = _weightedAverage(
+ toSubAcc.parameters[i],
+ oldSubBalance,
+ fromAcc.parameters[i],
+ amount
+ );
+ }
+ }
+ } else {
+ toSubAcc.parameters = fromAcc.parameters;
+ }
+
+ // Update fromAcc parameters
+ if (fromAcc.balance == 0) fromAcc.parameters = _parametersInit;
+
+ // Consume allowance
+ al.total -= amount;
+
+ emit TransferToSub(from, toSuper, toSubId, amount);
+ emit Transfer(from, toSuper, amount);
+
+ return true;
+ }
+
+ function approvedTransferFromSubToSub(
+ address fromSuper,
+ uint48 fromSubId,
+ address toSuper,
+ uint48 toSubId,
+ uint256 amount
+ ) external onlyValidSub(fromSuper, fromSubId) onlyValidSub(toSuper, toSubId) returns (bool) {
+ address spender = _msgSender();
+
+ // Execute transfer from sub to sub
+ SuperAccount storage fromSuperAcc = _supers[fromSuper];
+
+ require(amount > 0, "Void amount");
+ require(fromSuperAcc.subs[fromSubId].balance >= amount, "Insufficient balance");
+ require(_sufficientAllowanceForSub(fromSuper, spender, fromSubId, amount), "Insufficient allowance");
+
+ fromSuperAcc.subs[fromSubId].balance -= amount;
+ _accounts[fromSuper].balance -= amount;
+
+ SubAccount storage toSubAcc = _supers[toSuper].subs[toSubId];
+ uint256 oldSubBalance = toSubAcc.balance;
+ toSubAcc.balance += amount;
+ _accounts[toSuper].balance += amount;
+
+ // Update toSubAcc parameters
+ if (oldSubBalance > 0) {
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ toSubAcc.parameters[i] = _weightedAverage(
+ toSubAcc.parameters[i],
+ oldSubBalance,
+ fromSuperAcc.subs[fromSubId].parameters[i],
+ amount
+ );
+ }
+ }
+ } else {
+ toSubAcc.parameters = fromSuperAcc.subs[fromSubId].parameters;
+ }
+
+ // Check and consume allowance
+ _consumeAllowanceForSub(fromSuper, spender, fromSubId, amount);
+
+ emit TransferFromSubToSub(fromSuper, fromSubId, toSuper, toSubId, amount);
+ if (fromSuper != toSuper) emit Transfer(fromSuper, toSuper, amount);
+
+ return true;
+ }
+
+ // ========== Mint/Burn Helpers ==========
+
+ function mint(uint256 amount) external {
+ require(amount > 0, "Void amount");
+ address to = _msgSender();
+ _mintParametric(to, amount);
+ }
+
+ // function _mintParametric(address account, uint256 amount) internal {
+ // super._mint(account, amount);
+ // _accounts[account].balance += amount;
+ // // parameter logic...
+ // }
+
+ function _mintParametric(address account, uint256 amount) internal {
+ console.log(">> _mintParametric called");
+ console.log(">> account:", account);
+
+ Account storage acc = _accounts[account];
+
+ if (acc.accountType == AccountType.Super) {
+ // Mint to sub-account 0
+ SuperAccount storage superAcc = _supers[account];
+ require(superAcc.subsCount > 0, "No sub-accounts");
+
+ SubAccount storage sub0 = superAcc.subs[0];
+
+ // Calculate new weighted average parameters
+ uint256 oldBalance = sub0.balance;
+
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ if (oldBalance == 0) {
+ // First mint to this sub - set to block.timestamp
+ sub0.parameters[i] = uint64(block.timestamp);
+ } else {
+ // Weighted average for non-zero
+ sub0.parameters[i] = _weightedAverage(
+ sub0.parameters[i],
+ oldBalance,
+ uint64(block.timestamp),
+ amount
+ );
+ }
+ } else {
+ // Immutable parameter
+ if (oldBalance == 0) {
+ // First mint - set to constant
+ sub0.parameters[i] = IMMUTABLE_PARAMETER;
+ }
+ // If oldBalance > 0, immutable parameter stays as is (no change)
+ }
+ }
+
+ // Update balances
+ sub0.balance += amount;
+ acc.balance += amount;
+
+ super._mint(account, amount);
+ } else {
+ // Normal account
+ uint256 oldBalance = acc.balance;
+
+ for (uint256 i = 0; i < NUMBER_OF_PARAMETERS; i++) {
+ if (PARAM_CONFIG[i].isMutable) {
+ if (oldBalance == 0) {
+ // First mint - set to block.timestamp
+ acc.parameters[i] = uint64(block.timestamp);
+ } else {
+ // Non-zero balance
+ acc.parameters[i] = _weightedAverage(
+ acc.parameters[i],
+ oldBalance,
+ uint64(block.timestamp),
+ amount
+ );
+ }
+ } else {
+ // Immutable parameter
+ if (oldBalance == 0) {
+ // First mint - set to constant
+ acc.parameters[i] = IMMUTABLE_PARAMETER;
+ }
+ // If oldBalance > 0, immutable parameter stays as is
+ }
+ }
+
+ acc.balance += amount;
+ super._mint(account, amount);
+ }
+ }
+
+ // function _burn(address account, uint256 amount) internal {
+ // super._burn(account, amount);
+ // // Your custom logic here
+ // _accounts[account].balance -= amount;
+ // }
+
+ function _burnParametric(address account, uint256 amount) internal {
+ Account storage acc = _accounts[account];
+ require(amount > 0, "Void amount");
+ require(account == _msgSender(), "Burn allowed only from own account");
+
+ if (acc.accountType == AccountType.Super) {
+ // Burn from sub-account 0
+ SuperAccount storage superAcc = _supers[account];
+ require(superAcc.subsCount > 0, "No sub-accounts");
+
+ SubAccount storage sub0 = superAcc.subs[0];
+ require(sub0.balance >= amount, "Insufficient balance in sub-account 0");
+
+ // Calculate new balance after burn
+ uint256 newBalance = sub0.balance - amount;
+
+ // Update parameters if balance becomes zero
+ if (newBalance == 0) sub0.parameters = _parametersInit;
+
+ // Note: When balance > 0 after burn, parameters remain unchanged
+ // because burning doesn't introduce new tokens with different parameters
+
+ // Update balances
+ sub0.balance = newBalance;
+ acc.balance -= amount;
+
+ super._burn(account, amount);
+ } else {
+ // Normal account
+ require(acc.balance >= amount, "Insufficient balance");
+
+ uint256 newBalance = acc.balance - amount;
+
+ // Update parameters if balance becomes zero
+ if (newBalance == 0) acc.parameters = _parametersInit;
+
+ // Note: When balance > 0 after burn, parameters remain unchanged
+
+ acc.balance = newBalance;
+ super._burn(account, amount);
+ }
+ }
+}
diff --git a/contracts/src/PremintERC20.sol b/contracts/src/PremintERC20.sol
index c4f7c8026..5277c6db7 100644
--- a/contracts/src/PremintERC20.sol
+++ b/contracts/src/PremintERC20.sol
@@ -1,15 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.22;
-import {ERC20, ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
+import { ERC20, ERC20Capped } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
contract PremintERC20 is ERC20Capped {
uint8 private immutable DECIMALS;
- constructor(string memory name, string memory symbol, uint8 decimals_, address beneficiary, uint256 cap)
- ERC20(name, symbol)
- ERC20Capped(cap)
- {
+ constructor(
+ string memory name,
+ string memory symbol,
+ uint8 decimals_,
+ address beneficiary,
+ uint256 cap
+ ) ERC20(name, symbol) ERC20Capped(cap) {
DECIMALS = decimals_;
_mint(beneficiary, cap);
}
diff --git a/contracts/src/interfaces/IParametricToken.sol b/contracts/src/interfaces/IParametricToken.sol
new file mode 100644
index 000000000..e07ddb436
--- /dev/null
+++ b/contracts/src/interfaces/IParametricToken.sol
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.30;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+/**
+ * @title IParametricToken
+ * @dev Extension of ERC20 that allows a single address to manage multiple
+ * sub-accounts (partitions), each with its own parameters (e.g., mint time)
+ */
+interface IParametricToken is IERC20 {
+ // Account types
+ enum AccountType {
+ Normal,
+ Super
+ }
+
+ // Events
+ event AccountConvertedToSuper(address indexed account);
+ event SubAccountCreated(address indexed superAccount, uint48 indexed subId);
+ event TransferToSub(address indexed from, address indexed toSuper, uint48 indexed toSubId, uint256 amount);
+ event TransferFromSub(address indexed fromSuper, uint48 indexed fromSubId, address indexed to, uint256 amount);
+ event TransferBetweenSubs(
+ address indexed superAccount, uint48 indexed fromSubId, uint48 indexed toSubId, uint256 amount
+ );
+ event TransferFromSubToSub(
+ address indexed fromSuper, uint48 indexed fromSubId, address indexed toSuper, uint48 toSubId, uint256 amount
+ );
+ event ApprovalForSub(address indexed owner, uint48 indexed subId, address indexed spender, uint256 amount);
+
+ // Account management
+ function convertToSuper(address account) external returns (bool);
+
+ function createSubAccount(address account) external returns (uint48);
+
+ function accountType(address account) external view returns (AccountType);
+
+ // Sub-account queries
+ function balanceOfSub(address superAccount, uint48 subId) external view returns (uint256);
+
+ function subsCountOf(address superAccount) external view returns (uint48);
+
+ function numberOfParameters() external pure returns (uint8);
+
+ function parameterOf(uint8 paramIndex, address account) external view returns (uint64);
+
+ function parameterOfSub(uint8 paramIndex, address account, uint48 subId) external view returns (uint64);
+
+ function allowanceForSub(address owner, uint48 subId, address spender) external view returns (uint256);
+
+ // Parametric transfers
+ function transferToSub(address toSuper, uint48 toSubId, uint256 amount) external returns (bool);
+
+ function transferFromSub(uint48 fromSubId, address to, uint256 amount) external returns (bool);
+
+ function transferBetweenSubs(uint48 fromSubId, uint48 toSubId, uint256 amount) external returns (bool);
+
+ // Approved parametric transfers
+ function approveForSub(uint48 ownerSubId, address spender, uint256 amount) external returns (bool);
+
+ function approvedTransferToSub(address from, address toSuper, uint48 toSubId, uint256 amount)
+ external
+ returns (bool);
+
+ function approvedTransferFromSubToSub(
+ address fromSuper,
+ uint48 fromSubId,
+ address toSuper,
+ uint48 toSubId,
+ uint256 amount
+ ) external returns (bool);
+}
diff --git a/contracts/src/interfaces/ISignatureValidator.sol b/contracts/src/interfaces/ISignatureValidator.sol
index c456937fc..2181aa6b1 100644
--- a/contracts/src/interfaces/ISignatureValidator.sol
+++ b/contracts/src/interfaces/ISignatureValidator.sol
@@ -22,6 +22,9 @@ ValidationResult constant VALIDATION_SUCCESS = ValidationResult.wrap(1);
* construct the full message according to their specific signing scheme.
*/
interface ISignatureValidator {
+ error EmptyChannelId();
+ error InvalidSignerAddress();
+
/**
* @notice Validates a participant's signature
* @param channelId The channel identifier to be included in the signed message
diff --git a/contracts/src/interfaces/IVault.sol b/contracts/src/interfaces/IVault.sol
index f8fcaef24..a58884f38 100644
--- a/contracts/src/interfaces/IVault.sol
+++ b/contracts/src/interfaces/IVault.sol
@@ -13,7 +13,7 @@ interface IVault {
* @param token Token address (use address(0) for native tokens)
* @param amount Amount of tokens deposited
*/
- event Deposited(address indexed wallet, address indexed token, uint256 amount);
+ event Deposited(address indexed wallet, address indexed token, uint48 indexed subId, uint256 amount);
/**
* @notice Emitted when tokens are withdrawn from the contract
@@ -21,7 +21,7 @@ interface IVault {
* @param token Token address (use address(0) for native tokens)
* @param amount Amount of tokens withdrawn
*/
- event Withdrawn(address indexed wallet, address indexed token, uint256 amount);
+ event Withdrawn(address indexed wallet, address indexed token, uint48 indexed subId, uint256 amount);
/**
* @notice Gets the balances of multiple accounts for multiple tokens
@@ -30,7 +30,7 @@ interface IVault {
* @param token Token address to check balance for (use address(0) for native tokens)
* @return The balance of the specified token for the specified account
*/
- function getAccountBalance(address account, address token) external view returns (uint256);
+ function getAccountBalance(address account, address token, uint48 subId) external view returns (uint256);
/**
* @notice Deposits tokens into the contract
@@ -39,7 +39,7 @@ interface IVault {
* @param token Token address (use address(0) for native tokens)
* @param amount Amount of tokens to deposit
*/
- function depositToVault(address account, address token, uint256 amount) external payable;
+ function depositToVault(address account, address token, uint48 subId, uint256 amount) external payable;
/**
* @notice Withdraws tokens from the contract
@@ -48,5 +48,5 @@ interface IVault {
* @param token Token address (use address(0) for native tokens)
* @param amount Amount of tokens to withdraw
*/
- function withdrawFromVault(address account, address token, uint256 amount) external;
+ function withdrawFromVault(address account, address token, uint48 subId, uint256 amount) external;
}
diff --git a/contracts/src/interfaces/Types.sol b/contracts/src/interfaces/Types.sol
index 364411e6d..efd49c6ea 100644
--- a/contracts/src/interfaces/Types.sol
+++ b/contracts/src/interfaces/Types.sol
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.30;
+pragma solidity ^0.8.30;
// ========= Channel Types ==========
@@ -52,13 +52,11 @@ struct State {
uint64 version;
StateIntent intent;
bytes32 metadata;
-
// to be added for fees logic:
// bytes data;
Ledger homeLedger;
Ledger nonHomeLedger;
-
bytes userSig;
bytes nodeSig;
}
@@ -67,10 +65,8 @@ struct Ledger {
uint64 chainId;
address token;
uint8 decimals;
-
uint256 userAllocation; // FIXME: investigate whether naming the same thing differently in different components is good
int256 userNetFlow; // can be negative as user can withdraw funds without depositing them (e.g., on a non-home chain)
-
uint256 nodeAllocation;
int256 nodeNetFlow; // can be negative as node can withdraw user funds
}
diff --git a/contracts/src/sigValidators/ECDSAValidator.sol b/contracts/src/sigValidators/ECDSAValidator.sol
index f17ab9c51..5bee7b456 100644
--- a/contracts/src/sigValidators/ECDSAValidator.sol
+++ b/contracts/src/sigValidators/ECDSAValidator.sol
@@ -35,6 +35,9 @@ contract ECDSAValidator is ISignatureValidator {
bytes calldata signature,
address participant
) external pure returns (ValidationResult) {
+ require(channelId != bytes32(0), EmptyChannelId());
+ require(participant != address(0), InvalidSignerAddress());
+
bytes memory message = Utils.pack(channelId, signingData);
if (EcdsaSignatureUtils.validateEcdsaSigner(message, signature, participant)) {
return VALIDATION_SUCCESS;
diff --git a/contracts/src/sigValidators/SessionKeyValidator.sol b/contracts/src/sigValidators/SessionKeyValidator.sol
index 34c8288a3..0302e3cba 100644
--- a/contracts/src/sigValidators/SessionKeyValidator.sol
+++ b/contracts/src/sigValidators/SessionKeyValidator.sol
@@ -71,6 +71,9 @@ contract SessionKeyValidator is ISignatureValidator {
bytes calldata signature,
address participant
) external pure returns (ValidationResult) {
+ require(channelId != bytes32(0), EmptyChannelId());
+ require(participant != address(0), InvalidSignerAddress());
+
(SessionKeyAuthorization memory skAuth, bytes memory skSignature) =
abi.decode(signature, (SessionKeyAuthorization, bytes));
diff --git a/contracts/test/ChannelHub_Base.t.sol b/contracts/test/ChannelHub_Base.t.sol
index 7ae2cfc4b..0227c1985 100644
--- a/contracts/test/ChannelHub_Base.t.sol
+++ b/contracts/test/ChannelHub_Base.t.sol
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.30;
+pragma solidity ^0.8.30;
import {Test} from "forge-std/Test.sol";
@@ -9,8 +9,9 @@ import {TestUtils, SESSION_KEY_VALIDATOR_ID} from "./TestUtils.sol";
import {ChannelHub} from "../src/ChannelHub.sol";
import {ECDSAValidator} from "../src/sigValidators/ECDSAValidator.sol";
import {SessionKeyValidator, SessionKeyAuthorization} from "../src/sigValidators/SessionKeyValidator.sol";
-import {State, StateIntent, Ledger} from "../src/interfaces/Types.sol";
+import {ChannelStatus, State, StateIntent, Ledger, DEFAULT_SIG_VALIDATOR_ID} from "../src/interfaces/Types.sol";
import {ISignatureValidator} from "../src/interfaces/ISignatureValidator.sol";
+import {Utils} from "../src/Utils.sol";
// forge-lint: disable-next-item(unsafe-typecast)
contract ChannelHubTest_Base is Test {
@@ -22,6 +23,8 @@ contract ChannelHubTest_Base is Test {
uint256 constant ALICE_SK1_PK = 3;
uint256 constant BOB_PK = 4;
+ uint48 constant SUB_ID_0 = 0;
+
address node;
address alice;
address aliceSk1;
@@ -53,13 +56,11 @@ contract ChannelHubTest_Base is Test {
vm.startPrank(node);
token.approve(address(cHub), INITIAL_BALANCE);
- cHub.depositToVault(node, address(token), INITIAL_BALANCE);
+ cHub.depositToVault(node, address(token), SUB_ID_0, INITIAL_BALANCE);
vm.stopPrank();
// Register SessionKeyValidator for the node
- bytes memory skValidatorSig = TestUtils.buildAndSignValidatorRegistration(
- vm, SESSION_KEY_VALIDATOR_ID, address(SK_SIG_VALIDATOR), NODE_PK
- );
+ bytes memory skValidatorSig = TestUtils.buildAndSignValidatorRegistration(vm, SESSION_KEY_VALIDATOR_ID, address(SK_SIG_VALIDATOR), NODE_PK);
cHub.registerNodeValidator(node, SESSION_KEY_VALIDATOR_ID, SK_SIG_VALIDATOR, skValidatorSig);
vm.prank(alice);
@@ -69,36 +70,25 @@ contract ChannelHubTest_Base is Test {
token.approve(address(cHub), INITIAL_BALANCE);
}
- function nextState(State memory state, StateIntent intent, uint256[2] memory allocations, int256[2] memory netFlows)
- internal
- pure
- returns (State memory)
- {
- return State({
- version: state.version + 1,
- intent: intent,
- metadata: state.metadata,
- homeLedger: Ledger({
- chainId: state.homeLedger.chainId,
- token: state.homeLedger.token,
- decimals: state.homeLedger.decimals,
- userAllocation: allocations[0],
- userNetFlow: netFlows[0],
- nodeAllocation: allocations[1],
- nodeNetFlow: netFlows[1]
- }),
- nonHomeLedger: Ledger({
- chainId: 0,
- token: address(0),
- decimals: 0,
- userAllocation: 0,
- userNetFlow: 0,
- nodeAllocation: 0,
- nodeNetFlow: 0
- }),
- userSig: "",
- nodeSig: ""
- });
+ function nextState(State memory state, StateIntent intent, uint256[2] memory allocations, int256[2] memory netFlows) internal pure returns (State memory) {
+ return
+ State({
+ version: state.version + 1,
+ intent: intent,
+ metadata: state.metadata,
+ homeLedger: Ledger({
+ chainId: state.homeLedger.chainId,
+ token: state.homeLedger.token,
+ decimals: state.homeLedger.decimals,
+ userAllocation: allocations[0],
+ userNetFlow: netFlows[0],
+ nodeAllocation: allocations[1],
+ nodeNetFlow: netFlows[1]
+ }),
+ nonHomeLedger: Ledger({chainId: 0, token: address(0), decimals: 0, userAllocation: 0, userNetFlow: 0, nodeAllocation: 0, nodeNetFlow: 0}),
+ userSig: "",
+ nodeSig: ""
+ });
}
function nextState(
@@ -111,31 +101,32 @@ contract ChannelHubTest_Base is Test {
uint256[2] memory nonHomeAllocations,
int256[2] memory nonHomeNetFlows
) internal pure returns (State memory) {
- return State({
- version: state.version + 1,
- intent: intent,
- metadata: state.metadata,
- homeLedger: Ledger({
- chainId: state.homeLedger.chainId,
- token: state.homeLedger.token,
- decimals: state.homeLedger.decimals,
- userAllocation: allocations[0],
- userNetFlow: netFlows[0],
- nodeAllocation: allocations[1],
- nodeNetFlow: netFlows[1]
- }),
- nonHomeLedger: Ledger({
- chainId: nonHomeChainId,
- token: nonHomeChainToken,
- decimals: 18,
- userAllocation: nonHomeAllocations[0],
- userNetFlow: nonHomeNetFlows[0],
- nodeAllocation: nonHomeAllocations[1],
- nodeNetFlow: nonHomeNetFlows[1]
- }),
- userSig: "",
- nodeSig: ""
- });
+ return
+ State({
+ version: state.version + 1,
+ intent: intent,
+ metadata: state.metadata,
+ homeLedger: Ledger({
+ chainId: state.homeLedger.chainId,
+ token: state.homeLedger.token,
+ decimals: state.homeLedger.decimals,
+ userAllocation: allocations[0],
+ userNetFlow: netFlows[0],
+ nodeAllocation: allocations[1],
+ nodeNetFlow: netFlows[1]
+ }),
+ nonHomeLedger: Ledger({
+ chainId: nonHomeChainId,
+ token: nonHomeChainToken,
+ decimals: 18,
+ userAllocation: nonHomeAllocations[0],
+ userNetFlow: nonHomeNetFlows[0],
+ nodeAllocation: nonHomeAllocations[1],
+ nodeNetFlow: nonHomeNetFlows[1]
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
}
function nextState(
@@ -149,38 +140,35 @@ contract ChannelHubTest_Base is Test {
uint256[2] memory nonHomeAllocations,
int256[2] memory nonHomeNetFlows
) internal pure returns (State memory) {
- return State({
- version: state.version + 1,
- intent: intent,
- metadata: state.metadata,
- homeLedger: Ledger({
- chainId: state.homeLedger.chainId,
- token: state.homeLedger.token,
- decimals: state.homeLedger.decimals,
- userAllocation: allocations[0],
- userNetFlow: netFlows[0],
- nodeAllocation: allocations[1],
- nodeNetFlow: netFlows[1]
- }),
- nonHomeLedger: Ledger({
- chainId: nonHomeChainId,
- token: nonHomeChainToken,
- decimals: nonHomeDecimals,
- userAllocation: nonHomeAllocations[0],
- userNetFlow: nonHomeNetFlows[0],
- nodeAllocation: nonHomeAllocations[1],
- nodeNetFlow: nonHomeNetFlows[1]
- }),
- userSig: "",
- nodeSig: ""
- });
+ return
+ State({
+ version: state.version + 1,
+ intent: intent,
+ metadata: state.metadata,
+ homeLedger: Ledger({
+ chainId: state.homeLedger.chainId,
+ token: state.homeLedger.token,
+ decimals: state.homeLedger.decimals,
+ userAllocation: allocations[0],
+ userNetFlow: netFlows[0],
+ nodeAllocation: allocations[1],
+ nodeNetFlow: netFlows[1]
+ }),
+ nonHomeLedger: Ledger({
+ chainId: nonHomeChainId,
+ token: nonHomeChainToken,
+ decimals: nonHomeDecimals,
+ userAllocation: nonHomeAllocations[0],
+ userNetFlow: nonHomeNetFlows[0],
+ nodeAllocation: nonHomeAllocations[1],
+ nodeNetFlow: nonHomeNetFlows[1]
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
}
- function mutualSignStateBothWithEcdsaValidator(State memory state, bytes32 channelId, uint256 userPk)
- internal
- pure
- returns (State memory)
- {
+ function mutualSignStateBothWithEcdsaValidator(State memory state, bytes32 channelId, uint256 userPk) internal pure returns (State memory) {
state.userSig = TestUtils.signStateEip191WithEcdsaValidator(vm, channelId, state, userPk);
state.nodeSig = TestUtils.signStateEip191WithEcdsaValidator(vm, channelId, state, NODE_PK);
return state;
@@ -197,32 +185,36 @@ contract ChannelHubTest_Base is Test {
return state;
}
- function verifyChannelState(
+ function signChallengeEip191WithEcdsaValidator(bytes32 channelId_, State memory state, uint256 privateKey) internal pure returns (bytes memory) {
+ bytes memory signingData = Utils.toSigningData(state);
+ bytes memory challengerSigningData = abi.encodePacked(signingData, "challenge");
+ bytes memory message = Utils.pack(channelId_, challengerSigningData);
+ bytes memory signature = TestUtils.signEip191(vm, privateKey, message);
+ return abi.encodePacked(DEFAULT_SIG_VALIDATOR_ID, signature);
+ }
+
+ function verifyChannelData(
bytes32 channelId,
- uint256 expectedUserAllocation,
- int256 expectedUserNetFlow,
- uint256 expectedNodeAllocation,
- int256 expectedNodeNetFlow,
+ ChannelStatus expectedStatus,
+ uint64 expectedVersion,
+ uint256 expectedChallengeExpiry,
string memory description
) internal view {
- (,, State memory latestState,,) = cHub.getChannelData(channelId);
- assertEq(
- latestState.homeLedger.userAllocation,
- expectedUserAllocation,
- string.concat("User allocation ", description)
- );
- assertEq(latestState.homeLedger.userNetFlow, expectedUserNetFlow, string.concat("User net flow ", description));
- assertEq(
- latestState.homeLedger.nodeAllocation,
- expectedNodeAllocation,
- string.concat("Node allocation ", description)
- );
- assertEq(latestState.homeLedger.nodeNetFlow, expectedNodeNetFlow, string.concat("Node net flow ", description));
-
- uint256 nodeBalance = cHub.getAccountBalance(node, address(token));
- uint256 expectedNodeBalance = expectedNodeNetFlow < 0
- ? INITIAL_BALANCE + uint256(-expectedNodeNetFlow)
- : INITIAL_BALANCE - uint256(expectedNodeNetFlow);
- assertEq(nodeBalance, expectedNodeBalance, string.concat("Node vault balance ", description));
+ (ChannelStatus status, , State memory latestState, uint256 challengeExpiry, ) = cHub.getChannelData(channelId);
+ assertEq(uint8(status), uint8(expectedStatus), string.concat(description, ": Channel status: "));
+ assertEq(latestState.version, expectedVersion, string.concat(description, ": Channel version: "));
+ assertEq(challengeExpiry, expectedChallengeExpiry, string.concat(description, ": Challenge expiry: "));
+ }
+
+ function verifyChannelState(bytes32 channelId, uint256[2] memory allocations, int256[2] memory netFlows, string memory description) internal view {
+ (, , State memory latestState, , ) = cHub.getChannelData(channelId);
+ assertEq(latestState.homeLedger.userAllocation, allocations[0], string.concat(description, ": User allocation: "));
+ assertEq(latestState.homeLedger.userNetFlow, netFlows[0], string.concat(description, ": User net flow: "));
+ assertEq(latestState.homeLedger.nodeAllocation, allocations[1], string.concat(description, ": Node allocation: "));
+ assertEq(latestState.homeLedger.nodeNetFlow, netFlows[1], string.concat(description, ": Node net flow: "));
+
+ uint256 nodeBalance = cHub.getAccountBalance(node, address(token), SUB_ID_0);
+ uint256 expectedNodeBalance = netFlows[1] < 0 ? INITIAL_BALANCE + uint256(-netFlows[1]) : INITIAL_BALANCE - uint256(netFlows[1]);
+ assertEq(nodeBalance, expectedNodeBalance, string.concat(description, ": Node balance: "));
}
}
diff --git a/contracts/test/ChannelHub_challenge/ChannelHub_Challenge_Base.t.sol b/contracts/test/ChannelHub_challenge/ChannelHub_Challenge_Base.t.sol
new file mode 100644
index 000000000..037a9d3ca
--- /dev/null
+++ b/contracts/test/ChannelHub_challenge/ChannelHub_Challenge_Base.t.sol
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.30;
+
+import {ChannelHubTest_Base} from "../ChannelHub_Base.t.sol";
+
+import {Utils} from "../../src/Utils.sol";
+import {State, ChannelDefinition, StateIntent, Ledger} from "../../src/interfaces/Types.sol";
+
+/**
+ * @dev Base contract for challenge tests with common helper functions.
+ */
+abstract contract ChannelHubTest_Challenge_Base is ChannelHubTest_Base {
+ ChannelDefinition internal def;
+ bytes32 internal channelId;
+ State internal initState;
+
+ uint64 constant NON_HOME_CHAIN_ID = 42;
+ address constant NON_HOME_TOKEN = address(0x42);
+
+ function setUp() public virtual override {
+ super.setUp();
+
+ def = ChannelDefinition({
+ challengeDuration: CHALLENGE_DURATION,
+ user: alice,
+ node: node,
+ nonce: NONCE,
+ approvedSignatureValidators: 0,
+ metadata: bytes32(0)
+ });
+
+ channelId = Utils.getChannelId(def, CHANNEL_HUB_VERSION);
+ }
+
+ function createChannelWithDeposit() internal {
+ initState = State({
+ version: 0,
+ intent: StateIntent.DEPOSIT,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 1000,
+ userNetFlow: 1000,
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ nonHomeLedger: Ledger({
+ chainId: 0,
+ token: address(0),
+ decimals: 0,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ initState = mutualSignStateBothWithEcdsaValidator(initState, channelId, ALICE_PK);
+
+ vm.prank(alice);
+ cHub.createChannel(def, initState);
+ }
+}
diff --git a/contracts/test/ChannelHub_challenge/ChannelHub_challengeHomeChain.t.sol b/contracts/test/ChannelHub_challenge/ChannelHub_challengeHomeChain.t.sol
new file mode 100644
index 000000000..7e999f05d
--- /dev/null
+++ b/contracts/test/ChannelHub_challenge/ChannelHub_challengeHomeChain.t.sol
@@ -0,0 +1,790 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.30;
+
+import { ChannelHubTest_Challenge_Base } from "./ChannelHub_Challenge_Base.t.sol";
+
+// forge-lint: disable-start(unsafe-typecast)
+
+import { Utils } from "../../src/Utils.sol";
+import { State, ChannelDefinition, StateIntent, Ledger, ChannelStatus, ParticipantIndex } from "../../src/interfaces/Types.sol";
+import { ChannelHub } from "../../src/ChannelHub.sol";
+import { ChannelEngine } from "../../src/ChannelEngine.sol";
+
+/*
+ * @dev This file uses integration / blackbox testing through ChannelHub to verify
+ * critical end-to-end challenge flows (signature validation, fund movements, storage updates, events).
+ * Complex state machine logic and edge cases are tested exhaustively in dedicated engine unit tests
+ * (ChannelEngine.t.sol, EscrowDepositEngine.t.sol, EscrowWithdrawalEngine.t.sol) for faster execution
+ * and better isolation.
+ */
+contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Challenge_Base {
+ /*
+ Test cases:
+ - a channel can be challenged with a newer state, which is enforced during challenge
+ - a channel can be challenged with existing state, which is NOT enforced the second time during challenge
+ - challenge is finalized (funds can be withdrawn) after `challengeExpireAt` time expires
+ - challenged "operating" state can be resolved with a newer state until `challengeExpireAt` time has NOT passed
+ - challenged state can NOT be resolved after `challengeExpireAt` time has passed
+ - a channel can NOT be challenged again during a challenge
+ - a channel can NOT be challenged with an earlier state
+ - a non-yet-on-chain channel can NOT be challenged
+ */
+
+ function setUp() public override {
+ super.setUp();
+ createChannelWithDeposit();
+ }
+
+ function test_challengeWithNewerState_enforcesState() public {
+ // Off-chain: user transfers 100 to node
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ // Off-chain: user transfers another 50 to node
+ State memory stateV2 = nextState(stateV1, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]);
+ stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK);
+
+ // Node challenges with newer state V2, which should be enforced during challenge
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV2, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV2, challengerSig, ParticipantIndex.NODE);
+
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, 2, block.timestamp + CHALLENGE_DURATION, "State V2 should be enforced during challenge");
+ verifyChannelState(channelId, [uint256(850), uint256(0)], [int256(1000), int256(-150)], "State V2 should be enforced during challenge");
+ }
+
+ function test_challengeWithExistingState_notEnforcedAgain() public {
+ // Checkpoint a new state
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, stateV1);
+
+ // Verify state V1 is on-chain
+ (, , State memory latestStateBefore, , ) = cHub.getChannelData(channelId);
+ assertEq(latestStateBefore.version, 1, "State version should be 1 before challenge");
+
+ // Node challenges with the same state V1 (already on-chain)
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE);
+
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, 1, block.timestamp + CHALLENGE_DURATION, "State V1 should be enforced during challenge");
+ verifyChannelState(channelId, [uint256(900), uint256(0)], [int256(1000), int256(-100)], "State V1 should be enforced during challenge");
+ }
+
+ function test_challengeFinalization_afterTimeout() public {
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ // Challenge with current state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE);
+
+ vm.warp(block.timestamp + CHALLENGE_DURATION + 1);
+
+ uint256 aliceBalanceBefore = token.balanceOf(alice);
+ uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
+
+ // Finalize challenge by closing the channel (unilateral closure)
+ // When doing unilateral closure after timeout, any state works
+ vm.prank(alice);
+ cHub.closeChannel(channelId, initState);
+
+ // Verify channel is CLOSED and funds were distributed according to last enforced state (V1)
+ verifyChannelData(channelId, ChannelStatus.CLOSED, 1, 0, "Channel should be CLOSED after challenge finalization");
+
+ uint256 aliceBalanceAfter = token.balanceOf(alice);
+ uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token), SUB_ID_0);
+
+ assertEq(aliceBalanceAfter, aliceBalanceBefore + 900, "Alice should receive her allocation");
+ // Node balance should remain unchanged because:
+ // 1. The node already received its 100 when the challenge was processed (nodeNetFlow -100 released funds)
+ // 2. During unilateral closure, node gets nodeAllocation (0)
+ assertEq(nodeBalanceAfter, nodeBalanceBefore, "Node balance should remain unchanged (already received net flow during challenge)");
+ }
+
+ function test_resolveChallenge_withNewerState_beforeTimeout() public {
+ // State V1: user transfers 100
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ // Challenge with stateV1
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE);
+
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, 1, block.timestamp + CHALLENGE_DURATION, "Channel should be DISPUTED after challenge");
+
+ // State V2: user transfers another 50 (newer state to resolve challenge)
+ State memory stateV2 = nextState(stateV1, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]);
+ stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK);
+
+ // Resolve challenge by checkpointing newer state (before timeout)
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, stateV2);
+
+ verifyChannelData(channelId, ChannelStatus.OPERATING, 2, 0, "Channel should be OPERATING after resolving challenge with newer state");
+ verifyChannelState(
+ channelId,
+ [uint256(850), uint256(0)],
+ [int256(1000), int256(-150)],
+ "State V2 should be enforced after resolving challenge with newer state"
+ );
+ }
+
+ function test_revert_resolveChallenge_withOlderState_beforeTimeout() public {
+ // State V1: user transfers 100
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ // State V2: user receives 50 back
+ State memory stateV2 = nextState(stateV1, StateIntent.OPERATE, [uint256(950), uint256(0)], [int256(1000), int256(-50)]);
+ stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK);
+
+ // Challenge with stateV2
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV2, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV2, challengerSig, ParticipantIndex.NODE);
+
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, 2, block.timestamp + CHALLENGE_DURATION, "Channel should be DISPUTED after challenge");
+
+ // Try to resolve with older state V1 (should fail)
+ vm.expectRevert(ChannelEngine.IncorrectStateVersion.selector);
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, stateV1);
+ }
+
+ function test_revert_resolveChallenge_withNewerState_afterTimeout() public {
+ // State V1
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ // Challenge
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV1, challengerSig, ParticipantIndex.NODE);
+
+ vm.warp(block.timestamp + CHALLENGE_DURATION + 1);
+
+ // State V2: user transfers another 50 (newer state to resolve challenge)
+ State memory stateV2 = nextState(stateV1, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]);
+ stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK);
+
+ // Cannot resolve challenge after timeout - must close channel instead
+ vm.expectRevert(ChannelEngine.ChallengeExpired.selector);
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, stateV2);
+ }
+
+ function test_revert_challengeAlreadyChallengedChannel() public {
+ // First challenge
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, 0, block.timestamp + CHALLENGE_DURATION, "Channel should be DISPUTED after first challenge");
+
+ // Try to challenge again (should fail)
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(850), uint256(0)], [int256(1000), int256(-150)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ bytes memory challengerSig2 = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK);
+
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.IncorrectChannelStatus.selector);
+ cHub.challengeChannel(channelId, stateV1, challengerSig2, ParticipantIndex.NODE);
+ }
+
+ function test_revert_challengeWithOlderState() public {
+ // State V1
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ // Checkpoint V1
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, stateV1);
+
+ // Try to challenge with older state (initial) (should fail)
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK);
+
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.ChallengerVersionTooLow.selector);
+ cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE);
+ }
+
+ function test_revert_challengeNonExistingChannel() public {
+ ChannelDefinition memory newDef = ChannelDefinition({
+ challengeDuration: CHALLENGE_DURATION,
+ user: alice,
+ node: node,
+ nonce: NONCE + 42,
+ approvedSignatureValidators: 0,
+ metadata: bytes32("42")
+ });
+ bytes32 newChannelId = Utils.getChannelId(newDef, CHANNEL_HUB_VERSION);
+
+ // Off-chain: user transfers 100 to node
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, newChannelId, ALICE_PK);
+
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(newChannelId, stateV1, NODE_PK);
+
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.IncorrectChannelStatus.selector);
+ cHub.challengeChannel(newChannelId, stateV1, challengerSig, ParticipantIndex.NODE);
+ }
+}
+
+contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Challenge_Base {
+ /*
+ Test cases:
+ - a channel can be challenged with a newer state, which is enforced during challenge:
+ (new: InitiateEscrowDeposit, FinalizeEscrowDeposit)
+ - a channel can be challenged with existing state, which is NOT enforced the second time during challenge:
+ (existing: InitiateEscrowDeposit, FinalizeEscrowDeposit)
+ - a challenged channel can be resolved with "InitiateEscrowDeposit" / "FinalizeEscrowDeposit" state until `challengeExpireAt` time has NOT passed
+ */
+
+ bytes32 escrowId;
+
+ uint64 initiateEscrowDepositVersion = 1;
+ State initiateEscrowDepositState;
+ uint64 finalizeEscrowDepositVersion = 2;
+ State finalizeEscrowDepositState;
+
+ function setUp() public override {
+ super.setUp();
+ createChannelWithDeposit();
+
+ initiateEscrowDepositState = nextState(
+ initState,
+ StateIntent.INITIATE_ESCROW_DEPOSIT,
+ [uint256(1000), uint256(500)],
+ [int256(1000), int256(500)],
+ NON_HOME_CHAIN_ID,
+ NON_HOME_TOKEN,
+ [uint256(500), uint256(0)],
+ [int256(500), int256(0)]
+ );
+ initiateEscrowDepositState = mutualSignStateBothWithEcdsaValidator(initiateEscrowDepositState, channelId, ALICE_PK);
+
+ escrowId = Utils.getEscrowId(channelId, initiateEscrowDepositVersion);
+
+ finalizeEscrowDepositState = nextState(
+ initiateEscrowDepositState,
+ StateIntent.FINALIZE_ESCROW_DEPOSIT,
+ [uint256(1500), uint256(0)],
+ [int256(1000), int256(500)],
+ NON_HOME_CHAIN_ID,
+ NON_HOME_TOKEN,
+ [uint256(0), uint256(0)],
+ [int256(500), int256(-500)]
+ );
+ finalizeEscrowDepositState = mutualSignStateBothWithEcdsaValidator(finalizeEscrowDepositState, channelId, ALICE_PK);
+ }
+
+ function test_challenge_initiateEscrowDeposit_asNew() public {
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and initiateEscrowDepositState was enforced
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ initiateEscrowDepositVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "InitiateEscrowDepositState should be enforced"
+ );
+ verifyChannelState(channelId, [uint256(1000), uint256(500)], [int256(1000), int256(500)], "InitiateEscrowDepositState should be enforced");
+ }
+
+ function test_challenge_initiateEscrowDeposit_asExisting() public {
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ // Challenge with already enforced initiateEscrowDepositState state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify state is still initiateEscrowDepositState
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ initiateEscrowDepositVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "State should not be re-enforced"
+ );
+ verifyChannelState(channelId, [uint256(1000), uint256(500)], [int256(1000), int256(500)], "State should not be re-enforced");
+ }
+
+ function test_challenge_initiateEscrowDeposit_resolve() public {
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE);
+
+ // Resolve challenge with newer initiateEscrowDepositState state (before timeout)
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ // Verify challenge was resolved
+ verifyChannelData(channelId, ChannelStatus.OPERATING, initiateEscrowDepositVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(1000), uint256(500)], [int256(1000), int256(500)], "initiateEscrowDepositState should be enforced");
+ }
+
+ function test_challenge_finalizeEscrowDeposit_asNew() public {
+ // First enforce INITIATE_ESCROW_DEPOSIT on-chain (required for FINALIZE to be valid)
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ // Now challenge with FINALIZE_ESCROW_DEPOSIT
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, finalizeEscrowDepositState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and finalizeEscrowDepositState was enforced
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ finalizeEscrowDepositVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "FinalizeEscrowDepositState should be enforced"
+ );
+ verifyChannelState(channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "finalizeEscrowDepositState should be enforced");
+ }
+
+ function test_challenge_finalizeEscrowDeposit_asExisting() public {
+ // First enforce INITIATE_ESCROW_DEPOSIT on-chain
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ // Then enforce FINALIZE_ESCROW_DEPOSIT on-chain
+ vm.prank(alice);
+ cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState);
+
+ // Challenge with already enforced finalizeEscrowDepositState state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, finalizeEscrowDepositState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify state is still finalizeEscrowDepositState
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ finalizeEscrowDepositVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "State should not be re-enforced"
+ );
+ verifyChannelState(channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "State should not be re-enforced");
+ }
+
+ function test_challenge_finalizeEscrowDeposit_resolve() public {
+ // First enforce INITIATE_ESCROW_DEPOSIT on-chain
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ // Challenge with older initiate state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE);
+
+ // Resolve challenge with newer finalizeEscrowDepositState state (before timeout)
+ vm.prank(alice);
+ cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState);
+
+ // Verify challenge was resolved
+ verifyChannelData(channelId, ChannelStatus.OPERATING, finalizeEscrowDepositVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "finalizeEscrowDepositState should be enforced");
+ }
+
+ function test_finalizeEscrowDeposit_resolve_newlyChallenged_initializeEscrowDeposit() public {
+ // Challenge with INITIATE_ESCROW_DEPOSIT state (without enforcing it on-chain first)
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE);
+
+ // Resolve challenge with finalizeEscrowDepositState state (before timeout)
+ vm.prank(alice);
+ cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState);
+
+ // Verify challenge was resolved
+ verifyChannelData(channelId, ChannelStatus.OPERATING, finalizeEscrowDepositVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "finalizeEscrowDepositState should be enforced");
+ }
+
+ function test_revert_onChallengeEscrowDeposit() public {
+ // First enforce INITIATE_ESCROW_DEPOSIT on-chain
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ // Challenge with INITIATE_ESCROW_DEPOSIT state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.NoChannelIdFoundForEscrow.selector);
+ cHub.challengeEscrowDeposit(escrowId, challengerSig, ParticipantIndex.NODE);
+ }
+}
+
+contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_Challenge_Base {
+ /*
+ Test cases:
+ - a channel can be challenged with a newer state, which is enforced during challenge:
+ (new: InitiateEscrowWithdrawal, FinalizeEscrowWithdrawal)
+ - a channel can be challenged with existing state, which is NOT enforced the second time during challenge:
+ (existing: InitiateEscrowWithdrawal, FinalizeEscrowWithdrawal)
+ - a challenged channel can be resolved with "InitiateEscrowWithdrawal" / "FinalizeEscrowWithdrawal" state until `challengeExpireAt` time has NOT passed
+ */
+
+ bytes32 escrowId;
+
+ uint64 initiateEscrowWithdrawalVersion = 1;
+ State initiateEscrowWithdrawalState;
+ uint64 finalizeEscrowWithdrawalVersion = 2;
+ State finalizeEscrowWithdrawalState;
+
+ function setUp() public override {
+ super.setUp();
+ createChannelWithDeposit();
+
+ initiateEscrowWithdrawalState = nextState(
+ initState,
+ StateIntent.INITIATE_ESCROW_WITHDRAWAL,
+ [uint256(1000), uint256(0)],
+ [int256(1000), int256(0)],
+ NON_HOME_CHAIN_ID,
+ NON_HOME_TOKEN,
+ [uint256(0), uint256(300)],
+ [int256(0), int256(300)]
+ );
+ initiateEscrowWithdrawalState = mutualSignStateBothWithEcdsaValidator(initiateEscrowWithdrawalState, channelId, ALICE_PK);
+
+ escrowId = Utils.getEscrowId(channelId, initiateEscrowWithdrawalVersion);
+
+ finalizeEscrowWithdrawalState = nextState(
+ initiateEscrowWithdrawalState,
+ StateIntent.FINALIZE_ESCROW_WITHDRAWAL,
+ [uint256(700), uint256(0)],
+ [int256(1000), int256(-300)],
+ NON_HOME_CHAIN_ID,
+ NON_HOME_TOKEN,
+ [uint256(0), uint256(0)],
+ [int256(-300), int256(300)]
+ );
+ finalizeEscrowWithdrawalState = mutualSignStateBothWithEcdsaValidator(finalizeEscrowWithdrawalState, channelId, ALICE_PK);
+ }
+
+ function test_challenge_initiateEscrowWithdrawal_asNew() public {
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and initiateEscrowWithdrawalState was enforced
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ initiateEscrowWithdrawalVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "InitiateEscrowWithdrawalState should be enforced"
+ );
+ verifyChannelState(channelId, [uint256(1000), uint256(0)], [int256(1000), int256(0)], "InitiateEscrowWithdrawalState should be enforced");
+ }
+
+ function test_challenge_initiateEscrowWithdrawal_asExisting() public {
+ vm.prank(alice);
+ cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState);
+
+ // Challenge with already enforced initiateEscrowWithdrawalState state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify state is still initiateEscrowWithdrawalState
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ initiateEscrowWithdrawalVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "State should not be re-enforced"
+ );
+ verifyChannelState(channelId, [uint256(1000), uint256(0)], [int256(1000), int256(0)], "State should not be re-enforced");
+ }
+
+ function test_challenge_initiateEscrowWithdrawal_resolve() public {
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE);
+
+ // Resolve challenge with newer initiateEscrowWithdrawalState state (before timeout)
+ vm.prank(alice);
+ cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState);
+
+ // Verify challenge was resolved
+ verifyChannelData(channelId, ChannelStatus.OPERATING, initiateEscrowWithdrawalVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(1000), uint256(0)], [int256(1000), int256(0)], "initiateEscrowWithdrawalState should be enforced");
+ }
+
+ function test_challenge_finalizeEscrowWithdrawal_asNew() public {
+ // INITIATE_ESCROW_WITHDRAWAL is NOT required to be enforced first on-chain
+
+ // Challenge with FINALIZE_ESCROW_WITHDRAWAL
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, finalizeEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and finalizeEscrowWithdrawalState was enforced
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ finalizeEscrowWithdrawalVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "FinalizeEscrowWithdrawalState should be enforced"
+ );
+ verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "finalizeEscrowWithdrawalState should be enforced");
+ }
+
+ function test_challenge_finalizeEscrowWithdrawal_asExisting() public {
+ // INITIATE_ESCROW_WITHDRAWAL is NOT required to be enforced first on-chain
+
+ // Enforce FINALIZE_ESCROW_WITHDRAWAL on-chain
+ vm.prank(alice);
+ cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState);
+
+ // Challenge with already enforced finalizeEscrowWithdrawalState state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, finalizeEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify state is still finalizeEscrowWithdrawalState
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ finalizeEscrowWithdrawalVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "State should not be re-enforced"
+ );
+ verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "State should not be re-enforced");
+ }
+
+ function test_challenge_finalizeEscrowWithdrawal_resolve() public {
+ // INITIATE_ESCROW_WITHDRAWAL is NOT required to be enforced first on-chain
+
+ // Challenge with older initiate state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initState, challengerSig, ParticipantIndex.NODE);
+
+ // Resolve challenge with newer finalizeEscrowWithdrawalState state (before timeout)
+ vm.prank(alice);
+ cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState);
+
+ // Verify challenge was resolved
+ verifyChannelData(channelId, ChannelStatus.OPERATING, finalizeEscrowWithdrawalVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "finalizeEscrowWithdrawalState should be enforced");
+ }
+
+ function test_finalizeEscrowWithdrawal_resolve_newlyChallenged_initializeEscrowWithdrawal() public {
+ // Challenge with INITIATE_ESCROW_WITHDRAWAL state (without enforcing it on-chain first)
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE);
+
+ // Resolve challenge with finalizeEscrowWithdrawalState state (before timeout)
+ vm.prank(alice);
+ cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState);
+
+ // Verify challenge was resolved
+ verifyChannelData(channelId, ChannelStatus.OPERATING, finalizeEscrowWithdrawalVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "finalizeEscrowWithdrawalState should be enforced");
+ }
+
+ function test_revert_onChallengeEscrowWithdrawal() public {
+ // First enforce INITIATE_ESCROW_WITHDRAWAL on-chain
+ vm.prank(alice);
+ cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState);
+
+ // Challenge with INITIATE_ESCROW_WITHDRAWAL state
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.NoChannelIdFoundForEscrow.selector);
+ cHub.challengeEscrowWithdrawal(escrowId, challengerSig, ParticipantIndex.NODE);
+ }
+}
+
+contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Challenge_Base {
+ /*
+ Test cases:
+ - a channel in Operate status can be challenged with initiated migration state
+ - a channel challenged with "InitiateMigration" state can be checkpointed calling "finalizeMigration" (-> MigratedOut status)
+ - a channel challenged with "InitiateMigration" state can be resolved with "operation" state
+ (although this should not happen in practice since the node should finalize migration instead of resolving with an older state, but just to be safe)
+ - a channel can NOT be challenged when in MIGRATED_OUT status
+ - a channel can NOT be challenged in Operating status with finalize migration state (use `finalizeMigration` function instead)
+ */
+
+ uint64 initiateMigrationVersion = 1;
+ State initiateMigrationState;
+ uint64 finalizeMigrationVersion = 2;
+ State finalizeMigrationState;
+ uint64 operateAfterMigrationInitVersion = 2;
+ State operateAfterMigrationInitState;
+
+ // New channel for testing NEW home chain behavior
+ ChannelDefinition newHomeDef;
+ bytes32 newHomeChannelId;
+ State newHomeInitiateMigrationState;
+ uint64 newHomeOperateVersion = 3;
+ State newHomeOperateState;
+
+ function setUp() public override {
+ super.setUp();
+ createChannelWithDeposit();
+
+ // INITIATE_MIGRATION state:
+ initiateMigrationState = nextState(
+ initState,
+ StateIntent.INITIATE_MIGRATION,
+ [uint256(700), uint256(0)],
+ [int256(1000), int256(-300)],
+ NON_HOME_CHAIN_ID,
+ NON_HOME_TOKEN,
+ [uint256(0), uint256(700)], // Node locks user allocation on new home
+ [int256(0), int256(700)]
+ );
+ initiateMigrationState = mutualSignStateBothWithEcdsaValidator(initiateMigrationState, channelId, ALICE_PK);
+
+ // FINALIZE_MIGRATION state: Allocations zero out on old home, user receives allocation on new home
+ finalizeMigrationState = nextState(
+ initiateMigrationState,
+ StateIntent.FINALIZE_MIGRATION,
+ [uint256(0), uint256(0)], // Old home: allocations zero out
+ [int256(1000), int256(-1000)], // Old home: net flows balance
+ NON_HOME_CHAIN_ID,
+ NON_HOME_TOKEN,
+ [uint256(700), uint256(0)], // New home: user receives allocation
+ [int256(0), int256(700)]
+ );
+ // Swap home and non-home states as per migration protocol
+ Ledger memory temp = finalizeMigrationState.homeLedger;
+ finalizeMigrationState.homeLedger = finalizeMigrationState.nonHomeLedger;
+ finalizeMigrationState.nonHomeLedger = temp;
+ finalizeMigrationState = mutualSignStateBothWithEcdsaValidator(finalizeMigrationState, channelId, ALICE_PK);
+
+ // OPERATE state after migration initiation (for resolving challenge)
+ operateAfterMigrationInitState = nextState(initiateMigrationState, StateIntent.OPERATE, [uint256(650), uint256(0)], [int256(1000), int256(-350)]);
+ operateAfterMigrationInitState = mutualSignStateBothWithEcdsaValidator(operateAfterMigrationInitState, channelId, ALICE_PK);
+ }
+
+ function test_challenge_initiateMigration_fromOperating() public {
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateMigrationState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and initiateMigrationState was enforced
+ verifyChannelData(
+ channelId,
+ ChannelStatus.DISPUTED,
+ initiateMigrationVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "InitiateMigrationState should be enforced"
+ );
+ verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "InitiateMigrationState should be enforced");
+ }
+
+ function test_challenge_initiateMigration_resolve_withFinalizeMigration() public {
+ // Challenge with INITIATE_MIGRATION
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateMigrationState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE);
+
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, initiateMigrationVersion, block.timestamp + CHALLENGE_DURATION, "Channel should be DISPUTED");
+
+ // Resolve challenge with FINALIZE_MIGRATION (before timeout)
+ vm.prank(alice);
+ cHub.finalizeMigration(channelId, finalizeMigrationState);
+
+ // Verify channel is MIGRATED_OUT and initiateMigrationState was enforced
+ verifyChannelData(channelId, ChannelStatus.MIGRATED_OUT, finalizeMigrationVersion, 0, "finalizeMigration should resolve the challenge");
+ }
+
+ function test_challenge_initiateMigration_resolve_withOperate() public {
+ // Challenge with INITIATE_MIGRATION
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateMigrationState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE);
+
+ verifyChannelData(channelId, ChannelStatus.DISPUTED, initiateMigrationVersion, block.timestamp + CHALLENGE_DURATION, "Channel should be DISPUTED");
+
+ // Resolve challenge with newer OPERATE state (before timeout)
+ // This is technically possible but shouldn't happen in practice as participants should NOT sign OPERATE state as direct successor of INITIATE_MIGRATION
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, operateAfterMigrationInitState);
+
+ // Verify channel is back to OPERATING
+ verifyChannelData(channelId, ChannelStatus.OPERATING, operateAfterMigrationInitVersion, 0, "Challenge should be resolved");
+ verifyChannelState(channelId, [uint256(650), uint256(0)], [int256(1000), int256(-350)], "operateAfterMigrationInitState should be enforced");
+ }
+
+ function test_revert_challenge_migratedOut() public {
+ // First initiate migration
+ vm.prank(alice);
+ cHub.initiateMigration(def, initiateMigrationState);
+
+ // Then finalize migration to put channel in MIGRATED_OUT status
+ vm.prank(alice);
+ cHub.finalizeMigration(channelId, finalizeMigrationState);
+
+ // Verify channel is in MIGRATED_OUT status
+ verifyChannelData(channelId, ChannelStatus.MIGRATED_OUT, finalizeMigrationVersion, 0, "Channel should be MIGRATED_OUT");
+
+ // Try to challenge channel in MIGRATED_OUT status (should fail)
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeMigrationState, NODE_PK);
+
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.IncorrectChannelStatus.selector);
+ cHub.challengeChannel(channelId, finalizeMigrationState, challengerSig, ParticipantIndex.NODE);
+ }
+
+ function test_revert_challenge_operating_withFinalizeMigration() public {
+ // Channel is in OPERATING status
+ // Try to challenge with FINALIZE_MIGRATION without INITIATE_MIGRATION first (should fail)
+ bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeMigrationState, NODE_PK);
+
+ vm.prank(node);
+ // NOTE: IncorrectHomeChainId check happens before IncorrectPreviousStateIntent check
+ // finalizeMigrationState has swapped ledgers, so homeLedger.chainId != block.chainid
+ vm.expectRevert(ChannelEngine.IncorrectHomeChainId.selector);
+ cHub.challengeChannel(channelId, finalizeMigrationState, challengerSig, ParticipantIndex.NODE);
+ }
+}
+// forge-lint: disable-end(unsafe-typecast)
diff --git a/contracts/test/ChannelHub_challenge/ChannelHub_challengeNonHomeChain.t.sol b/contracts/test/ChannelHub_challenge/ChannelHub_challengeNonHomeChain.t.sol
new file mode 100644
index 000000000..1c1a15e96
--- /dev/null
+++ b/contracts/test/ChannelHub_challenge/ChannelHub_challengeNonHomeChain.t.sol
@@ -0,0 +1,526 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.30;
+
+import {ChannelHubTest_Challenge_Base} from "./ChannelHub_Challenge_Base.t.sol";
+
+// forge-lint: disable-start(unsafe-typecast)
+
+import {Utils} from "../../src/Utils.sol";
+import {
+ ChannelDefinition,
+ ChannelStatus,
+ State,
+ StateIntent,
+ Ledger,
+ EscrowStatus,
+ ParticipantIndex
+} from "../../src/interfaces/Types.sol";
+import {ChannelHub} from "../../src/ChannelHub.sol";
+import {EscrowDepositEngine} from "../../src/EscrowDepositEngine.sol";
+import {EscrowWithdrawalEngine} from "../../src/EscrowWithdrawalEngine.sol";
+
+/*
+ * @dev This file uses integration / blackbox testing through ChannelHub to verify
+ * critical end-to-end challenge flows (signature validation, fund movements, storage updates, events).
+ * Complex state machine logic and edge cases are tested exhaustively in dedicated engine unit tests
+ * (ChannelEngine.t.sol, EscrowDepositEngine.t.sol, EscrowWithdrawalEngine.t.sol) for faster execution
+ * and better isolation.
+ */
+
+contract ChannelHubTest_Challenge_NonHomeChain_EscrowDeposit is ChannelHubTest_Challenge_Base {
+ /*
+ - reverts on challenging NON-EXISTENT escrow deposit
+ - escrow deposit can be challenged until `unlockAt` time has NOT passed
+ - escrow deposit can NOT be challenged after `unlockAt` time has passed
+ - challenged escrow deposit can be resolved until `challengeExpireAt` time has passed with a newer finalization state, which removes challenge and unlock funds
+ - challenged escrow deposit can NOT be resolved if `challengeExpireAt` has passed, but
+ can be withdrawn after `challengeExpireAt` time passes
+ - reverts on challenging already challenged escrow deposit
+ */
+
+ uint64 constant ESCROW_VERSION = 1;
+ uint256 constant ESCROW_AMOUNT = 500;
+
+ bytes32 escrowId;
+ State initiateEscrowDepositState;
+ State finalizeEscrowDepositState;
+
+ function setUp() public override {
+ super.setUp();
+ // `def` and `channelId` are set by ChannelHubTest_Challenge_Base.setUp()
+ // For non-home chain: NON_HOME_CHAIN_ID (42) is the home chain, block.chainid is non-home
+
+ initiateEscrowDepositState = State({
+ version: ESCROW_VERSION,
+ intent: StateIntent.INITIATE_ESCROW_DEPOSIT,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: NON_HOME_CHAIN_ID, // 42 — this IS the home chain (not current chain)
+ token: NON_HOME_TOKEN,
+ decimals: 18,
+ userAllocation: 500,
+ userNetFlow: 500,
+ nodeAllocation: ESCROW_AMOUNT, // must equal deposit amount in WAD (same decimals here)
+ nodeNetFlow: int256(ESCROW_AMOUNT)
+ }),
+ nonHomeLedger: Ledger({
+ chainId: uint64(block.chainid), // current chain is non-home
+ token: address(token),
+ decimals: 18,
+ userAllocation: ESCROW_AMOUNT,
+ userNetFlow: int256(ESCROW_AMOUNT),
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ initiateEscrowDepositState =
+ mutualSignStateBothWithEcdsaValidator(initiateEscrowDepositState, channelId, ALICE_PK);
+
+ vm.prank(alice);
+ cHub.initiateEscrowDeposit(def, initiateEscrowDepositState);
+
+ escrowId = Utils.getEscrowId(channelId, ESCROW_VERSION);
+
+ // Finalize state (version = ESCROW_VERSION + 1):
+ // home: userAllocation += ESCROW_AMOUNT, nodeAllocation = 0, userNetFlow unchanged
+ // non-home: allocations = 0; userNetFlow = +ESCROW_AMOUNT, nodeNetFlow = -ESCROW_AMOUNT
+ finalizeEscrowDepositState = nextState(
+ initiateEscrowDepositState,
+ StateIntent.FINALIZE_ESCROW_DEPOSIT,
+ [uint256(500 + ESCROW_AMOUNT), uint256(0)],
+ [int256(500), int256(ESCROW_AMOUNT)],
+ uint64(block.chainid),
+ address(token),
+ [uint256(0), uint256(0)],
+ [int256(ESCROW_AMOUNT), -int256(ESCROW_AMOUNT)]
+ );
+ finalizeEscrowDepositState =
+ mutualSignStateBothWithEcdsaValidator(finalizeEscrowDepositState, channelId, ALICE_PK);
+ }
+
+ function _challengeEscrowDeposit() internal {
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+ vm.prank(node);
+ cHub.challengeEscrowDeposit(escrowId, challengerSig, ParticipantIndex.NODE);
+ }
+
+ function test_revert_challengeEscrowDeposit_nonExistentEscrow() public {
+ bytes32 nonExistentEscrowId = Utils.getEscrowId(channelId, 999);
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.NoChannelIdFoundForEscrow.selector);
+ cHub.challengeEscrowDeposit(nonExistentEscrowId, challengerSig, ParticipantIndex.NODE);
+ }
+
+ function test_success_challengeEscrowDeposit_beforeUnlockAt() public {
+ _challengeEscrowDeposit();
+
+ (, EscrowStatus status,, uint64 challengeExpireAt,,) = cHub.getEscrowDepositData(escrowId);
+ assertEq(uint8(status), uint8(EscrowStatus.DISPUTED), "Escrow should be DISPUTED after challenge");
+ assertEq(
+ challengeExpireAt,
+ uint64(block.timestamp) + EscrowDepositEngine.CHALLENGE_DURATION,
+ "challengeExpireAt should be set to timestamp + CHALLENGE_DURATION"
+ );
+ }
+
+ function test_revert_challengeEscrowDeposit_afterUnlockAt() public {
+ vm.warp(block.timestamp + cHub.ESCROW_DEPOSIT_UNLOCK_DELAY() + 1);
+
+ vm.expectRevert(EscrowDepositEngine.UnlockPeriodPassed.selector);
+ _challengeEscrowDeposit();
+ }
+
+ function test_resolveChallengedEscrowDeposit_withFinalizeState_beforeChallengeExpiry() public {
+ _challengeEscrowDeposit();
+
+ (, EscrowStatus statusAfterChallenge,,,,) = cHub.getEscrowDepositData(escrowId);
+ assertEq(uint8(statusAfterChallenge), uint8(EscrowStatus.DISPUTED), "Should be DISPUTED after challenge");
+
+ uint256 nodeVaultBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
+
+ // Cooperative finalization with FINALIZE state (before challengeExpireAt)
+ vm.prank(node);
+ cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState);
+
+ (, EscrowStatus statusAfterFinalize,,, uint256 lockedAmount,) = cHub.getEscrowDepositData(escrowId);
+ assertEq(uint8(statusAfterFinalize), uint8(EscrowStatus.FINALIZED), "Escrow should be FINALIZED");
+ assertEq(lockedAmount, 0, "Locked amount should be 0 after finalization");
+
+ // Cooperative path: locked funds released to node vault (node earned them for providing cross-chain liquidity)
+ assertEq(
+ cHub.getAccountBalance(node, address(token), SUB_ID_0),
+ nodeVaultBefore + ESCROW_AMOUNT,
+ "Node vault should receive locked amount"
+ );
+ }
+
+ function test_challengedEscrowDeposit_canNotBeResolved_nodeReclaimsAfterChallengeExpiry() public {
+ _challengeEscrowDeposit();
+
+ (, EscrowStatus statusAfterChallenge,,,,) = cHub.getEscrowDepositData(escrowId);
+ assertEq(uint8(statusAfterChallenge), uint8(EscrowStatus.DISPUTED), "Should be DISPUTED after challenge");
+
+ vm.warp(block.timestamp + EscrowDepositEngine.CHALLENGE_DURATION + 1);
+
+ uint256 aliceBalanceBefore = token.balanceOf(alice);
+
+ // Unilateral finalization: anyone can call, state is ignored
+ vm.prank(node);
+ cHub.finalizeEscrowDeposit(channelId, escrowId, initiateEscrowDepositState);
+
+ (, EscrowStatus statusAfterFinalize,,, uint256 lockedAmount,) = cHub.getEscrowDepositData(escrowId);
+ assertEq(uint8(statusAfterFinalize), uint8(EscrowStatus.FINALIZED), "Escrow should be FINALIZED");
+ assertEq(lockedAmount, 0, "Locked amount should be 0 after finalization");
+
+ // Deposit Escrow funds are withdrawn to user wallet
+ assertEq(token.balanceOf(alice), aliceBalanceBefore + ESCROW_AMOUNT, "User should receive locked amount");
+ }
+
+ function test_revert_challengeEscrowDeposit_alreadyChallenged() public {
+ _challengeEscrowDeposit();
+
+ // Attempt to challenge the same escrow deposit again
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK);
+ vm.prank(node);
+ vm.expectRevert(EscrowDepositEngine.IncorrectEscrowStatus.selector);
+ cHub.challengeEscrowDeposit(escrowId, challengerSig, ParticipantIndex.NODE);
+ }
+}
+
+contract ChannelHubTest_Challenge_NonHomeChain_EscrowWithdrawal is ChannelHubTest_Challenge_Base {
+ /*
+ - reverts on challenging NON-EXISTENT escrow withdrawal
+ - escrow withdrawal can be challenged
+ - challenged escrow withdrawal can be resolved until `challengeExpireAt` time has passed with a newer finalization state, which removes challenge and unlock funds
+ - challenged escrow withdrawal can NOT be resolved if `challengeExpireAt` has passed, but
+ can be withdrawn after `challengeExpireAt` time passes
+ - reverts on challenging already challenged escrow withdrawal
+ */
+
+ uint64 constant WITHDRAWAL_VERSION = 1;
+ uint256 constant WITHDRAWAL_AMOUNT = 300;
+
+ bytes32 escrowId;
+ State initiateEscrowWithdrawalState;
+ State finalizeEscrowWithdrawalState;
+
+ function setUp() public override {
+ super.setUp();
+ // `def` and `channelId` are set by ChannelHubTest_Challenge_Base.setUp()
+ // For non-home chain: NON_HOME_CHAIN_ID (42) is the home chain, block.chainid is non-home
+
+ initiateEscrowWithdrawalState = State({
+ version: WITHDRAWAL_VERSION,
+ intent: StateIntent.INITIATE_ESCROW_WITHDRAWAL,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: NON_HOME_CHAIN_ID, // 42 — this IS the home chain (not current chain)
+ token: NON_HOME_TOKEN,
+ decimals: 18,
+ userAllocation: 500, // user has enough allocation to withdraw
+ userNetFlow: 500,
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ nonHomeLedger: Ledger({
+ chainId: uint64(block.chainid), // current chain is non-home
+ token: address(token),
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: WITHDRAWAL_AMOUNT, // node locks this amount for user's withdrawal
+ nodeNetFlow: int256(WITHDRAWAL_AMOUNT)
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ initiateEscrowWithdrawalState =
+ mutualSignStateBothWithEcdsaValidator(initiateEscrowWithdrawalState, channelId, ALICE_PK);
+
+ vm.prank(alice);
+ cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState);
+
+ escrowId = Utils.getEscrowId(channelId, WITHDRAWAL_VERSION);
+
+ // Finalize state (version = WITHDRAWAL_VERSION + 1):
+ // home: userAllocation decreases by WITHDRAWAL_AMOUNT, nodeNetFlow decreases by WITHDRAWAL_AMOUNT
+ // non-home: allocations = 0; userNetFlow = -WITHDRAWAL_AMOUNT, nodeNetFlow = +WITHDRAWAL_AMOUNT
+ finalizeEscrowWithdrawalState = nextState(
+ initiateEscrowWithdrawalState,
+ StateIntent.FINALIZE_ESCROW_WITHDRAWAL,
+ [uint256(500 - WITHDRAWAL_AMOUNT), uint256(0)],
+ [int256(500), -int256(WITHDRAWAL_AMOUNT)],
+ uint64(block.chainid),
+ address(token),
+ [uint256(0), uint256(0)],
+ [-int256(WITHDRAWAL_AMOUNT), int256(WITHDRAWAL_AMOUNT)]
+ );
+ finalizeEscrowWithdrawalState =
+ mutualSignStateBothWithEcdsaValidator(finalizeEscrowWithdrawalState, channelId, ALICE_PK);
+ }
+
+ function _challengeEscrowWithdrawal() internal {
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+ vm.prank(node);
+ cHub.challengeEscrowWithdrawal(escrowId, challengerSig, ParticipantIndex.NODE);
+ }
+
+ function test_revert_challengeEscrowWithdrawal_nonExistentEscrow() public {
+ bytes32 nonExistentEscrowId = Utils.getEscrowId(channelId, 999);
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+ vm.prank(node);
+ vm.expectRevert(ChannelHub.NoChannelIdFoundForEscrow.selector);
+ cHub.challengeEscrowWithdrawal(nonExistentEscrowId, challengerSig, ParticipantIndex.NODE);
+ }
+
+ function test_challengeEscrowWithdrawal() public {
+ _challengeEscrowWithdrawal();
+
+ (, EscrowStatus status, uint64 challengeExpireAt,,) = cHub.getEscrowWithdrawalData(escrowId);
+ assertEq(uint8(status), uint8(EscrowStatus.DISPUTED), "Escrow should be DISPUTED after challenge");
+ assertEq(
+ challengeExpireAt,
+ uint64(block.timestamp) + EscrowWithdrawalEngine.CHALLENGE_DURATION,
+ "challengeExpireAt should be set to timestamp + CHALLENGE_DURATION"
+ );
+ }
+
+ function test_resolveChallengedEscrowWithdrawal_withFinalizeState_beforeChallengeExpiry() public {
+ _challengeEscrowWithdrawal();
+
+ (, EscrowStatus statusAfterChallenge,,,) = cHub.getEscrowWithdrawalData(escrowId);
+ assertEq(uint8(statusAfterChallenge), uint8(EscrowStatus.DISPUTED), "Should be DISPUTED after challenge");
+
+ uint256 aliceBalanceBefore = token.balanceOf(alice);
+ uint256 nodeVaultBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
+
+ // Cooperative finalization with FINALIZE state (before challengeExpireAt)
+ vm.prank(node);
+ cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState);
+
+ (, EscrowStatus statusAfterFinalize,, uint256 lockedAmount,) = cHub.getEscrowWithdrawalData(escrowId);
+ assertEq(uint8(statusAfterFinalize), uint8(EscrowStatus.FINALIZED), "Escrow should be FINALIZED");
+ assertEq(lockedAmount, 0, "Locked amount should be 0 after finalization");
+
+ // Cooperative path: locked funds released to user wallet (withdrawal succeeded)
+ assertEq(
+ token.balanceOf(alice), aliceBalanceBefore + WITHDRAWAL_AMOUNT, "User should receive withdrawal amount"
+ );
+ // Node vault should be unchanged (locked amount was already deducted at initiation)
+ assertEq(
+ cHub.getAccountBalance(node, address(token), SUB_ID_0), nodeVaultBefore, "Node vault should be unchanged"
+ );
+ }
+
+ function test_challengedEscrowWithdrawal_canNotBeResolved_nodeReclaimsAfterChallengeExpiry() public {
+ _challengeEscrowWithdrawal();
+
+ vm.warp(block.timestamp + EscrowWithdrawalEngine.CHALLENGE_DURATION + 1);
+
+ uint256 aliceBalanceBefore = token.balanceOf(alice);
+ uint256 nodeVaultBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
+
+ // Attempt cooperative resolution with a valid FINALIZE state after challengeExpireAt
+ // The unilateral path intercepts and ignores the candidate state
+ vm.prank(node);
+ cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState);
+
+ (, EscrowStatus status,, uint256 lockedAmount,) = cHub.getEscrowWithdrawalData(escrowId);
+ assertEq(uint8(status), uint8(EscrowStatus.FINALIZED), "Escrow should be FINALIZED");
+ assertEq(lockedAmount, 0, "Locked amount should be 0");
+
+ // Unilateral path (not cooperative): locked funds returned to node vault (withdrawal failed)
+ assertEq(
+ cHub.getAccountBalance(node, address(token), SUB_ID_0),
+ nodeVaultBefore + WITHDRAWAL_AMOUNT,
+ "Node vault should reclaim locked amount (cooperative resolution bypassed)"
+ );
+ assertEq(token.balanceOf(alice), aliceBalanceBefore, "User wallet unchanged: withdrawal was not completed");
+ }
+
+ function test_revert_challengeEscrowWithdrawal_alreadyChallenged() public {
+ _challengeEscrowWithdrawal();
+
+ // Attempt to challenge the same escrow withdrawal again
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK);
+ vm.prank(node);
+ vm.expectRevert(EscrowWithdrawalEngine.IncorrectEscrowStatus.selector);
+ cHub.challengeEscrowWithdrawal(escrowId, challengerSig, ParticipantIndex.NODE);
+ }
+}
+
+contract ChannelHubTest_Challenge_NonHomeChain_HomeMigration is ChannelHubTest_Challenge_Base {
+ /*
+ Test cases:
+ - a channel in Migrating_in status (empty channel after being called with `initiateMigration`) can be challenged with it
+ - a channel in Migrating_in status (empty channel after being called with `initiateMigration`) can be challenged with a newer Operation state
+ */
+
+ uint64 initiateMigrationVersion = 1;
+ State initiateMigrationState;
+ uint64 finalizeMigrationVersion = 2;
+ State finalizeMigrationState;
+ uint64 operateAfterMigrationInitVersion = 2;
+ State operateAfterMigrationInitState;
+
+ // New channel for testing NEW home chain behavior
+ ChannelDefinition newHomeDef;
+ bytes32 newHomeChannelId;
+ State newHomeInitiateMigrationState;
+ uint64 newHomeOperateVersion = 3;
+ State newHomeOperateState;
+
+ function setUp() public override {
+ super.setUp();
+
+ // Setup for NEW home chain tests (migration IN)
+ newHomeDef = ChannelDefinition({
+ challengeDuration: CHALLENGE_DURATION,
+ user: alice,
+ node: node,
+ nonce: uint64(42), // Different nonce to create a new channel
+ approvedSignatureValidators: 0,
+ metadata: bytes32(0)
+ });
+ newHomeChannelId = Utils.getChannelId(newHomeDef, CHANNEL_HUB_VERSION);
+
+ // INITIATE_MIGRATION state for NEW home chain (migration IN)
+ // homeLedger = OLD home chain (NON_HOME_CHAIN_ID)
+ // nonHomeLedger = NEW home chain (current chain)
+ newHomeInitiateMigrationState = State({
+ version: initiateMigrationVersion,
+ intent: StateIntent.INITIATE_MIGRATION,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: NON_HOME_CHAIN_ID,
+ token: NON_HOME_TOKEN,
+ decimals: 18,
+ userAllocation: 500,
+ userNetFlow: 500,
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ nonHomeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: 500, // Node locks user allocation on new home
+ nodeNetFlow: 500
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ newHomeInitiateMigrationState =
+ mutualSignStateBothWithEcdsaValidator(newHomeInitiateMigrationState, newHomeChannelId, ALICE_PK);
+
+ // OPERATE state on NEW home chain after migration
+ // After initiateMigration on NEW home, ledgers are swapped, so homeLedger becomes current chain
+ // OPERATE requires userNfDelta == 0, so userNetFlow must stay 0
+ newHomeOperateState = State({
+ version: newHomeOperateVersion,
+ intent: StateIntent.OPERATE,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 450,
+ userNetFlow: 0,
+ nodeAllocation: 0,
+ nodeNetFlow: 450
+ }),
+ nonHomeLedger: Ledger({
+ chainId: 0,
+ token: address(0),
+ decimals: 0,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ newHomeOperateState = mutualSignStateBothWithEcdsaValidator(newHomeOperateState, newHomeChannelId, ALICE_PK);
+ }
+
+ function test_challenge_newHomeChain_withInitiateMigration_asExisting() public {
+ // Initiate migration IN on NEW home chain
+ vm.prank(alice);
+ cHub.initiateMigration(newHomeDef, newHomeInitiateMigrationState);
+
+ // Verify channel is in MIGRATING_IN status
+ verifyChannelData(
+ newHomeChannelId,
+ ChannelStatus.MIGRATING_IN,
+ initiateMigrationVersion,
+ 0,
+ "newHomeInitiateMigrationState should be enforced"
+ );
+
+ // Challenge with the same INITIATE_MIGRATION state (already enforced)
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(newHomeChannelId, newHomeInitiateMigrationState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(newHomeChannelId, newHomeInitiateMigrationState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and state is still version 0
+ verifyChannelData(
+ newHomeChannelId,
+ ChannelStatus.DISPUTED,
+ initiateMigrationVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "initiateMigrationVersion should remain enforced"
+ );
+ }
+
+ function test_challenge_newHomeChain_withOperate_inMigratingIn() public {
+ // Initiate migration IN on NEW home chain
+ vm.prank(alice);
+ cHub.initiateMigration(newHomeDef, newHomeInitiateMigrationState);
+
+ // Verify channel is in MIGRATING_IN status
+ verifyChannelData(
+ newHomeChannelId,
+ ChannelStatus.MIGRATING_IN,
+ initiateMigrationVersion,
+ 0,
+ "newHomeInitiateMigrationState should be enforced"
+ );
+
+ // Challenge with newer OPERATE state
+ bytes memory challengerSig =
+ signChallengeEip191WithEcdsaValidator(newHomeChannelId, newHomeOperateState, NODE_PK);
+
+ vm.prank(node);
+ cHub.challengeChannel(newHomeChannelId, newHomeOperateState, challengerSig, ParticipantIndex.NODE);
+
+ // Verify channel is DISPUTED and newHomeOperateState was enforced
+ verifyChannelData(
+ newHomeChannelId,
+ ChannelStatus.DISPUTED,
+ newHomeOperateVersion,
+ block.timestamp + CHALLENGE_DURATION,
+ "newHomeOperateState should start a challenge"
+ );
+ verifyChannelState(
+ newHomeChannelId,
+ [uint256(450), uint256(0)],
+ [int256(0), int256(450)],
+ "newHomeOperateState should be enforced"
+ );
+ }
+}
+// forge-lint: disable-end(unsafe-typecast)
diff --git a/contracts/test/ChannelHub_claimFunds.t.sol b/contracts/test/ChannelHub_claimFunds.t.sol
index a1bb3886a..0fea484ac 100644
--- a/contracts/test/ChannelHub_claimFunds.t.sol
+++ b/contracts/test/ChannelHub_claimFunds.t.sol
@@ -20,6 +20,8 @@ contract ChannelHubTest_claimFunds is Test {
address public claimer;
address public destination;
+ uint48 SUB_ID_0 = 0;
+
uint256 constant RECLAIM_AMOUNT = 100 ether;
uint256 constant BALANCE_AMOUNT = RECLAIM_AMOUNT * 10;
@@ -61,10 +63,10 @@ contract ChannelHubTest_claimFunds is Test {
cHub.workaround_setReclaim(claimer, address(token), RECLAIM_AMOUNT);
vm.expectEmit(true, true, true, true);
- emit ChannelHub.FundsClaimed(claimer, address(token), claimer, RECLAIM_AMOUNT);
+ emit ChannelHub.FundsClaimed(claimer, address(token), 0, claimer, RECLAIM_AMOUNT);
vm.prank(claimer);
- cHub.claimFunds(address(token), claimer);
+ cHub.claimFunds(address(token), SUB_ID_0, claimer);
_verifyTransferSuccess(claimer, claimer, address(token), RECLAIM_AMOUNT);
}
@@ -73,10 +75,10 @@ contract ChannelHubTest_claimFunds is Test {
cHub.workaround_setReclaim(claimer, address(token), RECLAIM_AMOUNT);
vm.expectEmit(true, true, true, true);
- emit ChannelHub.FundsClaimed(claimer, address(token), destination, RECLAIM_AMOUNT);
+ emit ChannelHub.FundsClaimed(claimer, address(token), SUB_ID_0, destination, RECLAIM_AMOUNT);
vm.prank(claimer);
- cHub.claimFunds(address(token), destination);
+ cHub.claimFunds(address(token), SUB_ID_0, destination);
_verifyTransferSuccess(claimer, destination, address(token), RECLAIM_AMOUNT);
}
@@ -87,7 +89,7 @@ contract ChannelHubTest_claimFunds is Test {
cHub.workaround_setReclaim(claimer, address(token), totalAccumulated);
vm.prank(claimer);
- cHub.claimFunds(address(token), destination);
+ cHub.claimFunds(address(token), SUB_ID_0, destination);
_verifyTransferSuccess(claimer, destination, address(token), totalAccumulated);
}
@@ -98,10 +100,10 @@ contract ChannelHubTest_claimFunds is Test {
cHub.workaround_setReclaim(claimer, address(0), RECLAIM_AMOUNT);
vm.expectEmit(true, true, true, true);
- emit ChannelHub.FundsClaimed(claimer, address(0), claimer, RECLAIM_AMOUNT);
+ emit ChannelHub.FundsClaimed(claimer, address(0), SUB_ID_0, claimer, RECLAIM_AMOUNT);
vm.prank(claimer);
- cHub.claimFunds(address(0), claimer);
+ cHub.claimFunds(address(0), SUB_ID_0, claimer);
_verifyTransferSuccess(claimer, claimer, address(0), RECLAIM_AMOUNT);
}
@@ -110,10 +112,10 @@ contract ChannelHubTest_claimFunds is Test {
cHub.workaround_setReclaim(claimer, address(0), RECLAIM_AMOUNT);
vm.expectEmit(true, true, true, true);
- emit ChannelHub.FundsClaimed(claimer, address(0), destination, RECLAIM_AMOUNT);
+ emit ChannelHub.FundsClaimed(claimer, address(0), SUB_ID_0, destination, RECLAIM_AMOUNT);
vm.prank(claimer);
- cHub.claimFunds(address(0), destination);
+ cHub.claimFunds(address(0), SUB_ID_0, destination);
_verifyTransferSuccess(claimer, destination, address(0), RECLAIM_AMOUNT);
}
@@ -125,7 +127,7 @@ contract ChannelHubTest_claimFunds is Test {
vm.prank(claimer);
vm.expectRevert(ChannelHub.InvalidAddress.selector);
- cHub.claimFunds(address(token), address(0));
+ cHub.claimFunds(address(token), SUB_ID_0, address(0));
}
function test_revert_ifReclaimBalanceIsZero() public {
@@ -133,7 +135,7 @@ contract ChannelHubTest_claimFunds is Test {
vm.prank(claimer);
vm.expectRevert(ChannelHub.IncorrectAmount.selector);
- cHub.claimFunds(address(token), destination);
+ cHub.claimFunds(address(token), SUB_ID_0, destination);
}
function test_revert_ifETHTransferFails() public {
@@ -143,7 +145,7 @@ contract ChannelHubTest_claimFunds is Test {
vm.expectRevert(
abi.encodeWithSelector(ChannelHub.NativeTransferFailed.selector, address(revertingReceiver), RECLAIM_AMOUNT)
);
- cHub.claimFunds(address(0), address(revertingReceiver));
+ cHub.claimFunds(address(0), SUB_ID_0, address(revertingReceiver));
}
// ========== State Change Tests ==========
@@ -156,7 +158,7 @@ contract ChannelHubTest_claimFunds is Test {
// Other user tries to claim
vm.prank(otherUser);
vm.expectRevert(ChannelHub.IncorrectAmount.selector);
- cHub.claimFunds(address(token), destination);
+ cHub.claimFunds(address(token), SUB_ID_0, destination);
// Verify reclaim still exists for claimer
assertEq(cHub.getReclaimBalance(claimer, address(token)), RECLAIM_AMOUNT, "Reclaim should still exist");
@@ -170,12 +172,12 @@ contract ChannelHubTest_claimFunds is Test {
cHub.workaround_setReclaim(claimer, address(token2), RECLAIM_AMOUNT);
vm.prank(claimer);
- cHub.claimFunds(address(token), destination);
+ cHub.claimFunds(address(token), SUB_ID_0, destination);
_verifyTransferSuccess(claimer, destination, address(token), RECLAIM_AMOUNT);
vm.prank(claimer);
- cHub.claimFunds(address(token2), destination);
+ cHub.claimFunds(address(token2), SUB_ID_0, destination);
_verifyTransferSuccess(claimer, destination, address(token2), RECLAIM_AMOUNT);
}
diff --git a/contracts/test/ChannelHub_emitsNodeBalanceUpdated.t.sol b/contracts/test/ChannelHub_emitsNodeBalanceUpdated.t.sol
new file mode 100644
index 000000000..a3c49e711
--- /dev/null
+++ b/contracts/test/ChannelHub_emitsNodeBalanceUpdated.t.sol
@@ -0,0 +1,592 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.30;
+
+import {Vm} from "forge-std/Vm.sol";
+
+import {ChannelHubTest_Base} from "./ChannelHub_Base.t.sol";
+
+import {Utils} from "../src/Utils.sol";
+import {ChannelHub} from "../src/ChannelHub.sol";
+import {ChannelDefinition, State, StateIntent, Ledger, ParticipantIndex} from "../src/interfaces/Types.sol";
+import {EscrowWithdrawalEngine} from "../src/EscrowWithdrawalEngine.sol";
+
+/**
+ * Black-box tests verifying that NodeBalanceUpdated is emitted on every operation
+ * that mutates internal node vault balance (_nodeBalances), and is NOT emitted when
+ * no mutation occurs.
+ *
+ * # Scope
+ *
+ * "Node balance" here means the internal vault balance tracked by _nodeBalances,
+ * i.e. the value returned by getAccountBalance(). It does NOT include funds pushed
+ * directly to the node's address (e.g. nodeAllocation paid out on channel close),
+ * because those bypass the vault and require no event.
+ *
+ * # Off-chain batching
+ *
+ * The protocol allows multiple off-chain transfers to be batched into a single on-chain
+ * state update (checkpoint). From the contract's perspective this is indistinguishable
+ * from a single transfer of the same net amount. Batching correctness is an
+ * off-chain concern and belongs in off-chain unit tests.
+ */
+
+// forge-lint: disable-start(unsafe-typecast)
+contract ChannelHubTest_emitsNodeBalanceUpdated is ChannelHubTest_Base {
+ /**
+ * Emits NodeBalanceUpdated:
+ * - depositToVault — direct vault deposit by node
+ * - withdrawFromVault — direct vault withdrawal by node
+ * - createChannel (DEPOSIT intent, both lock) — node locks funds into channel
+ * - createChannel (WITHDRAW intent) — node locks funds into channel
+ * - depositToChannel (both lock) — node locks funds into channel
+ * - withdrawFromChannel — node unlocks funds from channel
+ * - checkpoint (with node fund change) — node balance changes due to off-chain transfer(s)
+ * - closeChannel cooperative (CLOSE intent) — node unlocks funds from channel
+ * - challengeChannel with newer state — when newer state carries non-zero node delta
+ * - initiateEscrowWithdrawal (non-home chain) — node locks liquidity for cross-chain withdrawal
+ * - finalizeEscrowDeposit (non-home chain) — node releases locked liquidity after swap
+ * - finalizeEscrowWithdrawal (non-home chain, timeout) — node reclaims locked liquidity after challenge timeout
+ * - purgeEscrowDeposits — expired escrow deposits released back to node vault
+ *
+ * Does NOT emit NodeBalanceUpdated:
+ * - createChannel (DEPOSIT intent, only user deposits) - status change only, no node fund movement
+ * - depositToChannel (no change from Node) - no fund movement
+ * - checkpoint with no node fund change - no fund movement
+ * - initiateEscrowDeposit (non-home chain) — status change only, no fund movement
+ * - challengeEscrowDeposit — status change only, no fund movement
+ * - challengeEscrowWithdrawal — status change only, no fund movement
+ */
+ // ======== State ========
+
+ ChannelDefinition internal def;
+ bytes32 internal channelId;
+
+ // Used for non-home chain escrow tests (bob = user, node = node)
+ ChannelDefinition internal bobDef;
+ bytes32 internal bobChannelId;
+
+ bytes32 constant NODE_BALANCE_UPDATED_SIG = keccak256("NodeBalanceUpdated(address,address,uint48,uint256)");
+
+ // Non-home chain constants (fake foreign chain)
+ uint64 constant FOREIGN_CHAIN_ID = 42;
+ address constant FOREIGN_TOKEN = address(42);
+
+ Ledger EMPTY_LEDGER = Ledger({chainId: 0, token: address(0), decimals: 0, userAllocation: 0, userNetFlow: 0, nodeAllocation: 0, nodeNetFlow: 0});
+
+ // ======== Setup ========
+
+ function setUp() public override {
+ super.setUp();
+
+ def = ChannelDefinition({
+ challengeDuration: CHALLENGE_DURATION,
+ user: alice,
+ node: node,
+ nonce: NONCE,
+ approvedSignatureValidators: 0,
+ metadata: bytes32(0)
+ });
+ channelId = Utils.getChannelId(def, CHANNEL_HUB_VERSION);
+
+ bobDef = ChannelDefinition({
+ challengeDuration: CHALLENGE_DURATION,
+ user: bob,
+ node: node,
+ nonce: NONCE,
+ approvedSignatureValidators: 0,
+ metadata: bytes32(0)
+ });
+ bobChannelId = Utils.getChannelId(bobDef, CHANNEL_HUB_VERSION);
+ }
+
+ // ======== Helpers ========
+
+ /// @dev Expects the next NodeBalanceUpdated(node, token, expectedBalance) emission.
+ function _expectEmitNodeBalanceUpdated(uint256 expectedBalance) internal {
+ vm.expectEmit(true, true, true, true, address(cHub));
+ emit ChannelHub.NodeBalanceUpdated(node, address(token), SUB_ID_0, expectedBalance);
+ }
+
+ /// @dev Asserts NodeBalanceUpdated was NOT emitted in the logs recorded since the last vm.recordLogs().
+ function _assertNoEmitNodeBalanceUpdated() internal view {
+ Vm.Log[] memory logs = vm.getRecordedLogs();
+ for (uint256 i = 0; i < logs.length; i++) {
+ assertNotEq(logs[i].topics[0], NODE_BALANCE_UPDATED_SIG, "NodeBalanceUpdated was unexpectedly emitted");
+ }
+ }
+
+ /// @dev Creates a channel for alice where node contributes nothing (nodeNetFlow = 0).
+ /// Returns the signed initial state.
+ function _createSimpleChannel() internal returns (State memory state) {
+ state = State({
+ version: 0,
+ intent: StateIntent.DEPOSIT,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: DEPOSIT_AMOUNT,
+ userNetFlow: int256(DEPOSIT_AMOUNT),
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ nonHomeLedger: EMPTY_LEDGER,
+ userSig: "",
+ nodeSig: ""
+ });
+ state = mutualSignStateBothWithEcdsaValidator(state, channelId, ALICE_PK);
+ vm.prank(alice);
+ cHub.createChannel(def, state);
+ }
+
+ /// @dev Creates a channel via OPERATE intent where node locks DEPOSIT_AMOUNT from vault.
+ /// Returns the signed initial state.
+ function _createChannelNodeLocks() internal returns (State memory state) {
+ state = State({
+ version: 0,
+ intent: StateIntent.OPERATE,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: DEPOSIT_AMOUNT,
+ userNetFlow: 0,
+ nodeAllocation: 0,
+ nodeNetFlow: int256(DEPOSIT_AMOUNT)
+ }),
+ nonHomeLedger: EMPTY_LEDGER,
+ userSig: "",
+ nodeSig: ""
+ });
+ state = mutualSignStateBothWithEcdsaValidator(state, channelId, ALICE_PK);
+ vm.prank(alice);
+ cHub.createChannel(def, state);
+ }
+
+ /// @dev Sets up an escrow deposit on the non-home chain for bob (current chain = non-home).
+ /// Returns (escrowId, initState). Node vault unchanged; bob's DEPOSIT_AMOUNT is locked.
+ function _initiateEscrowDeposit() internal returns (bytes32 escrowId, State memory initState) {
+ initState = State({
+ version: 1,
+ intent: StateIntent.INITIATE_ESCROW_DEPOSIT,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: FOREIGN_CHAIN_ID,
+ token: FOREIGN_TOKEN,
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: DEPOSIT_AMOUNT,
+ nodeNetFlow: int256(DEPOSIT_AMOUNT)
+ }),
+ nonHomeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: DEPOSIT_AMOUNT,
+ userNetFlow: int256(DEPOSIT_AMOUNT),
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ initState = mutualSignStateBothWithEcdsaValidator(initState, bobChannelId, BOB_PK);
+ escrowId = Utils.getEscrowId(bobChannelId, initState.version);
+ vm.prank(bob);
+ cHub.initiateEscrowDeposit(bobDef, initState);
+ }
+
+ /// @dev Sets up an escrow withdrawal on the non-home chain for bob.
+ /// Returns (escrowId, initState). Node locks DEPOSIT_AMOUNT from vault.
+ function _initiateEscrowWithdrawal() internal returns (bytes32 escrowId, State memory initState) {
+ initState = State({
+ version: 1,
+ intent: StateIntent.INITIATE_ESCROW_WITHDRAWAL,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: FOREIGN_CHAIN_ID,
+ token: FOREIGN_TOKEN,
+ decimals: 18,
+ userAllocation: DEPOSIT_AMOUNT,
+ userNetFlow: int256(DEPOSIT_AMOUNT),
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ nonHomeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: DEPOSIT_AMOUNT,
+ nodeNetFlow: int256(DEPOSIT_AMOUNT)
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ initState = mutualSignStateBothWithEcdsaValidator(initState, bobChannelId, BOB_PK);
+ escrowId = Utils.getEscrowId(bobChannelId, initState.version);
+ vm.prank(bob);
+ cHub.initiateEscrowWithdrawal(bobDef, initState);
+ }
+
+ // ======== Tests: emits NodeBalanceUpdated ========
+
+ function test_success_onDepositToVault() public {
+ token.mint(node, DEPOSIT_AMOUNT);
+ vm.startPrank(node);
+ token.approve(address(cHub), DEPOSIT_AMOUNT);
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE + DEPOSIT_AMOUNT);
+ cHub.depositToVault(node, address(token), SUB_ID_0, DEPOSIT_AMOUNT);
+ vm.stopPrank();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE + DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onWithdrawFromVault() public {
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ vm.prank(node);
+ cHub.withdrawFromVault(node, address(token), SUB_ID_0, DEPOSIT_AMOUNT);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onCreateChannel_depositIntent_bothDeposit() public {
+ // both deposit
+ State memory state = State({
+ version: 0,
+ intent: StateIntent.DEPOSIT,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: DEPOSIT_AMOUNT,
+ userNetFlow: int256(DEPOSIT_AMOUNT),
+ nodeAllocation: DEPOSIT_AMOUNT,
+ nodeNetFlow: int256(DEPOSIT_AMOUNT)
+ }),
+ nonHomeLedger: EMPTY_LEDGER,
+ userSig: "",
+ nodeSig: ""
+ });
+ state = mutualSignStateBothWithEcdsaValidator(state, channelId, ALICE_PK);
+
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ vm.prank(alice);
+ cHub.createChannel(def, state);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onCreateChannel_withdrawIntent() public {
+ // both deposit, node immediately transfers some funds for user to withdraw
+ State memory state = State({
+ version: 0,
+ intent: StateIntent.WITHDRAW,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 500,
+ userNetFlow: -500,
+ nodeAllocation: 0,
+ nodeNetFlow: int256(DEPOSIT_AMOUNT)
+ }),
+ nonHomeLedger: EMPTY_LEDGER,
+ userSig: "",
+ nodeSig: ""
+ });
+ state = mutualSignStateBothWithEcdsaValidator(state, channelId, ALICE_PK);
+
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ vm.prank(alice);
+ cHub.createChannel(def, state);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onDepositToChannel_bothDeposit() public {
+ // Setup: channel with user=DA, node=0
+ State memory prevState = _createSimpleChannel();
+
+ // Deposit: both User and Node specify amounts
+ State memory candidate = nextState(
+ prevState,
+ StateIntent.DEPOSIT,
+ [DEPOSIT_AMOUNT * 2, DEPOSIT_AMOUNT],
+ [int256(DEPOSIT_AMOUNT) * 2, int256(DEPOSIT_AMOUNT)]
+ );
+ candidate = mutualSignStateBothWithEcdsaValidator(candidate, channelId, ALICE_PK);
+
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ vm.prank(alice);
+ cHub.depositToChannel(channelId, candidate);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onWithdrawFromChannel() public {
+ // Setup: channel via OPERATE where node locks DEPOSIT_AMOUNT (vault = INITIAL_BALANCE - DA)
+ State memory prevState = _createChannelNodeLocks();
+
+ // User withdraws 500
+ State memory candidate = State({
+ version: prevState.version + 1,
+ intent: StateIntent.WITHDRAW,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: -500,
+ nodeAllocation: 0,
+ nodeNetFlow: 500
+ }),
+ nonHomeLedger: EMPTY_LEDGER,
+ userSig: "",
+ nodeSig: ""
+ });
+ candidate = mutualSignStateBothWithEcdsaValidator(candidate, channelId, ALICE_PK);
+
+ uint256 expectedBalance = INITIAL_BALANCE - DEPOSIT_AMOUNT + 500;
+ _expectEmitNodeBalanceUpdated(expectedBalance);
+ vm.prank(alice);
+ cHub.withdrawFromChannel(channelId, candidate);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), expectedBalance);
+ }
+
+ function test_success_onCheckpointChannel_withNodeFundChange() public {
+ State memory prevState = _createSimpleChannel();
+
+ // Off-chain: user transferred 500 to node.
+ State memory candidate = nextState(prevState, StateIntent.OPERATE, [DEPOSIT_AMOUNT - 500, 0], [int256(DEPOSIT_AMOUNT), -500]);
+ candidate = mutualSignStateBothWithEcdsaValidator(candidate, channelId, ALICE_PK);
+
+ uint256 expectedBalance = INITIAL_BALANCE + 500;
+ _expectEmitNodeBalanceUpdated(expectedBalance);
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, candidate);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), expectedBalance);
+ }
+
+ function test_success_onCloseChannel() public {
+ // Setup: channel via OPERATE where node locks DEPOSIT_AMOUNT (vault = INITIAL_BALANCE - DEPOSIT_AMOUNT)
+ State memory prevState = _createChannelNodeLocks();
+
+ // Close: node balance returns to initial balance
+ State memory candidate = State({
+ version: prevState.version + 1,
+ intent: StateIntent.CLOSE,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: 0,
+ nodeAllocation: 0,
+ nodeNetFlow: 0
+ }),
+ nonHomeLedger: EMPTY_LEDGER,
+ userSig: "",
+ nodeSig: ""
+ });
+ candidate = mutualSignStateBothWithEcdsaValidator(candidate, channelId, ALICE_PK);
+
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE);
+ vm.prank(alice);
+ cHub.closeChannel(channelId, candidate);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_success_onChallengeChannel_newerStateChangesNodeFunds() public {
+ // Setup: simple channel; node vault = INITIAL_BALANCE, lockedFunds = DEPOSIT_AMOUNT (user's)
+ State memory initState = _createSimpleChannel();
+
+ // Off-chain: user transferred 500 to node (nodeNF goes from 0 to -500)
+ // Enforce via challenge: nodeFundsDelta = -500 - 0 = -500 → vault += 500
+ State memory stateV1 = nextState(initState, StateIntent.OPERATE, [DEPOSIT_AMOUNT - 500, uint256(0)], [int256(DEPOSIT_AMOUNT), -500]);
+ stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK);
+
+ bytes memory sig = signChallengeEip191WithEcdsaValidator(channelId, stateV1, NODE_PK);
+
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE + 500);
+ vm.prank(node);
+ cHub.challengeChannel(channelId, stateV1, sig, ParticipantIndex.NODE);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE + 500);
+ }
+
+ function test_success_onInitiateEscrowWithdrawal_nonHome() public {
+ // Non-home chain (current): node locks DEPOSIT_AMOUNT from vault to fund user withdrawal
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ _initiateEscrowWithdrawal();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onFinalizeEscrowDeposit_nonHome() public {
+ // Setup: bob deposits DEPOSIT_AMOUNT into escrow (node vault unchanged = INITIAL_BALANCE)
+ (bytes32 escrowId, State memory initState) = _initiateEscrowDeposit();
+
+ // Finalize: DEPOSIT_AMOUNT flows from escrow (user's locked funds) to node vault
+ State memory finalizeState = State({
+ version: initState.version + 1,
+ intent: StateIntent.FINALIZE_ESCROW_DEPOSIT,
+ metadata: bytes32(0),
+ homeLedger: Ledger({
+ chainId: FOREIGN_CHAIN_ID,
+ token: FOREIGN_TOKEN,
+ decimals: 18,
+ userAllocation: DEPOSIT_AMOUNT,
+ userNetFlow: 0,
+ nodeAllocation: 0,
+ nodeNetFlow: int256(DEPOSIT_AMOUNT)
+ }),
+ nonHomeLedger: Ledger({
+ chainId: uint64(block.chainid),
+ token: address(token),
+ decimals: 18,
+ userAllocation: 0,
+ userNetFlow: int256(DEPOSIT_AMOUNT),
+ nodeAllocation: 0,
+ nodeNetFlow: -int256(DEPOSIT_AMOUNT)
+ }),
+ userSig: "",
+ nodeSig: ""
+ });
+ finalizeState = mutualSignStateBothWithEcdsaValidator(finalizeState, bobChannelId, BOB_PK);
+
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE + DEPOSIT_AMOUNT);
+ vm.prank(node);
+ cHub.finalizeEscrowDeposit(bobChannelId, escrowId, finalizeState);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE + DEPOSIT_AMOUNT);
+ }
+
+ function test_success_onFinalizeEscrowWithdrawal_nonHome_afterChallengeTimeout() public {
+ // Setup: node locks DEPOSIT_AMOUNT → vault = INITIAL_BALANCE - DA
+ (bytes32 escrowId, State memory initState) = _initiateEscrowWithdrawal();
+
+ // Challenge: INITIALIZED → DISPUTED
+ bytes memory sig = signChallengeEip191WithEcdsaValidator(bobChannelId, initState, BOB_PK);
+ vm.prank(bob);
+ cHub.challengeEscrowWithdrawal(escrowId, sig, ParticipantIndex.USER);
+
+ // Expire the challenge
+ vm.warp(block.timestamp + EscrowWithdrawalEngine.CHALLENGE_DURATION + 1);
+
+ // Finalize via timeout: node reclaims DEPOSIT_AMOUNT → vault = INITIAL_BALANCE
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE);
+ vm.prank(node);
+ cHub.finalizeEscrowWithdrawal(bobChannelId, escrowId, initState);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_success_onPurgeEscrowDeposits() public {
+ // Setup: bob deposits DEPOSIT_AMOUNT into escrow (node vault unchanged = INITIAL_BALANCE)
+ _initiateEscrowDeposit();
+
+ // Wait past unlock delay: escrow becomes unlockable
+ vm.warp(block.timestamp + cHub.ESCROW_DEPOSIT_UNLOCK_DELAY() + 1);
+
+ // Purge: DEPOSIT_AMOUNT flows from expired escrow to node vault
+ _expectEmitNodeBalanceUpdated(INITIAL_BALANCE + DEPOSIT_AMOUNT);
+ cHub.purgeEscrowDeposits(1);
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE + DEPOSIT_AMOUNT);
+ }
+
+ // ======== Tests: does NOT emit NodeBalanceUpdated ========
+
+ function test_noEmit_onCreateChannel_depositIntent_onlyUserDeposits() public {
+ // channel is created in _createSimpleChannel; re-verify logs from that call are cleared
+ vm.recordLogs();
+
+ _createSimpleChannel();
+
+ _assertNoEmitNodeBalanceUpdated();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_noEmit_onDepositToChannel_noNodeChange() public {
+ // Setup: simple channel, nodeNetFlow = 0
+ State memory prevState = _createSimpleChannel();
+
+ // Deposit: only user adds funds, nodeNetFlow stays at 0
+ State memory candidate = nextState(prevState, StateIntent.DEPOSIT, [DEPOSIT_AMOUNT * 2, uint256(0)], [int256(DEPOSIT_AMOUNT) * 2, int256(0)]);
+ candidate = mutualSignStateBothWithEcdsaValidator(candidate, channelId, ALICE_PK);
+
+ vm.recordLogs();
+ vm.prank(alice);
+ cHub.depositToChannel(channelId, candidate);
+ _assertNoEmitNodeBalanceUpdated();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_noEmit_onCheckpointChannel_noNodeChange() public {
+ // Setup: simple channel, nodeNetFlow = 0
+ State memory prevState = _createSimpleChannel();
+
+ // Checkpoint: nodeNetFlow stays at 0, userNetFlow unchanged (OPERATE requires userNfDelta == 0)
+ State memory candidate = nextState(prevState, StateIntent.OPERATE, [DEPOSIT_AMOUNT, uint256(0)], [int256(DEPOSIT_AMOUNT), int256(0)]);
+ candidate = mutualSignStateBothWithEcdsaValidator(candidate, channelId, ALICE_PK);
+
+ vm.recordLogs();
+ vm.prank(alice);
+ cHub.checkpointChannel(channelId, candidate);
+ _assertNoEmitNodeBalanceUpdated();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_noEmit_onInitiateEscrowDeposit_nonHome() public {
+ // Non-home chain initiate: only user funds move (userFundsDelta > 0, nodeFundsDelta = 0)
+ vm.recordLogs();
+ _initiateEscrowDeposit();
+ _assertNoEmitNodeBalanceUpdated();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_noEmit_onChallengeEscrowDeposit() public {
+ // Setup: bob deposits DEPOSIT_AMOUNT (node vault = INITIAL_BALANCE, no change)
+ (bytes32 escrowId, State memory initState) = _initiateEscrowDeposit();
+
+ bytes memory sig = signChallengeEip191WithEcdsaValidator(bobChannelId, initState, BOB_PK);
+
+ vm.recordLogs();
+ vm.prank(bob);
+ cHub.challengeEscrowDeposit(escrowId, sig, ParticipantIndex.USER);
+ _assertNoEmitNodeBalanceUpdated();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE);
+ }
+
+ function test_noEmit_onChallengeEscrowWithdrawal() public {
+ // Setup: node locks DEPOSIT_AMOUNT (vault = INITIAL_BALANCE - DEPOSIT_AMOUNT)
+ (bytes32 escrowId, State memory initState) = _initiateEscrowWithdrawal();
+
+ bytes memory sig = signChallengeEip191WithEcdsaValidator(bobChannelId, initState, BOB_PK);
+
+ vm.recordLogs();
+ vm.prank(bob);
+ cHub.challengeEscrowWithdrawal(escrowId, sig, ParticipantIndex.USER);
+ _assertNoEmitNodeBalanceUpdated();
+
+ assertEq(cHub.getAccountBalance(node, address(token), SUB_ID_0), INITIAL_BALANCE - DEPOSIT_AMOUNT);
+ }
+}
+// forge-lint: disable-end(unsafe-typecast)
diff --git a/contracts/test/ChannelHub_crosschain.lifecycle.t.sol b/contracts/test/ChannelHub_lifecycle/ChannelHub_crosschain.lifecycle.t.sol
similarity index 95%
rename from contracts/test/ChannelHub_crosschain.lifecycle.t.sol
rename to contracts/test/ChannelHub_lifecycle/ChannelHub_crosschain.lifecycle.t.sol
index 9dbfb9bbd..7a77ab324 100644
--- a/contracts/test/ChannelHub_crosschain.lifecycle.t.sol
+++ b/contracts/test/ChannelHub_lifecycle/ChannelHub_crosschain.lifecycle.t.sol
@@ -1,11 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
-import {ChannelHubTest_Base} from "./ChannelHub_Base.t.sol";
-import {MockERC20} from "./mocks/MockERC20.sol";
-
-import {Utils} from "../src/Utils.sol";
-import {State, ChannelDefinition, StateIntent, Ledger, ChannelStatus, EscrowStatus} from "../src/interfaces/Types.sol";
+import {ChannelHubTest_Base} from "../ChannelHub_Base.t.sol";
+import {MockERC20} from "../mocks/MockERC20.sol";
+
+import {Utils} from "../../src/Utils.sol";
+import {
+ State,
+ ChannelDefinition,
+ StateIntent,
+ Ledger,
+ ChannelStatus,
+ EscrowStatus
+} from "../../src/interfaces/Types.sol";
// forge-lint: disable-next-item(unsafe-typecast)
contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
@@ -109,7 +116,9 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
// Expected: user allocation = 958, user net flow = 1000, node allocation = 0, node net flow = -42
vm.prank(alice);
cHub.initiateEscrowDeposit(def, state);
- verifyChannelState(channelId, 958, 1000, 500, 458, "after cross chain deposit");
+ verifyChannelState(
+ channelId, [uint256(958), uint256(500)], [int256(1000), int256(458)], "after cross chain deposit"
+ );
// finalize escrow deposit
state = nextState(
@@ -141,7 +150,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
vm.prank(alice);
cHub.withdrawFromChannel(channelId, state);
- verifyChannelState(channelId, 1220, 750, 0, 470, "after withdrawal");
+ verifyChannelState(channelId, [uint256(1220), uint256(0)], [int256(750), int256(470)], "after withdrawal");
// Verify user balance after withdrawal (withdrew 250)
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 750, "User balance after withdrawal");
@@ -199,7 +208,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
// checkpoint on home chain
vm.prank(alice);
cHub.checkpointChannel(channelId, state);
- verifyChannelState(channelId, 477, 750, 0, -273, "after checkpoint");
+ verifyChannelState(channelId, [uint256(477), uint256(0)], [int256(750), int256(-273)], "after checkpoint");
// Verify user balance hasn't changed
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 750, "User balance after checkpoint");
@@ -258,7 +267,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
cHub.finalizeMigration(channelId, state);
// Verify channel is migrated out
- verifyChannelState(channelId, 0, 750, 0, -750, "after migration");
+ verifyChannelState(channelId, [uint256(0), uint256(0)], [int256(750), int256(-750)], "after migration");
// Verify user balance hasn't changed (migration doesn't move funds on home chain)
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 750, "User balance after migration");
@@ -339,7 +348,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
// escrow deposit locked funds should also be unlocked after `unlockAt` time passes alongside any other on-chain call
vm.warp(block.timestamp + cHub.ESCROW_DEPOSIT_UNLOCK_DELAY() + 1);
- uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token));
+ uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
// state from the "happyPath" test, but with home and nonHome states swapped
state = nextState(
@@ -356,12 +365,12 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK);
vm.prank(node);
- cHub.finalizeEscrowDeposit(escrowId, state);
+ cHub.finalizeEscrowDeposit(bobChannelId, escrowId, state);
// Verify user balance after deposit finalized has NOT changed
assertEq(token.balanceOf(bob), INITIAL_BALANCE - 500, "User balance after escrow deposit finalized");
- uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token));
+ uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token), SUB_ID_0);
assertEq(nodeBalanceAfter, nodeBalanceBefore + 500, "Node balance after escrow deposit finalized");
// Verify escrow struct is updated on ChannelsHub
@@ -452,7 +461,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
// ====== Finalize escrow deposit ======
vm.warp(block.timestamp + cHub.ESCROW_DEPOSIT_UNLOCK_DELAY() + 1);
- uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token14dec));
+ uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token14dec), SUB_ID_0);
// After finalization, home chain user allocation increases, non-home releases funds to node
state = nextState(
@@ -469,13 +478,13 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK);
vm.prank(node);
- cHub.finalizeEscrowDeposit(escrowId, state);
+ cHub.finalizeEscrowDeposit(bobChannelId, escrowId, state);
// Verify user balance after deposit finalized has NOT changed
assertEq(token14dec.balanceOf(bob), 990 * 1e14, "User balance after escrow deposit finalized");
// Verify node received the deposited tokens
- uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token14dec));
+ uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token14dec), SUB_ID_0);
assertEq(nodeBalanceAfter, nodeBalanceBefore + 10 * 1e14, "Node balance after escrow deposit finalized");
// Verify escrow struct is updated on ChannelsHub
@@ -493,7 +502,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
(ChannelStatus status,,,,) = cHub.getChannelData(bobChannelId);
assertEq(uint8(status), uint8(ChannelStatus.VOID), "Channel should be VOID on non-home chain");
- uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token));
+ uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
// state from the "happyPath" test, but with home and nonHome states swapped
State memory state = State({
@@ -533,7 +542,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
cHub.initiateEscrowWithdrawal(bobDef, state);
// Verify user node's after deposit (deposited 500)
- uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token));
+ uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token), SUB_ID_0);
assertEq(nodeBalanceAfter, nodeBalanceBefore - 750, "Node balance after escrow withdrawal");
// Verify escrow struct is updated on ChannelsHub: escrow data exists, `locked` equals to withdrawalAmount
@@ -560,7 +569,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK);
vm.prank(node);
- cHub.finalizeEscrowWithdrawal(escrowId, state);
+ cHub.finalizeEscrowWithdrawal(bobChannelId, escrowId, state);
// Verify user balance after withdrawal (withdrew 750)
uint256 bobBalanceAfter = token.balanceOf(bob);
@@ -587,10 +596,10 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
vm.startPrank(node);
token8dec.mint(node, 100 * 1e8);
token8dec.approve(address(cHub), 100 * 1e8);
- cHub.depositToVault(node, address(token8dec), 100 * 1e8);
+ cHub.depositToVault(node, address(token8dec), SUB_ID_0, 100 * 1e8);
vm.stopPrank();
- uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token8dec));
+ uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token8dec), SUB_ID_0);
// Bob wants to withdraw 5 tokens on non-home chain (5e8 with 8 decimals = 5e2 with 2 decimals)
State memory state = State({
@@ -631,7 +640,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
cHub.initiateEscrowWithdrawal(bobDef, state);
// Verify node locked the withdrawal amount
- uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token8dec));
+ uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token8dec), SUB_ID_0);
assertEq(nodeBalanceAfter, nodeBalanceBefore - 5 * 1e8, "Node balance after escrow withdrawal initiation");
// Verify escrow struct is created
@@ -660,7 +669,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
state = mutualSignStateBothWithEcdsaValidator(state, bobChannelId, BOB_PK);
vm.prank(node);
- cHub.finalizeEscrowWithdrawal(escrowId, state);
+ cHub.finalizeEscrowWithdrawal(bobChannelId, escrowId, state);
// Verify user received the withdrawal
uint256 bobBalanceAfter = token8dec.balanceOf(bob);
@@ -679,7 +688,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
(ChannelStatus status,,,,) = cHub.getChannelData(bobChannelId);
assertEq(uint8(status), uint8(ChannelStatus.VOID), "Channel should be VOID on non-home chain");
- uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token));
+ uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token), SUB_ID_0);
uint256 userBalanceBefore = token.balanceOf(bob);
// state from the "happyPath" test
@@ -714,7 +723,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
cHub.initiateMigration(bobDef, state);
// Verify node's balance after migration (should have locked 469)
- uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token));
+ uint256 nodeBalanceAfter = cHub.getAccountBalance(node, address(token), SUB_ID_0);
assertEq(nodeBalanceAfter, nodeBalanceBefore - 469, "Node balance after migration initiation");
// user balance should not have changed
@@ -773,7 +782,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
vm.prank(bob);
cHub.withdrawFromChannel(bobChannelId, state);
- verifyChannelState(bobChannelId, 61, -400, 0, 461, "after withdrawal");
+ verifyChannelState(bobChannelId, [uint256(61), uint256(0)], [int256(-400), int256(461)], "after withdrawal");
// Verify user balance after withdrawal (withdrew 400)
assertEq(token.balanceOf(bob), userBalanceBefore + 400, "User balance after withdrawal");
@@ -794,7 +803,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
vm.startPrank(node);
token10dec.mint(node, 100 * 1e10);
token10dec.approve(address(cHub), 100 * 1e10);
- cHub.depositToVault(node, address(token10dec), 100 * 1e10);
+ cHub.depositToVault(node, address(token10dec), SUB_ID_0, 100 * 1e10);
vm.stopPrank();
// 1. Create Channel with 10-decimal token on Old Home Chain
@@ -848,7 +857,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base {
vm.startPrank(node);
token14dec.mint(node, 100 * 1e14);
token14dec.approve(address(cHub), 100 * 1e14);
- cHub.depositToVault(node, address(token14dec), 100 * 1e14);
+ cHub.depositToVault(node, address(token14dec), SUB_ID_0, 100 * 1e14);
vm.stopPrank();
// Initiate migration: Old home has 45 tokens (45e10 with 10 decimals)
diff --git a/contracts/test/ChannelHub_singlechain.lifecycle.t.sol b/contracts/test/ChannelHub_lifecycle/ChannelHub_singlechain.lifecycle.t.sol
similarity index 92%
rename from contracts/test/ChannelHub_singlechain.lifecycle.t.sol
rename to contracts/test/ChannelHub_lifecycle/ChannelHub_singlechain.lifecycle.t.sol
index 28e270261..66b1f186b 100644
--- a/contracts/test/ChannelHub_singlechain.lifecycle.t.sol
+++ b/contracts/test/ChannelHub_lifecycle/ChannelHub_singlechain.lifecycle.t.sol
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
-import {ChannelHubTest_Base} from "./ChannelHub_Base.t.sol";
+import {ChannelHubTest_Base} from "../ChannelHub_Base.t.sol";
-import {Utils} from "../src/Utils.sol";
-import {State, ChannelDefinition, StateIntent, Ledger, ChannelStatus} from "../src/interfaces/Types.sol";
-import {SessionKeyAuthorization} from "../src/sigValidators/SessionKeyValidator.sol";
-import {TestUtils, SESSION_KEY_VALIDATOR_ID} from "./TestUtils.sol";
+import {Utils} from "../../src/Utils.sol";
+import {State, ChannelDefinition, StateIntent, Ledger, ChannelStatus} from "../../src/interfaces/Types.sol";
+import {SessionKeyAuthorization} from "../../src/sigValidators/SessionKeyValidator.sol";
+import {TestUtils, SESSION_KEY_VALIDATOR_ID} from "../TestUtils.sol";
contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
function test_happyPath() public {
@@ -81,7 +81,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
// Expected: user allocation = 958, user net flow = 1000, node allocation = 0, node net flow = -42
vm.prank(alice);
cHub.checkpointChannel(channelId, state);
- verifyChannelState(channelId, 958, 1000, 0, -42, "after checkpoint");
+ verifyChannelState(channelId, [uint256(958), uint256(0)], [int256(1000), int256(-42)], "after checkpoint");
// receive 24 (allocation increases by 24, node net flow increases by 24)
state = nextState(state, StateIntent.OPERATE, [uint256(982), uint256(0)], [int256(1000), int256(-18)]);
@@ -95,7 +95,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
vm.prank(alice);
cHub.depositToChannel(channelId, state);
- verifyChannelState(channelId, 1482, 1500, 0, -18, "after deposit");
+ verifyChannelState(channelId, [uint256(1482), uint256(0)], [int256(1500), int256(-18)], "after deposit");
// Verify user balance after first deposit (deposited 500 more)
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1500, "User balance after first deposit");
@@ -116,7 +116,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
vm.prank(alice);
cHub.withdrawFromChannel(channelId, state);
- verifyChannelState(channelId, 1379, 1400, 0, -21, "after withdrawal");
+ verifyChannelState(channelId, [uint256(1379), uint256(0)], [int256(1400), int256(-21)], "after withdrawal");
// Verify user balance after first withdrawal (withdrew 100)
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1400, "User balance after first withdrawal");
@@ -137,7 +137,7 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
vm.prank(alice);
cHub.depositToChannel(channelId, state);
- verifyChannelState(channelId, 1586, 1600, 0, -14, "after second deposit");
+ verifyChannelState(channelId, [uint256(1586), uint256(0)], [int256(1600), int256(-14)], "after second deposit");
// Verify user balance after second deposit (deposited 200 more)
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1600, "User balance after second deposit");
@@ -170,7 +170,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
vm.prank(alice);
cHub.withdrawFromChannel(channelId, state);
- verifyChannelState(channelId, 1289, 1300, 0, -11, "after second withdrawal");
+ verifyChannelState(
+ channelId, [uint256(1289), uint256(0)], [int256(1300), int256(-11)], "after second withdrawal"
+ );
// Verify user balance after second withdrawal (withdrew 300)
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 1300, "User balance after second withdrawal");
@@ -259,7 +261,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
cHub.createChannel(def, state);
assertEq(token.balanceOf(alice), INITIAL_BALANCE, "User balance after OPERATE creation stays the same");
- verifyChannelState(channelId, 1000, 0, 0, 1000, "after create with OPERATE intent");
+ verifyChannelState(
+ channelId, [uint256(1000), uint256(0)], [int256(0), int256(1000)], "after create with OPERATE intent"
+ );
(ChannelStatus status,, State memory latestState,, uint256 lockedFunds) = cHub.getChannelData(channelId);
assertEq(
uint8(status), uint8(ChannelStatus.OPERATING), "Channel created with OPERATE intent should be OPERATING"
@@ -324,7 +328,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
cHub.createChannel(def, state);
assertEq(token.balanceOf(alice), INITIAL_BALANCE - 500, "User balance after DEPOSIT creation decreases");
- verifyChannelState(channelId, 1500, 500, 0, 1000, "after create with DEPOSIT intent");
+ verifyChannelState(
+ channelId, [uint256(1500), uint256(0)], [int256(500), int256(1000)], "after create with DEPOSIT intent"
+ );
(ChannelStatus status,, State memory latestState,, uint256 lockedFunds) = cHub.getChannelData(channelId);
assertEq(
uint8(status), uint8(ChannelStatus.OPERATING), "Channel created with DEPOSIT intent should be OPERATING"
@@ -389,7 +395,9 @@ contract ChannelHubTest_SingleChain_Lifecycle is ChannelHubTest_Base {
cHub.createChannel(def, state);
assertEq(token.balanceOf(alice), INITIAL_BALANCE + 500, "User balance after WITHDRAW creation increases");
- verifyChannelState(channelId, 500, -500, 0, 1000, "after create with WITHDRAW intent");
+ verifyChannelState(
+ channelId, [uint256(500), uint256(0)], [int256(-500), int256(1000)], "after create with WITHDRAW intent"
+ );
(ChannelStatus status,, State memory latestState,, uint256 lockedFunds) = cHub.getChannelData(channelId);
assertEq(
uint8(status), uint8(ChannelStatus.OPERATING), "Channel created with WITHDRAW intent should be OPERATING"
diff --git a/contracts/test/ChannelHub_pushFunds.t.sol b/contracts/test/ChannelHub_pushFunds.t.sol
index 8c0f8fac3..576f4c5c7 100644
--- a/contracts/test/ChannelHub_pushFunds.t.sol
+++ b/contracts/test/ChannelHub_pushFunds.t.sol
@@ -38,6 +38,8 @@ contract ChannelHubTest_pushFunds is Test {
address public recipient;
+ uint48 constant SUB_ID_0 = 0;
+
uint256 constant TRANSFER_AMOUNT = 1000 ether;
uint256 constant BALANCE_AMOUNT = TRANSFER_AMOUNT * 10;
@@ -122,7 +124,7 @@ contract ChannelHubTest_pushFunds is Test {
function test_accumulatesReclaims_whenERC20Reverts() public {
vm.expectEmit(true, true, false, true);
- emit ChannelHub.TransferFailed(recipient, address(revertingToken), TRANSFER_AMOUNT);
+ emit ChannelHub.TransferFailed(SUB_ID_0, recipient, address(revertingToken), TRANSFER_AMOUNT);
cHub.exposed_pushFunds(recipient, address(revertingToken), TRANSFER_AMOUNT);
@@ -141,7 +143,7 @@ contract ChannelHubTest_pushFunds is Test {
function test_accumulatesReclaims_whenERC20ConsumesAllGas() public {
vm.expectEmit(true, true, false, true);
- emit ChannelHub.TransferFailed(recipient, address(gasConsumingToken), TRANSFER_AMOUNT);
+ emit ChannelHub.TransferFailed(SUB_ID_0, recipient, address(gasConsumingToken), TRANSFER_AMOUNT);
cHub.exposed_pushFunds(recipient, address(gasConsumingToken), TRANSFER_AMOUNT);
@@ -152,7 +154,7 @@ contract ChannelHubTest_pushFunds is Test {
function test_accumulatesReclaims_whenERC20ReturnsMalformedData() public {
vm.expectEmit(true, true, false, true);
- emit ChannelHub.TransferFailed(recipient, address(malformedToken), TRANSFER_AMOUNT);
+ emit ChannelHub.TransferFailed(SUB_ID_0, recipient, address(malformedToken), TRANSFER_AMOUNT);
cHub.exposed_pushFunds(recipient, address(malformedToken), TRANSFER_AMOUNT);
@@ -177,7 +179,7 @@ contract ChannelHubTest_pushFunds is Test {
function test_accumulatesReclaims_whenETHReceiverReverts() public {
vm.expectEmit(true, true, false, true);
- emit ChannelHub.TransferFailed(address(revertingReceiver), address(0), TRANSFER_AMOUNT);
+ emit ChannelHub.TransferFailed(SUB_ID_0, address(revertingReceiver), address(0), TRANSFER_AMOUNT);
cHub.exposed_pushFunds(address(revertingReceiver), address(0), TRANSFER_AMOUNT);
@@ -188,7 +190,7 @@ contract ChannelHubTest_pushFunds is Test {
function test_accumulatesReclaims_whenETHReceiverConsumesAllGas() public {
vm.expectEmit(true, true, false, true);
- emit ChannelHub.TransferFailed(address(gasConsumingReceiver), address(0), TRANSFER_AMOUNT);
+ emit ChannelHub.TransferFailed(SUB_ID_0, address(gasConsumingReceiver), address(0), TRANSFER_AMOUNT);
cHub.exposed_pushFunds(address(gasConsumingReceiver), address(0), TRANSFER_AMOUNT);
diff --git a/contracts/test/ParametricToken/ParametricToken.t.sol b/contracts/test/ParametricToken/ParametricToken.t.sol
new file mode 100644
index 000000000..753ae99c1
--- /dev/null
+++ b/contracts/test/ParametricToken/ParametricToken.t.sol
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.30;
+
+import "forge-std/Test.sol";
+import "../../src/ParametricToken.sol";
+
+contract ParametricTokenTest is Test {
+ ParametricToken public token;
+ address public alice = address(0x1);
+ address public bob = address(0x2);
+ address public charlie = address(0x3);
+
+ uint48 constant SUBID0 = 0;
+ uint48 constant SUBID1 = 1;
+ uint48 constant SUBID10 = 1000;
+
+ function setUp() public {
+ token = new ParametricToken("Shortbit", "sBTC");
+
+ // Alice mint
+ vm.prank(alice);
+ vm.warp(1_000_000);
+ token.mint(1000 ether);
+ console.log("Alice parameter", token.parameterOf(0, alice));
+ vm.stopPrank();
+
+ // Bob mint
+ vm.prank(bob);
+ vm.warp(2_000_000);
+ token.mint(400 ether);
+ console.log("Bob parameter after mint 1", token.parameterOf(0, bob));
+
+ // Bob mint 2
+ vm.prank(bob);
+ vm.warp(4_000_000);
+ token.mint(100 ether);
+ console.log("Bob parameter after mint 2", token.parameterOf(0, bob));
+
+ assertEq(token.parameterOf(0, bob), 2_400_000);
+ }
+
+ function testNormalTransfer() public {
+ vm.prank(alice);
+ token.transfer(bob, 100 ether);
+
+ assertEq(token.balanceOf(alice), 900 ether);
+ assertEq(token.balanceOf(bob), 600 ether);
+ }
+
+ function testConvertToSuper() public {
+ vm.prank(alice);
+ token.convertToSuper(alice);
+
+ assertEq(uint8(token.accountType(alice)), uint8(IParametricToken.AccountType.Super));
+ assertEq(token.balanceOf(alice), 1000 ether);
+ assertEq(token.balanceOfSub(alice, 0), 1000 ether);
+ }
+
+ function testCreateSubAccount() public {
+ vm.startPrank(alice);
+ token.convertToSuper(alice);
+
+ uint48 subId = token.createSubAccount(alice);
+ // SubId 3 doesn't exist, should revert
+ vm.expectRevert("Sub-account doesn't exist");
+ token.balanceOfSub(alice, 3);
+ vm.stopPrank();
+
+ assertEq(subId, 1);
+ assertEq(token.subsCountOf(alice), 2);
+ assertEq(token.balanceOfSub(alice, subId), 0);
+ }
+
+ function testTransferToSub() public {
+ // Setup
+ vm.startPrank(alice);
+ token.convertToSuper(alice);
+ uint48 subId = token.createSubAccount(alice);
+ vm.stopPrank();
+
+ // Bob transfers to alice's sub-account 0
+ vm.startPrank(bob);
+ token.transferToSub(alice, 0, 100 ether);
+ token.transferToSub(alice, subId, 50 ether);
+ vm.stopPrank();
+
+ assertEq(token.balanceOfSub(alice, 0), 1100 ether);
+ assertEq(token.balanceOfSub(alice, subId), 50 ether);
+ assertEq(token.balanceOf(alice), 1150 ether);
+ assertEq(token.balanceOf(bob), 350 ether);
+
+ console.log("Alice param sub 0", token.parameterOfSub(0, alice, 0));
+ console.log("Alice param sub", subId, token.parameterOfSub(0, alice, subId));
+ }
+
+ function testTransferFromSub() public {
+ // Setup: alice becomes super, creates sub, funds it
+ vm.startPrank(alice);
+ token.convertToSuper(alice);
+ uint48 subId = token.createSubAccount(alice);
+ token.transferBetweenSubs(0, subId, 200 ether);
+
+ // Transfer from sub to bob
+ token.transferFromSub(subId, bob, 150 ether);
+ vm.stopPrank();
+
+ assertEq(token.balanceOfSub(alice, subId), 50 ether);
+ assertEq(token.balanceOf(bob), 650 ether);
+
+ console.log("Alice param sub 0", token.parameterOfSub(0, alice, 0));
+ console.log("Alice param sub", subId, token.parameterOfSub(0, alice, subId));
+ console.log("Bob param", token.parameterOf(0, bob));
+ }
+}
diff --git a/contracts/test/TestChannelHub.sol b/contracts/test/TestChannelHub.sol
index b27491b86..ce346b8e8 100644
--- a/contracts/test/TestChannelHub.sol
+++ b/contracts/test/TestChannelHub.sol
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-pragma solidity 0.8.30;
+pragma solidity ^0.8.30;
import {ChannelHub} from "../src/ChannelHub.sol";
import {ISignatureValidator} from "../src/interfaces/ISignatureValidator.sol";
@@ -9,6 +9,8 @@ import {ISignatureValidator} from "../src/interfaces/ISignatureValidator.sol";
* @notice Test harness contract that exposes internal ChannelHub functions for testing
*/
contract TestChannelHub is ChannelHub {
+ uint48 constant SUB_ID = 0;
+
constructor(ISignatureValidator _defaultSigValidator) ChannelHub(_defaultSigValidator) {}
/**
@@ -21,14 +23,14 @@ contract TestChannelHub is ChannelHub {
* @notice Exposed version of _pushFunds for testing
*/
function exposed_pushFunds(address to, address token, uint256 amount) external payable {
- _pushFunds(to, token, amount);
+ _pushFunds(SUB_ID, to, token, amount);
}
/**
* @notice Exposed version of _pullFunds for testing
*/
function exposed_pullFunds(address from, address token, uint256 amount) external payable {
- _pullFunds(from, token, amount);
+ _pullFunds(from, SUB_ID, token, amount);
}
/**
diff --git a/docs/README.md b/docs/README.md
index b05548aa3..e12b36c42 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,76 +1,68 @@
# Nitrolite V1 Clearnode Specifications
-This directory introduces new Clearnode architecture, models and communication flows to facilitate communication between user, SDK client, Node and Blockchains that will become the core off-chain engine for the Nitrolite V1 Protocol.
+This directory contains Clearnode architecture, models and communication flows that facilitate communication between user, SDK client, Node and Blockchains — the core off-chain engine for the Nitrolite V1 Protocol.
## Contents
- **[api.yaml](api.yaml)** - API definitions including types, state transitions, and RPC methods
- **[data_models.mmd](data_models.mmd)** - Data model diagrams
-- **[rpc_message.md](rpc_message.md)** - Standardized RPC message format for communication with a Clearnode via WebSocket
### Communication Flows
-- **[transfer.mmd](communication_flows/transfer.mmd)** - Off-chain transfer flow
-- **[app_session_deposit.mmd](communication_flows/app_session_deposit.mmd)** - Application session deposit
-- **[escrow_chan_deposit.mmd](communication_flows/escrow_chan_deposit.mmd)** - Escrow channel deposit
-- **[escrow_chan_withdrawal.mmd](communication_flows/escrow_chan_withdrawal.mmd)** - Escrow channel withdrawal
-- **[home_chan_creation_from_scratch.mmd](communication_flows/home_chan_creation_from_scratch.mmd)** - Home channel creation
-- **[home_chan_withdraw.mmd](communication_flows/home_chan_withdraw.mmd)** - Home channel withdrawal
-- **[home_chan_withdraw_on_create_from_state.mmd](communication_flows/home_chan_withdraw_on_create_from_state.mmd)** - State-based channel creation with withdrawal
+- **[home_chan_creation_from_scratch.mmd](communication_flows/home_chan_creation_from_scratch.mmd)** - Home channel creation with initial deposit
+- **[home_chan_deposit.mmd](communication_flows/home_chan_deposit.mmd)** - Home channel deposit (existing channel)
+- **[home_chan_withdraw.mmd](communication_flows/home_chan_withdraw.mmd)** - Home channel withdrawal (existing channel)
+- **[home_chan_withdraw_on_create_from_state.mmd](communication_flows/home_chan_withdraw_on_create_from_state.mmd)** - Channel creation with withdrawal from pending state
+- **[transfer.mmd](communication_flows/transfer.mmd)** - Off-chain transfer (sender + automatic receiver state creation)
+- **[app_session_deposit.mmd](communication_flows/app_session_deposit.mmd)** - Application session deposit with quorum verification
+- **[escrow_chan_deposit.mmd](communication_flows/escrow_chan_deposit.mmd)** - Cross-chain escrow deposit (mutual lock → on-chain → finalize)
+- **[escrow_chan_withdrawal.mmd](communication_flows/escrow_chan_withdrawal.mmd)** - Cross-chain escrow withdrawal (escrow lock → on-chain → finalize)
#### Remaining Flows
-The following communication flows are not yet documented but will be added in future iterations:
+The following communication flows are not yet documented:
-- **Remaining app session endpoints** are not affected and will be added here later. The only new requirement includes creating app sessions with 0 allocations, and participants depositing one by one. Now app session deposits are limited to one participant deposit at a time.
-
-- **home channel deposit** - Similar to home channel creation with deposit, but for existing channels
- **home chain migration** - Cross-chain state migration between home channels
-- **off-chain transfer to a non-existing user** - Handles receiver account creation during transfer
+- **app session create / operate / withdraw / close** - Full app session lifecycle beyond deposits
---
-**Note:** This directory contains ongoing work on Nitrolite V1 protocol architecture.
-
## Project Structure
-The following is a suggested project structure that may change as the implementation evolves:
-
-```t
-cerebro/
+```text
+cerebro/ # Cerebro Testing Client
clearnode/
- api/ # AppSessionService
- app_session/
- channel/
- user/
- node/
+ action_gateway/ # Rate limiting via gated actions
+ api/
+ app_session_v1/ # App session endpoints (create, deposit, operate, withdraw, close)
+ apps_v1/ # Application registry endpoints
+ channel_v1/ # Channel endpoints (create, submit_state, get_state, transfer)
+ node_v1/ # Node info endpoints
+ user_v1/ # User endpoints (balances, staking)
config/
- migrations/ # database migration files
- postgres/
- sqlite/
- metric/
- prometheus/ # Prometheus metrics exporter
+ migrations/
+ postgres/ # Goose SQL migrations (embedded at compile time)
+ event_handlers/ # Blockchain event processing (channel events, locking events)
+ metrics/ # Prometheus metrics + lifespan metric aggregation
store/
- db/ # struct Database implements Store interface
- memory/ # may include in-memory store for Asset's, Blockchain's etc.
- blockchain_worker.go # service: BlockchainWorker, BWStore
- config.go
- event_handler.go # service: EventHandler
- eth_listener.go # service: SmartContractListener, SCLStore (TBD)
- main.go # 1st - monolithic clearnode implementation; then - refactor into microservices
- rpc_router.go # RPC Router binding RPC methods to handlers
-contract/
-docs/
+ database/ # GORM-based DB store
+ memory/ # In-memory store for assets, blockchains, config
+ blockchain_worker.go # Processes pending BlockchainAction records
+ runtime.go # Embeds migrations, initializes services
+ main.go # Entry point, EVM listeners, metric exporters
+contracts/ # Smart contracts (ChannelHub, Locking, etc.)
+docs/ # This directory
pkg/
- amm/
- app_session/
+ app/ # App session types (AppSessionStatus, quorum, allocations)
blockchain/
- evm/ # Client implementations for EVM-based blockchains
- core/ # Client interface (Create, Checkpoint, Challenge etc.), PackState, UnpackState, TransitionValidator, functions related to State build
- rpc/ # Node, Client, Requests, Responses, Events, Errors
+ evm/ # EVM client implementations
+ core/ # Core types: Channel, State, Transaction, Signer, Transition
+ log/ # Structured logging
+ rpc/ # RPC protocol: messages, requests, responses, errors
+ sign/ # Signer implementations (EthereumMsgSigner, EthereumRawSigner)
sdk/
- go/
- ts/ # should include implementations for everything inside /pkg/
-test/ # integration test scenarios executed by all SDKs inside sdk/ directory
-go.mod
+ go/ # Go SDK client
+ ts/ # TypeScript SDK client
+ ts-compat/ # TypeScript compatibility SDK
+test/ # Integration test scenarios
```
diff --git a/docs/api.yaml b/docs/api.yaml
index 886019203..6de9fe49b 100644
--- a/docs/api.yaml
+++ b/docs/api.yaml
@@ -156,7 +156,7 @@ types:
- app_definition:
description: Definition for an app session
fields:
- - name: application
+ - name: application_id
type: string
description: Application identifier from an app registry
- name: participants
@@ -338,24 +338,16 @@ types:
- name: status
type: string
description: Session status (open/closed)
- - name: participants
- type: array
- items:
- type: app_participant
- description: List of participant wallet addresses with weights
+ - name: app_definition
+ type: app_definition
+ description: The application definition for this session
- name: session_data
type: string
description: JSON stringified session data
optional: true
- - name: quorum
- type: integer
- description: Quorum required for operations
- name: version
type: string
description: Current version of the session state
- - name: nonce
- type: string
- description: Nonce for the session
- name: allocations
type: array
items:
@@ -415,6 +407,66 @@ types:
type: string
description: User's signature over the session key metadata to authorize the registration/update of the session key
+ - app:
+ description: Application definition
+ fields:
+ - name: id
+ type: string
+ description: Application identifier
+ - name: owner_wallet
+ type: string
+ description: Owner's wallet address
+ - name: metadata
+ type: string
+ description: Application metadata (bytes32 hash)
+ - name: version
+ type: string
+ description: Current version of the application
+ - name: creation_approval_not_required
+ type: boolean
+ description: Whether app sessions can be created without owner approval
+
+ - app_info:
+ description: Full application info
+ fields:
+ - name: id
+ type: string
+ description: Application identifier
+ - name: owner_wallet
+ type: string
+ description: Owner's wallet address
+ - name: metadata
+ type: string
+ description: Application metadata (bytes32 hash)
+ - name: version
+ type: string
+ description: Current version of the application
+ - name: creation_approval_not_required
+ type: boolean
+ description: Whether app sessions can be created without owner approval
+ - name: created_at
+ type: string
+ description: Creation timestamp (unix seconds)
+ - name: updated_at
+ type: string
+ description: Last update timestamp (unix seconds)
+
+ - action_allowance:
+ description: Allowance information for a specific gated action
+ fields:
+ - name: gated_action
+ type: string
+ description: The specific action being gated (transfer, app_session_deposit, app_session_operation, app_session_withdrawal)
+ - name: time_window
+ type: string
+ description: Time window for which the allowance is valid (e.g. "24h0m0s")
+ - name: allowance
+ type: string
+ description: Total allowance for the action within the time window
+ - name: used
+ type: string
+ description: Amount already used within the time window
+
- pagination_params:
description: Pagination request parameters
fields:
@@ -760,7 +812,7 @@ api:
- message: invalid_parameters
description: The request parameters are invalid
- name: create_app_session
- description: Create a new application session between participants
+ description: Create a new application session between participants. The application must be registered in the app registry. If the application requires creation approval (creation_approval_not_required is false), an owner signature is required.
request:
- field_name: definition
type: app_definition
@@ -773,6 +825,9 @@ api:
description: Participant signatures for the app session creation
items:
type: string
+ - field_name: owner_sig
+ type: string
+ description: Owner signature for app session creation, required when the application's creation_approval_not_required is false
optional: true
response:
- field_name: app_session_id
@@ -787,6 +842,12 @@ api:
errors:
- message: invalid_definition
description: The application definition is invalid
+ - message: application_not_registered
+ description: The application is not registered in the app registry
+ - message: owner_sig_required
+ description: Owner signature is required for this application (creation_approval_not_required is false)
+ - message: invalid_owner_signature
+ description: The owner signature is invalid or does not match the registered app owner
- message: insufficient_balance
description: Participant has insufficient balance for allocations
- name: submit_session_key_state
@@ -819,6 +880,58 @@ api:
- message: account_not_found
description: The specified account was not found
+ - name: apps
+ description: Operations related to application registry management
+ versions:
+ - version: v1
+ methods:
+ - name: get_apps
+ description: Retrieve registered applications with optional filtering by app ID and owner wallet
+ request:
+ - field_name: app_id
+ type: string
+ description: Filter by application ID
+ optional: true
+ - field_name: owner_wallet
+ type: string
+ description: Filter by owner wallet address
+ optional: true
+ - field_name: pagination
+ type: pagination_params
+ description: Pagination parameters (offset, limit, sort)
+ optional: true
+ response:
+ - field_name: apps
+ type: array
+ items:
+ type: app_info
+ description: List of registered applications
+ - field_name: metadata
+ type: pagination_metadata
+ description: Pagination information
+ errors:
+ - message: invalid_parameters
+ description: The request parameters are invalid
+ - name: submit_app_version
+ description: Register a new application in the app registry. Currently only version 1 (creation) is supported. The owner must sign the packed app data to prove ownership.
+ request:
+ - field_name: app
+ type: app
+ description: Application definition including ID, owner wallet, metadata, version, and creation approval flag
+ - field_name: owner_sig
+ type: string
+ description: Owner's EIP-191 signature over the packed application data
+ response: []
+ errors:
+ - message: invalid_app_id
+ description: The application ID does not match the required format
+ - message: invalid_version
+ description: Only version 1 (creation) is currently supported
+ - message: invalid_signature
+ description: The owner signature is invalid
+ - message: app_already_exists
+ description: An application with this ID already exists
+
- name: session_keys
description: Operations related to session key management
versions:
@@ -879,6 +992,23 @@ api:
type: pagination_metadata
description: Pagination information
optional: true
+ - name: get_action_allowances
+ description: Retrieve action allowances for a user based on their staking level
+ request:
+ - field_name: wallet
+ type: string
+ description: User's wallet address
+ response:
+ - field_name: allowances
+ type: array
+ items:
+ type: action_allowance
+ description: List of action allowances
+ errors:
+ - message: wallet_required
+ description: The wallet address is required
+ - message: retrieval_failed
+ description: Failed to retrieve action allowances
- name: node
description: Utility methods to get node's configuration and check connectivity
diff --git a/docs/communication_flows/app_session_deposit.mmd b/docs/communication_flows/app_session_deposit.mmd
index a8f7cf71e..fe724e339 100644
--- a/docs/communication_flows/app_session_deposit.mmd
+++ b/docs/communication_flows/app_session_deposit.mmd
@@ -2,49 +2,48 @@ sequenceDiagram
actor User
actor SenderClient
actor Node
-
+
Note over SenderClient: Connected to Node
- Note over Node: Contains user's state with Home Chain
-
- %% {
- %% newAppState {
- %% app_session_id: appSessionId as Hex,
- %% intentDeposit,
- %% version,
- %% allocations,
- %% session_data: JSON.stringify(sessionData),
- %% }
- %% sigQuorum: []
- %% NewUserState
- %% }
-
- User->>SenderClient: submit_app_state(newAppState,sigQuorum)
+ Note over Node: Contains user's state with Home Chain
+
+ User->>SenderClient: submit_app_state(newAppState, sigQuorum)
SenderClient->>Node: GetAppSessionState(app_session_id)
- Node->>SenderClient: Returns an actual app session state
+ Node->>SenderClient: Returns current app session state
SenderClient-->>SenderClient: ValidateSessionAppState(currentAppState, newAppState, sigQuorum)
- Note right of SenderClient: intent=deposit, userWallet=appParticipant, only the participant deposits, validate quorum
+ Note right of SenderClient: intent=deposit, userWallet=appParticipant, validate quorum
SenderClient->>Node: GetLastState(UserWallet, asset)
- Note right of Node: GetLastState(userWallet, asset)
Node->>SenderClient: Returns a state with home chain
- %% Note over SenderClient: Check current state transitions
Note over SenderClient: createNextState(currentState) returns state
- Note over SenderClient: state.setID(CalculateStateID(state.userWallet, state.asset, cycleId, state.version))
+ Note over SenderClient: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
Note over SenderClient: NewTransition(commitT, state.ID(), appSessionId, amount)
Note over SenderClient: state.applyTransitions(transitions) returns true
- Note over SenderClient: signState(state) returns userSig
+ Note over SenderClient: signState(state) returns userSig (prepends signer type byte)
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)
-
- Node->>SenderClient: Sends AppSessionUpdate
- Node->>SenderClient: Return node signature
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: GetAppSession(appSessionId) → validate exists, open, version matches
+ Note over Node: Verify intent == Deposit
+ Note over Node: GetRegisteredApp(applicationId) → validate exists
+ Note over Node: ActionGateway.AllowAction(appOwner, GatedActionAppSessionDeposit)
+ Note over Node: GetLastUserState(userWallet, asset) → currentState
+ Note over Node: CheckOpenChannel(userWallet, asset) → approvedSigValidators
+ Note over Node: EnsureNoOngoingStateTransitions(userWallet, asset)
+ Note over Node: ValidateStateAdvancement(currentState, incomingState)
+ Note over Node: PackState(incomingState) → packedState
+ Note over Node: Extract signer type from userSig (0x00=default, 0x01=session key)
+ Note over Node: ChannelSigValidator.Verify(userWallet, packedState, userSig)
+ Note over Node: VerifyQuorum(packedAppState, sigQuorum, participants)
+ Note over Node: Validate allocations: no negatives, valid participants, asset match
+ Note over Node: RecordLedgerEntry(participant, sessionID, asset, depositAmount)
+ Note over Node: Verify total deposit == state transition amount
+ Note over Node: nodeSigner.Sign(packedState) → nodeSig
+ Note over Node: StoreUserState(incomingState) → also updates UserBalance
+ Note over Node: NewTransactionFromTransition(state, transition) → RecordTransaction
+ Note over Node: Update app session version + store
+
+ Node->>SenderClient: Return node signature (StateNodeSig)
SenderClient->>User: Returns success & tx hash
diff --git a/docs/communication_flows/escrow_chan_deposit.mmd b/docs/communication_flows/escrow_chan_deposit.mmd
index 69cb8a369..7f721d501 100644
--- a/docs/communication_flows/escrow_chan_deposit.mmd
+++ b/docs/communication_flows/escrow_chan_deposit.mmd
@@ -5,69 +5,83 @@ sequenceDiagram
actor HomeChain
actor EscrowChain
Note over HomeChain: User already has a home channel
- Note over Node: Contains user's state with Home Chain
+ Note over Node: Contains user's state with Home Chain
Note right of Client: Connected to Node
+
User->>Client: async deposit(blockchainId, asset, amount)
+ %% Phase 1: Mutual Lock - lock funds from home to escrow
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: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, 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: NewTransition(TransitionTypeMutualLock, state.ID(), homeChannelID, amount)
Note over Client: state.applyTransitions(transitions) returns true
- Note over Client: signState(state) returns userSig
+ Note over Client: signState(state) returns userSig (prepends signer type byte)
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)
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: CheckOpenChannel(userWallet, asset) → approvedSigValidators
+ Note over Node: GetLastUserState(userWallet, asset) → currentState
+ Note over Node: EnsureNoOngoingStateTransitions(userWallet, asset)
+ Note over Node: ValidateStateAdvancement(currentState, incomingState)
+ Note over Node: PackState → verify userSig → nodeSigner.Sign(packedState)
+ Note over Node: StoreEscrowChannel(escrow_channel)
+ Note over Node: RecordTransaction → StoreUserState(state)
Node->>Client: Return node signature
+
+ %% Phase 2: On-chain escrow deposit initiation
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)
+ Note over Node: HandleEscrowDepositInitiated()
+ Note over Node: escrowChannel.StateVersion = event.StateVersion
+ Note over Node: escrowChannel.Status = Open
+ Note over Node: ScheduleInitiateEscrowDeposit(state.ID, blockchainId)
+ Note over Node: UpdateChannel(escrowChannel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
-
+
+ %% Phase 3: Node checkpoints on home chain
Node->>HomeChain: checkpoint(homeChannelId, packedState)
- HomeChain-->>Node: Emits Checkpointed Event
- Node-->>Node: HandleCheckpointed()
+ HomeChain-->>Node: Emits HomeChannelCheckpointed Event
+ Note over Node: HandleHomeChannelCheckpointed()
+ Note over Node: channel.StateVersion = event.StateVersion
+ Note over Node: UpdateChannel(channel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
-
+
+ %% Phase 4: Escrow deposit finalization - credit home from escrow
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.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
+ Note over Client: NewTransition(TransitionTypeEscrowDeposit, state.ID(), homeChannelID, amount)
Note over Client: state.applyTransitions(transitions) returns true
Note over Client: signState(state) returns userSig
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)
+ Note over Node: LockUserState → CheckOpenChannel → GetLastUserState
+ Note over Node: EnsureNoOngoingStateTransitions
+ Note over Node: ValidateStateAdvancement → PackState → verify sig → node sign
+ Note over Node: RecordTransaction → StoreUserState(state)
Node->>Client: Return node signature
Client-->>User: Returns success
- Note over Node: Escrowed funds would be released automatically after lock period
- Note over Node: If fast unlock is needed, the node can checkpoint on escrow channel.
+ %% Phase 5: Node finalizes escrow on-chain
+ Note over Node: Escrowed funds released automatically after lock period
+ Note over Node: If fast unlock needed, node can checkpoint on escrow channel
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)
+ Note over Node: HandleEscrowDepositFinalized()
+ Note over Node: escrowChannel.StateVersion = event.StateVersion
+ Note over Node: escrowChannel.Status = Closed
+ Note over Node: UpdateChannel(escrowChannel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
-
\ No newline at end of file
diff --git a/docs/communication_flows/escrow_chan_withdrawal.mmd b/docs/communication_flows/escrow_chan_withdrawal.mmd
index cec96b3b4..479d01dad 100644
--- a/docs/communication_flows/escrow_chan_withdrawal.mmd
+++ b/docs/communication_flows/escrow_chan_withdrawal.mmd
@@ -5,58 +5,68 @@ sequenceDiagram
actor HomeChain
actor EscrowChain
Note over HomeChain: User already has a home channel
- Note over Node: Contains user's state with Home Chain
+ Note over Node: Contains user's state with Home Chain
Note right of Client: Connected to Node
+
User->>Client: async withdraw(blockchainId, asset, amount)
+ %% Phase 1: Escrow Lock - lock funds for withdrawal
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: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, 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: NewTransition(TransitionTypeEscrowLock, state.ID(), escrowChannelID, amount)
Note over Client: state.applyTransitions(transitions) returns true
- Note over Client: signState(state) returns userSig
+ Note over Client: signState(state) returns userSig (prepends signer type byte)
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)
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: CheckOpenChannel(userWallet, asset) → approvedSigValidators
+ Note over Node: GetLastUserState(userWallet, asset) → currentState
+ Note over Node: EnsureNoOngoingStateTransitions(userWallet, asset)
+ Note over Node: ValidateStateAdvancement(currentState, incomingState)
+ Note over Node: PackState → verify userSig → nodeSigner.Sign(packedState)
+ Note over Node: StoreEscrowChannel(escrow_channel)
+ Note over Node: RecordTransaction → StoreUserState(state)
Node->>Client: Return node signature
+
+ %% Phase 2: On-chain escrow withdrawal initiation
Note over Client: PackChannelDefinition(channelDef)
Note over Client: PackState(channelId, state)
Node->>EscrowChain: initiateEscrowWithdrawal(packedChannelDef, packedState)
EscrowChain->>Node: Return Tx Hash
EscrowChain-->>Node: Emits EscrowWithdrawalInitiated Event
- Note right of Node: HandleEscrowWithdrawalInitiated()
- Note right of Node: UpdateEscrowChannel(escrow_channel)
+ Note over Node: HandleEscrowWithdrawalInitiated()
+ Note over Node: escrowChannel.StateVersion = event.StateVersion
+ Note over Node: escrowChannel.Status = Open
+ Note over Node: UpdateChannel(escrowChannel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
-
+
+ %% Phase 3: Escrow withdrawal finalization - debit home, credit escrow
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.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
+ Note over Client: NewTransition(TransitionTypeEscrowWithdraw, state.ID(), escrowChannelID, amount)
Note over Client: state.applyTransitions(transitions) returns true
Note over Client: signState(state) returns userSig
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)
+ Note over Node: LockUserState → CheckOpenChannel → GetLastUserState
+ Note over Node: ValidateStateAdvancement → PackState → verify sig → node sign
+ Note over Node: RecordTransaction → StoreUserState(state)
Node->>Client: Return node signature
+ %% Phase 4: Finalize on-chain
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)
+ Note over Node: HandleEscrowWithdrawalFinalized()
+ Note over Node: escrowChannel.StateVersion = event.StateVersion
+ Note over Node: escrowChannel.Status = Closed
+ Note over Node: UpdateChannel(escrowChannel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
-
\ No newline at end of file
diff --git a/docs/communication_flows/home_chan_creation_from_scratch.mmd b/docs/communication_flows/home_chan_creation_from_scratch.mmd
index 7b35cd279..76425d73e 100644
--- a/docs/communication_flows/home_chan_creation_from_scratch.mmd
+++ b/docs/communication_flows/home_chan_creation_from_scratch.mmd
@@ -8,20 +8,34 @@ sequenceDiagram
Client->>Node: GetLastState(UserWallet, asset)
Note right of Node: GetLastState(userWallet, asset) returns nil
Node->>Client: Returns an "empty state with 0 version" error
- Note over Client: newChannelDefinition()
+ Note over Client: newChannelDefinition(nonce, challenge, approvedSigValidators)
Note over Client: newEmptyState(asset) returns state
- Note over Client: state.setID(CalculateStateID(state.userWallet, state.asset, cycleId, state.version))
+ Note over Client: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
Note over Client: GetTokenAddress(blockchainId, asset)
Note over Client: state.setHomeToken(blockchainId, tokenAddress)
- Note over Client: NewTransition(depositT, state.ID(), userWallet, amount)
+ Note over Client: NewTransition(TransitionTypeHomeDeposit, state.ID(), userWallet, amount)
Note over Client: state.applyTransitions(transitions) returns true
- Note over Client: signState(state) returns userSig
+ Note over Client: signState(state) returns userSig (prepends signer type byte)
+
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)
+ Note over Node: Validate userWallet is valid hex address
+ Note over Node: IsAssetSupported(asset, tokenAddress, blockchainId)
+ Note over Node: SignerValidatorsSupported(approvedSigValidators)
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: GetLastUserState(userWallet, asset) → nil → NewVoidState()
+ Note over Node: If channel exists and not final → reject "already initialized"
+ Note over Node: Calculate homeChannelID = GetHomeChannelID(node, user, asset, nonce, challenge, sigValidators)
+ Note over Node: Validate incoming homeChannelID matches calculated ID
+ Note over Node: Validate nonce != 0, challenge >= minChallenge
+ Note over Node: ValidateStateAdvancement(voidState, incomingState)
+ Note over Node: PackState(incomingState) → packedState
+ Note over Node: Extract signer type from userSig (0x00=default, 0x01=session key)
+ Note over Node: Verify signer type approved: IsChannelSignerSupported(approvedSigValidators, sigType)
+ Note over Node: ChannelSigValidator.Verify(userWallet, packedState, userSig)
+ Note over Node: CreateChannel(homeChannelID, userWallet, asset, Home, blockchainID, token, nonce, challenge, approvedSigValidators)
+ Note over Node: nodeSigner.Sign(packedState) → nodeSig
+ Note over Node: NewTransactionFromTransition(state, transition) → RecordTransaction
+ Note over Node: StoreUserState(state) → also updates UserBalance
Node->>Client: Return node signature
Note over Client: GetChannelId(channelDef)
Note over Client: PackChannelDefinition(channelDef)
@@ -29,8 +43,9 @@ sequenceDiagram
Client->>HomeChain: createHomeChannel(packedChannelDef, packedState)
HomeChain->>Client: Return Tx Hash
HomeChain-->>Node: Emits HomeChannelCreated Event
- Note right of Node: HandleHomeChannelCreated()
- Note right of Node: UpdateChannel(channel)
+ Note over Node: HandleHomeChannelCreated()
+ Note over Node: channel.StateVersion = event.StateVersion
+ Note over Node: channel.Status = Open
+ Note over Node: UpdateChannel(channel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
Client-->>User: Returns success
-
\ No newline at end of file
diff --git a/docs/communication_flows/home_chan_deposit.mmd b/docs/communication_flows/home_chan_deposit.mmd
index 02db0d350..30123727d 100644
--- a/docs/communication_flows/home_chan_deposit.mmd
+++ b/docs/communication_flows/home_chan_deposit.mmd
@@ -5,32 +5,40 @@ sequenceDiagram
actor HomeChain
Note over Client: Connected to Node
Note over HomeChain: User already has a home channel
- Note over Node: Contains user's state with Home Chain
+ Note over Node: Contains user's state with Home Chain
User->>Client: deposit(blockchainId, asset, amount)
Client->>Node: GetLastState(UserWallet, asset)
- Note right of Node: GetLastState(userWallet, asset)
Node->>Client: Returns a state with home chain
- %% Note over Client: Check current state transitions
Note over Client: createNextState(currentState) returns state
Note over Client: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
- Note over Client: NewTransition(core.TransitionTypeHomeDeposit, state.ID(), userWallet, amount)
+ Note over Client: NewTransition(TransitionTypeHomeDeposit, state.ID(), userWallet, amount)
Note over Client: state.applyTransitions(transitions) returns true
- Note over Client: signState(state) returns userSig
+ Note over Client: signState(state) returns userSig (prepends signer type byte)
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)
+ Note over Node: ActionGateway.AllowAction(userWallet, GatedActionTransfer)
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: CheckOpenChannel(userWallet, asset) → approvedSigValidators
+ Note over Node: GetLastUserState(userWallet, asset) → currentState
+ Note over Node: EnsureNoOngoingStateTransitions(userWallet, asset)
+ Note over Node: ValidateStateAdvancement(currentState, incomingState)
+ Note over Node: PackState(incomingState) → packedState
+ Note over Node: Extract signer type from userSig (0x00=default, 0x01=session key)
+ Note over Node: Verify signer type approved: IsChannelSignerSupported(approvedSigValidators, sigType)
+ Note over Node: ChannelSigValidator.Verify(userWallet, packedState, userSig)
+ Note over Node: nodeSigner.Sign(packedState) → nodeSig
+ Note over Node: NewTransactionFromTransition(state, transition) → RecordTransaction
+ Note over Node: StoreUserState(state) → also updates UserBalance
Node->>Client: Return node signature
Note over Client: PackState(state)
Client->>HomeChain: checkpoint(channelId, packedState)
HomeChain->>Client: Return Tx Hash
- HomeChain-->>Node: Emits Checkpointed Event
- Note right of Node: HandleCheckpointed()
- Note right of Node: UpdateChannel(channel)
+ HomeChain-->>Node: Emits HomeChannelCheckpointed Event
+ Note over Node: HandleHomeChannelCheckpointed()
+ Note over Node: channel.StateVersion = event.StateVersion
+ Note over Node: If challenged → set status = Open
+ Note over Node: UpdateChannel(channel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
Client-->>User: Returns success
-
\ No newline at end of file
diff --git a/docs/communication_flows/home_chan_withdraw.mmd b/docs/communication_flows/home_chan_withdraw.mmd
index 60ef5d801..f128fb6d7 100644
--- a/docs/communication_flows/home_chan_withdraw.mmd
+++ b/docs/communication_flows/home_chan_withdraw.mmd
@@ -5,32 +5,40 @@ sequenceDiagram
actor HomeChain
Note over Client: Connected to Node
Note over HomeChain: User already has a home channel
- Note over Node: Contains user's state with Home Chain
+ Note over Node: Contains user's state with Home Chain
User->>Client: withdraw(blockchainId, asset, amount)
Client->>Node: GetLastState(UserWallet, asset)
- Note right of Node: GetLastState(userWallet, asset)
Node->>Client: Returns a state with home chain
- %% Note over Client: Check current state transitions
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.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
+ Note over Client: NewTransition(TransitionTypeHomeWithdrawal, state.ID(), userWallet, amount)
Note over Client: state.applyTransitions(transitions) returns true
- Note over Client: signState(state) returns userSig
+ Note over Client: signState(state) returns userSig (prepends signer type byte)
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)
+ Note over Node: ActionGateway.AllowAction(userWallet, GatedActionTransfer)
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: CheckOpenChannel(userWallet, asset) → approvedSigValidators
+ Note over Node: GetLastUserState(userWallet, asset) → currentState
+ Note over Node: EnsureNoOngoingStateTransitions(userWallet, asset)
+ Note over Node: ValidateStateAdvancement(currentState, incomingState)
+ Note over Node: PackState(incomingState) → packedState
+ Note over Node: Extract signer type from userSig (0x00=default, 0x01=session key)
+ Note over Node: Verify signer type approved: IsChannelSignerSupported(approvedSigValidators, sigType)
+ Note over Node: ChannelSigValidator.Verify(userWallet, packedState, userSig)
+ Note over Node: nodeSigner.Sign(packedState) → nodeSig
+ Note over Node: NewTransactionFromTransition(state, transition) → RecordTransaction
+ Note over Node: StoreUserState(state) → also updates UserBalance
Node->>Client: Return node signature
Note over Client: PackState(channelId, state)
Client->>HomeChain: checkpoint(channelId, packedState)
HomeChain->>Client: Return Tx Hash
- HomeChain-->>Node: Emits Checkpointed Event
- Note right of Node: HandleCheckpointed()
- Note right of Node: UpdateChannel(channel)
+ HomeChain-->>Node: Emits HomeChannelCheckpointed Event
+ Note over Node: HandleHomeChannelCheckpointed()
+ Note over Node: channel.StateVersion = event.StateVersion
+ Note over Node: If challenged → set status = Open
+ Note over Node: UpdateChannel(channel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
Client-->>User: Returns success
-
\ No newline at end of file
diff --git a/docs/communication_flows/home_chan_withdraw_on_create_from_state.mmd b/docs/communication_flows/home_chan_withdraw_on_create_from_state.mmd
index 992d0f983..93326d07e 100644
--- a/docs/communication_flows/home_chan_withdraw_on_create_from_state.mmd
+++ b/docs/communication_flows/home_chan_withdraw_on_create_from_state.mmd
@@ -7,23 +7,34 @@ sequenceDiagram
Note over Node: Contains a state with no channel
User->>Client: withdraw(blockchainId, asset, amount)
Client->>Node: GetLastState(UserWallet, asset)
- Note right of Node: GetLastState(userWallet, asset)
Node->>Client: Returns a state with no home chain
- %% Note over Client: Check current state transitions
- Note over Client: newChannelDefinition()
+ Note over Client: newChannelDefinition(nonce, challenge, approvedSigValidators)
Note over Client: createNextState(currentState) returns state
- Note over Client: state.setID(CalculateStateID(state.userWallet, state.asset, cycleId, state.version))
+ Note over Client: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
Note over Client: GetTokenAddress(blockchainId, asset)
Note over Client: state.setHomeToken(blockchainId, tokenAddress)
- Note over Client: NewTransition(withdrawalT, state.ID(), userWallet, amount)
+ Note over Client: NewTransition(TransitionTypeHomeWithdrawal, state.ID(), userWallet, amount)
Note over Client: state.applyTransitions(transitions) returns true
- Note over Client: signState(state) returns userSig
+ Note over Client: signState(state) returns userSig (prepends signer type byte)
+
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)
+ Note over Node: Validate userWallet is valid hex address
+ Note over Node: IsAssetSupported(asset, tokenAddress, blockchainId)
+ Note over Node: SignerValidatorsSupported(approvedSigValidators)
+ Note over Node: LockUserState(userWallet, asset)
+ Note over Node: GetLastUserState(userWallet, asset) → pendingState
+ Note over Node: Calculate homeChannelID = GetHomeChannelID(node, user, asset, nonce, challenge, sigValidators)
+ Note over Node: Validate incoming homeChannelID matches calculated ID
+ Note over Node: Validate nonce != 0, challenge >= minChallenge
+ Note over Node: ValidateStateAdvancement(pendingState, incomingState)
+ Note over Node: PackState(incomingState) → packedState
+ Note over Node: Extract signer type from userSig (0x00=default, 0x01=session key)
+ Note over Node: Verify signer type approved: IsChannelSignerSupported(approvedSigValidators, sigType)
+ Note over Node: ChannelSigValidator.Verify(userWallet, packedState, userSig)
+ Note over Node: CreateChannel(homeChannelID, userWallet, asset, Home, blockchainID, token, nonce, challenge, approvedSigValidators)
+ Note over Node: nodeSigner.Sign(packedState) → nodeSig
+ Note over Node: NewTransactionFromTransition(state, transition) → RecordTransaction
+ Note over Node: StoreUserState(state) → also updates UserBalance
Node->>Client: Return node signature
Note over Client: GetChannelId(channelDef)
Note over Client: PackChannelDefinition(channelDef)
@@ -31,8 +42,9 @@ sequenceDiagram
Client->>HomeChain: createHomeChannel(packedChannelDef, packedState)
HomeChain->>Client: Return Tx Hash
HomeChain-->>Node: Emits HomeChannelCreated Event
- Note right of Node: HandleHomeChannelCreated()
- Note right of Node: UpdateChannel(channel)
+ Note over Node: HandleHomeChannelCreated()
+ Note over Node: channel.StateVersion = event.StateVersion
+ Note over Node: channel.Status = Open
+ Note over Node: UpdateChannel(channel)
Node-->>Client: Sends ChannelUpdate & BalanceUpdate
Client-->>User: Returns success
-
\ No newline at end of file
diff --git a/docs/communication_flows/transfer.mmd b/docs/communication_flows/transfer.mmd
index 4db410738..802cde6eb 100644
--- a/docs/communication_flows/transfer.mmd
+++ b/docs/communication_flows/transfer.mmd
@@ -3,35 +3,47 @@ sequenceDiagram
actor SenderClient
actor Node
actor ReceiverClient
-
+
Note over SenderClient: Connected to Node
- Note over Node: Contains user's state with Home Chain
+ Note over Node: Contains user's state with Home Chain
SenderUser->>SenderClient: transfer(DestinationUserWallet, asset, amount)
SenderClient->>Node: GetLastState(SenderUserWallet, asset)
- Note right of Node: GetLastState(userWallet, asset)
Node->>SenderClient: Returns a state with home chain
- %% Note over SenderClient: Check current state transitions
Note over SenderClient: createNextState(currentState) returns state
- Note over SenderClient: state.setID(CalculateStateID(state.userWallet, state.asset, cycleId, state.version))
- Note over SenderClient: NewTransition(transferT, state.ID(), DestinationSenderUserWallet, amount)
+ Note over SenderClient: state.setID(CalculateStateID(state.userWallet, state.asset, epoch, state.version))
+ Note over SenderClient: NewTransition(TransitionTypeTransferSend, state.ID(), DestinationUserWallet, amount)
Note over SenderClient: state.applyTransitions(transitions) returns true
- Note over SenderClient: signState(state) returns userSig
+ Note over SenderClient: signState(state) returns userSig (prepends signer type byte)
SenderClient->>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)
-
- Note right of Node: CreateReceiverState(DestinationUserWallet)
- Note right of Node: GetLastState(DestinationUserWallet, asset)
- %% Note over SenderClient: Check current state transitions
- Note over Node: createNextState(receiver_state) returns new_receiver_state
- Note over Node: new_receiver_state.setID(CalculateStateID(new_receiver_state.userWallet, new_receiver_state.asset, cycleId, new_receiver_state.version))
- Note over Node: NewTransition(transfer_receiveT, 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
+ Note over Node: ActionGateway.AllowAction(userWallet, GatedActionTransfer)
+ Note over Node: LockUserState(senderWallet, asset)
+ Note over Node: CheckOpenChannel(senderWallet, asset) → approvedSigValidators
+ Note over Node: GetLastUserState(senderWallet, asset) → currentState
+ Note over Node: EnsureNoOngoingStateTransitions(senderWallet, asset)
+ Note over Node: ValidateStateAdvancement(currentState, incomingState)
+ Note over Node: PackState(incomingState) → packedState
+ Note over Node: Extract signer type from userSig (0x00=default, 0x01=session key)
+ Note over Node: Verify signer type approved: IsChannelSignerSupported(approvedSigValidators, sigType)
+ Note over Node: ChannelSigValidator.Verify(senderWallet, packedState, userSig)
+ Note over Node: nodeSigner.Sign(packedState) → senderNodeSig
+
+ rect rgb(240, 248, 255)
+ Note over Node: issueTransferReceiverState()
+ Note over Node: Validate sender ≠ receiver
+ Note over Node: LockUserState(receiverWallet, asset)
+ Note over Node: GetLastUserState(receiverWallet, asset) → receiverState (or VoidState)
+ Note over Node: receiverState.NextState() → increment version
+ Note over Node: ApplyTransferReceiveTransition(senderWallet, amount, txID)
+ Note over Node: GetLastSignedState(receiverWallet, asset)
+ Note over Node: If last signed was MutualLock or EscrowLock → skip signing
+ Note over Node: Else if HomeChannelID exists → nodeSigner.Sign(packedReceiverState)
+ Note over Node: StoreUserState(receiverState) → also updates receiver UserBalance
+ end
+
+ Note over Node: NewTransactionFromTransition(senderState, receiverState, transition) → RecordTransaction
+ Note over Node: StoreUserState(senderState) → also updates sender UserBalance
Node->>SenderClient: Sends ChannelUpdate & BalanceUpdate
Node->>ReceiverClient: Sends ChannelUpdate & BalanceUpdate
diff --git a/docs/data_models.mmd b/docs/data_models.mmd
index db5d2c9c8..9ce8ff680 100644
--- a/docs/data_models.mmd
+++ b/docs/data_models.mmd
@@ -1,151 +1,332 @@
classDiagram
+ %% ===== ENUMS =====
+
class ChannelStatus {
- open
- closed
- challenged
+ <>
+ 0 void
+ 1 open
+ 2 challenged
+ 3 closed
}
class ChannelType {
- escrow
- home
+ <>
+ 1 home
+ 2 escrow
}
- class Channel {
- +string ChannelID // should be different for home and escrow channels
- +string UserWallet // better than ParticipantWallet, because we won't have more than one participant
- +ChannelType Type
- +uint32 BlockchainID
- +string Token
- +uint64 Challenge
- +uint64 Nonce
- +ChannelStatus Status
- +uint64 OnChainStateVersion
-
- +time.Time CreatedAt
- +time.Time UpdatedAt
+ class TransactionType {
+ <>
+ 10 home_deposit
+ 11 home_withdrawal
+ 20 escrow_deposit
+ 21 escrow_withdraw
+ 30 transfer
+ 40 commit
+ 41 release
+ 42 rebalance
+ 100 migrate
+ 110 escrow_lock
+ 120 mutual_lock
+ 200 finalize
}
- class State { // Immutable
- +[64]char ID // Deterministic: Hash(UserWallet, Asset, UserCycleIndex, Version)
-
- +string Data
- +string Asset
- +string UserWallet
- +uint64 CycleIndex
-
- +uint64 Version
+ class TransitionType {
+ <>
+ 0 void
+ 1 acknowledgement
+ 10 home_deposit
+ 11 home_withdrawal
+ 20 escrow_deposit
+ 21 escrow_withdraw
+ 30 transfer_send
+ 31 transfer_receive
+ 40 commit
+ 41 release
+ 100 migrate
+ 110 escrow_lock
+ 120 mutual_lock
+ 200 finalize
+ }
- +string *HomeChannelID
- +string *EscrowChannelID
+ class AppSessionStatus {
+ <>
+ 0 void
+ 1 open
+ 2 closed
+ }
- // It seem to better not to use State array in SC in CrossChainState
- // And CrossChainState now can be renamed to "State"
+ class BlockchainActionType {
+ <>
+ 1 checkpoint
+ 10 initiate_escrow_deposit
+ 11 finalize_escrow_deposit
+ 20 initiate_escrow_withdrawal
+ 21 finalize_escrow_withdrawal
+ }
- // Home Channel
- +int256 HomeUserBalance
- +int64 HomeUserNetFlow
- +int256 HomeNodeBalance
- +int64 HomeNodeNetFlow
+ class BlockchainActionStatus {
+ <>
+ 0 pending
+ 1 completed
+ 2 failed
+ }
- // Escrow Channel
- +int256 EscrowUserBalance
- +int64 EscrowUserNetFlow
- +int256 EscrowNodeBalance
- +int64 EscrowNodeNetFlow
+ class GatedAction {
+ <>
+ 1 transfer
+ 10 app_session_creation
+ 11 app_session_operation
+ 12 app_session_deposit
+ 13 app_session_withdrawal
+ }
- +bool IsFinal
- %% TODO: Remove in the future if redundant
+ %% ===== CORE TABLES =====
- +string UserSig
- +string NodeSig
+ class Channel {
+ +char~66~ channel_id PK
+ +char~42~ user_wallet
+ +varchar~20~ asset
+ +smallint type
+ +numeric blockchain_id
+ +char~42~ token
+ +bigint challenge_duration
+ +timestamptz challenge_expires_at
+ +numeric nonce
+ +varchar~66~ approved_sig_validators
+ +smallint status
+ +numeric state_version
+ +timestamptz created_at
+ +timestamptz updated_at
+ }
- +time.Time CreatedAt
+ class ChannelState {
+ +char~66~ id PK
+ +varchar~20~ asset
+ +char~42~ user_wallet
+ +numeric epoch
+ +numeric version
+ +smallint transition_type
+ +char~66~ transition_tx_id FK
+ +varchar~66~ transition_account_id
+ +numeric transition_amount
+ +char~66~ home_channel_id FK
+ +char~66~ escrow_channel_id FK
+ +numeric home_user_balance
+ +numeric home_user_net_flow
+ +numeric home_node_balance
+ +numeric home_node_net_flow
+ +numeric escrow_user_balance
+ +numeric escrow_user_net_flow
+ +numeric escrow_node_balance
+ +numeric escrow_node_net_flow
+ +text user_sig
+ +text node_sig
+ +timestamptz created_at
}
- class ChannelOfTypeHome {
+ class Transaction {
+ +char~66~ id PK
+ +smallint tx_type
+ +varchar~20~ asset_symbol
+ +varchar~66~ from_account
+ +varchar~66~ to_account
+ +char~66~ sender_new_state_id FK
+ +char~66~ receiver_new_state_id FK
+ +numeric amount
+ +timestamptz created_at
}
- class ChannelOfTypeEscrow {
+ class UserBalance {
+ +char~42~ user_wallet PK
+ +varchar~20~ asset PK
+ +numeric balance
+ +timestamptz created_at
+ +timestamptz updated_at
}
- %% Relationships
- Channel --> ChannelStatus
- State --> ChannelOfTypeHome
- State --> ChannelOfTypeEscrow
- ChannelOfTypeHome --> Channel
- ChannelOfTypeEscrow --> Channel
- Channel --> ChannelType
- Transaction --> TransactionType
- Transaction --> SenderNewState
- Transaction --> ReceiverNewState
- SenderNewState --> State
- ReceiverNewState --> State
+ %% ===== APPLICATION TABLES =====
- class SenderNewState {
- *Optional
+ class AppV1 {
+ +varchar~66~ id PK
+ +char~42~ owner_wallet
+ +text metadata
+ +numeric version
+ +boolean creation_approval_not_required
+ +timestamptz created_at
+ +timestamptz updated_at
}
- class ReceiverNewState {
- *Optional
+ class AppSessionV1 {
+ +char~66~ id PK
+ +varchar~66~ application_id FK
+ +numeric nonce
+ +text session_data
+ +smallint quorum
+ +numeric version
+ +smallint status
+ +timestamptz created_at
+ +timestamptz updated_at
}
+ class AppParticipantV1 {
+ +char~66~ app_session_id PK FK
+ +char~42~ wallet_address PK
+ +smallint signature_weight
+ }
- class Transaction { // Immutable
- +[64]char ID
- // Deterministic:
- // 1) Initiated by User: Hash(ToAccount, SenderNewStateID)
- // 2) Initiated by Node: Hash(FromAccount, ReceiverNewStateID)
-
- +TransactionType Type
- +string AssetSymbol
-
- +string FromAccount
- +string ToAccount
+ class AppLedgerEntryV1 {
+ +uuid id PK
+ +char~66~ account_id FK
+ +varchar~20~ asset_symbol
+ +char~42~ wallet
+ +numeric credit
+ +numeric debit
+ +timestamptz created_at
+ }
- +[64]char *SenderNewStateID
- +[64]char *ReceiverNewStateID
+ %% ===== SESSION KEY TABLES =====
+
+ class AppSessionKeyStateV1 {
+ +char~66~ id PK
+ +char~42~ user_address
+ +char~42~ session_key
+ +numeric version
+ +timestamptz expires_at
+ +text user_sig
+ +timestamptz created_at
+ +timestamptz updated_at
+ }
- +decimal.Decimal Amount
- +time.Time CreatedAt
+ class AppSessionKeyApplicationV1 {
+ +char~66~ session_key_state_id PK FK
+ +varchar~66~ application_id PK FK
}
- class TransactionType {
- %% isWallet(ToAccount) && isWallet(FromAccount) -> transfer
- transfer
+ class AppSessionKeyAppSessionV1 {
+ +char~66~ session_key_state_id PK FK
+ +char~66~ app_session_id PK FK
+ }
- %% ToAccount = AppSessionID -> commit
- %% FromAccount = UserWallet -> commit
- commit
+ class ChannelSessionKeyStateV1 {
+ +char~66~ id PK
+ +char~42~ user_address
+ +char~42~ session_key
+ +numeric version
+ +char~66~ metadata_hash
+ +timestamptz expires_at
+ +text user_sig
+ +timestamptz created_at
+ }
- %% ToAccount = UserWallet -> release
- %% FromAccount = AppSessionID -> release
- release
+ class ChannelSessionKeyAssetV1 {
+ +char~66~ session_key_state_id PK FK
+ +varchar~20~ asset PK
+ }
- %% FromAccount = HomeChannelID -> home_deposit
- %% ToAccount = UserWallet -> home_deposit
- home_deposit
+ %% ===== BLOCKCHAIN TABLES =====
+
+ class ContractEvent {
+ +bigserial id PK
+ +char~42~ contract_address
+ +numeric blockchain_id
+ +varchar~255~ name
+ +numeric block_number
+ +varchar~255~ transaction_hash
+ +bigint log_index
+ +timestamptz created_at
+ }
- %% FromAccount = UserWallet -> home_withdrawal
- %% ToAccount = HomeChannelID -> home_withdrawal
- home_withdrawal
+ class BlockchainAction {
+ +bigserial id PK
+ +smallint action_type
+ +char~66~ state_id FK
+ +numeric blockchain_id
+ +jsonb action_data
+ +smallint status
+ +smallint retry_count
+ +text last_error
+ +char~66~ transaction_hash
+ +timestamptz created_at
+ +timestamptz updated_at
+ }
- %% ToAccount = EscrowChannelID -> mutual_lock
- %% FromAccount = HomeChannelID -> mutual_lock
- mutual_lock
+ %% ===== OPERATIONAL TABLES =====
- %% FromAccount = EscrowChannelID -> escrow_deposit
- %% ToAccount = HomeChannelID -> escrow_deposit
- escrow_deposit
-
- %% ToAccount = HomeChannelID -> escrow_lock
- %% FromAccount = EscrowChannelID -> escrow_lock
- escrow_lock
+ class UserStakedV1 {
+ +char~42~ user_wallet PK
+ +numeric blockchain_id PK
+ +numeric amount
+ +timestamptz created_at
+ +timestamptz updated_at
+ }
- %% FromAccount = HomeChannelID -> escrow_withdraw
- %% ToAccount = EscrowChannelID -> escrow_withdraw
- escrow_withdraw
+ class ActionLogEntryV1 {
+ +uuid id PK
+ +char~42~ user_wallet
+ +smallint gated_action
+ +timestamptz created_at
+ }
- %% FromAccount = HomeChannelID -> migrate
- %% ToAccount = EscrowChannelID -> migrate
- migrate
+ class LifespanMetric {
+ +varchar~66~ id PK
+ +varchar~255~ name
+ +jsonb labels
+ +numeric value
+ +timestamptz last_timestamp
+ +timestamptz updated_at
}
+
+ %% ===== RELATIONSHIPS =====
+
+ %% -- Channel core --
+ Channel --> ChannelStatus : status
+ Channel --> ChannelType : type
+
+ %% -- ChannelState references channels and transitions --
+ ChannelState --> TransitionType : transition_type
+ ChannelState --> Channel : home_channel_id
+ ChannelState --> Channel : escrow_channel_id
+ ChannelState --> Transaction : transition_tx_id
+
+ %% -- Transaction references states --
+ Transaction --> TransactionType : tx_type
+ Transaction --> ChannelState : sender_new_state_id
+ Transaction --> ChannelState : receiver_new_state_id
+
+ %% -- UserBalance derived from ChannelState.home_user_balance --
+ ChannelState ..> UserBalance : StoreUserState updates balance
+
+ %% -- Blockchain: actions reference states, events update channels --
+ BlockchainAction --> BlockchainActionType : action_type
+ BlockchainAction --> BlockchainActionStatus : status
+ BlockchainAction --> ChannelState : state_id
+ ContractEvent ..> Channel : events update channel status
+ ContractEvent ..> BlockchainAction : events schedule actions
+ ContractEvent ..> UserStakedV1 : UserLockedBalanceUpdated
+
+ %% -- App layer --
+ AppSessionV1 --> AppV1 : application_id
+ AppSessionV1 --> AppSessionStatus : status
+ AppParticipantV1 --> AppSessionV1 : app_session_id
+ AppLedgerEntryV1 --> AppSessionV1 : account_id
+
+ %% -- App session keys link to apps and sessions --
+ AppSessionKeyApplicationV1 --> AppSessionKeyStateV1 : session_key_state_id
+ AppSessionKeyApplicationV1 --> AppV1 : application_id
+ AppSessionKeyAppSessionV1 --> AppSessionKeyStateV1 : session_key_state_id
+ AppSessionKeyAppSessionV1 --> AppSessionV1 : app_session_id
+
+ %% -- Channel session keys link to channels via user_address + asset --
+ ChannelSessionKeyAssetV1 --> ChannelSessionKeyStateV1 : session_key_state_id
+ ChannelSessionKeyStateV1 ..> Channel : validated against user_address + asset
+
+ %% -- Action log gates user operations --
+ ActionLogEntryV1 --> GatedAction : gated_action
+ ActionLogEntryV1 ..> AppSessionV1 : gates session operations
+
+ %% -- Lifespan metrics aggregate from core tables --
+ LifespanMetric ..> Channel : aggregates channel counts
+ LifespanMetric ..> Transaction : aggregates TVL
+ LifespanMetric ..> AppSessionV1 : aggregates session counts
+ LifespanMetric ..> UserBalance : counts active users
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 000000000..e3fa489b4
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,402 @@
+# Nitrolite Documentation Guide
+
+This document defines **how documentation should be written and structured** inside the Nitrolite repository.
+
+Its purpose is to ensure that documentation:
+
+* is consistent across the repository
+* is easy for developers to navigate
+* is easily retrievable by AI systems
+* avoids duplication
+* focuses on the most important information first
+
+This guide must be followed when writing any documentation for the Nitrolite repository.
+
+---
+
+# 1. Documentation Principles
+
+All documentation in the Nitrolite repository must follow these principles.
+
+## 1.1 Single Source of Truth
+
+Every piece of information must exist in **one canonical location**.
+
+Other places may reference it but must not duplicate the content.
+
+Example:
+
+| Information | Canonical location |
+| --------------------- | ------------------------------ |
+| Protocol definitions | `docs/protocol/terminology.md` |
+| System architecture | `docs/architecture/` |
+| Build Apps on Yellow Network | `docs/build/` |
+| Operator instructions | `docs/operator/` |
+| Code behaviour | Go code comments |
+
+The website documentation repository must **reuse content from the main repository**, not redefine it.
+
+---
+
+## 1.2 AI-Friendly Documentation
+
+Documentation should be written in a way that allows AI systems to reliably retrieve answers.
+
+This requires:
+
+* clear headings
+* explicit terminology
+* short conceptual sections
+* clear definitions
+* structured documents
+
+Avoid narrative writing or long unstructured explanations.
+
+---
+
+## 1.3 Clear Separation of Concerns
+
+Documentation must be separated into four main domains:
+
+1. Protocol
+2. System Architecture
+3. Build (as in "build your applications on Yellow Network")
+4. Operator
+
+Each domain serves a different audience and must not mix responsibilities.
+
+---
+
+# 2. Documentation Structure
+
+All documentation inside the repository must follow this directory structure.
+
+```
+docs/
+ protocol/
+ architecture/
+ build/
+ operator/
+```
+
+Each directory contains documentation for a specific domain.
+
+---
+
+# 3. Terminology Documentation
+
+Terminology must be defined in a single canonical document.
+
+Location:
+
+```
+docs/protocol/terminology.md
+```
+
+This document defines all protocol-level concepts.
+
+Examples of concepts that belong here include:
+
+* Channel
+* State
+* Epoch
+* Settlement
+* Operator
+* Client
+
+Each term must be defined once and used consistently across all documentation.
+
+---
+
+## Terminology Format
+
+Each term must follow the same structure.
+
+Example:
+
+```
+## Channel
+
+Definition
+A channel is a state container shared between participants that allows
+off-chain updates while maintaining on-chain security guarantees.
+
+Purpose
+Channels enable fast off-chain execution while preserving the ability
+to settle on-chain if necessary.
+
+Used In
+- Channel lifecycle
+- State updates
+- Settlement
+```
+
+Terminology definitions must not contain implementation details.
+
+---
+
+# 4. Protocol Documentation
+
+Protocol documentation describes **the system as a protocol**, independent of any specific implementation.
+
+A reader must be able to implement the protocol from this documentation without reading the Nitrolite code.
+
+Location:
+
+```
+docs/protocol/
+```
+
+Recommended documents:
+
+```
+overview.md
+terminology.md
+state-advancement.md
+state-enforcement.md
+```
+
+---
+
+## Protocol Documentation Must Include
+
+Protocol documents must describe:
+
+* protocol concepts
+* state structures
+* rules governing state transitions
+* lifecycle of channels
+* settlement and dispute behaviour
+* interaction with blockchains
+
+Protocol documentation must avoid:
+
+* code references
+* repository structure
+* implementation details
+
+## Language for Structures and Functions
+
+Protocol documentation must use **language-neutral pseudocode** when describing structures or functions.
+
+Use simple struct-like notation for data structures.
+
+Use plain function signatures with named parameters and return types.
+
+Rules:
+
+* Do not use syntax specific to any programming language (Go, TypeScript, Solidity, etc.)
+* Use CamelCase for field and function names
+* Keep pseudocode minimal — only show what is needed to convey the concept
+
+---
+
+# 5. System Architecture Documentation
+
+System architecture documentation explains **how the Nitrolite implementation realizes the protocol**.
+
+Location:
+
+```
+docs/architecture/
+```
+
+Recommended documents:
+
+```
+system-overview.md
+node-architecture.md
+storage.md
+networking.md
+security.md
+```
+
+---
+
+## Architecture Documentation Must Include
+
+Architecture documentation must describe:
+
+* system components
+* internal services
+* communication patterns
+* storage model
+* security mechanisms
+* how the protocol is implemented
+
+Architecture documentation may reference code modules.
+
+Architecture documentation must not redefine protocol rules.
+
+---
+
+# 6. Separating Protocol and Architecture
+
+The protocol and architecture documentation may appear similar because the protocol was developed together with the implementation.
+
+However they must remain conceptually separate.
+
+### Protocol answers
+
+```
+What are the rules of the system?
+```
+
+### Architecture answers
+
+```
+How does Nitrolite implement those rules?
+```
+
+Example:
+
+| Topic | Protocol | Architecture |
+| ----------------- | --------------------------------- | -------------------------------------------- |
+| State | Defines state structure and rules | Explains how state is stored |
+| Settlement | Defines settlement process | Explains which component executes settlement |
+
+Protocol documentation must remain **implementation-independent**.
+
+Architecture documentation describes **Clearnet specifically**.
+
+---
+
+# 7. "Build Apps on Yellow Network" Documentation
+
+Build documentation must onboard developers to start building on top of Yellow Network with minimum friction. This documentation must highlight only protocol concepts and SDK methods necessary for app developers. It must not describe protocol internals.
+
+
+Location:
+
+```
+docs/build/
+```
+
+Recommended documents:
+
+```
+overview.md
+app.md
+develop.md
+examples.md
+```
+
+
+
+1. **app.md** must cover:
+
+* how to register an app
+* app session lifecycle
+* concept of daily allowances
+* app session keys
+
+2. **develop.md** must list SDK methods necessary for app development.
+3. **examples.md** must show real-world use case examples of application flows built with the SDK. Starting with simplest examples and gradually increasing complexity.
+
+
+---
+
+# 8. Operator Documentation
+
+Operator documentation explains how to run and maintain infrastructure.
+
+Location:
+
+```
+docs/operator/
+```
+
+Recommended documents:
+
+```
+running-node.md
+configuration.md
+monitoring.md
+upgrades.md
+```
+
+---
+
+## Operator Documentation Must Include
+
+Operator documentation must cover:
+
+* node deployment
+* configuration parameters
+* operational procedures
+* monitoring requirements
+* upgrade procedures
+
+Operator documentation must not include protocol explanations.
+
+---
+
+# 9. Document Structure Requirements
+
+All documents must follow a predictable structure.
+
+This ensures both developers and AI systems can quickly locate information.
+
+---
+
+## README.md Structure
+
+Every repository README must contain the following Header, followed with flexible component-specific sections.
+
+```
+# Project Name
+
+Short description of the project.
+
+## Overview
+
+High level explanation of the system.
+
+## Documentation
+
+Links to detailed documentation.
+```
+
+---
+
+## Overview Document Structure
+
+Overview documents must contain:
+
+```
+# Overview
+
+## Purpose
+
+Why this component exists.
+
+## Concepts
+
+Key ideas required to understand it.
+
+## How It Works
+
+Explanation of behaviour and interactions.
+
+## Table of contents
+
+Bulletpoints with links to documentation and short descriptions.
+```
+
+---
+
+# 10. Writing Requirements
+
+All documentation must follow these writing rules.
+
+### Use precise terminology
+
+Always use defined protocol terms.
+
+### Avoid ambiguity
+
+Explain behaviour explicitly.
+
+### Avoid implementation leakage in protocol docs
+
+Protocol documentation must not reference code.
diff --git a/docs/protocol/channel-protocol.md b/docs/protocol/channel-protocol.md
new file mode 100644
index 000000000..803b94a13
--- /dev/null
+++ b/docs/protocol/channel-protocol.md
@@ -0,0 +1,272 @@
+# Channel Protocol
+
+Previous: [State Model](state-model.md) | Next: [Enforcement and Settlement](enforcement.md)
+
+---
+
+This document describes how channels operate and how states evolve through off-chain state advancement.
+
+## Purpose
+
+Channels are the primary mechanism for off-chain interaction in the Nitrolite protocol. They allow participants to exchange assets and update state without on-chain transactions.
+
+## Channel Definition
+
+A channel is defined by a set of immutable parameters fixed at creation time.
+
+| Field | Description |
+| --------------------------- | -------------------------------------------------------------- |
+| User | Identifier of the user participant |
+| Node | Identifier of the node participant |
+| Asset | Identifier of the asset operated within the channel |
+| Nonce | Unique nonce to distinguish channels with identical parameters |
+| ChallengeDuration | Challenge period duration in seconds |
+| ApprovedSignatureValidators | Bitmask of approved signature validation modes |
+
+The channel definition MUST NOT change after creation.
+
+## Channel Identifier
+
+The channel identifier is derived deterministically from the channel definition using canonical encoding and hashing.
+
+The derivation produces a 32-byte identifier where:
+
+- the first byte encodes the smart contract version
+- the remaining bytes are derived from the hash of the canonical encoded channel definition parameters
+
+This ensures that:
+
+- each unique channel definition produces a unique identifier
+- the identifier can be independently computed by any party
+- no central authority is required to assign identifiers
+- identifiers are scoped to a specific protocol version
+
+## Channel Lifecycle
+
+A channel progresses through four primary actions.
+
+**Create** *(off-chain, then optionally on-chain)*
+The node validates and stores the channel definition. An initial state is constructed and signed by all participants. This initial state MAY subsequently be submitted to the blockchain layer for on-chain enforcement, or any later state with a higher version MAY be used instead.
+
+**Checkpoint** *(off-chain, then optionally on-chain)*
+The node validates and stores a new state off-chain. Depending on the transition type or a participant's initiative, the node MAY also submit the state to the blockchain layer for on-chain enforcement. Any party MAY independently submit a signed state to the blockchain layer.
+
+**Challenge** *(on-chain only)*
+A participant submits a signed state along with a challenger signature to the blockchain layer. Upon successful validation, the challenge duration begins. During this period, other participants MAY respond by submitting a state with a higher version (if exists) via checkpoint to refute the challenge.
+
+**Close** *(off-chain for cooperative, on-chain for execution)*
+Off-chain, a close represents a mutual agreement to finalize the channel. On-chain, a close MAY be executed either through a mutually signed close state or after the challenge duration has elapsed without a successful response. Upon close, the channel's funds are released according to the final state allocations and the channel's lifecycle ends.
+
+## State Signing Categories
+
+During the channel lifecycle, states exist in one of the following signing categories:
+
+**Mutually signed state** — a state that carries valid signatures from both the user and the node. This is the authoritative off-chain state and the only category that is enforceable on-chain.
+
+**Node-issued pending state** — a state produced by the node (e.g. for TransferReceive or Release transitions) that carries only the node's signature. A pending state is not enforceable on-chain and MUST NOT be treated as the latest authoritative state. It becomes mutually signed only after the user acknowledges it.
+
+The off-chain and enforcement representations encode the same logical state. A state that is mutually signed off-chain is directly enforceable on-chain without transformation, provided the enforcement representation is derived correctly. Session-key signatures are valid for enforcement if the channel's approved signature validators include the session key validator.
+
+## State Advancement Rules
+
+When a new state is proposed during off-chain advancement, the following general rules apply:
+
+**Version validation**
+The state version MUST equal the current version incremented by one.
+
+**Signature validation**
+A valid signature from the proposing participant MUST be present. The signature validation mode MUST be among the channel's approved signature validators.
+
+**Channel binding**
+The channel identifier MUST be present and MUST match the channel definition.
+
+**Transition admissibility**
+The transition type MUST be valid for the current channel state. Transition-specific validation rules MUST be satisfied.
+
+**Ledger admissibility**
+Ledger invariants MUST hold: allocations MUST equal net flows, and allocation values MUST be non-negative. Declared decimal precision MUST match the asset's actual precision. Additionally, transition-specific ledger validations apply.
+
+## Transition Families
+
+Transitions are organized into the following families:
+
+**Local channel transitions** — operations that affect the channel's home ledger directly: Home Deposit, Home Withdrawal, Finalize.
+
+**Transfer transitions** — operations that move assets between users via the node: TransferSend, TransferReceive, Acknowledgement.
+
+**Extension bridge transitions** — operations that move assets between the channel and an extension: Commit, Release.
+
+**Cross-chain escrow transitions** — operations that manage cross-chain deposits and withdrawals through escrow: Escrow Deposit Initiate, Escrow Deposit Finalize, Escrow Withdrawal Initiate, Escrow Withdrawal Finalize.
+
+**Migration transitions** — operations that move the channel's home chain: Migration Initiate, Migration Finalize.
+
+## Transitions
+
+Each transition below describes its purpose, the expected transition field values, and the resulting ledger effects. Ledger fields are abbreviated as: UB (UserAllocation), UNF (UserNetFlow), NB (NodeAllocation), NNF (NodeNetFlow).
+
+For all transitions that do not modify the non-home ledger, the non-home ledger MUST be empty (see [Empty Non-Home Ledger](state-model.md#empty-non-home-ledger)).
+
+State Ledgers Operation-specific advancement diagram:
+
+
+
+### Acknowledgement
+
+- Purpose: allows the user to acknowledge and sign a pending node-issued state
+- Acknowledgement creates a new state version. The new state is identical to the pending node-issued state in all fields except version (incremented by one) and the addition of the user's signature
+- Valid only when the current state has no user signature
+- Applies only to node-issued pending states (TransferReceive, Release)
+- A node-issued pending state is NOT enforceable on-chain before acknowledgement, because it lacks the user's signature
+- The non-home ledger MUST be empty
+
+### Home Deposit
+
+- Purpose: records an asset deposit from the home chain into the channel
+- AccountId MUST reference the home channel identifier
+- Amount MUST be the deposited quantity
+- Home ledger effects: UB increases by Amount, UNF increases by Amount
+- The non-home ledger MUST be empty
+- Requires an on-chain checkpoint to lock the deposited assets
+
+### Home Withdrawal
+
+- Purpose: records an asset withdrawal from the channel to the home chain
+- AccountId MUST reference the home channel identifier
+- Amount MUST be the withdrawn quantity
+- Home ledger effects: UB decreases by Amount, UNF decreases by Amount
+- The non-home ledger MUST be empty
+- Requires an on-chain checkpoint to release the withdrawn assets
+
+### TransferSend
+
+- Purpose: transfers assets from the user to a counterparty via the node
+- AccountId MUST reference the receiver's address
+- Amount MUST be the transfer quantity
+- TxId uniquely identifies this transfer and is used to correlate with the corresponding TransferReceive on the receiver's channel
+- Home ledger effects: UB decreases by Amount, NNF decreases by Amount
+- The non-home ledger MUST be empty
+
+### TransferReceive
+
+- Purpose: records an inbound transfer from a counterparty via the node
+- AccountId MUST reference the sender's address
+- Amount MUST exactly match the sender's TransferSend amount (no scaling or normalization; transfers require the same unified asset)
+- TxId MUST match the TxId from the corresponding TransferSend
+- Home ledger effects: UB increases by Amount, NNF increases by Amount
+- The non-home ledger MUST be empty
+- This is a node-issued pending state: it carries only the node's signature and MUST NOT be considered the last mutually signed state until the user acknowledges it
+
+### Commit
+
+- Purpose: moves assets from the channel into an extension (such as an application session)
+- AccountId MUST reference the extension object identifier (e.g. application session id)
+- Amount MUST be the committed quantity
+- Home ledger effects: UB decreases by Amount, NNF decreases by Amount
+- The non-home ledger MUST be empty
+
+### Release
+
+- Purpose: returns assets from an extension back to channel allocations
+- AccountId MUST reference the extension object identifier (e.g. application session id)
+- Amount MUST be the released quantity
+- The extension state MUST authorize the release
+- Home ledger effects: UB increases by Amount, NNF increases by Amount
+- The non-home ledger MUST be empty
+- This is a node-issued pending state: it carries only the node's signature and MUST NOT be considered the last mutually signed state until the user acknowledges it
+
+### Escrow Deposit Initiate
+
+- Purpose: initiates a cross-chain deposit by creating an escrow between the home and non-home chains
+- AccountId MUST reference the escrow channel identifier (derived from the home channel identifier and state version)
+- Amount MUST be the deposit quantity
+- A non-home ledger MUST be provided in the state
+- The non-home ledger MUST have a different blockchain identifier than the home ledger
+- Home ledger effects: NB increases by Amount, NNF increases by Amount
+- Non-home ledger is initialized: UB set to Amount, UNF set to Amount, NB and NNF set to zero
+
+### Escrow Deposit Finalize
+
+- Purpose: completes a cross-chain deposit previously initiated by an escrow deposit initiate
+- AccountId MUST reference the escrow channel identifier
+- Amount MUST match the amount from the initiating transition
+- Home ledger effects: UB increases by Amount, NB decreases by Amount, NNF does not change
+- Non-home ledger effects: UB decreases by Amount, NNF decreases by Amount
+
+### Escrow Withdrawal Initiate
+
+- Purpose: initiates a cross-chain withdrawal by creating an escrow on the non-home chain
+- AccountId MUST reference the escrow channel identifier (derived from the home channel identifier and state version)
+- Amount MUST be the withdrawal quantity
+- A non-home ledger MUST be provided in the state
+- The non-home ledger MUST have a different blockchain identifier than the home ledger
+- Non-home ledger is initialized: NB set to Amount, NNF set to Amount, UB and UNF set to zero
+
+### Escrow Withdrawal Finalize
+
+- Purpose: completes a cross-chain withdrawal previously initiated by an escrow withdrawal initiate
+- AccountId MUST reference the escrow channel identifier
+- Amount MUST match the amount from the initiating transition
+- Home ledger effects: UB decreases by Amount, NNF decreases by Amount
+- Non-home ledger effects: UNF decreases by Amount, NB decreases by Amount
+
+### Migration Initiate
+
+- Purpose: initiates migration of the channel from the current home chain to a different chain
+- AccountId MUST reference the escrow channel identifier
+- A non-home ledger MUST be provided in the state
+- On the home chain (outgoing): UB MUST remain unchanged, UNF MUST NOT change, NB MUST be zero; the non-home ledger NB MUST equal the home ledger UB (normalized by decimal precision), non-home NNF MUST equal non-home NB, non-home UB and UNF MUST be zero
+- On the non-home chain (incoming): the blockchain layer internally swaps ledgers so the non-home ledger becomes the home ledger; NB MUST equal the user allocation from the originating chain (normalized by decimal precision), NNF MUST equal NB, UB MUST be zero, UNF MUST be zero; the node locks funds equal to NB
+
+VERSION NOTE: Migration transitions are functional but may be refined in future protocol versions.
+
+### Migration Finalize
+
+- Purpose: completes a previously initiated migration
+- The version MUST be the immediate successor of the migration initiate state
+- On the new home chain: UB MUST equal the user allocation from the initiate state, NB MUST be zero, UNF and NNF MUST NOT change from the initiate state; the non-home ledger MUST be zeroed out; the channel transitions to operating status; no fund movement occurs
+- On the old home chain: the blockchain layer internally swaps ledgers before validation; UB and NB on the old home MUST be zero; the non-home ledger carries the user allocation to the new chain; all locked funds are released and the channel is marked as migrated out
+
+VERSION NOTE: Migration transitions are functional but may be refined in future protocol versions.
+
+### Finalize
+
+- Purpose: indicates cooperative intent to close the channel and release all funds
+- AccountId MUST reference the home channel identifier
+- Amount MUST equal the user's current UB
+- Home ledger effects: UNF decreases by the current UB, UB is set to zero
+- The non-home ledger MUST be empty — open escrows or incomplete migrations MUST be resolved before finalization
+- All participants MUST sign
+- Final allocations become the settlement distribution
+- NodeAllocation on finalization reflects the node's remaining share
+
+## Atomicity and Dependent State Changes
+
+Certain transitions produce side effects that create or modify states in other channels. The entire advancement — including all dependent state changes — MUST succeed or fail as a whole.
+
+**TransferSend** — when the node accepts a TransferSend, it MUST atomically create the corresponding TransferReceive state on the receiver's channel. If receiver-side state creation fails, the sender-side advancement MUST also fail.
+
+**Release** — when an extension releases assets, the node MUST atomically create the Release state on the user's channel.
+
+**Cross-chain escrow transitions** — escrow initiate and finalize operations MAY trigger on-chain actions (escrow creation, fund locking) that MUST be coordinated with the off-chain state change.
+
+## Checkpoint-Relevant Transitions
+
+The following transitions require or MAY trigger a checkpoint to the blockchain layer. These are all transitions whose intent does not map to OPERATE:
+
+| Transition | Intent | Checkpoint Behaviour |
+| -------------------------- | -------------------------- | ------------------------------------------- |
+| Home Deposit | DEPOSIT | Required to lock deposited assets |
+| Home Withdrawal | WITHDRAW | Required to release withdrawn assets |
+| Escrow Deposit Initiate | INITIATE_ESCROW_DEPOSIT | Required to create escrow on non-home chain |
+| Escrow Deposit Finalize | FINALIZE_ESCROW_DEPOSIT | Required to complete cross-chain deposit |
+| Escrow Withdrawal Initiate | INITIATE_ESCROW_WITHDRAWAL | Required to create escrow for withdrawal |
+| Escrow Withdrawal Finalize | FINALIZE_ESCROW_WITHDRAWAL | Required to release assets on non-home chain|
+| Migration Initiate | INITIATE_MIGRATION | Required to begin chain migration |
+| Migration Finalize | FINALIZE_MIGRATION | Required to complete chain migration |
+| Finalize | CLOSE | Required to settle and release funds |
+
+Any transition MAY also be checkpointed at a participant's discretion to enforce the current state on-chain. Any party MAY independently submit a validly signed state to the blockchain layer.
+
+---
+
+Previous: [State Model](state-model.md) | Next: [Enforcement and Settlement](enforcement.md)
diff --git a/docs/protocol/cross-chain-and-assets.md b/docs/protocol/cross-chain-and-assets.md
new file mode 100644
index 000000000..91e0c95ed
--- /dev/null
+++ b/docs/protocol/cross-chain-and-assets.md
@@ -0,0 +1,151 @@
+# Cross-Chain and Asset Model
+
+Previous: [Enforcement and Settlement](enforcement.md) | Next: [Interactions](interactions.md)
+
+---
+
+This document describes the unified asset model and cross-chain functionality.
+
+## Purpose
+
+The unified asset model allows participants to operate on assets from multiple blockchains within a single channel. This eliminates the need for separate channels per blockchain and enables cross-chain interactions.
+
+## Unified Asset Concept
+
+Assets in the Nitrolite protocol are identified independently of any specific blockchain.
+
+A unified asset is defined by:
+
+| Field | Description |
+| -------- | -------------------------------------------------- |
+| Symbol | Human-readable canonical asset identifier (e.g. "USDC") |
+| Decimals | Decimal precision of the asset |
+
+### Canonical Asset Identification
+
+The protocol identifies a unified asset by its symbol. Within channel metadata, the symbol is represented as the first 8 bytes of its Keccak-256 hash, providing a compact canonical identifier. Two chain-specific tokens are recognized as the same unified asset if they share the same symbol-derived identifier and are configured as such by the node.
+
+Symbol collisions are prevented by the node's asset configuration. The protocol does not maintain a global on-chain registry of unified assets.
+
+### Amount Normalization
+
+Assets on different blockchains MAY have different decimal precisions (e.g. USDC has 6 decimals on Ethereum but may have different precision on other chains). The protocol normalizes amounts for cross-chain comparisons using WAD normalization, which scales chain-specific amounts as if a token had 18 decimals:
+
+```
+NormalizedAmount = Amount * 10^(18 - ChainDecimals)
+```
+
+Each unified asset defines a canonical decimal precision (e.g. 6 for USDC) that is used during User <> Clearnode interactions (e.g. on-chain deposit, on-chain state submission requests, transfers, app session operations etc.).
+
+Rules:
+
+- Normalization is used **only for cross-chain comparisons** (e.g. validating that escrow amounts match across chains). It is not used for storage or accounting — stored values remain in their chain-native precision.
+- The asset's configured decimal precision acts as the base, whereas 18 is the target of the upscaling. The maximum supported decimal precision is 18.
+- Normalization is exact and lossless when scaling up. No rounding or remainder occurs.
+- The blockchain layer validates that declared decimals match the actual token decimals on the current chain.
+
+## Home Chain
+
+The home chain is the blockchain against which a given channel state is enforced. It is identified by the chain identifier in the home ledger of that state.
+
+The home chain determines:
+
+- where enforcement operations for that state are executed
+- which blockchain holds the locked funds for the channel
+- the authoritative source for state validation
+
+The home chain MAY change over the lifetime of a channel through a migration operation. After migration, the new home chain becomes the authoritative enforcement target.
+
+## Home and Non-Home Ledger Roles
+
+**Home Ledger**
+The home ledger is the primary record of asset allocations. It is associated with the home chain and is directly enforceable through the blockchain layer.
+
+Responsibilities:
+
+- tracks the authoritative asset allocations
+- receives checkpoints for enforcement
+- holds deposited assets in the enforcement contract
+
+**Non-Home Ledger**
+The non-home ledger tracks asset allocations on a blockchain other than the home chain. When no cross-chain operation is in progress, the non-home ledger MUST be empty (see [Empty Non-Home Ledger](state-model.md#empty-non-home-ledger)).
+
+Responsibilities:
+
+- tracks assets involved in cross-chain escrow operations
+- reflects cross-chain deposit and withdrawal allocations
+- coordinates with the home ledger for consistency
+
+## Escrow Model
+
+Cross-chain operations use an **escrow** mechanism to coordinate fund movements across two independent blockchains.
+
+An escrow is a temporary on-chain record that locks funds on one chain while a corresponding state update is being finalized on the other chain. Each escrow is identified by an **escrow channel identifier**, derived deterministically from the home channel identifier and the state version at initiation.
+
+| Property | Description |
+| -------------- | --------------------------------------------------------------- |
+| Identifier | 32-byte hash derived from the home channel identifier and state version |
+| Hosting chain | The non-home chain (for deposits: where the user's funds are locked; for withdrawals: where the node's funds are locked) |
+| Tracked amount | The amount locked in escrow, corresponding to the non-home ledger allocations |
+| Unlock delay | Escrow deposits include an unlock delay after which funds are automatically unlocked to the node if not challenged |
+| ChallengeDuration | A period after a challenge was initiated that allows resolution. If no finalization state was supplied, the initiate state is finalized, and funds are returned |
+
+An escrow is not a separate protocol entity with its own state — it is an on-chain record derived from a channel state transition. The escrow exists only between initiation and finalization (or timeout).
+
+## Cross-Chain Deposit
+
+To deposit assets from a non-home chain into a channel, the protocol uses a two-phase escrow process:
+
+1. **Initiate (Escrow Deposit Initiate)** — participants sign a state that creates an escrow. On the home chain, the node's allocation increases to reserve funds. On the non-home chain, the user's deposit is locked in an escrow record with an unlock delay.
+2. **Finalize (Escrow Deposit Finalize)** — after the escrow is created, participants sign a state that completes the deposit. On the home chain, the user's allocation increases by the deposited amount. On the non-home chain, the escrowed funds are released to the node's vault.
+
+If the escrow is not finalized within the unlock delay, the escrowed funds on the non-home chain are automatically unlocked to the Node. Either participant MAY challenge the escrow during the challenge period. Note that it is NOT possible to challenge a deposit escrow after unlock delay has passed as the funds were already unlocked to the Node.
+
+Cross-chain amounts are validated using WAD normalization to ensure the home-chain node allocation matches the non-home-chain user deposit.
+
+## Cross-Chain Withdrawal
+
+To withdraw assets to a non-home chain, the protocol uses a similar two-phase escrow process:
+
+1. **Initiate (Escrow Withdrawal Initiate)** — participants sign a state that creates an escrow. On the non-home chain, the node locks funds from its vault into the escrow record.
+2. **Finalize (Escrow Withdrawal Finalize)** — participants sign a state that completes the withdrawal. On the home chain, the user's allocation decreases. On the non-home chain, the escrowed funds are released to the user.
+
+If the escrow is not finalized cooperatively, either participant MAY challenge the escrow.
+
+## Home Chain Migration
+
+The home chain of a channel MAY be changed through a two-phase migration process:
+
+1. **Initiate (Migration Initiate)** — participants sign a state that begins the migration. On the current home chain, the state records the target chain allocation. On the target chain, a new channel record is created with status "migrating in" and the node locks funds equal to the user's allocation (validated via WAD normalization).
+2. **Finalize (Migration Finalize)** — participants sign a state that completes the migration. On the new home chain, the channel transitions to operating status. On the old home chain, all locked funds are released to the node and the channel is marked as migrated out.
+
+After migration, the following changes take effect:
+
+- **Home chain identifier** is updated to reflect the migration
+- **Home token address** is updated to reflect the migration
+- **Ledger roles** — the former non-home ledger becomes the home ledger; the former home ledger becomes the non-home ledger (and its allocations are zeroed out on finalization)
+- **Enforcement target** — all subsequent enforcement operations execute against the new home chain
+- **Balances** — the user's allocation is preserved (normalized by decimal precision); the node's allocation is recalculated for the new chain
+
+VERSION NOTE: Migration transitions are functional but may be refined in future protocol versions.
+
+## Cross-Chain Replay Protection
+
+The protocol prevents cross-chain replay through multiple binding mechanisms:
+
+- **Chain identifier binding** — each ledger is bound to a specific chain identifier. The blockchain layer validates that the home ledger chain identifier matches the current blockchain. This prevents a state signed for one chain from being enforced on another.
+- **Channel identifier scoping** — channel identifiers incorporate a protocol version byte, preventing replay across smart contract deployments. The same channel definition on a different protocol version produces a different channel identifier.
+- **Escrow identifier uniqueness** — escrow channel identifiers are derived from the home channel identifier and the state version at initiation. This ensures that each escrow operation produces a unique identifier, preventing a completed escrow from being replayed.
+- **Ledger validation** — on-chain enforcement validates that both home and non-home ledger's declared decimals match the actual token decimals on the current execution chain, preventing states crafted for a different token from being accepted. Additionally, a specific set of invariants is enforced for security purposes.
+
+## Current Version Notes
+
+In the current protocol version:
+
+- Cross-chain operations require trust in the node to relay state correctly between chains. The node is responsible for submitting escrow initiation and finalization transactions on the appropriate chains.
+- Full cross-chain enforcement (trustless bridging) is a planned future improvement.
+- Each channel state supports exactly two ledgers: one home ledger and one non-home ledger. This is a V1-specific design constraint; future protocol versions MAY support additional ledger configurations.
+
+---
+
+Previous: [Enforcement and Settlement](enforcement.md) | Next: [Interactions](interactions.md)
diff --git a/docs/protocol/cryptography.md b/docs/protocol/cryptography.md
new file mode 100644
index 000000000..226123b4e
--- /dev/null
+++ b/docs/protocol/cryptography.md
@@ -0,0 +1,122 @@
+# Cryptography
+
+Previous: [Terminology](terminology.md) | Next: [State Model](state-model.md)
+
+---
+
+This document defines how protocol objects are encoded, hashed, and signed.
+
+All rules are described as algorithms and canonical procedures, independent of any specific programming language.
+
+## Purpose
+
+Cryptography in the Nitrolite protocol serves three functions:
+
+1. **Authentication** — proving that a specific participant authorized a state update
+2. **Integrity** — ensuring that signed data has not been modified
+3. **Replay protection** — preventing previously signed states from being reused in unintended contexts
+
+## Cryptographic Algorithms
+
+The protocol uses the following cryptographic primitives.
+
+**Signature Algorithm**
+ECDSA over the secp256k1 curve, producing a 65-byte signature (r, s, v).
+
+**Hash Function**
+Keccak-256, producing a 32-byte digest.
+
+## Canonical Encoding
+
+Protocol objects that require signing MUST be encoded into a canonical binary representation before hashing.
+
+The canonical encoding uses RLP encoding (`abi.encode` in Solidity) as defined in [this paper](https://doi.org/10.48550/arXiv.2009.13769) and by [Ethereum documentation](https://ethereum.org/developers/docs/data-structures-and-encoding/rlp/). This ensures deterministic byte sequences regardless of implementation language.
+
+## Message Digest Construction
+
+The digest of a signable payload is constructed as follows:
+
+1. Encode the object using canonical encoding
+2. Prepend the EIP-191 personal message prefix: the ASCII string `"\x19Ethereum Signed Message:\n"` followed by the decimal length of the encoded bytes, then the encoded bytes themselves
+3. Compute the Keccak-256 hash of the prefixed message
+
+The resulting 32-byte digest is the value that is signed.
+
+## ECDSA Signature Format
+
+The raw ECDSA signature consists of:
+
+| Field | Size | Description |
+| ----- | -------- | ------------------------ |
+| R | 32 bytes | ECDSA r component |
+| S | 32 bytes | ECDSA s component |
+| V | 1 byte | Recovery identifier |
+
+The signer's address is recovered from the signature and the message digest. The protocol does not transmit the signer's public key or address alongside the signature.
+
+## Protocol Signature Envelope
+
+A protocol signature is a wrapper around the raw ECDSA signature that includes a validation mode prefix:
+
+```
+ProtocolSignature = ValidationMode || SignatureData
+```
+
+The first byte (`ValidationMode`) determines the validation method, which must map to a signature validator registered by the Node on the Smart Contract infrastructure. The remaining bytes (`SignatureData`) contain mode-specific data including the raw signature.
+
+## Signature Validation Modes
+
+The protocol supports multiple signature validation modes to allow different key types and authorization schemes.
+
+**Default Mode (0x00)**
+Standard ECDSA signature validation. SignatureData contains the raw ECDSA signature (R, S, V). The signer's address is recovered from the signature. The recovered address MUST match the expected participant address.
+
+**Session Key Mode (0x01)**
+Delegated signature validation. SignatureData contains a session key authorization and the session key's ECDSA signature over the state data, ABI-encoded as a tuple. The validator first verifies that the participant authorized the session key, then verifies that the session key produced a valid signature over the state. The session key authorization MUST be associated with the same address as the channel's user or node participant. The recovered session key address MUST match the address authorized by the participant.
+
+Session-key signatures are valid for both off-chain state advancement and on-chain enforcement, provided the session key validation mode is among the channel's approved signature validators.
+
+## Signable Object Classes
+
+The protocol defines a general signing framework that accommodates multiple classes of signable objects:
+
+- **Channel Objects**: primarily, the state of a channel, but also a session key registration and challenger signature
+- **Extension Objects**: primarily, the state of an extension entity (such as an application session), signed by the relevant session participants
+
+Please note that channel and extension states are identified by a unique entity identifier and follows the same canonical encoding and digest construction rules.
+
+This framework is extensible: future protocol extensions MAY introduce additional signable object classes without requiring changes to the core signing rules.
+
+## Session Key Authorization
+
+A participant MAY delegate signing authority to a session key.
+
+The authorization is constructed as follows:
+
+1. The participant signs a message containing:
+ - the session key address
+ - authorization metadata hash (`keccak256` over scope, expiration and possible other data)
+2. The authorization signature is produced using the participant's primary key
+3. The session key MAY then produce signatures on behalf of the participant within the authorized scope
+
+Session key signatures MUST include the authorization proof alongside the session key signature. The authorization proof is canonically encoded as a tuple containing the session key authorization and the raw signature bytes.
+
+## Replay Protection
+
+The protocol prevents replay attacks through the following mechanisms:
+
+**Entity Identifier**
+Each signable entity has a unique identifier derived from its definition. Signed states are bound to a specific entity, preventing a signature over one entity's state from being replayed against another.
+
+**State Version**
+Each state includes a monotonically increasing version number. The blockchain layer MUST reject states with a version less than or equal to the currently enforced version.
+
+**Blockchain Identifier**
+States include blockchain-specific identifiers preventing cross-chain replay.
+
+**Smart Contract Version**
+The channel entity identifier incorporates a contract version (currently as the first byte), preventing replay across different deployments.
+
+---
+
+Previous: [Terminology](terminology.md) | Next: [State Model](state-model.md)
diff --git a/docs/protocol/enforcement.md b/docs/protocol/enforcement.md
new file mode 100644
index 000000000..ae8820863
--- /dev/null
+++ b/docs/protocol/enforcement.md
@@ -0,0 +1,176 @@
+# State Enforcement
+
+Previous: [Channel Protocol](channel-protocol.md) | Next: [Cross-Chain and Assets](cross-chain-and-assets.md)
+
+---
+
+This document describes how channel states are enforced on the blockchain layer.
+
+## Purpose
+
+Enforcement is the mechanism by which off-chain state is reflected on-chain. It serves two complementary roles:
+
+1. **Regular state synchronization** — participants submit signed states to the blockchain layer to keep the on-chain record up-to-date with the latest off-chain state, particularly for transitions that require on-chain effects (deposits, withdrawals, escrow operations, migrations)
+2. **Dispute resolution** — any participant MAY independently submit the latest mutually signed state to the blockchain layer to protect their assets if off-chain cooperation fails
+
+The blockchain layer acts as the ultimate arbiter of channel state, providing security guarantees that do not depend on participant cooperation.
+
+## Enforceable State Requirements
+
+A state is enforceable on-chain if and only if:
+
+- It is **mutually signed** — it carries valid signatures from both the user and the node
+- The signatures use validation modes that are among the channel's approved signature validators (including session-key signatures if the session key validation mode is approved)
+- The state has passed off-chain state advancement validation
+- The node has sufficient balance on the target chain to cover any required fund locking
+
+Node-issued pending states (those carrying only the node's signature) are NOT enforceable. They become enforceable only after the user acknowledges them, producing a mutually signed state.
+
+## Enforcement Model
+
+Off-chain states and on-chain enforcement states are related as follows:
+
+- Participants advance state off-chain through signed updates
+- At any time, any party MAY submit the latest mutually signed state to the blockchain layer
+- The blockchain layer validates the submitted state and updates its record
+- On-chain state always reflects the latest successfully checkpointed state
+
+The on-chain state MAY lag behind the off-chain state. This is expected during normal operation for transitions with the OPERATE intent.
+
+## Locked Funds Model
+
+The blockchain layer tracks **locked funds** for each channel. Locked funds represent the total assets held by the enforcement contract on behalf of the channel.
+
+Rules:
+
+- Locked funds increase when assets are pulled from the user or from the node's vault into the channel
+- Locked funds decrease when assets are released to the user or to the node
+- Unless the channel is being closed, the sum of UserAllocation and NodeAllocation in the enforced state MUST equal the locked funds
+- Locked funds MUST never be negative
+
+The node maintains a **vault balance** per token on each chain. The vault is a pool of available funds separate from any specific channel. When a transition requires the node to lock additional funds, the required amount is deducted from the node's vault balance and added to the channel's locked funds.
+
+| Operation | User Fund Effect | Node Fund Effect | Locked Funds Effect |
+| ---------- | ----------------------------------------- | ------------------------------------------ | ----------------------------- |
+| DEPOSIT | Pull from user (positive delta) | Adjusted by node net flow delta | Increases by total deltas |
+| WITHDRAW | Release to user (negative delta) | Adjusted by node net flow delta | Decreases by total deltas |
+| OPERATE | No user fund movement | Adjusted by node net flow delta | Adjusted by node delta only |
+| CLOSE | Release UserAllocation to user | Release NodeAllocation to node | Set to zero |
+| Challenge | No fund movement | No fund movement | Unchanged (status changes) |
+
+## Channel Creation
+
+Channels are created through an enforcement operation. A channel does not need to be created on-chain with its initial off-chain-created state — any validly signed state MAY be used for on-chain creation, provided the channel does not yet exist on-chain. This allows participants to advance state off-chain before enforcing the channel on-chain, e.g. when the user's first action is to receive a transfer from another user, they can additionally perform several transfer send or receive operations before submitting the state on-chain with a "WITHDRAW" intent, receiving funds simultaneously with creating a channel, both on-chain.
+
+The creation process:
+
+1. Participants agree on a channel definition and exchange signed state updates off-chain
+2. A participant submits the channel definition and a signed state to the blockchain layer
+3. The blockchain layer validates signatures, creates the channel record, and applies fund effects according to the state's intent
+4. The channel is now active on the on-chain layer
+
+The state submitted for channel creation MAY carry a DEPOSIT, WITHDRAW, or OPERATE intent.
+
+## State Submission
+
+State submission covers checkpoint, deposit, and withdrawal operations. The general process is identical for all three:
+
+1. A participant constructs the enforcement representation of a signed state
+2. The participant submits the enforcement representation along with all required signatures to the blockchain layer
+3. The blockchain layer validates the submission
+4. If valid, the on-chain state is updated and fund movements are applied
+
+The behaviour differs only in intent-specific validation rules:
+
+- **OPERATE** — the blockchain layer validates that the user net flow has not changed and that the node allocation is zero. No user fund movement occurs.
+- **DEPOSIT** — the blockchain layer validates that the user net flow delta is positive (assets are flowing in). The deposited amount is pulled from the user and added to the channel's locked funds.
+- **WITHDRAW** — the blockchain layer validates that the user net flow delta is negative (assets are flowing out). The withdrawn amount is released from the channel's locked funds to the user.
+
+In all cases, the node's fund delta is adjusted according to the node net flow change.
+
+## Challenge Operation
+
+A challenge allows a participant to dispute the current on-chain state by submitting a signed state along with a separate challenger signature.
+
+### Challenger Signature
+
+The challenger signature is distinct from the state signatures. It is produced by signing the enforcement representation of the candidate state with the string "challenge" appended to the signing data. This guarantees that only a User or a Node can start a challenge, and not the third-party. However, a channel participant MAY share a valid challenger signature with a third-party, who then can successfully initiate a challenge.
+
+**Only** the user or the node MAY act as the challenger.
+
+### Challenge Process
+
+1. The challenger submits a candidate state, state signatures, the challenger signature, and the challenger's participant index
+2. The channel MUST NOT be in DISPUTE, MIGRATED_OUT or CLOSED statuses
+3. The candidate version MUST be greater than or equal to the current on-chain version
+4. If the candidate version is strictly greater than the current on-chain version, the blockchain layer validates and applies the new state (including fund effects)
+5. The channel status is set to **DISPUTED** and the challenge expiry is set to the current time plus the challenge duration
+
+### Resolving a Challenge
+
+During the challenge period, any participant MAY respond by submitting a new valid state whose version is strictly greater than the currently disputed state. This replaces the disputed state, changes channel's status (transitions out from DISPUTED) and clears the challenge timer.
+
+It should be noted that it is NOT possible to file another challenge on a channel that is already disputed. The current challenge must be resolved first.
+
+Additionally, it is possible to close the channel unilaterally by submitting a valid "CLOSE" state (if present) even after a channel was challenged. In such case, the channel will transition to CLOSED status immediately, transferring out all funds to the User and the Node according to amounts agreed about in the CLOSE state.
+
+### Challenge Finality
+
+After the challenge period expires without being resolved, the disputed state becomes **final**. However, a separate **close call** is still required to release the channel's locked funds. Such close call does not require any state to be submitted alongside, only the id of a channel, and can be invoked by anyone.
+
+## Close Operation
+
+A close releases the channel's locked funds and terminates the channel lifecycle.
+
+Two paths exist:
+
+**Cooperative close** — a participant submits a state with the CLOSE intent, signed by all participants. The blockchain layer validates that amounts from the allocations are moved to the respective net flows (basically, it is a withdrawal operation). It should be noted that it is not possible to close an already CLOSED or MIGRATED_OUT channel.
+
+**Unilateral close** — after a challenge period has expired, any party MAY call close without additional signatures. The blockchain layer releases assets according to the last enforced state's allocations (UserAllocation to the user, NodeAllocation to the node).
+
+In both cases, the channel's locked funds are set to zero and the channel lifecycle ends.
+
+## Enforcement Validation
+
+The blockchain layer applies the following common validation rules when processing any enforcement operation:
+
+1. The submitted state MUST reference the correct channel identifier
+2. The home ledger chain identifier MUST match the current blockchain
+3. The state version MUST be strictly greater than the currently recorded version
+4. All required signatures MUST be present and valid
+5. The approved signature validation modes MUST be respected
+6. The ledger invariant MUST hold: UserAllocation + NodeAllocation == UserNetFlow + NodeNetFlow
+7. The resulting locked funds (previous locked funds plus user and node fund deltas) MUST be non-negative
+8. Unless the channel is being closed, the sum of allocations MUST equal the resulting locked funds
+9. The node MUST have sufficient available funds in its vault when required to lock additional assets
+
+## Escrow and Migration Enforcement
+
+Cross-chain transitions are enforced through dedicated operations on the blockchain layer. The detailed escrow model is described in [Cross-Chain and Assets](cross-chain-and-assets.md). The following summarizes the on-chain effects:
+
+| Operation | On-Chain Effect |
+| -------------------------- | --------------------------------------------------------------------------- |
+| Escrow Deposit Initiate | On home chain: state updated, node funds adjusted. On non-home chain: escrow record created, user funds locked. |
+| Escrow Deposit Finalize | On home chain: state updated, user allocation increased. On home chain: state updated, node funds adjusted. On non-home chain: escrow record created, user funds locked, automatic release to the Node timer started. |
+| Escrow Withdrawal Initiate | On home chain: state updated. On non-home chain: escrow record created, node funds locked from vault. |
+| Escrow Withdrawal Finalize | On home chain: state updated, user allocation decreased. On non-home chain: escrowed funds released to user. |
+| Migration Initiate | On old home chain: state updated. On new home chain: channel created with migrating-in status, node funds locked. |
+| Migration Finalize | On new home chain: channel transitions to operating. On old home chain: all locked funds released, channel marked as migrated out. |
+
+## Failure Conditions
+
+Enforcement MAY fail in the following situations:
+
+- **Invalid signatures** — one or more signatures cannot be verified
+- **Stale version** — the submitted state version is not greater than the current on-chain version
+- **Inconsistent allocations** — the ledger invariant is violated or resulting locked funds would be negative
+- **Allocation-locked-funds mismatch** — the sum of allocations does not equal the expected locked funds (except during close)
+- **Unknown channel** — the channel identifier does not correspond to a registered channel (except for channel creation)
+- **Insufficient node funds** — the node's vault does not have enough assets to cover required fund locking
+- **Invalid intent** — the transition intent does not match the expected operation
+- **Chain mismatch** — the home / non-home ledger chain identifier does not match the current blockchain during home-chain / escrow operations
+- **Incorrect channel status** — the operation is not permitted in the channel's current status (e.g. challenging an already challenged channel)
+
+---
+
+Previous: [Channel Protocol](channel-protocol.md) | Next: [Cross-Chain and Assets](cross-chain-and-assets.md)
diff --git a/docs/protocol/interactions.md b/docs/protocol/interactions.md
new file mode 100644
index 000000000..6a3c90ceb
--- /dev/null
+++ b/docs/protocol/interactions.md
@@ -0,0 +1,124 @@
+# Interaction Model
+
+Previous: [Cross-Chain and Assets](cross-chain-and-assets.md) | Next: [Security and Limitations](security-and-limitations.md)
+
+---
+
+This document defines the logical communication protocol between participants.
+
+All operations are defined as semantic protocol operations, independent of transport technologies such as WebSocket or gRPC.
+
+## Purpose
+
+Participants exchange protocol messages to advance state, manage channels, and coordinate operations. This document defines the structure and semantics of those messages.
+
+## Connection Assumptions
+
+The protocol assumes the following about the communication channel:
+
+- Messages are delivered reliably (no silent loss)
+- Messages are delivered in order between any two participants
+- The transport supports bidirectional message exchange
+
+The protocol does not require a specific transport technology.
+
+## Message Envelope
+
+All protocol messages share a common envelope structure.
+
+| Field | Description |
+| --------- | --------------------------------------------------- |
+| Type | Message type (request, response, event, or error) |
+| RequestId | Numeric identifier unique within the connection |
+| Method | Operation name identifying the requested action |
+| Payload | Type-specific message data |
+| Timestamp | Time the message was created, in milliseconds |
+
+Messages are encoded as compact ordered arrays: [Type, RequestId, Method, Payload, Timestamp].
+
+## Message Types
+
+| Type |
+| ---------------------------------------- |
+| Request |
+| Successful response |
+| Event notification |
+| Error response |
+
+## Core Operations
+
+The protocol defines the following core operations:
+
+| Operation | Direction | Description |
+| ----------------- | ------------- | ---------------------------------------------- |
+| RequestCreation | User → Node | Request to create a new channel |
+| SubmitState | User → Node | Submit a signed state transition |
+| GetLatestState | User → Node | Retrieve the current state for a channel |
+| GetHomeChannel | User → Node | Retrieve on-chain home channel data |
+| GetEscrowChannel | User → Node | Retrieve on-chain escrow channel data |
+
+### Operation: RequestCreation
+
+Creates a new channel with an initial state.
+
+The request MUST include the channel definition parameters, the initial state and the user's signature over it. The node validates the channel definition, computes the channel identifier, verifies the user's signature, co-signs the state, and stores the channel record.
+
+The response includes Node's signature over the submitted state.
+
+### Operation: SubmitState
+
+Submits a user-signed state transition for processing.
+
+The request MUST include the signed state with a valid transition. The node validates the state against advancement rules, verifies the user's signature, co-signs the state, and applies any side effects (e.g. scheduling blockchain operations for non-OPERATE intents, creating receiver states for transfers).
+
+The response includes Node's signature over the submitted state.
+
+### Operation: GetLatestState
+
+Retrieves the current state for a given user and asset.
+
+The response includes the latest state. Implementations MAY support filtering to return only mutually signed states.
+
+### Operation: GetHomeChannel
+
+Retrieves the on-chain home channel data for a given user and asset.
+
+### Operation: GetEscrowChannel
+
+Retrieves the on-chain escrow channel data for a given escrow channel identifier.
+
+## Event Messages
+
+The event message system is reserved for future specification. Events are asynchronous notifications generated by the protocol and are not responses to specific requests.
+
+## Correlation and Identifiers
+
+Responses are correlated with requests using the RequestId field.
+
+Rules:
+
+- Each request MUST include a RequestId unique within the connection
+- The corresponding response MUST include the same RequestId
+
+## Error Handling
+
+Errors are communicated through error response messages.
+
+Rules:
+
+- Every failed operation MUST return an error response
+- The error payload MUST contain a human-readable error message
+- Errors MUST NOT expose internal implementation details
+
+## Message Ordering
+
+Message ordering requirements MAY depend on the implementation. The following constraints apply at the protocol level:
+
+- RequestId values MUST NOT be reused within a single connection
+- Events MAY arrive at any time and MUST NOT block request processing
+
+State update ordering (version sequencing) is governed by the [Channel Protocol](channel-protocol.md) and is not a concern of the message transport layer.
+
+---
+
+Previous: [Cross-Chain and Assets](cross-chain-and-assets.md) | Next: [Security and Limitations](security-and-limitations.md)
diff --git a/docs/protocol/overview.md b/docs/protocol/overview.md
new file mode 100644
index 000000000..58da06d92
--- /dev/null
+++ b/docs/protocol/overview.md
@@ -0,0 +1,97 @@
+# Nitrolite Protocol Overview
+
+Nitrolite is a state channel protocol that enables high-speed off-chain interactions between users while preserving on-chain security guarantees.
+
+Users exchange signed state updates off-chain with Nodes that act as a hub connecting network participants. Any user can enforce the latest agreed state on the blockchain layer at any time.
+
+## Table of Contents
+
+1. [Overview](overview.md) — high-level protocol description and design goals
+2. [Terminology](terminology.md) — canonical definitions of all protocol terms
+3. [Cryptography](cryptography.md) — encoding, hashing, signing, and replay protection
+4. [State Model](state-model.md) — state structure, versioning, and consistency rules
+5. [Channel Protocol](channel-protocol.md) — channel lifecycle, transitions, and advancement rules
+6. [State Enforcement](enforcement.md) — checkpoints, on-chain validation, and enforcement
+7. [Cross-Chain and Assets](cross-chain-and-assets.md) — unified asset model and cross-chain operations
+8. [Interactions](interactions.md) — message envelope, core operations, and events
+9. [Security and Limitations](security-and-limitations.md) — security guarantees, trust assumptions, and known limitations
+10. [Extensions](extensions/overview.md) — extension model, lifecycle, and safety constraints
+
+
+## Design Goals
+
+The protocol is designed to achieve:
+
+- **Off-chain scalability** — minimize on-chain transactions by moving state advancement off-chain
+- **Blockchain security guarantees** — any user can fall back to the blockchain layer to enforce the latest state
+- **Cross-chain asset interaction** — operate on assets across multiple blockchains through a unified model
+- **Extensibility** — support additional functionality through protocol extensions without modifying the core protocol
+
+## System Roles
+
+The protocol defines the following roles.
+
+**User**
+An entity that opens channels, signs state updates, and holds assets within the protocol.
+
+**Node**
+An entity that facilitates off-chain state advancement, manages channels, and syncs with the blockchain layer.
+
+**Blockchain**
+The on-chain storage and execution layer that validates enforceable incoming states according to the protocol rules, stores states and resolves disputes.
+
+## High-Level Architecture
+
+The system operates in three conceptual layers:
+
+1. **Protocol layer** — defines rules for state validity, advancement, and enforcement
+2. **Off-chain layer** — signed state updates exchange with a node
+3. **Blockchain layer** — blockchain contracts that hold assets and enforce states
+
+## Core Protocol Concepts
+
+**Channels**
+A channel is a state container shared between a Node and a User. It holds user asset allocations and supports off-chain state updates. Each channel is defined by immutable parameters including the participants, asset, challenge duration, and approved signature validators.
+
+**States**
+A state represents the current agreed asset allocations and metadata shared between a Node and a User. Each state contains two ledgers (home and non-home), a version number, and a transition describing the operation that produced it.
+
+**State Advancement**
+User and a node advance states off-chain by exchanging signed state transitions. Each new state MUST have a version exactly one greater than the previous state. Transitions include deposits, withdrawals, transfers, commits, releases, escrow operations, and migrations.
+
+**State Enforcement**
+Any party MAY submit the latest signed state to the blockchain layer for on-chain enforcement. The blockchain layer validates signatures, version ordering, and ledger invariants before accepting a state.
+
+**Unified Assets**
+The same asset from multiple blockchains is represented in a unified model, enabling cross-chain operations among users and apps. The protocol normalizes amounts by decimal precision when comparing allocations across chains.
+
+**Extensions**
+Additional protocol functionality, such as application sessions, is provided through the extension layer without modifying core protocol rules. Extensions interact with channels through commit and release transitions.
+
+## Protocol Layers
+
+The protocol separates responsibilities into distinct layers.
+
+**Core Protocol**
+Defines channels, states, state advancement rules, and enforcement mechanisms.
+
+**Extension Layer**
+Provides additional functionality such as application sessions. Extensions interact with the core protocol through defined interfaces.
+
+**Blockchain Layer**
+Blockchain contracts that create channels, hold deposits, accept state checkpoints, manage escrow operations, and release funds.
+
+## Protocol Version
+
+This documentation describes Nitrolite Protocol V1.
+
+Compatibility expectations:
+
+- State structures and signing rules defined in this version are stable
+- Extension interfaces may evolve in future versions
+- Blockchain layer contracts are version-specific
+- ChannelIDs are generated by including protocol version into a hashing function to prevent cross-version replay
+
+---
+
+Next: [Terminology](terminology.md)
diff --git a/docs/protocol/security-and-limitations.md b/docs/protocol/security-and-limitations.md
new file mode 100644
index 000000000..c7dae6e2e
--- /dev/null
+++ b/docs/protocol/security-and-limitations.md
@@ -0,0 +1,92 @@
+# Security and Limitations
+
+Previous: [Interactions](interactions.md) | Next: [Extensions Overview](extensions/overview.md)
+
+---
+
+This document describes the security guarantees of the Nitrolite protocol, its current trust assumptions, and the known limitations of the present version.
+
+## Protocol Maturity
+
+The core protocol functionality is implemented and operational. A user MAY operate over a unified asset, deposit and withdraw on any supported blockchain, and conduct the majority of interactions without direct blockchain involvement. The protocol provides protection against unauthorized state changes from the user side — no user can unilaterally alter the state without valid signatures from all required participants.
+
+However, the protocol in its current form is not fully trust-minimized. The primary remaining trust assumption concerns node behaviour and liquidity, as described in the sections below. The protocol is under active development, with planned improvements to address these limitations.
+
+## Security Goals
+
+The protocol aims to guarantee:
+
+- **Asset safety** — participants MUST NOT lose assets without signing a state that authorizes the change
+- **State finality** — the latest mutually signed state can always be enforced on-chain
+- **Non-repudiation** — a participant cannot deny having signed a state
+- **Censorship resistance** — any party MAY independently enforce state on the blockchain layer
+
+## Off-Chain Safety
+
+The protocol protects against invalid or malicious state submissions through:
+
+**Signature requirements**
+Every state update requires valid signatures from all required participants. No participant can unilaterally change the state.
+
+**Version ordering**
+State versions are strictly increasing. Old states cannot replace newer states.
+
+**Asset conservation**
+State transitions MUST preserve total asset amounts within each ledger. No assets can be created or destroyed through state updates.
+
+**Transition validation**
+Each state update MUST satisfy transition-specific rules. Invalid transitions are rejected.
+
+## Enforcement Guarantees
+
+The blockchain layer provides the following guarantees:
+
+- Any party MAY submit the latest signed state at any time
+- The blockchain layer accepts only states with valid signatures and a higher version than the current on-chain state
+- After the challenge period, the enforced state becomes final
+- Final state allocations determine asset distribution
+
+## Node Liquidity and Cross-Chain Trust
+
+Each user channel is opened with a node. To maintain cross-chain functionality, the node MUST hold sufficient liquidity on each supported blockchain to satisfy off-chain state allocations.
+
+When a user with home chain A transfers assets to a user with home chain B, the node receives the amount on chain A and allocates from its own balance to the recipient on chain B. This process occurs entirely off-chain. If the recipient subsequently wishes to enforce their state on chain B and the node does not hold sufficient liquidity on that chain, the on-chain enforcement will fail.
+
+In the current protocol version, this constitutes a trust assumption: users rely on the node operator to maintain adequate liquidity across all supported chains. Node operators are expected to manage their liquidity to cover off-chain obligations, but users cannot independently verify that this condition holds at all times.
+
+## Current Trust Assumptions
+
+In the current protocol version, participants MUST trust nodes for:
+
+- **Liveness** — nodes MUST be online to facilitate off-chain state advancement
+- **Cross-chain liquidity** — nodes MUST maintain sufficient funds on each supported chain to honour off-chain allocations; insufficient liquidity may cause on-chain enforcement to fail
+- **Cross-chain relay** — nodes relay cross-chain state updates; trustless cross-chain enforcement is not yet implemented
+- **Timely enforcement** — nodes are expected to submit checkpoints when requested; delayed enforcement may affect user experience but does not compromise single-chain asset safety
+
+Participants do not need to trust nodes for:
+
+- **Single-chain asset custody** — assets on the home chain can always be recovered through on-chain enforcement
+- **State validity** — invalid states are rejected by signature and validation rules
+
+## Known Limitations
+
+The following capabilities are not yet implemented:
+
+- Trustless off-chain state operations (node liquidity enforcement)
+- Validator network for monitoring node behaviour and enforcing correctness
+- Watchtower services for automated enforcement
+- Support for non-EVM blockchains
+- Formal verification of protocol rules
+
+## Future Improvements
+
+The protocol roadmap includes the following planned improvements:
+
+- **Validator network** — off-chain state advancement can be independently validated; a validator network would monitor on-chain actions and penalize node misbehaviour that harms the ecosystem
+- **Extension layer on-chain enforcement** — removing the reliance on node liquidity trust for extension layer operations
+- **Non-EVM blockchain support** — redesigning the protocol to support blockchains beyond the EVM ecosystem (planned for V2)
+- **Watchtower integration** — automated monitoring and enforcement on behalf of users
+
+---
+
+Previous: [Interactions](interactions.md) | Next: [Extensions Overview](extensions/overview.md)
diff --git a/docs/protocol/state-model.md b/docs/protocol/state-model.md
new file mode 100644
index 000000000..51543782d
--- /dev/null
+++ b/docs/protocol/state-model.md
@@ -0,0 +1,175 @@
+# State Model
+
+Previous: [Cryptography](cryptography.md) | Next: [Channel Protocol](channel-protocol.md)
+
+---
+
+This document describes the abstract structure of protocol states.
+
+It explains how states are defined and structured. Operational flows are described in separate documents.
+
+## Purpose
+
+States represent the current agreed configuration of protocol entities. The state model defines:
+
+- what information a state contains
+- how states are identified and versioned
+- how states are represented for off-chain and on-chain use
+
+## Common State Fields
+
+All protocol states share the following common properties:
+
+| Field | Description |
+| -------- | -------------------------------------------------------------- |
+| EntityId | 32-byte unique identifier of the entity this state belongs to |
+| Version | 64-bit unsigned integer, monotonically increasing |
+
+In addition to these common fields, each state contains entity-specific data whose structure varies depending on the entity type and use case. The entity-specific data is defined by the respective entity specification.
+
+## State Identification and Versioning
+
+Each state is identified by the combination of its entity identifier and version number.
+
+Rules:
+
+- The entity identifier is derived from the entity definition and is immutable
+- The version MUST start at 1 for the initial state
+- Versions are strictly increasing; the exact increment rule depends on the context:
+ - Off-chain state advancement requires each new version to be exactly the previous version plus one
+ - On-chain enforcement requires only that the submitted version be strictly greater than the currently recorded on-chain version
+
+## Channel State
+
+The channel state is the primary protocol state. It represents the current configuration of a channel.
+
+| Field | Description |
+| ------------- | ------------------------------------------------------ |
+| ChannelId | 32-byte identifier derived from the channel definition |
+| Metadata | 32-byte Hash of channel metadata |
+| Version | 64-bit unsigned integer, state version |
+| HomeLedger | Asset allocations on the home chain |
+| NonHomeLedger | Asset allocations on the non-home chain |
+| Transition | Describes the operation that produced this state |
+| UserSig | User signature for the state |
+| NodeSig | Node signature for the state |
+
+The channel identifier encodes a protocol version byte as its first byte, followed by the hash of the channel definition parameters. This ensures uniqueness across protocol deployments.
+
+### Ledger
+
+A ledger records asset allocations for a specific blockchain within a channel. Each channel state contains exactly two ledgers: a home ledger and a non-home ledger.
+
+| Field | Description |
+| -------------- | ------------------------------------------------------------ |
+| ChainId | Identifier of the blockchain this ledger is associated with |
+| Token | Token contract address on this chain |
+| Decimals | Decimal precision of the token on this chain |
+| UserAllocation | Amount allocated to the user |
+| UserNetFlow | Cumulative net flow for the user (may be negative) |
+| NodeAllocation | Amount allocated to the node |
+| NodeNetFlow | Cumulative net flow for the node (may be negative) |
+
+**Ledger invariant:** A ledger MUST satisfy the following invariant at all times:
+
+```
+UserAllocation + NodeAllocation == UserNetFlow + NodeNetFlow
+```
+
+UserNetFlow tracks the cumulative net amount that has flowed into or out of the user's position through deposits, withdrawals, and cross-chain operations. NodeNetFlow tracks the cumulative net amount that has flowed through the node's position, including transfers, commits, and releases. Allocations represent the current distributable balances. The invariant ensures that the total distributable balance always equals the total cumulative flows — no assets can be created or destroyed through state transitions.
+
+All allocation values MUST be non-negative. Net flow values MAY be negative, reflecting outbound transfers or withdrawals that exceed inbound flows.
+
+### Empty Non-Home Ledger
+
+When a channel state does not involve cross-chain operations, the non-home ledger MUST be empty. An empty non-home ledger is defined as a ledger where all fields are set to their zero values:
+
+| Field | Value |
+| -------------- | ------------------------------------------ |
+| ChainId | 0 |
+| Token | Zero address (0x0000...0000) |
+| Decimals | 0 |
+| UserAllocation | 0 |
+| UserNetFlow | 0 |
+| NodeAllocation | 0 |
+| NodeNetFlow | 0 |
+
+An empty non-home ledger is structurally present but zeroed. A non-home ledger with metadata (non-zero ChainId or Token) but zero balances is NOT considered empty.
+
+## Off-Chain Representation
+
+The off-chain representation is the primary operational format of a channel state. It is the representation exchanged between participants during state advancement, and it is the representation that is signed.
+
+The off-chain representation contains all channel state fields directly, including the full transition data (type, transaction identifier, account identifier, and amount). This representation is optimized for human readability, ease of validation, and efficient signature generation.
+
+## Enforcement Representation
+
+The off-chain and on-chain (enforcement) representations depict the **same logical state**. The on-chain (enforcement) representation is derived deterministically from the off-chain one — no additional information is required.
+
+When a state is submitted to the blockchain layer, it uses an enforcement representation optimized for on-chain verification, gas efficiency, and deterministic encoding.
+
+The following fields are preserved exactly from the off-chain representation:
+
+- Version
+- Home and non-home ledger fields (ChainId, Token, Decimals, UserAllocation, UserNetFlow, NodeAllocation, NodeNetFlow)
+
+The following fields are derived:
+
+- **Intent** — derived from the transition type via the intent mapping table
+- **MetadataHash** — the Keccak-256 hash of the ABI-encoded transition data (type, transaction identifier, account identifier, and amount). This captures all off-chain transition information in a single hash, ensuring that the enforcement representation is bound to the specific transition without transmitting the full transition data on-chain.
+
+The enforcement representation is constructed by packing these fields into an ABI-encoded structure:
+
+```
+SignablePayload = AbiEncode(ChannelId, AbiEncode(Version, Intent, MetadataHash, HomeLedger, NonHomeLedger))
+```
+
+Where each ledger is encoded as a tuple of (chain identifier, token address, decimals, user allocation, user net flow, node allocation, node net flow).
+
+Because the mapping is deterministic, both the off-chain and enforcement representations produce the same message digest when signed, ensuring that a signature over the off-chain state is valid for enforcement and vice versa.
+
+## Intent Mapping
+
+Each transition type maps to an intent value used in the enforcement representation. The intent determines how the blockchain layer processes the state.
+
+| On-chain Intent | Transition |
+| -------------------------- | --------------------------- |
+| OPERATE | TransferSend, TransferReceive, Commit, Release, Acknowledgement |
+| CLOSE | Finalize |
+| DEPOSIT | Home Deposit |
+| WITHDRAW | Home Withdrawal |
+| INITIATE_ESCROW_DEPOSIT | Escrow Deposit Initiate |
+| FINALIZE_ESCROW_DEPOSIT | Escrow Deposit Finalize |
+| INITIATE_ESCROW_WITHDRAWAL | Escrow Withdrawal Initiate |
+| FINALIZE_ESCROW_WITHDRAWAL | Escrow Withdrawal Finalize |
+| INITIATE_MIGRATION | Migration Initiate |
+| FINALIZE_MIGRATION | Migration Finalize |
+
+Transitions that map to the OPERATE intent do not require on-chain checkpointing under normal operation.
+
+## Transition Field
+
+Each state update includes a transition that describes the operation that produced the new state.
+
+| Field | Description |
+| --------- | ---------------------------------------------------------------- |
+| Type | Transition type identifier |
+| TxId | Transaction identifier hash |
+| AccountId | Context-dependent account identifier (varies by transition type) |
+| Amount | Amount involved in the transition |
+
+The transition type determines the validation rules applied to the state update. The account identifier carries different semantics depending on the transition type — for example, it references the channel identifier for deposit and withdrawal operations, the counterparty address for transfers, or the application session identifier for commit and release operations.
+
+## State Consistency Rules
+
+State validity requirements differ between off-chain advancement and on-chain enforcement contexts. Off-chain advancement rules are defined in the [Channel Protocol](channel-protocol.md) document, and on-chain enforcement rules are defined in the [State Enforcement](enforcement.md) document.
+
+In both contexts, the following invariants MUST hold:
+
+- The entity identifier MUST match the entity definition
+- The version MUST be strictly greater than the previously accepted version
+- Ledger invariants MUST be satisfied (allocations equal net flows, allocation values non-negative)
+
+---
+
+Previous: [Cryptography](cryptography.md) | Next: [Channel Protocol](channel-protocol.md)
diff --git a/docs/protocol/state_ledger_advancement.png b/docs/protocol/state_ledger_advancement.png
new file mode 100644
index 000000000..aa4653d27
Binary files /dev/null and b/docs/protocol/state_ledger_advancement.png differ
diff --git a/docs/protocol/terminology.md b/docs/protocol/terminology.md
new file mode 100644
index 000000000..e6d266daf
--- /dev/null
+++ b/docs/protocol/terminology.md
@@ -0,0 +1,175 @@
+# Terminology
+
+Previous: [Overview](overview.md) | Next: [Cryptography](cryptography.md)
+
+---
+
+This document defines all protocol terms used throughout the Nitrolite protocol documentation.
+
+Each term is defined once. All other documents MUST use these terms consistently.
+
+## Naming Conventions
+
+- Protocol entities use CamelCase (e.g., ChannelState, AppSession)
+- Field names use CamelCase (e.g., ChannelId, StateVersion)
+- Operations use lowercase with hyphens in document references (e.g., state-advancement)
+
+## Core Entities
+
+### Channel
+
+A state container shared between a user and a node that allows off-chain state updates while maintaining on-chain security guarantees. Each channel operates on a single unified asset.
+
+### Channel Definition
+
+The immutable parameters that define a channel: user, node, asset, nonce, challenge duration, and approved signature validators. A channel definition is fixed at creation time and MUST NOT change during the channel lifecycle.
+
+### Channel State
+
+The current agreed configuration of a channel, including home and non-home ledger allocations, a version number, and a transition field. Channel state evolves through off-chain state advancement.
+
+### Participant
+
+An entity that holds a signing key and participates in a channel. Each channel has exactly two participants: a user and a node.
+
+### Asset
+
+A representation of value within the protocol, identified by a human-readable symbol and decimal precision. Assets are identified independently of any specific blockchain; the same logical asset MAY exist on multiple chains with different token addresses.
+
+## State Concepts
+
+### State
+
+An abstract data structure representing the current configuration of a protocol entity at a specific version.
+
+### State Version
+
+A monotonically increasing integer that identifies the order of state updates. During off-chain advancement, each new state MUST have a version exactly one greater than the previous state.
+
+### State Advancement
+
+The process of updating a protocol entity's state off-chain through signed transitions exchanged between participants.
+
+### State Enforcement
+
+The process of submitting a signed state to the blockchain layer for on-chain validation and enforcement.
+
+### Transition
+
+A typed operation that describes the reason and parameters for a state update. Each transition carries a type, transaction identifier, account identifier, and amount.
+
+### Intent
+
+A value derived from the transition type that determines how the blockchain layer processes an enforced state. Intents include OPERATE, CLOSE, DEPOSIT, WITHDRAW, and various escrow and migration intents.
+
+## Cryptographic Concepts
+
+### Signature
+
+A cryptographic proof that a specific key holder authorized a specific message. The protocol uses ECDSA over secp256k1.
+
+### Signer
+
+An entity capable of producing signatures. Each signer is associated with a specific key.
+
+### Session Key
+
+A delegated signing key authorized by a participant's primary key to sign specific types of state updates on their behalf. Session key authorization MUST be associated with the same address as the channel's user or node participant.
+
+### Signature Validation Mode
+
+A mechanism that determines how a signature is verified. The protocol currently defines two modes: default (0x00) for standard ECDSA validation and session key (0x01) for delegated validation.
+
+## Ledger Concepts
+
+### Ledger
+
+A record of asset allocations within a channel, associated with a specific blockchain. Each ledger tracks user and node allocations and net flows, and MUST satisfy the invariant that allocations equal net flows.
+
+### Home Ledger
+
+The primary ledger of a channel state, associated with the blockchain where the state is enforced. The home ledger is the authoritative source for channel state enforcement.
+
+### Non-Home Ledger
+
+A secondary ledger tracking asset allocations on a blockchain other than the home chain. Used for cross-chain escrow operations and migrations.
+
+### Home Chain
+
+The blockchain identified by the home ledger's chain identifier. The home chain determines where enforcement operations are executed. It MAY change through a migration operation.
+
+### Locked Funds
+
+The total assets held by the blockchain enforcement contract on behalf of a specific channel. Unless the channel is being closed, the sum of UserAllocation and NodeAllocation MUST equal the locked funds.
+
+### Vault
+
+A pool of available funds maintained by the node on a specific blockchain, separate from any specific channel. The vault is used to cover required fund locking when a transition requires the node to lock additional assets into a channel.
+
+### WAD Normalization
+
+The process of scaling chain-specific asset amounts to the asset's configured decimal precision for exact, lossless cross-chain comparisons:
+
+```
+NormalizedAmount = Amount * 10^(18 - ChainDecimals)
+```
+
+Each unified asset defines a canonical decimal precision (e.g. 6 for USDC) that is used during User <> Clearnode interactions (e.g. on-chain deposit, on-chain state submission requests, transfers, app session operations etc.). The maximum supported decimal precision is 18.
+
+## State Signing Categories
+
+### Mutually Signed State
+
+A state that carries valid signatures from both the user and the node. Only mutually signed states are enforceable on-chain.
+
+### Node-Issued Pending State
+
+A state produced by the node that carries only the node's signature. A pending state is NOT enforceable on-chain and becomes mutually signed only after the user acknowledges it.
+
+### Channel Status
+
+A specific on-chain channel data configuration, which changes throughout channel lifecycle, and includes *operating*, *disputed*, *migrating-in*, *migrated out*, etc. This can be thought of as a Finite State-Machine State (do not confuse with State Channel State).
+
+### Escrow Channel Identifier
+
+A 32-byte hash derived deterministically from the home channel identifier and the state version. Used to uniquely identify each escrow operation.
+
+## Protocol Operations
+
+### Checkpoint
+
+The operation of submitting a signed state to the blockchain layer. A checkpoint records the latest agreed state on-chain.
+
+### Challenge
+
+An on-chain operation where a participant disputes the current enforced state by submitting a signed state along with a challenger signature. Initiates the challenge duration, during which other participants MAY respond with a higher-version state.
+
+### Commit
+
+The operation of moving assets from a channel into an extension, such as an application session. Decreases the user's allocation and the node's net flow.
+
+### Release
+
+The operation of returning assets from an extension back to the channel. Increases the user's allocation and the node's net flow.
+
+### Escrow
+
+A two-phase mechanism for cross-chain operations. An "escrow initiate" locks funds, and an "escrow finalize" releases them upon cooperative completion or after a timeout period.
+
+## Extension Concepts
+
+### Extension
+
+An additional protocol module that provides functionality beyond the core channel protocol. Extensions interact with channels through commit and release transitions.
+
+### Application Session
+
+An extension that enables off-chain application functionality. Application sessions hold committed assets and maintain their own state.
+
+### Application State
+
+The state associated with an application session, tracking committed assets and application-specific data.
+
+---
+
+Previous: [Overview](overview.md) | Next: [Cryptography](cryptography.md)
diff --git a/erc7824-docs/.firebaserc b/erc7824-docs/.firebaserc
deleted file mode 100644
index 40c8735e8..000000000
--- a/erc7824-docs/.firebaserc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "projects": {
- "default": "erc7824"
- }
-}
diff --git a/erc7824-docs/.gitignore b/erc7824-docs/.gitignore
deleted file mode 100644
index d3e664850..000000000
--- a/erc7824-docs/.gitignore
+++ /dev/null
@@ -1,23 +0,0 @@
-# Dependencies
-/node_modules
-
-# Production
-/build
-
-# Generated files
-.docusaurus
-.cache-loader
-
-# Misc
-.firebase
-.DS_Store
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-sdk
-frontend
\ No newline at end of file
diff --git a/erc7824-docs/README.md b/erc7824-docs/README.md
deleted file mode 100644
index 1a5805b96..000000000
--- a/erc7824-docs/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Website
-
-This repository hosts the source files from which the [erc7284.org website](https://erc7284.org) is built
-using [Docusaurus](https://docusaurus.io/), a modern static website generator.
-
-### Installation
-
-```
-$ yarn
-```
-
-### Local Development
-
-```
-$ yarn start
-```
-
-This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
-
-### Build
-
-```
-$ yarn build
-```
-
-This command generates static content into the `build` directory and can be served using any static contents hosting service.
-
-### Deployment
-
-Using SSH:
-
-```
-$ USE_SSH=true yarn deploy
-```
-
-Not using SSH:
-
-```
-$ GIT_USER= yarn deploy
-```
-
-If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
diff --git a/erc7824-docs/blog/authors.yml b/erc7824-docs/blog/authors.yml
deleted file mode 100644
index 8bfa5c7c4..000000000
--- a/erc7824-docs/blog/authors.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-yangshun:
- name: Yangshun Tay
- title: Front End Engineer @ Facebook
- url: https://github.com/yangshun
- image_url: https://github.com/yangshun.png
- page: true
- socials:
- x: yangshunz
- github: yangshun
-
-slorber:
- name: Sébastien Lorber
- title: Docusaurus maintainer
- url: https://sebastienlorber.com
- image_url: https://github.com/slorber.png
- page:
- # customize the url of the author page at /blog/authors/
- permalink: '/all-sebastien-lorber-articles'
- socials:
- x: sebastienlorber
- linkedin: sebastienlorber
- github: slorber
- newsletter: https://thisweekinreact.com
diff --git a/erc7824-docs/blog/tags.yml b/erc7824-docs/blog/tags.yml
deleted file mode 100644
index bfaa778fb..000000000
--- a/erc7824-docs/blog/tags.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-facebook:
- label: Facebook
- permalink: /facebook
- description: Facebook tag description
-
-hello:
- label: Hello
- permalink: /hello
- description: Hello tag description
-
-docusaurus:
- label: Docusaurus
- permalink: /docusaurus
- description: Docusaurus tag description
-
-hola:
- label: Hola
- permalink: /hola
- description: Hola tag description
diff --git a/erc7824-docs/docs/_legacy/examples/index.md b/erc7824-docs/docs/_legacy/examples/index.md
deleted file mode 100644
index 0cd8cd5ca..000000000
--- a/erc7824-docs/docs/_legacy/examples/index.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-sidebar_position: 5
-title: Examples
-description: State Channels tutorials, application examples in solidity and golang
-keywords: [erc7824, statechannels, state channels, ethereum scaling, layer 2, off-chain, tutorial, tictactoe, gaming]
-tags:
- - erc7824
- - games
- - golang
- - docs
----
-
-# Example Apps
diff --git a/erc7824-docs/docs/_legacy/examples/tictactoe.md b/erc7824-docs/docs/_legacy/examples/tictactoe.md
deleted file mode 100644
index bb9eefc0a..000000000
--- a/erc7824-docs/docs/_legacy/examples/tictactoe.md
+++ /dev/null
@@ -1,167 +0,0 @@
----
-sidebar_position: 3
-title: Gaming with TicTacToe
-description: Example of out to manage nitro state using go-nitro SDK
-keywords: [erc7824, statechannels, nitro, sdk, development, state channels, ethereum scaling, L2]
-tags:
- - erc7824
- - golang
- - go-nitro
- - nitro
- - docs
----
-# TicTacToe Offchain
-
-Below an example in go lang of usage of our upcoming go lang SDK
-Typescript will be also available soon.
-
-```go
-package main
-
-import (
- "errors"
- "testing"
-
- "github.com/ethereum/go-ethereum/common"
- ecrypto "github.com/ethereum/go-ethereum/crypto"
- "github.com/layer-3/clearsync/pkg/signer"
- "github.com/layer-3/neodax/internal/nitro"
- "github.com/stretchr/testify/require"
-)
-
-// TicTacToe represents a simple Nitro application implementing a Tic-Tac-Toe game.
-type TicTacToe struct {
- players []signer.Signer
- grid [3][3]byte // 3x3 grid for TicTacToe
-
- ch nitro.Channel
-}
-
-// NewTicTacToe initializes a new TicTacToe instance.
-func NewTicTacToe(players []signer.Signer) *TicTacToe {
- fp := nitro.FixedPart{
- Participants: []common.Address{players[0].CommonAddress(), players[1].CommonAddress()},
- }
- vp := nitro.VariablePart{}
- s := nitro.StateFromFixedAndVariablePart(fp, vp)
-
- return &TicTacToe{
- players: players,
- grid: [3][3]byte{},
- ch: *nitro.NewChannel(s),
- }
-}
-
-// Definition returns the FixedPart of the Nitro state.
-func (t *TicTacToe) Definition() nitro.FixedPart {
- return t.ch.FixedPart
-}
-
-// Data returns the VariablePart representing the current game state.
-func (t *TicTacToe) Data(turn uint64) nitro.VariablePart {
- return nitro.VariablePart{
- AppData: encodeGrid(t.grid),
- Outcome: nitro.Exit{},
- }
-}
-
-func (t *TicTacToe) State(turn uint64) nitro.SignedState {
- return t.ch.SignedStateForTurnNum[turn]
-}
-
-// Validate checks if the current state is valid.
-func (t *TicTacToe) Validate(turn uint64) bool {
- // Validate state can be done using the contract artifact
- return true
-}
-
-func (t *TicTacToe) LatestSupportedState() (nitro.SignedState, error) {
- return t.ch.LatestSupportedState()
-}
-
-// encodeGrid converts the grid to a byte slice for storage in AppData.
-func encodeGrid(grid [3][3]byte) []byte {
- var data []byte
- // Encode using ABI
- for _, row := range grid {
- data = append(data, row[:]...)
- }
- return data
-}
-
-// MakeMove updates the game state with a player's move.
-func (t *TicTacToe) MakeMove(player byte, x, y int) error {
- if x < 0 || x > 2 || y < 0 || y > 2 {
- return errors.New("invalid move: out of bounds")
- }
- if t.grid[x][y] != 0 {
- return errors.New("invalid move: cell already occupied")
- }
- t.grid[x][y] = player
- return nil
-}
-
-func (t *TicTacToe) StartGame() error {
- preFundState, err := t.ch.State(nitro.PreFundTurnNum)
- if err != nil {
- return err
- }
-
- for _, p := range t.players {
- if _, err := t.ch.SignAndAddState(preFundState.State(), p); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func (t *TicTacToe) FinishGame() error {
- lss, err := t.ch.LatestSignedState()
- if err != nil {
- return err
- }
-
- lastState := lss.State().Clone()
- lastState.IsFinal = true
- lastState.TurnNum += 1
-
- for _, p := range t.players {
- if _, err := t.ch.SignAndAddState(lastState, p); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func TestTicTacToe(t *testing.T) {
- // Create player signers
- var alice, bob signer.Signer
-
- app := NewTicTacToe([]signer.Signer{alice, bob})
- nch, err := nitro.NewClient() // Implements channel.Client
- require.NoError(t, err, "Error creating client")
-
- err = app.StartGame()
- require.NoError(t, err, "Error starting game")
-
- // Open the channel
- _, err = nch.Open(app)
- require.NoError(t, err, "Error opening channel")
-
- // Simulate moves
- err = app.MakeMove('X', 0, 0)
- require.NoError(t, err, "Invalid move")
-
- err = app.MakeMove('O', 1, 1)
- require.NoError(t, err, "Invalid move")
-
- err = app.FinishGame()
- require.NoError(t, err, "Error finishing game")
-
- // Close the channel at turn 2
- err = nch.Close(app)
- require.NoError(t, err, "Error closing channel")
-}
-```
diff --git a/erc7824-docs/docs/_legacy/faq.md b/erc7824-docs/docs/_legacy/faq.md
deleted file mode 100644
index a547155a5..000000000
--- a/erc7824-docs/docs/_legacy/faq.md
+++ /dev/null
@@ -1,128 +0,0 @@
----
-sidebar_position: 7
-title: FAQ
-description: State channels frequently asked questions
-keywords: [erc7824, statechannels, nitro, sdk, faq, state channels, ethereum scaling, L2]
-tags:
- - erc7824
- - nitro
- - faq
----
-# FAQ
-
-### State channels FAQ
-
-
- What is ERC-7824?
-
- ERC-7824 is a proposed standard for cross-chain trade execution systems that use state channels. It defines structures and interfaces to enable efficient, secure, and scalable off-chain interactions while leveraging the blockchain for finality and dispute resolution.
-
-
-
-
- What is a state channel?
-
- A state channel can be thought of as an account with multiple balances (often just two). The owners of that account can update those balances according to predefined rules, which are enforceable on a blockchain. This enables peer-to-peer games, payments, and other few-user applications to safely trade blockchain assets with extremely low latency, low cost, and high throughput without requiring trust in a third party.
-
-
-
-
- How do state channels work?
-
- 1. Setup: Participants lock assets into a blockchain-based smart contract.
- 2. Off-Chain Updates: Transactions or updates occur off-chain through cryptographically signed messages.
- 3. Finalization: The final state is submitted on-chain for settlement, or disputes are resolved if necessary.
-
-
-
-
- What are the benefits of state channels?
-
- - High Performance: Transactions are processed off-chain, providing low latency and high throughput.
- - Cost Efficiency: Minimal blockchain interactions significantly reduce gas fees.
- - Privacy: Off-chain interactions keep intermediate states confidential.
- - Flexibility: Supports a wide range of applications, including multi-chain trading.
-
-
-
-
- What kind of applications use state channels?
-
- State channels enable the redistribution of assets according to arbitrary logic, making them suitable for:
-
-
Games: Peer-to-peer poker or other interactive games.
-
Payments: Microtransactions and conditional payments.
- - On-Chain Components: Implemented in Solidity and included in the npm package @statechannels/nitro-protocol.
- - Off-Chain Components: A reference implementation provided through go-nitro, a lightweight client written in Go.
-
-
-
-
- Where is Nitro Protocol being used?
-
- The maintainers of Nitro Protocol are actively integrating it into the Filecoin Retrieval Market and the Filecoin Virtual Machine, enabling decentralized and efficient content distribution.
-
-
-
-
- What is the structure of a state in state channels?
-
- A state consists of:
-
-
Fixed Part: Immutable properties like participants, nonce, app definition, and challenge duration.
-
Variable Part: Changeable properties like outcomes, application data, and turn numbers.
-
- In Nitro, participants sign a keccak256 hash of both these parts to commit to a particular state. The turnNum determines the version of the state, while isFinal can trigger an instant finalization when fully countersigned.
-
-
-
-
- What is a challenge duration?
-
- The challenge duration is a time window during which disputes can be raised on-chain. If no disputes are raised, the state channel finalizes according to its latest agreed state. In Nitro, it is set at channel creation and cannot be changed later. During this period, an unresponsive or dishonest participant can be forced to progress the channel state via on-chain transactions.
-
-
-
-
- How do disputes get resolved in state channels?
-
- Participants can:
-
-
Submit signed updates to the blockchain as evidence.
-
Resolve disputes based on turn numbers and application-specific rules stored in an on-chain appDefinition.
-
Finalize the channel after the challenge duration if no valid disputes arise.
-
- Nitro Protocol introduces a challenge mechanism, enabling any participant to push the channel state on-chain, forcing the other side to respond. This ensures that unresponsive or malicious actors cannot stall the channel indefinitely.
-
-
-
-
- What is the typical channel lifecycle in Nitro Protocol?
-
- A direct channel often follows these stages:
-
- 1. Proposed: A participant signs the initial (prefund) state with turnNum=0.
- 2. ReadyToFund: All participants countersign the prefund state, ensuring it is safe to deposit on-chain.
- 3. Funded: Deposits appear on-chain, and participants exchange a postfund state (turnNum=1).
- 4. Running: The channel can be updated off-chain by incrementing turnNum and exchanging signatures.
- 5. Finalized: A state with isFinal=true is fully signed. No more updates are possible; the channel can pay out according to the final outcome.
-
-
-
-
- How do I finalize and withdraw funds from a direct channel in Nitro?
-
- - Finalization (Happy Path): If a fully signed state with isFinal=true exists off-chain, any participant can call conclude on the Nitro Adjudicator to finalize instantly.
- - Finalization (Dispute Path): If participants are unresponsive or disagree, one party can challenge with the latest supported state. After the challenge window, the channel is finalized if unchallenged or out-of-date states are resolved.
- - Withdrawing: Once finalized, participants use the transfer or concludeAndTransferAllAssets method to claim their allocations on-chain.
-
-
diff --git a/erc7824-docs/docs/_legacy/index.md b/erc7824-docs/docs/_legacy/index.md
deleted file mode 100644
index 9680d9e2c..000000000
--- a/erc7824-docs/docs/_legacy/index.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-sidebar_position: 5
-slug: /legacy
-title: Legacy
-description: Create high-performance chain agnostic dApps using state channels
-keywords: [erc7824, statechannels, chain abstraction, chain agnostic, state channels, ethereum scaling, layer 2, layer 3, nitro, trading, high-speed]
-tags:
- - erc7824
- - docs
----
-
-# Inject Nitro in your Stack
-
-Blockchain technology has introduced a paradigm shift in how digital assets and decentralized applications (dApps) operate. However, scalability, transaction costs, and cross-chain interoperability remain significant challenges. **ERC-7824** and the **Nitro Protocol** provide a solution by enabling **off-chain state channels**, allowing seamless and efficient transactions without compromising on security or performance.
-
-ERC-7824 defines a minimal, universal interface for state channel interactions without assuming any specific underlying chain, making it naturally chain agnostic. It abstracts away chain-specific details by specifying data structures and messaging formats that can be verified on any blockchain with basic smart contract capabilities. As a result, developers can rely on ERC-7824’s standard approach for off-chain interactions and dispute resolution across multiple L1 or L2 networks, preserving interoperability and reusability of state channel components in a wide range of environments.
-
-## Supercharging Web2/3 Applications
-
-The Nitro Protocol is designed to integrate effortlessly with **both Web2 and Web3 applications**. Whether operating in traditional infrastructure or leveraging blockchain ecosystems, developers can **enhance their software stack** with blockchain-grade security and efficiency without sacrificing speed or requiring major architectural changes.
-
-```mermaid
-quadrantChart
- title Distributed system scaling
- x-axis Low Speed --> High Speed
- y-axis Low Trust --> High Trust
- quadrant-1 Network Overlay
- quadrant-2 Decentralized Systems
- quadrant-3 Cross-chain Systems
- quadrant-4 Centralized Systems
- Bitcoin: [0.20, 0.8]
- L2 Chains: [0.57, 0.70]
- L3 Nitro: [0.75, 0.60]
- ClearSync: [0.80, 0.85]
- Solana: [0.60, 0.42]
- TradFi: [0.85, 0.34]
- CEX: [0.70, 0.20]
- Bridges: [0.24, 0.24]
- Cross-chain Messaging: [0.15, 0.40]
- Ethereum: [0.35, 0.78]
-```
-
-## Key Benefits of ERC-7824 and Nitro Protocol
-
-1. **Scalability Without Congestion**
- By moving most interactions off-chain and leveraging **state channels**, Nitro enables **near-instant, gas-free transactions**, reducing load on the base layer blockchain.
-
-2. **Cost Efficiency**
- Users and applications **avoid high gas fees** by settling transactions off-chain while maintaining an on-chain fallback for dispute resolution.
-
-3. **Chain Agnosticism & Interoperability**
- ERC-7824 is designed to be **cross-chain compatible**, allowing seamless **interactions between different blockchain ecosystems**. This enables **cross-chain asset transfers, atomic swaps, and multi-chain smart contract execution**.
-
-4. **Security & Trustless Execution**
- - Transactions occur off-chain but remain **cryptographically signed and enforceable** on-chain.
- - **ForceMove** dispute resolution mechanism ensures that **malicious actors cannot manipulate state transitions**.
-
-5. **Modular & Flexible**
- The protocol provides **plug-and-play interfaces**, allowing developers to build **custom business logic** while ensuring compatibility with the ERC-7824 framework.
-
-6. **Real-World Applications**
- - **DeFi Scaling:** Enables off-chain transactions and batch settlements for **DEXs and lending protocols**.
- - **Gaming & Metaverse:** Supports **high-speed microtransactions** for in-game economies.
- - **Cross-Chain Payments:** Facilitates **low-cost, instant remittances and global transactions**.
- - **Enterprise & Supply Chain:** Enhances **multi-party agreements and digital asset settlement**.
-
-## Summary
-
-ERC-7824 and the Nitro Protocol provide the **next-generation infrastructure** for decentralized applications, bridging the gap between **legacy systems and blockchain**. By combining **off-chain speed** with **on-chain security**, this framework allows developers to build high-performance, scalable solutions without being tied to a single blockchain network.
diff --git a/erc7824-docs/docs/_legacy/protocol.md b/erc7824-docs/docs/_legacy/protocol.md
deleted file mode 100644
index 6f638dca8..000000000
--- a/erc7824-docs/docs/_legacy/protocol.md
+++ /dev/null
@@ -1,430 +0,0 @@
----
-sidebar_position: 3
-title: Protocol
-description: Interfaces and nitro types for NitroApp development
-keywords: [erc7824, statechannels, nitro, protocol, sdk, development, state channels, ethereum scaling, L2]
-tags:
- - erc7824
- - protocol
- - nitro
- - sdk
- - docs
----
-
-# Nitro Protocol
-
-**Nitro Protocol** is a framework for building fast, secure, and flexible state channels on Ethereum. It enables developers to keep most transactions off-chain for near-instant updates while retaining the option to settle disputes on-chain as a last resort. At the heart of Nitro are well-defined states and transition rules enforced by the on-chain Adjudicator contract. The application-specific logic (“NitroApp”) is encapsulated in an on-chain contract, which the Nitro Adjudicator references during disputes to verify that a proposed state transition is valid.
-
-Off-chain, developers write the code responsible for constructing and signing new states, managing state progression, and orchestrating deposits and withdrawals. This off-chain logic ensures quick state updates without paying on-chain gas fees, since new states only require on-chain submission when there is a disagreement or final payout. By following Nitro’s API and implementing these components carefully, developers can build robust applications that benefit from low latency and trust-minimized on-chain settlements.
-
-## Overview
-
-```mermaid
-graph TD
- subgraph Nitro Protocol
- INitroAdjudicator[INitroAdjudicator] -->|Interacts with| IForceMove[IForceMove]
- INitroAdjudicator -->|Manages outcomes| IMultiAssetHolder[IMultiAssetHolder]
- INitroAdjudicator -->|Handles states| IStatusManager[IStatusManager]
-
- IForceMove -->|Implements state transitions| IForceMoveApp[IForceMoveApp]
- IForceMoveApp -->|Validates states| INitroTypes[INitroTypes]
- end
-
- subgraph NitroApps
- CountingApp[CountingApp] -->|Implement| IForceMoveApp
- EscrowApp[VEscrowApp] -->|Implement| IForceMoveApp
- end
-```
-
-## 1. States & Channels
-
-A state channel can be thought of as a private ledger containing balances and other arbitrary data housed in a data structure called a “state.” The state of the channel is updated, committed to (via signatures), and exchanged between a fixed set of actors (participants). A state channel controls funds which are locked — typically on an L1 blockchain — and those locked funds are unlocked according to the channel’s final state when the channel finishes.
-
-### States
-
-In Nitro protocol, a state is broken up into fixed and variable parts:
-
-
-Solidity
-
-```solidity
-import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol';
-
-struct FixedPart {
- address[] participants;
- uint48 channelNonce;
- address appDefinition;
- uint48 challengeDuration;
-}
-
-struct VariablePart {
- Outcome.SingleAssetExit[] outcome; // (1)
- bytes appData;
- uint48 turnNum;
- bool isFinal;
-}
-```
-
-
-
-
-TypeScript
-
-```typescript
-import * as ExitFormat from '@statechannels/exit-format';
-// (1)
-import {Address, Bytes, Bytes32, Uint256, Uint48, Uint64} from '@statechannels/nitro-protocol';
-
-export interface FixedPart {
- participants: Address[];
- channelNonce: Uint64;
- appDefinition: Address;
- challengeDuration: Uint48;
-}
-
-export interface VariablePart {
- outcome: ExitFormat.Exit; // (2)
- appData: Bytes;
- turnNum: Uint48;
- isFinal: boolean;
-}
-```
-
-
-
-
-Go
-
-```go
-import (
- "github.com/statechannels/go-nitro/channel/state/outcome"
- "github.com/statechannels/go-nitro/types" // (1)
-)
-
-type (
- FixedPart struct {
- Participants []types.Address
- ChannelNonce uint64
- AppDefinition types.Address
- ChallengeDuration uint32
- }
-
- VariablePart struct {
- AppData types.Bytes
- Outcome outcome.Exit // (2)
- TurnNum uint64
- IsFinal bool
- }
-)
-```
-
-
-
-1. `Bytes32`, `Bytes`, `Address`, `Uint256`, `Uint64` are aliases for hex-encoded strings. `Uint48` is aliased to a `number`.
-2. This composite type is explained in more detail in the “Outcomes” section.
-
-Each state has:
-
-- **Fixed Part**:
- - `participants`: The list of addresses (one per participant).
- - `channelNonce`: A unique identifier so that re-using the same participants and app definition does not cause replay attacks.
- - `appDefinition`: Address of the application contract (on-chain code) that enforces application-specific rules.
- - `challengeDuration`: The duration (in seconds) of the challenge window in case of on-chain disputes.
-
-- **Variable Part**:
- - `outcome`: The distribution of funds if the channel were finalized in that state.
- - `appData`: Extra data interpreted by the application (e.g. game state).
- - `turnNum`: A version counter for the state updates.
- - `isFinal`: A boolean that triggers instant finalization when fully signed.
-
-#### Channel IDs
-
-Channels are identified by the hash of the fixed part:
-
-```solidity
-bytes32 channelId = keccak256(
- abi.encode(
- fixedPart.participants,
- fixedPart.channelNonce,
- fixedPart.appDefinition,
- fixedPart.challengeDuration
- )
-);
-```
-
-#### State Commitments
-
-To commit to a state, we take the keccak256 hash of a combination of the channel ID and the variable part:
-
-```solidity
-bytes32 stateHash = keccak256(abi.encode(
- channelId,
- variablePart.appData,
- variablePart.outcome,
- variablePart.turnNum,
- variablePart.isFinal
-));
-```
-
-Participants sign this `stateHash` using their private key. A signature has the form `(v, r, s)` in ECDSA.
-
-#### Signed Variable Parts and Support Proofs
-
-A “signed variable part” is simply the variable part plus the signatures from participants. Submitting such bundles to the chain allows the chain to verify that a given state is “supported” off-chain. Typically, a single channel update is accompanied by a single “candidate” state plus (optionally) a sequence of older states that prove the channel progressed correctly. This bundle is called a “support proof.”
-
----
-
-## 2. Execution Rules
-
-A channel’s execution rules dictate which state updates can be considered valid and which are invalid. Once a state is considered “supported” (i.e., it has enough signatures to pass on-chain checks), it can become the channel’s final state if the channel is later finalized.
-
-### Core Protocol Rules
-
-1. **Higher Turn Number**: Among multiple states, the one with the highest turn number supersedes the rest.
-2. **Instant Finalization**: If a state has `isFinal = true` and is fully signed, it can finalize the channel without needing a challenge phase.
-
-### Application Rules
-
-Each channel references an on-chain contract (the `appDefinition`) that encodes custom logic:
-
-```solidity
-interface IForceMoveApp is INitroTypes {
- function stateIsSupported(
- FixedPart calldata fixedPart,
- RecoveredVariablePart[] calldata proof,
- RecoveredVariablePart calldata candidate
- ) external view returns (bool, string memory);
-}
-```
-
-When on-chain disputes occur, the chain calls this function to confirm that a “candidate” state is valid relative to the “proof” states.
-
----
-
-## 3. Outcomes
-
-The **outcome** of a state is the piece that dictates how funds in the channel will be disbursed once the channel is finalized. Nitro uses the [L2 exit format](https://github.com/statechannels/exit-format):
-
-- **Outcome**: An array of `SingleAssetExit`, each describing:
- - An `asset` (e.g. the zero address for native ETH, or an ERC20 contract address),
- - Optional `assetMetadata`,
- - A list of **allocations**.
-
-Example:
-
-```ts
-import {
- Exit,
- SingleAssetExit,
- NullAssetMetadata,
- AllocationType,
-} from "@statechannels/exit-format";
-
-const ethExit: SingleAssetExit = {
- asset: "0x0", // native token (ETH)
- assetMetadata: NullAssetMetadata,
- allocations: [
- {
- destination: "0x00000000000000000000000096f7123E3A80C9813eF50213ADEd0e4511CB820f",
- amount: "0x05",
- allocationType: AllocationType.simple,
- metadata: "0x",
- },
- {
- destination: "0x0000000000000000000000000737369d5F8525D039038Da1EdBAC4C4f161b949",
- amount: "0x05",
- allocationType: AllocationType.simple,
- metadata: "0x",
- },
- ],
-};
-
-const exit = [ethExit];
-```
-
-### Allocations
-
-Allocations specify a `destination` and an `amount`. For **direct channels**, these allocations are typically of `allocationType = 0` (simple). Each entry is the portion of the channel’s asset that the channel participant can claim once the channel is finalized on-chain.
-
-### Destinations
-
-A destination is a 32-byte value that may represent:
-
-- A channel ID, or
-- An “external destination” (an Ethereum address left-padded with zeros).
-
-For **direct** channels, the relevant destinations are ordinarily external addresses corresponding to participant wallets.
-
----
-
-## 4. Lifecycle of a Channel
-
-Below we describe the lifecycle of a **direct** channel.
-
-### Off-Chain Lifecycle (High-Level)
-
-1. **Proposed**: A participant signs the prefund state (`turnNum = 0`) and shares it with others.
-2. **ReadyToFund**: Once all have countersigned, the channel can safely be funded on-chain.
-3. **Funded**: After the postfund state (`turnNum = 1`) is signed, the channel is recognized as funded.
-4. **Running**: States with `turnNum > 1` can be signed as the channel operates.
-5. **Finalized**: A state with `isFinal = true` is fully signed.
-
-A diagram (off-chain states):
-
-```mermaid
-stateDiagram-v2
- [*] --> Proposed
- Proposed --> ReadyToFund
- ReadyToFund --> Funded
- Funded --> Running
- Running --> Finalized
-```
-
-### Funding Lifecycle (On-Chain)
-
-A channel is “funded” on-chain once participants’ deposits have been made to the adjudicator contract. Eventually, once the channel is finished, those funds are withdrawn to participants’ external addresses.
-
-```mermaid
-stateDiagram-v2
-state OnChain {
- state Funding {
- [*]-->NotFundedOnChain
- NotFundedOnChain --> FundedOnChain
- FundedOnChain --> NotFundedOnChain
- }
-}
-```
-
-- **NotFundedOnChain** → channel has 0 or insufficient deposit in the adjudicator.
-- **FundedOnChain** → channel has the full deposit allocated.
-
-### Adjudication Lifecycle (On-Chain)
-
-Nitro’s `NitroAdjudicator` contract tracks the channel’s status on-chain:
-
-1. **Open**: No challenge is in progress.
-2. **Challenge**: A challenge is ongoing; participants must respond or let it expire.
-3. **Finalized**: The channel has either concluded with a `conclude` transaction or the challenge timed out.
-
-```mermaid
-stateDiagram-v2
-state OnChain {
-state Adjudication {
-Open -->Challenge: challenge
-Open --> Open: checkpoint
-Open--> Finalized: conclude
-Challenge--> Challenge: challenge
-Challenge--> Open: checkpoint
-Challenge--> Finalized: conclude
-Challenge--> Finalized': timeout
-}
-}
-```
-
----
-
-## 5. Precautions and Limits
-
-### Precautions
-
-As a participant, you should verify:
-
-- The number of participants is correct.
-- Your own address is in `participants`.
-- The `channelNonce` is unique.
-- `challengeDuration` is acceptable.
-
-### Limits
-
-There are upper bounds to how large a state can be before it becomes impractical to finalize on-chain:
-
-- `MAX_TX_DATA_SIZE`: ~128 KB typical limit for Ethereum transaction data.
-- `NITRO_MAX_GAS`: ~6M gas, a safe upper bound for some Nitro operations.
-- `MAX_OUTCOME_ITEMS`: The protocol recommends limiting the number of allocation items to at most 2000 to remain under `MAX_TX_DATA_SIZE` and `NITRO_MAX_GAS`.
-
----
-
-## 6. Funding a Channel
-
-This section covers **on-chain** depositing for direct channels.
-
-### Fund with an On-Chain `deposit`
-
-When participants want to stake funds, they each perform a deposit transaction to the `NitroAdjudicator`:
-
-```solidity
-function deposit(
- address asset,
- bytes32 destination,
- uint256 expectedHeld,
- uint256 amount
-) public payable
-```
-
-- `asset`: zero address if depositing ETH, or an ERC20 token address if depositing tokens.
-- `destination`: must be the channel’s `channelId` (not an external address).
-- `expectedHeld`: ensures the holdings so far match what you expect, preventing over-funding or front-running.
-- `amount`: how much is being deposited in this transaction.
-
-If depositing ETH, include `{value: amount}` in the transaction call. If depositing ERC20 tokens, you must first `approve` the `NitroAdjudicator` for `amount`.
-
-#### Outcome Priority
-
-It is possible for a channel to be underfunded if not all participants have deposited yet. If a channel finalizes underfunded, the distribution is paid in priority order (the topmost allocation entries get paid first). Thus, participants commonly wait until they each see the correct deposit events on-chain before signing postfund states.
-
----
-
-## 7. Finalizing a Channel
-
-A channel is **finalized** either off-chain with unanimous agreement or via an on-chain transaction (the “happy path” or the “sad path”).
-
-### Happy Path (Off-Chain)
-
-- One participant proposes a state with `isFinal = true`.
-- All participants sign it.
-- No on-chain action is strictly needed to *logically* finalize the channel (they can defund off-chain if no on-chain deposit was used).
-
-### On-Chain – Calling `conclude`
-
-When the channel was funded on-chain, any participant (or anyone with the finalization proof) can call:
-
-```solidity
-function conclude(
- FixedPart memory fixedPart,
- SignedVariablePart memory candidate
-) external
-```
-
-This finalizes the channel’s outcome **instantly** on-chain (no challenge period). Afterward, participants can withdraw or `transfer` funds out of the adjudicator contract.
-
----
-
-## 8. Defunding a Channel
-
-Once a channel is **finalized**, its locked funds can be released. For **direct** channels, this typically means:
-
-### On-Chain Defunding Using `transfer`
-
-The on-chain `transfer` method in the `NitroAdjudicator` contract moves funds from the channel’s escrow to participant addresses:
-
-```solidity
-function transfer(
- uint256 assetIndex,
- bytes32 channelId,
- bytes memory outcomeBytes,
- bytes32 stateHash,
- uint256[] memory indices
-) public
-```
-
-- `assetIndex`: Which asset in the channel’s outcome you want to pay out.
-- `channelId`: The identifier of the channel.
-- `outcomeBytes`: The encoded outcome (list of allocations).
-- `stateHash`: If finalization was done via `conclude`, you may pass `bytes32(0)`.
-- `indices`: Which allocations to pay out in this transaction (`[]` means “all”).
-
-Once called, the relevant allocations are paid out to the listed `destination`s, and the contract’s tracked holdings are reduced accordingly. Multiple calls may be required to pay out all allocations, especially if there are many recipients.
-
-#### concludeAndTransferAllAssets
-
-The contract also offers a “batched” convenience method `concludeAndTransferAllAssets` that finalizes and transfers in a single transaction.
diff --git a/erc7824-docs/docs/_legacy/resources.md b/erc7824-docs/docs/_legacy/resources.md
deleted file mode 100644
index 263e6b44a..000000000
--- a/erc7824-docs/docs/_legacy/resources.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-sidebar_position: 4
-title: Resources
-description: Learn more about state channels
-keywords: [erc7824, statechannels, state channels, awesome state channels, ethereum scaling, layer 2, layer 3, nitro, trading, high-speed]
-tags:
- - erc7824
- - links
- - docs
----
-
-# Resources
-
-### General Overviews
-
-- [What are State Channels? (Ethereum Foundation)](https://ethereum.org/en/developers/docs/scaling/state-channels/)
-- [State Channels vs. Rollups (Vitalik Buterin)](https://vitalik.eth.limo/general/2021/01/05/rollup.html)
-
-### Technical Papers
-
-- [(2018) ForceMove: An n-party state channel protocol](https://magmo.com/force-move-games.pdf)
-- [(2019) Nitro Protocol: Virtual channels](https://magmo.com/nitro-protocol.pdf)
-- [(2022) Stateful Asset Transfer Protocol](https://statechannels.github.io/satp_paper/satp.pdf)
-- [Nitro Protocol GitHub Repository](https://github.com/statechannels/go-nitro)
-
-### Videos and Tutorials
-
-- [State Channels Explained (YouTube)](https://www.youtube.com/watch?v=p1OTfvCbRFo)
-- [Nitro Protocol Walkthrough (YouTube)](https://www.youtube.com/watch?v=Z5dcPAfIzP4)
-- [Ethereum Magicians State Channel Discussion](https://ethereum-magicians.org/c/scaling/state-channels)
-- [Protocol Tutorial](https://docs.statechannels.org/protocol-tutorial/0010-states-channels/)
-- [State Channels Blog](https://blog.statechannels.org/)
diff --git a/erc7824-docs/docs/_legacy/spec.md b/erc7824-docs/docs/_legacy/spec.md
deleted file mode 100644
index 0c79e4e62..000000000
--- a/erc7824-docs/docs/_legacy/spec.md
+++ /dev/null
@@ -1,355 +0,0 @@
----
-sidebar_position: 2
-title: Specification
-description: Request for Comment on statechannels framework
-keywords: [erc7824, ERC, Ethereum, statechannels, nitro, sdk, development, state channels, ethereum scaling, L2]
-tags:
- - erc7824
- - nitro
- - docs
----
-
-# ERC-7824
-
-## Abstract
-
-State Channels is allowing participants to perform off-chain transactions while maintaining the security guarantees of the Ethereum blockchain. The goal is to enhance scalability and reduce transaction costs for decentralized applications.
-
-This standard defines a framework for implementing state channel systems, enabling efficient off-chain transaction execution and dispute resolution. It provides interfaces for managing channel states and an example implementation.
-
-## Motivation
-
-The Ethereum network faces challenges in scalability and transaction costs, making it less feasible for high-frequency interactions. State Channels provide a mechanism to perform most interactions off-chain, reserving on-chain operations for dispute resolution or final settlement. This ERC facilitate the adoption of state channels by standardizing essential interfaces.
-
-## Specification
-
-The key words "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.
-
-### Glossary of Terms
-
-- **Channel**: A construct where participants interact off-chain using signed messages.
-- **Channel ID**: A unique identifier for a channel, derived from the channel's fixed part.
-- **Participant**: An entity (EOA) involved in a state channel.
-- **Turn Number**: A counter indicating the sequence of moves in the channel.
-- **State**: A representation of the channel's condition, updated via signed messages.
-- **Fixed Part**: Immutable channel parameters.
-- **Variable Part**: Mutable channel parameters that evolve as states are updated.
-- **ForceMove**: A procedure for resolving disputes by transitioning the state on-chain.
-- **Outcome**: The final result of a channel state used for settling balances.
-- **Nitro**: Historial name of the initial protocol
-
-### Data Structures
-
-#### ExitFormat
-
-Standard data structure for exiting an EVM based state channel.
-
-```solidity
-/// @title ExitFormat
-/// @notice Standard for encoding channel outcomes
-library ExitFormat {
- struct SingleAssetExit {
- address asset;
- AssetMetadata assetMetadata;
- Allocation[] allocations;
- }
-
- // AssetMetadata allows for different token standards
- struct AssetMetadata {
- AssetType assetType;
- bytes metadata;
- }
-
- enum AssetType {Default, ERC721, ERC1155, Qualified}
-
- enum AllocationType {simple, withdrawHelper, guarantee}
-
- struct Allocation {
- bytes32 destination;
- uint256 amount;
- uint8 allocationType;
- bytes metadata;
- }
-}
-```
-
-#### NitroTypes
-
-The `FixedPart`, `VariablePart` compose a "state". The state of the channel is updated, committed to and exchanged between a fixed set of participants.
-
-```solidity
-/// @title NitroTypes
-/// @notice Defines the core data structures used in state channels.
-interface INitroTypes {
- struct FixedPart {
- address[] participants;
- uint64 channelNonce; // This is a unique number used to differentiate channels
- address appDefinition; // This is an Ethereum address where a ForceMoveApp has been deployed
- uint48 challengeDuration; // This is duration in seconds of the challenge-response window
- }
-
- struct VariablePart {
- Outcome.SingleAssetExit[] outcome;
- bytes appData;
- uint48 turnNum;
- bool isFinal; // This is a boolean flag which allows the channel to be finalized "instantly"
- }
-
- struct SignedVariablePart {
- VariablePart variablePart;
- Signature[] sigs;
- }
-
- struct RecoveredVariablePart {
- VariablePart variablePart;
- uint256 signedBy; // bitmask
- }
-}
-```
-
-#### Channel ID
-
-A `channelId` is derived using:
-
-```solidity
-bytes32 channelId = keccak256(
- abi.encode(
- fixedPart.participants,
- fixedPart.channelNonce,
- fixedPart.appDefinition,
- fixedPart.challengeDuration
- )
-);
-```
-
-## State commitments
-
-To commit to a state, a hash is formed as follows:
-
-```solidity
-bytes32 stateHash = keccak256(abi.encode(
- channelId,
- variablePart.appData,
- variablePart.outcome,
- variablePart.turnNum,
- variablePart.isFinal
-));
-```
-
-### Interfaces
-
-#### IForceMoveApp
-
-Define the state machine of a ForceMove state channel protocol
-
-```solidity
-/// @title IForceMoveApp
-/// @notice Interface for implementing protocol rules in state channels
-interface IForceMoveApp is INitroTypes {
- // @notice Encodes application-specific rules for a particular ForceMove-compliant state channel. Must revert or return false when invalid support proof and a candidate are supplied.
- // @dev Depending on the application, it might be desirable to narrow the state mutability of an implementation to 'pure' to make security analysis easier.
- // @param fixedPart Fixed part of the state channel.
- // @param proof Array of recovered variable parts which constitutes a support proof for the candidate. May be omitted when `candidate` constitutes a support proof itself.
- // @param candidate Recovered variable part the proof was supplied for. Also may constitute a support proof itself.
- function stateIsSupported(
- FixedPart calldata fixedPart,
- RecoveredVariablePart[] calldata proof,
- RecoveredVariablePart calldata candidate
- ) external view returns (bool, string memory);
-}
-```
-
-#### IForceMove
-
-The IForceMove interface defines the interface that an implementation of ForceMove should implement.
-
-```solidity
-interface IForceMove is INitroTypes {
- /**
- * @notice Registers a challenge against a state channel. A challenge will either prompt another participant into clearing the challenge (via one of the other methods), or cause the channel to finalize at a specific time.
- * @dev Registers a challenge against a state channel. A challenge will either prompt another participant into clearing the challenge (via one of the other methods), or cause the channel to finalize at a specific time.
- * @param fixedPart Data describing properties of the state channel that do not change with state updates.
- * @param proof Additional proof material (in the form of an array of signed states) which completes the support proof.
- * @param candidate A candidate state (along with signatures) which is being claimed to be supported.
- * @param challengerSig The signature of a participant on the keccak256 of the abi.encode of (supportedStateHash, 'forceMove').
- */
- function challenge(
- FixedPart memory fixedPart,
- SignedVariablePart[] memory proof,
- SignedVariablePart memory candidate,
- Signature memory challengerSig
- ) external;
-
- /**
- * @notice Overwrites the `turnNumRecord` stored against a channel by providing a candidate with higher turn number.
- * @dev Overwrites the `turnNumRecord` stored against a channel by providing a candidate with higher turn number.
- * @param fixedPart Data describing properties of the state channel that do not change with state updates.
- * @param proof Additional proof material (in the form of an array of signed states) which completes the support proof.
- * @param candidate A candidate state (along with signatures) which is being claimed to be supported.
- */
- function checkpoint(
- FixedPart memory fixedPart,
- SignedVariablePart[] memory proof,
- SignedVariablePart memory candidate
- ) external;
-
- /**
- * @notice Finalizes a channel according to the given candidate. External wrapper for _conclude.
- * @dev Finalizes a channel according to the given candidate. External wrapper for _conclude.
- * @param fixedPart Data describing properties of the state channel that do not change with state updates.
- * @param candidate A candidate state (along with signatures) to change to.
- */
- function conclude(FixedPart memory fixedPart, SignedVariablePart memory candidate) external;
-
- // events
-
- /**
- * @dev Indicates that a challenge has been registered against `channelId`.
- * @param channelId Unique identifier for a state channel.
- * @param finalizesAt The unix timestamp when `channelId` will finalize.
- * @param proof Additional proof material (in the form of an array of signed states) which completes the support proof.
- * @param candidate A candidate state (along with signatures) which is being claimed to be supported.
- */
- event ChallengeRegistered(
- bytes32 indexed channelId,
- uint48 finalizesAt,
- SignedVariablePart[] proof,
- SignedVariablePart candidate
- );
-
- /**
- * @dev Indicates that a challenge, previously registered against `channelId`, has been cleared.
- * @param channelId Unique identifier for a state channel.
- * @param newTurnNumRecord A turnNum that (the adjudicator knows) is supported by a signature from each participant.
- */
- event ChallengeCleared(bytes32 indexed channelId, uint48 newTurnNumRecord);
-
- /**
- * @dev Indicates that an on-chain channel data was successfully updated and now has `newTurnNumRecord` as the latest turn number.
- * @param channelId Unique identifier for a state channel.
- * @param newTurnNumRecord A latest turnNum that (the adjudicator knows) is supported by adhering to channel application rules.
- */
- event Checkpointed(bytes32 indexed channelId, uint48 newTurnNumRecord);
-
- /**
- * @dev Indicates that a challenge has been registered against `channelId`.
- * @param channelId Unique identifier for a state channel.
- * @param finalizesAt The unix timestamp when `channelId` finalized.
- */
- event Concluded(bytes32 indexed channelId, uint48 finalizesAt);
-}
-```
-
-### Workflows
-
-1. **Opening a Channel**: Participants agree on initial states and open a channel on-chain.
-2. **Funding a Channel**: Participants transfer the agreed amount of funds.
-3. **Off-Chain Interaction**: Participants exchange signed state updates off-chain.
-4. **Dispute Resolution**: If a disagreement arises, a participant can force a state on-chain.
-5. **Finalization**: Upon agreement or after a timeout, the channel is finalized, and the outcome is settled.
-
-#### Example Application
-
-The CountingApp contract complies with the ForceMoveApp interface and strict turn taking logic and allows only for a simple counter to be incremented.
-
-```solidity
-contract CountingApp is IForceMoveApp {
- struct CountingAppData {
- uint256 counter;
- }
-
- /**
- * @notice Decodes the appData.
- * @dev Decodes the appData.
- * @param appDataBytes The abi.encode of a CountingAppData struct describing the application-specific data.
- * @return A CountingAppData struct containing the application-specific data.
- */
- function appData(bytes memory appDataBytes) internal pure returns (CountingAppData memory) {
- return abi.decode(appDataBytes, (CountingAppData));
- }
-
- /**
- * @notice Encodes application-specific rules for a particular ForceMove-compliant state channel.
- * @dev Encodes application-specific rules for a particular ForceMove-compliant state channel.
- * @param fixedPart Fixed part of the state channel.
- * @param proof Array of recovered variable parts which constitutes a support proof for the candidate.
- * @param candidate Recovered variable part the proof was supplied for.
- */
- function stateIsSupported(
- FixedPart calldata fixedPart,
- RecoveredVariablePart[] calldata proof,
- RecoveredVariablePart calldata candidate
- ) external pure override returns (bool, string memory) {
- StrictTurnTaking.requireValidTurnTaking(fixedPart, proof, candidate);
-
- require(proof.length != 0, '|proof| = 0');
-
- // validate the proof
- for (uint256 i = 1; i < proof.length; i++) {
- _requireIncrementedCounter(proof[i], proof[i - 1]);
- _requireEqualOutcomes(proof[i], proof[i - 1]);
- }
-
- _requireIncrementedCounter(candidate, proof[proof.length - 1]);
- _requireEqualOutcomes(candidate, proof[proof.length - 1]);
-
- return (true, '');
- }
-
- /**
- * @notice Checks that counter encoded in first variable part equals an incremented counter in second variable part.
- * @dev Checks that counter encoded in first variable part equals an incremented counter in second variable part.
- * @param b RecoveredVariablePart with incremented counter.
- * @param a RecoveredVariablePart with counter before incrementing.
- */
- function _requireIncrementedCounter(
- RecoveredVariablePart memory b,
- RecoveredVariablePart memory a
- ) internal pure {
- require(
- appData(b.variablePart.appData).counter == appData(a.variablePart.appData).counter + 1,
- 'Counter must be incremented'
- );
- }
-
- /**
- * @notice Checks that supplied signed variable parts contain the same outcome.
- * @dev Checks that supplied signed variable parts contain the same outcome.
- * @param a First RecoveredVariablePart.
- * @param b Second RecoveredVariablePart.
- */
- function _requireEqualOutcomes(
- RecoveredVariablePart memory a,
- RecoveredVariablePart memory b
- ) internal pure {
- require(
- Outcome.exitsEqual(a.variablePart.outcome, b.variablePart.outcome),
- 'Outcome must not change'
- );
- }
-}
-```
-
-## Rationale
-
-State channels offer significant scalability improvements by minimizing on-chain transactions. This standard provides clear interfaces to ensure interoperability and efficiency while retaining flexibility for custom protocol rules.
-
-**Scalability**: State channels alleviate these concerns by moving most interactions off-chain, while the blockchain serves as a settlement layer. This construction enable high frequency applications.
-
-**Modular**: Interfaces act as contracts that specify what functions must be implemented without dictating how, allowing developers to create custom implementations suited to specific use cases while maintaining compatibility with the framework.
-
-**Interoperability**: Enable cross-chain interactions, participants can be using two differents chains for opening their channels and perform for example cross-chain atomic swaps.
-
-**Security**: The standard would enable to build an audited framework of primitive which is flexible to accommodate a large number of use cases. ERC-7824 is designed to accommodate this diversity by normalizing common protocol patterns.
-
-## Backwards Compatibility
-
-No backward compatibility issues found. This ERC is designed to coexist with existing standards and can integrate with [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) and [ERC-4337](https://www.erc4337.io/)
-
-## Security Considerations
-
-This ERC is agnostic of the protocol rules that must be implemented using IForceMoveApp. While the smart-contract framework is a simple set of convention, much of the security risk is moved off-chain. Protocols using State channels must perform security audit of their client and server backend implementation.
-
-## Copyright
-
-Copyright and related rights waived via [CC0](https://github.com/ethereum/ERCs/blob/master/LICENSE.md).
diff --git a/erc7824-docs/docs/erc-7824.md b/erc7824-docs/docs/erc-7824.md
deleted file mode 100644
index f58d4542d..000000000
--- a/erc7824-docs/docs/erc-7824.md
+++ /dev/null
@@ -1,498 +0,0 @@
----
-sidebar_position: 2
-title: ERC-7824
-description: Interfaces and data types for cross-chain stateful asset transfer
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, javascript, typescript, sdk]
----
-# ERC-7824
-
-## Abstract
-
-Nitrolite is a lightweight, efficient state channel framework for Ethereum and other EVM-compatible blockchains, enabling off-chain interactions while maintaining on-chain security guarantees. The framework allows participants to perform instant transactions with reduced gas costs while preserving the security of the underlying blockchain.
-
-This standard defines a framework for implementing state channel systems through the Nitrolite protocol, providing interfaces for channel creation, state management, dispute resolution, and fund custody. It enables high-throughput applications with minimal on-chain footprint.
-
-## Motivation
-
-The Ethereum network faces challenges in scalability and transaction costs, making it less feasible for high-frequency interactions. The Nitrolite framework addresses these challenges by providing a lightweight state channel solution that enables:
-
-- **Instant Finality**: Transactions settle immediately between parties
-- **Reduced Gas Costs**: Most interactions happen off-chain, with minimal on-chain footprint
-- **High Throughput**: Support for thousands of transactions per second
-- **Security Guarantees**: Same security as on-chain, with cryptographic proofs
-- **Chain Agnostic**: Works with any EVM-compatible blockchain
-
-This ERC standardizes the Nitrolite protocol interfaces to facilitate widespread adoption of state channels.
-
-## Specification
-
-The key words "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.
-
-### Glossary of Terms
-
-- **Channel**: A relationship between participants that allows off-chain state updates with on-chain settlement
-- **Channel ID**: A unique identifier derived from channel configuration (participants, adjudicator, challenge period, nonce)
-- **Participant**: An entity (EOA) involved in a state channel
-- **State**: A signed data structure containing version, allocations, and application data
-- **Allocation**: Specification of token distribution to destinations
-- **Adjudicator**: Contract that validates state transitions according to application rules
-- **Challenge Period**: Duration for dispute resolution before finalization
-- **Status**: Channel lifecycle stage (VOID, INITIAL, ACTIVE, DISPUTE, FINAL)
-- **State Intent**: Purpose of a state (OPERATE, INITIALIZE, RESIZE, FINALIZE)
-- **Custody**: On-chain contract holding locked funds for channels
-- **Checkpoint**: Recording a valid state on-chain without closing the channel
-
-### Data Structures
-
-The Nitrolite protocol defines the following core data structures:
-
-#### Basic Types
-
-```solidity
-struct Amount {
- address token; // ERC-20 token address (address(0) for native tokens)
- uint256 amount; // Token amount
-}
-
-struct Allocation {
- address destination; // Where funds are sent on channel closure
- address token; // ERC-20 token contract address (address(0) for native tokens)
- uint256 amount; // Token amount allocated
-}
-```
-
-#### Channel Configuration
-
-```solidity
-struct Channel {
- address[] participants; // List of participants in the channel
- address adjudicator; // Address of the contract that validates state transitions
- uint64 challenge; // Duration in seconds for dispute resolution period
- uint64 nonce; // Unique per channel with same participants and adjudicator
-}
-```
-
-#### State Structure
-
-```solidity
-struct State {
- StateIntent intent; // Intent of the state
- uint256 version; // State version incremental number to compare most recent
- bytes data; // Application data encoded, decoded by the adjudicator
- Allocation[] allocations; // Asset allocation and destination for each participant
- bytes[] sigs; // stateHash signatures from participants
-}
-
-enum StateIntent {
- OPERATE, // Normal operation state
- INITIALIZE, // Initial funding state
- RESIZE, // Resize allocations state
- FINALIZE // Final closing state
-}
-```
-
-#### Channel Status
-
-```solidity
-enum Status {
- VOID, // Channel was not created, State.version must be 0
- INITIAL, // Channel is created and in funding process, State.version must be 0
- ACTIVE, // Channel fully funded and operational, State.version > 0
- DISPUTE, // Challenge period is active
- FINAL // Final state, channel can be closed
-}
-```
-
-#### Channel ID
-
-The channel ID is computed as:
-
-```solidity
-bytes32 channelId = keccak256(
- abi.encode(
- channel.participants,
- channel.adjudicator,
- channel.challenge,
- channel.nonce
- )
-);
-```
-
-#### State Hash
-
-For signature verification, the state hash is computed as:
-
-```solidity
-bytes32 stateHash = keccak256(
- abi.encode(
- channelId,
- state.intent,
- state.version,
- state.data,
- state.allocations
- )
-);
-```
-
-Note: The smart contract supports all popular signature formats, specifically: raw ECDSA, EIP-191, EIP-712, EIP-1271, and EIP-6492.
-
-### Interfaces
-
-#### IAdjudicator
-
-Defines the interface for contracts that validate state transitions according to application-specific rules:
-
-```solidity
-interface IAdjudicator {
- /**
- * @notice Validates a candidate state based on application-specific rules
- * @dev Used to determine if a state is valid during challenges or checkpoints
- * @param chan The channel configuration
- * @param candidate The proposed state to be validated
- * @param proofs Array of previous states that provide context for validation
- * @return valid True if the candidate state is valid according to application rules
- */
- function adjudicate(
- Channel calldata chan,
- State calldata candidate,
- State[] calldata proofs
- ) external returns (bool valid);
-}
-```
-
-#### IComparable
-
-Interface for determining the ordering between states:
-
-```solidity
-interface IComparable {
- /**
- * @notice Compares two states to determine their relative ordering
- * @dev Returns: -1 if candidate < previous, 0 if equal, 1 if candidate > previous
- * @param candidate The state being evaluated
- * @param previous The reference state to compare against
- * @return result The comparison result
- */
- function compare(
- State calldata candidate,
- State calldata previous
- ) external view returns (int8 result);
-}
-```
-
-#### IChannel
-
-The main state channel interface that manages the channel lifecycle:
-
-```solidity
-interface IChannel {
- // Events
- event Created(bytes32 indexed channelId, Channel channel, State initial);
- event Joined(bytes32 indexed channelId, uint256 index);
- event Opened(bytes32 indexed channelId);
- event Challenged(bytes32 indexed channelId, uint256 expiration);
- event Checkpointed(bytes32 indexed channelId);
- event Resized(bytes32 indexed channelId, int256[] deltaAllocations);
- event Closed(bytes32 indexed channelId);
-
- /**
- * @notice Creates a new channel and initializes funding
- * @dev Creator must sign the funding state with StateIntent.INITIALIZE.
- * If both participants sign the initial state, channel opens immediately in ACTIVE status.
- * If only creator signs, channel enters INITIAL status awaiting join().
- * @param ch Channel configuration
- * @param initial Initial state with StateIntent.INITIALIZE and expected allocations
- * @return channelId Unique identifier for the created channel
- */
- function create(Channel calldata ch, State calldata initial)
- external returns (bytes32 channelId);
-
- /**
- * @notice Allows a participant to join a channel by signing the funding state
- * @dev Only needed when channel was created with single signature.
- * Participant must provide signature on the same funding state.
- * @param channelId Unique identifier for the channel
- * @param index Index of the participant in the channel's participants array
- * @param sig Signature of the participant on the funding state
- * @return channelId Unique identifier for the joined channel
- */
- function join(bytes32 channelId, uint256 index, bytes calldata sig)
- external returns (bytes32);
-
- /**
- * @notice Finalizes a channel with a mutually signed closing state
- * @dev Requires all participants' signatures on a state with StateIntent.FINALIZE
- * @param channelId Unique identifier for the channel
- * @param candidate The latest known valid state to be finalized
- * @param proofs Additional states required by the adjudicator
- */
- function close(
- bytes32 channelId,
- State calldata candidate,
- State[] calldata proofs
- ) external;
-
- /**
- * @notice Resizes channel allocations with participant agreement
- * @dev Used for adjusting channel allocations without withdrawing funds
- * @param channelId Unique identifier for the channel
- * @param candidate The state with new allocations
- * @param proofs Supporting states for validation
- */
- function resize(
- bytes32 channelId,
- State calldata candidate,
- State[] calldata proofs
- ) external;
-
- /**
- * @notice Initiates or updates a challenge with a signed state
- * @dev Starts a challenge period during which participants can respond
- * @param channelId Unique identifier for the channel
- * @param candidate The state being submitted as the latest valid state
- * @param proofs Additional states required by the adjudicator
- * @param challengerSig Signature of the challenger on the candidate state. Must be signed by one of the participants
- */
- function challenge(
- bytes32 channelId,
- State calldata candidate,
- State[] calldata proofs,
- bytes calldata challengerSig
- ) external;
-
- /**
- * @notice Records a valid state on-chain without initiating a challenge
- * @dev Used to establish on-chain proof of the latest state
- * @param channelId Unique identifier for the channel
- * @param candidate The state to checkpoint
- * @param proofs Additional states required by the adjudicator
- */
- function checkpoint(
- bytes32 channelId,
- State calldata candidate,
- State[] calldata proofs
- ) external;
-}
-```
-
-#### IDeposit
-
-Interface for managing token deposits and withdrawals:
-
-```solidity
-interface IDeposit {
- /**
- * @notice Deposits tokens into the contract
- * @dev For native tokens, the value should be sent with the transaction
- * @param wallet Address of the account whose ledger is changed
- * @param token Token address (use address(0) for native tokens)
- * @param amount Amount of tokens to deposit
- */
- function deposit(address wallet, address token, uint256 amount) external payable;
-
- /**
- * @notice Withdraws tokens from the contract
- * @dev Can only withdraw available (not locked in channels) funds
- * @param wallet Address of the account whose ledger is changed
- * @param token Token address (use address(0) for native tokens)
- * @param amount Amount of tokens to withdraw
- */
- function withdraw(address wallet, address token, uint256 amount) external;
-}
-```
-
-### Channel Lifecycle
-
-1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE`. The channel can be opened immediately in one transaction if both participants provide signatures over the initial state, with both participants' funds being deducted from their available balances.
-2. **Active**: Once fully funded (either through single-transaction creation or separate join), the channel transitions to active state for off-chain operation
-3. **Off-chain Updates**: Participants exchange and sign state updates according to application logic
-4. **Resolution**:
- - **Cooperative Close**: All parties sign a final state with `StateIntent.FINALIZE`
- - **Challenge-Response**: Participant can post a state on-chain and initiate challenge period
- - **Checkpoint**: Record valid state on-chain without closing for future dispute resolution
- - **Resize**: Adjust allocations by agreement without closing the channel
-
-#### Two-Phase Opening (Legacy)
-
-For scenarios requiring separate funding from external accounts:
-
-1. **Creation**: Creator calls `create()` with single signature, channel enters INITIAL status
-2. **Joining**: Second participant calls `join()` with their signature, transitioning channel to ACTIVE status
-
-### Example Implementation: Remittance Adjudicator
-
-The Remittance adjudicator validates payment transfers between participants:
-
-```solidity
-contract Remittance is IAdjudicator, IComparable {
- struct Intent {
- uint8 payer; // Index of the paying participant
- Amount transfer; // Amount and token being transferred
- }
-
- /**
- * @notice Validates a payment state transition
- * @dev Checks that the payer has signed and allocations are correct
- */
- function adjudicate(
- Channel calldata chan,
- State calldata candidate,
- State[] calldata proofs
- ) external returns (bool valid) {
- // Decode the payment intent
- Intent memory intent = abi.decode(candidate.data, (Intent));
-
- // For first state (version 1), need funding state as proof
- if (candidate.version == 1) {
- require(proofs.length >= 1, "Missing funding state");
- State memory funding = proofs[0];
- require(funding.intent == StateIntent.INITIALIZE, "Invalid funding state");
- }
-
- // For subsequent states, need previous state
- if (candidate.version > 1) {
- require(proofs.length >= 2, "Missing previous state");
- State memory previous = proofs[1];
-
- // Verify state transition
- require(candidate.version == previous.version + 1, "Invalid version");
-
- // Verify allocations match the intent
- require(
- candidate.allocations[intent.payer].amount ==
- previous.allocations[intent.payer].amount - intent.transfer.amount,
- "Invalid payer allocation"
- );
-
- uint8 payee = intent.payer == 0 ? 1 : 0;
- require(
- candidate.allocations[payee].amount ==
- previous.allocations[payee].amount + intent.transfer.amount,
- "Invalid payee allocation"
- );
- }
-
- // Verify payer has signed
- require(candidate.sigs[intent.payer].v != 0, "Missing payer signature");
-
- return true;
- }
-
- /**
- * @notice Compares states by version number
- */
- function compare(
- State calldata candidate,
- State calldata previous
- ) external view returns (int8 result) {
- if (candidate.version < previous.version) return -1;
- if (candidate.version > previous.version) return 1;
- return 0;
- }
-}
-```
-
-### Example Usage
-
-```solidity
-// Create a channel between Alice and Bob
-Channel memory channel = Channel({
- participants: [alice, bob],
- adjudicator: address(remittanceAdjudicator),
- challenge: 3600, // 1 hour challenge period
- nonce: 1
-});
-
-// Create initial funding state
-State memory fundingState = State({
- intent: StateIntent.INITIALIZE,
- version: 0,
- data: "",
- allocations: [
- Allocation(alice, tokenAddress, 100 ether),
- Allocation(bob, tokenAddress, 50 ether)
- ],
- sigs: [aliceSignature, bobSignature]
-});
-
-// Alice creates and funds the channel - opens immediately since both signatures provided
-bytes32 channelId = custody.create(channel, fundingState);
-
-// Alternative: Single-signature creation followed by join
-State memory fundingStatePartial = State({
- intent: StateIntent.INITIALIZE,
- version: 0,
- data: "",
- allocations: [
- Allocation(alice, tokenAddress, 100 ether),
- Allocation(bob, tokenAddress, 50 ether)
- ],
- sigs: [aliceSignature] // Only Alice's signature
-});
-bytes32 channelId = custody.create(channel, fundingStatePartial);
-custody.join(channelId, 1, bobSignature);
-
-// Off-chain: Alice pays Bob 10 tokens
-Intent memory paymentIntent = Intent({
- payer: 0, // Alice is payer
- transfer: Amount(tokenAddress, 10 ether)
-});
-
-State memory paymentState = State({
- intent: StateIntent.OPERATE,
- version: 1,
- data: abi.encode(paymentIntent),
- allocations: [
- Allocation(alice, tokenAddress, 90 ether),
- Allocation(bob, tokenAddress, 60 ether)
- ],
- sigs: [aliceSignature, bobSignature]
-});
-
-// Either party can checkpoint this state on-chain
-custody.checkpoint(channelId, paymentState, [fundingState]);
-```
-
-## Rationale
-
-The Nitrolite framework addresses critical blockchain scalability challenges through a lightweight state channel design. This standard provides clear interfaces to ensure interoperability while maintaining flexibility for diverse applications.
-
-**Efficiency**: Nitrolite minimizes on-chain footprint by requiring only essential operations (create, join, close) to occur on-chain, with all application logic executed off-chain. This enables instant finality and dramatically reduces gas costs.
-
-**Modularity**: The separation of concerns between the custody contract (IChannel), state validation (IAdjudicator), and fund management (IDeposit) allows developers to implement custom application logic while leveraging battle-tested infrastructure.
-
-**Flexibility**: The adjudicator pattern supports any application logic - from simple payments to complex multi-step protocols. The state intent system (INITIALIZE, OPERATE, RESIZE, FINALIZE) provides clear semantics for different operation types.
-
-**Security**: The challenge-response mechanism ensures that participants can always recover their funds by posting the latest valid state on-chain. The checkpoint feature allows proactive security by recording states without closing channels.
-
-**Chain Agnostic**: The protocol design avoids chain-specific features, enabling deployment across any EVM-compatible blockchain and facilitating cross-chain applications through the clearnode architecture.
-
-## Backwards Compatibility
-
-No backward compatibility issues found. This ERC is designed to coexist with existing standards and can integrate with [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) and [ERC-4337](https://www.erc4337.io/)
-
-## Security Considerations
-
-### On-Chain Security
-
-- **Signature Verification**: All state transitions require valid signatures from participants. The protocol supports signatures of all popular formats, including EIP-191 and EIP-712.
-- **Challenge Period**: The configurable challenge duration provides time for honest participants to respond to invalid states.
-- **Adjudicator Validation**: Custom adjudicators must be carefully audited as they control state transition rules.
-- **Reentrancy Protection**: Implementation should follow checks-effects-interactions pattern, especially in fund distribution.
-
-### Off-Chain Security
-
-- **State Storage**: Participants must securely store all signed states as they may need them for disputes.
-- **Signature Security**: Private keys used for state signing must be protected as compromise allows unauthorized state transitions.
-- **Availability**: Participants must monitor the chain for challenges during the challenge period.
-- **Front-running**: Challenge transactions may be front-run; implementations should consider commit-reveal schemes if needed.
-
-### Implementation Considerations
-
-- The custody contract strictly enforces 2-participant channels in the reference implementation.
-- Adjudicators should validate all state transitions according to application rules.
-- Proper nonce management prevents replay attacks across different channels.
-
-## Copyright
-
-Copyright and related rights waived via [CC0](https://github.com/ethereum/ERCs/blob/master/LICENSE.md).
diff --git a/erc7824-docs/docs/guides/index.md b/erc7824-docs/docs/guides/index.md
deleted file mode 100644
index aab9eff73..000000000
--- a/erc7824-docs/docs/guides/index.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-sidebar_position: 1
-title: Guides
-description: Comprehensive guides for working with Nitrolite and the ERC-7824 standard
-keywords: [guides, tutorials, migration, best practices, nitrolite, erc7824]
----
-
-import { Card, CardGrid } from '@site/src/components/Card';
-
-# Guides
-
-Welcome to the Nitrolite guides section. These comprehensive resources will help you understand and implement various aspects of the ERC-7824 protocol and Nitrolite SDK.
-
-
-
-
-
-## Guide Categories
-
-### Getting Started
-For developers new to Nitrolite, we recommend starting with our [Quick Start](/quick_start) guide, which covers the fundamental concepts and basic implementation.
-
-### Migration & Upgrades
-The [Migration Guide](migration-guide) provides detailed information about breaking changes between versions and how to update your code accordingly. This is essential reading when upgrading your Nitrolite dependencies.
-
-### Coming Soon
-
-We're continuously expanding our documentation. Future guides will include:
-
-- **Best Practices**: Optimization techniques and design patterns for state channel applications
-- **Security Guide**: Security considerations and audit recommendations
-- **Performance Tuning**: Maximizing throughput and minimizing latency
-- **Integration Examples**: Real-world examples of integrating Nitrolite with popular frameworks
-- **Troubleshooting**: Common issues and their solutions
-
-## Contributing
-
-Have a guide idea or found an issue? We welcome contributions! Please visit our [GitHub repository](https://github.com/erc7824/nitrolite) to submit suggestions or improvements.
\ No newline at end of file
diff --git a/erc7824-docs/docs/guides/migration-guide.md b/erc7824-docs/docs/guides/migration-guide.md
deleted file mode 100644
index 39c8bf862..000000000
--- a/erc7824-docs/docs/guides/migration-guide.md
+++ /dev/null
@@ -1,930 +0,0 @@
----
-sidebar_position: 2
-title: Migration Guide
-description: Guide to migrate to newer versions of Nitrolite
-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 Nitrolite, 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 "`
-
-### Nitrolite SDK
-
-You should definitely read this section if you are using the Nitrolite 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
-
-#### 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 NitroliteClient 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.
-
-### Nitrolite SDK
-
-You should definitely read this section if you are using the Nitrolite SDK.
-
-#### Client: Replaced `stateWalletClient` with `StateSigner`
-
-The `stateWalletClient` parameter of `NitroliteClient` 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 {
- createNitroliteClient,
- WalletStateSigner
-} from '@erc7824/nitrolite';
-// add-end
-
-const client = createNitroliteClient({
- 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 = NitroliteRPC.createRequest(
- requestId,
- RPCMethod.GetChannels,
- [participant, status],
- timestamp
-);
-// remove-end
-// add-start
-const request = NitroliteRPC.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 Nitrolite 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 Nitrolite 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 Nitrolite 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 Nitrolite 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.
diff --git a/erc7824-docs/docs/index.md b/erc7824-docs/docs/index.md
deleted file mode 100644
index 09c3c9d3e..000000000
--- a/erc7824-docs/docs/index.md
+++ /dev/null
@@ -1,62 +0,0 @@
----
-sidebar_position: 1
-title: Introduction
-description: Build scalable web3 applications with state channels using Nitrolite.
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, javascript, typescript, sdk]
----
-
-import { Card, CardGrid } from '@site/src/components/Card';
-
-# Introduction
-
-Welcome to Nitrolite! Built on the ERC-7824 standard, Nitrolite is a powerful framework that enables developers to build high-performance decentralized applications with near-instant finality.
-
-The following guides will walk you through the complete lifecycle of Nitrolite applications, from client initialization to application sessions. Whether you're building payment systems, games, financial applications, or any use case requiring high-frequency transactions, Nitrolite provides the infrastructure you need.
-
-
-
-
-
-
-
-
-
-
-## Key Features
-
-- **Instant Transactions**: Off-chain operations mean no waiting for block confirmations.
-- **Minimal Gas Fees**: On-chain gas is primarily for channel opening and settlement.
-- **High Throughput**: Capable of handling thousands of transactions per second.
-- **Application Flexibility**: Ideal for games, payment systems, real-time interactions, and more.
-
-## Core SDK Architecture
-
-The Nitrolite SDK is designed with modularity and broad compatibility in mind:
-
-1. **NitroliteClient**: This is the main entry point for developers. It provides a high-level API to manage the lifecycle of state channels, including deposits, channel creation, application session management, and withdrawals.
-
-2. **Nitrolite RPC**: This component handles the secure, real-time, off-chain communication between channel participants and the broker. It's responsible for message signing, verification, and routing.
diff --git a/erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md b/erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md
deleted file mode 100644
index ce7b5e4c5..000000000
--- a/erc7824-docs/docs/nitrolite_client/advanced/abstract-accounts.md
+++ /dev/null
@@ -1,427 +0,0 @@
----
-sidebar_position: 1
-title: Abstract Accounts
-description: Using NitroliteClient with Account Abstraction
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, account abstraction, ERC-4337]
----
-
-import MethodDetails from '@site/src/components/MethodDetails';
-import { Card, CardGrid } from '@site/src/components/Card';
-
-# Using with Abstract Accounts
-
-The `NitroliteClient` provides special support for [ERC-4337 Account Abstraction](https://eips.ethereum.org/EIPS/eip-4337) through the `txPreparer` object. This allows dApps using smart contract wallets to prepare transactions without executing them, enabling batching and other advanced patterns.
-
-## Transaction Preparer Overview
-
-The `txPreparer` is a property of the `NitroliteClient` that provides methods for preparing transaction data without sending it to the blockchain. Each method returns one or more [`PreparedTransaction`](../types.md#preparedtransaction) objects that can be used with Account Abstraction providers.
-
-```typescript
-import { NitroliteClient } from '@erc7824/nitrolite';
-
-const client = new NitroliteClient({/* config */});
-
-// Instead of: await client.deposit(amount)
-const txs = await client.txPreparer.prepareDepositTransactions(amount);
-```
-
-## Transaction Preparation Methods
-
-These methods allow you to prepare transactions for the entire channel lifecycle without executing them.
-
-### 1. Deposit Methods
-
-`}
- example={`// Prepare deposit transaction(s) - may include ERC20 approval
-const txs = await client.txPreparer.prepareDepositTransactions(1000000n);
-
-// For each prepared transaction
-for (const tx of txs) {
- await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
- });
-}`}
-/>
-
-`}
- example={`// Prepare approval transaction
-const tx = await client.txPreparer.prepareApproveTokensTransaction(2000000n);
-
-// Send through your AA provider
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data
-});`}
-/>
-
-### 2. Channel Creation Methods
-
-`}
- example={`// Prepare channel creation transaction
-const tx = await client.txPreparer.prepareCreateChannelTransaction({
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
-});
-
-// Send it through your Account Abstraction provider
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
-});`}
-/>
-
-`}
- example={`// Prepare deposit + channel creation (potentially 3 txs: approve, deposit, create)
-const txs = await client.txPreparer.prepareDepositAndCreateChannelTransactions(
- 1000000n,
- {
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
- }
-);
-
-// Bundle these transactions into a single UserOperation
-await aaProvider.sendUserOperation({
- userOperations: txs.map(tx => ({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
- }))
-});`}
-/>
-
-### 3. Channel Operation Methods
-
-`}
- example={`// Prepare checkpoint transaction
-const tx = await client.txPreparer.prepareCheckpointChannelTransaction({
- channelId: '0x...',
- candidateState: state
-});
-
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data
-});`}
-/>
-
-`}
- example={`// Prepare challenge transaction
-const tx = await client.txPreparer.prepareChallengeChannelTransaction({
- channelId: '0x...',
- candidateState: state
-});
-
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data
-});`}
-/>
-
-`}
- example={`// Prepare resize transaction
-const tx = await client.txPreparer.prepareResizeChannelTransaction({
- channelId: '0x...',
- candidateState: state
-});
-
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data
-});`}
-/>
-
-### 4. Channel Closing Methods
-
-`}
- example={`// Prepare close channel transaction
-const tx = await client.txPreparer.prepareCloseChannelTransaction({
- finalState: {
- channelId: '0x...',
- stateData: '0x...',
- allocations: [allocation1, allocation2],
- version: 10n,
- serverSignature: signature
- }
-});
-
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data
-});`}
-/>
-
-### 5. Withdrawal Methods
-
-`}
- example={`// Prepare withdrawal transaction
-const tx = await client.txPreparer.prepareWithdrawalTransaction(500000n);
-
-await aaProvider.sendUserOperation({
- target: tx.to,
- data: tx.data
-});`}
-/>
-
-
-## Understanding PreparedTransaction
-
-The [`PreparedTransaction`](../types.md#preparedtransaction) type is the core data structure returned by all transaction preparation methods. It contains all the information needed to construct a transaction or UserOperation:
-
-```typescript
-type PreparedTransaction = {
- // Target contract address
- to: Address;
-
- // Contract call data
- data?: Hex;
-
- // ETH value to send (0n for token operations)
- value?: bigint;
-};
-```
-
-Each `PreparedTransaction` represents a single contract call that can be:
-
-1. **Executed directly** - If you're using a standard EOA wallet
-2. **Bundled into a UserOperation** - For account abstraction providers
-3. **Batched with other transactions** - For advanced use cases
-
-## Integration Examples
-
-The Nitrolite transaction preparer can be integrated with any Account Abstraction provider. Here are examples with popular AA SDKs:
-
-### With Safe Account Abstraction SDK
-
-```typescript
-import { NitroliteClient } from '@erc7824/nitrolite';
-import { AccountAbstraction } from '@safe-global/account-abstraction-kit-poc';
-
-// Initialize clients
-const client = new NitroliteClient({/* config */});
-const aaKit = new AccountAbstraction(safeProvider);
-
-// Prepare transaction
-const tx = await client.txPreparer.prepareCreateChannelTransaction({
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
-});
-
-// Send through AA provider
-const safeTransaction = await aaKit.createTransaction({
- transactions: [{
- to: tx.to,
- data: tx.data,
- value: tx.value?.toString() || '0'
- }]
-});
-
-const txResponse = await aaKit.executeTransaction(safeTransaction);
-```
-
-### With Biconomy SDK
-
-```typescript
-import { NitroliteClient } from '@erc7824/nitrolite';
-import { BiconomySmartAccountV2 } from "@biconomy/account";
-
-// Initialize clients
-const client = new NitroliteClient({/* config */});
-const smartAccount = await BiconomySmartAccountV2.create({/* config */});
-
-// Prepare transaction
-const txs = await client.txPreparer.prepareDepositAndCreateChannelTransactions(
- 1000000n,
- {
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
- }
-);
-
-// Build user operation
-const userOp = await smartAccount.buildUserOp(
- txs.map(tx => ({
- to: tx.to,
- data: tx.data,
- value: tx.value || 0n
- }))
-);
-
-// Send user operation
-const userOpResponse = await smartAccount.sendUserOp(userOp);
-await userOpResponse.wait();
-```
-
-## Advanced Use Cases
-
-The transaction preparer is especially powerful when combined with advanced Account Abstraction features.
-
-### Batching Multiple Operations
-
-One of the main advantages of Account Abstraction is the ability to batch multiple operations into a single transaction:
-
-```typescript
-// Collect prepared transactions from different operations
-const preparedTxs = [];
-
-// 1. Add token approval if needed
-const allowance = await client.getTokenAllowance();
-if (allowance < totalNeeded) {
- const approveTx = await client.txPreparer.prepareApproveTokensTransaction(totalNeeded);
- preparedTxs.push(approveTx);
-}
-
-// 2. Add deposit
-const depositTx = await client.txPreparer.prepareDepositTransactions(amount);
-preparedTxs.push(...depositTx);
-
-// 3. Add channel creation
-const createChannelTx = await client.txPreparer.prepareCreateChannelTransaction(params);
-preparedTxs.push(createChannelTx);
-
-// 4. Execute all as a batch with your AA provider
-await aaProvider.sendUserOperation({
- userOperations: preparedTxs.map(tx => ({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
- }))
-});
-```
-
-### Gas Sponsoring
-
-Account Abstraction enables gas sponsorship, where someone else pays for the transaction gas:
-
-```typescript
-// Prepare transaction
-const tx = await client.txPreparer.prepareCreateChannelTransaction(params);
-
-// Use a sponsored transaction
-await paymasterProvider.sponsorTransaction({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n,
- user: userAddress
-});
-```
-
-### Session Keys
-
-Some AA wallets support session keys, which are temporary keys with limited permissions:
-
-```typescript
-// Create a session key with permissions only for specific operations
-const sessionKeyData = await aaWallet.createSessionKey({
- permissions: [
- {
- target: client.addresses.custody,
- // Only allow specific functions
- functionSelector: [
- "0xdeposit(...)",
- "0xwithdraw(...)"
- ]
- }
- ],
- expirationTime: Date.now() + 3600 * 1000 // 1 hour
-});
-
-// Use the session key to prepare and send transactions
-const tx = await client.txPreparer.prepareDepositTransactions(amount);
-await aaWallet.executeWithSessionKey(sessionKeyData, {
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
-});
-```
-
-## Best Practices
-
-
-
-
Batch Related Operations
-
Use prepareDepositAndCreateChannelTransactions to batch deposit and channel creation into a single user operation.
-
-
-
-
Handle Approvals
-
For ERC20 tokens, prepareDepositTransactions will include an approval transaction if needed. Always process all returned transactions.
-
-
-
-
State Signing
-
Even when using Account Abstraction, state signatures are handled separately using the stateWalletClient (or walletClient if not specified).
-
-
-
-
Error Handling
-
The preparation methods throw the same errors as their execution counterparts, so use the same error handling patterns.
-
-
-
-
Check Token Allowances
-
Before preparing token operations, you can check if approval is needed:
When using Account Abstraction, gas estimation is typically handled by the AA provider, but you can request estimates if needed.
-
-
-
-## Limitations
-
-:::caution Important
-- The transaction preparer **doesn't handle sequencing or nonce management** - that's the responsibility of your AA provider.
-- Some operations (like checkpointing) require signatures from all participants, which must be collected separately from the transaction preparation.
-:::
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md b/erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md
deleted file mode 100644
index c8e3c6e23..000000000
--- a/erc7824-docs/docs/nitrolite_client/advanced/erc20-service.md
+++ /dev/null
@@ -1,218 +0,0 @@
----
-sidebar_position: 3
-title: Erc20Service
-description: Documentation for the ERC20Service class
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced, erc20, tokens]
----
-
-# Erc20Service
-
-The `Erc20Service` class provides a convenient interface for interacting with ERC20 tokens. It handles token approvals, allowance checks, and balance inquiries that are essential for deposit and withdrawal operations in the Nitrolite system.
-
-## Initialization
-
-```typescript
-import { Erc20Service } from '@erc7824/nitrolite';
-
-const erc20Service = new Erc20Service(
- publicClient, // viem PublicClient
- walletClient // viem WalletClient
-);
-```
-
-## Core Methods
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `approve` | Approves a spender to use tokens. | `tokenAddress: Address, spender: Address, amount: bigint` | `Promise` |
-| `getTokenAllowance` | Gets token allowance for a spender. | `tokenAddress: Address, owner: Address, spender: Address` | `Promise` |
-| `getTokenBalance` | Gets token balance for an account. | `tokenAddress: Address, account: Address` | `Promise` |
-
-## Method Details
-
-### Approve Tokens
-
-Approves a spender (typically the Custody contract) to transfer tokens on behalf of the owner.
-
-```typescript
-// Approve the custody contract to spend 1000 tokens
-const txHash = await erc20Service.approve(
- tokenAddress, // ERC20 token address
- spenderAddress, // Custody contract address
- 1000000000000000000n // Amount to approve (1 token with 18 decimals)
-);
-```
-
-**Important notes:**
-- For security reasons, always specify the exact amount you want to approve
-- Consider using the ERC20 token decimals for the amount calculation
-- The transaction will fail if the owner has insufficient balance
-
-### Get Token Allowance
-
-Retrieves the current allowance granted by an owner to a spender.
-
-```typescript
-// Check current allowance
-const allowance = await erc20Service.getTokenAllowance(
- tokenAddress, // ERC20 token address
- ownerAddress, // Owner's address
- spenderAddress // Spender's address (custody contract)
-);
-
-console.log(`Current allowance: ${allowance}`);
-
-// Check if allowance is sufficient
-if (allowance < requiredAmount) {
- console.log('Need to approve more tokens');
-}
-```
-
-### Get Token Balance
-
-Retrieves the token balance for a specific account.
-
-```typescript
-// Check token balance
-const balance = await erc20Service.getTokenBalance(
- tokenAddress, // ERC20 token address
- accountAddress // Account to check
-);
-
-console.log(`Account balance: ${balance}`);
-
-// Check if balance is sufficient
-if (balance < requiredAmount) {
- console.log('Insufficient token balance');
-}
-```
-
-## Transaction Preparation
-
-For Account Abstraction support, `Erc20Service` provides a transaction preparation method:
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `prepareApprove` | Prepares an approval transaction. | `tokenAddress: Address, spender: Address, amount: bigint` | `Promise` |
-
-Example:
-```typescript
-// Prepare approval transaction
-const tx = await erc20Service.prepareApprove(
- tokenAddress,
- spenderAddress,
- amount
-);
-
-// Use with your Account Abstraction provider
-const userOp = await aaProvider.buildUserOperation({
- target: tx.to,
- data: tx.data,
- value: 0n // ERC20 approvals don't require ETH
-});
-```
-
-## Implementation Details
-
-The `Erc20Service` uses the standard ERC20 interface methods:
-
-- `approve`: Allows a spender to withdraw tokens from the owner's account, up to the specified amount
-- `allowance`: Returns the remaining tokens that a spender is allowed to withdraw
-- `balanceOf`: Returns the token balance of the specified account
-
-## Working with Token Decimals
-
-ERC20 tokens typically have decimal places (most commonly 18). When working with token amounts, you should account for these decimals:
-
-```typescript
-import { parseUnits } from 'viem';
-
-// For a token with 18 decimals
-const tokenDecimals = 18;
-
-// Convert 1.5 tokens to the smallest unit
-const amount = parseUnits('1.5', tokenDecimals);
-
-// Approve the amount
-await erc20Service.approve(tokenAddress, spenderAddress, amount);
-```
-
-## Error Handling
-
-The `Erc20Service` throws specific error types:
-
-- `TokenError`: For token-specific errors (insufficient balance, approval failures)
-- `ContractCallError`: When calls to the contract fail
-- `WalletClientRequiredError`: When wallet client is needed but not provided
-
-Example:
-```typescript
-try {
- await erc20Service.approve(tokenAddress, spenderAddress, amount);
-} catch (error) {
- if (error instanceof TokenError) {
- console.error(`Token error: ${error.message}`);
- console.error(`Suggestion: ${error.suggestion}`);
-
- // Check for specific token error conditions
- if (error.details?.errorName === 'InsufficientBalance') {
- console.log(`Available balance: ${error.details.available}`);
- }
- }
-}
-```
-
-## Common Patterns
-
-### Checking and Approving Tokens
-
-A common pattern is to check if the current allowance is sufficient before approving more tokens:
-
-```typescript
-// Get current allowance
-const allowance = await erc20Service.getTokenAllowance(
- tokenAddress,
- ownerAddress,
- spenderAddress
-);
-
-// If allowance is insufficient, approve more tokens
-if (allowance < requiredAmount) {
- await erc20Service.approve(tokenAddress, spenderAddress, requiredAmount);
-}
-
-// Now proceed with the operation that requires the approval
-// (e.g., deposit into custody contract)
-```
-
-### Handling Multiple Tokens
-
-If your application works with multiple tokens, you can reuse the same `Erc20Service` instance:
-
-```typescript
-// Same service instance for different tokens
-const erc20Service = new Erc20Service(publicClient, walletClient);
-
-// Work with token A
-const balanceA = await erc20Service.getTokenBalance(tokenAddressA, accountAddress);
-
-// Work with token B
-const balanceB = await erc20Service.getTokenBalance(tokenAddressB, accountAddress);
-```
-
-## Integration with NitroliteClient
-
-When using the `NitroliteClient`, you typically don't need to interact with `Erc20Service` directly, as the client handles these operations for you:
-
-```typescript
-// NitroliteClient handles token approvals automatically during deposit
-await nitroliteClient.deposit(amount);
-
-// For explicit approval without deposit
-await nitroliteClient.approveTokens(amount);
-
-// Get token balance through the client
-const balance = await nitroliteClient.getTokenBalance();
-```
-
-However, for advanced use cases or custom token interaction, direct access to `Erc20Service` can be useful.
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_client/advanced/index.md b/erc7824-docs/docs/nitrolite_client/advanced/index.md
deleted file mode 100644
index 01f03d640..000000000
--- a/erc7824-docs/docs/nitrolite_client/advanced/index.md
+++ /dev/null
@@ -1,150 +0,0 @@
----
-sidebar_position: 3
-title: Advanced Usage
-description: Advanced topics for working with the NitroliteClient
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced]
----
-
-# Advanced Usage
-
-This section covers advanced topics for working with the `@erc7824/nitrolite` SDK. These are intended for developers who need deeper control over the state channel operations or want to integrate with specialized systems.
-
-## Available Topics
-
-### Low-level Services
-
-The NitroliteClient is built on top of specialized services that can be used directly for more fine-grained control:
-
-- [NitroliteService](./nitrolite-service.md) - Core service for interacting with the custody contract and managing channels
-- [Erc20Service](./erc20-service.md) - Service for interacting with ERC20 tokens
-
-### Account Abstraction Integration
-
-- [Abstract Accounts](./abstract-accounts.md) - Using NitroliteClient with ERC-4337 Account Abstraction for smart contract wallets
-
-## When to Use Advanced Features
-
-Consider using the advanced features when:
-
-1. **Building custom workflows** that require more control than the high-level NitroliteClient API provides
-2. **Integrating with smart contract wallets** or other account abstraction systems
-3. **Implementing specialized monitoring or management systems** for state channels
-4. **Developing cross-chain applications** that require custom handling of state channel operations
-5. **Optimizing gas usage** through transaction batching and other techniques
-
-## Example: Direct Service Usage
-
-While using the high-level NitroliteClient is recommended for most applications, here's how you can work directly with the services:
-
-```typescript
-import {
- NitroliteService,
- Erc20Service,
- getChannelId,
- signState
-} from '@erc7824/nitrolite';
-
-// Initialize services
-const nitroliteService = new NitroliteService(
- publicClient,
- addresses,
- walletClient,
- account.address
-);
-
-const erc20Service = new Erc20Service(
- publicClient,
- walletClient
-);
-
-// Check token allowance
-const allowance = await erc20Service.getTokenAllowance(
- tokenAddress,
- account.address,
- addresses.custody
-);
-
-// Approve tokens if needed
-if (allowance < depositAmount) {
- await erc20Service.approve(tokenAddress, addresses.custody, depositAmount);
-}
-
-// Deposit funds
-await nitroliteService.deposit(tokenAddress, depositAmount);
-
-// Create channel with custom parameters
-const channelNonce = generateChannelNonce(account.address);
-const channel = {
- participants: [account.address, counterpartyAddress],
- adjudicator: addresses.adjudicator,
- challenge: 100n, // Challenge duration in seconds
- nonce: channelNonce
-};
-
-// Prepare initial state
-const initialState = {
- intent: StateIntent.INITIALIZE,
- version: 0n,
- data: '0x1234', // Application-specific data
- allocations: [
- { destination: account.address, token: tokenAddress, amount: 700000n },
- { destination: counterpartyAddress, token: tokenAddress, amount: 300000n }
- ],
- sigs: [] // Will be filled with signatures
-};
-
-// Sign the state
-const channelId = getChannelId(channel);
-const stateHash = getStateHash(channelId, initialState);
-const signature = await signState(walletClient, stateHash);
-initialState.sigs = [signature];
-
-// Create the channel
-await nitroliteService.createChannel(channel, initialState);
-```
-
-## Example: Complex Transaction Preparation
-
-For applications using Account Abstraction, you can prepare complex transaction sequences:
-
-```typescript
-import { NitroliteClient, NitroliteTransactionPreparer } from '@erc7824/nitrolite';
-
-// Initialize client
-const client = new NitroliteClient({/* config */});
-
-// Access the transaction preparer directly
-const txPreparer = client.txPreparer;
-
-// Prepare a complete sequence (approve, deposit, create channel)
-const allTxs = [];
-
-// 1. Check and prepare token approval if needed
-const allowance = await client.getTokenAllowance();
-if (allowance < depositAmount) {
- const approveTx = await txPreparer.prepareApproveTokensTransaction(depositAmount);
- allTxs.push(approveTx);
-}
-
-// 2. Prepare deposit
-const depositTx = await txPreparer.prepareDepositTransactions(depositAmount);
-allTxs.push(...depositTx);
-
-// 3. Prepare channel creation
-const createChannelTx = await txPreparer.prepareCreateChannelTransaction({
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
-});
-allTxs.push(createChannelTx);
-
-// 4. Use with your Account Abstraction provider
-await aaProvider.sendUserOperation({
- userOperations: allTxs.map(tx => ({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
- }))
-});
-```
-
-These advanced techniques give you greater flexibility and control over the state channel operations, but they also require a deeper understanding of the underlying protocol.
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md b/erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md
deleted file mode 100644
index c4452fc41..000000000
--- a/erc7824-docs/docs/nitrolite_client/advanced/nitrolite-service.md
+++ /dev/null
@@ -1,202 +0,0 @@
----
-sidebar_position: 2
-title: NitroliteService
-description: Documentation for the core NitroliteService class
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced]
----
-
-# NitroliteService
-
-The `NitroliteService` class is the core service that directly interacts with the Nitrolite Custody smart contract. It handles channel management, deposits, withdrawals, and all other channel-specific operations following the channel lifecycle.
-
-## Initialization
-
-```typescript
-import { NitroliteService } from '@erc7824/nitrolite';
-
-const nitroliteService = new NitroliteService(
- publicClient, // viem PublicClient
- addresses, // ContractAddresses
- walletClient, // viem WalletClient
- account // Account address
-);
-```
-
-## Channel Lifecycle Methods
-
-### 1. Deposit Operations
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `deposit` | Deposits tokens/ETH into the custody contract. | `tokenAddress: Address, amount: bigint` | `Promise` |
-| `withdraw` | Withdraws tokens from the custody contract. | `tokenAddress: Address, amount: bigint` | `Promise` |
-
-Example:
-```typescript
-// Deposit ETH or token
-const txHash = await nitroliteService.deposit(tokenAddress, amount);
-
-// Withdraw ETH or token
-const txHash = await nitroliteService.withdraw(tokenAddress, amount);
-```
-
-### 2. Channel Creation
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `createChannel` | Creates a new channel with the given parameters. | `channel: Channel, initialState: State` | `Promise` |
-
-Example:
-```typescript
-// Create a channel
-const txHash = await nitroliteService.createChannel(channel, initialState);
-```
-
-Where:
-- `channel` defines the participants, adjudicator, challenge period, and nonce
-- `initialState` contains the initial allocation of funds and state data
-
-### 3. Channel Operations
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `checkpoint` | Checkpoints a state on-chain. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` |
-| `challenge` | Challenges a channel with a candidate state. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` |
-| `resize` | Resizes a channel with a candidate state. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` |
-
-Example:
-```typescript
-// Checkpoint a channel state
-const txHash = await nitroliteService.checkpoint(channelId, candidateState);
-
-// Challenge a channel
-const txHash = await nitroliteService.challenge(channelId, candidateState);
-
-// Resize a channel
-const txHash = await nitroliteService.resize(channelId, candidateState);
-```
-
-### 4. Channel Closing
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `close` | Closes a channel using a final state. | `channelId: ChannelId, finalState: State` | `Promise` |
-
-Example:
-```typescript
-// Close a channel
-const txHash = await nitroliteService.close(channelId, finalState);
-```
-
-### 5. Account Information
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `getAccountChannels` | Gets channel IDs for an account. | `accountAddress: Address` | `Promise` |
-| `getAccountInfo` | Gets account info for a token. | `accountAddress: Address, tokenAddress: Address` | `Promise` |
-
-Example:
-```typescript
-// Get all channels for an account
-const channels = await nitroliteService.getAccountChannels(accountAddress);
-
-// Get detailed account info
-const info = await nitroliteService.getAccountInfo(accountAddress, tokenAddress);
-console.log(`Available: ${info.available}, Locked: ${info.locked}`);
-```
-
-## Transaction Preparation Methods
-
-For Account Abstraction support, NitroliteService provides transaction preparation methods that return transaction data without executing it:
-
-| Method | Description | Parameters | Return Type |
-|--------|-------------|------------|------------|
-| `prepareDeposit` | Prepares deposit transaction. | `tokenAddress: Address, amount: bigint` | `Promise` |
-| `prepareCreateChannel` | Prepares channel creation transaction. | `channel: Channel, initialState: State` | `Promise` |
-| `prepareCheckpoint` | Prepares checkpoint transaction. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` |
-| `prepareChallenge` | Prepares challenge transaction. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` |
-| `prepareResize` | Prepares resize transaction. | `channelId: ChannelId, candidateState: State, proofStates?: State[]` | `Promise` |
-| `prepareClose` | Prepares close transaction. | `channelId: ChannelId, finalState: State` | `Promise` |
-| `prepareWithdraw` | Prepares withdraw transaction. | `tokenAddress: Address, amount: bigint` | `Promise` |
-
-Example:
-```typescript
-// Prepare deposit transaction
-const tx = await nitroliteService.prepareDeposit(tokenAddress, amount);
-
-// Use with your Account Abstraction provider
-const userOp = await aaProvider.buildUserOperation({
- target: tx.to,
- data: tx.data,
- value: tx.value || 0n
-});
-```
-
-## Implementation Details
-
-The `NitroliteService` connects to the Custody contract using:
-
-- A viem `PublicClient` for read operations
-- A viem `WalletClient` for write operations and signing
-- The contract address specified in the configuration
-
-The service handles:
-- Contract interaction
-- Parameter validation
-- Error handling
-- Transaction preparation
-
-## Error Handling
-
-The `NitroliteService` throws specific error types:
-
-- `ContractCallError`: When calls to the contract fail
-- `InvalidParameterError`: When parameters are invalid
-- `MissingParameterError`: When required parameters are missing
-- `WalletClientRequiredError`: When wallet client is needed but not provided
-- `AccountRequiredError`: When account is needed but not provided
-
-Example:
-```typescript
-try {
- await nitroliteService.deposit(tokenAddress, amount);
-} catch (error) {
- if (error instanceof ContractCallError) {
- console.error(`Contract call failed: ${error.message}`);
- console.error(`Suggestion: ${error.suggestion}`);
- }
-}
-```
-
-## Advanced Usage
-
-### Custom Contract Interaction
-
-For advanced use cases, you might need to interact directly with the contract:
-
-```typescript
-// Get the custody contract address
-const custodyAddress = nitroliteService.custodyAddress;
-
-// Use with your own custom contract interaction
-const customContract = getContract({
- address: custodyAddress,
- abi: custodyAbi,
- // Additional configuration...
-});
-```
-
-### Multiple Channel Management
-
-For applications managing multiple channels:
-
-```typescript
-// Get all channels for the account
-const channels = await nitroliteService.getAccountChannels(accountAddress);
-
-// Process each channel
-for (const channelId of channels) {
- // Get channel info from contract
- // Process channel state
-}
-```
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md b/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md
deleted file mode 100644
index 9422f138e..000000000
--- a/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md
+++ /dev/null
@@ -1,86 +0,0 @@
----
-sidebar_position: 4
-title: Supported Signature Formats
-description: Documentation for supported signature formats in NitroliteClient
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced, signature, format, ECDSA, EIP-191, EIP-712, EIP-1271, EIP-6492]
----
-
-# Supported Signature Formats
-
-The nitrolite smart contract supports multiple signature formats over a State to accommodate various use cases and compatibility with different wallets and applications.
-
-The message being signed is a channelId and State, formatted in a specific way. The most common is a `packedState`, which is calculated as follows:
-
-```solidity
-abi.encode(channelId, state.intent, state.version, state.data, state.allocations)
-```
-
-## EOA signatures
-
-Externally Owned Accounts (EOAs) can sign messages with their private key using the ECDSA.
-
-Based on how the message is handled before signing, the following formats are supported:
-
-### Raw ECDSA Signature
-
-The message is a `packedState`, that is hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature.
-
-### EIP-191 Signature
-
-You can read more about EIP-191 in the [EIP-191 specification](https://eips.ethereum.org/EIPS/eip-191).
-
-The message is a `packedState` prefixed with `"\x19Ethereum Signed Message:\n" + len(packedState)` and hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature.
-
-### EIP-712 Signature
-
-You can read more about EIP-712 in the [EIP-712 specification](https://eips.ethereum.org/EIPS/eip-712).
-
-The message is an `AllowStateHash` typed data, calculated as follows:
-
-```solidity
-abi.encode(
- typeHash,
- channelId,
- state.intent,
- state.version,
- keccak256(state.data),
- keccak256(abi.encode(state.allocations))
-);
-```
-
-Where `typeHash` is `AllowStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)`.
-
-The message is then hashed with `keccak256`, appended to `"\x19\x01" || domainSeparator` and signed. The signature is a 65-byte ECDSA signature.
-
-`||` is a concatenation operator, and `domainSeparator` is calculated as follows:
-
-```solidity
-keccak256(
- abi.encode(
- EIP712_TYPE_HASH,
- keccak256(name),
- keccak256(version),
- chainId,
- verifyingContract
- )
-);
-```
-
-`EIP712_TYPE_HASH` is `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`.
-
-Additionally, `name`, `version` are the name and version of the Custody contract, `chainId` is the chain ID of the network, and `verifyingContract` is the address of the contract.
-
-## Smart Contract Signatures
-
-Smart Contracts that support [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) or [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492) can sign messages using their own logic. When checking such signatures, the nitrolite smart contract will pass the `keccak256` hash of the `packedState` as a message hash for verification.
-
-See the aforementioned EIP standards for details on how these signatures are structured and verified. If you want to add support for such signatures in your client, you probably need to look at how signature verification logic is implemented in the Smart Contract (Smart Wallet, etc) that will use them.
-
-## Challenge Signatures
-
-The aforementioned signature formats are used to sign States, however to submit a challenge, the user must provide a `challengerSignature`, which proves that the user has the right to challenge a Channel.
-
-Depending on a signature format, the `challengerSignature` is calculated differently from the common State signature:
-
-- **Raw ECDSA, EIP-191, EIP-1271 and EIP-6492**: The message (`packedState`) is suffixed with a `challenge` string (`abi.encodePacked(packedState, "challenge")`).
-- **EIP-712**: The `typeHash` name is `AllowChallengeStateHash`, while type format remains the same.
diff --git a/erc7824-docs/docs/nitrolite_client/index.mdx b/erc7824-docs/docs/nitrolite_client/index.mdx
deleted file mode 100644
index 0ddb5914f..000000000
--- a/erc7824-docs/docs/nitrolite_client/index.mdx
+++ /dev/null
@@ -1,122 +0,0 @@
----
-sidebar_position: 2
-title: NitroliteClient
-description: Nitrolite client SDK documentation
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, react tutorial]
----
-
-import { Card, CardGrid } from '@site/src/components/Card';
-
-# NitroliteClient
-
-The `NitroliteClient` class is the main entry point for interacting with Nitrolite state channels. It provides a comprehensive set of methods for managing channels, deposits, and funds.
-
-
-
-
-
-
-
-
-## Quick Start
-
-```typescript
-import { NitroliteClient } from '@erc7824/nitrolite';
-
-// Initialize client
-const nitroliteClient = new NitroliteClient({
- publicClient,
- walletClient,
- addresses: {
- custody: '0x...',
- adjudicator: '0x...',
- guestAddress: '0x...',
- tokenAddress: '0x...'
- },
- chainId: 137, // Polygon mainnet
- challengeDuration: 100n
-});
-
-// 1. Deposit funds
-const depositTxHash = await nitroliteClient.deposit(1000000n);
-
-// 2. Create a channel
-const { channelId, initialState, txHash } = await nitroliteClient.createChannel({
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
-});
-
-// 3. Resize the channel when needed
-const resizeTxHash = await nitroliteClient.resizeChannel({
- resizeState: {
- channelId,
- stateData: '0x5678',
- allocations: newAllocations,
- version: 2n,
- intent: StateIntent.RESIZE,
- serverSignature: signature
- },
- proofStates: []
-});
-
-// 4. Close the channel
-const closeTxHash = await nitroliteClient.closeChannel({
- finalState: {
- channelId,
- stateData: '0x5678',
- allocations: newAllocations,
- version: 5n,
- serverSignature: signature
- }
-});
-
-// 5. Withdraw funds
-const withdrawTxHash = await nitroliteClient.withdrawal(800000n);
-```
-
-{/* ## Learning Path
-
-
-
-
-
-
-
-
- */}
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_client/methods.mdx b/erc7824-docs/docs/nitrolite_client/methods.mdx
deleted file mode 100644
index 2cd7f3973..000000000
--- a/erc7824-docs/docs/nitrolite_client/methods.mdx
+++ /dev/null
@@ -1,236 +0,0 @@
----
-sidebar_position: 1
-title: Methods
-description: Complete API reference for NitroliteClient
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, react tutorial]
----
-
-import MethodDetails from '@site/src/components/MethodDetails';
-import { Card, CardGrid } from '@site/src/components/Card';
-
-# Methods
-
-This page provides a complete reference for all methods available in the `NitroliteClient` class from the `@erc7824/nitrolite` package.
-
-## Channel Lifecycle Methods
-
-These methods are organized according to the typical lifecycle of a state channel.
-
-### 1. Deposit Methods
-
-The deposit phase includes methods for managing funds in the custody contract and handling token approvals.
-
-
-
-
-
-
-
-
-
-### 2. Channel Creation Methods
-
-
-
-
-
-### 3. Channel Operation Methods
-
-
-
-
-
-
-
-### 4. Channel Closing Methods
-
-
-
-### 5. Withdrawal Methods
-
-
-
-## Account Information Methods
-
-These methods provide information about your account's state:
-
-
-
-
-
-:::caution Advanced Usage: Transaction Preparation
-For Account Abstraction support and transaction preparation methods, see the [Abstract Accounts](./advanced/abstract-accounts) page.
-:::
-
-## Example: Complete Channel Lifecycle
-
-```typescript
-import { NitroliteClient } from '@erc7824/nitrolite';
-
-// Initialize the client
-const client = new NitroliteClient({
- publicClient,
- walletClient,
- addresses: { custody, adjudicator, guestAddress, tokenAddress },
- chainId: 137,
- challengeDuration: 100n
-});
-
-// 1. Deposit funds
-const depositTxHash = await client.deposit(1000000n);
-
-// 2. Create a channel
-const { channelId, initialState } = await client.createChannel({
- initialAllocationAmounts: [700000n, 300000n],
- stateData: '0x1234'
-});
-
-// 3. Get account info to verify funds are locked
-const accountInfo = await client.getAccountInfo();
-console.log(`Locked in channels: ${accountInfo.locked}`);
-
-// 4. Later, resize the channel
-const resizeTxHash = await client.resizeChannel({
- resizeState: {
- channelId,
- stateData: '0x5678',
- allocations: newAllocations,
- version: 2n,
- intent: StateIntent.RESIZE,
- serverSignature: signature
- },
- proofStates: []
-});
-
-// 5. Close the channel
-const closeTxHash = await client.closeChannel({
- finalState: {
- channelId,
- stateData: '0x5678',
- allocations: [
- { destination: userAddress, token: tokenAddress, amount: 800000n },
- { destination: counterpartyAddress, token: tokenAddress, amount: 200000n }
- ],
- version: 5n,
- serverSignature: signature
- }
-});
-
-// 6. Withdraw funds
-const withdrawTxHash = await client.withdrawal(800000n);
-```
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_client/types.md b/erc7824-docs/docs/nitrolite_client/types.md
deleted file mode 100644
index e82cec173..000000000
--- a/erc7824-docs/docs/nitrolite_client/types.md
+++ /dev/null
@@ -1,371 +0,0 @@
----
-sidebar_position: 2
-title: Type Definitions
-description: Detailed type definitions used in the NitroliteClient
-keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, react tutorial]
----
-
-# Type Definitions
-
-This page documents the core types used throughout the `@erc7824/nitrolite` SDK. Understanding these types is essential for effectively working with the `NitroliteClient`.
-
-## Core Types
-
-### ChannelId
-
-```typescript
-type ChannelId = Hex;
-```
-
-A unique identifier for a state channel, represented as a hexadecimal string.
-
-### StateHash
-
-```typescript
-type StateHash = Hex;
-```
-
-A hash of a channel state, represented as a hexadecimal string.
-
-### Signature
-
-```typescript
-type Signature = Hex;
-```
-
-Represents a cryptographic signature used for signing state channel states as a hexadecimal string.
-
-### Allocation
-
-```typescript
-interface Allocation {
- destination: Address; // Where funds are sent on channel closure
- token: Address; // ERC-20 token address (zero address for ETH)
- amount: bigint; // Token amount allocated
-}
-```
-
-Represents the allocation of funds to a particular destination.
-
-### Channel
-
-```typescript
-interface Channel {
- participants: [Address, Address]; // List of participants [Host, Guest]
- adjudicator: Address; // Address of the contract that validates states
- challenge: bigint; // Duration in seconds for challenge period
- nonce: bigint; // Unique per channel with same participants and adjudicator
-}
-```
-
-Represents the core configuration of a state channel.
-
-### StateIntent
-
-```typescript
-enum StateIntent {
- OPERATE, // Operate the state application
- INITIALIZE, // Initial funding state
- RESIZE, // Resize state
- FINALIZE, // Final closing state
-}
-```
-
-Indicates the intent of a state update. The intent determines how the state is processed by the channel participants and the blockchain.
-
-### State
-
-```typescript
-interface State {
- intent: StateIntent; // Intent of the state
- version: bigint; // Version number, incremented for each update
- data: Hex; // Application data encoded as hex
- allocations: [Allocation, Allocation]; // Asset allocation for each participant
- sigs: Signature[]; // State hash signatures
-}
-```
-
-Represents a complete state channel state, including allocations and signatures.
-
-## Configuration Types
-
-### NitroliteClientConfig
-
-```typescript
-interface NitroliteClientConfig {
- /** The viem PublicClient for reading blockchain data. */
- publicClient: PublicClient;
-
- /**
- * The viem WalletClient used for:
- * 1. Sending on-chain transactions in direct execution methods (e.g., `client.deposit`).
- * 2. Providing the 'account' context for transaction preparation (`client.txPreparer`).
- * 3. Signing off-chain states *if* `stateWalletClient` is not provided.
- */
- walletClient: WalletClient>;
-
- /**
- * Optional: A separate viem WalletClient used *only* for signing off-chain state updates (`signMessage`).
- * Provide this if you want to use a different key (e.g., a "hot" key from localStorage)
- * for state signing than the one used for on-chain transactions.
- * If omitted, `walletClient` will be used for state signing.
- */
- stateWalletClient?: WalletClient>;
-
- /** Contract addresses required by the SDK. */
- addresses: ContractAddresses;
-
- /** Chain ID for the channel */
- chainId: number;
-
- /** Optional: Default challenge duration (in seconds) for new channels. Defaults to 0 if omitted. */
- challengeDuration?: bigint;
-}
-```
-
-Configuration for initializing the `NitroliteClient`.
-
-### ContractAddresses
-
-```typescript
-interface ContractAddresses {
- custody: Address; // Custody contract address
- adjudicator: Address; // Adjudicator contract address
- guestAddress: Address; // Guest participant address
- tokenAddress: Address; // Token address (zero address for ETH)
-}
-```
-
-Addresses of contracts used by the Nitrolite SDK.
-
-## Channel Lifecycle Parameter Types
-
-### 1. Deposit Phase
-
-Deposit operations primarily use simple `bigint` parameters for amounts.
-
-### 2. Channel Creation
-
-```typescript
-interface CreateChannelParams {
- initialAllocationAmounts: [bigint, bigint]; // Initial allocation for [host, guest]
- stateData?: Hex; // Application-specific data
-}
-```
-
-Parameters for creating a new channel.
-
-### 3. Channel Operations
-
-```typescript
-interface CheckpointChannelParams {
- channelId: ChannelId; // Channel ID to checkpoint
- candidateState: State; // State to checkpoint
- proofStates?: State[]; // Optional proof states
-}
-
-interface ChallengeChannelParams {
- channelId: ChannelId; // Channel ID to challenge
- candidateState: State; // State to submit as a challenge
- proofStates?: State[]; // Optional proof states
-}
-
-interface ResizeChannelParams {
- resizeState: {
- channelId: ChannelId;
- stateData: Hex;
- allocations: [Allocation, Allocation];
- version: bigint;
- intent: StateIntent;
- serverSignature: Signature;
- };
- proofStates: State[];
-}
-```
-
-Parameters for channel operations.
-
-### 4. Channel Closing
-
-```typescript
-interface CloseChannelParams {
- stateData?: Hex; // Optional application data for the final state
- finalState: {
- channelId: ChannelId; // Channel ID to close
- stateData: Hex; // Application-specific data
- allocations: [Allocation, Allocation]; // Final allocations
- version: bigint; // State version
- serverSignature: Signature; // Server's signature on the state
- };
-}
-```
-
-Parameters for collaboratively closing a channel.
-
-## Return Types
-
-### AccountInfo
-
-```typescript
-interface AccountInfo {
- available: bigint; // Available funds in the custody contract
- channelCount: bigint; // Number of channels
-}
-```
-
-Information about an account's funds in the custody contract.
-
-### PreparedTransaction
-
-```typescript
-type PreparedTransaction = {
- to: Address; // Target contract address
- data?: Hex; // Contract call data
- value?: bigint; // ETH value to send
-};
-```
-
-Represents the data needed to construct a transaction, used by the transaction preparer for Account Abstraction.
-
-## Type Usage By Channel Lifecycle Phase
-
-### 1. Deposit Phase
-
-```typescript
-// Deposit
-await client.deposit(amount: bigint): Promise
-
-// Check token details
-const balance: bigint = await client.getTokenBalance()
-const allowance: bigint = await client.getTokenAllowance()
-```
-
-### 2. Channel Creation Phase
-
-```typescript
-// Create channel
-const result: {
- channelId: ChannelId;
- initialState: State;
- txHash: Hash
-} = await client.createChannel({
- initialAllocationAmounts: [bigint, bigint],
- stateData: Hex
-})
-```
-
-### 3. Channel Operation Phase
-
-```typescript
-// Resize
-await client.resizeChannel({
- resizeState: {
- channelId: ChannelId,
- stateData: Hex,
- allocations: [Allocation, Allocation],
- version: bigint,
- intent: StateIntent,
- serverSignature: Signature
- },
- proofStates: State[]
-}): Promise
-
-// Resize is structured as:
-const resizeParams: ResizeChannelParams = {
- resizeState: {
- channelId,
- stateData: '0x1234',
- allocations: [
- { destination: addr1, token: tokenAddr, amount: 600000n },
- { destination: addr2, token: tokenAddr, amount: 400000n }
- ],
- version: 2n, // Incremented from previous
- intent: StateIntent.RESIZE,
- serverSignature: signature
- },
- proofStates: []
-}
-```
-
-### 4. Channel Closing Phase
-
-```typescript
-// Close
-await client.closeChannel({
- finalState: {
- channelId: ChannelId,
- stateData: Hex,
- allocations: [Allocation, Allocation],
- version: bigint,
- serverSignature: Signature
- }
-}): Promise
-```
-
-### 5. Withdrawal Phase
-
-```typescript
-// Withdraw
-await client.withdrawal(amount: bigint): Promise
-```
-
-## State Intent Lifecycle
-
-The `StateIntent` enum value determines how a state is interpreted:
-
-1. `StateIntent.INITIALIZE`: Used when creating a channel, defines the initial funding allocations
-2. `StateIntent.OPERATE`: Used during normal operations, for application-specific state updates
-3. `StateIntent.RESIZE`: Used when changing allocation amounts, e.g., adding funds to a channel
-4. `StateIntent.FINALIZE`: Used when closing a channel, defines the final allocations
-
-Example of state progression:
-
-```typescript
-// 1. Initial state (on channel creation)
-const initialState = {
- intent: StateIntent.INITIALIZE,
- version: 0n,
- data: '0x1234',
- allocations: [
- { destination: userAddr, token: tokenAddr, amount: 700000n },
- { destination: guestAddr, token: tokenAddr, amount: 300000n }
- ],
- sigs: [userSig, guestSig]
-};
-
-// 2. Operation state (during application usage)
-const operationalState = {
- intent: StateIntent.OPERATE,
- version: 1n, // Incremented
- data: '0x5678', // Application data changed
- allocations: [
- { destination: userAddr, token: tokenAddr, amount: 650000n }, // Balance changed
- { destination: guestAddr, token: tokenAddr, amount: 350000n } // Balance changed
- ],
- sigs: [userSig, guestSig]
-};
-
-// 3. Resize state (adding funds)
-const resizeState = {
- intent: StateIntent.RESIZE,
- version: 2n,
- data: '0x5678',
- allocations: [
- { destination: userAddr, token: tokenAddr, amount: 950000n }, // Added funds
- { destination: guestAddr, token: tokenAddr, amount: 450000n } // Added funds
- ],
- sigs: [userSig, guestSig]
-};
-
-// 4. Final state (closing channel)
-const finalState = {
- intent: StateIntent.FINALIZE,
- version: 3n,
- data: '0x9ABC',
- allocations: [
- { destination: userAddr, token: tokenAddr, amount: 930000n },
- { destination: guestAddr, token: tokenAddr, amount: 470000n }
- ],
- sigs: [userSig, guestSig]
-};
-```
diff --git a/erc7824-docs/docs/nitrolite_rpc/index.md b/erc7824-docs/docs/nitrolite_rpc/index.md
deleted file mode 100644
index 75d0d1f7d..000000000
--- a/erc7824-docs/docs/nitrolite_rpc/index.md
+++ /dev/null
@@ -1,177 +0,0 @@
----
-sidebar_position: 3
-title: NitroliteRPC
-description: Overview of the NitroliteRPC, its core logic, and links to detailed API and type definitions.
-keywords: [erc7824, statechannels, state channels, nitrolite, rpc, websockets, messaging, protocol]
----
-
-import { Card, CardGrid } from '@site/src/components/Card';
-import MethodDetails from '@site/src/components/MethodDetails';
-
-# NitroliteRPC
-
-The NitroliteRPC provides a secure, reliable real-time communication protocol for state channel applications. It enables off-chain message exchange, state updates, and channel management. This system is built around the `NitroliteRPC` class, which provides the foundational methods for message construction, signing, parsing, and verification.
-
-
-
-
-
-
-## Core Logic: The `NitroliteRPC` Class
-
-The `NitroliteRPC` class is central to the RPC system. It offers a suite of static methods to handle the low-level details of the NitroliteRPC protocol.
-
-### Message Creation
-
-
-
-
-
-
-
-
-
-### Message Signing
-
-", description: "The RPC request object to sign." },
- { name: "signer", type: "MessageSigner", description: "An async function that takes a message string and returns a Promise (signature)." }
- ]}
- returns="Promise>"
- example={`// Assuming 'request' is a NitroliteRPCRequest and 'signer' is a MessageSigner
-const signedRequest = await NitroliteRPC.signRequestMessage(request, signer);`}
-/>
-
-", description: "The RPC response object to sign." },
- { name: "signer", type: "MessageSigner", description: "An async function that takes a message string and returns a Promise (signature)." }
- ]}
- returns="Promise>"
- example={`// Assuming 'response' is a NitroliteRPCResponse and 'signer' is a MessageSigner
-const signedResponse = await NitroliteRPC.signResponseMessage(response, signer);`}
-/>
-
-### Message Parsing & Validation
-
-
-
-These methods ensure that all communication adheres to the defined RPC structure and security requirements.
-
-## Generic Message Structure
-
-The `NitroliteRPC` class operates on messages adhering to the following general structures. For precise details on each field and for specific message types, please refer to the [RPC Type Definitions](./rpc_types).
-
-```typescript
-// Generic Request message structure
-{
- "req": [requestId, method, params, timestamp], // Core request tuple
- "int"?: Intent, // Optional intent for state changes
- "acc"?: AccountID, // Optional account scope (channel/app ID)
- "sig": [signature] // Array of signatures
-}
-
-// Generic Response message structure
-{
- "res": [requestId, method, dataPayload, timestamp], // Core response tuple
- "acc"?: AccountID, // Optional account scope
- "int"?: Intent, // Optional intent
- "sig"?: [signature] // Optional signatures for certain response types
-}
-```
-
-## Next Steps
-
-Dive deeper into the specifics of the RPC system:
-
-
-
-
-
\ No newline at end of file
diff --git a/erc7824-docs/docs/nitrolite_rpc/message_creation_api.md b/erc7824-docs/docs/nitrolite_rpc/message_creation_api.md
deleted file mode 100644
index 39e17ed53..000000000
--- a/erc7824-docs/docs/nitrolite_rpc/message_creation_api.md
+++ /dev/null
@@ -1,403 +0,0 @@
----
-sidebar_position: 3
-title: RPC Message Creation API
-description: Detailed reference for functions that create specific, signed Nitrolite RPC request messages.
-keywords: [erc7824, statechannels, state channels, nitrolite, rpc, api, message creation, sdk]
----
-
-import MethodDetails from '@site/src/components/MethodDetails';
-
-# RPC Message Creation API
-
-The functions detailed below are part of the `@erc7824/nitrolite` SDK and provide a convenient way to create fully formed, signed, and stringified JSON RPC request messages. These messages are ready for transmission over a WebSocket or any other transport layer communicating with a Nitrolite-compatible broker.
-
-Each function typically takes a `MessageSigner` (a function you provide to sign messages, usually integrating with a user's wallet) and other relevant parameters, then returns a `Promise` which resolves to the JSON string of the signed RPC message.
-
-## Authentication
-
-These functions are used for the client authentication flow with the broker. The typical sequence is:
-1. Client sends an `auth_request` (using `createAuthRequestMessage`).
-2. Broker responds with an `auth_challenge`.
-3. Client signs the challenge and sends an `auth_verify` (using `createAuthVerifyMessageFromChallenge` or `createAuthVerifyMessage`).
-4. Broker responds with `auth_success` or `auth_failure`.
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Broker
-
- Client->>Broker: auth_request (via createAuthRequestMessage)
- Broker-->>Client: auth_challenge (response)
- Client->>Broker: auth_verify (via createAuthVerifyMessage or createAuthVerifyMessageFromChallenge)
- Broker-->>Client: auth_success / auth_failure (response)
-```
-
-
-
-
-
-
-
-## General & Keep-Alive
-
-Functions for general RPC interactions like keep-alive messages.
-
-
-
-## Query Operations
-
-Functions for retrieving information from the broker.
-
-
-
-
-
-
-
-
-
-## Application Session Management
-
-Functions for creating and closing application sessions (state channels).
-
-
-
-
-
-## Application-Specific Messaging
-
-Function for sending messages within an active application session.
-
-
-
-## Ledger Channel Management
-
-Functions for managing the underlying ledger channels (direct channels with the broker).
-
-
-
-
-
-## Advanced: Creating a Local Signer for Development
-
-To begin, you'll often need a fresh key pair (private key, public key, and address). The `generateKeyPair` utility can be used for this:
-
-```typescript
-import { ethers } from "ethers"; // Make sure ethers is installed
-
-// Definition (as provided in your example)
-interface CryptoKeypair {
- privateKey: string;
- publicKey: string;
- address: string;
-}
-
-export const generateKeyPair = async (): Promise => {
- try {
- const wallet = ethers.Wallet.createRandom();
- const privateKeyHash = ethers.utils.keccak256(wallet.privateKey);
- const walletFromHashedKey = new ethers.Wallet(privateKeyHash);
-
- return {
- privateKey: privateKeyHash,
- publicKey: walletFromHashedKey.publicKey,
- address: walletFromHashedKey.address,
- };
- } catch (error) {
- console.error('Error generating keypair, using fallback:', error);
- const randomHex = ethers.utils.randomBytes(32);
- const privateKey = ethers.utils.keccak256(randomHex);
- const wallet = new ethers.Wallet(privateKey);
-
- return {
- privateKey: privateKey,
- publicKey: wallet.publicKey,
- address: wallet.address,
- };
- }
-};
-
-// Usage:
-async function main() {
- const keyPair = await generateKeyPair();
- console.log("Generated Private Key:", keyPair.privateKey);
- console.log("Generated Address:", keyPair.address);
- // Store keyPair.privateKey securely if you need to reuse this signer
-}
-```
-This function creates a new random wallet, hashes its private key for deriving a new one (a common pattern for deterministic key generation or added obfuscation, though the security implications depend on the exact use case), and returns the private key, public key, and address.
-
-### Creating a Signer from a Private Key
-
-Once you have a private key (either generated as above or from a known development account), you can create a `MessageSigner` compatible with the RPC message creation functions. The `MessageSigner` interface typically expects an asynchronous `sign` method.
-
-```typescript
-import { ethers } from "ethers";
-import { Hex } from "viem"; // Assuming Hex type is from viem or similar
-
-// Definitions (as provided in your example)
-type RequestData = unknown; // Placeholder for actual request data type
-type ResponsePayload = unknown; // Placeholder for actual response payload type
-
-interface WalletSigner {
- publicKey: string;
- address: Hex;
- sign: (payload: RequestData | ResponsePayload) => Promise;
-}
-
-export const createEthersSigner = (privateKey: string): WalletSigner => {
- try {
- const wallet = new ethers.Wallet(privateKey);
-
- return {
- publicKey: wallet.publicKey,
- address: wallet.address as Hex,
- sign: async (payload: RequestData | ResponsePayload): Promise => {
- try {
- // The NitroliteRPC.hashMessage method should ideally be used here
- // to ensure the exact same hashing logic as the SDK internals.
- // For demonstration, using a generic hashing approach:
- const messageToSign = JSON.stringify(payload);
- const messageHash = ethers.utils.id(messageToSign); // ethers.utils.id performs keccak256
- const messageBytes = ethers.utils.arrayify(messageHash);
-
- const flatSignature = await wallet._signingKey().signDigest(messageBytes);
- const signature = ethers.utils.joinSignature(flatSignature);
- return signature as Hex;
- } catch (error) {
- console.error('Error signing message:', error);
- throw error;
- }
- },
- };
- } catch (error) {
- console.error('Error creating ethers signer:', error);
- throw error;
- }
-};
-
-// Usage:
-async function setupSigner() {
- const keyPair = await generateKeyPair(); // Or use a known private key
- const localSigner = createEthersSigner(keyPair.privateKey);
-
- // Now 'localSigner' can be passed to the RPC message creation functions:
- // const authRequest = await createAuthRequestMessage(localSigner.sign, localSigner.address as Address);
- // console.log("Auth Request with local signer:", authRequest);
-}
-```
-
-**Important Considerations for `createEthersSigner`:**
-* **Hashing Consistency:** The `sign` method within `createEthersSigner` must hash the payload in a way that is **identical** to how the `NitroliteRPC` class (specifically `NitroliteRPC.hashMessage`) expects messages to be hashed before signing. The example above uses `ethers.utils.id(JSON.stringify(payload))`. It's crucial to verify if the SDK's internal hashing uses a specific message prefix (e.g., EIP-191 personal_sign prefix) or a different serialization method. If the SDK does *not* use a standard EIP-191 prefix, or uses a custom one, your local signer's hashing logic must replicate this exactly for signatures to be valid. Using `NitroliteRPC.hashMessage(payload)` directly (if `payload` matches the `NitroliteRPCMessage` structure) is the safest way to ensure consistency.
-* **Type Compatibility:** Ensure the `Address` type expected by functions like `createAuthRequestMessage` is compatible with `localSigner.address`. The example uses `localSigner.address as Address` assuming `Address` is `0x${string}`.
-* **Error Handling:** The provided examples include basic error logging. Robust applications should implement more sophisticated error handling.
diff --git a/erc7824-docs/docs/nitrolite_rpc/rpc_types.md b/erc7824-docs/docs/nitrolite_rpc/rpc_types.md
deleted file mode 100644
index 2848c3156..000000000
--- a/erc7824-docs/docs/nitrolite_rpc/rpc_types.md
+++ /dev/null
@@ -1,296 +0,0 @@
----
-sidebar_position: 4 # Adjusted sidebar_position if message_creation_api is 3
-title: RPC Type Definitions
-description: Comprehensive type definitions for the Nitrolite RPC protocol.
-keywords: [erc7824, statechannels, state channels, nitrolite, rpc, types, typescript, protocol, api, definitions]
----
-
-# Nitrolite RPC Type Definitions
-
-This page provides a comprehensive reference for all TypeScript types, interfaces, and enums used by the Nitrolite RPC system, as defined in the `@erc7824/nitrolite` SDK. These definitions are crucial for understanding the structure of messages exchanged with the Nitrolite broker.
-
-## Core Types
-
-These are fundamental types used throughout the RPC system.
-
-### `RequestID`
-A unique identifier for an RPC request. Typically a number.
-```typescript
-export type RequestID = number;
-```
-
-### `Timestamp`
-Represents a Unix timestamp in milliseconds. Used for message ordering and security.
-```typescript
-export type Timestamp = number;
-```
-
-### `AccountID`
-A unique identifier for a channel or application session, represented as a hexadecimal string.
-```typescript
-export type AccountID = Hex; // from 'viem'
-```
-
-### `Intent`
-Represents the allocation intent change as an array of big integers. This is used to specify how funds should be re-distributed in a state update.
-```typescript
-export type Intent = bigint[];
-```
-
-## Message Payloads
-
-These types define the core data arrays within RPC messages.
-
-### `RequestData`
-The structured data payload within a request message.
-```typescript
-export type RequestData = [RequestID, string, any[], Timestamp?];
-```
-- `RequestID`: The unique ID of this request.
-- `string`: The name of the RPC method being called.
-- `any[]`: An array of parameters for the method.
-- `Timestamp?`: An optional timestamp for when the request was created.
-
-### `ResponseData`
-The structured data payload within a successful response message.
-```typescript
-export type ResponseData = [RequestID, string, any[], Timestamp?];
-```
-- `RequestID`: The ID of the original request this response is for.
-- `string`: The name of the original RPC method.
-- `any[]`: An array containing the result(s) of the method execution.
-- `Timestamp?`: An optional timestamp for when the response was created.
-
-### `NitroliteRPCErrorDetail`
-Defines the structure of the error object within an error response.
-```typescript
-export interface NitroliteRPCErrorDetail {
- error: string;
-}
-```
-- `error`: A string describing the error that occurred.
-
-### `ErrorResponseData`
-The structured data payload for an error response message.
-```typescript
-export type ErrorResponseData = [RequestID, "error", [NitroliteRPCErrorDetail], Timestamp?];
-```
-- `RequestID`: The ID of the original request this error is for.
-- `"error"`: A literal string indicating this is an error response.
-- `[NitroliteRPCErrorDetail]`: An array containing a single `NitroliteRPCErrorDetail` object.
-- `Timestamp?`: An optional timestamp for when the error response was created.
-
-### `ResponsePayload`
-A union type representing the payload of a response, which can be either a success (`ResponseData`) or an error (`ErrorResponseData`).
-```typescript
-export type ResponsePayload = ResponseData | ErrorResponseData;
-```
-
-## Message Envelopes
-
-These interfaces define the overall structure of messages sent over the wire.
-
-### `NitroliteRPCMessage`
-The base wire format for Nitrolite RPC messages.
-```typescript
-export interface NitroliteRPCMessage {
- req?: RequestData;
- res?: ResponsePayload;
- int?: Intent;
- sig?: Hex[];
-}
-```
-- `req?`: The request payload, if this is a request message.
-- `res?`: The response payload, if this is a response message.
-- `int?`: Optional allocation intent change.
-- `sig?`: Optional array of cryptographic signatures (hex strings).
-
-## Parsing Results
-
-### `ParsedResponse`
-Represents the result of parsing an incoming Nitrolite RPC response message.
-```typescript
-export interface ParsedResponse {
- isValid: boolean;
- error?: string;
- isError?: boolean;
- requestId?: RequestID;
- method?: string;
- data?: any[] | NitroliteRPCErrorDetail;
- acc?: AccountID;
- int?: Intent;
- timestamp?: Timestamp;
-}
-```
-- `isValid`: `true` if the message was successfully parsed and passed basic structural validation.
-- `error?`: If `isValid` is `false`, contains a description of the parsing or validation error.
-- `isError?`: `true` if the parsed response represents an error (i.e., `method === "error"`). Undefined if `isValid` is `false`.
-- `requestId?`: The `RequestID` from the response payload. Undefined if the structure is invalid.
-- `method?`: The method name from the response payload. Undefined if the structure is invalid.
-- `data?`: The extracted data payload (result array for success, `NitroliteRPCErrorDetail` object for error). Undefined if the structure is invalid or the error payload is malformed.
-- `acc?`: The `AccountID` from the message envelope, if present.
-- `int?`: The `Intent` from the message envelope, if present.
-- `timestamp?`: The `Timestamp` from the response payload. Undefined if the structure is invalid.
-
-## Request Parameter Structures
-
-These interfaces define the expected parameters for specific RPC methods.
-
-### `AppDefinition`
-Defines the structure of an application's configuration.
-```typescript
-export interface AppDefinition {
- protocol: string;
- participants: Hex[];
- weights: number[];
- quorum: number;
- challenge: number;
- nonce?: number;
-}
-```
-- `protocol`: The protocol identifier or name for the application logic (e.g., `"NitroRPC/0.2"`).
-- `participants`: An array of participant addresses (Ethereum addresses as `Hex`) involved in the application.
-- `weights`: An array representing the relative weights or stakes of participants. Order corresponds to the `participants` array.
-- `quorum`: The number/percentage of participants (based on weights) required to reach consensus.
-- `challenge`: A parameter related to the challenge period or mechanism (e.g., duration in seconds).
-- `nonce?`: An optional unique number (nonce) used to ensure the uniqueness of the application instance and prevent replay attacks.
-
-### `CreateAppSessionRequest`
-Parameters for the `create_app_session` RPC method.
-```typescript
-export interface CreateAppSessionRequest {
- definition: AppDefinition;
- token: Hex;
- allocations: bigint[];
-}
-```
-- `definition`: The `AppDefinition` object detailing the application being created.
-- `token`: The `Hex` address of the ERC20 token contract used for allocations within this application session.
-- `allocations`: An array of `bigint` representing the initial allocation distribution among participants. The order corresponds to the `participants` array in the `definition`.
-
-Example:
-```json
-{
- "definition": {
- "protocol": "NitroRPC/0.2",
- "participants": [
- "0xAaBbCcDdEeFf0011223344556677889900aAbBcC",
- "0x00112233445566778899AaBbCcDdEeFf00112233"
- ],
- "weights": [100, 0], // Example: Participant 1 has 100% weight
- "quorum": 100, // Example: 100% quorum needed
- "challenge": 86400, // Example: 1 day challenge period
- "nonce": 12345
- },
- "token": "0xTokenContractAddress00000000000000000000",
- "allocations": ["1000000000000000000", "0"] // 1 Token for P1, 0 for P2 (as strings for bigint)
-}
-```
-
-### `CloseAppSessionRequest`
-Parameters for the `close_app_session` RPC method.
-```typescript
-export interface CloseAppSessionRequest {
- app_id: Hex;
- allocations: bigint[];
-}
-```
-- `app_id`: The unique `AccountID` (as `Hex`) of the application session to be closed.
-- `allocations`: An array of `bigint` representing the final allocation distribution among participants upon closing. Order corresponds to the `participants` array in the application's definition.
-
-### `ResizeChannel`
-Parameters for the `resize_channel` RPC method.
-```typescript
-export interface ResizeChannel {
- channel_id: Hex;
- participant_change: bigint;
- funds_destination: Hex;
-}
-```
-- `channel_id`: The unique `AccountID` (as `Hex`) of the direct ledger channel to be resized.
-- `participant_change`: The `bigint` amount by which the participant's allocation in the channel should change (positive to add funds, negative to remove).
-- `funds_destination`: The `Hex` address where funds will be sent if `participant_change` is negative (withdrawal), or the source of funds if positive (though typically handled by prior on-chain deposit).
-
-## Function Types (Signers & Verifiers)
-
-These types define the signatures for functions used in cryptographic operations.
-
-### `MessageSigner`
-A function that signs a message payload.
-```typescript
-export type MessageSigner = (payload: RequestData | ResponsePayload) => Promise;
-```
-- Takes a `RequestData` or `ResponsePayload` object (the array part of the message).
-- Returns a `Promise` that resolves to the cryptographic signature as a `Hex` string.
-
-### `SingleMessageVerifier`
-A function that verifies a single message signature.
-```typescript
-export type SingleMessageVerifier = (
- payload: RequestData | ResponsePayload,
- signature: Hex,
- address: Address // from 'viem'
-) => Promise;
-```
-- Takes the `RequestData` or `ResponsePayload` object, the `Hex` signature, and the expected signer's `Address`.
-- Returns a `Promise` that resolves to `true` if the signature is valid for the given payload and address, `false` otherwise.
-
-## Usage Examples
-
-### Creating Message Payloads and Envelopes
-```typescript
-// Example Request Payload (for a 'ping' method)
-const pingRequestData: RequestData = [1, "ping", []]; // Assuming timestamp is added by sender utility
-
-// Example Request Envelope
-const pingRequestMessage: NitroliteRPCMessage = {
- req: pingRequestData,
- // sig: ["0xSignatureIfPreSigned..."] // Signature added by signing utility
-};
-
-// Example Application-Specific Request
-const appActionData: RequestData = [2, "message", [{ move: "rock" }], Date.now()];
-const appActionMessage: ApplicationRPCMessage = {
- sid: "0xAppSessionId...",
- req: appActionData,
- // sig: ["0xSignature..."]
-};
-
-// Example Successful Response Payload
-const pongResponseData: ResponseData = [1, "ping", ["pong"], Date.now()];
-
-// Example Error Detail
-const errorDetail: NitroliteRPCErrorDetail = { error: "Method parameters are invalid." };
-
-// Example Error Response Payload
-const errorResponseData: ErrorResponseData = [2, "error", [errorDetail], Date.now()];
-
-// Example Response Envelope (Success)
-const successResponseEnvelope: NitroliteRPCMessage = {
- res: pongResponseData,
-};
-```
-
-### Working with Signers (Conceptual)
-```typescript
-// Conceptual: How a MessageSigner might be used
-async function signAndSend(payload: RequestData, signer: MessageSigner, sendMessageToServer: (msg: string) => void) {
- const signature = await signer(payload);
- const message: NitroliteRPCMessage = {
- req: payload,
- sig: [signature]
- };
- sendMessageToServer(JSON.stringify(message));
-}
-```
-
-## Implementation Considerations
-
-When working with these types:
-
-1. **Serialization**: Messages are typically serialized to JSON strings for transmission (e.g., over WebSockets).
-2. **Signing**: Payloads (`req` or `res` arrays) are what get signed, not the entire envelope. The resulting signature is then added to the `sig` field of the `NitroliteRPCMessage` envelope.
-3. **Validation**: Always validate the structure and types of incoming messages against these definitions, preferably using utilities provided by the SDK.
-4. **Error Handling**: Properly check for `isError` in `ParsedResponse` and use `NitroliteErrorCode` to understand the nature of failures.
-5. **BigInts**: Note the use of `bigint` for `Intent` and allocation amounts. Ensure your environment and serialization/deserialization logic handle `bigint` correctly (e.g., converting to/from strings for JSON).
-6. **Hex Strings**: Types like `AccountID`, `Hex` (for signatures, token addresses) imply hexadecimal string format (e.g., `"0x..."`).
diff --git a/erc7824-docs/docs/quick_start/application_session.md b/erc7824-docs/docs/quick_start/application_session.md
deleted file mode 100644
index 08f1e1e0c..000000000
--- a/erc7824-docs/docs/quick_start/application_session.md
+++ /dev/null
@@ -1,1071 +0,0 @@
----
-sidebar_position: 7
-title: Create Application Sessions
-description: Create and manage off-chain application sessions to interact with ClearNodes.
-keywords: [erc7824, nitrolite, application session, state channels, app session]
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-# Create Application Sessions
-
-After connecting to a ClearNode and checking your channel balances, you can create application sessions to interact with specific applications on the state channel network. Application sessions allow you to perform off-chain transactions and define custom behavior for your interactions.
-
-## Understanding Application Sessions
-
-```mermaid
-graph TD
- A[Create Application Session] --> B[Session Active]
- B --> C[Off-Chain Transactions]
- C --> D[Close Session]
-```
-
-Application sessions in Nitrolite allow you to:
-
-- Create isolated environments for specific interactions
-- Define rules for off-chain transactions
-- Specify how funds are allocated between participants
-- Implement custom application logic and state management
-
-An application session serves as a mechanism to track and manage interactions between participants, with the ClearNode acting as a facilitator.
-
-## Creating an Application Session
-
-To create an application session, you'll use the `createAppSessionMessage` helper from NitroliteRPC. Here's how to do it:
-
-
-
-
-```javascript
-import { createAppSessionMessage, parseRPCResponse, MessageSigner, CreateAppSessionRPCParams } from '@erc7824/nitrolite';
-import { useCallback } from 'react';
-import { Address } from 'viem';
-
-function useCreateApplicationSession() {
- const createApplicationSession = useCallback(
- async (
- signer: MessageSigner,
- sendRequest: (message: string) => Promise,
- participantA: Address,
- participantB: Address,
- amount: string,
- ) => {
- try {
- // Define the application parameters
- const appDefinition = {
- protocol: 'nitroliterpc',
- participants: [participantA, participantB],
- weights: [100, 0], // Weight distribution for consensus
- quorum: 100, // Required consensus percentage
- challenge: 0, // Challenge period
- nonce: Date.now(), // Unique identifier
- };
-
- // Define allocations with asset type instead of token address
- const allocations = [
- {
- participant: participantA,
- asset: 'usdc',
- amount: amount,
- },
- {
- participant: participantB,
- asset: 'usdc',
- amount: '0',
- },
- ];
-
- // Create a signed message using the createAppSessionMessage helper
- const signedMessage = await createAppSessionMessage(
- signer,
- [
- {
- definition: appDefinition,
- allocations: allocations,
- },
- ]
- );
-
- // Send the signed message to the ClearNode
- const response = await sendRequest(signedMessage);
-
- // Handle the response
- if (response.app_session_id) {
- // Store the app session ID for future reference
- localStorage.setItem('app_session_id', response.app_session_id);
- return { success: true, app_session_id: response.app_session_id, response };
- } else {
- return { success: true, response };
- }
- } catch (error) {
- console.error('Error creating application session:', error);
- return {
- success: false,
- error: error instanceof Error
- ? error.message
- : 'Unknown error during session creation',
- };
- }
- },
- []
- );
-
- return { createApplicationSession };
-}
-
-// Usage example
-function MyComponent() {
- const { createApplicationSession } = useCreateApplicationSession();
-
- const handleCreateSession = async () => {
- // Define your WebSocket send wrapper
- const sendRequest = async (payload: string) => {
- return new Promise((resolve, reject) => {
- // Assuming ws is your WebSocket connection
- const handleMessage = (event) => {
- try {
- const message = parseRPCResponse(event.data);
- if (message.method === RPCMethod.CreateAppSession) {
- ws.removeEventListener('message', handleMessage);
- resolve(message.params);
- }
- } catch (error) {
- console.error('Error parsing message:', error);
- }
- };
-
- ws.addEventListener('message', handleMessage);
- ws.send(payload);
-
- // Set timeout to prevent hanging
- setTimeout(() => {
- ws.removeEventListener('message', handleMessage);
- reject(new Error('App session creation timeout'));
- }, 10000);
- });
- };
-
- const result = await createApplicationSession(
- walletSigner, // Your signer object
- sendRequest, // Function to send the request
- '0xYourAddress', // Your address
- '0xOtherAddress', // Other participant's address
- '100', // Amount
- );
-
- if (result.success) {
- console.log(`Application session created with ID: ${result.app_session_id}`);
- } else {
- console.error(`Failed to create application session: ${result.error}`);
- }
- };
-
- return (
-
- );
-}
-```
-
-
-
-
-```typescript
-// app-session.service.ts
-import { Injectable } from '@angular/core';
-import { createAppSessionMessage } from '@erc7824/nitrolite';
-import { ethers } from 'ethers';
-import { BehaviorSubject, Observable, from } from 'rxjs';
-import { tap, catchError } from 'rxjs/operators';
-
-@Injectable({
- providedIn: 'root'
-})
-export class AppSessionService {
- private webSocket: WebSocket | null = null;
- private appIdSubject = new BehaviorSubject(null);
-
- public appId$ = this.appIdSubject.asObservable();
-
- constructor() {
- // Retrieve app ID from storage if available
- const storedAppId = localStorage.getItem('app_session_id');
- if (storedAppId) {
- this.appIdSubject.next(storedAppId);
- }
- }
-
- public setWebSocket(ws: WebSocket): void {
- this.webSocket = ws;
- }
-
- public createApplicationSession(
- signer: any,
- participantA: string,
- participantB: string,
- amount: string,
- ): Observable {
- if (!this.webSocket) {
- throw new Error('WebSocket connection is not established');
- }
-
- return from(this.createAppSessionAsync(
- signer,
- participantA,
- participantB,
- amount,
- )).pipe(
- tap(result => {
- if (result.success && result.app_session_id) {
- localStorage.setItem('app_session_id', result.app_session_id);
- this.appIdSubject.next(result.app_session_id);
- }
- }),
- catchError(error => {
- console.error('Error creating application session:', error);
- throw error;
- })
- );
- }
-
- private async createAppSessionAsync(
- signer: any,
- participantA: string,
- participantB: string,
- amount: string,
- ): Promise {
- try {
-
- // Define the application parameters
- const appDefinition = {
- protocol: 'nitroliterpc',
- participants: [participantA, participantB],
- weights: [100, 0],
- quorum: 100,
- challenge: 0,
- nonce: Date.now(),
- };
-
- // Define the allocations with asset type
- const allocations = [
- {
- participant: participantA,
- asset: 'usdc',
- amount: amount,
- },
- {
- participant: participantB,
- asset: 'usdc',
- amount: '0',
- },
- ];
-
- // Create message signer function
- const messageSigner = async (payload: any) => {
- const message = JSON.stringify(payload);
- const digestHex = ethers.id(message);
- const messageBytes = ethers.getBytes(digestHex);
- const signature = await signer.signMessage(messageBytes);
- return signature;
- };
-
- // Create the signed message
- const signedMessage = await createAppSessionMessage(
- messageSigner,
- [
- {
- definition: appDefinition,
- allocations: allocations,
- },
- ]
- );
-
- // Send the message and wait for response
- return await this.sendRequest(signedMessage);
- } catch (error) {
- console.error('Error in createAppSessionAsync:', error);
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error'
- };
- }
- }
-
- private sendRequest(payload: string): Promise {
- return new Promise((resolve, reject) => {
- if (!this.webSocket) {
- reject(new Error('WebSocket not connected'));
- return;
- }
-
- const handleMessage = (event: MessageEvent) => {
- try {
- const message = JSON.parse(event.data);
- if (message.res && message.res[1] === 'create_app_session') {
- this.webSocket?.removeEventListener('message', handleMessage);
- resolve({
- success: true,
- app_session_id: message.res[2]?.[0]?.app_session_id || null,
- status: message.res[2]?.[0]?.status || "open",
- response: message.res[2]
- });
- }
-
- if (message.err) {
- this.webSocket?.removeEventListener('message', handleMessage);
- reject(new Error(`Error: ${message.err[1]} - ${message.err[2]}`));
- }
- } catch (error) {
- console.error('Error parsing message:', error);
- }
- };
-
- this.webSocket.addEventListener('message', handleMessage);
- this.webSocket.send(payload);
-
- // Set timeout to prevent hanging
- setTimeout(() => {
- this.webSocket?.removeEventListener('message', handleMessage);
- reject(new Error('App session creation timeout'));
- }, 10000);
- });
- }
-}
-
-// app-session.component.ts
-import { Component, OnInit } from '@angular/core';
-import { AppSessionService } from './app-session.service';
-
-@Component({
- selector: 'app-session-creator',
- template: `
-