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/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/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 7dfca72f5..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 = 2_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 = 2_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/src/ChannelEngine.sol b/contracts/src/ChannelEngine.sol index 7abef8b70..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"; @@ -54,13 +54,14 @@ 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; @@ -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); diff --git a/contracts/src/ChannelHub.sol b/contracts/src/ChannelHub.sol index bdade7b6d..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,7 @@ contract ChannelHub is IVault, ReentrancyGuard { error IncorrectStateIntent(); error IncorrectChannelStatus(); error ChallengerVersionTooLow(); - error NoChannelIdFound(); + error NoChannelIdFoundForEscrow(); error IncorrectChannelId(); struct ChannelMeta { @@ -103,6 +114,7 @@ contract ChannelHub is IVault, ReentrancyGuard { State lastState; uint256 lockedFunds; uint64 challengeExpireAt; + uint48 subId; } struct EscrowDepositMeta { @@ -159,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) { @@ -192,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(); @@ -212,7 +233,9 @@ contract ChannelHub is IVault, ReentrancyGuard { } } - function getChannelData(bytes32 channelId) + function getChannelData( + bytes32 channelId + ) external view returns ( @@ -231,7 +254,9 @@ contract ChannelHub is IVault, ReentrancyGuard { lockedFunds = meta.lockedFunds; } - function getEscrowDepositData(bytes32 escrowId) + function getEscrowDepositData( + bytes32 escrowId + ) external view returns ( @@ -252,7 +277,9 @@ contract ChannelHub is IVault, ReentrancyGuard { initState = meta.initState; } - function getEscrowWithdrawalData(bytes32 escrowId) + function getEscrowWithdrawalData( + bytes32 escrowId + ) external view returns ( @@ -275,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); } /** @@ -308,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 ========== @@ -387,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; } @@ -450,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); @@ -479,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); @@ -500,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); @@ -516,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); @@ -549,16 +655,20 @@ contract ChannelHub is IVault, ReentrancyGuard { if (candidate.version > prevState.version) { _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; @@ -578,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); @@ -596,8 +706,9 @@ contract ChannelHub is IVault, ReentrancyGuard { 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); @@ -622,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); @@ -634,22 +753,41 @@ 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]; bytes32 channelId = meta.channelId; - require(channelId != bytes32(0), NoChannelIdFound()); + 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); + (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); @@ -660,7 +798,10 @@ contract ChannelHub is IVault, ReentrancyGuard { // HOME CHAIN: Get user/node from channel definition ChannelMeta storage channelMeta = _channels[channelId]; _processHomeChainEscrowFinalize( - channelId, candidate, channelMeta.definition.user, channelMeta.definition.node + channelId, + candidate, + channelMeta.definition.user, + channelMeta.definition.node ); emit EscrowDepositFinalizedOnHome(escrowId, channelId, candidate); return; @@ -673,14 +814,15 @@ contract ChannelHub is IVault, ReentrancyGuard { address node = meta.node; EscrowStatus status = meta.status; - if (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, channelId, candidate); return; @@ -691,12 +833,20 @@ contract ChannelHub is IVault, ReentrancyGuard { // 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.TransitionContext memory ctx = _buildEscrowDepositContext( + escrowId, + _nodeBalances[node][candidate.nonHomeLedger.token] + ); EscrowDepositEngine.TransitionEffects memory effects = EscrowDepositEngine.validateTransition(ctx, candidate); _applyEscrowDepositEffects( - escrowId, channelId, candidate, effects, user, node, meta.approvedSignatureValidators + escrowId, + channelId, + candidate, + effects, + user, + node, + meta.approvedSignatureValidators ); emit EscrowDepositFinalized(escrowId, channelId, candidate); @@ -717,23 +867,33 @@ contract ChannelHub is IVault, ReentrancyGuard { } 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]; bytes32 channelId = meta.channelId; - require(channelId != bytes32(0), NoChannelIdFound()); + require(channelId != bytes32(0), NoChannelIdFoundForEscrow()); EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, meta.node); EscrowWithdrawalEngine.TransitionEffects memory effects = EscrowWithdrawalEngine.validateChallenge(ctx); @@ -741,12 +901,21 @@ contract ChannelHub is IVault, ReentrancyGuard { // Validate challenger signature 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); @@ -757,7 +926,10 @@ contract ChannelHub is IVault, ReentrancyGuard { // HOME CHAIN: Get user/node from channel definition ChannelMeta storage channelMeta = _channels[channelId]; _processHomeChainEscrowFinalize( - channelId, candidate, channelMeta.definition.user, channelMeta.definition.node + channelId, + candidate, + channelMeta.definition.user, + channelMeta.definition.node ); emit EscrowWithdrawalFinalizedOnHome(escrowId, channelId, candidate); return; @@ -770,15 +942,19 @@ contract ChannelHub is IVault, ReentrancyGuard { address node = meta.node; EscrowStatus status = meta.status; - if (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 NodeBalanceUpdated(node, withdrawalToken, 0, updatedWithdrawalBalance); emit EscrowWithdrawalFinalized(escrowId, channelId, candidate); return; } @@ -789,11 +965,19 @@ contract ChannelHub is IVault, ReentrancyGuard { _validateSignatures(channelId, candidate, user, node, meta.approvedSignatureValidators); EscrowWithdrawalEngine.TransitionContext memory ctx = _buildEscrowWithdrawalContext(escrowId, node); - EscrowWithdrawalEngine.TransitionEffects memory effects = - EscrowWithdrawalEngine.validateTransition(ctx, candidate); + EscrowWithdrawalEngine.TransitionEffects memory effects = EscrowWithdrawalEngine.validateTransition( + ctx, + candidate + ); _applyEscrowWithdrawalEffects( - escrowId, channelId, candidate, effects, user, node, meta.approvedSignatureValidators + escrowId, + channelId, + candidate, + effects, + user, + node, + meta.approvedSignatureValidators ); emit EscrowWithdrawalFinalized(escrowId, channelId, candidate); @@ -821,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); @@ -857,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); @@ -878,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); } @@ -906,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]); @@ -965,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; @@ -1000,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; @@ -1020,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; @@ -1080,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; } @@ -1146,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 @@ -1198,23 +1422,27 @@ 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 @@ -1269,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)) { @@ -1279,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/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/test/ChannelHub_Base.t.sol b/contracts/test/ChannelHub_Base.t.sol index a01bd3847..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 {ChannelStatus, 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,6 +185,14 @@ contract ChannelHubTest_Base is Test { return state; } + 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, ChannelStatus expectedStatus, @@ -204,31 +200,21 @@ contract ChannelHubTest_Base is Test { uint256 expectedChallengeExpiry, string memory description ) internal view { - (ChannelStatus status,, State memory latestState, uint256 challengeExpiry,) = cHub.getChannelData(channelId); + (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: ") - ); + 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.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)); - uint256 expectedNodeBalance = - netFlows[1] < 0 ? INITIAL_BALANCE + uint256(-netFlows[1]) : INITIAL_BALANCE - uint256(netFlows[1]); + 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_Base.t.sol b/contracts/test/ChannelHub_challenge/ChannelHub_Challenge_Base.t.sol similarity index 68% rename from contracts/test/ChannelHub_Challenge_Base.t.sol rename to contracts/test/ChannelHub_challenge/ChannelHub_Challenge_Base.t.sol index 92bccdf39..037a9d3ca 100644 --- a/contracts/test/ChannelHub_Challenge_Base.t.sol +++ b/contracts/test/ChannelHub_challenge/ChannelHub_Challenge_Base.t.sol @@ -1,11 +1,10 @@ // 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, DEFAULT_SIG_VALIDATOR_ID} from "../src/interfaces/Types.sol"; -import {TestUtils} from "./TestUtils.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. @@ -64,16 +63,4 @@ abstract contract ChannelHubTest_Challenge_Base is ChannelHubTest_Base { vm.prank(alice); cHub.createChannel(def, initState); } - - 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); - } } diff --git a/contracts/test/ChannelHub_challengeHomeChain.t.sol b/contracts/test/ChannelHub_challenge/ChannelHub_challengeHomeChain.t.sol similarity index 61% rename from contracts/test/ChannelHub_challengeHomeChain.t.sol rename to contracts/test/ChannelHub_challenge/ChannelHub_challengeHomeChain.t.sol index 6ce1b31ac..7e999f05d 100644 --- a/contracts/test/ChannelHub_challengeHomeChain.t.sol +++ b/contracts/test/ChannelHub_challenge/ChannelHub_challengeHomeChain.t.sol @@ -1,20 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.30; -import {ChannelHubTest_Challenge_Base} from "./ChannelHub_Challenge_Base.t.sol"; - -import {Utils} from "../src/Utils.sol"; -import { - State, - ChannelDefinition, - StateIntent, - Ledger, - ChannelStatus, - ParticipantIndex, - DEFAULT_SIG_VALIDATOR_ID -} from "../src/interfaces/Types.sol"; -import {ChannelHub} from "../src/ChannelHub.sol"; -import {ChannelEngine} from "../src/ChannelEngine.sol"; +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 @@ -33,6 +27,7 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch - 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 { @@ -42,13 +37,11 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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)]); + 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)]); + 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 @@ -57,32 +50,20 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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" - ); + 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)]); + 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); + (, , 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) @@ -91,24 +72,12 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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" - ); + 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)]); + 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 @@ -120,7 +89,7 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch vm.warp(block.timestamp + CHALLENGE_DURATION + 1); uint256 aliceBalanceBefore = token.balanceOf(alice); - uint256 nodeBalanceBefore = cHub.getAccountBalance(node, address(token)); + 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 @@ -128,28 +97,21 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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" - ); + 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)); + 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)" - ); + 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)]); + State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); // Challenge with stateV1 @@ -158,30 +120,17 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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" - ); + 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)]); + 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" - ); + verifyChannelData(channelId, ChannelStatus.OPERATING, 2, 0, "Channel should be OPERATING after resolving challenge with newer state"); verifyChannelState( channelId, [uint256(850), uint256(0)], @@ -192,13 +141,11 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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)]); + 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)]); + State memory stateV2 = nextState(stateV1, StateIntent.OPERATE, [uint256(950), uint256(0)], [int256(1000), int256(-50)]); stateV2 = mutualSignStateBothWithEcdsaValidator(stateV2, channelId, ALICE_PK); // Challenge with stateV2 @@ -207,13 +154,7 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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" - ); + 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); @@ -223,8 +164,7 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch function test_revert_resolveChallenge_withNewerState_afterTimeout() public { // State V1 - State memory stateV1 = - nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); // Challenge @@ -236,8 +176,7 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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)]); + 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 @@ -254,17 +193,10 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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" - ); + 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)]); + 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); @@ -276,8 +208,7 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch function test_revert_challengeWithOlderState() public { // State V1 - State memory stateV1 = - nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); + State memory stateV1 = nextState(initState, StateIntent.OPERATE, [uint256(900), uint256(0)], [int256(1000), int256(-100)]); stateV1 = mutualSignStateBothWithEcdsaValidator(stateV1, channelId, ALICE_PK); // Checkpoint V1 @@ -291,6 +222,28 @@ contract ChannelHubTest_Challenge_HomeChain_NormalOperation is ChannelHubTest_Ch 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 { @@ -324,8 +277,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal [uint256(500), uint256(0)], [int256(500), int256(0)] ); - initiateEscrowDepositState = - mutualSignStateBothWithEcdsaValidator(initiateEscrowDepositState, channelId, ALICE_PK); + initiateEscrowDepositState = mutualSignStateBothWithEcdsaValidator(initiateEscrowDepositState, channelId, ALICE_PK); escrowId = Utils.getEscrowId(channelId, initiateEscrowDepositVersion); @@ -339,13 +291,11 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal [uint256(0), uint256(0)], [int256(500), int256(-500)] ); - finalizeEscrowDepositState = - mutualSignStateBothWithEcdsaValidator(finalizeEscrowDepositState, channelId, ALICE_PK); + finalizeEscrowDepositState = mutualSignStateBothWithEcdsaValidator(finalizeEscrowDepositState, channelId, ALICE_PK); } function test_challenge_initiateEscrowDeposit_asNew() public { - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); @@ -358,12 +308,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal block.timestamp + CHALLENGE_DURATION, "InitiateEscrowDepositState should be enforced" ); - verifyChannelState( - channelId, - [uint256(1000), uint256(500)], - [int256(1000), int256(500)], - "InitiateEscrowDepositState should be enforced" - ); + verifyChannelState(channelId, [uint256(1000), uint256(500)], [int256(1000), int256(500)], "InitiateEscrowDepositState should be enforced"); } function test_challenge_initiateEscrowDeposit_asExisting() public { @@ -371,8 +316,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); // Challenge with already enforced initiateEscrowDepositState state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); @@ -385,9 +329,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal 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" - ); + verifyChannelState(channelId, [uint256(1000), uint256(500)], [int256(1000), int256(500)], "State should not be re-enforced"); } function test_challenge_initiateEscrowDeposit_resolve() public { @@ -401,15 +343,8 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal 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" - ); + 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 { @@ -418,8 +353,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); // Now challenge with FINALIZE_ESCROW_DEPOSIT - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, finalizeEscrowDepositState, challengerSig, ParticipantIndex.NODE); @@ -432,12 +366,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal block.timestamp + CHALLENGE_DURATION, "FinalizeEscrowDepositState should be enforced" ); - verifyChannelState( - channelId, - [uint256(1500), uint256(0)], - [int256(1000), int256(500)], - "finalizeEscrowDepositState should be enforced" - ); + verifyChannelState(channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "finalizeEscrowDepositState should be enforced"); } function test_challenge_finalizeEscrowDeposit_asExisting() public { @@ -450,8 +379,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal cHub.finalizeEscrowDeposit(channelId, escrowId, finalizeEscrowDepositState); // Challenge with already enforced finalizeEscrowDepositState state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowDepositState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, finalizeEscrowDepositState, challengerSig, ParticipantIndex.NODE); @@ -464,9 +392,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal 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" - ); + verifyChannelState(channelId, [uint256(1500), uint256(0)], [int256(1000), int256(500)], "State should not be re-enforced"); } function test_challenge_finalizeEscrowDeposit_resolve() public { @@ -475,8 +401,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); // Challenge with older initiate state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); @@ -486,21 +411,13 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal 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" - ); + 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); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowDepositState, challengerSig, ParticipantIndex.NODE); @@ -510,15 +427,8 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal 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" - ); + 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 { @@ -527,11 +437,10 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowDeposit is ChannelHubTest_Chal cHub.initiateEscrowDeposit(def, initiateEscrowDepositState); // Challenge with INITIATE_ESCROW_DEPOSIT state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowDepositState, NODE_PK); vm.prank(node); - vm.expectRevert(ChannelHub.NoChannelIdFound.selector); + vm.expectRevert(ChannelHub.NoChannelIdFoundForEscrow.selector); cHub.challengeEscrowDeposit(escrowId, challengerSig, ParticipantIndex.NODE); } } @@ -567,8 +476,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C [uint256(0), uint256(300)], [int256(0), int256(300)] ); - initiateEscrowWithdrawalState = - mutualSignStateBothWithEcdsaValidator(initiateEscrowWithdrawalState, channelId, ALICE_PK); + initiateEscrowWithdrawalState = mutualSignStateBothWithEcdsaValidator(initiateEscrowWithdrawalState, channelId, ALICE_PK); escrowId = Utils.getEscrowId(channelId, initiateEscrowWithdrawalVersion); @@ -582,13 +490,11 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C [uint256(0), uint256(0)], [int256(-300), int256(300)] ); - finalizeEscrowWithdrawalState = - mutualSignStateBothWithEcdsaValidator(finalizeEscrowWithdrawalState, channelId, ALICE_PK); + finalizeEscrowWithdrawalState = mutualSignStateBothWithEcdsaValidator(finalizeEscrowWithdrawalState, channelId, ALICE_PK); } function test_challenge_initiateEscrowWithdrawal_asNew() public { - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); @@ -601,12 +507,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C block.timestamp + CHALLENGE_DURATION, "InitiateEscrowWithdrawalState should be enforced" ); - verifyChannelState( - channelId, - [uint256(1000), uint256(0)], - [int256(1000), int256(0)], - "InitiateEscrowWithdrawalState should be enforced" - ); + verifyChannelState(channelId, [uint256(1000), uint256(0)], [int256(1000), int256(0)], "InitiateEscrowWithdrawalState should be enforced"); } function test_challenge_initiateEscrowWithdrawal_asExisting() public { @@ -614,8 +515,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState); // Challenge with already enforced initiateEscrowWithdrawalState state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); @@ -628,9 +528,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C 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" - ); + verifyChannelState(channelId, [uint256(1000), uint256(0)], [int256(1000), int256(0)], "State should not be re-enforced"); } function test_challenge_initiateEscrowWithdrawal_resolve() public { @@ -644,23 +542,15 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C 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" - ); + 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); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, finalizeEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); @@ -673,12 +563,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C block.timestamp + CHALLENGE_DURATION, "FinalizeEscrowWithdrawalState should be enforced" ); - verifyChannelState( - channelId, - [uint256(700), uint256(0)], - [int256(1000), int256(-300)], - "finalizeEscrowWithdrawalState should be enforced" - ); + verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "finalizeEscrowWithdrawalState should be enforced"); } function test_challenge_finalizeEscrowWithdrawal_asExisting() public { @@ -689,8 +574,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C cHub.finalizeEscrowWithdrawal(channelId, escrowId, finalizeEscrowWithdrawalState); // Challenge with already enforced finalizeEscrowWithdrawalState state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, finalizeEscrowWithdrawalState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, finalizeEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); @@ -703,9 +587,7 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C 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" - ); + verifyChannelState(channelId, [uint256(700), uint256(0)], [int256(1000), int256(-300)], "State should not be re-enforced"); } function test_challenge_finalizeEscrowWithdrawal_resolve() public { @@ -722,21 +604,13 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C 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" - ); + 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); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); vm.prank(node); cHub.challengeChannel(channelId, initiateEscrowWithdrawalState, challengerSig, ParticipantIndex.NODE); @@ -746,15 +620,8 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C 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" - ); + 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 { @@ -763,11 +630,10 @@ contract ChannelHubTest_Challenge_HomeChain_EscrowWithdrawal is ChannelHubTest_C cHub.initiateEscrowWithdrawal(def, initiateEscrowWithdrawalState); // Challenge with INITIATE_ESCROW_WITHDRAWAL state - bytes memory challengerSig = - signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); + bytes memory challengerSig = signChallengeEip191WithEcdsaValidator(channelId, initiateEscrowWithdrawalState, NODE_PK); vm.prank(node); - vm.expectRevert(ChannelHub.NoChannelIdFound.selector); + vm.expectRevert(ChannelHub.NoChannelIdFoundForEscrow.selector); cHub.challengeEscrowWithdrawal(escrowId, challengerSig, ParticipantIndex.NODE); } } @@ -779,10 +645,8 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal - 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 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 - a channel can NOT be challenged when in MIGRATED_OUT status - - a channel can NOT be challenged in Operating status with finalize migration state + - a channel can NOT be challenged in Operating status with finalize migration state (use `finalizeMigration` function instead) */ uint64 initiateMigrationVersion = 1; @@ -834,84 +698,8 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal 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); - - // Setup for NEW home chain tests (migration IN) - // Create a new channel definition with different nonce - newHomeDef = ChannelDefinition({ - challengeDuration: CHALLENGE_DURATION, - user: alice, - node: node, - nonce: uint64(42), // Different nonce to create a new channel - approvedSignatureValidators: DEFAULT_SIG_VALIDATOR_ID, - metadata: bytes32(0) - }); - newHomeChannelId = Utils.getChannelId(newHomeDef, cHub.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); + 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 { @@ -928,12 +716,7 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal block.timestamp + CHALLENGE_DURATION, "InitiateMigrationState should be enforced" ); - verifyChannelState( - channelId, - [uint256(700), uint256(0)], - [int256(1000), int256(-300)], - "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 { @@ -943,26 +726,14 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal vm.prank(node); cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE); - verifyChannelData( - channelId, - ChannelStatus.DISPUTED, - initiateMigrationVersion, - block.timestamp + CHALLENGE_DURATION, - "Channel should be DISPUTED" - ); + 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" - ); + verifyChannelData(channelId, ChannelStatus.MIGRATED_OUT, finalizeMigrationVersion, 0, "finalizeMigration should resolve the challenge"); } function test_challenge_initiateMigration_resolve_withOperate() public { @@ -972,13 +743,7 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal vm.prank(node); cHub.challengeChannel(channelId, initiateMigrationState, challengerSig, ParticipantIndex.NODE); - verifyChannelData( - channelId, - ChannelStatus.DISPUTED, - initiateMigrationVersion, - block.timestamp + CHALLENGE_DURATION, - "Channel should be DISPUTED" - ); + 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 @@ -986,83 +751,8 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal 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_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" - ); + 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 { @@ -1075,9 +765,7 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal cHub.finalizeMigration(channelId, finalizeMigrationState); // Verify channel is in MIGRATED_OUT status - verifyChannelData( - channelId, ChannelStatus.MIGRATED_OUT, finalizeMigrationVersion, 0, "Channel should be MIGRATED_OUT" - ); + 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); @@ -1099,3 +787,4 @@ contract ChannelHubTest_Challenge_HomeChain_HomeMigration is ChannelHubTest_Chal 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_challengeNonHomeChain.t.sol b/contracts/test/ChannelHub_challengeNonHomeChain.t.sol deleted file mode 100644 index 94c525641..000000000 --- a/contracts/test/ChannelHub_challengeNonHomeChain.t.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.30; - -import {ChannelHubTest_Base} from "./ChannelHub_Base.t.sol"; - -contract ChannelHubTest_Challenge_NonHomeChain_EscrowDeposit is ChannelHubTest_Base { - /* - - 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 funds can be withdrawn after `challengeExpireAt` time passes - - 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 - */ - - } - -contract ChannelHubTest_Challenge_NonHomeChain_EscrowWithdrawal is ChannelHubTest_Base { - /* - - escrow withdrawal can be challenged - - challenged escrow withdrawal funds can be withdrawn after `challengeExpireAt` time passes - - 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 - */ - - } - -contract ChannelHubTest_Challenge_NonHomeChain_Migration is ChannelHubTest_Base { - /* - - a channel in earlier state can be challenged with initiated migration state - - a channel in initiated migration state can be challenged with it - - a channel in earlier state can be challenged with finalize migration state - - a channel in finalize migration state can be challenged with it - */ - - } 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 98% rename from contracts/test/ChannelHub_crosschain.lifecycle.t.sol rename to contracts/test/ChannelHub_lifecycle/ChannelHub_crosschain.lifecycle.t.sol index d7f3f6502..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 { @@ -341,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( @@ -363,7 +370,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { // 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 @@ -454,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( @@ -477,7 +484,7 @@ contract ChannelHubTest_CrossChain_Lifecycle is ChannelHubTest_Base { 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 @@ -495,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({ @@ -535,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 @@ -589,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({ @@ -633,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 @@ -681,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 @@ -716,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 @@ -796,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 @@ -850,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 98% rename from contracts/test/ChannelHub_singlechain.lifecycle.t.sol rename to contracts/test/ChannelHub_lifecycle/ChannelHub_singlechain.lifecycle.t.sol index 6738c3fb4..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 { 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/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: + +![State Ledger Advancement](./state_ledger_advancement.png) + +### 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/go.mod b/go.mod index 0158c35d1..ec95cec81 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,11 @@ require ( github.com/jsternberg/zap-logfmt v1.3.0 github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.40.0 - github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 - go.yaml.in/yaml/v2 v2.4.2 - golang.org/x/term v0.40.0 - google.golang.org/api v0.269.0 + github.com/testcontainers/testcontainers-go v0.41.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 + go.yaml.in/yaml/v2 v2.4.4 + golang.org/x/term v0.41.0 + google.golang.org/api v0.271.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 @@ -33,20 +33,20 @@ require ( github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect github.com/stretchr/objx v0.5.2 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.2 // indirect ) require ( @@ -69,10 +69,10 @@ require ( github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -100,7 +100,7 @@ require ( github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/go-archive v0.2.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect @@ -120,23 +120,23 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/shirou/gopsutil/v4 v4.26.2 // indirect github.com/shopspring/decimal v1.4.0 github.com/sirupsen/logrus v1.9.3 // indirect github.com/supranational/blst v0.3.16 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.41.0 - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 golang.org/x/crypto v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 1ca2f8cda..8609fccd6 100644 --- a/go.sum +++ b/go.sum @@ -93,16 +93,16 @@ github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiD github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= -github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= @@ -160,8 +160,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -263,8 +263,8 @@ github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjU github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -337,8 +337,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -354,14 +354,14 @@ github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jq github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= -github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= -github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk= -github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= +github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -374,20 +374,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -396,19 +396,19 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -424,26 +424,26 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= -google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..cbe649e9e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,84 @@ +{ + "name": "nitrolite", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "prettier": "^3.8.1", + "prettier-plugin-solidity": "^2.3.1" + } + }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.8.tgz", + "integrity": "sha512-wS5kg8u0KCML1UeHQPJ1IuOI24x/XLentCzsqPER1+gDNC5Cz2hG4G2blLOZap+3CEGhIhnJ9mmZYj6a2W0Lww==", + "dev": true, + "license": "(Apache-2.0 WITH LLVM-exception)" + }, + "node_modules/@nomicfoundation/slang": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-1.3.4.tgz", + "integrity": "sha512-ghzrPSYH1sZO65id6+Bq2Ood87HT54QP3RGC8EkmpcrJ6tT9Ky0RtaJfrzV5G4jpDsnNua6+YEDpzOMori04hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "^0.17.2" + } + }, + "node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-solidity": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-2.3.1.tgz", + "integrity": "sha512-71sZM5oqgq6pnTlf+RH23U6Ej710APfCiMWO2Z/pHNjrXyvn9Nr0vTS1AUVaSf4GRW0V6hj6Djt0MyWudJUJbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/slang": "1.3.4", + "@solidity-parser/parser": "^0.20.2", + "semver": "^7.7.4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "prettier": ">=3.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..14905241c --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "prettier": "^3.8.1", + "prettier-plugin-solidity": "^2.3.1" + } +} diff --git a/sdk/package-lock.json b/sdk/package-lock.json new file mode 100644 index 000000000..9699e0851 --- /dev/null +++ b/sdk/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "sdk", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/sdk/ts/.prettierrc b/sdk/ts/.prettierrc index d32fbc15f..8b86bdcbb 100644 --- a/sdk/ts/.prettierrc +++ b/sdk/ts/.prettierrc @@ -1,6 +1,7 @@ { "jsxBracketSameLine": true, "singleQuote": true, + "tabWidth": 2, "overrides": [ { "files": ["**/*.css", "**/*.scss", "**/*.pcss", "**/*.html"], @@ -11,8 +12,7 @@ } ], "jsxSingleQuote": false, - "printWidth": 120, - "tabWidth": 4, + "printWidth": 100, "endOfLine": "auto", "semi": true, "tslintIntegration": true, diff --git a/sdk/ts/examples/app_sessions/package.json b/sdk/ts/examples/app_sessions/package.json index 8fb08af16..ae8a52246 100644 --- a/sdk/ts/examples/app_sessions/package.json +++ b/sdk/ts/examples/app_sessions/package.json @@ -9,7 +9,7 @@ "dependencies": { "@yellow-org/sdk": "file:../..", "decimal.js": "^10.4.3", - "viem": "^2.21.54" + "viem": "^2.46.0" }, "devDependencies": { "@types/node": "^22.10.2", diff --git a/sdk/ts/examples/example-app/package.json b/sdk/ts/examples/example-app/package.json index 1fd308e15..719e7eb1f 100644 --- a/sdk/ts/examples/example-app/package.json +++ b/sdk/ts/examples/example-app/package.json @@ -17,11 +17,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "decimal.js": "^10.4.3", - "lucide-react": "^0.563.0", + "lucide-react": "^0.564.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "tailwind-merge": "^3.4.0", - "viem": "^2.39.3" + "tailwind-merge": "^3.4.1", + "viem": "^2.46.1" }, "devDependencies": { "@types/react": "^18.3.12", diff --git a/sdk/ts/package-lock.json b/sdk/ts/package-lock.json index 97ddf92a9..98da16c0b 100644 --- a/sdk/ts/package-lock.json +++ b/sdk/ts/package-lock.json @@ -3008,9 +3008,9 @@ "license": "MIT" }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3460,9 +3460,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4002,9 +4002,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, diff --git a/sdk/ts/src/abis/generated.ts b/sdk/ts/src/abis/generated.ts index 561fbc2d3..47336d889 100644 --- a/sdk/ts/src/abis/generated.ts +++ b/sdk/ts/src/abis/generated.ts @@ -1,3 +1,1990 @@ // Auto-generated file. Do not edit manually. // Minified to reduce package size. View readable ABI at: contracts/out/ChannelHub.sol/ChannelHub.json -export const custodyAbi = [{"type":"constructor","inputs":[{"name":"_defaultSigValidator","type":"address","internalType":"contract ISignatureValidator"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_SIG_VALIDATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ISignatureValidator"}],"stateMutability":"view"},{"type":"function","name":"ESCROW_DEPOSIT_UNLOCK_DELAY","inputs":[],"outputs":[{"name":"","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"MAX_DEPOSIT_ESCROW_PURGE","inputs":[],"outputs":[{"name":"","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"MIN_CHALLENGE_DURATION","inputs":[],"outputs":[{"name":"","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"VERSION","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"type":"function","name":"challengeChannel","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]},{"name":"challengerSig","type":"bytes","internalType":"bytes"},{"name":"challengerIdx","type":"uint8","internalType":"enum ParticipantIndex"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"challengeEscrowDeposit","inputs":[{"name":"escrowId","type":"bytes32","internalType":"bytes32"},{"name":"challengerSig","type":"bytes","internalType":"bytes"},{"name":"challengerIdx","type":"uint8","internalType":"enum ParticipantIndex"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"challengeEscrowWithdrawal","inputs":[{"name":"escrowId","type":"bytes32","internalType":"bytes32"},{"name":"challengerSig","type":"bytes","internalType":"bytes"},{"name":"challengerIdx","type":"uint8","internalType":"enum ParticipantIndex"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"checkpointChannel","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"closeChannel","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"createChannel","inputs":[{"name":"def","type":"tuple","internalType":"struct ChannelDefinition","components":[{"name":"challengeDuration","type":"uint32","internalType":"uint32"},{"name":"user","type":"address","internalType":"address"},{"name":"node","type":"address","internalType":"address"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"approvedSignatureValidators","type":"uint256","internalType":"uint256"},{"name":"metadata","type":"bytes32","internalType":"bytes32"}]},{"name":"initState","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositToChannel","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositToVault","inputs":[{"name":"node","type":"address","internalType":"address"},{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"escrowHead","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeEscrowDeposit","inputs":[{"name":"escrowId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeEscrowWithdrawal","inputs":[{"name":"escrowId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeMigration","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getAccountBalance","inputs":[{"name":"node","type":"address","internalType":"address"},{"name":"token","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getChannelData","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"status","type":"uint8","internalType":"enum ChannelStatus"},{"name":"definition","type":"tuple","internalType":"struct ChannelDefinition","components":[{"name":"challengeDuration","type":"uint32","internalType":"uint32"},{"name":"user","type":"address","internalType":"address"},{"name":"node","type":"address","internalType":"address"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"approvedSignatureValidators","type":"uint256","internalType":"uint256"},{"name":"metadata","type":"bytes32","internalType":"bytes32"}]},{"name":"lastState","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]},{"name":"challengeExpiry","type":"uint256","internalType":"uint256"},{"name":"lockedFunds","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getChannelIds","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bytes32[]","internalType":"bytes32[]"}],"stateMutability":"view"},{"type":"function","name":"getEscrowDepositData","inputs":[{"name":"escrowId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"status","type":"uint8","internalType":"enum EscrowStatus"},{"name":"unlockAt","type":"uint64","internalType":"uint64"},{"name":"challengeExpiry","type":"uint64","internalType":"uint64"},{"name":"lockedAmount","type":"uint256","internalType":"uint256"},{"name":"initState","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"stateMutability":"view"},{"type":"function","name":"getEscrowDepositIds","inputs":[{"name":"page","type":"uint256","internalType":"uint256"},{"name":"pageSize","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"ids","type":"bytes32[]","internalType":"bytes32[]"}],"stateMutability":"view"},{"type":"function","name":"getEscrowWithdrawalData","inputs":[{"name":"escrowId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"status","type":"uint8","internalType":"enum EscrowStatus"},{"name":"challengeExpiry","type":"uint64","internalType":"uint64"},{"name":"lockedAmount","type":"uint256","internalType":"uint256"},{"name":"initState","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeValidator","inputs":[{"name":"node","type":"address","internalType":"address"},{"name":"validatorId","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"","type":"address","internalType":"contract ISignatureValidator"}],"stateMutability":"view"},{"type":"function","name":"getOpenChannels","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[{"name":"openChannels","type":"bytes32[]","internalType":"bytes32[]"}],"stateMutability":"view"},{"type":"function","name":"initiateEscrowDeposit","inputs":[{"name":"def","type":"tuple","internalType":"struct ChannelDefinition","components":[{"name":"challengeDuration","type":"uint32","internalType":"uint32"},{"name":"user","type":"address","internalType":"address"},{"name":"node","type":"address","internalType":"address"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"approvedSignatureValidators","type":"uint256","internalType":"uint256"},{"name":"metadata","type":"bytes32","internalType":"bytes32"}]},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"initiateEscrowWithdrawal","inputs":[{"name":"def","type":"tuple","internalType":"struct ChannelDefinition","components":[{"name":"challengeDuration","type":"uint32","internalType":"uint32"},{"name":"user","type":"address","internalType":"address"},{"name":"node","type":"address","internalType":"address"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"approvedSignatureValidators","type":"uint256","internalType":"uint256"},{"name":"metadata","type":"bytes32","internalType":"bytes32"}]},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"initiateMigration","inputs":[{"name":"def","type":"tuple","internalType":"struct ChannelDefinition","components":[{"name":"challengeDuration","type":"uint32","internalType":"uint32"},{"name":"user","type":"address","internalType":"address"},{"name":"node","type":"address","internalType":"address"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"approvedSignatureValidators","type":"uint256","internalType":"uint256"},{"name":"metadata","type":"bytes32","internalType":"bytes32"}]},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"purgeEscrowDeposits","inputs":[{"name":"maxToPurge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"registerNodeValidator","inputs":[{"name":"node","type":"address","internalType":"address"},{"name":"validatorId","type":"uint8","internalType":"uint8"},{"name":"validator","type":"address","internalType":"contract ISignatureValidator"},{"name":"signature","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"withdrawFromChannel","inputs":[{"name":"channelId","type":"bytes32","internalType":"bytes32"},{"name":"candidate","type":"tuple","internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"withdrawFromVault","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"ChannelChallenged","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"candidate","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]},{"name":"challengeExpireAt","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"ChannelCheckpointed","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"candidate","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"ChannelClosed","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"finalState","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"ChannelCreated","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"user","type":"address","indexed":true,"internalType":"address"},{"name":"node","type":"address","indexed":true,"internalType":"address"},{"name":"definition","type":"tuple","indexed":false,"internalType":"struct ChannelDefinition","components":[{"name":"challengeDuration","type":"uint32","internalType":"uint32"},{"name":"user","type":"address","internalType":"address"},{"name":"node","type":"address","internalType":"address"},{"name":"nonce","type":"uint64","internalType":"uint64"},{"name":"approvedSignatureValidators","type":"uint256","internalType":"uint256"},{"name":"metadata","type":"bytes32","internalType":"bytes32"}]},{"name":"initialState","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"ChannelDeposited","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"candidate","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"ChannelWithdrawn","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"candidate","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"Deposited","inputs":[{"name":"wallet","type":"address","indexed":true,"internalType":"address"},{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"EscrowDepositChallenged","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]},{"name":"challengeExpireAt","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"EscrowDepositFinalized","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowDepositFinalizedOnHome","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowDepositInitiated","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowDepositInitiatedOnHome","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowDepositsPurged","inputs":[{"name":"purgedCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"EscrowWithdrawalChallenged","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]},{"name":"challengeExpireAt","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"EscrowWithdrawalFinalized","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowWithdrawalFinalizedOnHome","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowWithdrawalInitiated","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"EscrowWithdrawalInitiatedOnHome","inputs":[{"name":"escrowId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"MigrationInFinalized","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"MigrationInInitiated","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"MigrationOutFinalized","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"MigrationOutInitiated","inputs":[{"name":"channelId","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"state","type":"tuple","indexed":false,"internalType":"struct State","components":[{"name":"version","type":"uint64","internalType":"uint64"},{"name":"intent","type":"uint8","internalType":"enum StateIntent"},{"name":"metadata","type":"bytes32","internalType":"bytes32"},{"name":"homeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"nonHomeLedger","type":"tuple","internalType":"struct Ledger","components":[{"name":"chainId","type":"uint64","internalType":"uint64"},{"name":"token","type":"address","internalType":"address"},{"name":"decimals","type":"uint8","internalType":"uint8"},{"name":"userAllocation","type":"uint256","internalType":"uint256"},{"name":"userNetFlow","type":"int256","internalType":"int256"},{"name":"nodeAllocation","type":"uint256","internalType":"uint256"},{"name":"nodeNetFlow","type":"int256","internalType":"int256"}]},{"name":"userSig","type":"bytes","internalType":"bytes"},{"name":"nodeSig","type":"bytes","internalType":"bytes"}]}],"anonymous":false},{"type":"event","name":"ValidatorRegistered","inputs":[{"name":"node","type":"address","indexed":true,"internalType":"address"},{"name":"validatorId","type":"uint8","indexed":true,"internalType":"uint8"},{"name":"validator","type":"address","indexed":true,"internalType":"contract ISignatureValidator"}],"anonymous":false},{"type":"event","name":"Withdrawn","inputs":[{"name":"wallet","type":"address","indexed":true,"internalType":"address"},{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AddressCollision","inputs":[{"name":"collision","type":"address","internalType":"address"}]},{"type":"error","name":"ChallengerVersionTooLow","inputs":[]},{"type":"error","name":"ECDSAInvalidSignature","inputs":[]},{"type":"error","name":"ECDSAInvalidSignatureLength","inputs":[{"name":"length","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ECDSAInvalidSignatureS","inputs":[{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"EmptySignature","inputs":[]},{"type":"error","name":"IncorrectAmount","inputs":[]},{"type":"error","name":"IncorrectChallengeDuration","inputs":[]},{"type":"error","name":"IncorrectChannelStatus","inputs":[]},{"type":"error","name":"IncorrectSignature","inputs":[]},{"type":"error","name":"IncorrectStateIntent","inputs":[]},{"type":"error","name":"IncorrectValue","inputs":[]},{"type":"error","name":"InsufficientBalance","inputs":[]},{"type":"error","name":"InvalidAddress","inputs":[]},{"type":"error","name":"InvalidValidatorId","inputs":[]},{"type":"error","name":"OnlyNonHomeEscrowsCanBeChallenged","inputs":[]},{"type":"error","name":"ReentrancyGuardReentrantCall","inputs":[]},{"type":"error","name":"SafeCastOverflowedIntToUint","inputs":[{"name":"value","type":"int256","internalType":"int256"}]},{"type":"error","name":"SafeERC20FailedOperation","inputs":[{"name":"token","type":"address","internalType":"address"}]},{"type":"error","name":"TransferFailed","inputs":[{"name":"recepient","type":"address","internalType":"address"},{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ValidatorAlreadyRegistered","inputs":[{"name":"node","type":"address","internalType":"address"},{"name":"validatorId","type":"uint8","internalType":"uint8"}]},{"type":"error","name":"ValidatorNotApproved","inputs":[]},{"type":"error","name":"ValidatorNotRegistered","inputs":[{"name":"node","type":"address","internalType":"address"},{"name":"validatorId","type":"uint8","internalType":"uint8"}]}] as const; +export const custodyAbi = [ + { type: 'constructor', inputs: [{ name: '_defaultSigValidator', type: 'address', internalType: 'contract ISignatureValidator' }], stateMutability: 'nonpayable' }, + { type: 'function', name: 'DEFAULT_SIG_VALIDATOR', inputs: [], outputs: [{ name: '', type: 'address', internalType: 'contract ISignatureValidator' }], stateMutability: 'view' }, + { type: 'function', name: 'ESCROW_DEPOSIT_UNLOCK_DELAY', inputs: [], outputs: [{ name: '', type: 'uint32', internalType: 'uint32' }], stateMutability: 'view' }, + { type: 'function', name: 'MAX_DEPOSIT_ESCROW_PURGE', inputs: [], outputs: [{ name: '', type: 'uint32', internalType: 'uint32' }], stateMutability: 'view' }, + { type: 'function', name: 'MIN_CHALLENGE_DURATION', inputs: [], outputs: [{ name: '', type: 'uint32', internalType: 'uint32' }], stateMutability: 'view' }, + { type: 'function', name: 'VERSION', inputs: [], outputs: [{ name: '', type: 'uint8', internalType: 'uint8' }], stateMutability: 'view' }, + { + type: 'function', + name: 'challengeChannel', + inputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + { name: 'challengerSig', type: 'bytes', internalType: 'bytes' }, + { name: 'challengerIdx', type: 'uint8', internalType: 'enum ParticipantIndex' }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'challengeEscrowDeposit', + inputs: [ + { name: 'escrowId', type: 'bytes32', internalType: 'bytes32' }, + { name: 'challengerSig', type: 'bytes', internalType: 'bytes' }, + { name: 'challengerIdx', type: 'uint8', internalType: 'enum ParticipantIndex' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'challengeEscrowWithdrawal', + inputs: [ + { name: 'escrowId', type: 'bytes32', internalType: 'bytes32' }, + { name: 'challengerSig', type: 'bytes', internalType: 'bytes' }, + { name: 'challengerIdx', type: 'uint8', internalType: 'enum ParticipantIndex' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'checkpointChannel', + inputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'closeChannel', + inputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'createChannel', + inputs: [ + { + name: 'def', + type: 'tuple', + internalType: 'struct ChannelDefinition', + components: [ + { name: 'challengeDuration', type: 'uint32', internalType: 'uint32' }, + { name: 'user', type: 'address', internalType: 'address' }, + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint64', internalType: 'uint64' }, + { name: 'approvedSignatureValidators', type: 'uint256', internalType: 'uint256' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { + name: 'initState', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'depositToChannel', + inputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'depositToVault', + inputs: [ + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'payable', + }, + { type: 'function', name: 'escrowHead', inputs: [], outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], stateMutability: 'view' }, + { + type: 'function', + name: 'finalizeEscrowDeposit', + inputs: [ + { name: 'escrowId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'finalizeEscrowWithdrawal', + inputs: [ + { name: 'escrowId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'finalizeMigration', + inputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getAccountBalance', + inputs: [ + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getChannelData', + inputs: [{ name: 'channelId', type: 'bytes32', internalType: 'bytes32' }], + outputs: [ + { name: 'status', type: 'uint8', internalType: 'enum ChannelStatus' }, + { + name: 'definition', + type: 'tuple', + internalType: 'struct ChannelDefinition', + components: [ + { name: 'challengeDuration', type: 'uint32', internalType: 'uint32' }, + { name: 'user', type: 'address', internalType: 'address' }, + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint64', internalType: 'uint64' }, + { name: 'approvedSignatureValidators', type: 'uint256', internalType: 'uint256' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { + name: 'lastState', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + { name: 'challengeExpiry', type: 'uint256', internalType: 'uint256' }, + { name: 'lockedFunds', type: 'uint256', internalType: 'uint256' }, + ], + stateMutability: 'view', + }, + { type: 'function', name: 'getChannelIds', inputs: [{ name: 'user', type: 'address', internalType: 'address' }], outputs: [{ name: '', type: 'bytes32[]', internalType: 'bytes32[]' }], stateMutability: 'view' }, + { + type: 'function', + name: 'getEscrowDepositData', + inputs: [{ name: 'escrowId', type: 'bytes32', internalType: 'bytes32' }], + outputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { name: 'status', type: 'uint8', internalType: 'enum EscrowStatus' }, + { name: 'unlockAt', type: 'uint64', internalType: 'uint64' }, + { name: 'challengeExpiry', type: 'uint64', internalType: 'uint64' }, + { name: 'lockedAmount', type: 'uint256', internalType: 'uint256' }, + { + name: 'initState', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getEscrowDepositIds', + inputs: [ + { name: 'page', type: 'uint256', internalType: 'uint256' }, + { name: 'pageSize', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: 'ids', type: 'bytes32[]', internalType: 'bytes32[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getEscrowWithdrawalData', + inputs: [{ name: 'escrowId', type: 'bytes32', internalType: 'bytes32' }], + outputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { name: 'status', type: 'uint8', internalType: 'enum EscrowStatus' }, + { name: 'challengeExpiry', type: 'uint64', internalType: 'uint64' }, + { name: 'lockedAmount', type: 'uint256', internalType: 'uint256' }, + { + name: 'initState', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getNodeValidator', + inputs: [ + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'validatorId', type: 'uint8', internalType: 'uint8' }, + ], + outputs: [{ name: '', type: 'address', internalType: 'contract ISignatureValidator' }], + stateMutability: 'view', + }, + { type: 'function', name: 'getOpenChannels', inputs: [{ name: 'user', type: 'address', internalType: 'address' }], outputs: [{ name: 'openChannels', type: 'bytes32[]', internalType: 'bytes32[]' }], stateMutability: 'view' }, + { + type: 'function', + name: 'initiateEscrowDeposit', + inputs: [ + { + name: 'def', + type: 'tuple', + internalType: 'struct ChannelDefinition', + components: [ + { name: 'challengeDuration', type: 'uint32', internalType: 'uint32' }, + { name: 'user', type: 'address', internalType: 'address' }, + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint64', internalType: 'uint64' }, + { name: 'approvedSignatureValidators', type: 'uint256', internalType: 'uint256' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'initiateEscrowWithdrawal', + inputs: [ + { + name: 'def', + type: 'tuple', + internalType: 'struct ChannelDefinition', + components: [ + { name: 'challengeDuration', type: 'uint32', internalType: 'uint32' }, + { name: 'user', type: 'address', internalType: 'address' }, + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint64', internalType: 'uint64' }, + { name: 'approvedSignatureValidators', type: 'uint256', internalType: 'uint256' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'initiateMigration', + inputs: [ + { + name: 'def', + type: 'tuple', + internalType: 'struct ChannelDefinition', + components: [ + { name: 'challengeDuration', type: 'uint32', internalType: 'uint32' }, + { name: 'user', type: 'address', internalType: 'address' }, + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint64', internalType: 'uint64' }, + { name: 'approvedSignatureValidators', type: 'uint256', internalType: 'uint256' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { type: 'function', name: 'purgeEscrowDeposits', inputs: [{ name: 'maxToPurge', type: 'uint256', internalType: 'uint256' }], outputs: [], stateMutability: 'nonpayable' }, + { + type: 'function', + name: 'registerNodeValidator', + inputs: [ + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'validatorId', type: 'uint8', internalType: 'uint8' }, + { name: 'validator', type: 'address', internalType: 'contract ISignatureValidator' }, + { name: 'signature', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'withdrawFromChannel', + inputs: [ + { name: 'channelId', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + name: 'withdrawFromVault', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'ChannelChallenged', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + { name: 'challengeExpireAt', type: 'uint64', indexed: false, internalType: 'uint64' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChannelCheckpointed', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChannelClosed', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'finalState', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChannelCreated', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'user', type: 'address', indexed: true, internalType: 'address' }, + { name: 'node', type: 'address', indexed: true, internalType: 'address' }, + { + name: 'definition', + type: 'tuple', + indexed: false, + internalType: 'struct ChannelDefinition', + components: [ + { name: 'challengeDuration', type: 'uint32', internalType: 'uint32' }, + { name: 'user', type: 'address', internalType: 'address' }, + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'nonce', type: 'uint64', internalType: 'uint64' }, + { name: 'approvedSignatureValidators', type: 'uint256', internalType: 'uint256' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { + name: 'initialState', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChannelDeposited', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChannelWithdrawn', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'candidate', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Deposited', + inputs: [ + { name: 'wallet', type: 'address', indexed: true, internalType: 'address' }, + { name: 'token', type: 'address', indexed: true, internalType: 'address' }, + { name: 'amount', type: 'uint256', indexed: false, internalType: 'uint256' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowDepositChallenged', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + { name: 'challengeExpireAt', type: 'uint64', indexed: false, internalType: 'uint64' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowDepositFinalized', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowDepositFinalizedOnHome', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowDepositInitiated', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowDepositInitiatedOnHome', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { type: 'event', name: 'EscrowDepositsPurged', inputs: [{ name: 'purgedCount', type: 'uint256', indexed: false, internalType: 'uint256' }], anonymous: false }, + { + type: 'event', + name: 'EscrowWithdrawalChallenged', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + { name: 'challengeExpireAt', type: 'uint64', indexed: false, internalType: 'uint64' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowWithdrawalFinalized', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowWithdrawalFinalizedOnHome', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowWithdrawalInitiated', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EscrowWithdrawalInitiatedOnHome', + inputs: [ + { name: 'escrowId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MigrationInFinalized', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MigrationInInitiated', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MigrationOutFinalized', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MigrationOutInitiated', + inputs: [ + { name: 'channelId', type: 'bytes32', indexed: true, internalType: 'bytes32' }, + { + name: 'state', + type: 'tuple', + indexed: false, + internalType: 'struct State', + components: [ + { name: 'version', type: 'uint64', internalType: 'uint64' }, + { name: 'intent', type: 'uint8', internalType: 'enum StateIntent' }, + { name: 'metadata', type: 'bytes32', internalType: 'bytes32' }, + { + name: 'homeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { + name: 'nonHomeLedger', + type: 'tuple', + internalType: 'struct Ledger', + components: [ + { name: 'chainId', type: 'uint64', internalType: 'uint64' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { name: 'userAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'userNetFlow', type: 'int256', internalType: 'int256' }, + { name: 'nodeAllocation', type: 'uint256', internalType: 'uint256' }, + { name: 'nodeNetFlow', type: 'int256', internalType: 'int256' }, + ], + }, + { name: 'userSig', type: 'bytes', internalType: 'bytes' }, + { name: 'nodeSig', type: 'bytes', internalType: 'bytes' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ValidatorRegistered', + inputs: [ + { name: 'node', type: 'address', indexed: true, internalType: 'address' }, + { name: 'validatorId', type: 'uint8', indexed: true, internalType: 'uint8' }, + { name: 'validator', type: 'address', indexed: true, internalType: 'contract ISignatureValidator' }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Withdrawn', + inputs: [ + { name: 'wallet', type: 'address', indexed: true, internalType: 'address' }, + { name: 'token', type: 'address', indexed: true, internalType: 'address' }, + { name: 'amount', type: 'uint256', indexed: false, internalType: 'uint256' }, + ], + anonymous: false, + }, + { type: 'error', name: 'AddressCollision', inputs: [{ name: 'collision', type: 'address', internalType: 'address' }] }, + { type: 'error', name: 'ChallengerVersionTooLow', inputs: [] }, + { type: 'error', name: 'ECDSAInvalidSignature', inputs: [] }, + { type: 'error', name: 'ECDSAInvalidSignatureLength', inputs: [{ name: 'length', type: 'uint256', internalType: 'uint256' }] }, + { type: 'error', name: 'ECDSAInvalidSignatureS', inputs: [{ name: 's', type: 'bytes32', internalType: 'bytes32' }] }, + { type: 'error', name: 'EmptySignature', inputs: [] }, + { type: 'error', name: 'IncorrectAmount', inputs: [] }, + { type: 'error', name: 'IncorrectChallengeDuration', inputs: [] }, + { type: 'error', name: 'IncorrectChannelStatus', inputs: [] }, + { type: 'error', name: 'IncorrectSignature', inputs: [] }, + { type: 'error', name: 'IncorrectStateIntent', inputs: [] }, + { type: 'error', name: 'IncorrectValue', inputs: [] }, + { type: 'error', name: 'InsufficientBalance', inputs: [] }, + { type: 'error', name: 'InvalidAddress', inputs: [] }, + { type: 'error', name: 'InvalidValidatorId', inputs: [] }, + { type: 'error', name: 'OnlyNonHomeEscrowsCanBeChallenged', inputs: [] }, + { type: 'error', name: 'ReentrancyGuardReentrantCall', inputs: [] }, + { type: 'error', name: 'SafeCastOverflowedIntToUint', inputs: [{ name: 'value', type: 'int256', internalType: 'int256' }] }, + { type: 'error', name: 'SafeERC20FailedOperation', inputs: [{ name: 'token', type: 'address', internalType: 'address' }] }, + { + type: 'error', + name: 'TransferFailed', + inputs: [ + { name: 'recepient', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ValidatorAlreadyRegistered', + inputs: [ + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'validatorId', type: 'uint8', internalType: 'uint8' }, + ], + }, + { type: 'error', name: 'ValidatorNotApproved', inputs: [] }, + { + type: 'error', + name: 'ValidatorNotRegistered', + inputs: [ + { name: 'node', type: 'address', internalType: 'address' }, + { name: 'validatorId', type: 'uint8', internalType: 'uint8' }, + ], + }, +] as const; diff --git a/sdk/ts/src/app/packing.ts b/sdk/ts/src/app/packing.ts index 421c9c7de..9d157ee60 100644 --- a/sdk/ts/src/app/packing.ts +++ b/sdk/ts/src/app/packing.ts @@ -1,20 +1,12 @@ import { Address, Hex, encodeAbiParameters, keccak256, pad, toHex } from 'viem'; -import { - AppDefinitionV1, - AppStateUpdateV1, - AppSessionKeyStateV1, - AppSessionVersionV1, -} from './types'; +import { AppDefinitionV1, AppStateUpdateV1, AppSessionKeyStateV1, AppSessionVersionV1 } from './types'; import { AppV1 } from '../rpc/types'; /** * PackCreateAppSessionRequestV1 packs the Definition and SessionData for signing using ABI encoding. * This is used to generate a deterministic hash that participants sign when creating an app session. */ -export function packCreateAppSessionRequestV1( - definition: AppDefinitionV1, - sessionData: string -): `0x${string}` { +export function packCreateAppSessionRequestV1(definition: AppDefinitionV1, sessionData: string): `0x${string}` { // Define the participant tuple type components const participantComponents = [ { name: 'walletAddress', type: 'address' }, @@ -36,13 +28,7 @@ export function packCreateAppSessionRequestV1( { type: 'uint64' }, // nonce { type: 'string' }, // sessionData ], - [ - definition.applicationId, - participants, - definition.quorum, - definition.nonce, - sessionData, - ] + [definition.applicationId, participants, definition.quorum, definition.nonce, sessionData], ); // Return the Keccak256 hash of the packed data @@ -80,13 +66,7 @@ export function packAppStateUpdateV1(stateUpdate: AppStateUpdateV1): `0x${string { type: 'tuple[]', components: allocationComponents }, // allocations array { type: 'string' }, // sessionData ], - [ - appSessionIdHash, - stateUpdate.intent, - stateUpdate.version, - allocations, - stateUpdate.sessionData, - ] + [appSessionIdHash, stateUpdate.intent, stateUpdate.version, allocations, stateUpdate.sessionData], ); // Return the Keccak256 hash of the packed data @@ -117,7 +97,7 @@ export function generateAppSessionIDV1(definition: AppDefinitionV1): `0x${string { type: 'uint8' }, // quorum { type: 'uint64' }, // nonce ], - [definition.applicationId, participants, definition.quorum, definition.nonce] + [definition.applicationId, participants, definition.quorum, definition.nonce], ); // Return the Keccak256 hash as hex string @@ -128,9 +108,7 @@ export function generateAppSessionIDV1(definition: AppDefinitionV1): `0x${string * GenerateRebalanceBatchIDV1 creates a deterministic batch ID from session versions using ABI encoding. * The batch ID is generated by hashing the list of (sessionID, version) pairs. */ -export function generateRebalanceBatchIDV1( - sessionVersions: AppSessionVersionV1[] -): `0x${string}` { +export function generateRebalanceBatchIDV1(sessionVersions: AppSessionVersionV1[]): `0x${string}` { // Define the session version tuple type components const sessionVersionComponents = [ { name: 'sessionID', type: 'bytes32' }, @@ -148,7 +126,7 @@ export function generateRebalanceBatchIDV1( [ { type: 'tuple[]', components: sessionVersionComponents }, // session versions array ], - [sessionVersionsArray] + [sessionVersionsArray], ); // Return the Keccak256 hash as hex string @@ -158,11 +136,7 @@ export function generateRebalanceBatchIDV1( /** * GenerateRebalanceTransactionIDV1 creates a deterministic transaction ID for a rebalance transaction using ABI encoding. */ -export function generateRebalanceTransactionIDV1( - batchId: string, - sessionId: string, - asset: string -): `0x${string}` { +export function generateRebalanceTransactionIDV1(batchId: string, sessionId: string, asset: string): `0x${string}` { // Pack the data using ABI encoding const packed = encodeAbiParameters( [ @@ -170,7 +144,7 @@ export function generateRebalanceTransactionIDV1( { type: 'bytes32' }, // sessionID { type: 'string' }, // asset ], - [batchId as `0x${string}`, sessionId as `0x${string}`, asset] + [batchId as `0x${string}`, sessionId as `0x${string}`, asset], ); // Return the Keccak256 hash as hex string @@ -189,19 +163,13 @@ export function packAppV1(app: AppV1): `0x${string}` { const packed = encodeAbiParameters( [ - { type: 'string' }, // id - { type: 'address' }, // ownerWallet - { type: 'bytes32' }, // metadata (hashed) - { type: 'uint64' }, // version - { type: 'bool' }, // creationApprovalNotRequired + { type: 'string' }, // id + { type: 'address' }, // ownerWallet + { type: 'bytes32' }, // metadata (hashed) + { type: 'uint64' }, // version + { type: 'bool' }, // creationApprovalNotRequired ], - [ - app.id, - app.owner_wallet as Address, - metadataHash, - BigInt(app.version), - app.creation_approval_not_required, - ] + [app.id, app.owner_wallet as Address, metadataHash, BigInt(app.version), app.creation_approval_not_required], ); return keccak256(packed); @@ -250,21 +218,14 @@ export function packAppSessionKeyStateV1(state: AppSessionKeyStateV1): `0x${stri const packed = encodeAbiParameters( [ - { type: 'address' }, // user_address - { type: 'address' }, // session_key - { type: 'uint64' }, // version - { type: 'bytes32[]' }, // application_ids - { type: 'bytes32[]' }, // app_session_ids - { type: 'uint64' }, // expires_at + { type: 'address' }, // user_address + { type: 'address' }, // session_key + { type: 'uint64' }, // version + { type: 'bytes32[]' }, // application_ids + { type: 'bytes32[]' }, // app_session_ids + { type: 'uint64' }, // expires_at ], - [ - state.user_address as Address, - state.session_key as Address, - BigInt(state.version), - applicationIDHashes, - appSessionIDHashes, - BigInt(state.expires_at), - ] + [state.user_address as Address, state.session_key as Address, BigInt(state.version), applicationIDHashes, appSessionIDHashes, BigInt(state.expires_at)], ); return keccak256(packed); diff --git a/sdk/ts/src/asset_store.ts b/sdk/ts/src/asset_store.ts index ca3b7ebde..ddc9f3a8a 100644 --- a/sdk/ts/src/asset_store.ts +++ b/sdk/ts/src/asset_store.ts @@ -57,10 +57,7 @@ export class ClientAssetStore { const tokenAddrLower = tokenAddress.toLowerCase(); for (const asset of this.cache.values()) { for (const token of asset.tokens) { - if ( - token.blockchainId === blockchainId && - token.address.toLowerCase() === tokenAddrLower - ) { + if (token.blockchainId === blockchainId && token.address.toLowerCase() === tokenAddrLower) { return token.decimals; } } diff --git a/sdk/ts/src/blockchain/evm/channel_hub_abi.ts b/sdk/ts/src/blockchain/evm/channel_hub_abi.ts index 794bc8d18..afd3925b1 100644 --- a/sdk/ts/src/blockchain/evm/channel_hub_abi.ts +++ b/sdk/ts/src/blockchain/evm/channel_hub_abi.ts @@ -829,6 +829,11 @@ export const ChannelHubAbi = [ type: 'address', internalType: 'address', }, + { + name: 'subId', + type: 'uint48', + internalType: 'uint48', + }, { name: 'amount', type: 'uint256', @@ -1252,6 +1257,11 @@ export const ChannelHubAbi = [ type: 'address', internalType: 'address', }, + { + name: 'subId', + type: 'uint48', + internalType: 'uint48', + }, ], outputs: [ { @@ -1463,6 +1473,13 @@ export const ChannelHubAbi = [ ], stateMutability: 'view', }, + { + type: 'function', + name: 'getChannelSubId', + inputs: [{ name: 'channelId', type: 'bytes32' }], + outputs: [{ name: '', type: 'uint48' }], + stateMutability: 'view', + }, { type: 'function', name: 'getEscrowDepositData', @@ -2528,6 +2545,11 @@ export const ChannelHubAbi = [ type: 'address', internalType: 'address', }, + { + name: 'subId', + type: 'uint48', + internalType: 'uint48', + }, { name: 'amount', type: 'uint256', @@ -3389,6 +3411,12 @@ export const ChannelHubAbi = [ indexed: true, internalType: 'address', }, + { + name: 'subId', + type: 'uint48', + indexed: true, + internalType: 'uint48', + }, { name: 'amount', type: 'uint256', @@ -5332,6 +5360,12 @@ export const ChannelHubAbi = [ indexed: true, internalType: 'address', }, + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, { name: 'amount', type: 'uint256', diff --git a/sdk/ts/src/blockchain/evm/client.ts b/sdk/ts/src/blockchain/evm/client.ts index 6a7594dd4..0cfc060dc 100644 --- a/sdk/ts/src/blockchain/evm/client.ts +++ b/sdk/ts/src/blockchain/evm/client.ts @@ -9,11 +9,7 @@ import * as core from '../../core/types'; import { decimalToBigInt } from '../../core/utils'; import { AssetStore, EVMClient, WalletSigner } from './interface'; import { ChannelHubAbi } from './channel_hub_abi'; -import { - coreDefToContractDef, - coreStateToContractState, - contractStateToCoreState, -} from './utils'; +import { coreDefToContractDef, coreStateToContractState, contractStateToCoreState } from './utils'; import { newERC20 } from './erc20'; /** @@ -45,7 +41,7 @@ export class Client { blockchainId: bigint, nodeAddress: Address, assetStore: AssetStore, - options?: ClientOptions + options?: ClientOptions, ) { this.contractAddress = contractAddress; this.evmClient = evmClient; @@ -84,7 +80,7 @@ export class Client { address: this.contractAddress, abi: ChannelHubAbi, functionName: 'getAccountBalance', - args: [account, token], + args: [account, token, 0], })) as bigint; accountBalances.push(new Decimal(balance.toString())); } @@ -94,6 +90,33 @@ export class Client { return result; } + async getAccountsSubBalances( + accounts: Address[], + token: Address, + subIds: number[], + ): Promise { + if (accounts.length === 0 || subIds.length === 0) { + return []; + } + + const result: Decimal[][] = []; + for (const account of accounts) { + const accountSubBalances: Decimal[] = []; + for (const subId of subIds) { + const balance = (await this.evmClient.readContract({ + address: this.contractAddress, + abi: ChannelHubAbi, + functionName: 'getAccountBalance', + args: [account, token, subId], + })) as bigint; + accountSubBalances.push(new Decimal(balance.toString())); + } + result.push(accountSubBalances); + } + + return result; + } + private async getAllowance(asset: string, owner: Address): Promise { const tokenAddress = await this.assetStore.getTokenAddress(asset, this.blockchainId); @@ -114,7 +137,9 @@ export class Client { // Native token (zero address) — query ETH balance directly if (tokenAddress === zeroAddress) { - const balance = await this.evmClient.getBalance({ address: walletAddress }); + const balance = await this.evmClient.getBalance({ + address: walletAddress, + }); // Native tokens use 18 decimals return new Decimal(balance.toString()).div(Decimal.pow(10, 18)); } @@ -168,15 +193,14 @@ export class Client { return await erc20.allowance(owner, this.contractAddress); } - // ========= Getters - ChannelHub ========= - async getNodeBalance(token: Address): Promise { + async getNodeBalance(token: Address, subId: number = 0): Promise { const balance = (await this.evmClient.readContract({ address: this.contractAddress, abi: ChannelHubAbi, functionName: 'getAccountBalance', - args: [this.nodeAddress, token], + args: [this.nodeAddress, token, subId], })) as bigint; const decimals = await this.assetStore.getTokenDecimals(this.blockchainId, token); @@ -193,6 +217,19 @@ export class Client { return channelIds.map((id) => id); } + async getChannelSubId(channelId: string): Promise { + const channelIdBytes = this.hexToBytes32(channelId); + + const subId = (await this.evmClient.readContract({ + address: this.contractAddress, + abi: ChannelHubAbi, + functionName: 'getChannelSubId', + args: [channelIdBytes], + })) as number; + + return Number(subId); + } + async getHomeChannelData(homeChannelId: string): Promise { const channelIdBytes = this.hexToBytes32(homeChannelId); @@ -204,7 +241,11 @@ export class Client { })) as any; // getChannelData returns flat values: (status, definition, lastState, challengeExpiry, lockedFunds) - const [, definition, lastState, challengeExpiry] = Array.isArray(data) ? data : [data.status, data.definition, data.lastState, data.challengeExpiry, data.lockedFunds]; + const [, definition, lastState, challengeExpiry] = Array.isArray(data) + ? data + : [data.status, data.definition, data.lastState, data.challengeExpiry, data.lockedFunds]; + + const subId = await this.getChannelSubId(homeChannelId); const coreState = contractStateToCoreState(lastState, homeChannelId); @@ -217,6 +258,7 @@ export class Client { node: definition.node, lastState: coreState, challengeExpiry, + subId, }; } @@ -227,14 +269,19 @@ export class Client { } async getEscrowWithdrawalData( - _escrowChannelId: string + _escrowChannelId: string, ): Promise { throw new Error('getEscrowWithdrawalData not implemented - needs contract ABI update'); } // ========= IVault Functions ========= - async deposit(node: Address, token: Address, amount: Decimal): Promise { + async deposit( + node: Address, + token: Address, + amount: Decimal, + subId: number = 0, + ): Promise { const decimals = await this.assetStore.getTokenDecimals(this.blockchainId, token); const amountBig = decimalToBigInt(amount, decimals); @@ -243,10 +290,11 @@ export class Client { blockchainId: this.blockchainId.toString(), node, token, + subId, amount: amount.toString(), amountBig: amountBig.toString(), walletChain: this.walletSigner.chain?.id, - walletChainName: this.walletSigner.chain?.name + walletChainName: this.walletSigner.chain?.name, }); try { @@ -256,7 +304,7 @@ export class Client { address: this.contractAddress, abi: ChannelHubAbi, functionName: 'depositToVault', - args: [node, token, amountBig], + args: [node, token, subId, amountBig], account: this.walletSigner.account!.address, ...(token === zeroAddress ? { value: amountBig } : {}), }); @@ -281,18 +329,29 @@ export class Client { } catch (error: any) { console.error('❌ Deposit transaction failed at blockchain level'); if (error.message?.includes('not supported') || error.message?.includes('not available')) { - console.error('⚠️ RPC ENDPOINT ISSUE: The RPC endpoint does not support sending transactions.'); - console.error(' This usually means the RPC only supports read operations (eth_call, eth_getBalance, etc.)'); + console.error( + '⚠️ RPC ENDPOINT ISSUE: The RPC endpoint does not support sending transactions.', + ); + console.error( + ' This usually means the RPC only supports read operations (eth_call, eth_getBalance, etc.)', + ); console.error(' but not write operations (eth_sendTransaction).'); console.error(' Solutions:'); - console.error(' 1. Use an RPC provider that supports transactions (Infura, Alchemy, etc.)'); + console.error( + ' 1. Use an RPC provider that supports transactions (Infura, Alchemy, etc.)', + ); console.error(' 2. Make sure your RPC endpoint includes transaction capabilities'); } throw error; } } - async withdraw(node: Address, token: Address, amount: Decimal): Promise { + async withdraw( + node: Address, + token: Address, + amount: Decimal, + subId: number = 0, + ): Promise { const decimals = await this.assetStore.getTokenDecimals(this.blockchainId, token); const amountBig = decimalToBigInt(amount, decimals); @@ -304,7 +363,7 @@ export class Client { amount: amount.toString(), amountBig: amountBig.toString(), walletChain: this.walletSigner.chain?.id, - walletChainName: this.walletSigner.chain?.name + walletChainName: this.walletSigner.chain?.name, }); try { @@ -314,7 +373,7 @@ export class Client { address: this.contractAddress, abi: ChannelHubAbi, functionName: 'withdrawFromVault', - args: [node, token, amountBig], + args: [node, token, subId, amountBig], account: this.walletSigner.account!.address, }); @@ -349,11 +408,11 @@ export class Client { def, initState.asset, initState.userWallet, - this.nodeAddress + this.nodeAddress, ); const contractState = await coreStateToContractState(initState, (blockchainId, tokenAddress) => - this.assetStore.getTokenDecimals(blockchainId, tokenAddress) + this.assetStore.getTokenDecimals(blockchainId, tokenAddress), ); // Check allowance and balance for deposits @@ -380,7 +439,7 @@ export class Client { contractAddress: this.contractAddress, blockchainId: this.blockchainId.toString(), walletChain: this.walletSigner.chain?.id, - walletChainName: this.walletSigner.chain?.name + walletChainName: this.walletSigner.chain?.name, }); // Resolve native ETH value for deposit intents @@ -441,7 +500,7 @@ export class Client { const contractCandidate = await coreStateToContractState( candidate, - (blockchainId, tokenAddress) => this.assetStore.getTokenDecimals(blockchainId, tokenAddress) + (blockchainId, tokenAddress) => this.assetStore.getTokenDecimals(blockchainId, tokenAddress), ); // Check for deposit intent @@ -460,15 +519,16 @@ export class Client { } } - const nativeValue = contractCandidate.homeLedger.token === zeroAddress - ? decimalToBigInt(candidate.transition.amount, contractCandidate.homeLedger.decimals) - : undefined; + const nativeValue = + contractCandidate.homeLedger.token === zeroAddress + ? decimalToBigInt(candidate.transition.amount, contractCandidate.homeLedger.decimals) + : undefined; console.log('💳 EVM Client - Deposit to channel transaction:', { contractAddress: this.contractAddress, blockchainId: this.blockchainId.toString(), channelId: channelIdBytes, - walletChain: this.walletSigner.chain?.id + walletChain: this.walletSigner.chain?.id, }); const hash = await this.walletSigner.writeContract({ @@ -490,7 +550,7 @@ export class Client { contractAddress: this.contractAddress, blockchainId: this.blockchainId.toString(), channelId: channelIdBytes, - walletChain: this.walletSigner.chain?.id + walletChain: this.walletSigner.chain?.id, }); const hash = await this.walletSigner.writeContract({ @@ -510,7 +570,7 @@ export class Client { contractAddress: this.contractAddress, blockchainId: this.blockchainId.toString(), channelId: channelIdBytes, - walletChain: this.walletSigner.chain?.id + walletChain: this.walletSigner.chain?.id, }); const hash = await this.walletSigner.writeContract({ @@ -525,7 +585,11 @@ export class Client { return hash; } - async challenge(candidate: core.State, challengerSig: `0x${string}`, challengerIdx: number = 0): Promise { + async challenge( + candidate: core.State, + challengerSig: `0x${string}`, + challengerIdx: number = 0, + ): Promise { if (!candidate.homeChannelId) { throw new Error('Candidate state must have a home channel ID'); } @@ -534,14 +598,14 @@ export class Client { const contractCandidate = await coreStateToContractState( candidate, - (blockchainId, tokenAddress) => this.assetStore.getTokenDecimals(blockchainId, tokenAddress) + (blockchainId, tokenAddress) => this.assetStore.getTokenDecimals(blockchainId, tokenAddress), ); console.log('💳 EVM Client - Challenge channel transaction:', { contractAddress: this.contractAddress, blockchainId: this.blockchainId.toString(), channelId: channelIdBytes, - walletChain: this.walletSigner.chain?.id + walletChain: this.walletSigner.chain?.id, }); const hash = await this.walletSigner.writeContract({ @@ -565,7 +629,7 @@ export class Client { const contractCandidate = await coreStateToContractState( candidate, - (blockchainId, tokenAddress) => this.assetStore.getTokenDecimals(blockchainId, tokenAddress) + (blockchainId, tokenAddress) => this.assetStore.getTokenDecimals(blockchainId, tokenAddress), ); // Verify close intent @@ -578,7 +642,7 @@ export class Client { blockchainId: this.blockchainId.toString(), channelId: channelIdBytes, walletChain: this.walletSigner.chain?.id, - walletChainName: this.walletSigner.chain?.name + walletChainName: this.walletSigner.chain?.name, }); const hash = await this.walletSigner.writeContract({ @@ -596,14 +660,17 @@ export class Client { // ========= Escrow Operations ========= // Note: These would need the full escrow methods in the ABI - async initiateEscrowDeposit(_def: core.ChannelDefinition, _initState: core.State): Promise { + async initiateEscrowDeposit( + _def: core.ChannelDefinition, + _initState: core.State, + ): Promise { throw new Error('initiateEscrowDeposit not implemented - needs contract ABI update'); } async challengeEscrowDeposit( _candidate: core.State, _challengerSig: `0x${string}`, - _challengerIdx: number = 0 + _challengerIdx: number = 0, ): Promise { throw new Error('challengeEscrowDeposit not implemented - needs contract ABI update'); } @@ -614,7 +681,7 @@ export class Client { async initiateEscrowWithdrawal( _def: core.ChannelDefinition, - _initState: core.State + _initState: core.State, ): Promise { throw new Error('initiateEscrowWithdrawal not implemented - needs contract ABI update'); } @@ -622,7 +689,7 @@ export class Client { async challengeEscrowWithdrawal( _candidate: core.State, _challengerSig: `0x${string}`, - _challengerIdx: number = 0 + _challengerIdx: number = 0, ): Promise { throw new Error('challengeEscrowWithdrawal not implemented - needs contract ABI update'); } @@ -646,7 +713,7 @@ export function newClient( blockchainId: bigint, nodeAddress: Address, assetStore: AssetStore, - options?: ClientOptions + options?: ClientOptions, ): Client { return new Client( contractAddress, @@ -655,6 +722,6 @@ export function newClient( blockchainId, nodeAddress, assetStore, - options + options, ); } diff --git a/sdk/ts/src/blockchain/evm/erc20.ts b/sdk/ts/src/blockchain/evm/erc20.ts index 5037ec5b4..a7daa3232 100644 --- a/sdk/ts/src/blockchain/evm/erc20.ts +++ b/sdk/ts/src/blockchain/evm/erc20.ts @@ -10,9 +10,9 @@ import { EVMClient, WalletSigner } from './interface'; * ERC20 contract wrapper for token interactions */ export class ERC20 { - private tokenAddress: Address; - private client: EVMClient; - private walletSigner?: WalletSigner; + protected tokenAddress: Address; + protected client: EVMClient; + protected walletSigner?: WalletSigner; constructor(tokenAddress: Address, client: EVMClient, walletSigner?: WalletSigner) { this.tokenAddress = tokenAddress; @@ -52,7 +52,6 @@ export class ERC20 { throw new Error('Wallet signer is required for approve operation'); } - try { const { request } = (await this.client.simulateContract({ address: this.tokenAddress, @@ -66,7 +65,6 @@ export class ERC20 { await this.client.waitForTransactionReceipt({ hash }); - return hash; } catch (error: any) { console.error('❌ Approve simulation/execution failed!'); diff --git a/sdk/ts/src/blockchain/evm/locking_client.ts b/sdk/ts/src/blockchain/evm/locking_client.ts index ae630779d..974efa33e 100644 --- a/sdk/ts/src/blockchain/evm/locking_client.ts +++ b/sdk/ts/src/blockchain/evm/locking_client.ts @@ -22,11 +22,7 @@ export class LockingClient { private tokenAddress?: Address; private tokenDecimals?: number; - constructor( - contractAddress: Address, - evmClient: EVMClient, - walletSigner?: WalletSigner, - ) { + constructor(contractAddress: Address, evmClient: EVMClient, walletSigner?: WalletSigner) { this.contractAddress = contractAddress; this.evmClient = evmClient; this.walletSigner = walletSigner; @@ -47,17 +43,17 @@ export class LockingClient { return { address: this.tokenAddress, decimals: this.tokenDecimals }; } - const tokenAddress = await this.evmClient.readContract({ + const tokenAddress = (await this.evmClient.readContract({ address: this.contractAddress, abi: AppRegistryAbi, functionName: 'asset', - }) as Address; + })) as Address; - const decimals = await this.evmClient.readContract({ + const decimals = (await this.evmClient.readContract({ address: tokenAddress, abi: Erc20Abi, functionName: 'decimals', - }) as number; + })) as number; this.tokenAddress = tokenAddress; this.tokenDecimals = decimals; @@ -185,12 +181,12 @@ export class LockingClient { async getBalance(user: Address): Promise { const { decimals } = await this.ensureTokenInfo(); - const balance = await this.evmClient.readContract({ + const balance = (await this.evmClient.readContract({ address: this.contractAddress, abi: AppRegistryAbi, functionName: 'balanceOf', args: [user], - }) as bigint; + })) as bigint; return new Decimal(balance.toString()).div(Decimal.pow(10, decimals)); } @@ -203,12 +199,12 @@ export class LockingClient { * @returns Lock state (0=None, 1=Locked, 2=Unlocking) */ async getLockState(user: Address): Promise { - return await this.evmClient.readContract({ + return (await this.evmClient.readContract({ address: this.contractAddress, abi: AppRegistryAbi, functionName: 'lockStateOf', args: [user], - }) as number; + })) as number; } /** diff --git a/sdk/ts/src/blockchain/evm/parametric.ts b/sdk/ts/src/blockchain/evm/parametric.ts new file mode 100644 index 000000000..335c854f9 --- /dev/null +++ b/sdk/ts/src/blockchain/evm/parametric.ts @@ -0,0 +1,292 @@ +/** + * Parametric ERC20 token contract wrapper with sub-account support + */ + +import { Address } from 'viem'; +import { ERC20 } from './erc20'; +import { ParametricTokenAbi } from './parametric_abi'; +import { EVMClient, WalletSigner } from './interface'; + +/** + * Account type enum matching Solidity + */ +export enum AccountType { + Normal = 0, + Super = 1, +} + +/** + * Parametric ERC20 contract wrapper with sub-account methods + */ +export class ParametricToken extends ERC20 { + constructor(tokenAddress: Address, client: EVMClient, walletSigner?: WalletSigner) { + super(tokenAddress, client, walletSigner); // Pass to parent + } + + /** + * Check if an address is a super account + */ + async getAccountType(account: Address): Promise { + try { + const result = (await this.client.readContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'accountType', + args: [account], + })) as number; + return result as AccountType; + } catch { + return AccountType.Normal; // Default to normal if not parametric + } + } + + /** + * Check if an address is a super account (convenience method) + */ + async isSuperAccount(account: Address): Promise { + return (await this.getAccountType(account)) === AccountType.Super; + } + + /** + * Convert a normal account to super account + */ + async convertToSuper(account: Address): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + if (!this.walletSigner.account?.address) { + throw new Error('Wallet signer account not configured'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'convertToSuper', + args: [account], + account: this.walletSigner.account.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } + + /** + * Create a new sub-account for a super account + */ + async createSubAccount(account: Address): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request, result } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'createSubAccount', + args: [account], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + + return Number(result); // uint48 -> number + } + + /** + * Get balance of a specific sub-account + */ + async balanceOfSub(superAccount: Address, subId: number): Promise { + return this.client.readContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'balanceOfSub', + args: [superAccount, subId], + }) as Promise; + } + + /** + * Get number of sub-accounts for a super account + */ + async subsCountOf(superAccount: Address): Promise { + const result = await this.client.readContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'subsCountOf', + args: [superAccount], + }); + return Number(result); + } + + /** + * Get parameter for a specific sub-account + */ + async getSubParameter(superAccount: Address, subId: number): Promise<`0x${string}`> { + return this.client.readContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'getSubParameter', + args: [superAccount, subId], + }) as Promise<`0x${string}`>; + } + + /** + * Get allowance for a specific sub-account + */ + async allowanceForSub(owner: Address, subId: number, spender: Address): Promise { + return this.client.readContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'allowanceForSub', + args: [owner, subId, spender], + }) as Promise; + } + + /** + * Transfer from normal account to specific sub-account + */ + async transferToSub(toSuper: Address, toSubId: number, amount: bigint): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'transferToSub', + args: [toSuper, toSubId, amount], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } + + /** + * Transfer from specific sub-account to normal account + */ + async transferFromSub(fromSubId: number, to: Address, amount: bigint): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'transferFromSub', + args: [fromSubId, to, amount], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } + + /** + * Transfer between sub-accounts of the same super account + */ + async transferBetweenSubs(fromSubId: number, toSubId: number, amount: bigint): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'transferBetweenSubs', + args: [fromSubId, toSubId, amount], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } + + /** + * Approve spender for a specific sub-account + */ + async approveForSub(ownerSubId: number, spender: Address, amount: bigint): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'approveForSub', + args: [ownerSubId, spender, amount], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } + + /** + * Approved transfer from normal to sub-account + */ + async approvedTransferToSub( + from: Address, + toSuper: Address, + toSubId: number, + amount: bigint, + ): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'approvedTransferToSub', + args: [from, toSuper, toSubId, amount], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } + + /** + * Approved transfer from one sub-account to another + */ + async approvedTransferFromSubToSub( + fromSuper: Address, + fromSubId: number, + toSuper: Address, + toSubId: number, + amount: bigint, + ): Promise { + if (!this.walletSigner) { + throw new Error('Wallet signer required'); + } + + const { request } = await this.client.simulateContract({ + address: this.tokenAddress, + abi: ParametricTokenAbi, + functionName: 'approvedTransferFromSubToSub', + args: [fromSuper, fromSubId, toSuper, toSubId, amount], + account: this.walletSigner.account!.address, + }); + + const hash = await this.walletSigner.writeContract(request); + await this.client.waitForTransactionReceipt({ hash }); + return hash; + } +} + +/** + * Create a new parametric ERC20 contract instance + */ +export function newParametricToken( + tokenAddress: Address, + client: EVMClient, + walletSigner?: WalletSigner, +): ParametricToken { + return new ParametricToken(tokenAddress, client, walletSigner); +} diff --git a/sdk/ts/src/blockchain/evm/parametric_abi.ts b/sdk/ts/src/blockchain/evm/parametric_abi.ts new file mode 100644 index 000000000..7474cc24f --- /dev/null +++ b/sdk/ts/src/blockchain/evm/parametric_abi.ts @@ -0,0 +1,188 @@ +/** + * Parametric token contract ABI + * Parametric token interface + */ + +export const ParametricTokenAbi = [ + { + type: 'function', + name: 'convertToSuper', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'createSubAccount', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint48' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'accountType', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'balanceOfSub', + inputs: [ + { name: 'superAccount', type: 'address' }, + { name: 'subId', type: 'uint48' }, + ], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'subsCountOf', + inputs: [{ name: 'superAccount', type: 'address' }], + outputs: [{ name: '', type: 'uint48' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getSubParameter', + inputs: [ + { name: 'superAccount', type: 'address' }, + { name: 'subId', type: 'uint48' }, + ], + outputs: [{ name: '', type: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'allowanceForSub', + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'subId', type: 'uint48' }, + { name: 'spender', type: 'address' }, + ], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transferToSub', + inputs: [ + { name: 'toSuper', type: 'address' }, + { name: 'toSubId', type: 'uint48' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFromSub', + inputs: [ + { name: 'fromSubId', type: 'uint48' }, + { name: 'to', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferBetweenSubs', + inputs: [ + { name: 'fromSubId', type: 'uint48' }, + { name: 'toSubId', type: 'uint48' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'approveForSub', + inputs: [ + { name: 'ownerSubId', type: 'uint48' }, + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'approvedTransferToSub', + inputs: [ + { name: 'from', type: 'address' }, + { name: 'toSuper', type: 'address' }, + { name: 'toSubId', type: 'uint48' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'approvedTransferFromSubToSub', + inputs: [ + { name: 'fromSuper', type: 'address' }, + { name: 'fromSubId', type: 'uint48' }, + { name: 'toSuper', type: 'address' }, + { name: 'toSubId', type: 'uint48' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AccountConvertedToSuper', + inputs: [{ name: 'account', type: 'address', indexed: true }], + }, + { + type: 'event', + name: 'SubAccountCreated', + inputs: [ + { name: 'superAccount', type: 'address', indexed: true }, + { name: 'subId', type: 'uint48', indexed: true }, + ], + }, + { + type: 'event', + name: 'TransferToSub', + inputs: [ + { name: 'from', type: 'address', indexed: true }, + { name: 'toSuper', type: 'address', indexed: true }, + { name: 'toSubId', type: 'uint48', indexed: true }, + { name: 'amount', type: 'uint256', indexed: false }, + ], + }, + { + type: 'event', + name: 'TransferFromSub', + inputs: [ + { name: 'fromSuper', type: 'address', indexed: true }, + { name: 'fromSubId', type: 'uint48', indexed: true }, + { name: 'to', type: 'address', indexed: true }, + { name: 'amount', type: 'uint256', indexed: false }, + ], + }, + { + type: 'event', + name: 'TransferBetweenSubs', + inputs: [ + { name: 'superAccount', type: 'address', indexed: true }, + { name: 'fromSubId', type: 'uint48', indexed: true }, + { name: 'toSubId', type: 'uint48', indexed: true }, + { name: 'amount', type: 'uint256', indexed: false }, + ], + }, + { + type: 'event', + name: 'ApprovalForSub', + inputs: [ + { name: 'owner', type: 'address', indexed: true }, + { name: 'subId', type: 'uint48', indexed: true }, + { name: 'spender', type: 'address', indexed: true }, + { name: 'amount', type: 'uint256', indexed: false }, + ], + }, +] as const; diff --git a/sdk/ts/src/blockchain/evm/utils.ts b/sdk/ts/src/blockchain/evm/utils.ts index 2fcc44e56..d6b21fd0d 100644 --- a/sdk/ts/src/blockchain/evm/utils.ts +++ b/sdk/ts/src/blockchain/evm/utils.ts @@ -22,12 +22,7 @@ export function hexToBytes32(s: string): Uint8Array { /** * coreDefToContractDef converts a core channel definition to a contract channel definition */ -export function coreDefToContractDef( - def: core.ChannelDefinition, - asset: string, - userWallet: Address, - nodeAddress: Address -): ChannelDefinition { +export function coreDefToContractDef(def: core.ChannelDefinition, asset: string, userWallet: Address, nodeAddress: Address): ChannelDefinition { return { challengeDuration: def.challenge, user: userWallet, @@ -41,23 +36,14 @@ export function coreDefToContractDef( /** * coreStateToContractState converts a core state to a contract state */ -export async function coreStateToContractState( - state: core.State, - tokenGetter: (blockchainId: bigint, tokenAddress: Address) => Promise -): Promise { - const homeDecimals = await tokenGetter( - state.homeLedger.blockchainId, - state.homeLedger.tokenAddress - ); +export async function coreStateToContractState(state: core.State, tokenGetter: (blockchainId: bigint, tokenAddress: Address) => Promise): Promise { + const homeDecimals = await tokenGetter(state.homeLedger.blockchainId, state.homeLedger.tokenAddress); const homeLedger = coreLedgerToContractLedger(state.homeLedger, homeDecimals); let nonHomeLedger: Ledger; if (state.escrowLedger) { - const nonHomeDecimals = await tokenGetter( - state.escrowLedger.blockchainId, - state.escrowLedger.tokenAddress - ); + const nonHomeDecimals = await tokenGetter(state.escrowLedger.blockchainId, state.escrowLedger.tokenAddress); nonHomeLedger = coreLedgerToContractLedger(state.escrowLedger, nonHomeDecimals); } else { nonHomeLedger = { @@ -75,8 +61,8 @@ export async function coreStateToContractState( const metadata = getStateTransitionHash(state.transition) as `0x${string}`; - const userSig = state.userSig ? (state.userSig as `0x${string}`) : '0x' as `0x${string}`; - const nodeSig = state.nodeSig ? (state.nodeSig as `0x${string}`) : '0x' as `0x${string}`; + const userSig = state.userSig ? (state.userSig as `0x${string}`) : ('0x' as `0x${string}`); + const nodeSig = state.nodeSig ? (state.nodeSig as `0x${string}`) : ('0x' as `0x${string}`); return { version: state.version, @@ -112,11 +98,7 @@ export function coreLedgerToContractLedger(ledger: core.Ledger, decimals: number /** * contractStateToCoreState converts a contract state to a core state */ -export function contractStateToCoreState( - contractState: State, - homeChannelId: string, - escrowChannelId?: string -): core.State { +export function contractStateToCoreState(contractState: State, homeChannelId: string, escrowChannelId?: string): core.State { const homeLedger = contractLedgerToCoreLedger(contractState.homeLedger); let escrowLedger: core.Ledger | undefined; diff --git a/sdk/ts/src/client.ts b/sdk/ts/src/client.ts index 08bc35c30..0efea1c47 100644 --- a/sdk/ts/src/client.ts +++ b/sdk/ts/src/client.ts @@ -54,10 +54,7 @@ function stripSignerTypePrefix(sig: Hex): Hex { } const prefixByte = parseInt(sig.slice(2, 4), 16); if (prefixByte !== core.ChannelSignerType.Default) { - throw new Error( - `expected ChannelDefaultSigner prefix 0x00, got 0x${prefixByte.toString(16).padStart(2, '0')}; ` + - `session key signing requires the default wallet signer, not a session key signer` - ); + throw new Error(`expected ChannelDefaultSigner prefix 0x00, got 0x${prefixByte.toString(16).padStart(2, '0')}; ` + `session key signing requires the default wallet signer, not a session key signer`); } return `0x${sig.slice(4)}` as Hex; } @@ -109,13 +106,7 @@ export class Client { private txSigner: TransactionSigner; private assetStore: ClientAssetStore; - private constructor( - rpcClient: RPCClient, - config: Config, - stateSigner: StateSigner, - txSigner: TransactionSigner, - assetStore: ClientAssetStore - ) { + private constructor(rpcClient: RPCClient, config: Config, stateSigner: StateSigner, txSigner: TransactionSigner, assetStore: ClientAssetStore) { this.rpcClient = rpcClient; this.config = config; this.stateSigner = stateSigner; @@ -154,12 +145,7 @@ export class Client { * ); * ``` */ - static async create( - wsURL: string, - stateSigner: StateSigner, - txSigner: TransactionSigner, - ...opts: Option[] - ): Promise { + static async create(wsURL: string, stateSigner: StateSigner, txSigner: TransactionSigner, ...opts: Option[]): Promise { // Build config starting with defaults const config: Config = { url: wsURL, @@ -224,9 +210,7 @@ export class Client { async setHomeBlockchain(asset: string, blockchainId: bigint): Promise { const existingBlockchainId = this.homeBlockchains.get(asset); if (existingBlockchainId !== undefined) { - throw new Error( - `home blockchain is already set for asset ${asset} to ${existingBlockchainId}, please use Migrate() if you want to change home blockchain` - ); + throw new Error(`home blockchain is already set for asset ${asset} to ${existingBlockchainId}, please use Migrate() if you want to change home blockchain`); } const exists = await this.assetStore.assetExistsOnBlockchain(blockchainId, asset); @@ -779,8 +763,7 @@ export class Client { case core.TransitionType.TransferSend: case core.TransitionType.TransferReceive: case core.TransitionType.Commit: - case core.TransitionType.Release: - { + case core.TransitionType.Release: { if (channel.status === core.ChannelStatus.Void) { // Channel not yet created on-chain, reconstruct definition and call Create const channelDef: core.ChannelDefinition = { @@ -800,9 +783,7 @@ export class Client { } default: - throw new Error( - `transition type ${state.transition.type} does not require a blockchain operation` - ); + throw new Error(`transition type ${state.transition.type} does not require a blockchain operation`); } } @@ -895,18 +876,11 @@ export class Client { * @param owner - The owner address * @returns Current allowance amount (in smallest unit) */ - async checkTokenAllowance( - chainId: bigint, - tokenAddress: string, - owner: string - ): Promise { + async checkTokenAllowance(chainId: bigint, tokenAddress: string, owner: string): Promise { await this.initializeBlockchainClient(chainId); const blockchainClient = this.blockchainClients.get(chainId)!; - return await blockchainClient.checkAllowanceByAddress( - tokenAddress as `0x${string}`, - owner as `0x${string}` - ); + return await blockchainClient.checkAllowanceByAddress(tokenAddress as `0x${string}`, owner as `0x${string}`); } // ============================================================================ @@ -925,10 +899,7 @@ export class Client { */ async escrowSecurityTokens(targetWalletAddress: string, blockchainId: bigint, amount: Decimal): Promise { await this.initializeLockingClient(blockchainId); - return this.blockchainLockingClients.get(blockchainId)!.lock( - targetWalletAddress as Address, - amount, - ); + return this.blockchainLockingClients.get(blockchainId)!.lock(targetWalletAddress as Address, amount); } /** @@ -965,9 +936,7 @@ export class Client { */ async withdrawSecurityTokens(blockchainId: bigint, destinationWalletAddress: string): Promise { await this.initializeLockingClient(blockchainId); - return this.blockchainLockingClients.get(blockchainId)!.withdraw( - destinationWalletAddress as Address, - ); + return this.blockchainLockingClients.get(blockchainId)!.withdraw(destinationWalletAddress as Address); } /** @@ -1122,7 +1091,7 @@ export class Client { toTime?: bigint; page?: number; pageSize?: number; - } + }, ): Promise<{ transactions: core.Transaction[]; metadata: core.PaginationMetadata }> { const req: API.UserV1GetTransactionsRequest = { wallet, @@ -1130,10 +1099,13 @@ export class Client { tx_type: options?.txType, from_time: options?.fromTime, to_time: options?.toTime, - pagination: options?.page && options?.pageSize ? { - offset: (options.page - 1) * options.pageSize, - limit: options.pageSize, - } : undefined, + pagination: + options?.page && options?.pageSize + ? { + offset: (options.page - 1) * options.pageSize, + limit: options.pageSize, + } + : undefined, }; const resp = await this.rpcClient.userV1GetTransactions(req); return { @@ -1181,10 +1153,7 @@ export class Client { * } * ``` */ - async getChannels( - wallet: Address, - options?: { status?: string; asset?: string; channelType?: string; pagination?: core.PaginationParams } - ): Promise<{ channels: core.Channel[]; metadata: core.PaginationMetadata }> { + async getChannels(wallet: Address, options?: { status?: string; asset?: string; channelType?: string; pagination?: core.PaginationParams }): Promise<{ channels: core.Channel[]; metadata: core.PaginationMetadata }> { const req: API.ChannelsV1GetChannelsRequest = { wallet, status: options?.status, @@ -1290,21 +1259,18 @@ export class Client { * }); * ``` */ - async getAppSessions(options?: { - appSessionId?: string; - wallet?: Address; - status?: string; - page?: number; - pageSize?: number; - }): Promise<{ sessions: app.AppSessionInfoV1[]; metadata: core.PaginationMetadata }> { + async getAppSessions(options?: { appSessionId?: string; wallet?: Address; status?: string; page?: number; pageSize?: number }): Promise<{ sessions: app.AppSessionInfoV1[]; metadata: core.PaginationMetadata }> { const req: API.AppSessionsV1GetAppSessionsRequest = { app_session_id: options?.appSessionId, participant: options?.wallet, status: options?.status, - pagination: options?.page && options?.pageSize ? { - offset: (options.page - 1) * options.pageSize, - limit: options.pageSize, - } : undefined, + pagination: + options?.page && options?.pageSize + ? { + offset: (options.page - 1) * options.pageSize, + limit: options.pageSize, + } + : undefined, }; const resp = await this.rpcClient.appSessionsV1GetAppSessions(req); return { @@ -1360,12 +1326,7 @@ export class Client { * console.log('Created session:', appSessionId); * ``` */ - async createAppSession( - definition: app.AppDefinitionV1, - sessionData: string, - quorumSigs: string[], - opts?: { ownerSig?: string } - ): Promise<{ appSessionId: string; version: string; status: string }> { + async createAppSession(definition: app.AppDefinitionV1, sessionData: string, quorumSigs: string[], opts?: { ownerSig?: string }): Promise<{ appSessionId: string; version: string; status: string }> { const req: API.AppSessionsV1CreateAppSessionRequest = { definition: transformAppDefinitionToRPC(definition) as any, // RPC type session_data: sessionData, @@ -1411,12 +1372,7 @@ export class Client { * ); * ``` */ - async submitAppSessionDeposit( - appStateUpdate: app.AppStateUpdateV1, - quorumSigs: string[], - asset: string, - depositAmount: Decimal - ): Promise { + async submitAppSessionDeposit(appStateUpdate: app.AppStateUpdateV1, quorumSigs: string[], asset: string, depositAmount: Decimal): Promise { // Get current state const currentState = await this.getLatestState(this.getUserAddress(), asset, false); @@ -1465,10 +1421,7 @@ export class Client { * await client.submitAppState(appUpdate, ['sig1', 'sig2']); * ``` */ - async submitAppState( - appStateUpdate: app.AppStateUpdateV1, - quorumSigs: string[] - ): Promise { + async submitAppState(appStateUpdate: app.AppStateUpdateV1, quorumSigs: string[]): Promise { const appUpdate = transformAppStateUpdateToRPC(appStateUpdate); const req: API.AppSessionsV1SubmitAppStateRequest = { @@ -1504,9 +1457,7 @@ export class Client { * console.log('Rebalance batch ID:', batchId); * ``` */ - async rebalanceAppSessions( - signedUpdates: app.SignedAppStateUpdateV1[] - ): Promise { + async rebalanceAppSessions(signedUpdates: app.SignedAppStateUpdateV1[]): Promise { // Transform SDK types to RPC types const rpcUpdates = signedUpdates.map(transformSignedAppStateUpdateToRPC); @@ -1536,19 +1487,17 @@ export class Client { * } * ``` */ - async getApps(options?: { - appId?: string; - ownerWallet?: string; - page?: number; - pageSize?: number; - }): Promise<{ apps: AppInfoV1[]; metadata: core.PaginationMetadata }> { + async getApps(options?: { appId?: string; ownerWallet?: string; page?: number; pageSize?: number }): Promise<{ apps: AppInfoV1[]; metadata: core.PaginationMetadata }> { const req: API.AppsV1GetAppsRequest = { app_id: options?.appId, owner_wallet: options?.ownerWallet, - pagination: options?.page && options?.pageSize ? { - offset: (options.page - 1) * options.pageSize, - limit: options.pageSize, - } : undefined, + pagination: + options?.page && options?.pageSize + ? { + offset: (options.page - 1) * options.pageSize, + limit: options.pageSize, + } + : undefined, }; const resp = await this.rpcClient.appsV1GetApps(req); return { @@ -1612,15 +1561,8 @@ export class Client { * @returns The hex-encoded signature string */ async signChannelSessionKeyState(state: ChannelSessionKeyStateV1): Promise { - const metadataHash = core.getChannelSessionKeyAuthMetadataHashV1( - BigInt(state.version), - state.assets, - BigInt(state.expires_at) - ); - const packed = core.packChannelKeyStateV1( - state.session_key as Address, - metadataHash - ); + const metadataHash = core.getChannelSessionKeyAuthMetadataHashV1(BigInt(state.version), state.assets, BigInt(state.expires_at)); + const packed = core.packChannelKeyStateV1(state.session_key as Address, metadataHash); const channelSig = await this.stateSigner.signMessage(packed); return stripSignerTypePrefix(channelSig); } @@ -1645,10 +1587,7 @@ export class Client { * @param sessionKey - Optional session key address to filter by * @returns List of active channel session key states */ - async getLastChannelKeyStates( - userAddress: string, - sessionKey?: string - ): Promise { + async getLastChannelKeyStates(userAddress: string, sessionKey?: string): Promise { const req: API.ChannelsV1GetLastKeyStatesRequest = { user_address: userAddress, session_key: sessionKey, @@ -1695,10 +1634,7 @@ export class Client { * @param sessionKey - Optional session key address to filter by * @returns List of active session key states */ - async getLastKeyStates( - userAddress: string, - sessionKey?: string - ): Promise { + async getLastKeyStates(userAddress: string, sessionKey?: string): Promise { const req: API.AppSessionsV1GetLastKeyStatesRequest = { user_address: userAddress, session_key: sessionKey, @@ -1718,9 +1654,7 @@ export class Client { private async getBlockchainRPCInfo(chainId: bigint): Promise<{ rpcUrl: string; blockchainInfo: core.Blockchain; config: core.NodeConfig }> { const rpcUrl = this.config.blockchainRPCs?.get(chainId); if (!rpcUrl) { - throw new Error( - `blockchain RPC not configured for chain ${chainId} (use withBlockchainRPC)` - ); + throw new Error(`blockchain RPC not configured for chain ${chainId} (use withBlockchainRPC)`); } const config = await this.getConfig(); @@ -1794,14 +1728,7 @@ export class Client { throw new Error('Node.js environment requires a TransactionSigner that implements getAccount() (e.g., EthereumRawSigner)'); } - const blockchainClient = new blockchain.evm.Client( - blockchainInfo.channelHubAddress, - publicClient, - walletClient, - chainId, - config.nodeAddress, - this.assetStore - ); + const blockchainClient = new blockchain.evm.Client(blockchainInfo.channelHubAddress, publicClient, walletClient, chainId, config.nodeAddress, this.assetStore); this.blockchainClients.set(chainId, blockchainClient); } @@ -1823,11 +1750,7 @@ export class Client { const { publicClient, walletClient } = this.createEVMClients(chainId, rpcUrl); - const lockingClient = new blockchain.evm.LockingClient( - blockchainInfo.lockingContractAddress, - publicClient, - walletClient || undefined, - ); + const lockingClient = new blockchain.evm.LockingClient(blockchainInfo.lockingContractAddress, publicClient, walletClient || undefined); this.blockchainLockingClients.set(chainId, lockingClient); } @@ -1885,10 +1808,7 @@ export class Client { * Request the node to co-sign a channel creation state. * Used when creating a new channel (via deposit, withdraw, transfer, or acknowledge). */ - private async requestChannelCreation( - state: core.State, - channelDef: core.ChannelDefinition - ): Promise { + private async requestChannelCreation(state: core.State, channelDef: core.ChannelDefinition): Promise { const req: API.ChannelsV1RequestCreationRequest = { state: this.transformStateToRPC(state), channel_definition: this.transformChannelDefinitionToRPC(channelDef), diff --git a/sdk/ts/src/core/interface.ts b/sdk/ts/src/core/interface.ts index edb47a9f1..44ff7ed03 100644 --- a/sdk/ts/src/core/interface.ts +++ b/sdk/ts/src/core/interface.ts @@ -53,6 +53,19 @@ export interface Client { */ getAccountsBalances(accounts: Address[], tokens: Address[]): Promise; + /** + * Get parametric token sub balances for multiple accounts and subIds + * @param accounts - Array of account addresses + * @param token - Parametric token address + * @param subIds - Array of subaccount IDs + * @returns 2D array of balances [account][token] + */ + getAccountsSubBalances( + accounts: Address[], + token: Address, + subIds: number[], + ): Promise; + // ========= Getters - Token Balance & Approval ========= /** @@ -75,8 +88,9 @@ export interface Client { /** * Get the node's balance for a specific token * @param token - Token address + * @param subId - Subaccount ID */ - getNodeBalance(token: Address): Promise; + getNodeBalance(token: Address, subId: number): Promise; /** * Get all open channel IDs for a user @@ -183,7 +197,7 @@ export interface Client { challengeEscrowDeposit( candidate: State, challengerSig: Uint8Array, - challengerIdx: number + challengerIdx: number, ): Promise; /** @@ -213,7 +227,7 @@ export interface Client { challengeEscrowWithdrawal( candidate: State, challengerSig: Uint8Array, - challengerIdx: number + challengerIdx: number, ): Promise; /** diff --git a/sdk/ts/src/core/state.ts b/sdk/ts/src/core/state.ts index 8cda58835..23e6f1dc0 100644 --- a/sdk/ts/src/core/state.ts +++ b/sdk/ts/src/core/state.ts @@ -1,13 +1,6 @@ import { Address } from 'viem'; import Decimal from 'decimal.js'; -import { - State, - Ledger, - Transition, - TransitionType, - ChannelDefinition, - newTransition, -} from './types'; +import { State, Ledger, Transition, TransitionType, ChannelDefinition, newTransition } from './types'; import { getHomeChannelId, getEscrowChannelId, getStateId, getSenderTransactionId, getReceiverTransactionId } from './utils'; // ============================================================================ @@ -25,10 +18,7 @@ export function getLastTransition(state: State): Transition | null { return null; } - if ( - state.transition.type === TransitionType.TransferReceive || - state.transition.type === TransitionType.Release - ) { + if (state.transition.type === TransitionType.TransferReceive || state.transition.type === TransitionType.Release) { return null; } @@ -103,10 +93,7 @@ export function nextState(state: State): State { // Copy transition if user hasn't signed yet newState.transition = { ...state.transition }; } else { - if ( - state.transition.type === TransitionType.EscrowDeposit || - state.transition.type === TransitionType.EscrowWithdraw - ) { + if (state.transition.type === TransitionType.EscrowDeposit || state.transition.type === TransitionType.EscrowWithdraw) { // Clear escrow channel and ledger after escrow operations complete newState.escrowChannelId = undefined; newState.escrowLedger = undefined; @@ -133,26 +120,13 @@ export function nextState(state: State): State { * @param nodeAddress - Node address * @returns Home channel ID */ -export function applyChannelCreation( - state: State, - channelDef: ChannelDefinition, - blockchainId: bigint, - tokenAddress: Address, - nodeAddress: Address -): string { +export function applyChannelCreation(state: State, channelDef: ChannelDefinition, blockchainId: bigint, tokenAddress: Address, nodeAddress: Address): string { // Set home ledger state.homeLedger.tokenAddress = tokenAddress; state.homeLedger.blockchainId = blockchainId; // Calculate home channel ID - const homeChannelId = getHomeChannelId( - nodeAddress, - state.userWallet, - state.asset, - channelDef.nonce, - channelDef.challenge, - channelDef.approvedSigValidators - ); + const homeChannelId = getHomeChannelId(nodeAddress, state.userWallet, state.asset, channelDef.nonce, channelDef.challenge, channelDef.approvedSigValidators); state.homeChannelId = homeChannelId; @@ -259,11 +233,7 @@ export function applyHomeWithdrawalTransition(state: State, amount: Decimal): Tr * @param amount - Amount to send * @returns The created transition */ -export function applyTransferSendTransition( - state: State, - recipient: string, - amount: Decimal -): Transition { +export function applyTransferSendTransition(state: State, recipient: string, amount: Decimal): Transition { const accountId = recipient; const txId = getSenderTransactionId(accountId, state.id); @@ -283,12 +253,7 @@ export function applyTransferSendTransition( * @param txId - Transaction ID * @returns The created transition */ -export function applyTransferReceiveTransition( - state: State, - sender: string, - amount: Decimal, - txId: string -): Transition { +export function applyTransferReceiveTransition(state: State, sender: string, amount: Decimal, txId: string): Transition { const accountId = sender; const newTransitionObj = newTransition(TransitionType.TransferReceive, txId, accountId, amount); @@ -343,12 +308,7 @@ export function applyReleaseTransition(state: State, accountId: string, amount: * @param amount - Amount to lock * @returns The created transition */ -export function applyMutualLockTransition( - state: State, - blockchainId: bigint, - tokenAddress: Address, - amount: Decimal -): Transition { +export function applyMutualLockTransition(state: State, blockchainId: bigint, tokenAddress: Address, amount: Decimal): Transition { if (!state.homeChannelId) { throw new Error('missing home channel ID'); } @@ -420,12 +380,7 @@ export function applyEscrowDepositTransition(state: State, amount: Decimal): Tra * @param amount - Amount to lock for withdrawal * @returns The created transition */ -export function applyEscrowLockTransition( - state: State, - blockchainId: bigint, - tokenAddress: Address, - amount: Decimal -): Transition { +export function applyEscrowLockTransition(state: State, blockchainId: bigint, tokenAddress: Address, amount: Decimal): Transition { if (!state.homeChannelId) { throw new Error('missing home channel ID'); } diff --git a/sdk/ts/src/core/state_packer.ts b/sdk/ts/src/core/state_packer.ts index 309779c70..9143889ed 100644 --- a/sdk/ts/src/core/state_packer.ts +++ b/sdk/ts/src/core/state_packer.ts @@ -39,10 +39,7 @@ export class StatePackerV1 implements StatePacker { const channelId = state.homeChannelId as `0x${string}`; const metadata = getStateTransitionHash(state.transition); - const homeDecimals = await this.assetStore.getTokenDecimals( - state.homeLedger.blockchainId, - state.homeLedger.tokenAddress - ); + const homeDecimals = await this.assetStore.getTokenDecimals(state.homeLedger.blockchainId, state.homeLedger.tokenAddress); const homeLedger: ContractLedger = { chainId: state.homeLedger.blockchainId, @@ -57,10 +54,7 @@ export class StatePackerV1 implements StatePacker { let nonHomeLedger: ContractLedger; if (state.escrowLedger) { - const escrowDecimals = await this.assetStore.getTokenDecimals( - state.escrowLedger.blockchainId, - state.escrowLedger.tokenAddress - ); + const escrowDecimals = await this.assetStore.getTokenDecimals(state.escrowLedger.blockchainId, state.escrowLedger.tokenAddress); nonHomeLedger = { chainId: state.escrowLedger.blockchainId, @@ -96,20 +90,8 @@ export class StatePackerV1 implements StatePacker { ] as const; const signingData = encodeAbiParameters( - [ - { type: 'uint64' }, - { type: 'uint8' }, - { type: 'bytes32' }, - { type: 'tuple', components: ledgerComponents }, - { type: 'tuple', components: ledgerComponents }, - ], - [ - state.version, - intent, - metadata as `0x${string}`, - homeLedger, - nonHomeLedger, - ] + [{ type: 'uint64' }, { type: 'uint8' }, { type: 'bytes32' }, { type: 'tuple', components: ledgerComponents }, { type: 'tuple', components: ledgerComponents }], + [state.version, intent, metadata as `0x${string}`, homeLedger, nonHomeLedger], ); return { channelId, signingData }; @@ -119,13 +101,7 @@ export class StatePackerV1 implements StatePacker { * Wraps signing data with channelId: abi.encode(channelId, signingData) */ private packWithChannelId(channelId: `0x${string}`, signingData: Hex): `0x${string}` { - return encodeAbiParameters( - [ - { type: 'bytes32' }, - { type: 'bytes' }, - ], - [channelId, signingData] - ); + return encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes' }], [channelId, signingData]); } /** diff --git a/sdk/ts/src/core/types.ts b/sdk/ts/src/core/types.ts index 686632d27..e0d70b696 100644 --- a/sdk/ts/src/core/types.ts +++ b/sdk/ts/src/core/types.ts @@ -100,6 +100,8 @@ export interface Channel { approvedSigValidators: string; // Hex string bitmap of approved signature validators status: ChannelStatus; // Current status of the channel (void, open, challenged, closed) stateVersion: bigint; // uint64 - On-chain state version of the channel + subId?: number; // uint48 - Optional sub-account ID for parametric tokens + isParametric?: boolean; // Whether this channel uses a parametric token } /** @@ -253,6 +255,7 @@ export interface HomeChannelDataResponse { node: Address; lastState: State; challengeExpiry: bigint; // uint64 + subId: number; // uint48 } export interface EscrowDepositDataResponse { @@ -286,7 +289,8 @@ export function newChannel( tokenAddress: Address, nonce: bigint, challenge: number, - approvedSigValidators: string = '0x00' + approvedSigValidators: string = '0x00', + subId?: number, ): Channel { return { channelId, @@ -300,6 +304,7 @@ export function newChannel( approvedSigValidators, status: ChannelStatus.Void, stateVersion: 0n, + subId, }; } @@ -333,7 +338,7 @@ export function newTransition( type: TransitionType, txId: string, accountId: string, - amount: Decimal + amount: Decimal, ): Transition { return { type, @@ -352,7 +357,7 @@ export function newTransaction( txType: TransactionType, fromAccount: Address, toAccount: Address, - amount: Decimal + amount: Decimal, ): Transaction { return { id, @@ -468,7 +473,7 @@ export function validateLedger(ledger: Ledger): void { const sumNetFlows = ledger.userNetFlow.add(ledger.nodeNetFlow); if (!sumBalances.equals(sumNetFlows)) { throw new Error( - `ledger balances do not match net flows: balances=${sumBalances.toString()}, net_flows=${sumNetFlows.toString()}` + `ledger balances do not match net flows: balances=${sumBalances.toString()}, net_flows=${sumNetFlows.toString()}`, ); } } @@ -480,7 +485,7 @@ export function validateLedger(ledger: Ledger): void { export function getOffsetAndLimit( params: PaginationParams | undefined, defaultLimit: number, - maxLimit: number + maxLimit: number, ): { offset: number; limit: number } { if (!params) { return { offset: 0, limit: defaultLimit }; diff --git a/sdk/ts/src/core/utils.ts b/sdk/ts/src/core/utils.ts index f9c4994fb..f67464a51 100644 --- a/sdk/ts/src/core/utils.ts +++ b/sdk/ts/src/core/utils.ts @@ -76,9 +76,7 @@ export function transitionToIntent(transition: Transition): number { export function validateDecimalPrecision(amount: Decimal, maxDecimals: number): void { const exponent = amount.decimalPlaces(); if (exponent > maxDecimals) { - throw new Error( - `amount exceeds maximum decimal precision: max ${maxDecimals} decimals allowed, got ${exponent}` - ); + throw new Error(`amount exceeds maximum decimal precision: max ${maxDecimals} decimals allowed, got ${exponent}`); } } @@ -100,9 +98,7 @@ export function decimalToBigInt(amount: Decimal, decimals: number): bigint { // Check if it's an integer if (!scaled.isInteger()) { - throw new Error( - `amount ${amount.toString()} exceeds maximum decimal precision: max ${decimals} decimals allowed` - ); + throw new Error(`amount ${amount.toString()} exceeds maximum decimal precision: max ${decimals} decimals allowed`); } // Convert to bigint @@ -124,14 +120,7 @@ export function decimalToBigInt(amount: Decimal, decimals: number): bigint { * @param challengeDuration - Challenge period in seconds (uint32) * @returns Channel ID as hex string */ -export function getHomeChannelId( - node: Address, - user: Address, - asset: string, - nonce: bigint, - challengeDuration: number, - approvedSigValidators: string = '0x00' -): string { +export function getHomeChannelId(node: Address, user: Address, asset: string, nonce: bigint, challengeDuration: number, approvedSigValidators: string = '0x00'): string { // Generate metadata from asset const metadata = generateChannelMetadata(asset); @@ -171,7 +160,7 @@ export function getHomeChannelId( approvedSignatureValidators: validatorsBigInt, metadata: metadata, }, - ] + ], ); // Calculate base channelId @@ -191,10 +180,7 @@ export function getHomeChannelId( * @returns Escrow channel ID as hex string */ export function getEscrowChannelId(homeChannelId: string, stateVersion: bigint): string { - const packed = encodeAbiParameters( - [{ type: 'bytes32' }, { type: 'uint64' }], - [homeChannelId as `0x${string}`, stateVersion] - ); + const packed = encodeAbiParameters([{ type: 'bytes32' }, { type: 'uint64' }], [homeChannelId as `0x${string}`, stateVersion]); return keccak256(packed); } @@ -211,16 +197,8 @@ export function getEscrowChannelId(homeChannelId: string, stateVersion: bigint): * @param version - State version (uint64) * @returns State ID as hex string */ -export function getStateId( - userWallet: Address, - asset: string, - epoch: bigint, - version: bigint -): string { - const packed = encodeAbiParameters( - [{ type: 'address' }, { type: 'string' }, { type: 'uint256' }, { type: 'uint256' }], - [userWallet, asset, epoch, version] - ); +export function getStateId(userWallet: Address, asset: string, epoch: bigint, version: bigint): string { + const packed = encodeAbiParameters([{ type: 'address' }, { type: 'string' }, { type: 'uint256' }, { type: 'uint256' }], [userWallet, asset, epoch, version]); return keccak256(packed); } @@ -250,7 +228,7 @@ export function getStateTransitionHash(transition: Transition): string { ], }, ], - [contractTransition] + [contractTransition], ); return keccak256(packed); @@ -280,10 +258,7 @@ export function getReceiverTransactionId(fromAccount: string, receiverNewStateId } function getTransactionId(account: string, newStateId: string): string { - const packed = encodeAbiParameters( - [{ type: 'string' }, { type: 'bytes32' }], - [account, newStateId as `0x${string}`] - ); + const packed = encodeAbiParameters([{ type: 'string' }, { type: 'bytes32' }], [account, newStateId as `0x${string}`]); return keccak256(packed); } @@ -342,7 +317,7 @@ function hexToBytes32(hexStr: string): `0x${string}` { } // Convert to hex string and right-pad to 32 bytes (matches Go's BytesToHash) - const hexResult = bytes.map(b => b.toString(16).padStart(2, '0')).join(''); + const hexResult = bytes.map((b) => b.toString(16).padStart(2, '0')).join(''); return pad(`0x${hexResult}` as `0x${string}`, { dir: 'left', size: 32 }); } @@ -374,7 +349,7 @@ function parseAccountIdToBytes32(accountId: string | undefined): `0x${string}` { // Valid hex - pad accordingly if (hexLength === 40) { // Address - left-pad with zeros - return pad((`0x${cleaned}`) as Address, { size: 32 }); + return pad(`0x${cleaned}` as Address, { size: 32 }); } else { // Already 32-byte hash return `0x${cleaned}` as `0x${string}`; @@ -402,7 +377,7 @@ function parseAccountIdToBytes32(accountId: string | undefined): `0x${string}` { } // Convert to hex string and left-pad to 32 bytes (matches Go's behavior) - const hexResult = bytes.map(b => b.toString(16).padStart(2, '0')).join(''); + const hexResult = bytes.map((b) => b.toString(16).padStart(2, '0')).join(''); return pad(`0x${hexResult}` as `0x${string}`, { dir: 'left', size: 32 }); } @@ -415,18 +390,14 @@ function parseAccountIdToBytes32(accountId: string | undefined): `0x${string}` { * @param expiresAt - Unix timestamp in seconds when the session key expires * @returns Keccak256 hash of the ABI-encoded metadata */ -export function getChannelSessionKeyAuthMetadataHashV1( - version: bigint, - assets: string[], - expiresAt: bigint -): `0x${string}` { +export function getChannelSessionKeyAuthMetadataHashV1(version: bigint, assets: string[], expiresAt: bigint): `0x${string}` { const packed = encodeAbiParameters( [ - { type: 'uint64' }, // version + { type: 'uint64' }, // version { type: 'string[]' }, // assets - { type: 'uint64' }, // expires_at + { type: 'uint64' }, // expires_at ], - [version, assets, expiresAt] + [version, assets, expiresAt], ); return keccak256(packed); } @@ -439,15 +410,12 @@ export function getChannelSessionKeyAuthMetadataHashV1( * @param metadataHash - The metadata hash from getChannelSessionKeyAuthMetadataHashV1 * @returns ABI-encoded (sessionKey, metadataHash) ready for EIP-191 signing */ -export function packChannelKeyStateV1( - sessionKey: Address, - metadataHash: `0x${string}` -): `0x${string}` { +export function packChannelKeyStateV1(sessionKey: Address, metadataHash: `0x${string}`): `0x${string}` { return encodeAbiParameters( [ - { type: 'address' }, // session_key - { type: 'bytes32' }, // hashed metadata + { type: 'address' }, // session_key + { type: 'bytes32' }, // hashed metadata ], - [sessionKey, metadataHash] + [sessionKey, metadataHash], ); } diff --git a/sdk/ts/src/index.ts b/sdk/ts/src/index.ts index 714885825..19ff85589 100644 --- a/sdk/ts/src/index.ts +++ b/sdk/ts/src/index.ts @@ -6,25 +6,10 @@ export { Client, DEFAULT_CHALLENGE_PERIOD, type StateSigner, type TransactionSigner } from './client'; // Export signers -export { - EthereumMsgSigner, - EthereumRawSigner, - ChannelDefaultSigner, - ChannelSessionKeyStateSigner, - AppSessionWalletSignerV1, - AppSessionKeySignerV1, - createSigners, -} from './signers'; +export { EthereumMsgSigner, EthereumRawSigner, ChannelDefaultSigner, ChannelSessionKeyStateSigner, AppSessionWalletSignerV1, AppSessionKeySignerV1, createSigners } from './signers'; // Export configuration -export { - type Config, - DefaultConfig, - type Option, - withHandshakeTimeout, - withErrorHandler, - withBlockchainRPC -} from './config'; +export { type Config, DefaultConfig, type Option, withHandshakeTimeout, withErrorHandler, withBlockchainRPC } from './config'; // Export asset store export { ClientAssetStore } from './asset_store'; diff --git a/sdk/ts/src/rpc/api.ts b/sdk/ts/src/rpc/api.ts index 0e2402225..88eabd135 100644 --- a/sdk/ts/src/rpc/api.ts +++ b/sdk/ts/src/rpc/api.ts @@ -5,29 +5,8 @@ */ import { Address } from 'viem'; -import { - ChannelV1, - ChannelDefinitionV1, - ChannelSessionKeyStateV1, - StateV1, - BalanceEntryV1, - TransactionV1, - PaginationParamsV1, - PaginationMetadataV1, - AssetV1, - BlockchainInfoV1, - AppV1, - AppInfoV1, - ActionAllowanceV1, -} from './types'; -import { - AppDefinitionV1, - AppStateUpdateV1, - AppSessionInfoV1, - AppAllocationV1, - AppSessionKeyStateV1, - SignedAppStateUpdateV1, -} from '../app/types'; +import { ChannelV1, ChannelDefinitionV1, ChannelSessionKeyStateV1, StateV1, BalanceEntryV1, TransactionV1, PaginationParamsV1, PaginationMetadataV1, AssetV1, BlockchainInfoV1, AppV1, AppInfoV1, ActionAllowanceV1 } from './types'; +import { AppDefinitionV1, AppStateUpdateV1, AppSessionInfoV1, AppAllocationV1, AppSessionKeyStateV1, SignedAppStateUpdateV1 } from '../app/types'; import { TransactionType } from '../core/types'; // ============================================================================ diff --git a/sdk/ts/src/rpc/client.ts b/sdk/ts/src/rpc/client.ts index 31823f87c..aae094100 100644 --- a/sdk/ts/src/rpc/client.ts +++ b/sdk/ts/src/rpc/client.ts @@ -31,11 +31,7 @@ export class RPCClient { * Call sends an RPC request with the specified method and parameters. * Returns the response payload or throws an error if the RPC call fails. */ - private async call( - method: string, - req: TReq, - signal?: AbortSignal - ): Promise { + private async call(method: string, req: TReq, signal?: AbortSignal): Promise { // Generate unique request ID const requestId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); @@ -60,52 +56,31 @@ export class RPCClient { // Channels Group - V1 API Methods // ============================================================================ - async channelsV1GetHomeChannel( - req: API.ChannelsV1GetHomeChannelRequest, - signal?: AbortSignal - ): Promise { + async channelsV1GetHomeChannel(req: API.ChannelsV1GetHomeChannelRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1GetHomeChannelMethod, req, signal); } - async channelsV1GetEscrowChannel( - req: API.ChannelsV1GetEscrowChannelRequest, - signal?: AbortSignal - ): Promise { + async channelsV1GetEscrowChannel(req: API.ChannelsV1GetEscrowChannelRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1GetEscrowChannelMethod, req, signal); } - async channelsV1GetChannels( - req: API.ChannelsV1GetChannelsRequest, - signal?: AbortSignal - ): Promise { + async channelsV1GetChannels(req: API.ChannelsV1GetChannelsRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1GetChannelsMethod, req, signal); } - async channelsV1GetLatestState( - req: API.ChannelsV1GetLatestStateRequest, - signal?: AbortSignal - ): Promise { + async channelsV1GetLatestState(req: API.ChannelsV1GetLatestStateRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1GetLatestStateMethod, req, signal); } - async channelsV1GetStates( - req: API.ChannelsV1GetStatesRequest, - signal?: AbortSignal - ): Promise { + async channelsV1GetStates(req: API.ChannelsV1GetStatesRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1GetStatesMethod, req, signal); } - async channelsV1RequestCreation( - req: API.ChannelsV1RequestCreationRequest, - signal?: AbortSignal - ): Promise { + async channelsV1RequestCreation(req: API.ChannelsV1RequestCreationRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1RequestCreationMethod, req, signal); } - async channelsV1SubmitState( - req: API.ChannelsV1SubmitStateRequest, - signal?: AbortSignal - ): Promise { + async channelsV1SubmitState(req: API.ChannelsV1SubmitStateRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1SubmitStateMethod, req, signal); } @@ -113,17 +88,11 @@ export class RPCClient { // Channel Session Key State - V1 API Methods // ============================================================================ - async channelsV1SubmitSessionKeyState( - req: API.ChannelsV1SubmitSessionKeyStateRequest, - signal?: AbortSignal - ): Promise { + async channelsV1SubmitSessionKeyState(req: API.ChannelsV1SubmitSessionKeyStateRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1SubmitSessionKeyStateMethod, req, signal); } - async channelsV1GetLastKeyStates( - req: API.ChannelsV1GetLastKeyStatesRequest, - signal?: AbortSignal - ): Promise { + async channelsV1GetLastKeyStates(req: API.ChannelsV1GetLastKeyStatesRequest, signal?: AbortSignal): Promise { return this.call(Methods.ChannelsV1GetLastKeyStatesMethod, req, signal); } @@ -131,52 +100,31 @@ export class RPCClient { // App Sessions Group - V1 API Methods // ============================================================================ - async appSessionsV1SubmitDepositState( - req: API.AppSessionsV1SubmitDepositStateRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1SubmitDepositState(req: API.AppSessionsV1SubmitDepositStateRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1SubmitDepositStateMethod, req, signal); } - async appSessionsV1SubmitAppState( - req: API.AppSessionsV1SubmitAppStateRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1SubmitAppState(req: API.AppSessionsV1SubmitAppStateRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1SubmitAppStateMethod, req, signal); } - async appSessionsV1RebalanceAppSessions( - req: API.AppSessionsV1RebalanceAppSessionsRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1RebalanceAppSessions(req: API.AppSessionsV1RebalanceAppSessionsRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1RebalanceAppSessionsMethod, req, signal); } - async appSessionsV1GetAppDefinition( - req: API.AppSessionsV1GetAppDefinitionRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1GetAppDefinition(req: API.AppSessionsV1GetAppDefinitionRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1GetAppDefinitionMethod, req, signal); } - async appSessionsV1GetAppSessions( - req: API.AppSessionsV1GetAppSessionsRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1GetAppSessions(req: API.AppSessionsV1GetAppSessionsRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1GetAppSessionsMethod, req, signal); } - async appSessionsV1CreateAppSession( - req: API.AppSessionsV1CreateAppSessionRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1CreateAppSession(req: API.AppSessionsV1CreateAppSessionRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1CreateAppSessionMethod, req, signal); } - async appSessionsV1CloseAppSession( - req: API.AppSessionsV1CloseAppSessionRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1CloseAppSession(req: API.AppSessionsV1CloseAppSessionRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1CloseAppSessionMethod, req, signal); } @@ -184,17 +132,11 @@ export class RPCClient { // App Session Key State - V1 API Methods // ============================================================================ - async appSessionsV1SubmitSessionKeyState( - req: API.AppSessionsV1SubmitSessionKeyStateRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1SubmitSessionKeyState(req: API.AppSessionsV1SubmitSessionKeyStateRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1SubmitSessionKeyStateMethod, req, signal); } - async appSessionsV1GetLastKeyStates( - req: API.AppSessionsV1GetLastKeyStatesRequest, - signal?: AbortSignal - ): Promise { + async appSessionsV1GetLastKeyStates(req: API.AppSessionsV1GetLastKeyStatesRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppSessionsV1GetLastKeyStatesMethod, req, signal); } @@ -202,17 +144,11 @@ export class RPCClient { // Apps Group - V1 API Methods // ============================================================================ - async appsV1GetApps( - req: API.AppsV1GetAppsRequest, - signal?: AbortSignal - ): Promise { + async appsV1GetApps(req: API.AppsV1GetAppsRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppsV1GetAppsMethod, req, signal); } - async appsV1SubmitAppVersion( - req: API.AppsV1SubmitAppVersionRequest, - signal?: AbortSignal - ): Promise { + async appsV1SubmitAppVersion(req: API.AppsV1SubmitAppVersionRequest, signal?: AbortSignal): Promise { return this.call(Methods.AppsV1SubmitAppVersionMethod, req, signal); } @@ -220,24 +156,15 @@ export class RPCClient { // User Group - V1 API Methods // ============================================================================ - async userV1GetBalances( - req: API.UserV1GetBalancesRequest, - signal?: AbortSignal - ): Promise { + async userV1GetBalances(req: API.UserV1GetBalancesRequest, signal?: AbortSignal): Promise { return this.call(Methods.UserV1GetBalancesMethod, req, signal); } - async userV1GetTransactions( - req: API.UserV1GetTransactionsRequest, - signal?: AbortSignal - ): Promise { + async userV1GetTransactions(req: API.UserV1GetTransactionsRequest, signal?: AbortSignal): Promise { return this.call(Methods.UserV1GetTransactionsMethod, req, signal); } - async userV1GetActionAllowances( - req: API.UserV1GetActionAllowancesRequest, - signal?: AbortSignal - ): Promise { + async userV1GetActionAllowances(req: API.UserV1GetActionAllowancesRequest, signal?: AbortSignal): Promise { return this.call(Methods.UserV1GetActionAllowancesMethod, req, signal); } @@ -253,10 +180,7 @@ export class RPCClient { return this.call(Methods.NodeV1GetConfigMethod, {}, signal); } - async nodeV1GetAssets( - req: API.NodeV1GetAssetsRequest, - signal?: AbortSignal - ): Promise { + async nodeV1GetAssets(req: API.NodeV1GetAssetsRequest, signal?: AbortSignal): Promise { return this.call(Methods.NodeV1GetAssetsMethod, req, signal); } diff --git a/sdk/ts/src/rpc/dialer.ts b/sdk/ts/src/rpc/dialer.ts index f7acc3fce..8b2f3ba70 100644 --- a/sdk/ts/src/rpc/dialer.ts +++ b/sdk/ts/src/rpc/dialer.ts @@ -239,7 +239,6 @@ export class WebsocketDialer implements Dialer { this.closeHandler(error); } } - } /** diff --git a/sdk/ts/src/rpc/message.ts b/sdk/ts/src/rpc/message.ts index 15c339675..262d596c8 100644 --- a/sdk/ts/src/rpc/message.ts +++ b/sdk/ts/src/rpc/message.ts @@ -116,12 +116,7 @@ export function messageError(message: Message): Error | null { * NewMessage creates a new Message with the given request ID, type, method, and parameters. * The timestamp is automatically set to the current time in Unix milliseconds. */ -export function newMessage( - type: MsgType, - requestId: number, - method: string, - payload: Payload = {} -): Message { +export function newMessage(type: MsgType, requestId: number, method: string, payload: Payload = {}): Message { return { type, requestId, @@ -168,13 +163,7 @@ export function newErrorResponse(requestId: number, method: string, errMsg: stri * MarshalJSON serializes Message to JSON array format */ export function marshalMessage(message: Message): string { - const arr = [ - message.type, - message.requestId, - message.method, - message.payload, - message.timestamp, - ]; + const arr = [message.type, message.requestId, message.method, message.payload, message.timestamp]; return JSON.stringify(arr, bigIntReplacer); } diff --git a/sdk/ts/src/signers.ts b/sdk/ts/src/signers.ts index db6784c3c..9fa716acb 100644 --- a/sdk/ts/src/signers.ts +++ b/sdk/ts/src/signers.ts @@ -199,12 +199,7 @@ export class ChannelSessionKeyStateSigner implements StateSigner { private metadataHash: Hex; private authSignature: Hex; - constructor( - sessionKeyPrivateKey: Hex, - walletAddress: Address, - metadataHash: Hex, - authSignature: Hex - ) { + constructor(sessionKeyPrivateKey: Hex, walletAddress: Address, metadataHash: Hex, authSignature: Hex) { this.account = privateKeyToAccount(sessionKeyPrivateKey); this.walletAddress = walletAddress; this.metadataHash = metadataHash; @@ -247,7 +242,7 @@ export class ChannelSessionKeyStateSigner implements StateSigner { authSignature: this.authSignature, }, sessionKeySig, - ] + ], ); // Prepend 0x01 type byte (ChannelSignerType_SessionKey) diff --git a/sdk/ts/src/utils.ts b/sdk/ts/src/utils.ts index 8199dd953..7b619094c 100644 --- a/sdk/ts/src/utils.ts +++ b/sdk/ts/src/utils.ts @@ -267,9 +267,7 @@ export function transformTransaction(tx: TransactionV1): core.Transaction { /** * Transform RPC PaginationMetadataV1 to core PaginationMetadata */ -export function transformPaginationMetadata( - metadata: PaginationMetadataV1 -): core.PaginationMetadata { +export function transformPaginationMetadata(metadata: PaginationMetadataV1): core.PaginationMetadata { return { page: metadata.page, perPage: metadata.per_page, @@ -308,7 +306,7 @@ import * as RPCApp from './rpc/api'; export function transformAppDefinitionToRPC(def: AppDefinitionV1): any { return { application_id: def.applicationId, - participants: def.participants.map(p => ({ + participants: def.participants.map((p) => ({ wallet_address: p.walletAddress, signature_weight: p.signatureWeight, })), @@ -326,7 +324,7 @@ export function transformAppStateUpdateToRPC(update: AppStateUpdateV1) { app_session_id: update.appSessionId, intent: update.intent, version: update.version.toString(), - allocations: update.allocations.map(a => ({ + allocations: update.allocations.map((a) => ({ participant: a.participant, asset: a.asset, amount: a.amount.toString(),