From 741d90bf5fe65bf8aeb6123d140671da2dd54ddb Mon Sep 17 00:00:00 2001 From: Sazonov Nikita <35502225+nksazonov@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:51:05 +0300 Subject: [PATCH 01/18] feat: add system support for EIP-191, contracts support for EIP-(712,1271,6492) (#294) * feat: move erc7824 documentation to nitrolite (#272) * feat: move erc7824 documentation to nitrolite * feat: move GH actions to common scope * fix: comment GITHUB_TOKEN * feat(custody): add EIP-191, EIP-712 signature support (#257) * feat(custody): add EIP-191, EIP-712 support * feat(custody): correct EIP-712 sig impl * feat(utils): add POLA principle comment * feat(contract): add EIP712AdjudicatorBase * feat(contract): integrate EIP712AdjudicatorBase to supported adjs * fix(contract): tests with updated adjs * test(contract): EIP712AdjudicatorBase * test(contract): SimpleConsensus EIP191, EIP712 sigs * test(contract): Utils signatures * test(contract): Custody, SimpleConsensus integration tests for sigs * refactor(contract): migrate sig to bytes (#279) * refactor(Types): replace Signature{v,r,s} with bytes * style(contract): add empty lines at the end * refactor(clearnode): replace Signature{r,s,v} with []byte (#283) * refactor(sdk): migrate `Signature{r,s,v}` to `Hex` (#284) * refactor(sdk): replace Signature{r,s,v} with Hex * fix(NitroliteService): perform convertStateFromContract correctly * fix(rpc): expect updated signature type from rpc * style(sdk/rpc): remove ServerSignatureSchema var * feat(sdk/rpc): add empty signatures array by default * feat(sdk): add build:force command * feat(sdk): remove npm build:force * refactor(clearnode): define `Signature []byte` type (#287) * refactor(clearnode): define `Signature []byte` type * refactor(clearnode::RPCEntry): replace sig `[]string` with `[]Signature` * feat(clearnode/nitrolite): add Sigs2Strings, Strings2Sigs helpers * fix(integration): tests sig migration (#289) * feat(docker-compose): add ability to pass logger visibility to clearnode * fix(clearnode): raw ECDSA sign and verify * fix(clearnode): try to extract req id on ummarshal error * fix(integration): update sig to new type * feat(sdk): change type of sig array to contain only Hex * refactor(clearnode): remove check on nil Req, zero-init instead * fix(clearnode:nitrolite): remove unintended side-effect of sig param modif * feat(custody): add note about ephemeral final state * style(contract): run forge fmt * fix(clearnode/docs): remove legacy signature format * feat(custody): eip-1271, eip-6492 sigs support (#293) * feat(contract): add ERC-1271,6492 support * test(contract/Utils.sol): add ERC-1271, 6492 tests * refactor(contract:Utils.t.sol): separate tests into contracts * refactor(UtilsHarness): do NOT expose constants * fix(contracts/adjudicators): use verifyStateSignature instead of just EOA sig * test(contract/Custody): add ERC-1271, 6492 sig to integration test * test(contract/Custody): add challenge with EIP-712, EIP-1271 tests * refactor(contract/Utils): reorder recoverStateEIP712Signer params for consistency * test(contract): remove console.logs * feat(contract): clarify Utils comments * refactor(contract): optimize Custody and Utils sig verification functions * style(contract): run forge fmt * feat: replace `stateHash` with `packedState` in message signing (#295) * feat(contract): replace stateHash with packedState in signing * test(contract): migrate to getPackedState * feat(contract): remove getStateHash * feat(clearnode): remove state_hash, rename EncodeState to Pack * feat(sdk): remove state_hash, add getPackedState * refactor(sdk): supply channelId and state to signState * docs(website): migrate Sig to Hex, add supported signature formats --------- Co-authored-by: MaxMoskalenko --- clearnode/channel_service.go | 33 +- clearnode/custody.go | 4 +- clearnode/custody_test.go | 8 +- clearnode/docs/API.md | 20 +- clearnode/nitrolite/bindings.go | 366 +- clearnode/nitrolite/signature.go | 74 +- clearnode/nitrolite/signature_test.go | 4 +- clearnode/nitrolite/state.go | 4 +- clearnode/rpc.go | 10 +- clearnode/rpc_node.go | 7 +- clearnode/rpc_node_test.go | 2 +- clearnode/rpc_router.go | 18 +- clearnode/rpc_router_auth.go | 2 +- clearnode/rpc_router_private.go | 27 +- clearnode/rpc_router_private_test.go | 40 +- clearnode/rpc_router_public_test.go | 2 +- clearnode/rpc_store.go | 7 +- clearnode/rpc_store_test.go | 19 +- clearnode/rpc_test.go | 2 +- clearnode/signer.go | 50 +- clearnode/signer_test.go | 9 +- clearnode/testing/client.go | 15 +- contract/script/setupChannel.s.sol | 16 +- contract/src/Custody.sol | 89 +- contract/src/Utils.sol | 253 +- .../src/adjudicators/ConsensusTransition.sol | 35 +- contract/src/adjudicators/Counter.sol | 11 +- .../adjudicators/EIP712AdjudicatorBase.sol | 72 + contract/src/adjudicators/Remittance.sol | 67 +- contract/src/adjudicators/SimpleConsensus.sol | 30 +- contract/src/interfaces/IAdjudicator.sol | 1 - contract/src/interfaces/IChannel.sol | 6 +- contract/src/interfaces/Types.sol | 15 +- contract/test/Custody.t.sol | 731 ++-- contract/test/Custody_integration.t.sol | 288 ++ contract/test/TestUtils.sol | 80 +- contract/test/Utils.t.sol | 536 +++ contract/test/UtilsHarness.sol | 104 + .../adjudicators/ConsensusTransition.t.sol | 104 +- contract/test/adjudicators/Counter.t.sol | 93 +- .../EIP712AdjudicatorBaseTest.t.sol | 77 + .../test/adjudicators/SimpleConsensus.t.sol | 80 +- contract/test/mocks/MockEIP712.sol | 12 + contract/test/mocks/MockERC4337Factory.sol | 30 + contract/test/mocks/MockFlagERC1271.sol | 26 + contract/test/mocks/TestEIP712Adjudicator.sol | 12 + docker-compose.yml | 14 +- erc7824-docs/docs/erc-7824.md | 45 +- .../advanced/supported-sig-formats.md | 86 + erc7824-docs/docs/nitrolite_client/types.md | 18 +- .../quick_start/connect_to_the_clearnode.md | 2 +- examples/cerebro/clearnet/clearnode_client.go | 2 - .../client/src/services/ClearNetService.ts | 1 + integration/common/nitroliteClient.ts | 6 +- integration/tests/lifecycle.test.ts | 2 +- integration/tests/resize_channel.test.ts | 4 - sdk/src/abis/generated.ts | 3294 ++++++++--------- sdk/src/client/services/NitroliteService.ts | 37 +- sdk/src/client/state.ts | 33 +- sdk/src/client/types.ts | 9 +- sdk/src/rpc/api.ts | 1 - sdk/src/rpc/nitrolite.ts | 4 +- sdk/src/rpc/parse/channel.ts | 26 +- sdk/src/rpc/types/index.ts | 4 +- sdk/src/rpc/types/response.ts | 11 +- sdk/src/utils/state.ts | 113 +- sdk/test/unit/client/index.test.ts | 9 +- sdk/test/unit/client/state.test.ts | 19 +- sdk/test/unit/rpc/api.test.ts | 4 +- sdk/test/unit/rpc/nitrolite.test.ts | 1 + sdk/test/unit/utils/state.test.ts | 90 +- 71 files changed, 4427 insertions(+), 2899 deletions(-) create mode 100644 contract/src/adjudicators/EIP712AdjudicatorBase.sol create mode 100644 contract/test/Custody_integration.t.sol create mode 100644 contract/test/Utils.t.sol create mode 100644 contract/test/UtilsHarness.sol create mode 100644 contract/test/adjudicators/EIP712AdjudicatorBaseTest.t.sol create mode 100644 contract/test/mocks/MockEIP712.sol create mode 100644 contract/test/mocks/MockERC4337Factory.sol create mode 100644 contract/test/mocks/MockFlagERC1271.sol create mode 100644 contract/test/mocks/TestEIP712Adjudicator.sol create mode 100644 erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md diff --git a/clearnode/channel_service.go b/clearnode/channel_service.go index a98e37ad2..01622100c 100644 --- a/clearnode/channel_service.go +++ b/clearnode/channel_service.go @@ -7,7 +7,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -114,13 +113,12 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam // 6) Encode & sign the new state channelIDHash := common.HexToHash(channel.ChannelID) - encodedState, err := nitrolite.EncodeState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations) + packedState, err := nitrolite.PackState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations) if err != nil { - logger.Error("failed to encode state hash", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to encode state hash") + logger.Error("failed to pack state", "error", err) + return ResizeChannelResponse{}, RPCErrorf("failed to pack state") } - stateHash := crypto.Keccak256Hash(encodedState).Hex() - sig, err := s.signer.NitroSign(encodedState) + sig, err := s.signer.Sign(packedState) if err != nil { logger.Error("failed to sign state", "error", err) return ResizeChannelResponse{}, RPCErrorf("failed to sign state") @@ -131,12 +129,7 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam Intent: uint8(nitrolite.IntentRESIZE), Version: channel.Version + 1, StateData: hexutil.Encode(encodedIntentions), - StateHash: stateHash, - Signature: Signature{ - V: sig.V, - R: hexutil.Encode(sig.R[:]), - S: hexutil.Encode(sig.S[:]), - }, + Signature: sig, } for _, alloc := range allocations { @@ -217,13 +210,12 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams, logger.Error("failed to decode state data hex", "error", err) return CloseChannelResponse{}, RPCErrorf("failed to decode state data hex") } - encodedState, err := nitrolite.EncodeState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations) + packedState, err := nitrolite.PackState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations) if err != nil { - logger.Error("failed to encode state hash", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to encode state hash") + logger.Error("failed to pack state", "error", err) + return CloseChannelResponse{}, RPCErrorf("failed to pack state") } - stateHash := crypto.Keccak256Hash(encodedState).Hex() - sig, err := s.signer.NitroSign(encodedState) + sig, err := s.signer.Sign(packedState) if err != nil { logger.Error("failed to sign state", "error", err) return CloseChannelResponse{}, RPCErrorf("failed to sign state") @@ -234,12 +226,7 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams, Intent: uint8(nitrolite.IntentFINALIZE), Version: channel.Version + 1, StateData: stateDataHex, - StateHash: stateHash, - Signature: Signature{ - V: sig.V, - R: hexutil.Encode(sig.R[:]), - S: hexutil.Encode(sig.S[:]), - }, + Signature: sig, } for _, alloc := range allocations { diff --git a/clearnode/custody.go b/clearnode/custody.go index dd4d13136..80ac6c95c 100644 --- a/clearnode/custody.go +++ b/clearnode/custody.go @@ -117,7 +117,7 @@ func (c *Custody) Join(channelID string, lastStateData []byte) (common.Hash, err // The broker will always join as participant with index 1 (second participant) index := big.NewInt(1) - sig, err := c.signer.NitroSign(lastStateData) + sig, err := c.signer.Sign(lastStateData) if err != nil { return common.Hash{}, fmt.Errorf("failed to sign data: %w", err) } @@ -288,7 +288,7 @@ func (c *Custody) handleCreated(logger Logger, ev *nitrolite.CustodyCreated) { return } - encodedState, err := nitrolite.EncodeState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations) + encodedState, err := nitrolite.PackState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations) if err != nil { logger.Error("error encoding state hash", "error", err) return diff --git a/clearnode/custody_test.go b/clearnode/custody_test.go index 7ce16df44..88421e844 100644 --- a/clearnode/custody_test.go +++ b/clearnode/custody_test.go @@ -135,7 +135,7 @@ func createMockCreatedEvent(t *testing.T, signer *Signer, token string, amount * Version: big.NewInt(0), Data: []byte{}, Allocations: allocation, - Sigs: []nitrolite.Signature{}, + Sigs: [][]byte{}, } event := &nitrolite.CustodyCreated{ @@ -189,7 +189,7 @@ func createMockClosedEvent(t *testing.T, signer *Signer, token string, amount *b Version: big.NewInt(1), Data: []byte{}, Allocations: allocation, - Sigs: []nitrolite.Signature{}, + Sigs: [][]byte{}, } event := &nitrolite.CustodyClosed{ @@ -226,7 +226,7 @@ func createMockChallengedEvent(t *testing.T, signer *Signer, token string, amoun Version: big.NewInt(2), Data: []byte{}, Allocations: allocation, - Sigs: []nitrolite.Signature{}, + Sigs: [][]byte{}, } event := &nitrolite.CustodyChallenged{ @@ -314,7 +314,7 @@ func TestHandleCreatedEvent(t *testing.T) { Version: big.NewInt(0), Data: []byte{}, Allocations: allocation, - Sigs: []nitrolite.Signature{}, + Sigs: [][]byte{}, } mockEvent := &nitrolite.CustodyCreated{ diff --git a/clearnode/docs/API.md b/clearnode/docs/API.md index 8c232f9ab..d05db9624 100644 --- a/clearnode/docs/API.md +++ b/clearnode/docs/API.md @@ -830,9 +830,9 @@ In the request, the user must specify funds destination. After the channel is cl { "res": [1, "close_channel", [{ "channel_id": "0x4567890123abcdef...", - "intent": 3, // IntentFINALIZE - constant magic number for closing channel + "intent": 3, // IntentFINALIZE - constant specifying that this is a final state "version": 123, - "state_data": "0x0000000000000000000000000000000000000000000000000000000000001ec7", + "state_data": "0xdeadbeef", "allocations": [ { "destination": "0x1234567890abcdef...", // Provided funds address @@ -845,12 +845,7 @@ In the request, the user must specify funds destination. After the channel is cl "amount": "50000" } ], - "state_hash": "0xLedgerStateHash", - "server_signature": { - "v": "27", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - } + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c", }], 1619123456789], "sig": ["0xabcd1234..."] } @@ -890,7 +885,7 @@ Example: { "res": [1, "resize_channel", [{ "channel_id": "0x4567890123abcdef...", - "state_data": "0x0000000000000000000000000000000000000000000000000000000000002ec7", + "state_data": "0xdeadbeef", "intent": 2, // IntentRESIZE "version": 5, "allocations": [ @@ -905,12 +900,7 @@ Example: "amount": "0" } ], - "state_hash": "0xLedgerStateHash", - "server_signature": { - "v": "28", - "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - "s": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - } + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c", }], 1619123456789], "sig": ["0xabcd1234..."] } diff --git a/clearnode/nitrolite/bindings.go b/clearnode/nitrolite/bindings.go index 3ed00fb19..e45cc2480 100644 --- a/clearnode/nitrolite/bindings.go +++ b/clearnode/nitrolite/bindings.go @@ -44,26 +44,19 @@ type Channel struct { Nonce uint64 } -// Signature is an auto generated low-level Go binding around an user-defined struct. -type Signature struct { - V uint8 - R [32]byte - S [32]byte -} - // State is an auto generated low-level Go binding around an user-defined struct. type State struct { Intent uint8 Version *big.Int Data []byte Allocations []Allocation - Sigs []Signature + Sigs [][]byte } // CustodyMetaData contains all meta data concerning the Custody contract. var CustodyMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature\",\"name\":\"challengerSig\",\"type\":\"tuple\"}],\"name\":\"challenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"checkpoint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"name\":\"close\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"create\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"depositAndCreate\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getAccountsBalances\",\"outputs\":[{\"internalType\":\"uint256[][]\",\"name\":\"\",\"type\":\"uint256[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getChannelBalances\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"balances\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"getChannelData\",\"outputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"internalType\":\"enumChannelStatus\",\"name\":\"status\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"wallets\",\"type\":\"address[]\"},{\"internalType\":\"uint256\",\"name\":\"challengeExpiry\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"lastValidState\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"}],\"name\":\"getOpenChannels\",\"outputs\":[{\"internalType\":\"bytes32[][]\",\"name\":\"\",\"type\":\"bytes32[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature\",\"name\":\"sig\",\"type\":\"tuple\"}],\"name\":\"join\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"resize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"expiration\",\"type\":\"uint256\"}],\"name\":\"Challenged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"}],\"name\":\"Checkpointed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"finalState\",\"type\":\"tuple\"}],\"name\":\"Closed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"indexed\":false,\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"Created\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposited\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"Joined\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"Opened\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"int256[]\",\"name\":\"deltaAllocations\",\"type\":\"int256[]\"}],\"name\":\"Resized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdrawn\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"ChallengeNotExpired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ChannelNotFinal\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"ChannelNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DepositAlreadyFulfilled\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expectedFulfilled\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"actualFulfilled\",\"type\":\"uint256\"}],\"name\":\"DepositsNotFulfilled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ECDSAInvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"ECDSAInvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"ECDSAInvalidSignatureS\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"available\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"required\",\"type\":\"uint256\"}],\"name\":\"InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAdjudicator\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAllocations\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAmount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengePeriod\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengerSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidParticipant\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidState\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStateSignatures\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidValue\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TransferFailed\",\"type\":\"error\"}]", - Bin: "0x0x608080604052346015576149a0908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063259311c9146118b05780632f33c4d6146116f75780635a9eb80e146116275780638340f549146115f2578063925bc4791461157c578063a22b823d14611054578063bc7b456f14610fc7578063d0cce1e814610b9f578063d37ff7b514610b33578063d710e92f146109a5578063de22731f146103a1578063e617208c1461023b5763f3fef3a3146100a8575f80fd5b34610237576040600319360112610237576100c161211f565b60243590335f52600160205260405f20906001600160a01b0381165f528160205260405f205491838310610207576001600160a01b0392508282165f5260205260405f20610110848254612691565b90551690816101b5575f80808084335af13d156101b0573d61013181612313565b9061013f60405192836120e4565b81525f60203d92013e5b1561017d575b6040519081527fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb60203392a3005b907fbf182be8000000000000000000000000000000000000000000000000000000005f526004523360245260445260645ffd5b610149565b6102026040517fa9059cbb000000000000000000000000000000000000000000000000000000006020820152336024820152826044820152604481526101fc6064826120e4565b83614666565b61014f565b50507fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5f80fd5b34610237576020600319360112610237575f606060405161025b81612090565b818152826020820152826040820152015261027461269e565b506004355f525f60205260405f2061028b81612602565b60ff60038301541691604051906102a36060836120e4565b600282526040366020840137600481015f5b600281106103735750506102d0600f600e8301549201612bdb565b6040519460a0865267ffffffffffffffff60606102fa8751608060a08b01526101208a01906121ae565b966001600160a01b0360208201511660c08a01528260408201511660e08a0152015116610100870152600581101561035f57859461035b9461034892602088015286820360408801526121ae565b9160608501528382036080850152612219565b0390f35b634e487b7160e01b5f52602160045260245ffd5b806001600160a01b0361038860019385612dcf565b90549060031b1c1661039a82876126f9565b52016102b5565b34610237576103af3661201d565b5050815f525f60205260405f2060ff600382015416600581101561035f57801561099257600281036106c8575081356004811015610237576103f0816121ea565b600381036106b957602083013580156106b95760808401916002610414848761258c565b9050036106915761043761042785612602565b6104313688612439565b90613fdc565b156106915761044990600f85016127ef565b60108301556011820161045f604085018561265e565b9067ffffffffffffffff821161067d576104838261047d8554612808565b85612856565b5f90601f8311600114610619576104b192915f918361060e575b50508160011b915f199060031b1c19161790565b90555b601282016104c5606085018561258c565b91906104d1838361289b565b905f5260205f205f915b8383106105a857505050506104f460138301918461258c565b906104ff828461291d565b915f5260205f205f925b82841061056857505050507f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c9109161054f6105496012610563945b01612b5a565b85614283565b604051918291602083526020830190612e13565b0390a2005b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290610509565b60036060826001600160a01b036105c0600195612909565b166001600160a01b03198654161785556105dc60208201612909565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906104db565b01359050888061049d565b601f19831691845f5260205f20925f5b818110610665575090846001959493921061064c575b505050811b0190556104b4565b01355f19600384901b60f8161c1916905587808061063f565b91936020600181928787013581550195019201610629565b634e487b7160e01b5f52604160045260245ffd5b7f773a750f000000000000000000000000000000000000000000000000000000005f5260045ffd5b63baf3f0f760e01b5f5260045ffd5b60030361096a57600e810180544210156109365782356004811015610237576106f0816121ea565b600381036106b95761070e61070484612602565b6104313687612439565b15610691575f6107229255600f83016127ef565b602082013560108201556011810161073d604084018461265e565b9067ffffffffffffffff821161067d5761075b8261047d8554612808565b5f90601f83116001146108d25761078892915f91836108c75750508160011b915f199060031b1c19161790565b90555b6012810161079c606084018461258c565b91906107a8838361289b565b905f5260205f205f915b8383106108615750505050601381016107ce608084018461258c565b906107d9828461291d565b915f5260205f205f925b82841061082157505050507f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c9109161054f610549601261056394610543565b80359060ff8216820361023757606060039160ff6001941660ff1986541617855560208101358486015560408101356002860155019201930192906107e3565b60036060826001600160a01b03610879600195612909565b166001600160a01b031986541617855561089560208201612909565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906107b2565b01359050878061049d565b601f19831691845f5260205f20925f5b81811061091e5750908460019594939210610905575b505050811b01905561078b565b01355f19600384901b60f8161c191690558680806108f8565b919360206001819287870135815501950192016108e2565b507f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c9109161054f610549601261056394610543565b7ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b836379c1d89f60e11b5f5260045260245ffd5b346102375760206003193601126102375760043567ffffffffffffffff8111610237576109d6903690600401612149565b8051906109fb6109e583612107565b926109f360405194856120e4565b808452612107565b90610a0e601f196020850193018361298b565b5f5b8151811015610aa2576001600160a01b03610a2b82846126f9565b51165f526001602052600160405f20016040519081602082549182815201915f5260205f20905f905b808210610a8a5750505090610a6e816001949303826120e4565b610a7882876126f9565b52610a8381866126f9565b5001610a10565b90919260016020819286548152019401920190610a54565b50509060405191829160208301906020845251809152604083019060408160051b85010192915f905b828210610ada57505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b808210610b1b575050506020806001929601920192018594939192610acb565b90919260208060019286518152019401920190610afb565b346102375760406003193601126102375760043567ffffffffffffffff811161023757608060031982360301126102375760243567ffffffffffffffff81116102375760a0600319823603011261023757602091610b9791600401906004016137da565b604051908152f35b3461023757610bad3661201d565b835f525f60205260405f2091600383019160ff83541690600582101561035f578115610fb4576004821461096a57853592600484101561023757610bf0846121ea565b836106b957600f86019260ff84541690600181145f14610c32577ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b600203610f0157506001600160a01b0360018701541691610c66610c56368a612439565b610c5f86612bdb565b9085614482565b156106b957610c90926020926040518095819482936305b959ef60e01b84528d8d60048601612f8f565b03915afa908115610ef6575f91610ec7575b50156106b957610cbd925b600260ff198254161790556127ef565b6020820135601082015560118101610cd8604084018461265e565b9067ffffffffffffffff821161067d57610cf68261047d8554612808565b5f90601f8311600114610e6357610d2392915f91836108c75750508160011b915f199060031b1c19161790565b90555b60128101610d37606084018461258c565b9190610d43838361289b565b905f5260205f205f915b838310610dfd57868660138701610d67608083018361258c565b90610d72828461291d565b915f5260205f205f925b828410610dbd57857fa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d61056387604051918291602083526020830190612e13565b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290610d7c565b60036060826001600160a01b03610e15600195612909565b166001600160a01b0319865416178555610e3160208201612909565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190610d4d565b601f19831691845f5260205f20925f5b818110610eaf5750908460019594939210610e96575b505050811b019055610d26565b01355f19600384901b60f8161c19169055868080610e89565b91936020600181928787013581550195019201610e73565b610ee9915060203d602011610eef575b610ee181836120e4565b81019061270d565b87610ca2565b503d610ed7565b6040513d5f823e3d90fd5b90916020610f35916001600160a01b0360018a0154169460405193849283926305b959ef60e01b84528d8d60048601612f8f565b0381865afa908115610ef6575f91610f95575b50156106b957610f57816121ea565b15610f6d575b50610cbd925f600e860155610cad565b610f8a90610f7b3688612439565b610f8484612bdb565b91614482565b156106b95786610f5d565b610fae915060203d602011610eef57610ee181836120e4565b89610f48565b866379c1d89f60e11b5f5260045260245ffd5b346102375760c06003193601126102375760243567ffffffffffffffff81116102375760a060031982360301126102375760443567ffffffffffffffff811161023757611018903690600401611fec565b9060607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c3601126102375761105292600401600435613027565b005b346102375760a060031936011261023757600435606060431936011261023757805f525f60205260405f2090600382019160ff835416600581101561035f578015611569575f190161096a5760016024350361154157600d81015461151957600f8101906110d36110c482612602565b6110cd84612bdb565b9061406d565b6001600160a01b03611100816110e885612d3a565b90549060031b1c16926110fa366123c5565b906146d3565b16036106915761110f82612bdb565b936040519461111f6060876120e4565b6002865260405f5b8181106114f0575050608081019561113f87516126c8565b51611149826126c8565b52611153816126c8565b5061115d366123c5565b611166826126e9565b52611170816126e9565b50865260018301546001600160a01b031660405190602061119181846120e4565b5f8352601f19015f5b8181106114d95750506111c79160209160405180809581946305b959ef60e01b8352888b6004850161276c565b03915afa908115610ef6575f916114ba575b50156106b9576112456111ee60088501612d67565b9461122086600c87019060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b6005850180546001600160a01b03191633179055825190611240826121ea565b6127ef565b6020810151601084015560118301604082015180519067ffffffffffffffff821161067d576112788261047d8554612808565b602090601f8311600114611457576112a692915f918361144c5750508160011b915f199060031b1c19161790565b90555b6060601284019101519060208251926112c2848461289b565b01905f5260205f205f915b8383106113e9575050505060138201945160208151916112ed838961291d565b01955f5260205f20955f905b8282106113b157835460ff19166002178455602087611359886001600160a01b036113238a612d3a565b90549060031b1c165f526001845261134183600160405f20016146fc565b50836001600160a01b03825116910151908333614154565b807fe8e915db7b3549b9e9e9b3e2ec2dc3edd1f76961504366998824836401f6846a8360405160018152a260405190807fd087f17acc177540af5f382bc30c65363705b90855144d285a822536ee11fdd15f80a28152f35b60036020826040600194518c60ff1960ff8084511616915416178d558c8685830151910155015160028c0155019801910190966112f9565b60036020826040600194516001600160a01b0380825116166001600160a01b03198854161787556001600160a01b0384820151166001600160a01b0387890191166001600160a01b031982541617905501516002860155019201920191906112cd565b015190508a8061049d565b90601f19831691845f52815f20925f5b8181106114a2575090846001959493921061148a575b505050811b0190556112a9565b01515f1960f88460031b161c1916905589808061147d565b92936020600181928786015181550195019301611467565b6114d3915060203d602011610eef57610ee181836120e4565b876111d9565b6020906114e461269e565b8282870101520161119a565b6020906040516114ff81612074565b5f81525f838201525f604082015282828b01015201611127565b7f1b136079000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa145c43e000000000000000000000000000000000000000000000000000000005f5260045ffd5b826379c1d89f60e11b5f5260045260245ffd5b60806003193601126102375761159061211f565b6044359067ffffffffffffffff82116102375760806003198336030112610237576064359167ffffffffffffffff83116102375760a06003198436030112610237576020926115e6610b979360243590336129e8565b600401906004016137da565b60606003193601126102375761160661211f565b602435906001600160a01b03821682036102375761105291604435916129e8565b346102375760406003193601126102375760043560243567ffffffffffffffff81116102375761165b903690600401612149565b61166581516129a7565b5f5b82518110156116b257600190845f525f602052601460405f20016001600160a01b038061169484886126f9565b5116165f5260205260405f20546116ab82856126f9565b5201611667565b506040518091602082016020835281518091526020604084019201905f5b8181106116de575050500390f35b82518452859450602093840193909201916001016116d0565b346102375760406003193601126102375760043567ffffffffffffffff811161023757611728903690600401611fec565b60243567ffffffffffffffff811161023757611748903690600401611fec565b91909261175482612107565b9361176260405195866120e4565b82855261176e83612107565b93611781601f196020880196018661298b565b5f5b84811061181a57858760405191829160208301906020845251809152604083019060408160051b85010192915f905b8282106117c157505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b8082106118025750505060208060019296019201920185949391926117b2565b909192602080600192865181520194019201906117e2565b611823826129a7565b61182d82896126f9565b5261183881886126f9565b505f5b82811061184b5750600101611783565b6001906001600160a01b03611869611864858a8a6129d8565b612909565b165f528160205260405f206001600160a01b0361188a61186484888a6129d8565b165f5260205260405f20546118a9826118a3868d6126f9565b516126f9565b520161183b565b34610237576118be3661201d565b9190835f525f60205260405f209360ff600386015416600581101561035f578015611fd9577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0161096a5783156106b957823592600484101561023757611924846121ea565b600284036106b957823593609e198436030194858112156102375761194c9085013690612439565b966020830135602089015160018101809111611b405781036106b95760608901936119778551613fa6565b606081019161199861199361198c858561258c565b369161232f565b613fa6565b6119ae6119a485612602565b6104313685612439565b15610691576119c0604083018361265e565b810195906020818803126102375780359067ffffffffffffffff821161023757019686601f89011215610237578735966119f988612107565b98611a076040519a8b6120e4565b888a5260208a01906020829a60051b82010192831161023757602001905b828210611fc95750505051611a3d61198c868661258c565b9060028951036106b957611a6f816040611a6681611a5d611aa6966126c8565b510151926126e9565b5101519061257f565b91611aa0611a90611a7f8c6126c8565b51611a898d6126e9565b5190614052565b916040611a6681611a5d846126c8565b92614052565b03611fa1575f198b019b8b8d11611b4057611ac08d612107565b9c806040519e8f90611ad290826120e4565b52611adc90612107565b601f19018d5f5b828110611f885750505060015b8c811015611b54578060051b8b01358c811215610237575f19820191908c01818311611b40578f92600193611b29611b39933690612439565b611b3383836126f9565b526126f9565b5001611af0565b634e487b7160e01b5f52601160045260245ffd5b5060208d611b88926001600160a01b0360018a015416906040518095819482936305b959ef60e01b84528d6004850161276c565b03915afa908115610ef6575f91611f69575b50156106b957611bad90600f86016127ef565b601084015560118301611bc3604083018361265e565b9067ffffffffffffffff821161067d57611be18261047d8554612808565b5f90601f8311600114611f0557611c0e92915f9183611efa5750508160011b915f199060031b1c19161790565b90555b60128301611c1f838361258c565b9190611c2b838361289b565b905f5260205f205f915b838310611e94575050505060138301611c51608083018361258c565b90611c5c828461291d565b915f5260205f205f925b828410611e545750505050611c7e9161198c9161258c565b9060068101916001600160a01b038354165f5b60028110611dfb57505f5b60028110611d5b575050600a5f9201915b60028110611d18575050505060405191602083019060208452518091526040830191905f5b818110611d0257857ff3b6c524f73df7344d9fcf2f960a57aba7fba7e292d8b79ed03d786f7b2b112f86860387a2005b8251845260209384019390920191600101611cd2565b806040611d27600193856126f9565b51015182611d358388612b47565b5001556040611d4482856126f9565b51015182611d528387612b47565b50015501611cad565b5f611d6982899796976126f9565b5112611d7b575b600101939293611c9c565b6001600160a01b03611d908260048801612dcf565b90549060031b1c1690611da381896126f9565b517f80000000000000000000000000000000000000000000000000000000000000008114611b4057600192611df49160405191611ddf83612074565b82528560208301525f0360408201528a6145b3565b9050611d70565b805f611e0c6001938a9897986126f9565b5113611e1c575b01939293611c91565b611e4f6001600160a01b03611e348360048a01612dcf565b90549060031b1c16848b611e48858d6126f9565b5192614154565b611e13565b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290611c66565b60036060826001600160a01b03611eac600195612909565b166001600160a01b0319865416178555611ec860208201612909565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190611c35565b013590508a8061049d565b601f19831691845f5260205f20925f5b818110611f515750908460019594939210611f38575b505050811b019055611c11565b01355f19600384901b60f8161c19169055898080611f2b565b91936020600181928787013581550195019201611f15565b611f82915060203d602011610eef57610ee181836120e4565b89611b9a565b6020918282611f9561269e565b92010152018e90611ae3565b7f52e4cb1c000000000000000000000000000000000000000000000000000000005f5260045ffd5b8135815260209182019101611a25565b506379c1d89f60e11b5f5260045260245ffd5b9181601f840112156102375782359167ffffffffffffffff8311610237576020808501948460051b01011161023757565b6060600319820112610237576004359160243567ffffffffffffffff81116102375760a0600319828503011261023757600401916044359067ffffffffffffffff82116102375761207091600401611fec565b9091565b6060810190811067ffffffffffffffff82111761067d57604052565b6080810190811067ffffffffffffffff82111761067d57604052565b60a0810190811067ffffffffffffffff82111761067d57604052565b6040810190811067ffffffffffffffff82111761067d57604052565b90601f601f19910116810190811067ffffffffffffffff82111761067d57604052565b67ffffffffffffffff811161067d5760051b60200190565b600435906001600160a01b038216820361023757565b35906001600160a01b038216820361023757565b9080601f8301121561023757813561216081612107565b9261216e60405194856120e4565b81845260208085019260051b82010192831161023757602001905b8282106121965750505090565b602080916121a384612135565b815201910190612189565b90602080835192838152019201905f5b8181106121cb5750505090565b82516001600160a01b03168452602093840193909201916001016121be565b6004111561035f57565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b8051612224816121ea565b825260208101516020830152612249604082015160a0604085015260a08401906121f4565b906060810151918381036060850152602080845192838152019301905f5b8181106122cb5750505060800151916080818303910152602080835192838152019201905f5b81811061229a5750505090565b909192602060606001926040875160ff8151168352848101518584015201516040820152019401910191909161228d565b909193602061230960019287519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b9501929101612267565b67ffffffffffffffff811161067d57601f01601f191660200190565b92919261233b82612107565b9361234960405195866120e4565b606060208685815201930282019181831161023757925b82841061236d5750505050565b60608483031261023757602060609160405161238881612074565b61239187612135565b815261239e838801612135565b8382015260408701356040820152815201930192612360565b359060ff8216820361023757565b604319606091011261023757604051906123de82612074565b8160443560ff8116810361023757815260643560208201526040608435910152565b91908260609103126102375760405161241881612074565b6040808294612426816123b7565b8452602081013560208501520135910152565b919060a08382031261023757604051612451816120ac565b80938035600481101561023757825260208101356020830152604081013567ffffffffffffffff811161023757810183601f8201121561023757803561249681612313565b916124a460405193846120e4565b818352856020838301011161023757815f92602080930183860137830101526040830152606081013567ffffffffffffffff811161023757810183601f8201121561023757838160206124f99335910161232f565b606083015260808101359067ffffffffffffffff8211610237570182601f8201121561023757803561252a81612107565b9361253860405195866120e4565b8185526020606081870193028401019281841161023757602001915b838310612565575050505060800152565b60206060916125748486612400565b815201920191612554565b91908201809211611b4057565b903590601e1981360301821215610237570180359067ffffffffffffffff82116102375760200191606082023603831361023757565b90602082549182815201915f5260205f20905f5b8181106125e35750505090565b82546001600160a01b03168452602090930192600192830192016125d6565b9060405161260f81612090565b606067ffffffffffffffff600283956040516126368161262f81856125c2565b03826120e4565b85528260018201546001600160a01b038116602088015260a01c166040860152015416910152565b903590601e1981360301821215610237570180359067ffffffffffffffff82116102375760200191813603831361023757565b91908203918211611b4057565b604051906126ab826120ac565b60606080835f81525f602082015282604082015282808201520152565b8051156126d55760200190565b634e487b7160e01b5f52603260045260245ffd5b8051600110156126d55760400190565b80518210156126d55760209160051b010190565b90816020910312610237575180151581036102375790565b9060808152606067ffffffffffffffff600261274460808501866125c2565b948260018201546001600160a01b038116602088015260a01c16604086015201541691015290565b9161278261279092606085526060850190612725565b908382036020850152612219565b906040818303910152815180825260208201916020808360051b8301019401925f915b8383106127c257505050505090565b90919293946020806127e083601f1986600196030187528951612219565b970193019301919392906127b3565b906127f9816121ea565b60ff60ff198354169116179055565b90600182811c92168015612836575b602083101461282257565b634e487b7160e01b5f52602260045260245ffd5b91607f1691612817565b81811061284b575050565b5f8155600101612840565b9190601f811161286557505050565b61288f925f5260205f20906020601f840160051c83019310612891575b601f0160051c0190612840565b565b9091508190612882565b9068010000000000000000811161067d578154918181558282106128be57505050565b82600302926003840403611b405781600302916003830403611b40575f5260205f2091820191015b8181106128f1575050565b805f600392555f60018201555f6002820155016128e6565b356001600160a01b03811681036102375790565b9068010000000000000000811161067d5781549181815582821061294057505050565b82600302926003840403611b405781600302916003830403611b40575f5260205f2091820191015b818110612973575050565b805f600392555f60018201555f600282015501612968565b5f5b82811061299957505050565b60608282015260200161298d565b906129b182612107565b6129be60405191826120e4565b828152601f196129ce8294612107565b0190602036910137565b91908110156126d55760051b0190565b908215612b1f576001600160a01b0316918215918215612ae857813403612ac0577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b036020925b1693845f526001835260405f20865f52835260405f20612a5883825461257f565b905515612a69575b604051908152a3565b612abb6040517f23b872dd000000000000000000000000000000000000000000000000000000008482015233602482015230604482015282606482015260648152612ab56084826120e4565b86614666565b612a60565b7faa7feadc000000000000000000000000000000000000000000000000000000005f5260045ffd5b34612ac0577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b03602092612a37565b7f2c5211c6000000000000000000000000000000000000000000000000000000005f5260045ffd5b9060028110156126d55760011b01905f90565b908154612b6681612107565b92612b7460405194856120e4565b81845260208401905f5260205f205f915b838310612b925750505050565b60036020600192604051612ba581612074565b6001600160a01b0386541681526001600160a01b0385870154168382015260028601546040820152815201920192019190612b85565b90604051612be8816120ac565b809260ff815416612bf8816121ea565b8252600181015460208301526040516002820180545f91612c1882612808565b8085529160018116908115612d135750600114612ccf575b505090612c42816004949303826120e4565b6040840152612c5360038201612b5a565b606084015201908154612c6581612107565b92612c7360405194856120e4565b81845260208401905f5260205f205f915b838310612c95575050505060800152565b60036020600192604051612ca881612074565b60ff8654168152848601548382015260028601546040820152815201920192019190612c84565b5f9081526020812094939250905b808210612cf75750919250908101602001612c4282612c30565b9192936001816020925483858801015201910190939291612cdd565b60ff191660208087019190915292151560051b85019092019250612c429150839050612c30565b8054600110156126d5575f52600160205f2001905f90565b80548210156126d5575f5260205f2001905f90565b90604051612d74816120c8565b6020600182946001600160a01b0381541684520154910152565b9190612dbc576020816001600160a01b03806001945116166001600160a01b03198554161784550151910155565b634e487b7160e01b5f525f60045260245ffd5b60028210156126d55701905f90565b9035601e198236030181121561023757016020813591019167ffffffffffffffff821161023757606082023603831361023757565b8035600481101561023757612e27816121ea565b8252602081013560208301526040810135601e198236030181121561023757810160208135910167ffffffffffffffff82116102375781360381136102375781601f1992601f9260a060408801528160a088015260c08701375f60c08287010152011682019060c082019160e0612ea16060840184612dde565b86840360c0016060880152948590529101925f5b818110612f2757505050612ecf8160806020930190612dde565b92909360808183039101528281520191905f5b818110612eef5750505090565b90919260608060019260ff612f03886123b7565b16815260208701356020820152604087013560408201520194019101919091612ee2565b9091936060806001926001600160a01b03612f4189612135565b1681526001600160a01b03612f5860208a01612135565b16602082015260408881013590820152019501929101612eb5565b929190612f8a602091604086526040860190612e13565b930152565b91612fa5612fb392606085526060850190612725565b908382036020850152612e13565b906040818303910152828152602081019260208160051b83010193835f91609e1982360301945b848410612feb575050505050505090565b90919293949596601f1982820301835287358781121561023757602061301660019387839401612e13565b990193019401929195949390612fda565b91939290825f525f60205260405f20600381019560ff87541690600582101561035f5781156136e6575f946003831480156136d9575b61096a578435916004831015968761023757613078846121ea565b600384146106b95761309661308c87612602565b6110cd368a612439565b6040516130a2816120c8565b8754156126d557875f526001600160a01b0360205f20541681526001600160a01b0380613142816130d28c612d3a565b90549060031b1c1694602085019586526130ed366064612400565b9060405160208101918252604080820152600960608201527f6368616c6c656e6765000000000000000000000000000000000000000000000060808201526080815261313a60a0826120e4565b5190206146d3565b92511691169081141591826136c4575b505061369c57600f86019460ff8654169161035f576001146135f5578790613179816121ea565b600181036134a257506102375761318f836121ea565b6001830361340e5750506131b56131a63686612439565b6131af84612bdb565b9061420e565b156106b9575b6131d667ffffffffffffffff600185015460a01c164261257f565b9485600e850155610237576131ea916127ef565b6020820135601082015560118101613205604084018461265e565b9067ffffffffffffffff821161067d576132238261047d8554612808565b5f90601f83116001146133aa5761325092915f918361339f5750508160011b915f199060031b1c19161790565b90555b60128101613264606084018461258c565b9190613270838361289b565b905f5260205f205f915b838310613339575050505060130194613296608083018361258c565b906132a1828961291d565b965f5260205f205f975b8289106132f9575050507f2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d0375693949550600360ff198254161790556132f460405192839283612f73565b0390a2565b80359060ff8216820361023757606060039160ff6001941660ff1986541617855560208101358486015560408101356002860155019201980197906132ab565b60036060826001600160a01b03613351600195612909565b166001600160a01b031986541617855561336d60208201612909565b6001600160a01b0385870191166001600160a01b0319825416179055604081013560028601550192019201919061327a565b013590505f8061049d565b601f19831691845f5260205f20925f5b8181106133f657509084600195949392106133dd575b505050811b019055613253565b01355f19600384901b60f8161c191690555f80806133d0565b919360206001819287870135815501950192016133ba565b6001600160a01b036001860154169161343361342a3689612439565b610c5f87612bdb565b156106b95761345d926020926040518095819482936305b959ef60e01b84528c8c60048601612f8f565b03915afa908115610ef6575f91613483575b506131bb5763baf3f0f760e01b5f5260045ffd5b61349c915060203d602011610eef57610ee181836120e4565b5f61346f565b90506134ad816121ea565b8061356e575086610237576134c1836121ea565b826106b9576134dc6134d33688612439565b6131af86612bdb565b156134ea575b50505b6131bb565b6001600160a01b036001860154169161350661342a3689612439565b156106b957613530926020926040518095819482936305b959ef60e01b84528c8c60048601612f8f565b03915afa908115610ef6575f9161354f575b50156106b9575f806134e2565b613568915060203d602011610eef57610ee181836120e4565b5f613542565b600291975061357c816121ea565b036106b95761358a826121ea565b600182146106b9575f9561359d836121ea565b826135be576001600160a01b036001860154169161343361342a3689612439565b505093505f936135cd816121ea565b600281036106b9576135e26131a63686612439565b6134e55763baf3f0f760e01b5f5260045ffd5b5050505091939495505061361791506131af6136113685612439565b91612bdb565b156106b9576132f48161365461054961198c60607f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c91096018461258c565b837f2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d0375660405180613685428683612f73565b0390a2604051918291602083526020830190612e13565b7f61a44f6e000000000000000000000000000000000000000000000000000000005f5260045ffd5b516001600160a01b0316141590505f80613152565b505f95506004831461305d565b856379c1d89f60e11b5f5260045260245ffd5b903590601e1981360301821215610237570180359067ffffffffffffffff821161023757602001918160051b3603831361023757565b3567ffffffffffffffff811681036102375790565b359067ffffffffffffffff8216820361023757565b919091608081840312610237576040519061377382612090565b819381359067ffffffffffffffff8211610237578261379b606094926137c594869401612149565b85526137a960208201612135565b60208601526137ba60408201613744565b604086015201613744565b910152565b91908110156126d5576060020190565b9060026137e783806136f9565b905014801590613f7e575b8015613f50575b8015613f07575b61154157602082016001600160a01b0361381982612909565b1615613edf576040830192610e1067ffffffffffffffff6138398661372f565b1610613eb7578235600481101561023757613853816121ea565b600181036106b957602084013594856106b9576138786138733685613759565b614547565b95865f525f60205260ff600360405f20015416600581101561035f5761096a576138af6138a53686613759565b6110cd3689612439565b90608087019160016138c1848a61258c565b905003610691576138d2838961258c565b919091156126d5576138e487806136f9565b919091156126d55761390e6001600160a01b0392916110fa6139068594612909565b953690612400565b92169116036106915760608701916002613928848a61258c565b905003611fa157885f525f60205260405f209161394587806136f9565b9067ffffffffffffffff821161067d5768010000000000000000821161067d578454828655808310613e9b575b50845f5260205f205f5b838110613e805750505050600183016001600160a01b0361399c8a612909565b166001600160a01b03198254161781556139b58661372f565b7fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff7bffffffffffffffff000000000000000000000000000000000000000083549260a01b169116179055613a736002840196606089019767ffffffffffffffff613a1e8a61372f565b82547fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000016911617905560038501805460ff191660011790556004850180546001600160a01b03191633179055600f85016127ef565b601083015560118201613a8960408a018a61265e565b9067ffffffffffffffff821161067d57613aa78261047d8554612808565b5f90601f8311600114613e1c57613ad492915f918361339f5750508160011b915f199060031b1c19161790565b90555b60128201613ae5848a61258c565b9190613af1838361289b565b905f5260205f205f915b838310613db65750505050613b1460138301918961258c565b90613b1f828461291d565b915f5260205f205f925b828410613d7657505050505f91600a600683019201925b60028110613ce9575050613b56613b8191612d67565b80929060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b613b8b84806136f9565b919091156126d5576001600160a01b03613ba7613bd893612909565b165f526001602052613bbf88600160405f20016146fc565b5060206001600160a01b03825116910151908833614154565b604051936040855260c08501938035601e198236030181121561023757016020813591019467ffffffffffffffff8211610237578160051b36038613610237576080604088015281905260e0860195889590949392915f5b818110613cb5575050509467ffffffffffffffff613c9a859482613c8f613caf966001600160a01b03613c847f7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c398429c9d612135565b1660608a0152613744565b166080870152613744565b1660a084015282810360208401523396612e13565b0390a390565b91965091929394966020806001926001600160a01b03613cd48b612135565b16815201970191019189969795949392613c30565b80613d4e8a6040613d1f84613d0d88613d196020613d1360019b613d0d858b61258c565b906137ca565b01612909565b9561258c565b01356001600160a01b0360405192613d36846120c8565b1682526020820152613d488387612b47565b90612d8e565b613d70604051613d5d816120c8565b5f81525f6020820152613d488388612b47565b01613b40565b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290613b29565b60036060826001600160a01b03613dce600195612909565b166001600160a01b0319865416178555613dea60208201612909565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190613afb565b601f19831691845f5260205f20925f5b818110613e685750908460019594939210613e4f575b505050811b019055613ad7565b01355f19600384901b60f8161c191690555f8080613e42565b91936020600181928787013581550195019201613e2c565b6001906020613e8e85612909565b940193818401550161397c565b613eb190865f528360205f209182019101612840565b5f613972565b7fb4e12433000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fea9e70ce000000000000000000000000000000000000000000000000000000005f5260045ffd5b50613f1282806136f9565b156126d557613f2090612909565b613f2a83806136f9565b600110156126d5576001600160a01b03613f476020829301612909565b16911614613800565b50613f5b82806136f9565b600110156126d557613f7760206001600160a01b039201612909565b16156137f9565b50613f8982806136f9565b156126d557613f9f6001600160a01b0391612909565b16156137f2565b60028151036106b9576001600160a01b036020613fd18282613fc7866126c8565b51015116936126e9565b51015116036106b957565b906080613fe9828461406d565b91019160028351510361404b575f5b600281106140095750505050600190565b6140148185516126f9565b516001600160a01b036140358161402c8587516126f9565b511692866146d3565b160361404357600101613ff8565b505050505f90565b5050505f90565b9190915f8382019384129112908015821691151617611b4057565b61407690614547565b90805190614083826121ea565b6020810151916140c86060604084015193015192604051948593602085019788526140ad816121ea565b6040850152606084015260a0608084015260c08301906121f4565b91601f198284030160a0830152602080825194858152019101925f5b818110614106575050614100925003601f1981018352826120e4565b51902090565b91600191935061414560209186519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b940191019184929391936140e4565b8315614208576001600160a01b03165f52600160205260405f206001600160a01b0383165f528060205260405f20548481106141d8578461419491612691565b906001600160a01b0384165f5260205260405f20555f525f6020526001600160a01b03601460405f200191165f526020526141d460405f2091825461257f565b9055565b84907fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b50505050565b6040516142398161422b6020820194602086526040830190612219565b03601f1981018352826120e4565b5190209060405161425a8161422b6020820194602086526040830190612219565b5190201490565b60048101905b818110614272575050565b5f8082556001820155600201614267565b90815f525f60205260405f209060038201600460ff1982541617905560028151036106b9575f5b600281106144655750505f5b6002811061442c5750505f525f60205260405f2080545f825580614412575b505f60018201555f60028201555f6003820155614301600682016142fc8160048501612840565b614261565b61430d600a8201614261565b5f600e8201555f600f8201555f60108201556011810161432d8154612808565b90816143cf575b5050601281018054905f815581614396575b50506013018054905f81558161435a575050565b81600302916003830403611b40575f5260205f20908101905b81811061437e575050565b805f600392555f60018201555f600282015501614373565b81600302916003830403611b40575f5260205f20908101905b8181101561434657805f600392555f60018201555f6002820155016143af565b81601f5f93116001146143e65750555b5f80614334565b8183526020832061440291601f0160051c810190600101612840565b80825281602081209155556143df565b61442690825f5260205f2090810190612840565b5f6142d5565b806001600160a01b0361444160019385612d52565b90549060031b1c165f528160205261445e848360405f2001614764565b50016142b6565b8061447c614475600193856126f9565b51866145b3565b016142aa565b602060405180927fcc2a842d00000000000000000000000000000000000000000000000000000000825260406004830152816001600160a01b03816144df6144cd604483018a612219565b6003198382030160248401528a612219565b0392165afa5f918161450a575b506144ff57506020809101519101511090565b90505f8092500b1390565b9091506020813d60201161453f575b81614526602093836120e4565b810103126102375751805f0b810361023757905f6144ec565b3d9150614519565b8051906141006001600160a01b036020830151169167ffffffffffffffff606081604084015116920151169260405193849261458f602085019760a0895260c08601906121ae565b926040850152606084015260808301524660a083015203601f1981018352826120e4565b906040810191825115614661575f525f602052601460405f20019160208201916001600160a01b0380845116165f528360205260405f205493841561465a576001600160a01b0392518086115f1461464f57614610908096612691565b908380865116165f5260205260405f205551165f5260016020526001600160a01b038060405f20925116165f526020526141d460405f2091825461257f565b506146108580612691565b5050505050565b505050565b905f602091828151910182855af115610ef6575f513d6146ca57506001600160a01b0381163b155b6146955750565b6001600160a01b03907f5274afe7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b6001141561468e565b6146f0906146f99260ff8151166040602083015192015192614821565b909291926148a3565b90565b6001810190825f528160205260405f2054155f1461404b5780546801000000000000000081101561067d5761475161473b826001879401855584612d52565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b906001820191815f528260205260405f20548015155f14614043575f198101818111611b405782545f19810191908211611b40578181036147ec575b505050805480156147d8575f1901906147b98282612d52565b8154905f199060031b1b19169055555f526020525f6040812055600190565b634e487b7160e01b5f52603160045260245ffd5b61480c6147fc61473b9386612d52565b90549060031b1c92839286612d52565b90555f528360205260405f20555f80806147a0565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411614898579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610ef6575f516001600160a01b0381161561488e57905f905f90565b505f906001905f90565b5050505f9160039190565b6148ac816121ea565b806148b5575050565b6148be816121ea565b600181036148ee577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b6148f7816121ea565b6002810361492b57507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b600390614937816121ea565b1461493f5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffdfea26469706673582212201a22c42288f1ba0e9314d3b1e6e8b7aa4d4a62d815a181d9b949bb05ac25096564736f6c634300081b0033", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes\",\"name\":\"challengerSig\",\"type\":\"bytes\"}],\"name\":\"challenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"checkpoint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"name\":\"close\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"create\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"depositAndCreate\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eip712Domain\",\"outputs\":[{\"internalType\":\"bytes1\",\"name\":\"fields\",\"type\":\"bytes1\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"verifyingContract\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"},{\"internalType\":\"uint256[]\",\"name\":\"extensions\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getAccountsBalances\",\"outputs\":[{\"internalType\":\"uint256[][]\",\"name\":\"\",\"type\":\"uint256[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getChannelBalances\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"balances\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"getChannelData\",\"outputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"internalType\":\"enumChannelStatus\",\"name\":\"status\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"wallets\",\"type\":\"address[]\"},{\"internalType\":\"uint256\",\"name\":\"challengeExpiry\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"lastValidState\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"}],\"name\":\"getOpenChannels\",\"outputs\":[{\"internalType\":\"bytes32[][]\",\"name\":\"\",\"type\":\"bytes32[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"sig\",\"type\":\"bytes\"}],\"name\":\"join\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"resize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"expiration\",\"type\":\"uint256\"}],\"name\":\"Challenged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"}],\"name\":\"Checkpointed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"finalState\",\"type\":\"tuple\"}],\"name\":\"Closed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"indexed\":false,\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"Created\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposited\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"EIP712DomainChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"Joined\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"Opened\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"int256[]\",\"name\":\"deltaAllocations\",\"type\":\"int256[]\"}],\"name\":\"Resized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdrawn\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"ChallengeNotExpired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ChannelNotFinal\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"ChannelNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DepositAlreadyFulfilled\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expectedFulfilled\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"actualFulfilled\",\"type\":\"uint256\"}],\"name\":\"DepositsNotFulfilled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ECDSAInvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"ECDSAInvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"ECDSAInvalidSignatureS\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"available\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"required\",\"type\":\"uint256\"}],\"name\":\"InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAdjudicator\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAllocations\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAmount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengePeriod\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengerSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidParticipant\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidShortString\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidState\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStateSignatures\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidValue\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"str\",\"type\":\"string\"}],\"name\":\"StringTooLong\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TransferFailed\",\"type\":\"error\"}]", + Bin: "0x0x610160604052346101365760405161001860408261013a565b601181526020810190704e6974726f6c6974653a437573746f647960781b82526040519161004760408461013a565b600683526020830191650c0b8c8b8c8d60d21b835261006581610171565b6101205261007284610307565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a081526100db60c08261013a565b5190206080523060c0526040516153549081610440823960805181614a97015260a05181614b4e015260c05181614a68015260e05181614ae601526101005181614b0c01526101205181611068015261014051816110940152f35b5f80fd5b601f909101601f19168101906001600160401b0382119082101761015d57604052565b634e487b7160e01b5f52604160045260245ffd5b908151602081105f146101eb575090601f8151116101ab57602081519101516020821061019c571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b03811161015d575f54600181811c911680156102fd575b60208210146102e957601f81116102b7575b50602092601f821160011461025857928192935f9261024d575b50508160011b915f199060031b1c1916175f5560ff90565b015190505f80610235565b601f198216935f8052805f20915f5b86811061029f5750836001959610610287575b505050811b015f5560ff90565b01515f1960f88460031b161c191690555f808061027a565b91926020600181928685015181550194019201610267565b5f8052601f60205f20910160051c810190601f830160051c015b8181106102de575061021b565b5f81556001016102d1565b634e487b7160e01b5f52602260045260245ffd5b90607f1690610209565b908151602081105f14610332575090601f8151116101ab57602081519101516020821061019c571790565b6001600160401b03811161015d57600154600181811c91168015610435575b60208210146102e957601f8111610402575b50602092601f82116001146103a157928192935f92610396575b50508160011b915f199060031b1c19161760015560ff90565b015190505f8061037d565b601f1982169360015f52805f20915f5b8681106103ea57508360019596106103d2575b505050811b0160015560ff90565b01515f1960f88460031b161c191690555f80806103c4565b919260206001819286850151815501940192016103b1565b60015f52601f60205f20910160051c810190601f830160051c015b81811061042a5750610363565b5f815560010161041d565b90607f169061035156fe60806040526004361015610011575f80fd5b5f3560e01c8062e2bb2c146123555780631474e410146122d1578063183b499814611af05780632f33c4d6146119365780634a7e7798146118ca5780635a9eb80e146118285780637f9ebbd7146111705780638340f5491461113957806384b0196e14611050578063bab3290a14610a6e578063d710e92f146108e0578063e617208c1461078d578063ecf668fd146102455763f3fef3a3146100b2575f80fd5b34610241576040600319360112610241576100cb6123cb565b60243590335f52600360205260405f20906001600160a01b0381165f528160205260405f205491838310610211576001600160a01b0392508282165f5260205260405f2061011a8482546137c5565b90551690816101bf575f80808084335af13d156101ba573d61013b8161275c565b90610149604051928361251b565b81525f60203d92013e5b15610187575b6040519081527fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb60203392a3005b907fbf182be8000000000000000000000000000000000000000000000000000000005f526004523360245260445260645ffd5b610153565b61020c6040517fa9059cbb0000000000000000000000000000000000000000000000000000000060208201523360248201528260448201526044815261020660648261251b565b83614fc1565b610159565b50507fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5f80fd5b346102415761025336612454565b835f52600260205260405f2091600383019160ff835416906005821015610779578115610766576004821461073e578535926004841015610241576102978461264f565b8361064257600f86019260ff84541690600181145f146102d9577ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b60020361068b57506001600160a01b036001870154169161030d6102fd368a612854565b61030686612af3565b9085614858565b15610642576103379260209260405180958194829363030232af60e21b84528d8d60048601612e43565b03915afa908115610680575f91610651575b501561064257610364925b600260ff19825416179055612ee8565b602082013560108201556011810161037f6040840184612f01565b9067ffffffffffffffff8211610559576103a38261039d85546129b8565b85612f4a565b5f90601f83116001146105de576103d192915f91836105d3575b50508160011b915f199060031b1c19161790565b90555b601281016103e56060840184612bb1565b91906103f18383612f8f565b905f5260205f205f915b83831061056d578686601387016104156080830183613011565b90916104218282613095565b5f9081526020812092805b83831061047257867f8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c361046d88604051918291602083526020830190612c6e565b0390a2005b61047c8183612f01565b9067ffffffffffffffff8211610559576104a08261049a89546129b8565b89612f4a565b5f90601f83116001146104ef57926104d5836001959460209487965f926104e45750508160011b915f199060031b1c19161790565b88555b0195019201919361042c565b013590508d806103bd565b601f19831691885f5260205f20925f5b8181106105415750936020936001969387969383889510610528575b505050811b0188556104d8565b01355f19600384901b60f8161c191690558c808061051b565b919360206001819287870135815501950192016104ff565b634e487b7160e01b5f52604160045260245ffd5b60036060826001600160a01b03610585600195612ffd565b166001600160a01b03198654161785556105a160208201612ffd565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906103fb565b0135905087806103bd565b601f19831691845f5260205f20925f5b81811061062a5750908460019594939210610611575b505050811b0190556103d4565b01355f19600384901b60f8161c19169055868080610604565b919360206001819287870135815501950192016105ee565b63baf3f0f760e01b5f5260045ffd5b610673915060203d602011610679575b61066b818361251b565b810190612de4565b87610349565b503d610661565b6040513d5f823e3d90fd5b909160206106bf916001600160a01b0360018a01541694604051938492839263030232af60e21b84528d8d60048601612e43565b0381865afa908115610680575f9161071f575b5015610642576106e18161264f565b156106f7575b50610364925f600e860155610354565b610714906107053688612854565b61070e84612af3565b91614858565b1561064257866106e7565b610738915060203d6020116106795761066b818361251b565b896106d2565b7ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b866379c1d89f60e11b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b34610241576020600319360112610241575f60606040516107ad816124c7565b81815282602082015282604082015201526107c66137d2565b506004355f52600260205260405f206107de81613770565b60ff60038301541691604051906107f660608361251b565b600282526040366020840137600481015f5b600281106108b2575050610823600f600e8301549201612af3565b6040519460a0865267ffffffffffffffff606061084d8751608060a08b01526101208a0190612613565b966001600160a01b0360208201511660c08a01528260408201511660e08a015201511661010087015260058110156107795785946108ae9461089b9260208801528682036040880152612613565b9160608501528382036080850152612659565b0390f35b806001600160a01b036108c76001938561394b565b90549060031b1c166108d9828761382d565b5201610808565b346102415760206003193601126102415760043567ffffffffffffffff811161024157610911903690600401612556565b8051906109366109208361253e565b9261092e604051948561251b565b80845261253e565b90610949601f19602085019301836138c4565b5f5b81518110156109dd576001600160a01b03610966828461382d565b51165f526003602052600160405f20016040519081602082549182815201915f5260205f20905f905b8082106109c557505050906109a98160019493038261251b565b6109b3828761382d565b526109be818661382d565b500161094b565b9091926001602081928654815201940192019061098f565b50509060405191829160208301906020845251809152604083019060408160051b85010192915f905b828210610a1557505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b808210610a56575050506020806001929601920192018594939192610a06565b90919260208060019286518152019401920190610a36565b346102415760606003193601126102415760043560443567ffffffffffffffff811161024157610aa2903690600401612426565b825f93929352600260205260405f2090600382019060ff825416600581101561077957801561103d575f190161073e5760016024350361101557600d830154610fed57600f830194610b2b610af687612af3565b610afe614a5e565b906001600160a01b03610b10886143d0565b90549060031b1c169188610b25368888612778565b92614b74565b15610fc557610b3986612af3565b90610b8960405191610b4c60608461251b565b60028352610b5e6040602085016138c4565b6080840194610b6d86516137fc565b51610b77856137fc565b52610b81846137fc565b503691612778565b610b928261381d565b52610b9c8161381d565b50825260018401546001600160a01b0316604051906020610bbd818461251b565b5f8352601f19015f5b818110610fae575050610bf391602091604051808095819463030232af60e21b8352888c60048501613841565b03915afa908115610680575f91610f8f575b501561064257610c71610c1a600886016139be565b96610c4c88600c88019060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b6005860180546001600160a01b03191633179055825190610c6c8261264f565b612ee8565b6020810151601085015560118401604082015180519067ffffffffffffffff821161055957610ca48261039d85546129b8565b602090601f8311600114610f2c57610cd292915f9183610f215750508160011b915f199060031b1c19161790565b90555b606060128501910151906020825192610cee8484612f8f565b01905f5260205f205f915b838310610ebe57505050506013830190516020815191610d198385613095565b01915f5260205f20915f905b828210610ddd57845460ff19166002178555602087610d858a6001600160a01b03610d4f8b6143d0565b90549060031b1c165f5260038452610d6d83600160405f2001614f59565b50836001600160a01b03825116910151908333614d06565b807fe8e915db7b3549b9e9e9b3e2ec2dc3edd1f76961504366998824836401f6846a8360405160018152a260405190807fd087f17acc177540af5f382bc30c65363705b90855144d285a822536ee11fdd15f80a28152f35b805180519067ffffffffffffffff821161055957610e0582610dff88546129b8565b88612f4a565b602090601f8311600114610e555792610e3b836001959460209487965f92610e4a5750508160011b915f199060031b1c19161790565b87555b01940191019092610d25565b015190508e806103bd565b90601f19831691875f52815f20925f5b818110610ea65750936020936001969387969383889510610e8e575b505050811b018755610e3e565b01515f1960f88460031b161c191690558d8080610e81565b92936020600181928786015181550195019301610e65565b60036020826040600194516001600160a01b0380825116166001600160a01b03198854161787556001600160a01b0384820151166001600160a01b0387890191166001600160a01b03198254161790550151600286015501920192019190610cf9565b015190508a806103bd565b90601f19831691845f52815f20925f5b818110610f775750908460019594939210610f5f575b505050811b019055610cd5565b01515f1960f88460031b161c19169055898080610f52565b92936020600181928786015181550195019301610f3c565b610fa8915060203d6020116106795761066b818361251b565b87610c05565b602090610fb96137d2565b82828701015201610bc6565b7f773a750f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1b136079000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa145c43e000000000000000000000000000000000000000000000000000000005f5260045ffd5b846379c1d89f60e11b5f5260045260245ffd5b34610241575f6003193601126102415761110b61108c7f000000000000000000000000000000000000000000000000000000000000000061502e565b6108ae6110b87f000000000000000000000000000000000000000000000000000000000000000061509f565b611119604051916110ca60208461251b565b5f83525f3681376040519586957f0f00000000000000000000000000000000000000000000000000000000000000875260e0602088015260e08701906125ee565b9085820360408701526125ee565b904660608501523060808501525f60a085015283820360c08501526125bb565b60606003193601126102415761114d6123cb565b602435906001600160a01b03821682036102415761116e9160443591614271565b005b346102415761117e36612454565b5050815f52600260205260405f2060ff600382015416600581101561077957801561181557600281036114e5575081356004811015610241576111c08161264f565b60038103610642576020830135801561064257608084019160026111e48487613011565b905003610fc5576112076111f785613770565b6112013688612854565b90614953565b15610fc55761121990600f8501612ee8565b60108301556011820161122f6040850185612f01565b9067ffffffffffffffff82116105595761124d8261039d85546129b8565b5f90601f83116001146114815761127a92915f91836114765750508160011b915f199060031b1c19161790565b90555b6012820161128e6060850185612bb1565b919061129a8383612f8f565b905f5260205f205f915b83831061141057505050506112bd601383019184613011565b90916112c98282613095565b5f9081526020812092805b83831061132f5750505050507fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a704969161131b611315601261046d945b01612a72565b856146b8565b604051918291602083526020830190612c6e565b6113398183612f01565b9067ffffffffffffffff8211610559576113578261049a89546129b8565b5f90601f83116001146113a6579261138c836001959460209487965f9261139b5750508160011b915f199060031b1c19161790565b88555b019501920191936112d4565b013590508e806103bd565b601f19831691885f5260205f20925f5b8181106113f857509360209360019693879693838895106113df575b505050811b01885561138f565b01355f19600384901b60f8161c191690558d80806113d2565b919360206001819287870135815501950192016113b6565b60036060826001600160a01b03611428600195612ffd565b166001600160a01b031986541617855561144460208201612ffd565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906112a4565b0135905088806103bd565b601f19831691845f5260205f20925f5b8181106114cd57509084600195949392106114b4575b505050811b01905561127d565b01355f19600384901b60f8161c191690558780806114a7565b91936020600181928787013581550195019201611491565b60030361073e57600e810180544210156117e157823560048110156102415761150d8161264f565b600381036106425761152b61152184613770565b6112013687612854565b15610fc5575f61153f9255600f8301612ee8565b602082013560108201556011810161155a6040840184612f01565b9067ffffffffffffffff8211610559576115788261039d85546129b8565b5f90601f831160011461177d576115a592915f91836105d35750508160011b915f199060031b1c19161790565b90555b601281016115b96060840184612bb1565b91906115c58383612f8f565b905f5260205f205f915b8383106117175750505050601381016115eb6080840184613011565b90916115f78282613095565b5f9081526020812092805b8383106116415750505050507fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a704969161131b611315601261046d9461130f565b61164b8183612f01565b9067ffffffffffffffff8211610559576116698261049a89546129b8565b5f90601f83116001146116ad579261169e836001959460209487965f9261139b5750508160011b915f199060031b1c19161790565b88555b01950192019193611602565b601f19831691885f5260205f20925f5b8181106116ff57509360209360019693879693838895106116e6575b505050811b0188556116a1565b01355f19600384901b60f8161c191690558d80806116d9565b919360206001819287870135815501950192016116bd565b60036060826001600160a01b0361172f600195612ffd565b166001600160a01b031986541617855561174b60208201612ffd565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906115cf565b601f19831691845f5260205f20925f5b8181106117c957509084600195949392106117b0575b505050811b0190556115a8565b01355f19600384901b60f8161c191690558680806117a3565b9193602060018192878701358155019501920161178d565b507fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a704969161131b611315601261046d9461130f565b836379c1d89f60e11b5f5260045260245ffd5b346102415760406003193601126102415760043560243567ffffffffffffffff81116102415761185c903690600401612556565b61186681516138e0565b5f5b82518110156118b457600190845f526002602052601460405f20016001600160a01b0380611896848861382d565b5116165f5260205260405f20546118ad828561382d565b5201611868565b604051602080825281906108ae908201856125bb565b346102415760406003193601126102415760043567ffffffffffffffff811161024157608060031982360301126102415760243567ffffffffffffffff81116102415760a060031982360301126102415760209161192e91600401906004016139e5565b604051908152f35b346102415760406003193601126102415760043567ffffffffffffffff8111610241576119679036906004016123f5565b60243567ffffffffffffffff8111610241576119879036906004016123f5565b9190926119938261253e565b936119a1604051958661251b565b8285526119ad8361253e565b936119c0601f19602088019601866138c4565b5f5b848110611a5957858760405191829160208301906020845251809152604083019060408160051b85010192915f905b828210611a0057505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b808210611a415750505060208060019296019201920185949391926119f1565b90919260208060019286518152019401920190611a21565b611a62826138e0565b611a6c828961382d565b52611a77818861382d565b505f5b828110611a8a57506001016119c2565b6001906001600160a01b03611aa8611aa3858a8a613911565b612ffd565b165f52600360205260405f206001600160a01b03611aca611aa384888a613911565b165f5260205260405f2054611ae982611ae3868d61382d565b5161382d565b5201611a7a565b3461024157611afe36612454565b9190835f52600260205260405f209360ff60038601541660058110156107795780156122be577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0161073e57831561064257823592600484101561024157611b658461264f565b6002840361064257823593609e1984360301948581121561024157611b8d9085013690612854565b966020830135602089015160018101809111611d81578103610642576060890193611bb8855161491d565b6060810191611bd9611bd4611bcd8585612bb1565b36916127cc565b61491d565b611bef611be585613770565b6112013685612854565b15610fc557611c016040830183612f01565b810195906020818803126102415780359067ffffffffffffffff821161024157019686601f8901121561024157873596611c3a8861253e565b98611c486040519a8b61251b565b888a5260208a01906020829a60051b82010192831161024157602001905b8282106122ae5750505051611c7e611bcd8686612bb1565b90600289510361064257611cb0816040611ca781611c9e611ce7966137fc565b5101519261381d565b51015190612edb565b91611ce1611cd1611cc08c6137fc565b51611cca8d61381d565b51906149d1565b916040611ca781611c9e846137fc565b926149d1565b03612286575f198b019b8b8d11611d8157611d018d61253e565b9c806040519e8f90611d13908261251b565b52611d1d9061253e565b601f19018d5f5b82811061226d5750505060015b8c811015611d95578060051b8b01358c811215610241575f19820191908c01818311611d81578f92600193611d6a611d7a933690612854565b611d74838361382d565b5261382d565b5001611d31565b634e487b7160e01b5f52601160045260245ffd5b5060208d611dc9926001600160a01b0360018a0154169060405180958194829363030232af60e21b84528d60048501613841565b03915afa908115610680575f9161224e575b501561064257611dee90600f8601612ee8565b601084015560118301611e046040830183612f01565b9067ffffffffffffffff821161055957611e228261039d85546129b8565b5f90601f83116001146121ea57611e4f92915f91836121df5750508160011b915f199060031b1c19161790565b90555b60128301611e608383612bb1565b9190611e6c8383612f8f565b905f5260205f205f915b838310612179575050505060138301611e926080830183613011565b9091611e9e8282613095565b5f9081526020812092805b838310612098575050505050611ec291611bcd91612bb1565b9060068101916001600160a01b038354165f5b6002811061203f57505f5b60028110611f9f575050600a5f9201915b60028110611f5c575050505060405191602083019060208452518091526040830191905f5b818110611f4657857ff3b6c524f73df7344d9fcf2f960a57aba7fba7e292d8b79ed03d786f7b2b112f86860387a2005b8251845260209384019390920191600101611f16565b806040611f6b6001938561382d565b51015182611f79838861396a565b5001556040611f88828561382d565b51015182611f96838761396a565b50015501611ef1565b5f611fad828997969761382d565b5112611fbf575b600101939293611ee0565b6001600160a01b03611fd4826004880161394b565b90549060031b1c1690611fe7818961382d565b517f80000000000000000000000000000000000000000000000000000000000000008114611d81576001926120389160405191612023836124ab565b82528560208301525f0360408201528a614ea5565b9050611fb4565b805f6120506001938a98979861382d565b5113612060575b01939293611ed5565b6120936001600160a01b036120788360048a0161394b565b90549060031b1c16848b61208c858d61382d565b5192614d06565b612057565b6120a28183612f01565b9067ffffffffffffffff8211610559576120c08261049a89546129b8565b5f90601f831160011461210f57926120f5836001959460209487965f926121045750508160011b915f199060031b1c19161790565b88555b01950192019193611ea9565b013590505f806103bd565b601f19831691885f5260205f20925f5b8181106121615750936020936001969387969383889510612148575b505050811b0188556120f8565b01355f19600384901b60f8161c191690555f808061213b565b9193602060018192878701358155019501920161211f565b60036060826001600160a01b03612191600195612ffd565b166001600160a01b03198654161785556121ad60208201612ffd565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190611e76565b013590508a806103bd565b601f19831691845f5260205f20925f5b818110612236575090846001959493921061221d575b505050811b019055611e52565b01355f19600384901b60f8161c19169055898080612210565b919360206001819287870135815501950192016121fa565b612267915060203d6020116106795761066b818361251b565b89611ddb565b602091828261227a6137d2565b92010152018e90611d24565b7f52e4cb1c000000000000000000000000000000000000000000000000000000005f5260045ffd5b8135815260209182019101611c66565b506379c1d89f60e11b5f5260045260245ffd5b346102415760806003193601126102415760243567ffffffffffffffff81116102415760a060031982360301126102415760443567ffffffffffffffff8111610241576123229036906004016123f5565b916064359267ffffffffffffffff84116102415761234761116e943690600401612426565b9390926004016004356130e1565b6080600319360112610241576123696123cb565b6044359067ffffffffffffffff82116102415760806003198336030112610241576064359167ffffffffffffffff83116102415760a06003198436030112610241576020926123bf61192e936024359033614271565b600401906004016139e5565b600435906001600160a01b038216820361024157565b35906001600160a01b038216820361024157565b9181601f840112156102415782359167ffffffffffffffff8311610241576020808501948460051b01011161024157565b9181601f840112156102415782359167ffffffffffffffff8311610241576020838186019501011161024157565b6060600319820112610241576004359160243567ffffffffffffffff81116102415760a0600319828503011261024157600401916044359067ffffffffffffffff8211610241576124a7916004016123f5565b9091565b6060810190811067ffffffffffffffff82111761055957604052565b6080810190811067ffffffffffffffff82111761055957604052565b60a0810190811067ffffffffffffffff82111761055957604052565b6040810190811067ffffffffffffffff82111761055957604052565b90601f601f19910116810190811067ffffffffffffffff82111761055957604052565b67ffffffffffffffff81116105595760051b60200190565b9080601f8301121561024157813561256d8161253e565b9261257b604051948561251b565b81845260208085019260051b82010192831161024157602001905b8282106125a35750505090565b602080916125b0846123e1565b815201910190612596565b90602080835192838152019201905f5b8181106125d85750505090565b82518452602093840193909201916001016125cb565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b90602080835192838152019201905f5b8181106126305750505090565b82516001600160a01b0316845260209384019390920191600101612623565b6004111561077957565b80516126648161264f565b825260208101516020830152612689604082015160a0604085015260a08401906125ee565b906060810151918381036060850152602080845192838152019301905f5b8181106127145750505060800151916080818303910152815180825260208201916020808360051b8301019401925f915b8383106126e757505050505090565b909192939460208061270583601f19866001960301875289516125ee565b970193019301919392906126d8565b909193602061275260019287519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b95019291016126a7565b67ffffffffffffffff811161055957601f01601f191660200190565b9291926127848261275c565b91612792604051938461251b565b829481845281830111610241578281602093845f960137010152565b9080601f83011215610241578160206127c993359101612778565b90565b9291926127d88261253e565b936127e6604051958661251b565b606060208685815201930282019181831161024157925b82841061280a5750505050565b606084830312610241576020606091604051612825816124ab565b61282e876123e1565b815261283b8388016123e1565b83820152604087013560408201528152019301926127fd565b919060a0838203126102415760405161286c816124e3565b80938035600481101561024157825260208101356020830152604081013567ffffffffffffffff811161024157836128a59183016127ae565b6040830152606081013567ffffffffffffffff811161024157810183601f8201121561024157838160206128db933591016127cc565b606083015260808101359067ffffffffffffffff821161024157019180601f8401121561024157823561290d8161253e565b9361291b604051958661251b565b81855260208086019260051b820101918383116102415760208201905b83821061294a57505050505060800152565b813567ffffffffffffffff81116102415760209161296d878480948801016127ae565b815201910190612938565b90602082549182815201915f5260205f20905f5b8181106129995750505090565b82546001600160a01b031684526020909301926001928301920161298c565b90600182811c921680156129e6575b60208310146129d257565b634e487b7160e01b5f52602260045260245ffd5b91607f16916129c7565b5f92918154916129ff836129b8565b8083529260018116908115612a545750600114612a1b57505050565b5f9081526020812093945091925b838310612a3a575060209250010190565b600181602092949394548385870101520191019190612a29565b9050602094955060ff1991509291921683830152151560051b010190565b908154612a7e8161253e565b92612a8c604051948561251b565b81845260208401905f5260205f205f915b838310612aaa5750505050565b60036020600192604051612abd816124ab565b6001600160a01b0386541681526001600160a01b0385870154168382015260028601546040820152815201920192019190612a9d565b90604051612b00816124e3565b6004819360ff815416612b128161264f565b835260018101546020840152604051612b3981612b3281600286016129f0565b038261251b565b6040840152612b4a60038201612a72565b606084015201908154612b5c8161253e565b92612b6a604051948561251b565b81845260208401905f5260205f205f915b838310612b8c575050505060800152565b600160208192604051612ba381612b3281896129f0565b815201920192019190612b7b565b903590601e1981360301821215610241570180359067ffffffffffffffff82116102415760200191606082023603831361024157565b9035601e198236030181121561024157016020813591019167ffffffffffffffff821161024157813603831361024157565b601f8260209493601f1993818652868601375f8582860101520116010190565b9035601e198236030181121561024157016020813591019167ffffffffffffffff8211610241578160051b3603831361024157565b8035600481101561024157612c828161264f565b825260208101356020830152612caf612c9e6040830183612be7565b60a0604086015260a0850191612c19565b906060810135601e19823603018112156102415781016020813591019267ffffffffffffffff82116102415760608202360384136102415784810360608601528181526020019392905f5b818110612d7a57505050806080612d12920190612c39565b90916080818503910152808352602083019260208260051b82010193835f925b848410612d425750505050505090565b909192939495602080612d6a83601f198660019603018852612d648b88612be7565b90612c19565b9801940194019294939190612d32565b909193946060806001926001600160a01b03612d95896123e1565b1681526001600160a01b03612dac60208a016123e1565b1660208201526040888101359082015201969501929101612cfa565b929190612ddf602091604086526040860190612c6e565b930152565b90816020910312610241575180151581036102415790565b9060808152606067ffffffffffffffff6002612e1b6080850186612978565b948260018201546001600160a01b038116602088015260a01c16604086015201541691015290565b91612e59612e6792606085526060850190612dfc565b908382036020850152612c6e565b906040818303910152828152602081019260208160051b83010193835f91609e1982360301945b848410612e9f575050505050505090565b90919293949596601f19828203018352873587811215610241576020612eca60019387839401612c6e565b990193019401929195949390612e8e565b91908201809211611d8157565b90612ef28161264f565b60ff60ff198354169116179055565b903590601e1981360301821215610241570180359067ffffffffffffffff82116102415760200191813603831361024157565b818110612f3f575050565b5f8155600101612f34565b9190601f8111612f5957505050565b612f83925f5260205f20906020601f840160051c83019310612f85575b601f0160051c0190612f34565b565b9091508190612f76565b9068010000000000000000811161055957815491818155828210612fb257505050565b82600302926003840403611d815781600302916003830403611d81575f5260205f2091820191015b818110612fe5575050565b805f600392555f60018201555f600282015501612fda565b356001600160a01b03811681036102415790565b903590601e1981360301821215610241570180359067ffffffffffffffff821161024157602001918160051b3603831361024157565b61305181546129b8565b908161305b575050565b81601f5f931160011461306c575055565b8183526020832061308891601f0160051c810190600101612f34565b8082528160208120915555565b90680100000000000000008111610559578154918181558282106130b857505050565b5f5260205f2091820191015b8181106130cf575050565b806130db600192613047565b016130c4565b93909195949295845f52600260205260405f2091600383019760ff89541692600584101561077957831561375d575f600385148015613751575b61073e5786359360048510159889610241576131368661264f565b600386146106425761316361317161317893613152368d612854565b9361316a8c60405195868092612978565b038561251b565b3691612778565b918c6143fd565b600f86019460ff86541691610779576001146136aa5787906131998161264f565b600181036135575750610241576131af8361264f565b600183036134c35750506131d56131c63686612854565b6131cf84612af3565b90614651565b15610642575b6131f667ffffffffffffffff600185015460a01c1642612edb565b9485600e8501556102415761320a91612ee8565b60208201356010820155601181016132256040840184612f01565b9067ffffffffffffffff8211610559576132438261039d85546129b8565b5f90601f831160011461345f5761327092915f91836121045750508160011b915f199060031b1c19161790565b90555b601281016132846060840184612bb1565b91906132908383612f8f565b905f5260205f205f915b8383106133f95750505050601301946132b66080830183613011565b90966132c28282613095565b5f9081526020812097805b83831061331c57505050507f44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e1793949550600360ff1982541617905561331760405192839283612dc8565b0390a2565b6133268183612f01565b9067ffffffffffffffff82116105595761334b828d61334581546129b8565b90612f4a565b5f90601f831160011461338f5792613380836001959460209487965f926121045750508160011b915f199060031b1c19161790565b8d555b019a01920191986132cd565b601f198316918d5f5260205f20925f5b8181106133e157509360209360019693879693838895106133c8575b505050811b018d55613383565b01355f19600384901b60f8161c191690555f80806133bb565b9193602060018192878701358155019501920161339f565b60036060826001600160a01b03613411600195612ffd565b166001600160a01b031986541617855561342d60208201612ffd565b6001600160a01b0385870191166001600160a01b0319825416179055604081013560028601550192019201919061329a565b601f19831691845f5260205f20925f5b8181106134ab5750908460019594939210613492575b505050811b019055613273565b01355f19600384901b60f8161c191690555f8080613485565b9193602060018192878701358155019501920161346f565b6001600160a01b03600186015416916134e86134df3689612854565b61030687612af3565b15610642576135129260209260405180958194829363030232af60e21b84528c8c60048601612e43565b03915afa908115610680575f91613538575b506131db5763baf3f0f760e01b5f5260045ffd5b613551915060203d6020116106795761066b818361251b565b5f613524565b90506135628161264f565b80613623575086610241576135768361264f565b82610642576135916135883688612854565b6131cf86612af3565b1561359f575b50505b6131db565b6001600160a01b03600186015416916135bb6134df3689612854565b15610642576135e59260209260405180958194829363030232af60e21b84528c8c60048601612e43565b03915afa908115610680575f91613604575b5015610642575f80613597565b61361d915060203d6020116106795761066b818361251b565b5f6135f7565b60029197506136318161264f565b036106425761363f8261264f565b60018214610642575f956136528361264f565b82613673576001600160a01b03600186015416916134e86134df3689612854565b505093505f936136828161264f565b60028103610642576136976131c63686612854565b61359a5763baf3f0f760e01b5f5260045ffd5b505050509193949550506136cc91506131cf6136c63685612854565b91612af3565b156106425761331781613709611315611bcd60607fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496960184612bb1565b837f44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e176040518061373a428683612dc8565b0390a2604051918291602083526020830190612c6e565b50505f6004851461311b565b876379c1d89f60e11b5f5260045260245ffd5b9060405161377d816124c7565b606067ffffffffffffffff6002839560405161379d81612b328185612978565b85528260018201546001600160a01b038116602088015260a01c166040860152015416910152565b91908203918211611d8157565b604051906137df826124e3565b60606080835f81525f602082015282604082015282808201520152565b8051156138095760200190565b634e487b7160e01b5f52603260045260245ffd5b8051600110156138095760400190565b80518210156138095760209160051b010190565b9161385761386592606085526060850190612dfc565b908382036020850152612659565b906040818303910152815180825260208201916020808360051b8301019401925f915b83831061389757505050505090565b90919293946020806138b583601f1986600196030187528951612659565b97019301930191939290613888565b5f5b8281106138d257505050565b6060828201526020016138c6565b906138ea8261253e565b6138f7604051918261251b565b828152601f19613907829461253e565b0190602036910137565b91908110156138095760051b0190565b3567ffffffffffffffff811681036102415790565b359067ffffffffffffffff8216820361024157565b60028210156138095701905f90565b9190811015613809576060020190565b9060028110156138095760011b01905f90565b91906139ab576020816001600160a01b03806001945116166001600160a01b03198554161784550151910155565b634e487b7160e01b5f525f60045260245ffd5b906040516139cb816124ff565b6020600182946001600160a01b0381541684520154910152565b9060026139f28380613011565b905014801590614249575b801561421b575b80156141d2575b61101557602082016001600160a01b03613a2482612ffd565b16156141aa576040830192610e1067ffffffffffffffff613a4486613921565b161061418257823593600485101561024157613a5f8561264f565b600185036106425760208401359081610642576080833603126102415760405191613a89836124c7565b833567ffffffffffffffff811161024157613aa79036908601612556565b8352613ab2856123e1565b6020840152613ac082613936565b6040840152613ae06060850193613ad685613936565b60608201526149ec565b96875f52600260205260ff600360405f2001541660058110156107795761073e57608087016001613b11828a613011565b905003610fc557613b223689612854565b613b2a614a5e565b90613b35838b613011565b156138095780613b4491612f01565b919092613b518a80613011565b9490941561380957610b258e91613b6a613b7297612ffd565b953691612778565b15610fc55760608801926002613b88858b612bb1565b90500361228657895f52600260205260405f2092613ba68880613011565b9067ffffffffffffffff821161055957680100000000000000008211610559578554828755808310614166575b50855f5260205f205f5b83811061414b5750505050613ccf90600185016001600160a01b03613c018c612ffd565b166001600160a01b0319825416178155613c1a88613921565b7fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff7bffffffffffffffff000000000000000000000000000000000000000083549260a01b1691161790556002850167ffffffffffffffff613c7a8a613921565b82547fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000016911617905560038501805460ff191660011790556004850180546001600160a01b03191633179055600f8501612ee8565b601083015560118201613ce560408a018a612f01565b9067ffffffffffffffff821161055957613d038261039d85546129b8565b5f90601f83116001146140e757613d3092915f91836121045750508160011b915f199060031b1c19161790565b90555b60128201613d41848a612bb1565b9190613d4d8383612f8f565b905f5260205f205f915b8383106140815750505050613d70601383019189613011565b9091613d7c8282613095565b5f9081526020812092805b838310613fab5750505050505f91600a600683019201925b60028110613f1e575050613db5613de0916139be565b80929060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b613dea8480613011565b9190911561380957613e3d906001600160a01b03613e0c8a9798969594612ffd565b165f526003602052613e2486600160405f2001614f59565b5060206001600160a01b03825116910151908633614d06565b6040519260408452613e5360c085019680612c39565b608060408701529687905260e08501965f5b818110613eea575050509467ffffffffffffffff613ecf859482613ec4613ee4966001600160a01b03613eb97f4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc79c9d6123e1565b1660608a0152613936565b166080870152613936565b1660a084015282810360208401523396612c6e565b0390a390565b91965091929394966020806001926001600160a01b03613f098b6123e1565b16815201970191019189969795949392613e65565b80613f838a6040613f5484613f4288613f4e6020613f4860019b613f42858b612bb1565b9061395a565b01612ffd565b95612bb1565b01356001600160a01b0360405192613f6b846124ff565b1682526020820152613f7d838761396a565b9061397d565b613fa5604051613f92816124ff565b5f81525f6020820152613f7d838861396a565b01613d9f565b613fb58183612f01565b9067ffffffffffffffff821161055957613fd38261049a89546129b8565b5f90601f83116001146140175792614008836001959460209487965f926121045750508160011b915f199060031b1c19161790565b88555b01950192019193613d87565b601f19831691885f5260205f20925f5b8181106140695750936020936001969387969383889510614050575b505050811b01885561400b565b01355f19600384901b60f8161c191690555f8080614043565b91936020600181928787013581550195019201614027565b60036060826001600160a01b03614099600195612ffd565b166001600160a01b03198654161785556140b560208201612ffd565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190613d57565b601f19831691845f5260205f20925f5b818110614133575090846001959493921061411a575b505050811b019055613d33565b01355f19600384901b60f8161c191690555f808061410d565b919360206001819287870135815501950192016140f7565b600190602061415985612ffd565b9401938184015501613bdd565b61417c90875f528360205f209182019101612f34565b5f613bd3565b7fb4e12433000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fea9e70ce000000000000000000000000000000000000000000000000000000005f5260045ffd5b506141dd8280613011565b15613809576141eb90612ffd565b6141f58380613011565b60011015613809576001600160a01b036142126020829301612ffd565b16911614613a0b565b506142268280613011565b600110156138095761424260206001600160a01b039201612ffd565b1615613a04565b506142548280613011565b156138095761426a6001600160a01b0391612ffd565b16156139fd565b9082156143a8576001600160a01b031691821591821561437157813403614349577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b036020925b1693845f526003835260405f20865f52835260405f206142e1838254612edb565b9055156142f2575b604051908152a3565b6143446040517f23b872dd00000000000000000000000000000000000000000000000000000000848201523360248201523060448201528260648201526064815261433e60848261251b565b86614fc1565b6142e9565b7faa7feadc000000000000000000000000000000000000000000000000000000005f5260045ffd5b34614349577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b036020926142c0565b7f2c5211c6000000000000000000000000000000000000000000000000000000005f5260045ffd5b805460011015613809575f52600160205f2001905f90565b8054821015613809575f5260205f2001905f90565b919092825f52600260205261441c61441760405f20613770565b6149ec565b9084516144288161264f565b6020860194855193604088019461446586519461445760608c0196875190604051958694602086019889614e26565b03601f19810183528261251b565b51902060405160208101918252604080820152600960608201527f6368616c6c656e676500000000000000000000000000000000000000000000006080820152608081526144b460a08261251b565b5190206144d66144d06144c7868461519b565b909291926151d5565b87614e66565b614647577f19457468657265756d205369676e6564204d6573736167653a0a3332000000005f52601c526145196145136144c785603c5f2061519b565b86614e66565b61463e576146129661460d9661460495604294614534614a5e565b9351926145408461264f565b519151602081519101209051604051614569816144576020820194602086526040830190614dc1565b5190209160405193602085019586527f74875af04779d70f933aef147d5751a32a32b3fa275f5022499f396ea394cf5360408601526145a78161264f565b6060850152608084015260a083015260c082015260c081526145ca60e08261251b565b519020604051917f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201522061519b565b909391936151d5565b614e66565b612f83577f61a44f6e000000000000000000000000000000000000000000000000000000005f5260045ffd5b50505050505050565b5050505050505050565b60405161466e816144576020820194602086526040830190612659565b5190209060405161468f816144576020820194602086526040830190612659565b5190201490565b60048101905b8181106146a7575050565b5f808255600182015560020161469c565b90815f52600260205260405f209060038201600460ff198254161790556002815103610642575f5b6002811061483b5750505f5b600281106148015750505f52600260205260405f2080545f8255806147e7575b505f60018201555f60028201555f6003820155614738600682016147338160048501612f34565b614696565b614744600a8201614696565b5f600e8201555f600f8201555f601082015561476260118201613047565b601281018054905f8155816147ae575b50506013018054905f815581614786575050565b5f5260205f20908101905b81811061479c575050565b806147a8600192613047565b01614791565b81600302916003830403611d81575f5260205f20908101905b8181101561477257805f600392555f60018201555f6002820155016147c7565b6147fb90825f5260205f2090810190612f34565b5f61470c565b806001600160a01b03614816600193856143e8565b90549060031b1c165f526003602052614834848360405f20016150d6565b50016146ec565b8061485261484b6001938561382d565b5186614ea5565b016146e0565b602060405180927f7f7d6ab500000000000000000000000000000000000000000000000000000000825260406004830152816001600160a01b03816148b56148a3604483018a612659565b6003198382030160248401528a612659565b0392165afa5f91816148e0575b506148d557506020809101519101511090565b90505f8092500b1390565b9091506020813d602011614915575b816148fc6020938361251b565b810103126102415751805f0b810361024157905f6148c2565b3d91506148ef565b6002815103610642576001600160a01b036020614948828261493e866137fc565b510151169361381d565b510151160361064257565b60808201916002835151036149ca5761496b826149ec565b915f5b60028110614980575050505050600190565b6149b461498b614a5e565b61499683885161382d565b516001600160a01b036149aa85875161382d565b5116918787614b74565b156149c15760010161496e565b50505050505f90565b5050505f90565b9190915f8382019384129112908015821691151617611d8157565b805190614a586001600160a01b036020830151169167ffffffffffffffff6060816040840151169201511692604051938492614a34602085019760a0895260c0860190612613565b926040850152606084015260808301524660a083015203601f19810183528261251b565b51902090565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016301480614b4b575b15614ab9577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a08152614a5860c08261251b565b507f00000000000000000000000000000000000000000000000000000000000000004614614a90565b926001600160a01b03949193805193614b8c8561264f565b6020820195865194614bd58960408601988951614bbe606089019a6144578c5160405194859360208501978c89614e26565b51902092614bcc888561519b565b909491946151d5565b1699168914614cf8577f19457468657265756d205369676e6564204d6573736167653a0a3332000000005f52601c52876001600160a01b03614c1d6144c787603c5f2061519b565b1614614ceb577fdeeda0875e527d63774890b89d23bff91e0ec84761e45d2b578ee592780095bb8214614cdf576001600160a01b0396614cd0966144c7966042955192614c698461264f565b519151602081519101209051604051614c92816144576020820194602086526040830190614dc1565b519020916040519360208501957fb02e61f8dbbfba070321cdff64845b04358ee41db88ba372ef47e352446f4b9c875260408601526145a78161264f565b1614614cda575f90565b600190565b50505050505050505f90565b5050505050505050600190565b505050505050505050600190565b8315614dbb576001600160a01b03165f52600360205260405f206001600160a01b0383165f528060205260405f2054848110614d8b5784614d46916137c5565b906001600160a01b0384165f5260205260405f20555f5260026020526001600160a01b03601460405f200191165f52602052614d8760405f20918254612edb565b9055565b84907fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b50505050565b90602080835192838152019201905f5b818110614dde5750505090565b9091926020614e1c60019286519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b9401929101614dd1565b939092614e58926127c996948652614e3d8161264f565b6020860152604085015260a0606085015260a08401906125ee565b916080818403910152614dc1565b905f5b82518110156149ca576001600160a01b03614e84828561382d565b51166001600160a01b03831614614e9d57600101614e69565b505050600190565b906040810191825115614f54575f526002602052601460405f20019160208201916001600160a01b0380845116165f528360205260405f2054938415614f4d576001600160a01b0392518086115f14614f4257614f039080966137c5565b908380865116165f5260205260405f205551165f5260036020526001600160a01b038060405f20925116165f52602052614d8760405f20918254612edb565b50614f0385806137c5565b5050505050565b505050565b6001810190825f528160205260405f2054155f146149ca5780546801000000000000000081101561055957614fae614f988260018794018555846143e8565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b905f602091828151910182855af115610680575f513d61502557506001600160a01b0381163b155b614ff05750565b6001600160a01b03907f5274afe7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b60011415614fe9565b60ff811461508d5760ff811690601f8211615065576040519161505260408461251b565b6020808452838101919036833783525290565b7fb3512b0c000000000000000000000000000000000000000000000000000000005f5260045ffd5b506040516127c981612b32815f6129f0565b60ff81146150c35760ff811690601f8211615065576040519161505260408461251b565b506040516127c981612b328160016129f0565b906001820191815f528260205260405f20548015155f14615193575f198101818111611d815782545f19810191908211611d815781810361515e575b5050508054801561514a575f19019061512b82826143e8565b8154905f199060031b1b19169055555f526020525f6040812055600190565b634e487b7160e01b5f52603160045260245ffd5b61517e61516e614f9893866143e8565b90549060031b1c928392866143e8565b90555f528360205260405f20555f8080615112565b505050505f90565b81519190604183036151cb576151c49250602082015190606060408401519301515f1a9061529c565b9192909190565b50505f9160029190565b6151de8161264f565b806151e7575050565b6151f08161264f565b60018103615220577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b6152298161264f565b6002810361525d57507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6003906152698161264f565b146152715750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411615313579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610680575f516001600160a01b0381161561530957905f905f90565b505f906001905f90565b5050505f916003919056fea2646970667358221220a42ae78844a10ed8f2b9c143b570a4d8ecbb65dda4c532c3bdd322fa72ea304064736f6c634300081b0033", } // CustodyABI is the input ABI used to generate the binding from. @@ -233,6 +226,76 @@ func (_Custody *CustodyTransactorRaw) Transact(opts *bind.TransactOpts, method s return _Custody.Contract.contract.Transact(opts, method, params...) } +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_Custody *CustodyCaller) Eip712Domain(opts *bind.CallOpts) (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "eip712Domain") + + outstruct := new(struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.Fields = *abi.ConvertType(out[0], new([1]byte)).(*[1]byte) + outstruct.Name = *abi.ConvertType(out[1], new(string)).(*string) + outstruct.Version = *abi.ConvertType(out[2], new(string)).(*string) + outstruct.ChainId = *abi.ConvertType(out[3], new(*big.Int)).(**big.Int) + outstruct.VerifyingContract = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Salt = *abi.ConvertType(out[5], new([32]byte)).(*[32]byte) + outstruct.Extensions = *abi.ConvertType(out[6], new([]*big.Int)).(*[]*big.Int) + + return *outstruct, err + +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_Custody *CustodySession) Eip712Domain() (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + return _Custody.Contract.Eip712Domain(&_Custody.CallOpts) +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_Custody *CustodyCallerSession) Eip712Domain() (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + return _Custody.Contract.Eip712Domain(&_Custody.CallOpts) +} + // GetAccountsBalances is a free data retrieval call binding the contract method 0x2f33c4d6. // // Solidity: function getAccountsBalances(address[] accounts, address[] tokens) view returns(uint256[][]) @@ -297,7 +360,7 @@ func (_Custody *CustodyCallerSession) GetChannelBalances(channelId [32]byte, tok // GetChannelData is a free data retrieval call binding the contract method 0xe617208c. // -// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) lastValidState) +// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) lastValidState) func (_Custody *CustodyCaller) GetChannelData(opts *bind.CallOpts, channelId [32]byte) (struct { Channel Channel Status uint8 @@ -331,7 +394,7 @@ func (_Custody *CustodyCaller) GetChannelData(opts *bind.CallOpts, channelId [32 // GetChannelData is a free data retrieval call binding the contract method 0xe617208c. // -// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) lastValidState) +// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) lastValidState) func (_Custody *CustodySession) GetChannelData(channelId [32]byte) (struct { Channel Channel Status uint8 @@ -344,7 +407,7 @@ func (_Custody *CustodySession) GetChannelData(channelId [32]byte) (struct { // GetChannelData is a free data retrieval call binding the contract method 0xe617208c. // -// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) lastValidState) +// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) lastValidState) func (_Custody *CustodyCallerSession) GetChannelData(channelId [32]byte) (struct { Channel Channel Status uint8 @@ -386,86 +449,86 @@ func (_Custody *CustodyCallerSession) GetOpenChannels(accounts []common.Address) return _Custody.Contract.GetOpenChannels(&_Custody.CallOpts, accounts) } -// Challenge is a paid mutator transaction binding the contract method 0xbc7b456f. +// Challenge is a paid mutator transaction binding the contract method 0x1474e410. // -// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs, (uint8,bytes32,bytes32) challengerSig) returns() -func (_Custody *CustodyTransactor) Challenge(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State, challengerSig Signature) (*types.Transaction, error) { +// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs, bytes challengerSig) returns() +func (_Custody *CustodyTransactor) Challenge(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State, challengerSig []byte) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "challenge", channelId, candidate, proofs, challengerSig) } -// Challenge is a paid mutator transaction binding the contract method 0xbc7b456f. +// Challenge is a paid mutator transaction binding the contract method 0x1474e410. // -// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs, (uint8,bytes32,bytes32) challengerSig) returns() -func (_Custody *CustodySession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig Signature) (*types.Transaction, error) { +// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs, bytes challengerSig) returns() +func (_Custody *CustodySession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig []byte) (*types.Transaction, error) { return _Custody.Contract.Challenge(&_Custody.TransactOpts, channelId, candidate, proofs, challengerSig) } -// Challenge is a paid mutator transaction binding the contract method 0xbc7b456f. +// Challenge is a paid mutator transaction binding the contract method 0x1474e410. // -// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs, (uint8,bytes32,bytes32) challengerSig) returns() -func (_Custody *CustodyTransactorSession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig Signature) (*types.Transaction, error) { +// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs, bytes challengerSig) returns() +func (_Custody *CustodyTransactorSession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig []byte) (*types.Transaction, error) { return _Custody.Contract.Challenge(&_Custody.TransactOpts, channelId, candidate, proofs, challengerSig) } -// Checkpoint is a paid mutator transaction binding the contract method 0xd0cce1e8. +// Checkpoint is a paid mutator transaction binding the contract method 0xecf668fd. // -// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactor) Checkpoint(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "checkpoint", channelId, candidate, proofs) } -// Checkpoint is a paid mutator transaction binding the contract method 0xd0cce1e8. +// Checkpoint is a paid mutator transaction binding the contract method 0xecf668fd. // -// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodySession) Checkpoint(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Checkpoint(&_Custody.TransactOpts, channelId, candidate, proofs) } -// Checkpoint is a paid mutator transaction binding the contract method 0xd0cce1e8. +// Checkpoint is a paid mutator transaction binding the contract method 0xecf668fd. // -// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactorSession) Checkpoint(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Checkpoint(&_Custody.TransactOpts, channelId, candidate, proofs) } -// Close is a paid mutator transaction binding the contract method 0xde22731f. +// Close is a paid mutator transaction binding the contract method 0x7f9ebbd7. // -// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] ) returns() +// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] ) returns() func (_Custody *CustodyTransactor) Close(opts *bind.TransactOpts, channelId [32]byte, candidate State, arg2 []State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "close", channelId, candidate, arg2) } -// Close is a paid mutator transaction binding the contract method 0xde22731f. +// Close is a paid mutator transaction binding the contract method 0x7f9ebbd7. // -// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] ) returns() +// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] ) returns() func (_Custody *CustodySession) Close(channelId [32]byte, candidate State, arg2 []State) (*types.Transaction, error) { return _Custody.Contract.Close(&_Custody.TransactOpts, channelId, candidate, arg2) } -// Close is a paid mutator transaction binding the contract method 0xde22731f. +// Close is a paid mutator transaction binding the contract method 0x7f9ebbd7. // -// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] ) returns() +// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] ) returns() func (_Custody *CustodyTransactorSession) Close(channelId [32]byte, candidate State, arg2 []State) (*types.Transaction, error) { return _Custody.Contract.Close(&_Custody.TransactOpts, channelId, candidate, arg2) } -// Create is a paid mutator transaction binding the contract method 0xd37ff7b5. +// Create is a paid mutator transaction binding the contract method 0x4a7e7798. // -// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) returns(bytes32 channelId) +// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) returns(bytes32 channelId) func (_Custody *CustodyTransactor) Create(opts *bind.TransactOpts, ch Channel, initial State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "create", ch, initial) } -// Create is a paid mutator transaction binding the contract method 0xd37ff7b5. +// Create is a paid mutator transaction binding the contract method 0x4a7e7798. // -// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) returns(bytes32 channelId) +// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) returns(bytes32 channelId) func (_Custody *CustodySession) Create(ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.Create(&_Custody.TransactOpts, ch, initial) } -// Create is a paid mutator transaction binding the contract method 0xd37ff7b5. +// Create is a paid mutator transaction binding the contract method 0x4a7e7798. // -// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) returns(bytes32 channelId) +// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) returns(bytes32 channelId) func (_Custody *CustodyTransactorSession) Create(ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.Create(&_Custody.TransactOpts, ch, initial) } @@ -491,65 +554,65 @@ func (_Custody *CustodyTransactorSession) Deposit(account common.Address, token return _Custody.Contract.Deposit(&_Custody.TransactOpts, account, token, amount) } -// DepositAndCreate is a paid mutator transaction binding the contract method 0x925bc479. +// DepositAndCreate is a paid mutator transaction binding the contract method 0x00e2bb2c. // -// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) payable returns(bytes32) +// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) payable returns(bytes32) func (_Custody *CustodyTransactor) DepositAndCreate(opts *bind.TransactOpts, token common.Address, amount *big.Int, ch Channel, initial State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "depositAndCreate", token, amount, ch, initial) } -// DepositAndCreate is a paid mutator transaction binding the contract method 0x925bc479. +// DepositAndCreate is a paid mutator transaction binding the contract method 0x00e2bb2c. // -// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) payable returns(bytes32) +// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) payable returns(bytes32) func (_Custody *CustodySession) DepositAndCreate(token common.Address, amount *big.Int, ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.DepositAndCreate(&_Custody.TransactOpts, token, amount, ch, initial) } -// DepositAndCreate is a paid mutator transaction binding the contract method 0x925bc479. +// DepositAndCreate is a paid mutator transaction binding the contract method 0x00e2bb2c. // -// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) payable returns(bytes32) +// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) payable returns(bytes32) func (_Custody *CustodyTransactorSession) DepositAndCreate(token common.Address, amount *big.Int, ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.DepositAndCreate(&_Custody.TransactOpts, token, amount, ch, initial) } -// Join is a paid mutator transaction binding the contract method 0xa22b823d. +// Join is a paid mutator transaction binding the contract method 0xbab3290a. // -// Solidity: function join(bytes32 channelId, uint256 index, (uint8,bytes32,bytes32) sig) returns(bytes32) -func (_Custody *CustodyTransactor) Join(opts *bind.TransactOpts, channelId [32]byte, index *big.Int, sig Signature) (*types.Transaction, error) { +// Solidity: function join(bytes32 channelId, uint256 index, bytes sig) returns(bytes32) +func (_Custody *CustodyTransactor) Join(opts *bind.TransactOpts, channelId [32]byte, index *big.Int, sig []byte) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "join", channelId, index, sig) } -// Join is a paid mutator transaction binding the contract method 0xa22b823d. +// Join is a paid mutator transaction binding the contract method 0xbab3290a. // -// Solidity: function join(bytes32 channelId, uint256 index, (uint8,bytes32,bytes32) sig) returns(bytes32) -func (_Custody *CustodySession) Join(channelId [32]byte, index *big.Int, sig Signature) (*types.Transaction, error) { +// Solidity: function join(bytes32 channelId, uint256 index, bytes sig) returns(bytes32) +func (_Custody *CustodySession) Join(channelId [32]byte, index *big.Int, sig []byte) (*types.Transaction, error) { return _Custody.Contract.Join(&_Custody.TransactOpts, channelId, index, sig) } -// Join is a paid mutator transaction binding the contract method 0xa22b823d. +// Join is a paid mutator transaction binding the contract method 0xbab3290a. // -// Solidity: function join(bytes32 channelId, uint256 index, (uint8,bytes32,bytes32) sig) returns(bytes32) -func (_Custody *CustodyTransactorSession) Join(channelId [32]byte, index *big.Int, sig Signature) (*types.Transaction, error) { +// Solidity: function join(bytes32 channelId, uint256 index, bytes sig) returns(bytes32) +func (_Custody *CustodyTransactorSession) Join(channelId [32]byte, index *big.Int, sig []byte) (*types.Transaction, error) { return _Custody.Contract.Join(&_Custody.TransactOpts, channelId, index, sig) } -// Resize is a paid mutator transaction binding the contract method 0x259311c9. +// Resize is a paid mutator transaction binding the contract method 0x183b4998. // -// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactor) Resize(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "resize", channelId, candidate, proofs) } -// Resize is a paid mutator transaction binding the contract method 0x259311c9. +// Resize is a paid mutator transaction binding the contract method 0x183b4998. // -// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodySession) Resize(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Resize(&_Custody.TransactOpts, channelId, candidate, proofs) } -// Resize is a paid mutator transaction binding the contract method 0x259311c9. +// Resize is a paid mutator transaction binding the contract method 0x183b4998. // -// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactorSession) Resize(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Resize(&_Custody.TransactOpts, channelId, candidate, proofs) } @@ -650,9 +713,9 @@ type CustodyChallenged struct { Raw types.Log // Blockchain specific contextual infos } -// FilterChallenged is a free log retrieval operation binding the contract event 0x2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d03756. +// FilterChallenged is a free log retrieval operation binding the contract event 0x44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e17. // -// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state, uint256 expiration) +// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state, uint256 expiration) func (_Custody *CustodyFilterer) FilterChallenged(opts *bind.FilterOpts, channelId [][32]byte) (*CustodyChallengedIterator, error) { var channelIdRule []interface{} @@ -667,9 +730,9 @@ func (_Custody *CustodyFilterer) FilterChallenged(opts *bind.FilterOpts, channel return &CustodyChallengedIterator{contract: _Custody.contract, event: "Challenged", logs: logs, sub: sub}, nil } -// WatchChallenged is a free log subscription operation binding the contract event 0x2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d03756. +// WatchChallenged is a free log subscription operation binding the contract event 0x44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e17. // -// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state, uint256 expiration) +// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state, uint256 expiration) func (_Custody *CustodyFilterer) WatchChallenged(opts *bind.WatchOpts, sink chan<- *CustodyChallenged, channelId [][32]byte) (event.Subscription, error) { var channelIdRule []interface{} @@ -709,9 +772,9 @@ func (_Custody *CustodyFilterer) WatchChallenged(opts *bind.WatchOpts, sink chan }), nil } -// ParseChallenged is a log parse operation binding the contract event 0x2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d03756. +// ParseChallenged is a log parse operation binding the contract event 0x44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e17. // -// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state, uint256 expiration) +// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state, uint256 expiration) func (_Custody *CustodyFilterer) ParseChallenged(log types.Log) (*CustodyChallenged, error) { event := new(CustodyChallenged) if err := _Custody.contract.UnpackLog(event, "Challenged", log); err != nil { @@ -795,9 +858,9 @@ type CustodyCheckpointed struct { Raw types.Log // Blockchain specific contextual infos } -// FilterCheckpointed is a free log retrieval operation binding the contract event 0xa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d. +// FilterCheckpointed is a free log retrieval operation binding the contract event 0x8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c3. // -// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state) +// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state) func (_Custody *CustodyFilterer) FilterCheckpointed(opts *bind.FilterOpts, channelId [][32]byte) (*CustodyCheckpointedIterator, error) { var channelIdRule []interface{} @@ -812,9 +875,9 @@ func (_Custody *CustodyFilterer) FilterCheckpointed(opts *bind.FilterOpts, chann return &CustodyCheckpointedIterator{contract: _Custody.contract, event: "Checkpointed", logs: logs, sub: sub}, nil } -// WatchCheckpointed is a free log subscription operation binding the contract event 0xa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d. +// WatchCheckpointed is a free log subscription operation binding the contract event 0x8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c3. // -// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state) +// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state) func (_Custody *CustodyFilterer) WatchCheckpointed(opts *bind.WatchOpts, sink chan<- *CustodyCheckpointed, channelId [][32]byte) (event.Subscription, error) { var channelIdRule []interface{} @@ -854,9 +917,9 @@ func (_Custody *CustodyFilterer) WatchCheckpointed(opts *bind.WatchOpts, sink ch }), nil } -// ParseCheckpointed is a log parse operation binding the contract event 0xa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d. +// ParseCheckpointed is a log parse operation binding the contract event 0x8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c3. // -// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state) +// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state) func (_Custody *CustodyFilterer) ParseCheckpointed(log types.Log) (*CustodyCheckpointed, error) { event := new(CustodyCheckpointed) if err := _Custody.contract.UnpackLog(event, "Checkpointed", log); err != nil { @@ -940,9 +1003,9 @@ type CustodyClosed struct { Raw types.Log // Blockchain specific contextual infos } -// FilterClosed is a free log retrieval operation binding the contract event 0x3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c910. +// FilterClosed is a free log retrieval operation binding the contract event 0xd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496. // -// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) finalState) +// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) finalState) func (_Custody *CustodyFilterer) FilterClosed(opts *bind.FilterOpts, channelId [][32]byte) (*CustodyClosedIterator, error) { var channelIdRule []interface{} @@ -957,9 +1020,9 @@ func (_Custody *CustodyFilterer) FilterClosed(opts *bind.FilterOpts, channelId [ return &CustodyClosedIterator{contract: _Custody.contract, event: "Closed", logs: logs, sub: sub}, nil } -// WatchClosed is a free log subscription operation binding the contract event 0x3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c910. +// WatchClosed is a free log subscription operation binding the contract event 0xd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496. // -// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) finalState) +// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) finalState) func (_Custody *CustodyFilterer) WatchClosed(opts *bind.WatchOpts, sink chan<- *CustodyClosed, channelId [][32]byte) (event.Subscription, error) { var channelIdRule []interface{} @@ -999,9 +1062,9 @@ func (_Custody *CustodyFilterer) WatchClosed(opts *bind.WatchOpts, sink chan<- * }), nil } -// ParseClosed is a log parse operation binding the contract event 0x3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c910. +// ParseClosed is a log parse operation binding the contract event 0xd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496. // -// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) finalState) +// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) finalState) func (_Custody *CustodyFilterer) ParseClosed(log types.Log) (*CustodyClosed, error) { event := new(CustodyClosed) if err := _Custody.contract.UnpackLog(event, "Closed", log); err != nil { @@ -1087,9 +1150,9 @@ type CustodyCreated struct { Raw types.Log // Blockchain specific contextual infos } -// FilterCreated is a free log retrieval operation binding the contract event 0x7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c39842. +// FilterCreated is a free log retrieval operation binding the contract event 0x4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc7. // -// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) +// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) func (_Custody *CustodyFilterer) FilterCreated(opts *bind.FilterOpts, channelId [][32]byte, wallet []common.Address) (*CustodyCreatedIterator, error) { var channelIdRule []interface{} @@ -1108,9 +1171,9 @@ func (_Custody *CustodyFilterer) FilterCreated(opts *bind.FilterOpts, channelId return &CustodyCreatedIterator{contract: _Custody.contract, event: "Created", logs: logs, sub: sub}, nil } -// WatchCreated is a free log subscription operation binding the contract event 0x7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c39842. +// WatchCreated is a free log subscription operation binding the contract event 0x4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc7. // -// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) +// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) func (_Custody *CustodyFilterer) WatchCreated(opts *bind.WatchOpts, sink chan<- *CustodyCreated, channelId [][32]byte, wallet []common.Address) (event.Subscription, error) { var channelIdRule []interface{} @@ -1154,9 +1217,9 @@ func (_Custody *CustodyFilterer) WatchCreated(opts *bind.WatchOpts, sink chan<- }), nil } -// ParseCreated is a log parse operation binding the contract event 0x7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c39842. +// ParseCreated is a log parse operation binding the contract event 0x4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc7. // -// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) +// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) func (_Custody *CustodyFilterer) ParseCreated(log types.Log) (*CustodyCreated, error) { event := new(CustodyCreated) if err := _Custody.contract.UnpackLog(event, "Created", log); err != nil { @@ -1320,6 +1383,139 @@ func (_Custody *CustodyFilterer) ParseDeposited(log types.Log) (*CustodyDeposite return event, nil } +// CustodyEIP712DomainChangedIterator is returned from FilterEIP712DomainChanged and is used to iterate over the raw logs and unpacked data for EIP712DomainChanged events raised by the Custody contract. +type CustodyEIP712DomainChangedIterator struct { + Event *CustodyEIP712DomainChanged // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *CustodyEIP712DomainChangedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(CustodyEIP712DomainChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(CustodyEIP712DomainChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *CustodyEIP712DomainChangedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *CustodyEIP712DomainChangedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// CustodyEIP712DomainChanged represents a EIP712DomainChanged event raised by the Custody contract. +type CustodyEIP712DomainChanged struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterEIP712DomainChanged is a free log retrieval operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_Custody *CustodyFilterer) FilterEIP712DomainChanged(opts *bind.FilterOpts) (*CustodyEIP712DomainChangedIterator, error) { + + logs, sub, err := _Custody.contract.FilterLogs(opts, "EIP712DomainChanged") + if err != nil { + return nil, err + } + return &CustodyEIP712DomainChangedIterator{contract: _Custody.contract, event: "EIP712DomainChanged", logs: logs, sub: sub}, nil +} + +// WatchEIP712DomainChanged is a free log subscription operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_Custody *CustodyFilterer) WatchEIP712DomainChanged(opts *bind.WatchOpts, sink chan<- *CustodyEIP712DomainChanged) (event.Subscription, error) { + + logs, sub, err := _Custody.contract.WatchLogs(opts, "EIP712DomainChanged") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(CustodyEIP712DomainChanged) + if err := _Custody.contract.UnpackLog(event, "EIP712DomainChanged", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseEIP712DomainChanged is a log parse operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_Custody *CustodyFilterer) ParseEIP712DomainChanged(log types.Log) (*CustodyEIP712DomainChanged, error) { + event := new(CustodyEIP712DomainChanged) + if err := _Custody.contract.UnpackLog(event, "EIP712DomainChanged", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // CustodyJoinedIterator is returned from FilterJoined and is used to iterate over the raw logs and unpacked data for Joined events raised by the Custody contract. type CustodyJoinedIterator struct { Event *CustodyJoined // Event containing the contract specifics and raw log diff --git a/clearnode/nitrolite/signature.go b/clearnode/nitrolite/signature.go index 124910294..beb75ff2c 100644 --- a/clearnode/nitrolite/signature.go +++ b/clearnode/nitrolite/signature.go @@ -2,49 +2,95 @@ package nitrolite import ( "crypto/ecdsa" + "encoding/json" "fmt" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" ) +type Signature []byte + +func (s Signature) MarshalJSON() ([]byte, error) { + return json.Marshal(hexutil.Encode(s)) +} + +func (s *Signature) UnmarshalJSON(data []byte) error { + var hexStr string + if err := json.Unmarshal(data, &hexStr); err != nil { + return err + } + decoded, err := hexutil.Decode(hexStr) + if err != nil { + return err + } + *s = decoded + return nil +} + +func (s Signature) String() string { + return hexutil.Encode(s) +} + +func SignaturesToStrings(signatures []Signature) []string { + strs := make([]string, len(signatures)) + for i, sig := range signatures { + strs[i] = sig.String() + } + return strs +} + +func SignaturesFromStrings(strs []string) ([]Signature, error) { + signatures := make([]Signature, len(strs)) + for i, str := range strs { + sig, err := hexutil.Decode(str) + if err != nil { + return nil, fmt.Errorf("failed to decode signature %d (%s): %w", i, str, err) + } + signatures[i] = sig + } + return signatures, nil +} + // Sign hashes the provided data using Keccak256 and signs it with the given private key. func Sign(data []byte, privateKey *ecdsa.PrivateKey) (Signature, error) { if privateKey == nil { - return Signature{}, fmt.Errorf("private key is nil") + return nil, fmt.Errorf("private key is nil") } dataHash := crypto.Keccak256Hash(data) signature, err := crypto.Sign(dataHash.Bytes(), privateKey) if err != nil { - return Signature{}, fmt.Errorf("failed to sign data: %w", err) + return nil, fmt.Errorf("failed to sign data: %w", err) } if len(signature) != 65 { - return Signature{}, fmt.Errorf("invalid signature length: got %d, want 65", len(signature)) + return nil, fmt.Errorf("invalid signature length: got %d, want 65", len(signature)) } - var sig Signature - copy(sig.R[:], signature[:32]) - copy(sig.S[:], signature[32:64]) - sig.V = signature[64] + 27 + // This step is necessary to remain compatible with the ecrecover precompile + if signature[64] < 27 { + signature[64] += 27 + } - return sig, nil + return signature, nil } // Verify checks if the signature on the provided data was created by the given address. func Verify(data []byte, sig Signature, address common.Address) (bool, error) { dataHash := crypto.Keccak256Hash(data) - signature := make([]byte, 65) - copy(signature[0:32], sig.R[:]) - copy(signature[32:64], sig.S[:]) + // Create a copy of the signature to avoid modifying the original + sigToVerify := make(Signature, len(sig)) + copy(sigToVerify, sig) - if sig.V >= 27 { - signature[64] = sig.V - 27 + // Ensure the signature is in the correct format + if sigToVerify[64] >= 27 { + sigToVerify[64] -= 27 } - pubKeyRaw, err := crypto.Ecrecover(dataHash.Bytes(), signature) + pubKeyRaw, err := crypto.Ecrecover(dataHash.Bytes(), sigToVerify) if err != nil { return false, fmt.Errorf("failed to recover public key: %w", err) } diff --git a/clearnode/nitrolite/signature_test.go b/clearnode/nitrolite/signature_test.go index 17a83812d..716662437 100644 --- a/clearnode/nitrolite/signature_test.go +++ b/clearnode/nitrolite/signature_test.go @@ -71,8 +71,8 @@ func TestVerifyInvalidSignature(t *testing.T) { t.Fatalf("failed to sign data: %v", err) } - // Tamper with the signature (flip a bit in R). - sig.R[0] ^= 0xff + // Tamper with the signature (flip some bit). + sig[0] ^= 0xff // Use the original public address. publicAddress := crypto.PubkeyToAddress(privateKey.PublicKey) diff --git a/clearnode/nitrolite/state.go b/clearnode/nitrolite/state.go index c36223f11..89ec62df1 100644 --- a/clearnode/nitrolite/state.go +++ b/clearnode/nitrolite/state.go @@ -16,8 +16,8 @@ const ( IntentFINALIZE Intent = 3 ) -// EncodeState encodes channel state into a byte array using channelID, intent, version, state data, and allocations. -func EncodeState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) { +// PackState encodes channel id and state into a byte array +func PackState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) { allocationType, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{ {Name: "destination", Type: "address"}, {Name: "token", Type: "address"}, diff --git a/clearnode/rpc.go b/clearnode/rpc.go index 9f92c684f..9e373853c 100644 --- a/clearnode/rpc.go +++ b/clearnode/rpc.go @@ -9,10 +9,10 @@ import ( // RPCMessage represents a complete message in the RPC protocol, including data and signatures type RPCMessage struct { - Req *RPCData `json:"req,omitempty" validate:"required_without=Res,excluded_with=Res"` - Res *RPCData `json:"res,omitempty" validate:"required_without=Req,excluded_with=Req"` - AppSessionID string `json:"sid,omitempty"` - Sig []string `json:"sig"` + Req *RPCData `json:"req,omitempty" validate:"required_without=Res,excluded_with=Res"` + Res *RPCData `json:"res,omitempty" validate:"required_without=Req,excluded_with=Req"` + AppSessionID string `json:"sid,omitempty"` + Sig []Signature `json:"sig"` } // ParseRPCMessage parses a JSON string into an RPCMessage @@ -100,7 +100,7 @@ func CreateResponse(id uint64, method string, responseParams []any) *RPCMessage Params: responseParams, Timestamp: uint64(time.Now().UnixMilli()), }, - Sig: []string{}, + Sig: []Signature{}, } } diff --git a/clearnode/rpc_node.go b/clearnode/rpc_node.go index 9f11b713b..48399dfbc 100644 --- a/clearnode/rpc_node.go +++ b/clearnode/rpc_node.go @@ -9,7 +9,6 @@ import ( "sync" "time" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/go-playground/validator/v10" "github.com/google/uuid" "github.com/gorilla/websocket" @@ -168,10 +167,10 @@ read_loop: } } - var msg RPCMessage + msg := RPCMessage{Req: &RPCData{}} if err := json.Unmarshal(messageBytes, &msg); err != nil { n.logger.Debug("invalid message format", "error", err, "message", string(messageBytes)) - n.sendErrorResponse(rpcConn, 0, "invalid message format") + n.sendErrorResponse(rpcConn, msg.Req.RequestID, "invalid message format") continue } @@ -360,7 +359,7 @@ func prepareRawRPCResponse(signer *Signer, data *RPCData) ([]byte, error) { responseMessage := &RPCMessage{ Res: data, - Sig: []string{hexutil.Encode(signature)}, + Sig: []Signature{signature}, } resMessageBytes, err := json.Marshal(responseMessage) if err != nil { diff --git a/clearnode/rpc_node_test.go b/clearnode/rpc_node_test.go index 4d21d4011..bd013069c 100644 --- a/clearnode/rpc_node_test.go +++ b/clearnode/rpc_node_test.go @@ -191,7 +191,7 @@ func TestRPCNode(t *testing.T) { reqMsg := &RPCMessage{ Req: reqData, - Sig: []string{}, + Sig: []Signature{}, } // Send request diff --git a/clearnode/rpc_router.go b/clearnode/rpc_router.go index 2bfc45cc9..a21772862 100644 --- a/clearnode/rpc_router.go +++ b/clearnode/rpc_router.go @@ -204,15 +204,15 @@ func (r *RPCRouter) MetricsMiddleware(c *RPCContext) { } type RPCEntry struct { - ID uint `json:"id"` - Sender string `json:"sender"` - ReqID uint64 `json:"req_id"` - Method string `json:"method"` - Params string `json:"params"` - Timestamp uint64 `json:"timestamp"` - ReqSig []string `json:"req_sig"` - Result string `json:"response"` - ResSig []string `json:"res_sig"` + ID uint `json:"id"` + Sender string `json:"sender"` + ReqID uint64 `json:"req_id"` + Method string `json:"method"` + Params string `json:"params"` + Timestamp uint64 `json:"timestamp"` + ReqSig []Signature `json:"req_sig"` + Result string `json:"response"` + ResSig []Signature `json:"res_sig"` } func (r *RPCRouter) HistoryMiddleware(c *RPCContext) { diff --git a/clearnode/rpc_router_auth.go b/clearnode/rpc_router_auth.go index 0f92cdb09..16fd29e9b 100644 --- a/clearnode/rpc_router_auth.go +++ b/clearnode/rpc_router_auth.go @@ -244,7 +244,7 @@ func (r *RPCRouter) handleAuthJWTVerify(ctx context.Context, authParams AuthVeri } // handleAuthJWTVerify verifies the challenge signature and returns the policy, response data and error. -func (r *RPCRouter) handleAuthSigVerify(ctx context.Context, sig string, authParams AuthVerifyParams) (*Policy, any, error) { +func (r *RPCRouter) handleAuthSigVerify(ctx context.Context, sig Signature, authParams AuthVerifyParams) (*Policy, any, error) { logger := LoggerFromContext(ctx) challenge, err := r.AuthManager.GetChallenge(authParams.Challenge) diff --git a/clearnode/rpc_router_private.go b/clearnode/rpc_router_private.go index 262d40990..ca2eb689f 100644 --- a/clearnode/rpc_router_private.go +++ b/clearnode/rpc_router_private.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/erc7824/nitrolite/clearnode/nitrolite" "github.com/ethereum/go-ethereum/common" "github.com/shopspring/decimal" "gorm.io/gorm" @@ -84,7 +85,6 @@ type ResizeChannelResponse struct { Intent uint8 `json:"intent"` Version uint64 `json:"version"` Allocations []Allocation `json:"allocations"` - StateHash string `json:"state_hash"` Signature Signature `json:"server_signature"` } @@ -105,7 +105,6 @@ type CloseChannelResponse struct { Version uint64 `json:"version"` StateData string `json:"state_data"` FinalAllocations []Allocation `json:"allocations"` - StateHash string `json:"state_hash"` Signature Signature `json:"server_signature"` } @@ -125,12 +124,6 @@ type ChannelResponse struct { UpdatedAt string `json:"updated_at"` } -type Signature struct { - V uint8 `json:"v,string"` - R string `json:"r"` - S string `json:"s"` -} - type Balance struct { Asset string `json:"asset"` Amount decimal.Decimal `json:"amount"` @@ -579,6 +572,20 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { response := make([]RPCEntry, 0, len(rpcHistory)) for _, record := range rpcHistory { + reqSigs, err := nitrolite.SignaturesFromStrings(record.ReqSig) + if err != nil { + logger.Error("failed to decode request signature", "error", err, "recordID", record.ID) + c.Fail(err, "failed to decode request signature") + return + } + + resSigs, err := nitrolite.SignaturesFromStrings(record.ResSig) + if err != nil { + logger.Error("failed to decode response signature", "error", err, "recordID", record.ID) + c.Fail(err, "failed to decode response signature") + return + } + response = append(response, RPCEntry{ ID: record.ID, Sender: record.Sender, @@ -586,8 +593,8 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { Method: record.Method, Params: string(record.Params), Timestamp: record.Timestamp, - ReqSig: record.ReqSig, - ResSig: record.ResSig, + ReqSig: reqSigs, + ResSig: resSigs, Result: string(record.Response), }) } diff --git a/clearnode/rpc_router_private_test.go b/clearnode/rpc_router_private_test.go index 48c854cd5..589b0b6f5 100644 --- a/clearnode/rpc_router_private_test.go +++ b/clearnode/rpc_router_private_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" @@ -25,10 +24,10 @@ func createSignedRPCContext(id int, method string, params any, signers ...Signer rawReq, _ := json.Marshal(ctx.Message.Req) ctx.Message.Req.rawBytes = rawReq - ctx.Message.Sig = make([]string, 0, len(signers)) + ctx.Message.Sig = make([]Signature, 0, len(signers)) for _, signer := range signers { sigBytes, _ := signer.Sign(rawReq) - ctx.Message.Sig = append(ctx.Message.Sig, hexutil.Encode(sigBytes)) + ctx.Message.Sig = append(ctx.Message.Sig, sigBytes) } return ctx @@ -65,18 +64,27 @@ func TestRPCRouterHandleGetRPCHistory(t *testing.T) { baseTime := uint64(time.Now().Unix()) // Create 11 test records for pagination testing records := []RPCRecord{ - {Sender: userAddress, ReqID: 1, Method: "ping", Params: []byte(`[null]`), Timestamp: baseTime - 10, ReqSig: []string{"sig1"}, Response: []byte(`{"res":[1,"pong",[],1621234567890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 2, Method: "get_config", Params: []byte(`[]`), Timestamp: baseTime - 9, ReqSig: []string{"sig2"}, Response: []byte(`{"res":[2,"get_config",[{"broker_address":"0xBroker"}],1621234597890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 3, Method: "get_channels", Params: []byte(fmt.Sprintf(`[{"participant":"%s"}]`, userAddress)), Timestamp: baseTime - 8, ReqSig: []string{"sig3"}, Response: []byte(`{"res":[3,"get_channels",[[]],1621234627890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 4, Method: "transfer", Params: []byte(`[{"destination":"0xDest","allocations":[{"asset":"USDC","amount":"100"}]}]`), Timestamp: baseTime - 7, ReqSig: []string{"sig4"}, Response: []byte(`{"res":[4,"transfer",[],1621234657890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 5, Method: "get_ledger_balances", Params: []byte(`[]`), Timestamp: baseTime - 6, ReqSig: []string{"sig5"}, Response: []byte(`{"res":[5,"get_ledger_balances",[],1621234687890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 6, Method: "create_application", Params: []byte(`[{"definition":{"protocol":"test"}}]`), Timestamp: baseTime - 5, ReqSig: []string{"sig6"}, Response: []byte(`{"res":[6,"create_application",[],1621234717890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 7, Method: "submit_app_state", Params: []byte(`[{"app_session_id":"123"}]`), Timestamp: baseTime - 4, ReqSig: []string{"sig7"}, Response: []byte(`{"res":[7,"submit_app_state",[],1621234747890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 8, Method: "close_application", Params: []byte(`[{"app_session_id":"123"}]`), Timestamp: baseTime - 3, ReqSig: []string{"sig8"}, Response: []byte(`{"res":[8,"close_application",[],1621234777890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 9, Method: "resize_channel", Params: []byte(`[{"channel_id":"ch123"}]`), Timestamp: baseTime - 2, ReqSig: []string{"sig9"}, Response: []byte(`{"res":[9,"resize_channel",[],1621234807890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 10, Method: "close_channel", Params: []byte(`[{"channel_id":"ch123"}]`), Timestamp: baseTime - 1, ReqSig: []string{"sig10"}, Response: []byte(`{"res":[10,"close_channel",[],1621234837890]}`), ResSig: []string{}}, - {Sender: userAddress, ReqID: 11, Method: "get_user_tag", Params: []byte(`[]`), Timestamp: baseTime, ReqSig: []string{"sig11"}, Response: []byte(`{"res":[11,"get_user_tag",[],1621234867890]}`), ResSig: []string{}}, - {Sender: "0xOtherParticipant", ReqID: 12, Method: "ping", Params: []byte(`[null]`), Timestamp: baseTime + 1, ReqSig: []string{"sig12"}, Response: []byte(`{"res":[12,"pong",[],1621234897890]}`)}, + {Sender: userAddress, Method: "ping", Params: []byte(`[null]`), Response: []byte(`{"res":[1,"pong",[],1621234567890]}`)}, + {Sender: userAddress, Method: "get_config", Params: []byte(`[]`), Response: []byte(`{"res":[2,"get_config",[{"broker_address":"0xBroker"}],1621234597890]}`)}, + {Sender: userAddress, Method: "get_channels", Params: []byte(fmt.Sprintf(`[{"participant":"%s"}]`, userAddress)), Response: []byte(`{"res":[3,"get_channels",[[]],1621234627890]}`)}, + {Sender: userAddress, Method: "transfer", Params: []byte(`[{"destination":"0xDest","allocations":[{"asset":"USDC","amount":"100"}]}]`), Response: []byte(`{"res":[4,"transfer",[],1621234657890]}`)}, + {Sender: userAddress, Method: "get_ledger_balances", Params: []byte(`[]`), Response: []byte(`{"res":[5,"get_ledger_balances",[],1621234687890]}`)}, + {Sender: userAddress, Method: "create_application", Params: []byte(`[{"definition":{"protocol":"test"}}]`), ReqSig: []string{"0x0006"}, Response: []byte(`{"res":[6,"create_application",[],1621234717890]}`)}, + {Sender: userAddress, Method: "submit_app_state", Params: []byte(`[{"app_session_id":"123"}]`), Response: []byte(`{"res":[7,"submit_app_state",[],1621234747890]}`)}, + {Sender: userAddress, Method: "close_application", Params: []byte(`[{"app_session_id":"123"}]`), Response: []byte(`{"res":[8,"close_application",[],1621234777890]}`)}, + {Sender: userAddress, Method: "resize_channel", Params: []byte(`[{"channel_id":"ch123"}]`), Response: []byte(`{"res":[9,"resize_channel",[],1621234807890]}`)}, + {Sender: userAddress, Method: "close_channel", Params: []byte(`[{"channel_id":"ch123"}]`), Response: []byte(`{"res":[10,"close_channel",[],1621234837890]}`)}, + {Sender: userAddress, Method: "get_user_tag", Params: []byte(`[]`), Response: []byte(`{"res":[11,"get_user_tag",[],1621234867890]}`)}, + {Sender: "0xOtherParticipant", Method: "ping", Params: []byte(`[null]`), Response: []byte(`{"res":[12,"pong",[],1621234897890]}`)}, + } + + numOfTestRecords := len(records) + + for i := range records { + records[i].ReqID = uint64(i + 1) + records[i].Timestamp = baseTime - uint64(numOfTestRecords-i) + records[i].ReqSig = []string{fmt.Sprintf("0x%04X", i+1)} + records[i].ResSig = []string{} } require.NoError(t, router.DB.Create(records).Error) @@ -350,7 +358,7 @@ func TestRPCRouterHandleTransfer(t *testing.T) { require.NoError(t, GetWalletLedger(db, senderAddr).Record(senderAccountID, "usdc", decimal.NewFromInt(1000))) require.NoError(t, GetWalletLedger(db, senderAddr).Record(senderAccountID, "eth", decimal.NewFromInt(5))) - // Setup user tag for receipient + // Setup user tag for recipient recipientTag, err := GenerateOrRetrieveUserTag(db, recipientAddr.Hex()) require.NoError(t, err) diff --git a/clearnode/rpc_router_public_test.go b/clearnode/rpc_router_public_test.go index 38739cda5..46ebefbdb 100644 --- a/clearnode/rpc_router_public_test.go +++ b/clearnode/rpc_router_public_test.go @@ -28,7 +28,7 @@ func createRPCContext(id int, method string, params any) *RPCContext { Params: rpcParams, Timestamp: uint64(time.Now().Unix()), }, - Sig: []string{"dummy-signature"}, + Sig: []Signature{Signature([]byte("dummy-signature"))}, }, } } diff --git a/clearnode/rpc_store.go b/clearnode/rpc_store.go index ce3db3c78..de65cf490 100644 --- a/clearnode/rpc_store.go +++ b/clearnode/rpc_store.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" + "github.com/erc7824/nitrolite/clearnode/nitrolite" "github.com/lib/pq" "gorm.io/gorm" ) @@ -36,7 +37,7 @@ func NewRPCStore(db *gorm.DB) *RPCStore { } // StoreMessage stores an RPC message in the database -func (s *RPCStore) StoreMessage(sender string, req *RPCData, reqSig []string, resBytes []byte, resSig []string) error { +func (s *RPCStore) StoreMessage(sender string, req *RPCData, reqSigs []Signature, resBytes []byte, resSigs []Signature) error { paramsBytes, err := json.Marshal(req.Params) if err != nil { return err @@ -48,8 +49,8 @@ func (s *RPCStore) StoreMessage(sender string, req *RPCData, reqSig []string, re Method: req.Method, Params: paramsBytes, Response: resBytes, - ReqSig: reqSig, - ResSig: resSig, + ReqSig: nitrolite.SignaturesToStrings(reqSigs), + ResSig: nitrolite.SignaturesToStrings(resSigs), Timestamp: req.Timestamp, } diff --git a/clearnode/rpc_store_test.go b/clearnode/rpc_store_test.go index cb89eb259..9fc27178d 100644 --- a/clearnode/rpc_store_test.go +++ b/clearnode/rpc_store_test.go @@ -5,6 +5,8 @@ import ( "testing" "time" + "github.com/erc7824/nitrolite/clearnode/nitrolite" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -39,9 +41,9 @@ func TestRPCStoreStoreMessage(t *testing.T) { "key1": "value1", "key2": 42, } - reqSig := []string{"sig1", "sig2"} + reqSig := []Signature{Signature(hexutil.MustDecode("0x1234")), Signature(hexutil.MustDecode("0x4567"))} resBytes := []byte(`{"result": "ok"}`) - resSig := []string{"resSig1"} + resSig := []Signature{Signature(hexutil.MustDecode("0x4321"))} // Create RPCData req := &RPCData{ @@ -69,8 +71,9 @@ func TestRPCStoreStoreMessage(t *testing.T) { assert.Equal(t, reqID, record.ReqID) assert.Equal(t, method, record.Method) assert.Equal(t, timestamp, record.Timestamp) - assert.ElementsMatch(t, reqSig, record.ReqSig) - assert.ElementsMatch(t, resSig, record.ResSig) + + assert.ElementsMatch(t, nitrolite.SignaturesToStrings(reqSig), record.ReqSig) + assert.ElementsMatch(t, nitrolite.SignaturesToStrings(resSig), record.ResSig) assert.Equal(t, resBytes, record.Response) // Verify params were stored correctly @@ -83,7 +86,7 @@ func TestRPCStoreStoreMessage(t *testing.T) { // Extract the first element which is our original map storedParams := storedParamsArray[0] assert.Equal(t, "value1", storedParams["key1"]) - assert.Equal(t, float64(42), storedParams["key2"]) // JSON unmarshals numbers as float64 + assert.Equal(t, float64(42), storedParams["key2"]) // JSON unmarshal numbers as float64 } // TestRPCStoreStoreMessageError tests error handling for StoreMessage @@ -103,11 +106,11 @@ func TestRPCStoreStoreMessageError(t *testing.T) { Params: []any{make(chan int)}, // Channels cannot be marshalled to JSON Timestamp: uint64(time.Now().Unix()), } - reqSig := []string{"sig1"} + reqSig := []Signature{Signature([]byte("sig1"))} resBytes := []byte(`{"result": "ok"}`) - resSig := []string{"resSig1"} + resSig := []Signature{Signature([]byte("resSig1"))} - // Attempt to store the message, should fail due to unmarshalable params + // Attempt to store the message, should fail due to unmarshal-able params err := store.StoreMessage(sender, req, reqSig, resBytes, resSig) assert.Error(t, err) assert.Contains(t, err.Error(), "json") diff --git a/clearnode/rpc_test.go b/clearnode/rpc_test.go index 25ee6ee82..2320679f1 100644 --- a/clearnode/rpc_test.go +++ b/clearnode/rpc_test.go @@ -16,7 +16,7 @@ func TestRPCMessageValidate(t *testing.T) { Params: []any{"param1", 2}, Timestamp: uint64(time.Now().Unix()), }, - Sig: []string{"0x1234567890abcdef"}, + Sig: []Signature{Signature([]byte("0x1234567890abcdef"))}, } if err := validate.Struct(rpcMsg); err != nil { diff --git a/clearnode/signer.go b/clearnode/signer.go index 26a297c80..2dfab1db1 100644 --- a/clearnode/signer.go +++ b/clearnode/signer.go @@ -8,11 +8,12 @@ import ( "github.com/erc7824/nitrolite/clearnode/nitrolite" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) +type Signature = nitrolite.Signature + // Allowance represents allowances for connection type Allowance struct { Asset string `json:"asset"` @@ -39,33 +40,8 @@ func NewSigner(privateKeyHex string) (*Signer, error) { } // Sign creates an ECDSA signature for the provided data -func (s *Signer) Sign(data []byte) ([]byte, error) { - sig, err := nitrolite.Sign(data, s.privateKey) - if err != nil { - return nil, err - } - - signature := make([]byte, 65) - copy(signature[0:32], sig.R[:]) - copy(signature[32:64], sig.S[:]) - - if sig.V >= 27 { - signature[64] = sig.V - 27 - } - return signature, nil -} - -// NitroSign creates a signature for the provided state in nitrolite.Signature format -func (s *Signer) NitroSign(encodedState []byte) (nitrolite.Signature, error) { - sig, err := nitrolite.Sign(encodedState, s.privateKey) - if err != nil { - return nitrolite.Signature{}, fmt.Errorf("failed to sign encoded state: %w", err) - } - return nitrolite.Signature{ - V: sig.V, - R: sig.R, - S: sig.S, - }, nil +func (s *Signer) Sign(data []byte) (Signature, error) { + return nitrolite.Sign(data, s.privateKey) } // GetPublicKey returns the public key associated with the signer @@ -84,11 +60,7 @@ func (s *Signer) GetAddress() common.Address { } // RecoverAddress takes the original message and its hex-encoded signature, and returns the address -func RecoverAddress(message []byte, signatureHex string) (string, error) { - sig, err := hexutil.Decode(signatureHex) - if err != nil { - return "", fmt.Errorf("invalid signature hex: %w", err) - } +func RecoverAddress(message []byte, sig Signature) (string, error) { if len(sig) != 65 { return "", fmt.Errorf("invalid signature length: got %d, want 65", len(sig)) } @@ -117,7 +89,7 @@ func RecoverAddressFromEip712Signature( scope string, application string, expire string, - signatureHex string) (string, error) { + sig Signature) (string, error) { convertedAllowances := convertAllowances(allowances) typedData := apitypes.TypedData{ @@ -159,18 +131,12 @@ func RecoverAddressFromEip712Signature( return "", err } - // 2. Example signature - sig, err := hexutil.Decode(signatureHex) - if err != nil { - return "", err - } - - // 3. Fix V if needed (Ethereum uses 27/28, go-ethereum expects 0/1) + // 2. Fix V if needed (Ethereum uses 27/28, go-ethereum expects 0/1) if sig[64] >= 27 { sig[64] -= 27 } - // 4. Recover public key + // 3. Recover public key pubKey, err := crypto.SigToPub(typedDataHash, sig) if err != nil { return "", err diff --git a/clearnode/signer_test.go b/clearnode/signer_test.go index 9d5f7b53f..15f6a68f6 100644 --- a/clearnode/signer_test.go +++ b/clearnode/signer_test.go @@ -1,8 +1,10 @@ package main import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" ) func TestEIPSignature(t *testing.T) { @@ -12,6 +14,9 @@ func TestEIPSignature(t *testing.T) { Amount: "0", }, } + signature, err := hexutil.Decode("0xe758880bc3d75e9433e0f50c9b40712b0bcf90f437a8c42ba6f8a5a3d144a5ce4b10b020c4eb728323daad49c4cc6329eaa45e3ea88c95d76e55b982f6a0a8741b") + assert.NoError(t, err) + recoveredSigner, err := RecoverAddressFromEip712Signature( "0x21f7d1f35979b125f6f7918fc16cb9101e5882d7", "a9d5b4fd-ef30-4bb6-b9b6-4f2778f004fd", @@ -21,7 +26,7 @@ func TestEIPSignature(t *testing.T) { "console", "0x21f7d1f35979b125f6f7918fc16cb9101e5882d7", "1748608702", - "0xe758880bc3d75e9433e0f50c9b40712b0bcf90f437a8c42ba6f8a5a3d144a5ce4b10b020c4eb728323daad49c4cc6329eaa45e3ea88c95d76e55b982f6a0a8741b", + signature, ) assert.Equal(t, recoveredSigner, "0x21F7D1F35979B125f6F7918fC16Cb9101e5882d7") diff --git a/clearnode/testing/client.go b/clearnode/testing/client.go index b2f4d8f45..1649bef15 100644 --- a/clearnode/testing/client.go +++ b/clearnode/testing/client.go @@ -105,20 +105,7 @@ func NewSigner(privateKeyHex string) (*Signer, error) { // Sign creates an ECDSA signature for the provided data func (s *Signer) Sign(data []byte) ([]byte, error) { - sig, err := nitrolite.Sign(data, s.privateKey) - if err != nil { - return nil, err - } - - signature := make([]byte, 65) - copy(signature[0:32], sig.R[:]) - copy(signature[32:64], sig.S[:]) - - // EIP-155 style V: subtract 27 if needed - if sig.V >= 27 { - signature[64] = sig.V - 27 - } - return signature, nil + return nitrolite.Sign(data, s.privateKey) } // GetAddress returns the address derived from the signer's public key diff --git a/contract/script/setupChannel.s.sol b/contract/script/setupChannel.s.sol index d933c65c7..f9865b46e 100644 --- a/contract/script/setupChannel.s.sol +++ b/contract/script/setupChannel.s.sol @@ -6,7 +6,8 @@ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {Custody} from "../src/Custody.sol"; import {Utils} from "../src/Utils.sol"; -import {Channel, State, Allocation, Signature, ChannelStatus, StateIntent, Amount} from "../src/interfaces/Types.sol"; +import {Channel, State, Allocation, ChannelStatus, StateIntent, Amount} from "../src/interfaces/Types.sol"; +import {TestUtils} from "../test/TestUtils.sol"; contract SetupChannelScript is Script { uint64 constant CHALLENGE_DURATION = 1 days; @@ -40,8 +41,8 @@ contract SetupChannelScript is Script { Channel memory channel = createChannel(USER_SESSION_KEY_ADDRESS, addresses[0], adjudicator); State memory initialState = createInitialState(token, USER_SESSION_KEY_ADDRESS, addresses[0]); - Signature memory userSig = signState(channel, initialState, USER_SESSION_KEY); - initialState.sigs = new Signature[](1); + bytes memory userSig = signState(channel, initialState, USER_SESSION_KEY); + initialState.sigs = new bytes[](1); initialState.sigs[0] = userSig; vm.broadcast(addresses[1]); @@ -78,17 +79,16 @@ contract SetupChannelScript is Script { version: 0, data: bytes(""), allocations: allocations, - sigs: new Signature[](0) + sigs: new bytes[](0) }); } function signState(Channel memory chan, State memory state, uint256 privateKey) internal view - returns (Signature memory) + returns (bytes memory) { - bytes32 stateHash = Utils.getStateHash(chan, state); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, stateHash); - return Signature({v: v, r: r, s: s}); + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(chan), state); + return TestUtils.sign(vm, privateKey, packedState); } } diff --git a/contract/src/Custody.sol b/contract/src/Custody.sol index c2a990550..3fb0e364b 100644 --- a/contract/src/Custody.sol +++ b/contract/src/Custody.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; +import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {EnumerableSet} from "lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; @@ -10,7 +11,7 @@ import {IChannelReader} from "./interfaces/IChannelReader.sol"; import {IComparable} from "./interfaces/IComparable.sol"; import {IChannel} from "./interfaces/IChannel.sol"; import {IDeposit} from "./interfaces/IDeposit.sol"; -import {Channel, State, Allocation, ChannelStatus, StateIntent, Signature, Amount} from "./interfaces/Types.sol"; +import {Channel, State, Allocation, ChannelStatus, StateIntent, Amount} from "./interfaces/Types.sol"; import {Utils} from "./Utils.sol"; /** @@ -18,9 +19,10 @@ import {Utils} from "./Utils.sol"; * @notice A simple custody contract for state channels that delegates most state transition logic to an adjudicator * @dev This implementation currently only supports 2 participant channels (CLIENT and SERVER) */ -contract Custody is IChannel, IDeposit, IChannelReader { +contract Custody is IChannel, IDeposit, IChannelReader, EIP712 { using EnumerableSet for EnumerableSet.Bytes32Set; using SafeERC20 for IERC20; + using Utils for State; // Errors // TODO: sort errors @@ -47,7 +49,11 @@ contract Custody is IChannel, IDeposit, IChannelReader { uint256 constant CLIENT_IDX = 0; // Participant index for the channel creator uint256 constant SERVER_IDX = 1; // Participant index for the server in clearnet context - uint256 constant MIN_CHALLENGE_PERIOD = 1 hours; + uint256 public constant MIN_CHALLENGE_PERIOD = 1 hours; + + bytes32 public constant CHALLENGE_STATE_TYPEHASH = keccak256( + "AllowChallengeStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)" + ); // Recommended structure to keep track of states struct Metadata { @@ -71,6 +77,12 @@ contract Custody is IChannel, IDeposit, IChannelReader { mapping(bytes32 channelId => Metadata chMeta) internal _channels; mapping(address account => Ledger ledger) internal _ledgers; + // ========== Constructor ========== + // TODO: make sure the last nitrolite sdk npm package version used + constructor() EIP712("Nitrolite:Custody", "0.2.24") { + // No state initialization needed + } + // ========== Read methods ========== /** @@ -226,10 +238,13 @@ contract Custody is IChannel, IDeposit, IChannelReader { channelId = Utils.getChannelId(ch); if (_channels[channelId].stage != ChannelStatus.VOID) revert InvalidStatus(); - bytes32 stateHash = Utils.getStateHash(ch, initial); if (initial.sigs.length != 1) revert InvalidStateSignatures(); // TODO: later we can lift the restriction that first sig must be from CLIENT - if (!Utils.verifySignature(stateHash, initial.sigs[CLIENT_IDX], ch.participants[CLIENT_IDX])) { + if ( + !initial.verifyStateSignature( + channelId, _domainSeparatorV4(), initial.sigs[CLIENT_IDX], ch.participants[CLIENT_IDX] + ) + ) { revert InvalidStateSignatures(); } @@ -294,7 +309,7 @@ contract Custody is IChannel, IDeposit, IChannelReader { * @param sig Signature of SERVER on the funding state * @return The channelId of the joined channel */ - function join(bytes32 channelId, uint256 index, Signature calldata sig) external returns (bytes32) { + function join(bytes32 channelId, uint256 index, bytes calldata sig) external returns (bytes32) { Metadata storage meta = _channels[channelId]; // checks @@ -304,11 +319,14 @@ contract Custody is IChannel, IDeposit, IChannelReader { if (index != SERVER_IDX) revert InvalidParticipant(); if (meta.actualDeposits[SERVER_IDX].amount != 0) revert DepositAlreadyFulfilled(); - bytes32 stateHash = Utils.getStateHash(meta.chan, meta.lastValidState); - if (!Utils.verifySignature(stateHash, sig, meta.chan.participants[SERVER_IDX])) revert InvalidStateSignatures(); + if ( + !meta.lastValidState.verifyStateSignature( + channelId, _domainSeparatorV4(), sig, meta.chan.participants[SERVER_IDX] + ) + ) revert InvalidStateSignatures(); State memory lastValidState = meta.lastValidState; - Signature[] memory sigs = new Signature[](PART_NUM); + bytes[] memory sigs = new bytes[](PART_NUM); sigs[CLIENT_IDX] = lastValidState.sigs[CLIENT_IDX]; sigs[SERVER_IDX] = sig; lastValidState.sigs = sigs; @@ -390,7 +408,7 @@ contract Custody is IChannel, IDeposit, IChannelReader { bytes32 channelId, State calldata candidate, State[] calldata proofs, - Signature calldata challengerSig + bytes calldata challengerSig ) external { Metadata storage meta = _channels[channelId]; @@ -399,11 +417,7 @@ contract Custody is IChannel, IDeposit, IChannelReader { if (meta.stage == ChannelStatus.DISPUTE || meta.stage == ChannelStatus.FINAL) revert InvalidStatus(); if (candidate.intent == StateIntent.FINALIZE) revert InvalidState(); - _requireChallengerIsParticipant( - Utils.getStateHash(meta.chan, candidate), - [meta.chan.participants[CLIENT_IDX], meta.chan.participants[SERVER_IDX]], - challengerSig - ); + _requireChallengerIsParticipant(channelId, candidate, meta.chan.participants, challengerSig); StateIntent lastValidStateIntent = meta.lastValidState.intent; @@ -592,6 +606,7 @@ contract Custody is IChannel, IDeposit, IChannelReader { Metadata storage meta = _channels[channelId]; // effects + // NOTE: FINAL state is ephemeral because `meta` is deleted at the end of this function meta.stage = ChannelStatus.FINAL; // interactions @@ -638,30 +653,52 @@ contract Custody is IChannel, IDeposit, IChannelReader { * @param state The state to verify signatures for * @return valid True if both signatures are valid */ - function _verifyAllSignatures(Channel memory chan, State memory state) internal view returns (bool valid) { - bytes32 stateHash = Utils.getStateHash(chan, state); - + function _verifyAllSignatures(Channel memory chan, State memory state) internal returns (bool valid) { if (state.sigs.length != PART_NUM) { return false; } + bytes32 channelId = Utils.getChannelId(chan); + for (uint256 i = 0; i < PART_NUM; i++) { - if (!Utils.verifySignature(stateHash, state.sigs[i], chan.participants[i])) return false; + if (!state.verifyStateSignature(channelId, _domainSeparatorV4(), state.sigs[i], chan.participants[i])) { + return false; + } } return true; } function _requireChallengerIsParticipant( - bytes32 challengedStateHash, - address[2] memory participants, - Signature memory challengerSig - ) internal pure { - address challenger = Utils.recoverSigner(keccak256(abi.encode(challengedStateHash, "challenge")), challengerSig); + bytes32 channelId, + State memory state, + address[] memory participants, + bytes memory challengerSig + ) internal view { + // NOTE: ERC-6492 signature is NOT checked as at this point participants should already be deployed + + // NOTE: the "challenge" suffix substitution for raw ECDSA and EIP-191 signatures + bytes memory packedChallengeState = abi.encodePacked(Utils.getPackedState(channelId, state), "challenge"); + address rawSigner = Utils.recoverRawECDSASigner(packedChallengeState, challengerSig); + address eip191Signer = Utils.recoverEIP191Signer(packedChallengeState, challengerSig); + address eip712Signer = Utils.recoverStateEIP712Signer( + _domainSeparatorV4(), CHALLENGE_STATE_TYPEHASH, channelId, state, challengerSig + ); - if (challenger != participants[CLIENT_IDX] && challenger != participants[SERVER_IDX]) { - revert InvalidChallengerSignature(); + for (uint256 i = 0; i < participants.length; i++) { + address participant = participants[i]; + if (participant.code.length != 0) { + if (Utils.isValidERC1271Signature(keccak256(packedChallengeState), challengerSig, participant)) { + return; + } + } else { + if (rawSigner == participant || eip191Signer == participant || eip712Signer == participant) { + return; + } + } } + + revert InvalidChallengerSignature(); } /** diff --git a/contract/src/Utils.sol b/contract/src/Utils.sol index 2bdeef4b3..9d9d67871 100644 --- a/contract/src/Utils.sol +++ b/contract/src/Utils.sol @@ -2,7 +2,10 @@ pragma solidity ^0.8.13; import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {Channel, State, Signature, StateIntent} from "./interfaces/Types.sol"; +import {MessageHashUtils} from "lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sol"; +import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; +import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; +import {STATE_TYPEHASH, Channel, State, StateIntent} from "./interfaces/Types.sol"; /** * @title Channel Utilities @@ -10,9 +13,20 @@ import {Channel, State, Signature, StateIntent} from "./interfaces/Types.sol"; */ library Utils { using ECDSA for bytes32; + using MessageHashUtils for bytes; + using {MessageHashUtils.toTypedDataHash} for bytes32; - uint256 constant CLIENT = 0; - uint256 constant SERVER = 1; + error ERC6492DeploymentFailed(address factory, bytes calldata_); + error ERC6492NoCode(address expectedSigner); + + uint256 public constant CLIENT = 0; + uint256 public constant SERVER = 1; + + bytes32 public constant NO_EIP712_SUPPORT = keccak256("NoEIP712Support"); + + bytes32 public constant ERC6492_DETECTION_SUFFIX = + 0x6492649264926492649264926492649264926492649264926492649264926492; + bytes4 public constant ERC1271_SUCCESS = 0x1626ba7e; /** * @notice Compute the unique identifier for a channel @@ -28,48 +42,204 @@ library Utils { } /** - * @notice Compute the hash of a channel state in a canonical way (ignoring the signature) - * @param ch The channel struct - * @param state The state struct - * @return The state hash as bytes32 - * @dev The state hash is computed according to the specification in the README, using channelId, data, version, and allocations + * @notice Packs the channelId and the state into a byte array for signing + * @param channelId The unique identifier for the channel + * @param state The state struct to pack + * @return The packed channelId and state as bytes */ - function getStateHash(Channel memory ch, State memory state) internal view returns (bytes32) { - bytes32 channelId = getChannelId(ch); - return keccak256(abi.encode(channelId, state.intent, state.version, state.data, state.allocations)); + function getPackedState(bytes32 channelId, State memory state) internal pure returns (bytes memory) { + return abi.encode(channelId, state.intent, state.version, state.data, state.allocations); } /** * @notice Recovers the signer of a state hash from a signature - * @param stateHash The hash of the state to verify (computed using the canonical form) + * @param message The message to verify the signature against + * @param sig The signature to verify + * @return The address of the signer + */ + function recoverRawECDSASigner(bytes memory message, bytes memory sig) internal pure returns (address) { + // Verify the signature directly on the message hash without using EIP-191 + return keccak256(message).recover(sig); + } + + /** + * @notice Recovers the signer of a state hash using EIP-191 format + * @param message The message to verify the signature against + * @param sig The signature to verify + * @return The address of the signer + */ + function recoverEIP191Signer(bytes memory message, bytes memory sig) internal pure returns (address) { + return message.toEthSignedMessageHash().recover(sig); + } + + /** + * @notice Recovers the signer of a state hash using the EIP-712 format + * @param domainSeparator The EIP-712 domain separator + * @param structHash The hash of the struct to verify the signature against + * @param sig The signature to verify + * @return The address of the signer + */ + function recoverEIP712Signer(bytes32 domainSeparator, bytes32 structHash, bytes memory sig) + internal + pure + returns (address) + { + return domainSeparator.toTypedDataHash(structHash).recover(sig); + } + + /** + * @notice Recovers the signer of a state using EIP-712 format + * @param domainSeparator The EIP-712 domain separator + * @param typeHash The type hash for the state structure + * @param channelId The unique identifier for the channel + * @param state The state to verify * @param sig The signature to verify * @return The address of the signer */ - function recoverSigner(bytes32 stateHash, Signature memory sig) internal pure returns (address) { - // Verify the signature directly on the stateHash without using EIP-191 - return ECDSA.recover(stateHash, sig.v, sig.r, sig.s); + function recoverStateEIP712Signer( + bytes32 domainSeparator, + bytes32 typeHash, + bytes32 channelId, + State memory state, + bytes memory sig + ) internal pure returns (address) { + return Utils.recoverEIP712Signer( + domainSeparator, + keccak256( + abi.encode( + typeHash, + channelId, + state.intent, + state.version, + keccak256(state.data), + keccak256(abi.encode(state.allocations)) + ) + ), + sig + ); } /** - * @notice Verifies that a state is signed by the specified participant - * @param stateHash The hash of the state to verify (computed using the canonical form) + * @notice Verifies that a state is signed by the specified EOA participant in either raw ECDSA, EIP-191, or EIP-712 format + * @param state The state to verify + * @param channelId The ID of the channel + * @param domainSeparator The EIP-712 domain separator for the channel * @param sig The signature to verify * @param signer The address of the expected signer * @return True if the signature is valid, false otherwise */ - function verifySignature(bytes32 stateHash, Signature memory sig, address signer) internal pure returns (bool) { - address recoveredSigner = recoverSigner(stateHash, sig); - return recoveredSigner == signer; + function verifyStateEOASignature( + State memory state, + bytes32 channelId, + bytes32 domainSeparator, + bytes memory sig, + address signer + ) internal pure returns (bool) { + bytes memory packedState = Utils.getPackedState(channelId, state); + + address rawECDSASigner = Utils.recoverRawECDSASigner(packedState, sig); + if (rawECDSASigner == signer) { + return true; + } + + address eip191Signer = Utils.recoverEIP191Signer(packedState, sig); + if (eip191Signer == signer) { + return true; + } + + if (domainSeparator == NO_EIP712_SUPPORT) { + return false; + } + + address eip712Signer = Utils.recoverStateEIP712Signer(domainSeparator, STATE_TYPEHASH, channelId, state, sig); + if (eip712Signer == signer) { + return true; + } + + return false; } /** - * @notice Compares two states for equality - * @param a The first state to compare - * @param b The second state to compare - * @return True if the states are equal, false otherwise + * @notice Checks if a signature is valid by calling the expected signer contract according to the ERC-1271 standard + * @param msgHash The hash of the message to verify the signature against + * @param sig The signature to verify + * @param expectedSigner The address of the expected signer + * @return True if the signature is valid, false otherwise or if signer is not a contract */ - function statesAreEqual(State memory a, State memory b) internal pure returns (bool) { - return keccak256(abi.encode(a)) == keccak256(abi.encode(b)); + function isValidERC1271Signature(bytes32 msgHash, bytes memory sig, address expectedSigner) + internal + view + returns (bool) + { + return IERC1271(expectedSigner).isValidSignature(msgHash, sig) == ERC1271_SUCCESS; + } + + /** + * @notice Checks the validity of a smart contract signature. If the expected signer has no code, it is deployed using the provided factory and calldata from the signature. + * Otherwise, it checks the signature using the ERC-1271 standard. + * @param msgHash The hash of the message to verify the signature against + * @param sig The signature to verify + * @param expectedSigner The address of the expected signer + * @return True if the signature is valid, false otherwise or if signer is not a contract + */ + function isValidERC6492Signature(bytes32 msgHash, bytes memory sig, address expectedSigner) + internal + returns (bool) + { + (address create2Factory, bytes memory factoryCalldata, bytes memory originalSig) = + abi.decode(sig, (address, bytes, bytes)); + + if (expectedSigner.code.length == 0) { + (bool success,) = create2Factory.call(factoryCalldata); + require(success, ERC6492DeploymentFailed(create2Factory, factoryCalldata)); + require(expectedSigner.code.length != 0, ERC6492NoCode(expectedSigner)); + } + + return IERC1271(expectedSigner).isValidSignature(msgHash, originalSig) == ERC1271_SUCCESS; + } + + /** + * @notice Returns the last 32 bytes of a byte array + * @param data The byte array to extract from + * @return result The last 32 bytes of the byte array + */ + function trailingBytes32(bytes memory data) internal pure returns (bytes32 result) { + if (data.length < 32) { + return bytes32(0); + } + assembly { + result := mload(add(data, mload(data))) + } + } + + /** + * @notice Verifies that a state is signed by the specified participant as an EOA or a Smart Contract + * @param state The state to verify + * @param channelId The ID of the channel + * @param domainSeparator The EIP-712 domain separator for the channel + * @param sig The signature to verify + * @param signer The address of the expected signer + * @return True if the signature is valid, false otherwise + */ + function verifyStateSignature( + State memory state, + bytes32 channelId, + bytes32 domainSeparator, + bytes memory sig, + address signer + ) internal returns (bool) { + // NOTE: both EIP-1271 and EIP-6492 signatures use message hash + bytes32 stateHash = keccak256(Utils.getPackedState(channelId, state)); + + if (trailingBytes32(sig) == ERC6492_DETECTION_SUFFIX) { + return isValidERC6492Signature(stateHash, sig, signer); + } + + if (signer.code.length != 0) { + return isValidERC1271Signature(stateHash, sig, signer); + } + + return Utils.verifyStateEOASignature(state, channelId, domainSeparator, sig, signer); } /** @@ -77,9 +247,13 @@ library Utils { * @dev Initial states must have version 0 and INITIALIZE intent * @param state The state to validate * @param chan The channel configuration + * @param domainSeparator The EIP-712 domain separator for the channel * @return True if the state is a valid initial state, false otherwise */ - function validateInitialState(State memory state, Channel memory chan) internal view returns (bool) { + function validateInitialState(State memory state, Channel memory chan, bytes32 domainSeparator) + internal + returns (bool) + { if (state.version != 0) { return false; } @@ -88,7 +262,7 @@ library Utils { return false; } - return validateUnanimousSignatures(state, chan); + return validateUnanimousStateSignatures(state, chan, domainSeparator); } /** @@ -96,18 +270,31 @@ library Utils { * @dev For 2-participant channels, both must sign to establish unanimous consent * @param state The state to validate * @param chan The channel configuration + * @param domainSeparator The EIP-712 domain separator for the channel * @return True if the state has valid signatures from both participants, false otherwise */ - function validateUnanimousSignatures(State memory state, Channel memory chan) internal view returns (bool) { + function validateUnanimousStateSignatures(State memory state, Channel memory chan, bytes32 domainSeparator) + internal + returns (bool) + { if (state.sigs.length != 2) { return false; } - // Compute the state hash for signature verification. - bytes32 stateHash = getStateHash(chan, state); + bytes32 channelId = getChannelId(chan); - return verifySignature(stateHash, state.sigs[0], chan.participants[CLIENT]) - && verifySignature(stateHash, state.sigs[1], chan.participants[SERVER]); + return Utils.verifyStateSignature(state, channelId, domainSeparator, state.sigs[0], chan.participants[CLIENT]) + && Utils.verifyStateSignature(state, channelId, domainSeparator, state.sigs[1], chan.participants[SERVER]); + } + + /** + * @notice Compares two states for equality + * @param a The first state to compare + * @param b The second state to compare + * @return True if the states are equal, false otherwise + */ + function statesAreEqual(State memory a, State memory b) internal pure returns (bool) { + return keccak256(abi.encode(a)) == keccak256(abi.encode(b)); } /** diff --git a/contract/src/adjudicators/ConsensusTransition.sol b/contract/src/adjudicators/ConsensusTransition.sol index d93d44505..94553f89e 100644 --- a/contract/src/adjudicators/ConsensusTransition.sol +++ b/contract/src/adjudicators/ConsensusTransition.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.13; import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, Signature, StateIntent} from "../interfaces/Types.sol"; +import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; +import {EIP712AdjudicatorBase} from "./EIP712AdjudicatorBase.sol"; import {Utils} from "../Utils.sol"; /** @@ -10,19 +11,25 @@ import {Utils} from "../Utils.sol"; * @notice An adjudicator that validates state based on mutual signatures from both participants and the transition from the previous state. * @dev Any state is considered valid as long as it's signed by both participants and is a valid transition from the previous state. */ -contract ConsensusTransition is IAdjudicator { +contract ConsensusTransition is IAdjudicator, EIP712AdjudicatorBase { using Utils for State; /** - * @notice Validates that the state is signed by both participants - * @param chan The channel configuration - * @param candidate The proposed state - * @param proofs Array of previous states - * @return valid True if the state is valid, false otherwise + * @notice Constructor for the ConsensusTransition adjudicator. + * @param owner The owner of the adjudicator contract. + * @param channelImpl The address of the channel implementation contract. + */ + constructor(address owner, address channelImpl) EIP712AdjudicatorBase(owner, channelImpl) {} + + /** + * @notice Validates that the state is signed by both participants. + * @param chan The channel configuration. + * @param candidate The proposed state. + * @param proofs Array of previous states. + * @return valid True if the state is valid, false otherwise. */ function adjudicate(Channel calldata chan, State calldata candidate, State[] calldata proofs) external - view override returns (bool valid) { @@ -35,14 +42,18 @@ contract ConsensusTransition is IAdjudicator { return false; } + bytes32 channelImplDomainSeparator = getChannelImplDomainSeparator(); + // proof is Initialize State if (candidate.version == 1) { - return proofs[0].validateTransitionTo(candidate) && proofs[0].validateInitialState(chan) - && candidate.validateUnanimousSignatures(chan); + return proofs[0].validateTransitionTo(candidate) + && proofs[0].validateInitialState(chan, channelImplDomainSeparator) + && candidate.validateUnanimousStateSignatures(chan, channelImplDomainSeparator); } // proof is Operate or Resize State (both have same validation) - return proofs[0].validateTransitionTo(candidate) && proofs[0].validateUnanimousSignatures(chan) - && candidate.validateUnanimousSignatures(chan); + return proofs[0].validateTransitionTo(candidate) + && proofs[0].validateUnanimousStateSignatures(chan, channelImplDomainSeparator) + && candidate.validateUnanimousStateSignatures(chan, channelImplDomainSeparator); } } diff --git a/contract/src/adjudicators/Counter.sol b/contract/src/adjudicators/Counter.sol index 6a057e59c..111da9200 100644 --- a/contract/src/adjudicators/Counter.sol +++ b/contract/src/adjudicators/Counter.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.13; import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, Signature, StateIntent} from "../interfaces/Types.sol"; +import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; import {Utils} from "../Utils.sol"; /** @@ -32,7 +32,6 @@ contract Counter is IAdjudicator { */ function adjudicate(Channel calldata chan, State calldata candidate, State[] calldata proofs) external - view override returns (bool valid) { @@ -59,7 +58,7 @@ contract Counter is IAdjudicator { // proof is Initialize State if (candidate.version == 1) { return proofs[0].validateTransitionTo(candidate) && _validateAppTransitionTo(proofs[0].data, candidate.data) - && proofs[0].validateInitialState(chan) && _validateStateSig(chan, candidate); + && proofs[0].validateInitialState(chan, Utils.NO_EIP712_SUPPORT) && _validateStateSig(chan, candidate); } bytes memory proofData = proofs[0].data; @@ -85,7 +84,7 @@ contract Counter is IAdjudicator { return candidateDataDecoded.target == previousDataDecoded.target; } - function _validateStateSig(Channel calldata chan, State calldata state) internal view returns (bool) { + function _validateStateSig(Channel calldata chan, State calldata state) internal returns (bool) { if (state.sigs.length != 1) { return false; } @@ -97,6 +96,8 @@ contract Counter is IAdjudicator { signerIdx = 1; // guest signer } - return Utils.verifySignature(Utils.getStateHash(chan, state), state.sigs[0], chan.participants[signerIdx]); + return state.verifyStateSignature( + Utils.getChannelId(chan), Utils.NO_EIP712_SUPPORT, state.sigs[0], chan.participants[signerIdx] + ); } } diff --git a/contract/src/adjudicators/EIP712AdjudicatorBase.sol b/contract/src/adjudicators/EIP712AdjudicatorBase.sol new file mode 100644 index 000000000..29b611f22 --- /dev/null +++ b/contract/src/adjudicators/EIP712AdjudicatorBase.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {IERC5267} from "lib/openzeppelin-contracts/contracts/interfaces/IERC5267.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Ownable2Step} from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol"; + +import {Utils} from "../Utils.sol"; + +/** + * @title EIP712 Adjudicator Base + * @notice Base contract for EIP712 compliant adjudicators when EIP-712 domain specifies channel implementation contract address as a verifying contract. + * @dev Contains a link to the channel implementation contract and provides a method to retrieve its domain separator. + * Channel implementation contract must be ERC-5267 compliant. + */ +abstract contract EIP712AdjudicatorBase is Ownable2Step { + /// Taken from EIP-712 specification + /// https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + IERC5267 public channelImpl; + + /** + * @notice Constructor for the EIP712AdjudicatorBase. + * @param owner The owner of the adjudicator contract. + * @param channelImpl_ The address of the channel implementation contract. + */ + constructor(address owner, address channelImpl_) Ownable(owner) { + channelImpl = IERC5267(channelImpl_); + } + + /** + * @notice Returns the domain separator for the channel implementation contract. + * @return The domain separator as bytes32. + * @dev This function retrieves the domain separator from the channel implementation contract. + */ + function getChannelImplDomainSeparator() public view returns (bytes32) { + if (address(channelImpl).code.length == 0) { + // NOTE: soft failure, if channel implementation contract is not set or does not exist + return Utils.NO_EIP712_SUPPORT; + } + + try channelImpl.eip712Domain() returns ( + bytes1, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32, + uint256[] memory + ) { + /// Taken from EIP-712 specification + /// https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator + return keccak256( + abi.encode(TYPE_HASH, keccak256(bytes(name)), keccak256(bytes(version)), chainId, verifyingContract) + ); + } catch { + // NOTE: soft failure, if channel implementation contract does not support EIP-712 + return Utils.NO_EIP712_SUPPORT; + } + } + + /** + * @notice Sets the channel implementation contract address. + * @dev Callable only by Owner. + * @param channelImpl_ The address of the channel implementation contract. + */ + function setChannelImpl(address channelImpl_) external onlyOwner { + channelImpl = IERC5267(channelImpl_); + } +} diff --git a/contract/src/adjudicators/Remittance.sol b/contract/src/adjudicators/Remittance.sol index 1cc8bd909..245ecc993 100644 --- a/contract/src/adjudicators/Remittance.sol +++ b/contract/src/adjudicators/Remittance.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.13; import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; import {IComparable} from "../interfaces/IComparable.sol"; -import {Channel, State, Allocation, Signature, Amount, StateIntent} from "../interfaces/Types.sol"; +import {Channel, State, Allocation, Amount, StateIntent} from "../interfaces/Types.sol"; +import {EIP712AdjudicatorBase} from "./EIP712AdjudicatorBase.sol"; import {Utils} from "../Utils.sol"; /** @@ -13,16 +14,16 @@ import {Utils} from "../Utils.sol"; * This prevents forging "more balance out of thin air" since only the person giving up funds * needs to sign, making it secure for two-party channels with single allocations per party */ -contract RemittanceAdjudicator is IAdjudicator, IComparable { +contract RemittanceAdjudicator is IAdjudicator, IComparable, EIP712AdjudicatorBase { using Utils for State; uint8 constant CREATOR = 0; uint8 constant BROKER = 1; /** - * @dev Remittance represents a payment transfer from one participant to another - * @param sender Index of the participant sending funds (0 for CREATOR, 1 for BROKER) - * @param amount Amount and token being transferred + * @dev Remittance represents a payment transfer from one participant to another. + * @param sender Index of the participant sending funds (0 for CREATOR, 1 for BROKER). + * @param amount Amount and token being transferred. */ struct Remittance { uint8 sender; // Index of the participant sending funds @@ -30,16 +31,22 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { } /** - * @notice Validates state transitions based on the principle that only the sender needs to sign - * @param chan The channel configuration - * @param candidate The proposed state - * @param proofs Array containing previous states in increasing order up to a starting state, which is either INITIALIZE, RESIZE or state signed by the other party + * @notice Constructor for the Remittance adjudicator. + * @param owner The owner of the adjudicator contract. + * @param channelImpl The address of the channel implementation contract. + */ + constructor(address owner, address channelImpl) EIP712AdjudicatorBase(owner, channelImpl) {} + + /** + * @notice Validates state transitions based on the principle that only the sender needs to sign. + * @param chan The channel configuration. + * @param candidate The proposed state. + * @param proofs Array containing previous states in increasing order up to a starting state, which is either INITIALIZE, RESIZE or state signed by the other party. * I.e. if the same party has consequently been signing Remittances, they should supply all their subsequent states, based on a starting state. - * @return valid True if the state transition is valid, false otherwise + * @return valid True if the state transition is valid, false otherwise. */ function adjudicate(Channel calldata chan, State calldata candidate, State[] calldata proofs) external - view override returns (bool valid) { @@ -48,6 +55,8 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { return false; } + bytes32 channelImplDomainSeparator = getChannelImplDomainSeparator(); + Remittance memory candidateRemittance = abi.decode(candidate.data, (Remittance)); // The last proof must be either INITIALIZE, RESIZE, or signed by the other party @@ -55,11 +64,11 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { // Check if the last proof is a valid starting point if (earliestProof.intent == StateIntent.INITIALIZE) { - if (!earliestProof.validateInitialState(chan)) { + if (!earliestProof.validateInitialState(chan, channelImplDomainSeparator)) { return false; } } else if (earliestProof.intent == StateIntent.RESIZE) { - if (!earliestProof.validateUnanimousSignatures(chan)) { + if (!earliestProof.validateUnanimousStateSignatures(chan, channelImplDomainSeparator)) { return false; } // NOTE: "extract" resize amounts, keep only the RESIZE state data @@ -76,8 +85,14 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { uint8 otherParty = candidateRemittance.sender == CREATOR ? BROKER : CREATOR; // Verify the other party signed the last proof - bytes32 lastProofHash = Utils.getStateHash(chan, earliestProof); - if (!Utils.verifySignature(lastProofHash, earliestProof.sigs[0], chan.participants[otherParty])) { + if ( + !earliestProof.verifyStateSignature( + Utils.getChannelId(chan), + channelImplDomainSeparator, + earliestProof.sigs[0], + chan.participants[otherParty] + ) + ) { return false; } } else { @@ -89,7 +104,7 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { uint256 proofsLength = proofs.length; if (proofsLength == 0) { - if (!_validateRemittanceState(chan, candidate)) { + if (!_validateRemittanceState(channelImplDomainSeparator, chan, candidate)) { return false; } @@ -102,7 +117,7 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { previousState = currentState; currentState = proofs[currIdx]; - if (!_validateRemittanceState(chan, currentState)) { + if (!_validateRemittanceState(channelImplDomainSeparator, chan, currentState)) { return false; } @@ -114,7 +129,10 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { return true; } - function _validateRemittanceState(Channel calldata chan, State memory state) internal view returns (bool) { + function _validateRemittanceState(bytes32 domainSeparator, Channel calldata chan, State memory state) + internal + returns (bool) + { if (state.intent != StateIntent.OPERATE) { return false; } @@ -134,8 +152,9 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { } // Verify signature is from the sender - bytes32 stateHash = Utils.getStateHash(chan, state); - return Utils.verifySignature(stateHash, state.sigs[0], chan.participants[currentRemittance.sender]); + return state.verifyStateSignature( + Utils.getChannelId(chan), domainSeparator, state.sigs[0], chan.participants[currentRemittance.sender] + ); } function _validateRemittanceTransition(State memory previousState, State memory currentState) @@ -144,7 +163,7 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { returns (bool) { // basic validations: version, allocations sum - if (!previousState.validateTransitionTo(currentState)) { + if (!Utils.validateTransitionTo(previousState, currentState)) { return false; } @@ -188,9 +207,9 @@ contract RemittanceAdjudicator is IAdjudicator, IComparable { } /** - * @notice Compares two states to determine their relative ordering - * @param candidate The state being evaluated - * @param previous The reference state to compare against + * @notice Compares two states to determine their relative ordering. + * @param candidate The state being evaluated. + * @param previous The reference state to compare against. * @return result The comparison result: * -1: candidate < previous (candidate is older) * 0: candidate == previous (same recency) diff --git a/contract/src/adjudicators/SimpleConsensus.sol b/contract/src/adjudicators/SimpleConsensus.sol index 6ffb2d80d..99b1de3cb 100644 --- a/contract/src/adjudicators/SimpleConsensus.sol +++ b/contract/src/adjudicators/SimpleConsensus.sol @@ -2,7 +2,8 @@ pragma solidity ^0.8.13; import {IAdjudicator} from "../interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, Signature, StateIntent} from "../interfaces/Types.sol"; +import {Channel, State, Allocation, StateIntent} from "../interfaces/Types.sol"; +import {EIP712AdjudicatorBase} from "./EIP712AdjudicatorBase.sol"; import {Utils} from "../Utils.sol"; /** @@ -10,19 +11,25 @@ import {Utils} from "../Utils.sol"; * @notice An adjudicator that validates state based on mutual signatures from both participants. * @dev Any state is considered valid as long as it's signed by both participants. */ -contract SimpleConsensus is IAdjudicator { +contract SimpleConsensus is IAdjudicator, EIP712AdjudicatorBase { using Utils for State; /** - * @notice Validates that the state is signed by both participants - * @param chan The channel configuration - * @param candidate The proposed state - * @param proofs Array of previous states (unused in this implementation) - * @return valid True if the state is valid, false otherwise + * @notice Constructor for the SimpleConsensus adjudicator. + * @param owner The owner of the adjudicator contract. + * @param channelImpl The address of the channel implementation contract. + */ + constructor(address owner, address channelImpl) EIP712AdjudicatorBase(owner, channelImpl) {} + + /** + * @notice Validates that the state is signed by both participants. + * @param chan The channel configuration. + * @param candidate The proposed state. + * @param proofs Array of previous states (unused in this implementation). + * @return valid True if the state is valid, false otherwise. */ function adjudicate(Channel calldata chan, State calldata candidate, State[] calldata proofs) external - view override returns (bool valid) { @@ -30,11 +37,14 @@ contract SimpleConsensus is IAdjudicator { return false; } + bytes32 channelImplDomainSeparator = getChannelImplDomainSeparator(); + if (candidate.version == 0) { - return candidate.validateInitialState(chan); + return candidate.validateInitialState(chan, channelImplDomainSeparator); } // proof is Operate or Resize State (both have same validation) - return candidate.intent != StateIntent.INITIALIZE && candidate.validateUnanimousSignatures(chan); + return candidate.intent != StateIntent.INITIALIZE + && candidate.validateUnanimousStateSignatures(chan, channelImplDomainSeparator); } } diff --git a/contract/src/interfaces/IAdjudicator.sol b/contract/src/interfaces/IAdjudicator.sol index 300a61385..7bf5838a6 100644 --- a/contract/src/interfaces/IAdjudicator.sol +++ b/contract/src/interfaces/IAdjudicator.sol @@ -19,6 +19,5 @@ interface IAdjudicator { */ function adjudicate(Channel calldata chan, State calldata candidate, State[] calldata proofs) external - view returns (bool valid); } diff --git a/contract/src/interfaces/IChannel.sol b/contract/src/interfaces/IChannel.sol index 95d8c85d7..e0d5c2675 100644 --- a/contract/src/interfaces/IChannel.sol +++ b/contract/src/interfaces/IChannel.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {Channel, State, Signature, Amount} from "./Types.sol"; +import {Channel, State, Amount} from "./Types.sol"; /** * @title State Channel Interface @@ -76,7 +76,7 @@ interface IChannel { * @param sig Signature of the participant on the funding state * @return channelId Unique identifier for the joined channel */ - function join(bytes32 channelId, uint256 index, Signature calldata sig) external returns (bytes32); + function join(bytes32 channelId, uint256 index, bytes calldata sig) external returns (bytes32); /** * @notice Finalizes a channel with a mutually signed closing state @@ -110,7 +110,7 @@ interface IChannel { bytes32 channelId, State calldata candidate, State[] calldata proofs, - Signature calldata challengerSig + bytes calldata challengerSig ) external; /** diff --git a/contract/src/interfaces/Types.sol b/contract/src/interfaces/Types.sol index f0a96c923..22fca8853 100644 --- a/contract/src/interfaces/Types.sol +++ b/contract/src/interfaces/Types.sol @@ -6,15 +6,10 @@ pragma solidity ^0.8.13; * @notice Shared types used in the Nitrolite state channel system */ -/** - * @notice Signature structure for digital signatures - * @dev Used for off-chain signatures verification in the state channel protocol - */ -struct Signature { - uint8 v; // Recovery ID - bytes32 r; // R component of the signature - bytes32 s; // S component of the signature -} +/// @dev EIP-712 domain separator type hash for state channel protocol +bytes32 constant STATE_TYPEHASH = keccak256( + "AllowStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)" +); /** * @notice Amount structure for token value storage @@ -80,7 +75,7 @@ struct State { uint256 version; // State version incremental number to compare most recent bytes data; // Application data encoded, decoded by the adjudicator for business logic Allocation[] allocations; // Combined asset allocation and destination for each participant - Signature[] sigs; // stateHash signatures from participants + bytes[] sigs; // stateHash signatures from participants } /** diff --git a/contract/test/Custody.t.sol b/contract/test/Custody.t.sol index 16d1738a2..6e4bd82e0 100644 --- a/contract/test/Custody.t.sol +++ b/contract/test/Custody.t.sol @@ -12,25 +12,30 @@ pragma solidity ^0.8.13; * - Version 98: Counter-challenge state * - Version 100: Closing state */ -import {Test, console} from "lib/forge-std/src/Test.sol"; +import {Test} from "lib/forge-std/src/Test.sol"; import {IERC20} from "lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import {MessageHashUtils} from "lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sol"; import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import {MockFlagERC1271} from "./mocks/MockFlagERC1271.sol"; // import to allow easier artifact fetching for `getCode` cheat code import {TestUtils} from "./TestUtils.sol"; import {Custody} from "../src/Custody.sol"; -import {Channel, State, Allocation, Signature, ChannelStatus, StateIntent, Amount} from "../src/interfaces/Types.sol"; +import { + Channel, State, Allocation, ChannelStatus, StateIntent, Amount, STATE_TYPEHASH +} from "../src/interfaces/Types.sol"; import {Utils} from "../src/Utils.sol"; import {FlagAdjudicator} from "./mocks/FlagAdjudicator.sol"; import {MockERC20} from "./mocks/MockERC20.sol"; -contract CustodyTest is Test { +contract CustodyTest_Base is Test { Custody public custody; FlagAdjudicator public adjudicator; MockERC20 public token; + bytes32 custodyDomainSeparator; + // Private keys for testing uint256 constant hostSKPrivKey = 1; uint256 constant guestSKPrivKey = 2; @@ -53,7 +58,14 @@ contract CustodyTest is Test { uint256 constant DEPOSIT_AMOUNT = 1000; uint256 constant INITIAL_BALANCE = 10000; - function setUp() public { + function setUp() public virtual { + // Deploy contracts + custody = new Custody(); + adjudicator = new FlagAdjudicator(); + token = new MockERC20("Test Token", "TST", 18); + + custodyDomainSeparator = TestUtils.buildDomainSeparatorForContract(custody); + // Set up user addresses from private keys hostSK = vm.addr(hostSKPrivKey); guestSK = vm.addr(guestSKPrivKey); @@ -62,11 +74,6 @@ contract CustodyTest is Test { hostWallet = vm.addr(hostWalletPrivKey); guestWallet = vm.addr(guestWalletPrivKey); - // Deploy contracts - custody = new Custody(); - adjudicator = new FlagAdjudicator(); - token = new MockERC20("Test Token", "TST", 18); - // Fund accounts token.mint(hostSK, INITIAL_BALANCE); token.mint(guestSK, INITIAL_BALANCE); @@ -157,7 +164,7 @@ contract CustodyTest is Test { version: 0, // Initial state has version 0 data: bytes(""), // Empty data allocations: allocations, - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); } @@ -172,7 +179,7 @@ contract CustodyTest is Test { version: 0, // Initial state has version 0 data: bytes(""), // Empty data allocations: allocations, - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); } @@ -188,7 +195,7 @@ contract CustodyTest is Test { version: 0, // Initial state has version 0 data: bytes(""), // Empty data allocations: allocations, - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); } @@ -203,29 +210,37 @@ contract CustodyTest is Test { version: 0, // Initial state has version 0 data: bytes(""), // Empty data allocations: allocations, - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); } function signState(Channel memory chan, State memory state, uint256 privateKey) internal view - returns (Signature memory) + returns (bytes memory) { - bytes32 stateHash = Utils.getStateHash(chan, state); - (uint8 v, bytes32 r, bytes32 s) = TestUtils.sign(vm, privateKey, stateHash); - return Signature({v: v, r: r, s: s}); + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(chan), state); + return TestUtils.sign(vm, privateKey, packedState); } function signChallenge(Channel memory chan, State memory state, uint256 privateKey) internal view - returns (Signature memory) + returns (bytes memory) { - bytes32 stateHash = Utils.getStateHash(chan, state); - bytes32 challengeHash = keccak256(abi.encode(stateHash, "challenge")); - (uint8 v, bytes32 r, bytes32 s) = TestUtils.sign(vm, privateKey, challengeHash); - return Signature({v: v, r: r, s: s}); + bytes memory packedChallengeState = + abi.encodePacked(Utils.getPackedState(Utils.getChannelId(chan), state), "challenge"); + return TestUtils.sign(vm, privateKey, packedChallengeState); + } + + function signChallengeEIP712(Channel memory chan, State memory state, uint256 privateKey) + internal + view + returns (bytes memory) + { + return TestUtils.signStateEIP712( + vm, Utils.getChannelId(chan), state, custody.CHALLENGE_STATE_TYPEHASH(), custodyDomainSeparator, privateKey + ); } function depositTokens(address user, uint256 amount) internal { @@ -236,7 +251,265 @@ contract CustodyTest is Test { function skipChallengeTime() internal { skip(CHALLENGE_DURATION + 1); } +} + +contract CustodyTest_challenge is CustodyTest_Base { + // Pre-challenge setup variables + Channel internal chan; + State internal initialState; + bytes32 internal channelId; + + function setUp() public override { + super.setUp(); + + // Create and fund a channel with both participants + chan = createTestChannelWithSK(); + initialState = createInitialStateWithSK(); + + // Set up signatures + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); + hostSigs[0] = hostSig; + initialState.sigs = hostSigs; + + // Create channel with host + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + vm.prank(hostSK); + channelId = custody.create(chan, initialState); + + // Guest joins the channel + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); + depositTokens(guestSK, DEPOSIT_AMOUNT * 2); + vm.prank(guestSK); + custody.join(channelId, 1, guestSig); + } + + function test_success_rawECDSASig() public { + // 1. Create a challenge state + State memory challengeState = initialState; + challengeState.intent = StateIntent.OPERATE; + challengeState.data = abi.encode(42); + challengeState.version = 97; // Version 97 indicates a challenge state + + // Host signs the challenge state + bytes memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); + bytes[] memory challengeSigs = new bytes[](1); + challengeSigs[0] = hostChallengeSig; + challengeState.sigs = challengeSigs; + + // 2. Host challenges with this state and signs the challenge + bytes memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); + vm.prank(hostSK); + custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); + + // 3. Verify channel is in CHALLENGED status + (, ChannelStatus status,, uint256 challengeExpiry,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.DISPUTE, "Channel should be in DISPUTE status"); + assertTrue(challengeExpiry > block.timestamp, "Channel should have challengeExpiry set in future"); + } + + function test_success_EIP712Sig() public { + // 1. Create a challenge state + State memory challengeState = initialState; + challengeState.intent = StateIntent.OPERATE; + challengeState.data = abi.encode(42); + challengeState.version = 97; // Version 97 indicates a challenge state + + // Host signs the challenge state + bytes memory hostChallengeSig = TestUtils.signStateEIP712( + vm, Utils.getChannelId(chan), challengeState, STATE_TYPEHASH, custodyDomainSeparator, hostSKPrivKey + ); + bytes[] memory challengeSigs = new bytes[](1); + challengeSigs[0] = hostChallengeSig; + challengeState.sigs = challengeSigs; + + // 2. Host challenges with this state and signs the challenge + bytes memory hostChallengerSig = signChallengeEIP712(chan, challengeState, hostSKPrivKey); + vm.prank(hostSK); + custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); + + // 3. Verify channel is in CHALLENGED status + (, ChannelStatus status,, uint256 challengeExpiry,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.DISPUTE, "Channel should be in DISPUTE status"); + assertTrue(challengeExpiry > block.timestamp, "Channel should have challengeExpiry set in future"); + } + + function test_success_EIP1271Sig() public { + // 0. Deploy an EIP-1271 contract at hostSK address + bool flag = true; // Assume the EIP-1271 contract always returns true for valid signatures + deployCodeTo("MockFlagERC1271", abi.encode(flag), hostSK); + // 1. Create a challenge state + State memory challengeState = initialState; + challengeState.intent = StateIntent.OPERATE; + challengeState.data = abi.encode(42); + challengeState.version = 97; // Version 97 indicates a challenge state + + // Host signs the challenge state. NOTE: assume signature verifier accepts raw ECDSA signatures + bytes memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); + bytes[] memory challengeSigs = new bytes[](1); + challengeSigs[0] = hostChallengeSig; + challengeState.sigs = challengeSigs; + + // 2. Host challenges with this state and signs the challenge + bytes memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); + vm.prank(hostSK); + custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); + + // 3. Verify channel is in CHALLENGED status + (, ChannelStatus status,, uint256 challengeExpiry,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.DISPUTE, "Channel should be in DISPUTE status"); + assertTrue(challengeExpiry > block.timestamp, "Channel should have challengeExpiry set in future"); + } + + function test_revert_whenOngoingChallenge() public { + // 1. Create and submit first challenge state + State memory challengeState = initialState; + challengeState.intent = StateIntent.OPERATE; + challengeState.data = abi.encode(42); + challengeState.version = 97; // Version 97 indicates a challenge state + + // Host signs the challenge state + bytes memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); + bytes[] memory challengeSigs = new bytes[](1); + challengeSigs[0] = hostChallengeSig; + challengeState.sigs = challengeSigs; + bytes memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); + + // Submit first challenge + vm.prank(hostSK); + custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); + + // 2. Create a new challenge state with the same version number + State memory sameVersionChallenge = initialState; + sameVersionChallenge.intent = StateIntent.OPERATE; + sameVersionChallenge.data = abi.encode(43); // Different data but same version + sameVersionChallenge.version = 97; // Same version as previous challenge (97) + + // Host signs the same version challenge + bytes memory hostSameVersionSig = signState(chan, sameVersionChallenge, hostSKPrivKey); + bytes[] memory sameVersionSigs = new bytes[](1); + sameVersionSigs[0] = hostSameVersionSig; + sameVersionChallenge.sigs = sameVersionSigs; + bytes memory sameVersionChallengerSig = signChallenge(chan, sameVersionChallenge, hostSKPrivKey); + + // 3. Try to challenge with the same version - should revert + vm.prank(hostSK); + vm.expectRevert(Custody.InvalidStatus.selector); + custody.challenge(channelId, sameVersionChallenge, new State[](0), sameVersionChallengerSig); + + // 4. Create a new challenge state with a higher version number + State memory higherVersionChallenge = initialState; + higherVersionChallenge.intent = StateIntent.OPERATE; + higherVersionChallenge.data = abi.encode(44); // Different data + higherVersionChallenge.version = 98; // Higher version than the previous challenge (97) + + // Host signs the higher version challenge + bytes memory hostHigherVersionSig = signState(chan, higherVersionChallenge, hostSKPrivKey); + bytes[] memory higherVersionSigs = new bytes[](1); + higherVersionSigs[0] = hostHigherVersionSig; + higherVersionChallenge.sigs = higherVersionSigs; + bytes memory higherVersionChallengerSig = signChallenge(chan, higherVersionChallenge, hostSKPrivKey); + + // 5. Try to challenge with the higher version - must revert + vm.prank(hostSK); + vm.expectRevert(Custody.InvalidStatus.selector); + custody.challenge(channelId, higherVersionChallenge, new State[](0), higherVersionChallengerSig); + } + + function test_revert_whenInvalidState() public { + // 1. Try to challenge with invalid state (adjudicator rejects) + State memory invalidState = initialState; + invalidState.intent = StateIntent.OPERATE; + invalidState.data = abi.encode(42); + invalidState.version = 97; // Version 97 indicates a challenge state (but will be rejected) + adjudicator.setAdjudicateReturnValue(false); // Set adjudicate return value to false for invalid state + + // Host signs the invalid state + bytes memory hostInvalidSig = signState(chan, invalidState, hostSKPrivKey); + bytes[] memory invalidSigs = new bytes[](1); + invalidSigs[0] = hostInvalidSig; + invalidState.sigs = invalidSigs; + + // Attempt to challenge with invalid state + bytes memory hostInvalidChallengerSig = signChallenge(chan, invalidState, hostSKPrivKey); + vm.prank(hostSK); + vm.expectRevert(Custody.InvalidState.selector); + custody.challenge(channelId, invalidState, new State[](0), hostInvalidChallengerSig); + + // 2. Try to challenge non-existent channel + bytes32 nonExistentChannelId = bytes32(uint256(1234)); + adjudicator.setAdjudicateReturnValue(true); // Set flag back to true + + bytes memory hostNonExistingChallengerSig = signChallenge(chan, invalidState, hostSKPrivKey); + + vm.prank(hostSK); + vm.expectRevert(abi.encodeWithSelector(Custody.ChannelNotFound.selector, nonExistentChannelId)); + custody.challenge(nonExistentChannelId, invalidState, new State[](0), hostNonExistingChallengerSig); + } + + function test_revert_whenInvalidChallengerSig() public { + // 1. Create a challenge state + State memory challengeState = initialState; + challengeState.data = abi.encode(42); + + // Host signs the challenge state + bytes memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); + bytes[] memory challengeSigs = new bytes[](1); + challengeSigs[0] = hostChallengeSig; + challengeState.sigs = challengeSigs; + + // 2. Non-participant tries to challenge with a signature from non-participant + bytes memory nonParticipantSig = signChallenge(chan, challengeState, nonParticipantPrivKey); + + vm.prank(nonParticipant); + vm.expectRevert(Custody.InvalidChallengerSignature.selector); + custody.challenge(channelId, challengeState, new State[](0), nonParticipantSig); + } + + function test_immediateClose_whenChallengingInitial() public { + (uint256 hostAvailableBefore, uint256 hostChannelCountBefore) = + getAvailableBalanceAndChannelCount(hostSK, address(token)); + (, uint256 guestChannelCountBefore) = getAvailableBalanceAndChannelCount(guestSK, address(token)); + + // Create a new channel for this test (guest doesn't join) + Channel memory testChan = createTestChannelWithSK(); + testChan.nonce += 1; // Increment nonce to avoid conflicts + State memory testInitialState = createInitialStateWithSK(); + + // Set up signatures + bytes memory hostSig = signState(testChan, testInitialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); + sigs[0] = hostSig; + testInitialState.sigs = sigs; + + // Create channel with host + uint256 hostDepositAmount = DEPOSIT_AMOUNT * 2; + depositTokens(hostSK, hostDepositAmount); + vm.prank(hostSK); + bytes32 testChannelId = custody.create(testChan, testInitialState); + + // Guest does NOT join the channel + // Host challenges with initial state + bytes memory hostChallengerSig = signChallenge(testChan, testInitialState, hostSKPrivKey); + vm.prank(hostSK); + custody.challenge(testChannelId, testInitialState, new State[](0), hostChallengerSig); + + // verify channel is immediately closed and funds distributed + (uint256 hostAvailable, uint256 hostChannelCount) = getAvailableBalanceAndChannelCount(hostSK, address(token)); + (, uint256 guestChannelCount) = getAvailableBalanceAndChannelCount(guestSK, address(token)); + + assertEq(hostChannelCount, hostChannelCountBefore, "Host should have no channels after challenge"); + assertEq(guestChannelCount, guestChannelCountBefore, "Guest should have no channels after challenge"); + + assertEq(hostAvailable, hostAvailableBefore + hostDepositAmount, "Host's available balance incorrect"); + } +} + +contract CustodyTest is CustodyTest_Base { // ==================== TEST CASES ==================== // ==== 1. Channel Creation and Joining ==== @@ -250,8 +523,8 @@ contract CustodyTest is Test { vm.deal(hostSK, 1 ether); // Ensure host has ETH for gas // Sign the state - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory sigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); sigs[0] = hostSig; initialState.sigs = sigs; @@ -279,8 +552,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // 2. Sign the state by the host - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory sigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); sigs[0] = hostSig; initialState.sigs = sigs; @@ -304,8 +577,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // 2. Sign the state by the host - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -315,7 +588,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // 4. Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestSK, DEPOSIT_AMOUNT * 2); vm.prank(guestSK); @@ -348,8 +621,8 @@ contract CustodyTest is Test { }); State memory initialState = createInitialStateWithSK(); - Signature memory hostSig = signState(chanWithInvalidParticipants, initialState, hostSKPrivKey); - Signature[] memory sigs = new Signature[](1); + bytes memory hostSig = signState(chanWithInvalidParticipants, initialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); sigs[0] = hostSig; initialState.sigs = sigs; @@ -395,8 +668,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // Host signs initial state - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -406,7 +679,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestSK, DEPOSIT_AMOUNT); vm.prank(guestSK); custody.join(channelId, 1, guestSig); @@ -419,7 +692,7 @@ contract CustodyTest is Test { hostSig = signState(chan, finalState, hostSKPrivKey); guestSig = signState(chan, finalState, guestSKPrivKey); - Signature[] memory bothSigs = new Signature[](2); + bytes[] memory bothSigs = new bytes[](2); bothSigs[0] = hostSig; bothSigs[1] = guestSig; finalState.sigs = bothSigs; @@ -445,8 +718,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -456,7 +729,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestSK, DEPOSIT_AMOUNT * 2); vm.prank(guestSK); custody.join(channelId, 1, guestSig); @@ -464,7 +737,7 @@ contract CustodyTest is Test { // 2. Try to close with invalid close state (missing CHANCLOSE magic number) State memory invalidState = initialState; // Not a closing state - Signature[] memory bothSigs = new Signature[](2); + bytes[] memory bothSigs = new bytes[](2); bothSigs[0] = hostSig; bothSigs[1] = guestSig; invalidState.sigs = bothSigs; @@ -485,265 +758,7 @@ contract CustodyTest is Test { vm.stopPrank(); } - // ==== 3. Challenge Mechanism ==== - - function test_RejectChallengeDuringChallenge() public { - // 1. Create and fund a channel - Channel memory chan = createTestChannelWithSK(); - State memory initialState = createInitialStateWithSK(); - - // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); - hostSigs[0] = hostSig; - initialState.sigs = hostSigs; - - // Create channel with host - depositTokens(hostSK, DEPOSIT_AMOUNT * 2); - vm.prank(hostSK); - bytes32 channelId = custody.create(chan, initialState); - - // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); - depositTokens(guestSK, DEPOSIT_AMOUNT * 2); - vm.prank(guestSK); - custody.join(channelId, 1, guestSig); - - // 2. Create and submit first challenge state - State memory challengeState = initialState; - challengeState.intent = StateIntent.OPERATE; - challengeState.data = abi.encode(42); - challengeState.version = 97; // Version 97 indicates a challenge state - - // Host signs the challenge state - Signature memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); - Signature[] memory challengeSigs = new Signature[](1); - challengeSigs[0] = hostChallengeSig; - challengeState.sigs = challengeSigs; - Signature memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); - - // Submit first challenge - vm.prank(hostSK); - custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); - - // 3. Create a new challenge state with the same version number - State memory sameVersionChallenge = initialState; - sameVersionChallenge.intent = StateIntent.OPERATE; - sameVersionChallenge.data = abi.encode(43); // Different data but same version - sameVersionChallenge.version = 97; // Same version as previous challenge (97) - - // Host signs the same version challenge - Signature memory hostSameVersionSig = signState(chan, sameVersionChallenge, hostSKPrivKey); - Signature[] memory sameVersionSigs = new Signature[](1); - sameVersionSigs[0] = hostSameVersionSig; - sameVersionChallenge.sigs = sameVersionSigs; - Signature memory sameVersionChallengerSig = signChallenge(chan, sameVersionChallenge, hostSKPrivKey); - - // 4. Try to challenge with the same version - should revert - vm.prank(hostSK); - vm.expectRevert(Custody.InvalidStatus.selector); - custody.challenge(channelId, sameVersionChallenge, new State[](0), sameVersionChallengerSig); - - // 5. Create a new challenge state with a higher version number - State memory higherVersionChallenge = initialState; - higherVersionChallenge.intent = StateIntent.OPERATE; - higherVersionChallenge.data = abi.encode(44); // Different data - higherVersionChallenge.version = 98; // Higher version than the previous challenge (97) - - // Host signs the higher version challenge - Signature memory hostHigherVersionSig = signState(chan, higherVersionChallenge, hostSKPrivKey); - Signature[] memory higherVersionSigs = new Signature[](1); - higherVersionSigs[0] = hostHigherVersionSig; - higherVersionChallenge.sigs = higherVersionSigs; - Signature memory higherVersionChallengerSig = signChallenge(chan, higherVersionChallenge, hostSKPrivKey); - - // 6. Try to challenge with the higher version - must revert - vm.prank(hostSK); - vm.expectRevert(Custody.InvalidStatus.selector); - custody.challenge(channelId, higherVersionChallenge, new State[](0), higherVersionChallengerSig); - } - - function test_Challenge() public { - // 1. Create and fund a channel - Channel memory chan = createTestChannelWithSK(); - State memory initialState = createInitialStateWithSK(); - - // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); - hostSigs[0] = hostSig; - initialState.sigs = hostSigs; - - // Create channel with host - depositTokens(hostSK, DEPOSIT_AMOUNT * 2); - vm.prank(hostSK); - bytes32 channelId = custody.create(chan, initialState); - - // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); - depositTokens(guestSK, DEPOSIT_AMOUNT * 2); - vm.prank(guestSK); - custody.join(channelId, 1, guestSig); - - // 2. Create a challenge state - State memory challengeState = initialState; - challengeState.intent = StateIntent.OPERATE; - challengeState.data = abi.encode(42); - challengeState.version = 97; // Version 97 indicates a challenge state - - // Host signs the challenge state - Signature memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); - Signature[] memory challengeSigs = new Signature[](1); - challengeSigs[0] = hostChallengeSig; - challengeState.sigs = challengeSigs; - - // 3. Host challenges with this state and signs the challenge - Signature memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); - vm.prank(hostSK); - custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); - - // 4. Skip time and close the channel - skipChallengeTime(); - - vm.prank(hostSK); - custody.close(channelId, challengeState, new State[](0)); - - // 5. Verify channel is closed and funds returned - bytes32[] memory hostChannels = getAccountChannels(hostSK); - assertEq(hostChannels.length, 0, "Host should have no channels after challenge resolution"); - - (uint256 hostAvailable,) = getAvailableBalanceAndChannelCount(hostSK, address(token)); - (uint256 guestAvailable,) = getAvailableBalanceAndChannelCount(guestSK, address(token)); - - assertEq(hostAvailable, DEPOSIT_AMOUNT * 2, "Host's available balance incorrect"); - assertEq(guestAvailable, DEPOSIT_AMOUNT * 2, "Guest's available balance incorrect"); - } - - function test_InvalidChallenge() public { - // 1. Create and fund a channel - Channel memory chan = createTestChannelWithSK(); - State memory initialState = createInitialStateWithSK(); - - // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); - hostSigs[0] = hostSig; - initialState.sigs = hostSigs; - - // Create channel with host - depositTokens(hostSK, DEPOSIT_AMOUNT * 2); - vm.prank(hostSK); - bytes32 channelId = custody.create(chan, initialState); - - // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); - depositTokens(guestSK, DEPOSIT_AMOUNT * 2); - vm.prank(guestSK); - custody.join(channelId, 1, guestSig); - - // 2. Try to challenge with invalid state (adjudicator rejects) - State memory invalidState = initialState; - invalidState.intent = StateIntent.OPERATE; - invalidState.data = abi.encode(42); - invalidState.version = 97; // Version 97 indicates a challenge state (but will be rejected) - adjudicator.setAdjudicateReturnValue(false); // Set adjudicate return value to false for invalid state - - // Host signs the invalid state - Signature memory hostInvalidSig = signState(chan, invalidState, hostSKPrivKey); - Signature[] memory invalidSigs = new Signature[](1); - invalidSigs[0] = hostInvalidSig; - invalidState.sigs = invalidSigs; - - // Attempt to challenge with invalid state - Signature memory hostInvalidChallengerSig = signChallenge(chan, invalidState, hostSKPrivKey); - vm.prank(hostSK); - vm.expectRevert(Custody.InvalidState.selector); - custody.challenge(channelId, invalidState, new State[](0), hostInvalidChallengerSig); - - // 3. Try to challenge non-existent channel - bytes32 nonExistentChannelId = bytes32(uint256(1234)); - adjudicator.setAdjudicateReturnValue(true); // Set flag back to true - - Signature memory hostNonExistingChallengerSig = signChallenge(chan, invalidState, hostSKPrivKey); - - vm.prank(hostSK); - vm.expectRevert(abi.encodeWithSelector(Custody.ChannelNotFound.selector, nonExistentChannelId)); - custody.challenge(nonExistentChannelId, invalidState, new State[](0), hostNonExistingChallengerSig); - } - - function test_InvalidChallengerSignature() public { - // 1. Create and fund a channel - Channel memory chan = createTestChannelWithSK(); - State memory initialState = createInitialStateWithSK(); - - // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); - hostSigs[0] = hostSig; - initialState.sigs = hostSigs; - - // Create channel with host - depositTokens(hostSK, DEPOSIT_AMOUNT * 2); - vm.prank(hostSK); - bytes32 channelId = custody.create(chan, initialState); - - // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); - depositTokens(guestSK, DEPOSIT_AMOUNT * 2); - vm.prank(guestSK); - custody.join(channelId, 1, guestSig); - - // 2. Create a challenge state - State memory challengeState = initialState; - challengeState.data = abi.encode(42); - - // Host signs the challenge state - Signature memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); - Signature[] memory challengeSigs = new Signature[](1); - challengeSigs[0] = hostChallengeSig; - challengeState.sigs = challengeSigs; - - // 3. Non-participant tries to challenge with a signature from non-participant - Signature memory nonParticipantSig = signChallenge(chan, challengeState, nonParticipantPrivKey); - - vm.prank(nonParticipant); - vm.expectRevert(Custody.InvalidChallengerSignature.selector); - custody.challenge(channelId, challengeState, new State[](0), nonParticipantSig); - } - - function test_challengeInitial() public { - // 1. Create and fund a channel - Channel memory chan = createTestChannelWithSK(); - State memory initialState = createInitialStateWithSK(); - - // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); - hostSigs[0] = hostSig; - initialState.sigs = hostSigs; - - // Create channel with host - depositTokens(hostSK, DEPOSIT_AMOUNT * 2); - vm.prank(hostSK); - bytes32 channelId = custody.create(chan, initialState); - - // Guest does NOT join the channel - // 2. Host challenges with initial state - Signature memory hostChallengerSig = signChallenge(chan, initialState, hostSKPrivKey); - vm.prank(hostSK); - custody.challenge(channelId, initialState, new State[](0), hostChallengerSig); - - // verify channel is immediately closed and funds distributed - (uint256 hostAvailable, uint256 hostChannelCount) = getAvailableBalanceAndChannelCount(hostSK, address(token)); - (, uint256 guestChannelCount) = getAvailableBalanceAndChannelCount(guestSK, address(token)); - - assertEq(hostChannelCount, 0, "Host should have no channels after challenge"); - assertEq(guestChannelCount, 0, "Guest should have no channels after challenge"); - - assertEq(hostAvailable, DEPOSIT_AMOUNT * 2, "Host's available balance incorrect"); - } - - // ==== 4. Checkpoint Mechanism ==== + // ==== 3. Checkpoint Mechanism ==== function test_RejectEqualVersion() public { // 1. Create and fund a channel @@ -751,8 +766,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -762,7 +777,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestSK, DEPOSIT_AMOUNT * 2); vm.prank(guestSK); custody.join(channelId, 1, guestSig); @@ -774,10 +789,10 @@ contract CustodyTest is Test { checkpointState.version = 55; // Version 55 indicates a checkpoint state // Both sign the checkpoint state - Signature memory hostCheckpointSig = signState(chan, checkpointState, hostSKPrivKey); - Signature memory guestCheckpointSig = signState(chan, checkpointState, guestSKPrivKey); + bytes memory hostCheckpointSig = signState(chan, checkpointState, hostSKPrivKey); + bytes memory guestCheckpointSig = signState(chan, checkpointState, guestSKPrivKey); - Signature[] memory checkpointSigs = new Signature[](2); + bytes[] memory checkpointSigs = new bytes[](2); checkpointSigs[0] = hostCheckpointSig; checkpointSigs[1] = guestCheckpointSig; checkpointState.sigs = checkpointSigs; @@ -793,10 +808,10 @@ contract CustodyTest is Test { sameVersionState.version = 55; // Same version as previous checkpoint (55) // Both sign the new state - Signature memory hostSameVersionSig = signState(chan, sameVersionState, hostSKPrivKey); - Signature memory guestSameVersionSig = signState(chan, sameVersionState, guestSKPrivKey); + bytes memory hostSameVersionSig = signState(chan, sameVersionState, hostSKPrivKey); + bytes memory guestSameVersionSig = signState(chan, sameVersionState, guestSKPrivKey); - Signature[] memory sameVersionSigs = new Signature[](2); + bytes[] memory sameVersionSigs = new bytes[](2); sameVersionSigs[0] = hostSameVersionSig; sameVersionSigs[1] = guestSameVersionSig; sameVersionState.sigs = sameVersionSigs; @@ -814,8 +829,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -825,7 +840,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestSK, DEPOSIT_AMOUNT * 2); vm.prank(guestSK); custody.join(channelId, 1, guestSig); @@ -837,10 +852,10 @@ contract CustodyTest is Test { checkpointState.version = 55; // Version 55 indicates a checkpoint state // Both sign the checkpoint state - Signature memory hostCheckpointSig = signState(chan, checkpointState, hostSKPrivKey); - Signature memory guestCheckpointSig = signState(chan, checkpointState, guestSKPrivKey); + bytes memory hostCheckpointSig = signState(chan, checkpointState, hostSKPrivKey); + bytes memory guestCheckpointSig = signState(chan, checkpointState, guestSKPrivKey); - Signature[] memory checkpointSigs = new Signature[](2); + bytes[] memory checkpointSigs = new bytes[](2); checkpointSigs[0] = hostCheckpointSig; checkpointSigs[1] = guestCheckpointSig; checkpointState.sigs = checkpointSigs; @@ -854,12 +869,12 @@ contract CustodyTest is Test { challengeState.intent = StateIntent.OPERATE; challengeState.data = abi.encode(21); challengeState.version = 97; // Version 97 indicates a challenge state (higher than checkpoint's 55) - Signature memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); - Signature[] memory challengeSigs = new Signature[](1); + bytes memory hostChallengeSig = signState(chan, challengeState, hostSKPrivKey); + bytes[] memory challengeSigs = new bytes[](1); challengeSigs[0] = hostChallengeSig; challengeState.sigs = challengeSigs; - Signature memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); + bytes memory hostChallengerSig = signChallenge(chan, challengeState, hostSKPrivKey); vm.prank(hostSK); custody.challenge(channelId, challengeState, new State[](0), hostChallengerSig); @@ -870,10 +885,10 @@ contract CustodyTest is Test { resolveState.version = 98; // Even higher version to resolve the challenge // Both sign the resolve state - Signature memory hostResolveSignature = signState(chan, resolveState, hostSKPrivKey); - Signature memory guestResolveSignature = signState(chan, resolveState, guestSKPrivKey); + bytes memory hostResolveSignature = signState(chan, resolveState, hostSKPrivKey); + bytes memory guestResolveSignature = signState(chan, resolveState, guestSKPrivKey); - Signature[] memory resolveSignatures = new Signature[](2); + bytes[] memory resolveSignatures = new bytes[](2); resolveSignatures[0] = hostResolveSignature; resolveSignatures[1] = guestResolveSignature; resolveState.sigs = resolveSignatures; @@ -885,9 +900,9 @@ contract CustodyTest is Test { State memory closeState = createClosingStateWithSK(); closeState.version = 100; // Version 100 indicates a closing state // Add signatures - Signature memory hostCloseSig = signState(chan, closeState, hostSKPrivKey); - Signature memory guestCloseSig = signState(chan, closeState, guestSKPrivKey); - Signature[] memory closeSigs = new Signature[](2); + bytes memory hostCloseSig = signState(chan, closeState, hostSKPrivKey); + bytes memory guestCloseSig = signState(chan, closeState, guestSKPrivKey); + bytes[] memory closeSigs = new bytes[](2); closeSigs[0] = hostCloseSig; closeSigs[1] = guestCloseSig; closeState.sigs = closeSigs; @@ -896,7 +911,7 @@ contract CustodyTest is Test { custody.close(channelId, closeState, new State[](0)); } - // ==== 5. Fund Management ==== + // ==== 4. Fund Management ==== function test_DepositAndWithdraw() public { // 1. Test deposit @@ -921,7 +936,7 @@ contract CustodyTest is Test { vm.stopPrank(); } - // ==== 6. Resize Function ==== + // ==== 5. Resize Function ==== function test_Resize() public { // 1. Create and fund a channel @@ -929,8 +944,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithSK(); // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -940,7 +955,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestSK, DEPOSIT_AMOUNT * 2); vm.prank(guestSK); custody.join(channelId, 1, guestSig); @@ -958,13 +973,13 @@ contract CustodyTest is Test { version: 41, // Version 41 indicates state before resize data: abi.encode(41), // Simple application data allocations: initialState.allocations, - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); // If adjudicator allows, then only one can sign the preceding state - Signature memory hostPrecedingSig = signState(chan, precedingState, hostSKPrivKey); + bytes memory hostPrecedingSig = signState(chan, precedingState, hostSKPrivKey); - Signature[] memory precedingSigs = new Signature[](1); + bytes[] memory precedingSigs = new bytes[](1); precedingSigs[0] = hostPrecedingSig; precedingState.sigs = precedingSigs; @@ -981,7 +996,7 @@ contract CustodyTest is Test { version: 42, // Version 42 indicates a resize state data: abi.encode(resizeAmounts), allocations: new Allocation[](2), - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); uint256 resizedHostLockedBalance = uint256(int256(DEPOSIT_AMOUNT) + resizeHostDelta); @@ -992,10 +1007,10 @@ contract CustodyTest is Test { resizedState.allocations[1].amount = resizedGuestLockedBalance; // Guest now has updated amount // Both sign the resized state - Signature memory hostResizeSig = signState(chan, resizedState, hostSKPrivKey); - Signature memory guestResizeSig = signState(chan, resizedState, guestSKPrivKey); + bytes memory hostResizeSig = signState(chan, resizedState, hostSKPrivKey); + bytes memory guestResizeSig = signState(chan, resizedState, guestSKPrivKey); - Signature[] memory resizeSigs = new Signature[](2); + bytes[] memory resizeSigs = new bytes[](2); resizeSigs[0] = hostResizeSig; resizeSigs[1] = guestResizeSig; resizedState.sigs = resizeSigs; @@ -1034,10 +1049,10 @@ contract CustodyTest is Test { afterResizeState.allocations[1].amount = resizedGuestLockedBalance; // Both sign the state after resize - Signature memory hostAfterSig = signState(chan, afterResizeState, hostSKPrivKey); - Signature memory guestAfterSig = signState(chan, afterResizeState, guestSKPrivKey); + bytes memory hostAfterSig = signState(chan, afterResizeState, hostSKPrivKey); + bytes memory guestAfterSig = signState(chan, afterResizeState, guestSKPrivKey); - Signature[] memory afterSigs = new Signature[](2); + bytes[] memory afterSigs = new bytes[](2); afterSigs[0] = hostAfterSig; afterSigs[1] = guestAfterSig; afterResizeState.sigs = afterSigs; @@ -1085,7 +1100,7 @@ contract CustodyTest is Test { ); } - // ==== 7. Implicit Transfer in Resize ==== + // ==== 6. Implicit Transfer in Resize ==== /** * @notice Test case for basic implicit transfer functionality @@ -1097,8 +1112,8 @@ contract CustodyTest is Test { State memory initialState = createInitialStateWithWallets(); // wallets are depositors and destinations in allocations // Set up signatures - Signature memory hostSig = signState(chan, initialState, hostSKPrivKey); - Signature[] memory hostSigs = new Signature[](1); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory hostSigs = new bytes[](1); hostSigs[0] = hostSig; initialState.sigs = hostSigs; @@ -1108,7 +1123,7 @@ contract CustodyTest is Test { bytes32 channelId = custody.create(chan, initialState); // Guest joins the channel - Signature memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); depositTokens(guestWallet, DEPOSIT_AMOUNT * 2); vm.prank(guestWallet); custody.join(channelId, 1, guestSig); @@ -1119,12 +1134,12 @@ contract CustodyTest is Test { version: 41, // Version 41 indicates state before resize data: abi.encode(41), // Simple application data allocations: initialState.allocations, - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); // Host signs the preceding state - Signature memory hostPrecedingSig = signState(chan, precedingState, hostSKPrivKey); - Signature[] memory precedingSigs = new Signature[](1); + bytes memory hostPrecedingSig = signState(chan, precedingState, hostSKPrivKey); + bytes[] memory precedingSigs = new bytes[](1); precedingSigs[0] = hostPrecedingSig; precedingState.sigs = precedingSigs; @@ -1145,7 +1160,7 @@ contract CustodyTest is Test { version: 42, // Version 42 indicates a resize state data: abi.encode(resizeAmounts), allocations: new Allocation[](2), - sigs: new Signature[](0) // Empty initially + sigs: new bytes[](0) // Empty initially }); // Calculate new allocation amounts @@ -1163,10 +1178,10 @@ contract CustodyTest is Test { }); // Both sign the resized state - Signature memory hostResizeSig = signState(chan, resizedState, hostSKPrivKey); - Signature memory guestResizeSig = signState(chan, resizedState, guestSKPrivKey); + bytes memory hostResizeSig = signState(chan, resizedState, hostSKPrivKey); + bytes memory guestResizeSig = signState(chan, resizedState, guestSKPrivKey); - Signature[] memory resizeSigs = new Signature[](2); + bytes[] memory resizeSigs = new bytes[](2); resizeSigs[0] = hostResizeSig; resizeSigs[1] = guestResizeSig; resizedState.sigs = resizeSigs; @@ -1188,7 +1203,7 @@ contract CustodyTest is Test { assertEq(guestAvailableAfterResize, 3 * DEPOSIT_AMOUNT, "Guest should have their 3 * deposit available"); } - // ==== 8. Separate Depositor and Participant Addresses ==== + // ==== 7. Separate Depositor and Participant Addresses ==== function test_SeparateDepositorAndParticipant() public { // 1. Prepare channel with different participant addresses @@ -1198,8 +1213,8 @@ contract CustodyTest is Test { initialState.allocations[0].destination = depositor; // 2. Sign the state by the host participant (not the depositor/creator) - Signature memory hostPartSig = signState(chan, initialState, hostWalletPrivKey); - Signature[] memory sigs = new Signature[](1); + bytes memory hostPartSig = signState(chan, initialState, hostWalletPrivKey); + bytes[] memory sigs = new bytes[](1); sigs[0] = hostPartSig; initialState.sigs = sigs; @@ -1228,7 +1243,7 @@ contract CustodyTest is Test { vm.stopPrank(); // Sign the state by guest participant - Signature memory guestPartSig = signState(chan, initialState, guestWalletPrivKey); + bytes memory guestPartSig = signState(chan, initialState, guestWalletPrivKey); // Guest participant joins with their own signature vm.prank(guestWallet); @@ -1251,10 +1266,10 @@ contract CustodyTest is Test { checkpointState.version = 55; // Version 55 indicates a checkpoint state // Both participants sign the checkpoint state - Signature memory hostPartCheckpointSig = signState(chan, checkpointState, hostWalletPrivKey); - Signature memory guestPartCheckpointSig = signState(chan, checkpointState, guestWalletPrivKey); + bytes memory hostPartCheckpointSig = signState(chan, checkpointState, hostWalletPrivKey); + bytes memory guestPartCheckpointSig = signState(chan, checkpointState, guestWalletPrivKey); - Signature[] memory checkpointSigs = new Signature[](2); + bytes[] memory checkpointSigs = new bytes[](2); checkpointSigs[0] = hostPartCheckpointSig; checkpointSigs[1] = guestPartCheckpointSig; checkpointState.sigs = checkpointSigs; @@ -1269,10 +1284,10 @@ contract CustodyTest is Test { finalState.allocations[0].destination = depositor; // Both participants sign the final state - Signature memory hostPartFinalSig = signState(chan, finalState, hostWalletPrivKey); - Signature memory guestPartFinalSig = signState(chan, finalState, guestWalletPrivKey); + bytes memory hostPartFinalSig = signState(chan, finalState, hostWalletPrivKey); + bytes memory guestPartFinalSig = signState(chan, finalState, guestWalletPrivKey); - Signature[] memory finalSigs = new Signature[](2); + bytes[] memory finalSigs = new bytes[](2); finalSigs[0] = hostPartFinalSig; finalSigs[1] = guestPartFinalSig; finalState.sigs = finalSigs; diff --git a/contract/test/Custody_integration.t.sol b/contract/test/Custody_integration.t.sol new file mode 100644 index 000000000..753e31409 --- /dev/null +++ b/contract/test/Custody_integration.t.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {Vm} from "lib/forge-std/src/Vm.sol"; + +import {TestUtils} from "./TestUtils.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {MockFlagERC1271} from "./mocks/MockFlagERC1271.sol"; + +import {Custody} from "../src/Custody.sol"; +import {SimpleConsensus} from "../src/adjudicators/SimpleConsensus.sol"; +import {Utils} from "../src/Utils.sol"; +import {ChannelStatus, Channel, State, Allocation, StateIntent, STATE_TYPEHASH} from "../src/interfaces/Types.sol"; + +/// @dev used to mock the deployment of ERC-4337 accounts to specific addresses +contract CheatERC6492Factory is Test { + function createAccount(address to, bool flag) external { + deployCodeTo("MockFlagERC1271", abi.encode(flag), to); + } +} + +contract CustodyIntegrationTest_Signatures is Test { + Custody public custody; + SimpleConsensus public adjudicator; + MockERC20 public token; + CheatERC6492Factory public factory; + + // Test participants + address public participant1; + address public participant2; + uint256 public participant1PrivateKey; + uint256 public participant2PrivateKey; + + // Test parameters + uint256 constant DEPOSIT_AMOUNT = 1000; + uint256 constant INITIAL_BALANCE = 10000; + uint64 constant CHALLENGE_DURATION = 3600; // 1 hour + uint64 constant NONCE = 1; + + // Channel and state tracking + Channel public channel; + bytes32 public channelId; + + // Constants for participant ordering + uint256 private constant PARTICIPANT_1 = 0; + uint256 private constant PARTICIPANT_2 = 1; + + function setUp() public { + // Deploy contracts + custody = new Custody(); + adjudicator = new SimpleConsensus(address(this), address(custody)); + token = new MockERC20("Test Token", "TEST", 18); + factory = new CheatERC6492Factory(); + + // Set up participants + participant1PrivateKey = vm.createWallet("participant1").privateKey; + participant2PrivateKey = vm.createWallet("participant2").privateKey; + participant1 = vm.addr(participant1PrivateKey); + participant2 = vm.addr(participant2PrivateKey); + + // Fund participants + token.mint(participant1, INITIAL_BALANCE); + token.mint(participant2, INITIAL_BALANCE); + + // Approve token transfers + vm.prank(participant1); + token.approve(address(custody), INITIAL_BALANCE); + + vm.prank(participant2); + token.approve(address(custody), INITIAL_BALANCE); + + // Create channel + address[] memory participants = new address[](2); + participants[PARTICIPANT_1] = participant1; + participants[PARTICIPANT_2] = participant2; + + channel = Channel({ + participants: participants, + adjudicator: address(adjudicator), + challenge: CHALLENGE_DURATION, + nonce: NONCE + }); + + channelId = Utils.getChannelId(channel); + } + + // ==================== SIGNATURE HELPERS ==================== + + function _signStateRaw(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedState = Utils.getPackedState(channelId, state); + return TestUtils.sign(vm, privateKey, packedState); + } + + function _signStateEIP191(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedState = Utils.getPackedState(channelId, state); + return TestUtils.signEIP191(vm, privateKey, packedState); + } + + function _signStateEIP712(State memory state, uint256 privateKey) internal view returns (bytes memory) { + (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = + custody.eip712Domain(); + bytes32 domainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainId, + verifyingContract + ) + ); + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + state.intent, + state.version, + keccak256(state.data), + keccak256(abi.encode(state.allocations)) + ) + ); + return TestUtils.signEIP712(vm, privateKey, domainSeparator, structHash); + } + + function _signStateEIP6492(address signer, State memory) internal view returns (bytes memory) { + bytes memory signature = "dummy signature"; + bool flag = true; // meaning each EIP-1271 signature is valid + + bytes memory factoryCalldata = abi.encodeWithSelector(CheatERC6492Factory.createAccount.selector, signer, flag); + + bytes memory erc6492Sig = abi.encode(address(factory), factoryCalldata, signature); + return abi.encodePacked(erc6492Sig, Utils.ERC6492_DETECTION_SUFFIX); + } + + function _signChallenge(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedChallengeState = abi.encodePacked(Utils.getPackedState(channelId, state), "challenge"); + return TestUtils.sign(vm, privateKey, packedChallengeState); + } + + // ==================== STATE CREATION HELPERS ==================== + + function _createState(StateIntent intent, uint256 version, bytes memory data, uint256 amount1, uint256 amount2) + internal + view + returns (State memory) + { + Allocation[] memory allocations = new Allocation[](2); + allocations[PARTICIPANT_1] = Allocation({destination: participant1, token: address(token), amount: amount1}); + allocations[PARTICIPANT_2] = Allocation({destination: participant2, token: address(token), amount: amount2}); + + return State({intent: intent, version: version, data: data, allocations: allocations, sigs: new bytes[](0)}); + } + + function _createInitialState() internal view returns (State memory) { + return _createState(StateIntent.INITIALIZE, 0, bytes(""), DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + function _createOperateState(uint256 version, bytes memory data) internal view returns (State memory) { + return _createState(StateIntent.OPERATE, version, data, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + function _createFinalState(uint256 version) internal view returns (State memory) { + return _createState(StateIntent.FINALIZE, version, bytes(""), DEPOSIT_AMOUNT, DEPOSIT_AMOUNT); + } + + // ==================== MAIN INTEGRATION TEST ==================== + + function test_fullChannelLifecycle_withMixedSignatures() public { + // ==================== 1. CREATE CHANNEL ==================== + + // Create initial state - participant1 uses EIP191 + State memory initialState = _createInitialState(); + initialState.sigs = new bytes[](1); + initialState.sigs[0] = _signStateEIP191(initialState, participant1PrivateKey); + + vm.prank(participant1); + custody.depositAndCreate(address(token), DEPOSIT_AMOUNT, channel, initialState); + + (, ChannelStatus status,,,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.INITIAL, "Channel should be in INITIAL status"); + + // ==================== 2. JOIN CHANNEL ==================== + + vm.prank(participant2); + custody.deposit(participant2, address(token), DEPOSIT_AMOUNT); + + // Participant2 joins using raw ECDSA signature + bytes memory participant2JoinSig = _signStateRaw(initialState, participant2PrivateKey); + + vm.prank(participant2); + custody.join(channelId, 1, participant2JoinSig); + + (, status,,,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.ACTIVE, "Channel should be in ACTIVE status"); + + // ==================== 3. CHALLENGE CHANNEL ==================== + + // Create challenge state - participant1 uses EIP712, participant2 uses raw ECDSA + State memory challengeState = _createOperateState(1, bytes("challenge data")); + challengeState.sigs = new bytes[](2); + challengeState.sigs[PARTICIPANT_1] = _signStateEIP712(challengeState, participant1PrivateKey); + challengeState.sigs[PARTICIPANT_2] = _signStateRaw(challengeState, participant2PrivateKey); + + bytes memory challengerSig = _signChallenge(challengeState, participant1PrivateKey); + + vm.prank(participant1); + custody.challenge(channelId, challengeState, new State[](0), challengerSig); + + uint256 challengeExpiry; + (, status,, challengeExpiry,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.DISPUTE, "Channel should be in DISPUTE status"); + assertTrue(challengeExpiry > block.timestamp, "Channel should have challengeExpiry set in future"); + + // ==================== 4. CHECKPOINT TO RESOLVE CHALLENGE ==================== + + // Create checkpoint state with higher version - participant1 uses raw ECDSA, participant2 uses raw ECDSA + State memory checkpointState = _createOperateState(2, bytes("checkpoint data")); + checkpointState.sigs = new bytes[](2); + checkpointState.sigs[PARTICIPANT_1] = _signStateRaw(checkpointState, participant1PrivateKey); + checkpointState.sigs[PARTICIPANT_2] = _signStateRaw(checkpointState, participant2PrivateKey); + + vm.prank(participant2); + custody.checkpoint(channelId, checkpointState, new State[](0)); + + (, status,, challengeExpiry,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.ACTIVE, "Channel should be back to ACTIVE status after checkpoint"); + assertEq(challengeExpiry, 0, "Channel should have no challengeExpiry after checkpoint"); + + // ==================== 5. CHECKPOINT AGAIN ==================== + + // Create checkpoint state with higher version - participant1 uses EIP-6492, participant2 uses raw ECDSA + checkpointState = _createOperateState(3, bytes("checkpoint data")); + checkpointState.sigs = new bytes[](2); + checkpointState.sigs[PARTICIPANT_1] = _signStateEIP6492(participant1, checkpointState); + checkpointState.sigs[PARTICIPANT_2] = _signStateRaw(checkpointState, participant2PrivateKey); + + vm.prank(participant2); + custody.checkpoint(channelId, checkpointState, new State[](0)); + + (, status,, challengeExpiry,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.ACTIVE, "Channel should still be in an ACTIVE status after checkpoint"); + + // ==================== 6. CLOSE CHANNEL ==================== + + // Create final state - participant1 uses EIP1271, participant2 uses raw ECDSA + State memory finalState = _createFinalState(4); + finalState.sigs = new bytes[](2); + // as participant1 already has a contract at its address, we assume this contract expects EIP-191 signature + finalState.sigs[PARTICIPANT_1] = _signStateEIP191(finalState, participant1PrivateKey); + finalState.sigs[PARTICIPANT_2] = _signStateRaw(finalState, participant2PrivateKey); + + vm.prank(participant1); + custody.close(channelId, finalState, new State[](0)); + + (, status,,,) = custody.getChannelData(channelId); + + assertTrue(status == ChannelStatus.VOID, "Channel should have VOID status after close (channel data deleted)"); + + // ==================== 7. VERIFY FINAL BALANCES ==================== + + address[] memory users = new address[](2); + users[0] = participant1; + users[1] = participant2; + + address[] memory tokens = new address[](1); + tokens[0] = address(token); + + uint256[][] memory balances = custody.getAccountsBalances(users, tokens); + + assertEq(balances[0][0], DEPOSIT_AMOUNT, "Participant1 should have deposit amount available"); + assertEq(balances[1][0], DEPOSIT_AMOUNT, "Participant2 should have deposit amount available"); + + bytes32[][] memory channels = custody.getOpenChannels(users); + assertEq(channels[0].length, 0, "Participant1 should have no open channels"); + assertEq(channels[1].length, 0, "Participant2 should have no open channels"); + } + + // ==================== HELPER FUNCTION FOR VERIFICATION ==================== + + function skipChallengeTime() internal { + skip(CHALLENGE_DURATION + 1); + } +} diff --git a/contract/test/TestUtils.sol b/contract/test/TestUtils.sol index 11275129b..733a976c0 100644 --- a/contract/test/TestUtils.sol +++ b/contract/test/TestUtils.sol @@ -1,11 +1,85 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; +import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; + import {Vm} from "lib/forge-std/src/Vm.sol"; +import {MessageHashUtils} from "lib/openzeppelin-contracts/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {State, Channel} from "../src/interfaces/Types.sol"; +import {Utils} from "../src/Utils.sol"; library TestUtils { - function sign(Vm vm, uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s) { - // Sign the digest directly without applying EIP-191 prefix - (v, r, s) = vm.sign(privateKey, digest); + bytes32 public constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + function buildDomainSeparator(string memory name, string memory version, uint256 chainId, address verifyingContract) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encode(TYPE_HASH, keccak256(bytes(name)), keccak256(bytes(version)), chainId, verifyingContract) + ); + } + + function buildDomainSeparatorForContract(EIP712 eip712Contract) internal view returns (bytes32) { + (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = + eip712Contract.eip712Domain(); + return buildDomainSeparator(name, version, chainId, verifyingContract); + } + + function sign(Vm vm, uint256 privateKey, bytes memory message) internal pure returns (bytes memory) { + // Sign the message directly without applying EIP-191 prefix + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, keccak256(message)); + return abi.encodePacked(r, s, v); + } + + function signEIP191(Vm vm, uint256 privateKey, bytes memory message) internal pure returns (bytes memory) { + // Apply EIP-191 prefix and sign + bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(message); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, ethSignedMessageHash); + return abi.encodePacked(r, s, v); + } + + function signEIP712(Vm vm, uint256 privateKey, bytes32 domainSeparator, bytes32 structHash) + internal + pure + returns (bytes memory) + { + // Apply EIP-712 prefix and sign + bytes32 typedDataHash = MessageHashUtils.toTypedDataHash(domainSeparator, structHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, typedDataHash); + return abi.encodePacked(r, s, v); + } + + function signStateEIP191(Vm vm, Channel memory channel, State memory state, uint256 privateKey) + internal + view + returns (bytes memory) + { + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(channel), state); + return TestUtils.signEIP191(vm, privateKey, packedState); + } + + function signStateEIP712( + Vm vm, + bytes32 channelId, + State memory state, + bytes32 stateTypehash, + bytes32 domainSeparator, + uint256 privateKey + ) internal pure returns (bytes memory) { + bytes32 structHash = keccak256( + abi.encode( + stateTypehash, + channelId, + state.intent, + state.version, + keccak256(state.data), + keccak256(abi.encode(state.allocations)) + ) + ); + return TestUtils.signEIP712(vm, privateKey, domainSeparator, structHash); } } diff --git a/contract/test/Utils.t.sol b/contract/test/Utils.t.sol new file mode 100644 index 000000000..9c8a6ec68 --- /dev/null +++ b/contract/test/Utils.t.sol @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "lib/forge-std/src/Test.sol"; + +import {MockEIP712} from "./mocks/MockEIP712.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {MockFlagERC1271} from "./mocks/MockFlagERC1271.sol"; +import {MockERC4337Factory} from "./mocks/MockERC4337Factory.sol"; +import {TestUtils} from "./TestUtils.sol"; +import {UtilsHarness} from "./UtilsHarness.sol"; + +import {Channel, State, Allocation, StateIntent, STATE_TYPEHASH} from "../src/interfaces/Types.sol"; +import {Utils} from "../src/Utils.sol"; + +abstract contract UtilsTest_SignaturesBase is Test { + UtilsHarness public utils; + MockEIP712 public mockEIP712; + MockERC4337Factory public erc4337Factory; + bytes32 public domainSeparator; + + address public signer; + uint256 public signerPrivateKey; + address public wrongSigner; + uint256 public wrongSignerPrivateKey; + + function setUp() public virtual { + utils = new UtilsHarness(); + mockEIP712 = new MockEIP712("TestDomain", "1.0"); + erc4337Factory = new MockERC4337Factory(); + domainSeparator = mockEIP712.domainSeparator(); + + signerPrivateKey = vm.createWallet("signer").privateKey; + wrongSignerPrivateKey = vm.createWallet("wrongSigner").privateKey; + signer = vm.addr(signerPrivateKey); + wrongSigner = vm.addr(wrongSignerPrivateKey); + } + + function getERC6492SignatureAndSigner(bool flag, bytes32 salt, bytes memory originalSig) + internal + view + virtual + returns (bytes memory, address) + { + address expectedSigner = erc4337Factory.getAddress(flag, salt); + bytes memory factoryCalldata = abi.encodeWithSelector(MockERC4337Factory.createAccount.selector, flag, salt); + + bytes memory erc6492Sig = abi.encode(address(erc4337Factory), factoryCalldata, originalSig); + bytes memory sigWithSuffix = abi.encodePacked(erc6492Sig, Utils.ERC6492_DETECTION_SUFFIX); + + return (sigWithSuffix, expectedSigner); + } +} + +contract UtilsTest_Signatures is UtilsTest_SignaturesBase { + bytes message = "test message"; + bytes32 messageHash = keccak256(message); + + // ==================== recoverRawECDSASigner TESTS ==================== + + function test_recoverRawECDSASigner_returnsCorrectSigner() public view { + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, message); + + address recoveredSigner = utils.recoverRawECDSASigner(message, sig); + + assertEq(recoveredSigner, signer, "Should recover correct signer"); + } + + function test_recoverRawECDSASigner_returnsWrongSignerForDifferentMessage() public view { + bytes memory differentMessage = "different message"; + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, message); + + address recoveredSigner = utils.recoverRawECDSASigner(differentMessage, sig); + + assertNotEq(recoveredSigner, signer, "Should not recover correct signer for different message"); + } + + // ==================== recoverEIP191Signer TESTS ==================== + + function test_recoverEIP191Signer_returnsCorrectSigner() public view { + bytes memory sig = TestUtils.signEIP191(vm, signerPrivateKey, message); + + address recoveredSigner = utils.recoverEIP191Signer(message, sig); + + assertEq(recoveredSigner, signer, "Should recover correct signer with EIP191"); + } + + function test_recoverEIP191Signer_returnsWrongSigner_forDifferentMessage() public view { + bytes memory differentMessage = "different message"; + bytes memory sig = TestUtils.signEIP191(vm, signerPrivateKey, message); + + address recoveredSigner = utils.recoverEIP191Signer(differentMessage, sig); + + assertNotEq(recoveredSigner, signer, "Should not recover correct signer for different message"); + } + + function test_recoverEIP191Signer_returnsWrongSigner_forRawSignature() public view { + // Sign with raw ECDSA instead of EIP191 + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, message); + + address recoveredSigner = utils.recoverEIP191Signer(message, sig); + + assertNotEq(recoveredSigner, signer, "Should not recover correct signer when using raw signature for EIP191"); + } + + // ==================== recoverEIP712Signer TESTS ==================== + + function test_recoverEIP712Signer_returnsCorrectSigner() public view { + bytes32 structHash = keccak256("test struct"); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + address recoveredSigner = utils.recoverEIP712Signer(domainSeparator, structHash, sig); + + assertEq(recoveredSigner, signer, "Should recover correct signer with EIP712"); + } + + function test_recoverEIP712Signer_returnsWrongSignerForDifferentDomain() public view { + bytes32 differentDomainSeparator = keccak256("different domain"); + bytes32 structHash = keccak256("test struct"); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + address recoveredSigner = utils.recoverEIP712Signer(differentDomainSeparator, structHash, sig); + + assertNotEq(recoveredSigner, signer, "Should not recover correct signer for different domain"); + } + + // ==================== isValidERC1271Signature TESTS ==================== + + function test_isValidERC1271Signature_returnsTrue_forValidSignature() public { + MockFlagERC1271 mockContract = new MockFlagERC1271(true); + bytes memory sig = "dummy signature"; + + bool isValid = utils.isValidERC1271Signature(messageHash, sig, address(mockContract)); + + assertTrue(isValid, "Should return true when flag is true"); + } + + function test_isValidERC1271Signature_returnsFalse_forInvalidSignature() public { + MockFlagERC1271 mockContract = new MockFlagERC1271(false); + bytes memory sig = "dummy signature"; + + bool isValid = utils.isValidERC1271Signature(messageHash, sig, address(mockContract)); + + assertFalse(isValid, "Should return false when flag is false"); + } + + // ==================== isValidERC6492Signature TESTS ==================== + + function test_isValidERC6492Signature_returnsTrue_forValidSignature_notDeployed() public { + (bytes memory signature, address expectedSigner) = + getERC6492SignatureAndSigner(true, keccak256("test salt"), "dummy signature"); + + bool isValid = utils.isValidERC6492Signature(messageHash, signature, expectedSigner); + + assertTrue(isValid, "Should return true for valid signature with undeployed contract"); + } + + function test_isValidERC6492Signature_returnsTrue_forValidSignature_deployed() public { + bytes32 salt = keccak256("test salt"); + bool flag = true; + + address expectedSigner = erc4337Factory.createAccount(flag, salt); + (bytes memory signature,) = getERC6492SignatureAndSigner(flag, salt, "dummy signature"); + + bool isValid = utils.isValidERC6492Signature(messageHash, signature, expectedSigner); + + assertTrue(isValid, "Should return true for valid signature with deployed contract"); + } + + function test_isValidERC6492Signature_returnsFalse_forInvalidSignature_notDeployed() public { + (bytes memory signature, address expectedSigner) = + getERC6492SignatureAndSigner(false, keccak256("test salt"), "dummy signature"); + + bool isValid = utils.isValidERC6492Signature(messageHash, signature, expectedSigner); + + assertFalse(isValid, "Should return false for invalid signature with undeployed contract"); + } + + function test_isValidERC6492Signature_reverts_forValidSignature_errorOnDeployment() public { + address expectedSigner = address(0x12345678); // Simulate a contract that will not deploy correctly + bytes memory originalSig = "dummy signature"; + + bytes memory factoryCalldata = + abi.encodeWithSelector(MockERC4337Factory.createAccount.selector, "corrupted data", keccak256("test salt")); + + bytes memory erc6492Sig = abi.encode(address(erc4337Factory), factoryCalldata, originalSig); + bytes memory sigWithSuffix = abi.encodePacked(erc6492Sig, Utils.ERC6492_DETECTION_SUFFIX); + + vm.expectRevert(abi.encodeWithSelector(Utils.ERC6492DeploymentFailed.selector, erc4337Factory, factoryCalldata)); + utils.isValidERC6492Signature(messageHash, sigWithSuffix, expectedSigner); + } + + function test_isValidERC6492Signature_reverts_forWrongExpectedSigner_thatIsNotContract() public { + address wrongExpectedSigner = address(0x33231234); // Wrong address + + (bytes memory signature,) = getERC6492SignatureAndSigner(true, keccak256("test salt"), "dummy signature"); + + vm.expectRevert(abi.encodeWithSelector(Utils.ERC6492NoCode.selector, wrongExpectedSigner)); + utils.isValidERC6492Signature(messageHash, signature, wrongExpectedSigner); + } + + function test_isValidERC6492Signature_returnsFalse_forWrongExpectedSigner_thatIsContract() public { + address wrongExpectedSigner = address(new MockFlagERC1271(false)); // Different deployed contract + + (bytes memory signature,) = getERC6492SignatureAndSigner(true, keccak256("test salt"), "dummy signature"); + + bool isValid = utils.isValidERC6492Signature(messageHash, signature, wrongExpectedSigner); + + assertFalse(isValid, "Should return false when expectedSigner is a different deployed contract"); + } +} + +contract UtilsTest_StateSignatures is UtilsTest_SignaturesBase { + MockERC20 public token; + MockERC4337Factory public factory; + + Channel public channel; + State public testState; + + bytes32 public channelId; + + function setUp() public override { + super.setUp(); + + token = new MockERC20("Test Token", "TEST", 18); + factory = new MockERC4337Factory(); + + // Create test channel + address[] memory participants = new address[](2); + participants[0] = signer; + participants[1] = wrongSigner; + + channel = Channel({participants: participants, adjudicator: address(0x123), challenge: 3600, nonce: 1}); + + // Create test state + Allocation[] memory allocations = new Allocation[](2); + allocations[0] = Allocation({destination: signer, token: address(token), amount: 100}); + allocations[1] = Allocation({destination: wrongSigner, token: address(token), amount: 200}); + + testState = State({ + intent: StateIntent.INITIALIZE, + version: 0, + data: bytes("test data"), + allocations: allocations, + sigs: new bytes[](0) + }); + + channelId = utils.getChannelId(channel); + } + + // ==================== recoverStateEIP712Signer TESTS ==================== + + function test_recoverStateEIP712Signer_returnsCorrectSigner() public view { + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + address recoveredSigner = + utils.recoverStateEIP712Signer(domainSeparator, STATE_TYPEHASH, channelId, testState, sig); + + assertEq(recoveredSigner, signer, "Should recover correct signer for state EIP712"); + } + + function test_recoverStateEIP712Signer_returnsWrongSignerForDifferentState() public view { + // Sign original state + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + // Create different state + State memory differentState = testState; + differentState.data = bytes("different data"); + + address recoveredSigner = + utils.recoverStateEIP712Signer(domainSeparator, STATE_TYPEHASH, channelId, differentState, sig); + + assertNotEq(recoveredSigner, signer, "Should not recover correct signer for different state"); + } + + // ==================== verifyStateEOASignature TESTS ==================== + + function test_verifyStateEOASignature_returnsTrue_forRawECDSASignature() public view { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify raw ECDSA signature"); + } + + function test_verifyStateEOASignature_returnsTrue_forEIP191Signature() public view { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.signEIP191(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify EIP191 signature"); + } + + function test_verifyStateEOASignature_returnsTrue_forEIP712Signature() public view { + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify EIP712 signature"); + } + + function test_verifyStateEOASignature_returnsFalse_forWrongSigner_rawECDSA() public view { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, wrongSigner); + + assertFalse(isValid, "Should not verify signature for wrong signer"); + } + + function test_verifyStateEOASignature_returnsFalse_forWrongSigner_EIP191() public view { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.signEIP191(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, wrongSigner); + + assertFalse(isValid, "Should not verify EIP191 signature for wrong signer"); + } + + function test_verifyStateEOASignature_returnsFalse_forWrongSigner_EIP712() public view { + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, wrongSigner); + + assertFalse(isValid, "Should not verify EIP712 signature for wrong signer"); + } + + function test_verifyStateEOASignature_returnsFalse_whenNoEIP712Support() public view { + bytes32 domainSeparator = Utils.NO_EIP712_SUPPORT; + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, signer); + + assertFalse(isValid, "Should not verify EIP712 signature when NO_EIP712_SUPPORT is set"); + } + + function test_verifyStateEOASignature_returnsTrue_forRawECDSAWhenNoEIP712Support() public view { + bytes32 domainSeparator = Utils.NO_EIP712_SUPPORT; + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify raw ECDSA signature even when NO_EIP712_SUPPORT is set"); + } + + function test_verifyStateEOASignature_returnsFalse_forEIP712WhenNoEIP712Support() public view { + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + bool isValid = utils.verifyStateEOASignature(testState, channelId, Utils.NO_EIP712_SUPPORT, sig, signer); + + assertFalse(isValid, "Should not verify EIP712 signature when NO_EIP712_SUPPORT is set"); + } + + // ==================== verifyStateSignature TESTS ==================== + + // EOA Signature Tests + function test_verifyStateSignature_returnsTrue_forEOA_rawECDSASignature() public { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify raw ECDSA signature for EOA"); + } + + function test_verifyStateSignature_returnsTrue_forEOA_EIP191Signature() public { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.signEIP191(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify EIP191 signature for EOA"); + } + + function test_verifyStateSignature_returnsTrue_forEOA_EIP712Signature() public { + bytes32 structHash = keccak256( + abi.encode( + STATE_TYPEHASH, + channelId, + testState.intent, + testState.version, + keccak256(testState.data), + keccak256(abi.encode(testState.allocations)) + ) + ); + bytes memory sig = TestUtils.signEIP712(vm, signerPrivateKey, domainSeparator, structHash); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, sig, signer); + + assertTrue(isValid, "Should verify EIP712 signature for EOA"); + } + + function test_verifyStateSignature_returnsFalse_forEOA_wrongSigner() public { + bytes memory packedState = utils.getPackedState(channelId, testState); + bytes memory sig = TestUtils.sign(vm, signerPrivateKey, packedState); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, sig, wrongSigner); + + assertFalse(isValid, "Should not verify signature for wrong EOA signer"); + } + + // ERC-1271 Contract Signature Tests + function test_verifyStateSignature_returnsTrue_forERC1271Contract_validSignature() public { + MockFlagERC1271 mockContract = new MockFlagERC1271(true); + bytes memory sig = "dummy signature"; + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, sig, address(mockContract)); + + assertTrue(isValid, "Should verify valid ERC1271 contract signature"); + } + + function test_verifyStateSignature_returnsFalse_forERC1271Contract_invalidSignature() public { + MockFlagERC1271 mockContract = new MockFlagERC1271(false); + bytes memory sig = "dummy signature"; + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, sig, address(mockContract)); + + assertFalse(isValid, "Should not verify invalid ERC1271 contract signature"); + } + + // ERC-6492 Signature Tests + function test_verifyStateSignature_returnsTrue_forERC6492_validSignature_notDeployed() public { + (bytes memory signature, address expectedSigner) = + getERC6492SignatureAndSigner(true, keccak256("test salt"), "dummy signature"); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, signature, expectedSigner); + + assertTrue(isValid, "Should verify valid ERC6492 signature for undeployed contract"); + } + + function test_verifyStateSignature_returnsTrue_forERC6492_validSignature_deployed() public { + bytes32 salt = keccak256("test salt"); + bool flag = true; + + address expectedSigner = factory.createAccount(flag, salt); + (bytes memory signature,) = getERC6492SignatureAndSigner(flag, salt, "dummy signature"); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, signature, expectedSigner); + + assertTrue(isValid, "Should verify valid ERC6492 signature for deployed contract"); + } + + function test_verifyStateSignature_returnsFalse_forERC6492_invalidSignature_notDeployed() public { + (bytes memory signature, address expectedSigner) = + getERC6492SignatureAndSigner(false, keccak256("test salt"), "dummy signature"); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, signature, expectedSigner); + + assertFalse(isValid, "Should not verify invalid ERC6492 signature for undeployed contract"); + } + + function test_verifyStateSignature_returnsFalse_forERC6492_invalidSignature_deployed() public { + bytes32 salt = keccak256("test salt"); + bool flag = false; + + address expectedSigner = factory.createAccount(flag, salt); + (bytes memory signature,) = getERC6492SignatureAndSigner(flag, salt, "dummy signature"); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, signature, expectedSigner); + + assertFalse(isValid, "Should not verify invalid ERC6492 signature for deployed contract"); + } + + function test_verifyStateSignature_returnsFalse_forERC6492_wrongExpectedSigner() public { + address wrongExpectedSigner = address(new MockFlagERC1271(false)); + + (bytes memory signature,) = getERC6492SignatureAndSigner(true, keccak256("test salt"), "dummy signature"); + + bool isValid = utils.verifyStateSignature(testState, channelId, domainSeparator, signature, wrongExpectedSigner); + + assertFalse(isValid, "Should not verify ERC6492 signature for wrong expected signer"); + } +} diff --git a/contract/test/UtilsHarness.sol b/contract/test/UtilsHarness.sol new file mode 100644 index 000000000..615ccdb8d --- /dev/null +++ b/contract/test/UtilsHarness.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Utils} from "../src/Utils.sol"; +import {Channel, State, StateIntent} from "../src/interfaces/Types.sol"; + +contract UtilsHarness { + using Utils for *; + + function getChannelId(Channel memory ch) external view returns (bytes32) { + return Utils.getChannelId(ch); + } + + function getPackedState(bytes32 channelId, State memory state) external pure returns (bytes memory) { + return Utils.getPackedState(channelId, state); + } + + function recoverRawECDSASigner(bytes memory message, bytes memory sig) external pure returns (address) { + return Utils.recoverRawECDSASigner(message, sig); + } + + function recoverEIP191Signer(bytes memory message, bytes memory sig) external pure returns (address) { + return Utils.recoverEIP191Signer(message, sig); + } + + function recoverEIP712Signer(bytes32 domainSeparator, bytes32 structHash, bytes memory sig) + external + pure + returns (address) + { + return Utils.recoverEIP712Signer(domainSeparator, structHash, sig); + } + + function recoverStateEIP712Signer( + bytes32 domainSeparator, + bytes32 typeHash, + bytes32 channelId, + State memory state, + bytes memory sig + ) external pure returns (address) { + return Utils.recoverStateEIP712Signer(domainSeparator, typeHash, channelId, state, sig); + } + + function verifyStateEOASignature( + State memory state, + bytes32 channelId, + bytes32 domainSeparator, + bytes memory sig, + address signer + ) external pure returns (bool) { + return Utils.verifyStateEOASignature(state, channelId, domainSeparator, sig, signer); + } + + function isValidERC1271Signature(bytes32 msgHash, bytes memory sig, address expectedSigner) + external + view + returns (bool) + { + return Utils.isValidERC1271Signature(msgHash, sig, expectedSigner); + } + + function isValidERC6492Signature(bytes32 msgHash, bytes memory sig, address expectedSigner) + external + returns (bool) + { + return Utils.isValidERC6492Signature(msgHash, sig, expectedSigner); + } + + function verifyStateSignature( + State memory state, + bytes32 channelId, + bytes32 domainSeparator, + bytes memory sig, + address signer + ) external returns (bool) { + return Utils.verifyStateSignature(state, channelId, domainSeparator, sig, signer); + } + + function validateInitialState(State memory state, Channel memory chan, bytes32 domainSeparator) + external + returns (bool) + { + return Utils.validateInitialState(state, chan, domainSeparator); + } + + function validateUnanimousStateSignatures(State memory state, Channel memory chan, bytes32 domainSeparator) + external + returns (bool) + { + return Utils.validateUnanimousStateSignatures(state, chan, domainSeparator); + } + + function statesAreEqual(State memory a, State memory b) external pure returns (bool) { + return Utils.statesAreEqual(a, b); + } + + function validateTransitionTo(State memory previous, State memory candidate) external pure returns (bool) { + return Utils.validateTransitionTo(previous, candidate); + } + + function trailingBytes32(bytes memory data) external pure returns (bytes32) { + return Utils.trailingBytes32(data); + } +} diff --git a/contract/test/adjudicators/ConsensusTransition.t.sol b/contract/test/adjudicators/ConsensusTransition.t.sol index 658dd88e6..8e905e555 100644 --- a/contract/test/adjudicators/ConsensusTransition.t.sol +++ b/contract/test/adjudicators/ConsensusTransition.t.sol @@ -10,7 +10,7 @@ import {TestUtils} from "../TestUtils.sol"; import {MockERC20} from "../mocks/MockERC20.sol"; import {IAdjudicator} from "../../src/interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, Signature, StateIntent} from "../../src/interfaces/Types.sol"; +import {Channel, State, Allocation, StateIntent} from "../../src/interfaces/Types.sol"; import {ConsensusTransition} from "../../src/adjudicators/ConsensusTransition.sol"; import {Utils} from "../../src/Utils.sol"; @@ -19,6 +19,10 @@ contract ConsensusTransitionTest is Test { ConsensusTransition public adjudicator; + // Mockup constructor parameters + address mockedOwner = address(0x456); + address mockedChannelImpl = address(0x123); + // Test accounts address public host; address public guest; @@ -35,7 +39,7 @@ contract ConsensusTransitionTest is Test { function setUp() public { // Deploy the adjudicator contract - adjudicator = new ConsensusTransition(); + adjudicator = new ConsensusTransition(mockedOwner, mockedChannelImpl); // Generate private keys and addresses for the participants hostPrivateKey = 0x1; @@ -58,29 +62,6 @@ contract ConsensusTransitionTest is Test { }); } - // Test case: Basic signing test to verify our signature approach - // using foundry's vm.sign and correctly formatted message for ecrecover - function test_BasicSignatureVerification() public view { - // Simple message to sign - bytes32 message = keccak256("test message"); - - // Sign the message directly without EIP-191 prefix - (uint8 v, bytes32 r, bytes32 s) = TestUtils.sign(vm, hostPrivateKey, message); - - // Recover the signer using direct recovery (no prefix) - address recovered = ECDSA.recover(message, v, r, s); - - // This should pass - verifying our signing approach - assertEq(recovered, host); - - // Now test with Utils.verifySignature - Signature memory signature = Signature({v: v, r: r, s: s}); - bool isValid = Utils.verifySignature(message, signature, host); - - // This should also pass with our updated Utils.verifySignature - assertTrue(isValid); - } - // Helper function to create test allocations function createAllocations(uint256 hostAmount, uint256 guestAmount) internal view returns (Allocation[2] memory) { Allocation[2] memory allocations; @@ -105,7 +86,7 @@ contract ConsensusTransitionTest is Test { state.allocations = new Allocation[](2); state.allocations[HOST] = allocations[HOST]; state.allocations[GUEST] = allocations[GUEST]; - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } @@ -123,7 +104,7 @@ contract ConsensusTransitionTest is Test { state.allocations = new Allocation[](2); state.allocations[HOST] = allocations[HOST]; state.allocations[GUEST] = allocations[GUEST]; - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } @@ -145,30 +126,29 @@ contract ConsensusTransitionTest is Test { state.allocations = new Allocation[](2); state.allocations[HOST] = allocations[HOST]; state.allocations[GUEST] = allocations[GUEST]; - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } // Helper to sign a state - function _signState(State memory state, uint256 privateKey) internal view returns (Signature memory) { - bytes32 stateHash = Utils.getStateHash(channel, state); - (uint8 v, bytes32 r, bytes32 s) = TestUtils.sign(vm, privateKey, stateHash); - return Signature({v: v, r: r, s: s}); + function _signState(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(channel), state); + return TestUtils.sign(vm, privateKey, packedState); } // -------------------- FIRST STATE TRANSITION TESTS -------------------- - function test_adjudicate_firstState_valid() public view { + function test_adjudicate_firstState_valid() public { // Create initial state with both signatures State memory initialState = _createInitialState("initial state"); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first operate state with both signatures State memory firstState = _createOperateState("first state", 1); - firstState.sigs = new Signature[](2); + firstState.sigs = new bytes[](2); firstState.sigs[HOST] = _signState(firstState, hostPrivateKey); firstState.sigs[GUEST] = _signState(firstState, guestPrivateKey); @@ -181,16 +161,16 @@ contract ConsensusTransitionTest is Test { assertTrue(valid, "Valid first state transition should be accepted"); } - function test_adjudicate_firstState_revert_whenMissingParticipantSignature() public view { + function test_adjudicate_firstState_revert_whenMissingParticipantSignature() public { // Create initial state with both signatures State memory initialState = _createInitialState("initial state"); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first operate state with only one signature State memory firstState = _createOperateState("first state", 1); - firstState.sigs = new Signature[](1); + firstState.sigs = new bytes[](1); firstState.sigs[0] = _signState(firstState, hostPrivateKey); // Provide the initial state as proof @@ -202,16 +182,16 @@ contract ConsensusTransitionTest is Test { assertFalse(valid, "First state without both signatures should be rejected"); } - function test_adjudicate_firstState_revert_whenIncorrectVersion() public view { + function test_adjudicate_firstState_revert_whenIncorrectVersion() public { // Create initial state with both signatures State memory initialState = _createInitialState("initial state"); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first operate state with incorrect version (2 instead of 1) State memory firstState = _createOperateState("first state", 2); - firstState.sigs = new Signature[](2); + firstState.sigs = new bytes[](2); firstState.sigs[HOST] = _signState(firstState, hostPrivateKey); firstState.sigs[GUEST] = _signState(firstState, guestPrivateKey); @@ -226,16 +206,16 @@ contract ConsensusTransitionTest is Test { // -------------------- LATER STATE TRANSITION TESTS -------------------- - function test_adjudicate_laterState_valid() public view { + function test_adjudicate_laterState_valid() public { // Create state 1 with both signatures State memory state1 = _createOperateState("state 1", 1); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); // Create state 2 with both signatures State memory state2 = _createOperateState("state 2", 2); - state2.sigs = new Signature[](2); + state2.sigs = new bytes[](2); state2.sigs[HOST] = _signState(state2, hostPrivateKey); state2.sigs[GUEST] = _signState(state2, guestPrivateKey); @@ -248,16 +228,16 @@ contract ConsensusTransitionTest is Test { assertTrue(valid, "Valid state transition from 1 to 2 should be accepted"); } - function test_adjudicate_laterState_revert_whenIncorrectVersionIncrement() public view { + function test_adjudicate_laterState_revert_whenIncorrectVersionIncrement() public { // Create state 1 with both signatures State memory state1 = _createOperateState("state 1", 1); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); // Create state 3 with both signatures (skipping version 2) State memory state3 = _createOperateState("state 3", 3); - state3.sigs = new Signature[](2); + state3.sigs = new bytes[](2); state3.sigs[HOST] = _signState(state3, hostPrivateKey); state3.sigs[GUEST] = _signState(state3, guestPrivateKey); @@ -270,10 +250,10 @@ contract ConsensusTransitionTest is Test { assertFalse(valid, "State with non-sequential version should be rejected"); } - function test_adjudicate_laterState_revert_whenAllocationSumChanged() public view { + function test_adjudicate_laterState_revert_whenAllocationSumChanged() public { // Create state 1 with both signatures State memory state1 = _createOperateState("state 1", 1); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); @@ -281,7 +261,7 @@ contract ConsensusTransitionTest is Test { State memory state2 = _createOperateState("state 2", 2); state2.allocations[HOST].amount = 60; // Changed from 50 state2.allocations[GUEST].amount = 50; // Kept at 50, total sum is now 110 instead of 100 - state2.sigs = new Signature[](2); + state2.sigs = new bytes[](2); state2.sigs[HOST] = _signState(state2, hostPrivateKey); state2.sigs[GUEST] = _signState(state2, guestPrivateKey); @@ -294,10 +274,10 @@ contract ConsensusTransitionTest is Test { assertFalse(valid, "State with changed allocation sum should be rejected"); } - function test_adjudicate_revert_whenNoStateProof() public view { + function test_adjudicate_revert_whenNoStateProof() public { // Create state 2 without providing a proof State memory state2 = _createOperateState("state 2", 2); - state2.sigs = new Signature[](2); + state2.sigs = new bytes[](2); state2.sigs[HOST] = _signState(state2, hostPrivateKey); state2.sigs[GUEST] = _signState(state2, guestPrivateKey); @@ -309,16 +289,16 @@ contract ConsensusTransitionTest is Test { assertFalse(valid, "State without proof should be rejected"); } - function test_adjudicate_revert_whenTooManyProofs() public view { + function test_adjudicate_revert_whenTooManyProofs() public { // Create state 1 State memory state1 = _createOperateState("state 1", 1); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); // Create state 2 State memory state2 = _createOperateState("state 2", 2); - state2.sigs = new Signature[](2); + state2.sigs = new bytes[](2); state2.sigs[HOST] = _signState(state2, hostPrivateKey); state2.sigs[GUEST] = _signState(state2, guestPrivateKey); @@ -334,10 +314,10 @@ contract ConsensusTransitionTest is Test { // -------------------- RESIZE STATE TRANSITION TESTS -------------------- - function test_adjudicate_afterResize_valid() public view { + function test_adjudicate_afterResize_valid() public { // Create state 1 with both signatures State memory state1 = _createOperateState("state 1", 1); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); @@ -346,13 +326,13 @@ contract ConsensusTransitionTest is Test { resizeAmounts[HOST] = 10; resizeAmounts[GUEST] = -10; State memory resizeState = _createResizeState("resize state", 2, resizeAmounts); - resizeState.sigs = new Signature[](2); + resizeState.sigs = new bytes[](2); resizeState.sigs[HOST] = _signState(resizeState, hostPrivateKey); resizeState.sigs[GUEST] = _signState(resizeState, guestPrivateKey); // Create state 3 after resize State memory state3 = _createOperateState("state 3", 3); - state3.sigs = new Signature[](2); + state3.sigs = new bytes[](2); state3.sigs[HOST] = _signState(state3, hostPrivateKey); state3.sigs[GUEST] = _signState(state3, guestPrivateKey); @@ -366,10 +346,10 @@ contract ConsensusTransitionTest is Test { } // Test signature validation using a non-corrupt signature but wrong signer - function test_WrongSignerRejected() public view { + function test_WrongSignerRejected() public { // Create state with signatures from wrong participants State memory state = _createOperateState("state", 1); - state.sigs = new Signature[](2); + state.sigs = new bytes[](2); // Use guest's signature for both slots state.sigs[HOST] = _signState(state, guestPrivateKey); // Should be host, but using guest @@ -377,7 +357,7 @@ contract ConsensusTransitionTest is Test { // Create a valid initial state as proof State memory initialState = _createInitialState("initial state"); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); diff --git a/contract/test/adjudicators/Counter.t.sol b/contract/test/adjudicators/Counter.t.sol index 3b4b369d6..ffc0bfb34 100644 --- a/contract/test/adjudicators/Counter.t.sol +++ b/contract/test/adjudicators/Counter.t.sol @@ -10,7 +10,7 @@ import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECD import {TestUtils} from "../TestUtils.sol"; import {IAdjudicator} from "../../src/interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, Signature, StateIntent} from "../../src/interfaces/Types.sol"; +import {Channel, State, Allocation, StateIntent} from "../../src/interfaces/Types.sol"; import {Counter, Counter as CounterContract} from "../../src/adjudicators/Counter.sol"; import {Utils} from "../../src/Utils.sol"; @@ -65,7 +65,7 @@ contract CounterTest is Test { state.allocations[HOST] = Allocation({destination: address(0), token: address(0), amount: 100}); state.allocations[GUEST] = Allocation({destination: address(0), token: address(0), amount: 100}); - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } @@ -82,7 +82,7 @@ contract CounterTest is Test { state.allocations[HOST] = Allocation({destination: address(0), token: address(0), amount: 100}); state.allocations[GUEST] = Allocation({destination: address(0), token: address(0), amount: 100}); - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } @@ -103,28 +103,27 @@ contract CounterTest is Test { state.allocations[HOST] = Allocation({destination: address(0), token: address(0), amount: 100}); state.allocations[GUEST] = Allocation({destination: address(0), token: address(0), amount: 100}); - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } - function _signState(State memory state, uint256 privateKey) internal view returns (Signature memory) { - bytes32 stateHash = Utils.getStateHash(channel, state); - (uint8 v, bytes32 r, bytes32 s) = TestUtils.sign(vm, privateKey, stateHash); - return Signature({v: v, r: r, s: s}); + function _signState(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(channel), state); + return TestUtils.sign(vm, privateKey, packedState); } // -------------------- FIRST STATE TRANSITION TESTS -------------------- - function test_adjudicate_firstState_valid() public view { + function test_adjudicate_firstState_valid() public { // Create initial state with target 10 State memory initialState = _createInitialState(10); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first state (version 1) State memory firstState = _createCounterState(10, 1); - firstState.sigs = new Signature[](1); + firstState.sigs = new bytes[](1); firstState.sigs[0] = _signState(firstState, hostPrivateKey); // Host signs state 1 // Provide the initial state as proof @@ -136,16 +135,16 @@ contract CounterTest is Test { assertTrue(valid, "Valid first state transition should be accepted"); } - function test_adjudicate_firstState_revert_whenTargetExceeded() public view { + function test_adjudicate_firstState_revert_whenTargetExceeded() public { // Create initial state with target 10 State memory initialState = _createInitialState(10); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first state with version exceeding target State memory firstState = _createCounterState(10, 11); // version > target - firstState.sigs = new Signature[](1); + firstState.sigs = new bytes[](1); firstState.sigs[0] = _signState(firstState, hostPrivateKey); // Provide the initial state as proof @@ -157,16 +156,16 @@ contract CounterTest is Test { assertFalse(valid, "State with version exceeding target should be rejected"); } - function test_adjudicate_firstState_revert_whenIncorrectSigner() public view { + function test_adjudicate_firstState_revert_whenIncorrectSigner() public { // Create initial state with target 10 State memory initialState = _createInitialState(10); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first state signed by guest (should be host) State memory firstState = _createCounterState(10, 1); - firstState.sigs = new Signature[](1); + firstState.sigs = new bytes[](1); firstState.sigs[0] = _signState(firstState, guestPrivateKey); // Guest signs instead of host // Provide the initial state as proof @@ -178,17 +177,17 @@ contract CounterTest is Test { assertFalse(valid, "First state signed by incorrect participant should be rejected"); } - function test_adjudicate_firstState_revert_whenWrongIntent() public view { + function test_adjudicate_firstState_revert_whenWrongIntent() public { // Create initial state with target 10 State memory initialState = _createInitialState(10); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); // Create first state with incorrect intent State memory firstState = _createCounterState(10, 1); firstState.intent = StateIntent.FINALIZE; // Wrong intent for first operational state - firstState.sigs = new Signature[](1); + firstState.sigs = new bytes[](1); firstState.sigs[0] = _signState(firstState, hostPrivateKey); // Provide the initial state as proof @@ -202,15 +201,15 @@ contract CounterTest is Test { // -------------------- LATER STATE TRANSITION TESTS -------------------- - function test_adjudicate_laterState_valid() public view { + function test_adjudicate_laterState_valid() public { // Create state 1 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create state 2 (signed by guest) State memory state2 = _createCounterState(10, 2); - state2.sigs = new Signature[](1); + state2.sigs = new bytes[](1); state2.sigs[0] = _signState(state2, guestPrivateKey); // Provide state 1 as proof for state 2 @@ -222,15 +221,15 @@ contract CounterTest is Test { assertTrue(valid, "Valid state transition from 1 to 2 should be accepted"); } - function test_adjudicate_laterState_revert_whenIncorrectVersionIncrement() public view { + function test_adjudicate_laterState_revert_whenIncorrectVersionIncrement() public { // Create state 1 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create state 3 (skipping version 2) State memory state3 = _createCounterState(10, 3); - state3.sigs = new Signature[](1); + state3.sigs = new bytes[](1); state3.sigs[0] = _signState(state3, guestPrivateKey); // Provide state 1 as proof for state 3 @@ -242,15 +241,15 @@ contract CounterTest is Test { assertFalse(valid, "State with non-sequential version should be rejected"); } - function test_adjudicate_laterState_revert_whenTargetChanged() public view { + function test_adjudicate_laterState_revert_whenTargetChanged() public { // Create state 1 with target 10 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create state 2 with different target State memory state2 = _createCounterState(15, 2); - state2.sigs = new Signature[](1); + state2.sigs = new bytes[](1); state2.sigs[0] = _signState(state2, guestPrivateKey); // Provide state 1 as proof for state 2 @@ -262,17 +261,17 @@ contract CounterTest is Test { assertFalse(valid, "State with changed target should be rejected"); } - function test_adjudicate_laterState_revert_whenAllocationSumChanged() public view { + function test_adjudicate_laterState_revert_whenAllocationSumChanged() public { // Create state 1 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create state 2 with different allocation sums State memory state2 = _createCounterState(10, 2); state2.allocations[HOST].amount = 120; // Changed from 100 state2.allocations[GUEST].amount = 100; - state2.sigs = new Signature[](1); + state2.sigs = new bytes[](1); state2.sigs[0] = _signState(state2, guestPrivateKey); // Provide state 1 as proof for state 2 @@ -284,15 +283,15 @@ contract CounterTest is Test { assertFalse(valid, "State with changed allocation sum should be rejected"); } - function test_adjudicate_laterState_revert_whenWrongSigner() public view { + function test_adjudicate_laterState_revert_whenWrongSigner() public { // Create state 1 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create state 2 signed by host (should be guest) State memory state2 = _createCounterState(10, 2); - state2.sigs = new Signature[](1); + state2.sigs = new bytes[](1); state2.sigs[0] = _signState(state2, hostPrivateKey); // Host signs again instead of guest // Provide state 1 as proof for state 2 @@ -304,10 +303,10 @@ contract CounterTest is Test { assertFalse(valid, "State signed by incorrect participant should be rejected"); } - function test_adjudicate_revert_whenNoStateProof() public view { + function test_adjudicate_revert_whenNoStateProof() public { // Create state 2 without providing a proof State memory state2 = _createCounterState(10, 2); - state2.sigs = new Signature[](1); + state2.sigs = new bytes[](1); state2.sigs[0] = _signState(state2, guestPrivateKey); // Provide empty proofs array @@ -318,15 +317,15 @@ contract CounterTest is Test { assertFalse(valid, "Non-initial state without proof should be rejected"); } - function test_adjudicate_revert_whenTooManyProofs() public view { + function test_adjudicate_revert_whenTooManyProofs() public { // Create state 1 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create state 2 State memory state2 = _createCounterState(10, 2); - state2.sigs = new Signature[](1); + state2.sigs = new bytes[](1); state2.sigs[0] = _signState(state2, guestPrivateKey); // Provide both state 0 and state 1 as proofs (too many) @@ -341,10 +340,10 @@ contract CounterTest is Test { // -------------------- RESIZE STATE TRANSITION TESTS -------------------- - function test_adjudicate_afterResize_valid() public view { + function test_adjudicate_afterResize_valid() public { // Create state 1 State memory state1 = _createCounterState(10, 1); - state1.sigs = new Signature[](1); + state1.sigs = new bytes[](1); state1.sigs[0] = _signState(state1, hostPrivateKey); // Create resize state 2 @@ -352,12 +351,12 @@ contract CounterTest is Test { resizeAmounts[0] = 20; resizeAmounts[1] = -20; State memory resizeState = _createResizeState(10, 2, resizeAmounts); - resizeState.sigs = new Signature[](1); + resizeState.sigs = new bytes[](1); resizeState.sigs[0] = _signState(resizeState, guestPrivateKey); // Create state 3 after resize (valid operation state) State memory state3 = _createCounterState(10, 3); - state3.sigs = new Signature[](1); + state3.sigs = new bytes[](1); state3.sigs[0] = _signState(state3, hostPrivateKey); // Provide resize state as proof for state 3 @@ -369,18 +368,18 @@ contract CounterTest is Test { assertTrue(valid, "Valid state transition after resize should be accepted"); } - function test_adjudicate_afterResize_revert_whenTargetChanged() public view { + function test_adjudicate_afterResize_revert_whenTargetChanged() public { // Create resize state 2 int256[] memory resizeAmounts = new int256[](2); resizeAmounts[0] = 20; resizeAmounts[1] = -20; State memory resizeState = _createResizeState(10, 2, resizeAmounts); - resizeState.sigs = new Signature[](1); + resizeState.sigs = new bytes[](1); resizeState.sigs[0] = _signState(resizeState, guestPrivateKey); // Create state 3 after resize with different target State memory state3 = _createCounterState(15, 3); // Changed target from 10 to 15 - state3.sigs = new Signature[](1); + state3.sigs = new bytes[](1); state3.sigs[0] = _signState(state3, hostPrivateKey); // Provide resize state as proof for state 3 diff --git a/contract/test/adjudicators/EIP712AdjudicatorBaseTest.t.sol b/contract/test/adjudicators/EIP712AdjudicatorBaseTest.t.sol new file mode 100644 index 000000000..a77ebb3ef --- /dev/null +++ b/contract/test/adjudicators/EIP712AdjudicatorBaseTest.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "lib/forge-std/src/Test.sol"; +import {IERC5267} from "lib/openzeppelin-contracts/contracts/interfaces/IERC5267.sol"; +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; + +import {EIP712AdjudicatorBase} from "../../src/adjudicators/EIP712AdjudicatorBase.sol"; +import {TestEIP712Adjudicator} from "../mocks/TestEIP712Adjudicator.sol"; +import {MockERC20} from "../mocks/MockERC20.sol"; +import {MockEIP712} from "../mocks/MockEIP712.sol"; +import {Utils} from "../../src/Utils.sol"; + +contract EIP712AdjudicatorBaseTest is Test { + TestEIP712Adjudicator public adjudicator; + MockERC20 public token; + + address public owner = address(0x1); + address public notOwner = address(0x2); + address public channelImpl = address(0x3); + address public nonContractAddress = address(0x4); + address public newChannelImpl = address(0x5); + + function setUp() public { + token = new MockERC20("Test Token", "TEST", 18); + adjudicator = new TestEIP712Adjudicator(owner, channelImpl); + } + + function test_constructor_setsValues() public view { + assertEq(adjudicator.owner(), owner, "Owner should be set correctly"); + assertEq(address(adjudicator.channelImpl()), channelImpl, "Channel implementation should be set correctly"); + } + + function test_getChannelImplDomainSeparator_returnsCorrectCustodyDomainSeparator() public { + // Deploy a contract that implements EIP712 + MockEIP712 mockCustody = new MockEIP712("Custody", "1.0"); + TestEIP712Adjudicator custodyAdjudicator = new TestEIP712Adjudicator(owner, address(mockCustody)); + + bytes32 result = custodyAdjudicator.getChannelImplDomainSeparator(); + bytes32 expectedDomainSeparator = mockCustody.domainSeparator(); + + assertEq(result, expectedDomainSeparator, "Domain separator should match expected value"); + } + + function test_getChannelImplDomainSeparator_returnsNoEip712Support_whenContractDoesNotSupportEip712() public { + // Set MockERC20 as channel impl (doesn't support EIP712) + vm.prank(owner); + adjudicator.setChannelImpl(address(token)); + + bytes32 result = adjudicator.getChannelImplDomainSeparator(); + + assertEq(result, Utils.NO_EIP712_SUPPORT, "Should return NO_EIP712_SUPPORT for non-EIP712 contract"); + } + + function test_getChannelImplDomainSeparator_returnsNoEip712Support_whenAddressIsNotContract() public { + // Set non-contract address as channel impl + vm.prank(owner); + adjudicator.setChannelImpl(nonContractAddress); + + bytes32 result = adjudicator.getChannelImplDomainSeparator(); + + assertEq(result, Utils.NO_EIP712_SUPPORT, "Should return NO_EIP712_SUPPORT for non-contract address"); + } + + function test_setChannelImpl_onlyWorksForOwner() public { + vm.prank(owner); + adjudicator.setChannelImpl(newChannelImpl); + + assertEq(address(adjudicator.channelImpl()), newChannelImpl, "Channel impl should be updated by owner"); + } + + function test_setChannelImpl_failsForNotOwner() public { + vm.prank(notOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, notOwner)); + adjudicator.setChannelImpl(newChannelImpl); + } +} diff --git a/contract/test/adjudicators/SimpleConsensus.t.sol b/contract/test/adjudicators/SimpleConsensus.t.sol index 32e9ac8b8..de48c6368 100644 --- a/contract/test/adjudicators/SimpleConsensus.t.sol +++ b/contract/test/adjudicators/SimpleConsensus.t.sol @@ -8,9 +8,10 @@ import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECD import {TestUtils} from "../TestUtils.sol"; import {MockERC20} from "../mocks/MockERC20.sol"; +import {MockEIP712} from "../mocks/MockEIP712.sol"; import {IAdjudicator} from "../../src/interfaces/IAdjudicator.sol"; -import {Channel, State, Allocation, Signature, StateIntent} from "../../src/interfaces/Types.sol"; +import {Channel, State, Allocation, StateIntent, STATE_TYPEHASH} from "../../src/interfaces/Types.sol"; import {SimpleConsensus} from "../../src/adjudicators/SimpleConsensus.sol"; import {Utils} from "../../src/Utils.sol"; @@ -18,6 +19,10 @@ contract SimpleConsensusTest is Test { using ECDSA for bytes32; SimpleConsensus public adjudicator; + MockEIP712 public mockedChannelImpl; + + // Mockup constructor parameters + address mockedOwner = address(0x456); address public host; address public guest; @@ -31,7 +36,8 @@ contract SimpleConsensusTest is Test { uint256 private constant GUEST = 1; function setUp() public { - adjudicator = new SimpleConsensus(); + mockedChannelImpl = new MockEIP712("TestChannelImpl", "1.0"); + adjudicator = new SimpleConsensus(mockedOwner, address(mockedChannelImpl)); hostPrivateKey = 0x1; guestPrivateKey = 0x2; @@ -82,7 +88,7 @@ contract SimpleConsensusTest is Test { state.allocations = new Allocation[](2); state.allocations[HOST] = allocations[HOST]; state.allocations[GUEST] = allocations[GUEST]; - state.sigs = new Signature[](0); + state.sigs = new bytes[](0); return state; } @@ -98,15 +104,25 @@ contract SimpleConsensusTest is Test { return state; } - function _signState(State memory state, uint256 privateKey) internal view returns (Signature memory) { - bytes32 stateHash = Utils.getStateHash(channel, state); - (uint8 v, bytes32 r, bytes32 s) = TestUtils.sign(vm, privateKey, stateHash); - return Signature({v: v, r: r, s: s}); + function _signState(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(channel), state); + return TestUtils.sign(vm, privateKey, packedState); + } + + function _signStateEIP191(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes memory packedState = Utils.getPackedState(Utils.getChannelId(channel), state); + return TestUtils.signEIP191(vm, privateKey, packedState); + } + + function _signStateEIP712(State memory state, uint256 privateKey) internal view returns (bytes memory) { + bytes32 channelId = Utils.getChannelId(channel); + bytes32 domainSeparator = mockedChannelImpl.domainSeparator(); + return TestUtils.signStateEIP712(vm, channelId, state, STATE_TYPEHASH, domainSeparator, privateKey); } - function test_adjudicate_firstState_valid() public view { + function test_adjudicate_firstState_valid_withRawECDSASignatures() public { State memory initialState = _createInitialState("initial state"); - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); @@ -114,19 +130,39 @@ contract SimpleConsensusTest is Test { assertTrue(valid, "Valid first state transition should be accepted"); } - function test_adjudicate_firstState_revert_whenMissingParticipantSignature() public view { + function test_adjudicate_firstState_valid_withEIP191Signatures() public { + State memory initialState = _createInitialState("initial state"); + initialState.sigs = new bytes[](2); + initialState.sigs[HOST] = _signStateEIP191(initialState, hostPrivateKey); + initialState.sigs[GUEST] = _signStateEIP191(initialState, guestPrivateKey); + + bool valid = adjudicator.adjudicate(channel, initialState, new State[](0)); + assertTrue(valid, "Valid first state transition with EIP191 signatures should be accepted"); + } + + function test_adjudicate_firstState_valid_withEIP712Signatures() public { + State memory initialState = _createInitialState("initial state"); + initialState.sigs = new bytes[](2); + initialState.sigs[HOST] = _signStateEIP712(initialState, hostPrivateKey); + initialState.sigs[GUEST] = _signStateEIP712(initialState, guestPrivateKey); + + bool valid = adjudicator.adjudicate(channel, initialState, new State[](0)); + assertTrue(valid, "Valid first state transition with EIP712 signatures should be accepted"); + } + + function test_adjudicate_firstState_revert_whenMissingParticipantSignature() public { State memory initialState = _createInitialState("initial state"); - initialState.sigs = new Signature[](1); + initialState.sigs = new bytes[](1); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); bool valid = adjudicator.adjudicate(channel, initialState, new State[](0)); assertFalse(valid, "First state without both signatures should be rejected"); } - function test_adjudicate_firstState_revert_whenIncorrectIntent() public view { + function test_adjudicate_firstState_revert_whenIncorrectIntent() public { State memory initialState = _createInitialState("initial state"); initialState.intent = StateIntent.OPERATE; // Incorrect intent, should be INITIALIZE - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); @@ -134,10 +170,10 @@ contract SimpleConsensusTest is Test { assertFalse(valid, "First state with incorrect intent should be rejected"); } - function test_adjudicate_firstState_revert_whenIncorrectVersion() public view { + function test_adjudicate_firstState_revert_whenIncorrectVersion() public { State memory initialState = _createInitialState("initial state"); initialState.version = 1; // Incorrect version, should be 0 - initialState.sigs = new Signature[](2); + initialState.sigs = new bytes[](2); initialState.sigs[HOST] = _signState(initialState, hostPrivateKey); initialState.sigs[GUEST] = _signState(initialState, guestPrivateKey); @@ -145,9 +181,9 @@ contract SimpleConsensusTest is Test { assertFalse(valid, "First state with incorrect version should be rejected"); } - function test_adjudicate_laterState_valid() public view { + function test_adjudicate_laterState_valid() public { State memory state1 = _createOperateState("state 42", 42); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); @@ -155,14 +191,14 @@ contract SimpleConsensusTest is Test { assertTrue(valid, "Valid state transition from 1 to 2 should be accepted"); } - function test_adjudicate_revert_whenTooManyProofs() public view { + function test_adjudicate_revert_whenTooManyProofs() public { State memory state1 = _createOperateState("state 1", 1); - state1.sigs = new Signature[](2); + state1.sigs = new bytes[](2); state1.sigs[HOST] = _signState(state1, hostPrivateKey); state1.sigs[GUEST] = _signState(state1, guestPrivateKey); State memory state2 = _createOperateState("state 2", 2); - state2.sigs = new Signature[](2); + state2.sigs = new bytes[](2); state2.sigs[HOST] = _signState(state2, hostPrivateKey); state2.sigs[GUEST] = _signState(state2, guestPrivateKey); @@ -174,10 +210,10 @@ contract SimpleConsensusTest is Test { } // Test signature validation using a non-corrupt signature but wrong signer - function test_adjudicate_revert_wrongSigner() public view { + function test_adjudicate_revert_wrongSigner() public { // Create state with signatures from wrong participants State memory state = _createOperateState("state 13", 13); - state.sigs = new Signature[](2); + state.sigs = new bytes[](2); state.sigs[HOST] = _signState(state, guestPrivateKey); // Should be host, but using guest state.sigs[GUEST] = _signState(state, guestPrivateKey); diff --git a/contract/test/mocks/MockEIP712.sol b/contract/test/mocks/MockEIP712.sol new file mode 100644 index 000000000..7a573de49 --- /dev/null +++ b/contract/test/mocks/MockEIP712.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {EIP712} from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; + +contract MockEIP712 is EIP712 { + constructor(string memory name, string memory version) EIP712(name, version) {} + + function domainSeparator() external view returns (bytes32) { + return _domainSeparatorV4(); + } +} diff --git a/contract/test/mocks/MockERC4337Factory.sol b/contract/test/mocks/MockERC4337Factory.sol new file mode 100644 index 000000000..d3077d46b --- /dev/null +++ b/contract/test/mocks/MockERC4337Factory.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {MockFlagERC1271} from "./MockFlagERC1271.sol"; + +contract MockERC4337Factory { + event AccountCreated(address indexed account, bytes32 salt, bool flag); + + function createAccount(bool flag, bytes32 salt) external returns (address) { + bytes memory bytecode = abi.encodePacked(type(MockFlagERC1271).creationCode, abi.encode(flag)); + + address account; + assembly { + account := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + } + + require(account != address(0), "Account creation failed"); + + emit AccountCreated(account, salt, flag); + return account; + } + + function getAddress(bool flag, bytes32 salt) external view returns (address) { + bytes memory bytecode = abi.encodePacked(type(MockFlagERC1271).creationCode, abi.encode(flag)); + + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))); + + return address(uint160(uint256(hash))); + } +} diff --git a/contract/test/mocks/MockFlagERC1271.sol b/contract/test/mocks/MockFlagERC1271.sol new file mode 100644 index 000000000..4897f2875 --- /dev/null +++ b/contract/test/mocks/MockFlagERC1271.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IERC1271} from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; + +contract MockFlagERC1271 is IERC1271 { + bool private _flag; + bytes4 private constant ERC1271_SUCCESS = 0x1626ba7e; + bytes4 private constant ERC1271_FAILURE = 0xffffffff; + + constructor(bool flag) { + _flag = flag; + } + + function isValidSignature(bytes32, bytes memory) external view override returns (bytes4) { + return _flag ? ERC1271_SUCCESS : ERC1271_FAILURE; + } + + function setFlag(bool flag) external { + _flag = flag; + } + + function getFlag() external view returns (bool) { + return _flag; + } +} diff --git a/contract/test/mocks/TestEIP712Adjudicator.sol b/contract/test/mocks/TestEIP712Adjudicator.sol new file mode 100644 index 000000000..c014a55d4 --- /dev/null +++ b/contract/test/mocks/TestEIP712Adjudicator.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {EIP712AdjudicatorBase} from "../../src/adjudicators/EIP712AdjudicatorBase.sol"; + +/** + * @title Test EIP712 Adjudicator + * @notice Test contract that inherits from EIP712AdjudicatorBase for testing purposes. + */ +contract TestEIP712Adjudicator is EIP712AdjudicatorBase { + constructor(address owner, address channelImpl_) EIP712AdjudicatorBase(owner, channelImpl_) {} +} diff --git a/docker-compose.yml b/docker-compose.yml index 8422f09e5..7af3512d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: container_name: anvil ports: - "8545:8545" - entrypoint: ["anvil"] + entrypoint: ["anvil"] command: ["--host","0.0.0.0","--chain-id","31337","--accounts","15","--balance","30000"] healthcheck: test: ["CMD", "cast", "block-number", "--rpc-url", "http://localhost:8545"] @@ -88,16 +88,16 @@ services: tstamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); \" && - + echo 'Running pending migrations...' && for migration in /migrations/*.sql; do filename=\$$(basename \$$migration) && version=\$$(echo \$$filename | grep -o '^[0-9]\\+') && - + if ! psql -h database -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres} -tAc \"SELECT 1 FROM goose_db_version WHERE version_id = \$$version\" | grep -q 1; then echo \"Applying migration: \$$filename\" && # Extract and execute only the Up migration - sed -n '/^-- +goose Up/,/^-- +goose Down/p' \$$migration | + sed -n '/^-- +goose Up/,/^-- +goose Down/p' \$$migration | grep -v '^-- +goose' | psql -h database -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres} && psql -h database -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres} -c \"INSERT INTO goose_db_version (version_id) VALUES (\$$version);\" && @@ -110,11 +110,11 @@ services: echo 'Checking database...' && # Check if token already exists TOKEN_EXISTS=$$(psql -h database -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres} -t -c \"SELECT COUNT(*) FROM assets WHERE token = '$$TOKEN_ADDRESS' AND chain_id = 31337;\" | xargs) && - + if [ \"$$TOKEN_EXISTS\" -eq \"0\" ]; then echo 'Seeding database with tokens...' && psql -h database -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-postgres} -c \" - INSERT INTO assets (token, chain_id, symbol, decimals) + INSERT INTO assets (token, chain_id, symbol, decimals) VALUES ('$$TOKEN_ADDRESS', 31337, 'USDC', 6); \" && echo 'Database seeded with tokens successfully' @@ -149,9 +149,9 @@ services: ANVIL_BALANCE_CHECKER_ADDRESS: ${ANVIL_BALANCE_CHECKER_ADDRESS:-0x730dB3A1D3Ca47e7BaEb260c24C74ED4378726Bc} ANVIL_INFURA_URL: ws://anvil:8545 CLEARNODE_DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@database:5432/${POSTGRES_DB:-postgres}?sslmode=disable + CLEARNODE_LOG_LEVEL: ${CLEARNODE_LOG_LEVEL:-info} restart: unless-stopped volumes: postgres_data: driver: local - diff --git a/erc7824-docs/docs/erc-7824.md b/erc7824-docs/docs/erc-7824.md index 2a6ae423a..a18b170f4 100644 --- a/erc7824-docs/docs/erc-7824.md +++ b/erc7824-docs/docs/erc-7824.md @@ -49,12 +49,6 @@ The Nitrolite protocol defines the following core data structures: #### Basic Types ```solidity -struct Signature { - uint8 v; - bytes32 r; - bytes32 s; -} - struct Amount { address token; // ERC-20 token address (address(0) for native tokens) uint256 amount; // Token amount @@ -74,7 +68,7 @@ struct Channel { address[] participants; // List of participants in the channel address adjudicator; // Address of the contract that validates state transitions uint64 challenge; // Duration in seconds for dispute resolution period - uint64 nonce; // Unique per channel with same participants and adjudicator + uint64 nonce; // Unique per channel with same participants and adjudicator } ``` @@ -82,11 +76,11 @@ struct Channel { ```solidity struct State { - StateIntent intent; // Intent of the state - uint256 version; // State version incremental number to compare most recent - bytes data; // Application data encoded, decoded by the adjudicator - Allocation[] allocations; // Asset allocation and destination for each participant - Signature[] sigs; // stateHash signatures from participants + StateIntent intent; // Intent of the state + uint256 version; // State version incremental number to compare most recent + bytes data; // Application data encoded, decoded by the adjudicator + Allocation[] allocations; // Asset allocation and destination for each participant + bytes[] sigs; // stateHash signatures from participants } enum StateIntent { @@ -132,14 +126,15 @@ For signature verification, the state hash is computed as: bytes32 stateHash = keccak256( abi.encode( channelId, - state.data, + state.intent, state.version, + state.data, state.allocations ) ); ``` -Note: The stateHash is bare signed without EIP-191 since the protocol is intended to be chain-agnostic. +Note: The smart contract supports all popular signature formats, specifically: raw ECDSA, EIP-191, EIP-712, EIP-1271, and EIP-6492. ### Interfaces @@ -161,7 +156,7 @@ interface IAdjudicator { Channel calldata chan, State calldata candidate, State[] calldata proofs - ) external view returns (bool valid); + ) external returns (bool valid); } ``` @@ -218,7 +213,7 @@ interface IChannel { * @param sig Signature of the participant on the funding state * @return channelId Unique identifier for the joined channel */ - function join(bytes32 channelId, uint256 index, Signature calldata sig) + function join(bytes32 channelId, uint256 index, bytes calldata sig) external returns (bytes32); /** @@ -253,11 +248,13 @@ interface IChannel { * @param channelId Unique identifier for the channel * @param candidate The state being submitted as the latest valid state * @param proofs Additional states required by the adjudicator + * @param challengerSig Signature of the challenger on the candidate state. Must be signed by one of the participants */ function challenge( bytes32 channelId, State calldata candidate, - State[] calldata proofs + State[] calldata proofs, + bytes calldata challengerSig ) external; /** @@ -284,25 +281,27 @@ interface IDeposit { /** * @notice Deposits tokens into the contract * @dev For native tokens, the value should be sent with the transaction + * @param wallet Address of the account whose ledger is changed * @param token Token address (use address(0) for native tokens) * @param amount Amount of tokens to deposit */ - function deposit(address token, uint256 amount) external payable; + function deposit(address wallet, address token, uint256 amount) external payable; /** * @notice Withdraws tokens from the contract * @dev Can only withdraw available (not locked in channels) funds + * @param wallet Address of the account whose ledger is changed * @param token Token address (use address(0) for native tokens) * @param amount Amount of tokens to withdraw */ - function withdraw(address token, uint256 amount) external; + function withdraw(address wallet, address token, uint256 amount) external; } ``` ### Channel Lifecycle -1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE` -2. **Joining**: Participants verify the channel and sign the same funding state +1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE`. Second participant are able to join a channel immediately by providing a signature over initial state, and funds will be deducted from their account, if available. +2. **Joining**: Participants verify the channel and sign the same funding state. This step can be omitted by providing a signature over the initial state when creating the channel. Note, however, that this means that funds will be locked from the participant's balance, while `join(...)` allows to fund the channel from external account. 3. **Active**: Once fully funded, the channel transitions to active state for off-chain operation 4. **Off-chain Updates**: Participants exchange and sign state updates according to application logic 5. **Resolution**: @@ -330,7 +329,7 @@ contract Remittance is IAdjudicator, IComparable { Channel calldata chan, State calldata candidate, State[] calldata proofs - ) external view returns (bool valid) { + ) external returns (bool valid) { // Decode the payment intent Intent memory intent = abi.decode(candidate.data, (Intent)); @@ -456,7 +455,7 @@ No backward compatibility issues found. This ERC is designed to coexist with exi ### On-Chain Security -- **Signature Verification**: All state transitions require valid signatures from participants. The protocol uses bare signatures (without EIP-191) for chain agnosticism. +- **Signature Verification**: All state transitions require valid signatures from participants. The protocol supports signatures of all popular formats, including EIP-191 and EIP-712. - **Challenge Period**: The configurable challenge duration provides time for honest participants to respond to invalid states. - **Adjudicator Validation**: Custom adjudicators must be carefully audited as they control state transition rules. - **Reentrancy Protection**: Implementation should follow checks-effects-interactions pattern, especially in fund distribution. diff --git a/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md b/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md new file mode 100644 index 000000000..9422f138e --- /dev/null +++ b/erc7824-docs/docs/nitrolite_client/advanced/supported-sig-formats.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 4 +title: Supported Signature Formats +description: Documentation for supported signature formats in NitroliteClient +keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced, signature, format, ECDSA, EIP-191, EIP-712, EIP-1271, EIP-6492] +--- + +# Supported Signature Formats + +The nitrolite smart contract supports multiple signature formats over a State to accommodate various use cases and compatibility with different wallets and applications. + +The message being signed is a channelId and State, formatted in a specific way. The most common is a `packedState`, which is calculated as follows: + +```solidity +abi.encode(channelId, state.intent, state.version, state.data, state.allocations) +``` + +## EOA signatures + +Externally Owned Accounts (EOAs) can sign messages with their private key using the ECDSA. + +Based on how the message is handled before signing, the following formats are supported: + +### Raw ECDSA Signature + +The message is a `packedState`, that is hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature. + +### EIP-191 Signature + +You can read more about EIP-191 in the [EIP-191 specification](https://eips.ethereum.org/EIPS/eip-191). + +The message is a `packedState` prefixed with `"\x19Ethereum Signed Message:\n" + len(packedState)` and hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature. + +### EIP-712 Signature + +You can read more about EIP-712 in the [EIP-712 specification](https://eips.ethereum.org/EIPS/eip-712). + +The message is an `AllowStateHash` typed data, calculated as follows: + +```solidity +abi.encode( + typeHash, + channelId, + state.intent, + state.version, + keccak256(state.data), + keccak256(abi.encode(state.allocations)) +); +``` + +Where `typeHash` is `AllowStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)`. + +The message is then hashed with `keccak256`, appended to `"\x19\x01" || domainSeparator` and signed. The signature is a 65-byte ECDSA signature. + +`||` is a concatenation operator, and `domainSeparator` is calculated as follows: + +```solidity +keccak256( + abi.encode( + EIP712_TYPE_HASH, + keccak256(name), + keccak256(version), + chainId, + verifyingContract + ) +); +``` + +`EIP712_TYPE_HASH` is `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. + +Additionally, `name`, `version` are the name and version of the Custody contract, `chainId` is the chain ID of the network, and `verifyingContract` is the address of the contract. + +## Smart Contract Signatures + +Smart Contracts that support [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) or [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492) can sign messages using their own logic. When checking such signatures, the nitrolite smart contract will pass the `keccak256` hash of the `packedState` as a message hash for verification. + +See the aforementioned EIP standards for details on how these signatures are structured and verified. If you want to add support for such signatures in your client, you probably need to look at how signature verification logic is implemented in the Smart Contract (Smart Wallet, etc) that will use them. + +## Challenge Signatures + +The aforementioned signature formats are used to sign States, however to submit a challenge, the user must provide a `challengerSignature`, which proves that the user has the right to challenge a Channel. + +Depending on a signature format, the `challengerSignature` is calculated differently from the common State signature: + +- **Raw ECDSA, EIP-191, EIP-1271 and EIP-6492**: The message (`packedState`) is suffixed with a `challenge` string (`abi.encodePacked(packedState, "challenge")`). +- **EIP-712**: The `typeHash` name is `AllowChallengeStateHash`, while type format remains the same. diff --git a/erc7824-docs/docs/nitrolite_client/types.md b/erc7824-docs/docs/nitrolite_client/types.md index 607f5e0a8..e82cec173 100644 --- a/erc7824-docs/docs/nitrolite_client/types.md +++ b/erc7824-docs/docs/nitrolite_client/types.md @@ -30,14 +30,10 @@ A hash of a channel state, represented as a hexadecimal string. ### Signature ```typescript -interface Signature { - v: number; // Recovery value - r: Hex; // First 32 bytes of the signature - s: Hex; // Second 32 bytes of the signature -} +type Signature = Hex; ``` -Represents a cryptographic signature used for signing state channel states. +Represents a cryptographic signature used for signing state channel states as a hexadecimal string. ### Allocation @@ -81,11 +77,11 @@ Indicates the intent of a state update. The intent determines how the state is p ```typescript interface State { - intent: StateIntent; // Intent of the state - version: bigint; // Version number, incremented for each update - data: Hex; // Application data encoded as hex - allocations: [Allocation, Allocation]; // Asset allocation for each participant - sigs: Signature[]; // State hash signatures + intent: StateIntent; // Intent of the state + version: bigint; // Version number, incremented for each update + data: Hex; // Application data encoded as hex + allocations: [Allocation, Allocation]; // Asset allocation for each participant + sigs: Signature[]; // State hash signatures } ``` diff --git a/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md b/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md index 4a410ffad..390866c38 100644 --- a/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md +++ b/erc7824-docs/docs/quick_start/connect_to_the_clearnode.md @@ -554,7 +554,7 @@ const messageSigner = async (payload: RequestData | ResponsePayload): Promise { const depositAmount = parseUnits('100', 6); // 100 USDC (decimals = 6) diff --git a/integration/tests/resize_channel.test.ts b/integration/tests/resize_channel.test.ts index 5e90b7cdc..41e8e5ea0 100644 --- a/integration/tests/resize_channel.test.ts +++ b/integration/tests/resize_channel.test.ts @@ -79,12 +79,8 @@ describe('Resize channel', () => { expect(resizeResponseParams.stateData).toBeDefined(); expect(resizeResponseParams.intent).toBe(2); // StateIntent.RESIZE // TODO: add enum to sdk expect(resizeResponseParams.version).toBe(createResponseParams.version + 1); - expect(resizeResponseParams.stateHash).toBeDefined(); expect(resizeResponseParams.serverSignature).toBeDefined(); - expect(resizeResponseParams.serverSignature.v).toBeDefined(); - expect(resizeResponseParams.serverSignature.r).toBeDefined(); - expect(resizeResponseParams.serverSignature.s).toBeDefined(); expect(resizeResponseParams.allocations).toBeDefined(); expect(resizeResponseParams.allocations).toHaveLength(2); diff --git a/sdk/src/abis/generated.ts b/sdk/src/abis/generated.ts index 2f1678212..fbf890555 100644 --- a/sdk/src/abis/generated.ts +++ b/sdk/src/abis/generated.ts @@ -1,1733 +1,1567 @@ // Auto-generated file. Do not edit manually. export const custodyAbi = [ - { - type: 'function', - name: 'challenge', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'candidate', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - { - name: 'proofs', - type: 'tuple[]', - internalType: 'struct State[]', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - { - name: 'challengerSig', - type: 'tuple', - internalType: 'struct Signature', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'checkpoint', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'candidate', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - { - name: 'proofs', - type: 'tuple[]', - internalType: 'struct State[]', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'close', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'candidate', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - { - name: '', - type: 'tuple[]', - internalType: 'struct State[]', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'create', - inputs: [ - { - name: 'ch', - type: 'tuple', - internalType: 'struct Channel', - components: [ - { - name: 'participants', - type: 'address[]', - internalType: 'address[]', - }, - { - name: 'adjudicator', - type: 'address', - internalType: 'address', - }, - { - name: 'challenge', - type: 'uint64', - internalType: 'uint64', - }, - { - name: 'nonce', - type: 'uint64', - internalType: 'uint64', - }, - ], - }, - { - name: 'initial', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - outputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'deposit', - inputs: [ - { - name: 'account', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - outputs: [], - stateMutability: 'payable', - }, - { - type: 'function', - name: 'depositAndCreate', - inputs: [ - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'ch', - type: 'tuple', - internalType: 'struct Channel', - components: [ - { - name: 'participants', - type: 'address[]', - internalType: 'address[]', - }, - { - name: 'adjudicator', - type: 'address', - internalType: 'address', - }, - { - name: 'challenge', - type: 'uint64', - internalType: 'uint64', - }, - { - name: 'nonce', - type: 'uint64', - internalType: 'uint64', - }, - ], - }, - { - name: 'initial', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - outputs: [ - { - name: '', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - stateMutability: 'payable', - }, - { - type: 'function', - name: 'getAccountsBalances', - inputs: [ - { - name: 'accounts', - type: 'address[]', - internalType: 'address[]', - }, - { - name: 'tokens', - type: 'address[]', - internalType: 'address[]', - }, - ], - outputs: [ - { - name: '', - type: 'uint256[][]', - internalType: 'uint256[][]', - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - name: 'getChannelBalances', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'tokens', - type: 'address[]', - internalType: 'address[]', - }, - ], - outputs: [ - { - name: 'balances', - type: 'uint256[]', - internalType: 'uint256[]', - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - name: 'getChannelData', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - outputs: [ - { - name: 'channel', - type: 'tuple', - internalType: 'struct Channel', - components: [ - { - name: 'participants', - type: 'address[]', - internalType: 'address[]', - }, - { - name: 'adjudicator', - type: 'address', - internalType: 'address', - }, - { - name: 'challenge', - type: 'uint64', - internalType: 'uint64', - }, - { - name: 'nonce', - type: 'uint64', - internalType: 'uint64', - }, - ], - }, - { - name: 'status', - type: 'uint8', - internalType: 'enum ChannelStatus', - }, - { - name: 'wallets', - type: 'address[]', - internalType: 'address[]', - }, - { - name: 'challengeExpiry', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'lastValidState', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - name: 'getOpenChannels', - inputs: [ - { - name: 'accounts', - type: 'address[]', - internalType: 'address[]', - }, - ], - outputs: [ - { - name: '', - type: 'bytes32[][]', - internalType: 'bytes32[][]', - }, - ], - stateMutability: 'view', - }, - { - type: 'function', - name: 'join', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'index', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'sig', - type: 'tuple', - internalType: 'struct Signature', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - outputs: [ - { - name: '', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'resize', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'candidate', - type: 'tuple', - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - { - name: 'proofs', - type: 'tuple[]', - internalType: 'struct State[]', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - name: 'withdraw', - inputs: [ - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'event', - name: 'Challenged', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - { - name: 'state', - type: 'tuple', - indexed: false, - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - { - name: 'expiration', - type: 'uint256', - indexed: false, - internalType: 'uint256', - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'Checkpointed', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - { - name: 'state', - type: 'tuple', - indexed: false, - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'Closed', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - { - name: 'finalState', - type: 'tuple', - indexed: false, - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'Created', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - { - name: 'wallet', - type: 'address', - indexed: true, - internalType: 'address', - }, - { - name: 'channel', - type: 'tuple', - indexed: false, - internalType: 'struct Channel', - components: [ - { - name: 'participants', - type: 'address[]', - internalType: 'address[]', - }, - { - name: 'adjudicator', - type: 'address', - internalType: 'address', - }, - { - name: 'challenge', - type: 'uint64', - internalType: 'uint64', - }, - { - name: 'nonce', - type: 'uint64', - internalType: 'uint64', - }, - ], - }, - { - name: 'initial', - type: 'tuple', - indexed: false, - internalType: 'struct State', - components: [ - { - name: 'intent', - type: 'uint8', - internalType: 'enum StateIntent', - }, - { - name: 'version', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'allocations', - type: 'tuple[]', - internalType: 'struct Allocation[]', - components: [ - { - name: 'destination', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - name: 'sigs', - type: 'tuple[]', - internalType: 'struct Signature[]', - components: [ - { - name: 'v', - type: 'uint8', - internalType: 'uint8', - }, - { - name: 'r', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 's', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - ], - }, - ], - 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: 'Joined', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - { - name: 'index', - type: 'uint256', - indexed: false, - internalType: 'uint256', - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'Opened', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - ], - anonymous: false, - }, - { - type: 'event', - name: 'Resized', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - indexed: true, - internalType: 'bytes32', - }, - { - name: 'deltaAllocations', - type: 'int256[]', - indexed: false, - internalType: 'int256[]', - }, - ], - 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: 'ChallengeNotExpired', - inputs: [], - }, - { - type: 'error', - name: 'ChannelNotFinal', - inputs: [], - }, - { - type: 'error', - name: 'ChannelNotFound', - inputs: [ - { - name: 'channelId', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - }, - { - type: 'error', - name: 'DepositAlreadyFulfilled', - inputs: [], - }, - { - type: 'error', - name: 'DepositsNotFulfilled', - inputs: [ - { - name: 'expectedFulfilled', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'actualFulfilled', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - 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: 'InsufficientBalance', - inputs: [ - { - name: 'available', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'required', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, - { - type: 'error', - name: 'InvalidAdjudicator', - inputs: [], - }, - { - type: 'error', - name: 'InvalidAllocations', - inputs: [], - }, - { - type: 'error', - name: 'InvalidAmount', - inputs: [], - }, - { - type: 'error', - name: 'InvalidChallengePeriod', - inputs: [], - }, - { - type: 'error', - name: 'InvalidChallengerSignature', - inputs: [], - }, - { - type: 'error', - name: 'InvalidParticipant', - inputs: [], - }, - { - type: 'error', - name: 'InvalidState', - inputs: [], - }, - { - type: 'error', - name: 'InvalidStateSignatures', - inputs: [], - }, - { - type: 'error', - name: 'InvalidStatus', - inputs: [], - }, - { - type: 'error', - name: 'InvalidValue', - inputs: [], - }, - { - type: 'error', - name: 'SafeERC20FailedOperation', - inputs: [ - { - name: 'token', - type: 'address', - internalType: 'address', - }, - ], - }, - { - type: 'error', - name: 'TransferFailed', - inputs: [ - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'to', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, - ], - }, + { + "type": "constructor", + "inputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "CHALLENGE_STATE_TYPEHASH", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "MIN_CHALLENGE_PERIOD", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "challenge", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "candidate", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + }, + { + "name": "proofs", + "type": "tuple[]", + "internalType": "struct State[]", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + }, + { + "name": "challengerSig", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "checkpoint", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "candidate", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + }, + { + "name": "proofs", + "type": "tuple[]", + "internalType": "struct State[]", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "close", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "candidate", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + }, + { + "name": "", + "type": "tuple[]", + "internalType": "struct State[]", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "create", + "inputs": [ + { + "name": "ch", + "type": "tuple", + "internalType": "struct Channel", + "components": [ + { + "name": "participants", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "adjudicator", + "type": "address", + "internalType": "address" + }, + { + "name": "challenge", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "initial", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "outputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "deposit", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "depositAndCreate", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "ch", + "type": "tuple", + "internalType": "struct Channel", + "components": [ + { + "name": "participants", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "adjudicator", + "type": "address", + "internalType": "address" + }, + { + "name": "challenge", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "initial", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { + "name": "fields", + "type": "bytes1", + "internalType": "bytes1" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "version", + "type": "string", + "internalType": "string" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "extensions", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAccountsBalances", + "inputs": [ + { + "name": "accounts", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "tokens", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[][]", + "internalType": "uint256[][]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getChannelBalances", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "tokens", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [ + { + "name": "balances", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getChannelData", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "channel", + "type": "tuple", + "internalType": "struct Channel", + "components": [ + { + "name": "participants", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "adjudicator", + "type": "address", + "internalType": "address" + }, + { + "name": "challenge", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "status", + "type": "uint8", + "internalType": "enum ChannelStatus" + }, + { + "name": "wallets", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "challengeExpiry", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "lastValidState", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOpenChannels", + "inputs": [ + { + "name": "accounts", + "type": "address[]", + "internalType": "address[]" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32[][]", + "internalType": "bytes32[][]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "join", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "index", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "sig", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "resize", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "candidate", + "type": "tuple", + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + }, + { + "name": "proofs", + "type": "tuple[]", + "internalType": "struct State[]", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdraw", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "Challenged", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "state", + "type": "tuple", + "indexed": false, + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + }, + { + "name": "expiration", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Checkpointed", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "state", + "type": "tuple", + "indexed": false, + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Closed", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "finalState", + "type": "tuple", + "indexed": false, + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "type": "bytes[]", + "internalType": "bytes[]" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Created", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "wallet", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "channel", + "type": "tuple", + "indexed": false, + "internalType": "struct Channel", + "components": [ + { + "name": "participants", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "adjudicator", + "type": "address", + "internalType": "address" + }, + { + "name": "challenge", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "nonce", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "name": "initial", + "type": "tuple", + "indexed": false, + "internalType": "struct State", + "components": [ + { + "name": "intent", + "type": "uint8", + "internalType": "enum StateIntent" + }, + { + "name": "version", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "allocations", + "type": "tuple[]", + "internalType": "struct Allocation[]", + "components": [ + { + "name": "destination", + "type": "address", + "internalType": "address" + }, + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "sigs", + "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": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "Joined", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "index", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Opened", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Resized", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "indexed": true, + "internalType": "bytes32" + }, + { + "name": "deltaAllocations", + "type": "int256[]", + "indexed": false, + "internalType": "int256[]" + } + ], + "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": "ChallengeNotExpired", + "inputs": [] + }, + { + "type": "error", + "name": "ChannelNotFinal", + "inputs": [] + }, + { + "type": "error", + "name": "ChannelNotFound", + "inputs": [ + { + "name": "channelId", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "DepositAlreadyFulfilled", + "inputs": [] + }, + { + "type": "error", + "name": "DepositsNotFulfilled", + "inputs": [ + { + "name": "expectedFulfilled", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "actualFulfilled", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "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": "ERC6492DeploymentFailed", + "inputs": [ + { + "name": "factory", + "type": "address", + "internalType": "address" + }, + { + "name": "calldata_", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "type": "error", + "name": "ERC6492NoCode", + "inputs": [ + { + "name": "expectedSigner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "required", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "InvalidAdjudicator", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAllocations", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAmount", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidChallengePeriod", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidChallengerSignature", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidParticipant", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidShortString", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidState", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidStateSignatures", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidStatus", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidValue", + "inputs": [] + }, + { + "type": "error", + "name": "SafeERC20FailedOperation", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "StringTooLong", + "inputs": [ + { + "name": "str", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "TransferFailed", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "address" + }, + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + } + ] + } ] as const; diff --git a/sdk/src/client/services/NitroliteService.ts b/sdk/src/client/services/NitroliteService.ts index a02b20f8b..77a135a68 100644 --- a/sdk/src/client/services/NitroliteService.ts +++ b/sdk/src/client/services/NitroliteService.ts @@ -1,4 +1,4 @@ -import { Account, Address, PublicClient, WalletClient, Hash, zeroAddress } from 'viem'; +import { Account, Address, PublicClient, WalletClient, Hash, zeroAddress, Hex } from 'viem'; import { custodyAbi } from '../../abis/generated'; import { ContractAddresses } from '../../abis'; import { Errors } from '../../errors'; @@ -127,27 +127,7 @@ export class NitroliteService { token: Address; amount: bigint; }[], - sigs: (state.sigs || []).map((sig) => ({ - v: sig.v, - r: sig.r, - s: sig.s, - })) as readonly { - v: number; - r: `0x${string}`; - s: `0x${string}`; - }[], - } as const; - } - - /** - * Converts Signature type to format expected by generated ABI - * REQUIRED: Ensures proper readonly typing for viem compatibility - */ - private convertSignatureForABI(signature: Signature) { - return { - v: signature.v, - r: signature.r, - s: signature.s, + sigs: state.sigs || [] as readonly Hex[], } as const; } @@ -176,11 +156,7 @@ export class NitroliteService { token: alloc.token, amount: alloc.amount, })), - sigs: contractState.sigs.map((sig: any) => ({ - v: sig.v, - r: sig.r, - s: sig.s, - })), + sigs: contractState.sigs, }; } @@ -371,13 +347,11 @@ export class NitroliteService { const operationName = 'prepareJoinChannel'; try { - const abiSignature = this.convertSignatureForABI(sig); - const { request } = await this.publicClient.simulateContract({ address: this.custodyAddress, abi: custodyAbi, functionName: 'join', - args: [channelId, index, abiSignature], + args: [channelId, index, sig], account: account, }); @@ -492,13 +466,12 @@ export class NitroliteService { try { const abiCandidate = this.convertStateForABI(candidate); const abiProofs = proofs.map((proof) => this.convertStateForABI(proof)); - const challengerSigABI = this.convertSignatureForABI(challengerSig); const { request } = await this.publicClient.simulateContract({ address: this.custodyAddress, abi: custodyAbi, functionName: 'challenge', - args: [channelId, abiCandidate, abiProofs, challengerSigABI], + args: [channelId, abiCandidate, abiProofs, challengerSig], account: account, }); diff --git a/sdk/src/client/state.ts b/sdk/src/client/state.ts index 6dfd076d8..40878f2a7 100644 --- a/sdk/src/client/state.ts +++ b/sdk/src/client/state.ts @@ -1,6 +1,6 @@ -import { Address, encodeAbiParameters, Hex, keccak256 } from 'viem'; +import { Address, encodeAbiParameters, encodePacked, Hex, keccak256 } from 'viem'; import * as Errors from '../errors'; -import { generateChannelNonce, getChannelId, getStateHash, removeQuotesFromRS, signState } from '../utils'; +import { generateChannelNonce, getChannelId, getPackedState, getStateHash, signChallengeState, signState } from '../utils'; import { PreparerDependencies } from './prepare'; import { ChallengeChannelParams, @@ -77,9 +77,8 @@ export async function _prepareAndSignInitialState( sigs: [], }; - const stateHash = getStateHash(channelId, stateToSign); - const accountSignature = await signState(stateHash, deps.stateWalletClient.signMessage); + const accountSignature = await signState(channelId, stateToSign, deps.stateWalletClient.signMessage); const initialState: State = { ...stateToSign, sigs: [accountSignature], @@ -105,17 +104,7 @@ export async function _prepareAndSignChallengeState( challengerSig: Signature; }> { const { channelId, candidateState, proofStates = [] } = params; - - const stateHash = getStateHash(channelId, candidateState); - const encoded = encodeAbiParameters( - [ - { type: 'bytes32', name: 'stateHash' }, - { type: 'string', name: 'challenge' }, - ], - [stateHash, 'challenge'], - ); - const challengeHash = keccak256(encoded) as Hex; - const challengerSig = await signState(challengeHash, deps.stateWalletClient.signMessage); + const challengerSig = await signChallengeState(channelId, candidateState, deps.stateWalletClient.signMessage); return { channelId, candidateState, proofs: proofStates, challengerSig }; } @@ -138,7 +127,6 @@ export async function _prepareAndSignResizeState( } const channelId = resizeState.channelId; - const serverSignature = removeQuotesFromRS(resizeState.serverSignature); const stateToSign: State = { data: resizeState.data, @@ -148,14 +136,12 @@ export async function _prepareAndSignResizeState( sigs: [], }; - const stateHash = getStateHash(channelId, stateToSign); - - const accountSignature = await signState(stateHash, deps.stateWalletClient.signMessage); + const accountSignature = await signState(channelId, stateToSign, deps.stateWalletClient.signMessage); // Create a new state with signatures in the requested style const resizeStateWithSigs: State = { ...stateToSign, - sigs: [accountSignature, serverSignature], + sigs: [accountSignature, resizeState.serverSignature], }; let proofs: State[] = [...proofStates]; @@ -181,7 +167,6 @@ export async function _prepareAndSignFinalState( } const channelId = finalState.channelId; - const serverSignature = removeQuotesFromRS(finalState.serverSignature); const stateToSign: State = { data: stateData, @@ -191,14 +176,12 @@ export async function _prepareAndSignFinalState( sigs: [], }; - const stateHash = getStateHash(channelId, stateToSign); - - const accountSignature = await signState(stateHash, deps.stateWalletClient.signMessage); + const accountSignature = await signState(channelId, stateToSign, deps.stateWalletClient.signMessage); // Create a new state with signatures in the requested style const finalStateWithSigs: State = { ...stateToSign, - sigs: [accountSignature, serverSignature], + sigs: [accountSignature, finalState.serverSignature], }; return { finalStateWithSigs, channelId }; diff --git a/sdk/src/client/types.ts b/sdk/src/client/types.ts index 50d2b1de5..2bb5171ea 100644 --- a/sdk/src/client/types.ts +++ b/sdk/src/client/types.ts @@ -12,13 +12,10 @@ export type ChannelId = Hex; export type StateHash = Hex; /** - * Signature structure used for state channel operations + * Signature type used when signing states + * @dev Hex is used to support EIP-1271 and EIP-6492 signatures. */ -export interface Signature { - v: number; - r: Hex; - s: Hex; -} +export type Signature = Hex; /** * Allocation structure representing fund distribution diff --git a/sdk/src/rpc/api.ts b/sdk/src/rpc/api.ts index 4309da4aa..775b04ccf 100644 --- a/sdk/src/rpc/api.ts +++ b/sdk/src/rpc/api.ts @@ -54,7 +54,6 @@ export async function createAuthRequestMessage( params.application ?? '', ]; const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthRequest, paramsArray, timestamp); - request.sig = ['']; return JSON.stringify(request); } diff --git a/sdk/src/rpc/nitrolite.ts b/sdk/src/rpc/nitrolite.ts index 8a6f5bd5a..e93f2fa1f 100644 --- a/sdk/src/rpc/nitrolite.ts +++ b/sdk/src/rpc/nitrolite.ts @@ -35,7 +35,7 @@ export class NitroliteRPC { timestamp: number = getCurrentTimestamp(), ): NitroliteRPCMessage { const requestData: RequestData = [requestId, method, params, timestamp]; - const message: NitroliteRPCMessage = { req: requestData }; + const message: NitroliteRPCMessage = { req: requestData, sig: [] }; return message; } @@ -230,7 +230,7 @@ export class NitroliteRPC { try { const payload = this.getMessagePayload(message); - if (typeof signature !== 'string' || signature === '') { + if (typeof signature !== 'string') { return false; } return await verifier(payload, signature as Hex, expectedSigner); diff --git a/sdk/src/rpc/parse/channel.ts b/sdk/src/rpc/parse/channel.ts index abc9edc30..5ffe9833e 100644 --- a/sdk/src/rpc/parse/channel.ts +++ b/sdk/src/rpc/parse/channel.ts @@ -17,12 +17,6 @@ const RPCAllocationSchema = z.object({ amount: z.string().transform((a) => BigInt(a)), }); -const ServerSignatureSchema = z.object({ - v: z.union([z.string(), z.number()]).transform((a) => Number(a)), - r: hexSchema, - s: hexSchema, -}); - const ResizeChannelParamsSchema = z .array( z @@ -32,8 +26,7 @@ const ResizeChannelParamsSchema = z intent: z.number(), version: z.number(), allocations: z.array(RPCAllocationSchema), - state_hash: hexSchema, - server_signature: ServerSignatureSchema, + server_signature: hexSchema, }) .transform( (raw) => @@ -47,12 +40,7 @@ const ResizeChannelParamsSchema = z token: a.token as Address, amount: a.amount, })), - stateHash: raw.state_hash as Hex, - serverSignature: { - v: +raw.server_signature.v, - r: raw.server_signature.r as Hex, - s: raw.server_signature.s as Hex, - }, + serverSignature: raw.server_signature, }) as ResizeChannelResponseParams, ), ) @@ -68,8 +56,7 @@ const CloseChannelParamsSchema = z intent: z.number(), version: z.number(), allocations: z.array(RPCAllocationSchema), - state_hash: hexSchema, - server_signature: ServerSignatureSchema, + server_signature: hexSchema, }) .transform( (raw) => @@ -83,12 +70,7 @@ const CloseChannelParamsSchema = z token: a.token as Address, amount: a.amount, })), - stateHash: raw.state_hash as Hex, - serverSignature: { - v: +raw.server_signature.v, - r: raw.server_signature.r as Hex, - s: raw.server_signature.s as Hex, - }, + serverSignature: raw.server_signature, }) as CloseChannelResponseParams, ), ) diff --git a/sdk/src/rpc/types/index.ts b/sdk/src/rpc/types/index.ts index 106d68c2d..c84491143 100644 --- a/sdk/src/rpc/types/index.ts +++ b/sdk/src/rpc/types/index.ts @@ -94,7 +94,7 @@ export interface NitroliteRPCMessage { /** Contains the response or error payload if this is a response message. */ res?: ResponsePayload; /** Optional cryptographic signature(s) for message authentication. */ - sig?: Hex[] | ['']; + sig?: Hex[]; } /** @@ -222,7 +222,7 @@ export type MessageSigner = (payload: RequestData | ResponsePayload) => Promise< /** * Defines the function signature for signing challenge state data. - * This signer is specifically used for signing state challenges in the form of keccak256(abi.encode(stateHash, 'challenge')). + * This signer is specifically used for signing state challenges in the form of keccak256(abi.encodePacked(packedState, 'challenge')). * * @param stateHash - The state hash as a Hex string * @returns A Promise that resolves to the cryptographic signature as a Hex string. diff --git a/sdk/src/rpc/types/response.ts b/sdk/src/rpc/types/response.ts index 8c07dc34b..cf9fd697a 100644 --- a/sdk/src/rpc/types/response.ts +++ b/sdk/src/rpc/types/response.ts @@ -208,12 +208,7 @@ export interface GetAppSessionsResponseParams { } export type GetAppSessionsRPCResponseParams = GetAppSessionsResponseParams; // for backward compatibility -export interface ServerSignature { - /** The recovery value of the signature. */ - v: number; - r: Hex; - s: Hex; -} +export type ServerSignature = Hex; export interface RPCAllocation { /** The destination address for the allocation. */ @@ -238,8 +233,6 @@ export interface ResizeChannelResponseParams { version: number; /** The list of allocations for the channel. */ allocations: RPCAllocation[]; - /** The hash of the channel state. */ - stateHash: Hex; /** The server's signature for the state update. */ serverSignature: ServerSignature; } @@ -259,8 +252,6 @@ export interface CloseChannelResponseParams { stateData: Hex; /** The list of final allocations for the channel. */ allocations: RPCAllocation[]; - /** The hash of the channel state. */ - stateHash: Hex; /** The server's signature for the state update. */ serverSignature: ServerSignature; } diff --git a/sdk/src/utils/state.ts b/sdk/src/utils/state.ts index 2547bcaf1..d02fabc16 100644 --- a/sdk/src/utils/state.ts +++ b/sdk/src/utils/state.ts @@ -1,18 +1,16 @@ -import { keccak256, encodeAbiParameters, Address, Hex, recoverMessageAddress, numberToHex, parseSignature } from 'viem'; +import { keccak256, encodeAbiParameters, Address, Hex, recoverMessageAddress, encodePacked } from 'viem'; import { State, StateHash, Signature, ChannelId } from '../client/types'; // Updated import path /** - * Compute the hash of a channel state in a canonical way (ignoring the signature) - * @param channelId The channelId - * @param state The state struct - * @returns The state hash as Hex + * Packs a channel state into a canonical format for hashing and signing. + * @param channelId The ID of the channel. + * @param state The state to pack. + * @returns The packed state as Hex. */ -export function getStateHash(channelId: ChannelId, state: State): StateHash { - const encoded = encodeAbiParameters( +export function getPackedState(channelId: ChannelId, state: State): Hex { + return encodeAbiParameters( [ { name: 'channelId', type: 'bytes32' }, - // For channel creation, state.version must be 0 (corresponds to INITIAL status) - // For active channels, state.version must be greater than 0 { name: 'intent', type: 'uint8', @@ -34,96 +32,99 @@ export function getStateHash(channelId: ChannelId, state: State): StateHash { ], [channelId, state.intent, state.version, state.data, state.allocations], ); +} - return keccak256(encoded); +/** + * Compute the hash of a channel state in a canonical way (ignoring the signature) + * @param channelId The channelId + * @param state The state struct + * @returns The state hash as Hex + */ +export function getStateHash(channelId: ChannelId, state: State): StateHash { + return keccak256(getPackedState(channelId, state)) as StateHash; } /** * Function type for signing messages, compatible with Viem's WalletClient or Account. - * @dev Signing should not add an EIP-191 prefix to the message. + * @dev Signing should NOT add an EIP-191 prefix to the message. * @param args An object containing the message to sign in the `{ message: { raw: Hex } }` format. * @returns A promise that resolves to the signature as a Hex string. * @throws If the signing fails. */ type SignMessageFn = (args: { message: { raw: Hex } }) => Promise; +// TODO: extract into an interface and provide on NitroliteClient creation /** - * Create a signature for a state hash using a Viem WalletClient or Account compatible signer. + * Create a raw ECDSA signature for a hash over a packed state using a Viem WalletClient or Account compatible signer. * Uses the locally defined parseSignature function. * @dev `signMessage` function should NOT add an EIP-191 prefix to the stateHash. See {@link SignMessageFn}. * @param stateHash The hash of the state to sign. * @param signer An object with a `signMessage` method compatible with Viem's interface (e.g., WalletClient, Account). - * @returns The signature object { v, r, s } + * @returns The signature over the state hash. * @throws If the signer cannot sign messages or signing/parsing fails. */ export async function signState( - stateHash: StateHash, + channelId: ChannelId, + state: State, signMessage: SignMessageFn, -): Promise<{ - r: Hex; - s: Hex; - v: number; -}> { +): Promise { + const stateHash = getStateHash(channelId, state); try { - const signatureHex = await signMessage({ message: { raw: stateHash } }); - const parsedSig = parseSignature(signatureHex); - - // Handle both legacy (27/28) and EIP-155 (0/1) v values - let v: number; - if (parsedSig.v !== undefined) { - v = Number(parsedSig.v); - } else if (parsedSig.yParity !== undefined) { - v = parsedSig.yParity + 27; - } else { - throw new Error('Invalid signature format: missing v or yParity value'); - } - - return { - r: parsedSig.r, - s: parsedSig.s, - v, - }; + return await signMessage({ message: { raw: stateHash } }); } catch (error) { console.error('Error signing state hash:', error); throw new Error(`Failed to sign state hash: ${error instanceof Error ? error.message : String(error)}`); } } -export function removeQuotesFromRS(input: Signature): Signature { - const output = { ...input }; - - if (typeof output.r === 'string') { - output.r = output.r.replace(/^"(.*)"$/, '$1') as Hex; - } +/** + * Signs a challenge state for a channel. + * This function encodes the packed state and the challenge string, hashes it, and signs it. + * @param channelId The ID of the channel. + * @param state The state to sign. + * @param signMessage The signing function compatible with Viem's WalletClient or Account. + * @returns The signature as a Hex string. + * @throws If signing fails. + */ +export async function signChallengeState( + channelId: ChannelId, + state: State, + signMessage: SignMessageFn, +): Promise { + const packedState = getPackedState(channelId, state); + const encoded = encodePacked( + [ 'bytes', 'string' ], + [packedState, 'challenge'], + ); + const challengeHash = keccak256(encoded) as Hex; - if (typeof output.s === 'string') { - output.s = output.s.replace(/^"(.*)"$/, '$1') as Hex; + try { + return await signMessage({ message: { raw: challengeHash } }); + } catch (error) { + console.error('Error signing challenge state:', error); + throw new Error(`Failed to sign challenge state: ${error instanceof Error ? error.message : String(error)}`); } - - return output; } +// TODO: extract into an interface and provide on NitroliteClient creation /** - * Verifies that a state hash was signed by the expected signer. + * Verifies a raw ECDSA signature over a hash of a packed state. * @param stateHash The hash of the state. - * @param signature The signature object { v, r, s }. + * @param signature The signature to verify. * @param expectedSigner The address of the participant expected to have signed. * @returns True if the signature is valid and recovers to the expected signer, false otherwise. */ export async function verifySignature( - stateHash: StateHash, + channelId: ChannelId, + state: State, signature: Signature, expectedSigner: Address, ): Promise { try { - // Reconstruct the flat hex signature needed by recoverMessageAddress - // Ensure v is adjusted if necessary (e.g., some signers might return 0/1 instead of 27/28) - const vNormalized = signature.v < 27 ? signature.v + 27 : signature.v; - const signatureHex = `${signature.r}${signature.s.slice(2)}${vNormalized.toString(16).padStart(2, '0')}` as Hex; - + const stateHash = getStateHash(channelId, state); const recoveredAddress = await recoverMessageAddress({ message: { raw: stateHash }, - signature: signatureHex, + signature: signature, }); return recoveredAddress.toLowerCase() === expectedSigner.toLowerCase(); diff --git a/sdk/test/unit/client/index.test.ts b/sdk/test/unit/client/index.test.ts index 12bf13bc6..b89d5c413 100644 --- a/sdk/test/unit/client/index.test.ts +++ b/sdk/test/unit/client/index.test.ts @@ -11,12 +11,9 @@ describe('NitroliteClient', () => { waitForTransactionReceipt: jest.fn(() => Promise.resolve({ status: 'success' })), } as any; const mockAccount = { address: '0x1234567890123456789012345678901234567890' as Address }; + const mockSignature = '0x' + '1234567890abcdef'.repeat(8) + '1b'; // 128 hex chars, v = 27 const mockSignMessage = jest.fn(() => - Promise.resolve( - '0x' + - '1234567890abcdef'.repeat(8) + // 128 hex chars - '1b', // v = 27 - ), + Promise.resolve(mockSignature), ); const mockWalletClient = { account: mockAccount, @@ -260,7 +257,7 @@ describe('NitroliteClient', () => { params.channelId, params.candidateState, params.proofStates, - expect.any(Object), // the signature + mockSignature, // the signature ); expect(tx).toBe('0xCHL'); }); diff --git a/sdk/test/unit/client/state.test.ts b/sdk/test/unit/client/state.test.ts index e8ac0eb7b..a4411998d 100644 --- a/sdk/test/unit/client/state.test.ts +++ b/sdk/test/unit/client/state.test.ts @@ -72,15 +72,14 @@ describe('_prepareAndSignInitialState', () => { version: 0n, sigs: ['accSig'], }); - // Hash and sign calls - expect(utils.getStateHash).toHaveBeenCalledWith('cid', { + // Signs the state + expect(utils.signState).toHaveBeenCalledWith('cid', { data: 'customData', intent: StateIntent.INITIALIZE, allocations: expect.any(Array), version: 0n, sigs: [], - }); - expect(utils.signState).toHaveBeenCalledWith('hsh', deps.stateWalletClient.signMessage); + }, deps.stateWalletClient.signMessage); }); test('throws if no adjudicator', async () => { @@ -105,7 +104,7 @@ describe('_prepareAndSignInitialState', () => { describe('_prepareAndSignFinalState', () => { let deps: any; - const serverSigRaw = '"srvSig"'; + const serverSig = 'srvSig'; const channelIdArg = 'cid' as Hex; const allocations = [{ destination: '0xA' as Hex, token: '0xT' as Hex, amount: 5n }]; const version = 7n; @@ -134,7 +133,7 @@ describe('_prepareAndSignFinalState', () => { channelId: channelIdArg, allocations, version, - serverSignature: serverSigRaw, + serverSignature: serverSig, }, }; const { finalStateWithSigs, channelId } = await _prepareAndSignFinalState(deps, params as any); @@ -148,15 +147,13 @@ describe('_prepareAndSignFinalState', () => { version, sigs: ['accSig', 'srvSig'], }); - expect(utils.getStateHash).toHaveBeenCalledWith(channelIdArg, { + expect(utils.signState).toHaveBeenCalledWith('cid', { data: 'finalData', intent: StateIntent.FINALIZE, allocations, version, sigs: [], - }); - expect(utils.signState).toHaveBeenCalledWith('hsh', deps.stateWalletClient.signMessage); - expect(utils.removeQuotesFromRS).toHaveBeenCalledWith(serverSigRaw); + }, deps.stateWalletClient.signMessage); }); test('throws if no stateData', async () => { @@ -166,7 +163,7 @@ describe('_prepareAndSignFinalState', () => { channelId: channelIdArg, allocations, version, - serverSignature: serverSigRaw, + serverSignature: serverSig, }, }; await expect(_prepareAndSignFinalState(deps, params as any)).rejects.toThrow(Errors.MissingParameterError); diff --git a/sdk/test/unit/rpc/api.test.ts b/sdk/test/unit/rpc/api.test.ts index e217746ee..650e1f712 100644 --- a/sdk/test/unit/rpc/api.test.ts +++ b/sdk/test/unit/rpc/api.test.ts @@ -67,7 +67,7 @@ describe('API message creators', () => { [clientAddress, clientAddress, 'test-app', [], '', '', clientAddress], timestamp, ], - sig: [''], + sig: [], }); }); @@ -244,7 +244,7 @@ describe('API message creators', () => { const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ req: [requestId, RPCMethod.AuthVerify, [{ jwt: jwtToken }], timestamp], - sig: undefined, + sig: [], }); }); diff --git a/sdk/test/unit/rpc/nitrolite.test.ts b/sdk/test/unit/rpc/nitrolite.test.ts index e6bac032b..3fc49b943 100644 --- a/sdk/test/unit/rpc/nitrolite.test.ts +++ b/sdk/test/unit/rpc/nitrolite.test.ts @@ -29,6 +29,7 @@ describe('NitroliteRPC', () => { expect(result).toEqual({ req: [requestId, method, params, timestamp], + sig: [], }); }); diff --git a/sdk/test/unit/utils/state.test.ts b/sdk/test/unit/utils/state.test.ts index c3f8b85dd..d66fc7795 100644 --- a/sdk/test/unit/utils/state.test.ts +++ b/sdk/test/unit/utils/state.test.ts @@ -1,12 +1,11 @@ import { describe, test, expect, jest } from '@jest/globals'; -import { getStateHash, signState, removeQuotesFromRS, verifySignature } from '../../../src/utils/state'; +import { getStateHash, signState, verifySignature } from '../../../src/utils/state'; import { type State, type Signature, type Allocation, StateIntent } from '../../../src/client/types'; -import { Hex, Address, recoverMessageAddress, parseSignature, encodeAbiParameters, keccak256 } from 'viem'; +import { Hex, Address, recoverMessageAddress, encodeAbiParameters, keccak256 } from 'viem'; jest.mock('viem', () => ({ encodeAbiParameters: jest.fn(() => '0xencoded'), keccak256: jest.fn(() => '0xhash'), - parseSignature: jest.fn(() => ({ r: '0xr', s: '0xs', v: 27 })), recoverMessageAddress: jest.fn(async () => '0xSignerAddress'), })); @@ -55,73 +54,60 @@ describe('getStateHash', () => { }); describe('signState', () => { - const fakeHash = '0xstatehash' as Hex; + const channelId = '0xChannelId' as Hex; + const state: State = { + data: '0xdata' as Hex, + intent: StateIntent.INITIALIZE, + allocations: [ + { destination: '0xA' as Address, token: '0xT' as Address, amount: 10n }, + { destination: '0xB' as Address, token: '0xT' as Address, amount: 20n }, + ] as [Allocation, Allocation], + version: 0n, + sigs: [], + }; + const stateHash = getStateHash(channelId, state); + const expectedSignature = '0xrs1b' as Hex; const signer = jest.fn(async ({ message }) => { - if (message.raw === fakeHash) return '0xr0xs1b'; + if (message.raw === stateHash) return expectedSignature; throw new Error('sign fail'); }); test('successfully signs and parses signature', async () => { // @ts-ignore - const sig = await signState(fakeHash, signer); - expect(signer).toHaveBeenCalledWith({ message: { raw: fakeHash } }); - expect(parseSignature).toHaveBeenCalledWith('0xr0xs1b'); - expect(sig).toEqual({ r: '0xr', s: '0xs', v: 27 }); - }); - - test('throws if parseSignature yields no v', async () => { - const viemMock = jest.requireMock('viem'); - // @ts-ignore - viemMock.parseSignature.mockReturnValueOnce({ r: '0xr', s: '0xs' }); - // @ts-ignore - await expect(signState(fakeHash, signer)).rejects.toThrow( - /Invalid signature format: missing v or yParity value/, - ); + const sig = await signState(channelId, state, signer); + expect(signer).toHaveBeenCalledWith({ message: { raw: stateHash } }); + expect(sig).toEqual(expectedSignature); }); test('throws on signer error', async () => { const badSigner = jest.fn(async () => { throw new Error('bad'); }); - await expect(signState(fakeHash, badSigner)).rejects.toThrow(/Failed to sign state hash: bad/); - }); -}); - -describe('removeQuotesFromRS', () => { - test('removes surrounding quotes from r and s', () => { - const input: any = { r: '"0xr"', s: '"0xs"', v: 27 }; - const out = removeQuotesFromRS(input); - expect(out).toEqual({ r: '0xr', s: '0xs', v: 27 }); - }); - test('leaves values without quotes untouched', () => { - const input: Signature = { r: '0xr', s: '0xs', v: 28 }; - expect(removeQuotesFromRS(input)).toEqual(input); + await expect(signState(channelId, state, badSigner)).rejects.toThrow(/Failed to sign state hash: bad/); }); }); describe('verifySignature', () => { - const stateHash = '0xstate' as Hex; - const signature: Signature = { r: '0xr', s: '0xs', v: 0 }; // v < 27 + const channelId = '0xChannelId' as Hex; + const state: State = { + data: '0xdata' as Hex, + intent: StateIntent.INITIALIZE, + allocations: [ + { destination: '0xA' as Address, token: '0xT' as Address, amount: 10n }, + { destination: '0xB' as Address, token: '0xT' as Address, amount: 20n }, + ] as [Allocation, Allocation], + version: 0n, + sigs: [], + }; + const stateHash = getStateHash(channelId, state); + const signature: Signature = "0xr0xs1b" as Signature; const expectedSigner = '0xSignerAddress' as Address; - test('normalizes v and recovers address', async () => { - const result = await verifySignature(stateHash, signature, expectedSigner); - // vNormalized = 0 + 27 = 27 -> hex "1b" - const sigHex = `${signature.r}${signature.s.slice(2)}1b`; - expect(recoverMessageAddress).toHaveBeenCalledWith({ - message: { raw: stateHash }, - signature: sigHex as Hex, - }); - expect(result).toBe(true); - }); - - test('handles v already >=27', async () => { - const sig2: Signature = { r: '0xr2', s: '0xs2', v: 28 }; - const result = await verifySignature(stateHash, sig2, expectedSigner); - const sigHex2 = `${sig2.r}${sig2.s.slice(2)}1c`; // 28 hex + test('recovers address', async () => { + const result = await verifySignature(channelId, state, signature, expectedSigner); expect(recoverMessageAddress).toHaveBeenCalledWith({ message: { raw: stateHash }, - signature: sigHex2 as Hex, + signature: signature as Hex, }); expect(result).toBe(true); }); @@ -130,12 +116,12 @@ describe('verifySignature', () => { const viemMock = jest.requireMock('viem'); // @ts-ignore viemMock.recoverMessageAddress.mockRejectedValueOnce(new Error('fail')); - const res = await verifySignature(stateHash, signature, expectedSigner); + const res = await verifySignature(channelId, state, signature, expectedSigner); expect(res).toBe(false); }); test('returns false on mismatched address', async () => { - const res = await verifySignature(stateHash, signature, '0xOther' as Address); + const res = await verifySignature(channelId, state, signature, '0xOther' as Address); expect(res).toBe(false); }); }); From e9578e55b8171c1326abae64e8ef1fb3d56d04bc Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Wed, 6 Aug 2025 15:34:31 +0300 Subject: [PATCH 02/18] feat: replace stateWalletClient with StateSigner (#297) --- integration/common/identity.ts | 20 ++---- integration/common/nitroliteClient.ts | 2 +- sdk/src/client/index.ts | 8 +-- sdk/src/client/prepare.ts | 3 +- sdk/src/client/signer.ts | 92 +++++++++++++++++++++++++++ sdk/src/client/state.ts | 16 ++--- sdk/src/client/types.ts | 10 +-- sdk/src/rpc/api.ts | 10 ++- sdk/src/utils/sign.ts | 9 +++ sdk/src/utils/state.ts | 67 +++++-------------- sdk/test/unit/client/index.test.ts | 8 ++- sdk/test/unit/client/state.test.ts | 60 +++++++++-------- sdk/test/unit/utils/state.test.ts | 36 +---------- 13 files changed, 185 insertions(+), 156 deletions(-) create mode 100644 sdk/src/client/signer.ts create mode 100644 sdk/src/utils/sign.ts diff --git a/integration/common/identity.ts b/integration/common/identity.ts index 28e75b8e4..4696f0330 100644 --- a/integration/common/identity.ts +++ b/integration/common/identity.ts @@ -2,10 +2,11 @@ import { Address, createWalletClient, Hex, http } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { chain } from './setup'; import { createECDSAMessageSigner } from '@erc7824/nitrolite'; +import { SessionKeyStateSigner } from '@erc7824/nitrolite/dist/client/signer'; export class Identity { public walletClient = null; - public stateWalletClient = null; + public stateSigner = null; public walletAddress: Address; public sessionAddress: Address; public messageSigner = null; @@ -20,21 +21,8 @@ export class Identity { transport: http(), }); - const sessionAccount = privateKeyToAccount(sessionPrivateKey); - this.sessionAddress = sessionAccount.address; - - this.stateWalletClient = { - ...this.walletClient, - account: { - address: this.sessionAddress, - }, - signMessage: async ({ message: { raw } }: { message: { raw: string } }) => { - const flatSignature = await sessionAccount.sign({ hash: raw as Hex }); - - return flatSignature as Hex; - }, - }; - + this.stateSigner = new SessionKeyStateSigner(sessionPrivateKey); this.messageSigner = createECDSAMessageSigner(sessionPrivateKey); + this.sessionAddress = this.stateSigner.getAddress(); } } diff --git a/integration/common/nitroliteClient.ts b/integration/common/nitroliteClient.ts index 6240e6b43..52b11494c 100644 --- a/integration/common/nitroliteClient.ts +++ b/integration/common/nitroliteClient.ts @@ -21,7 +21,7 @@ export class TestNitroliteClient extends NitroliteClient { // @ts-ignore publicClient, walletClient: identity.walletClient, - stateWalletClient: identity.stateWalletClient, + stateSigner: identity.stateSigner, account: identity.walletClient.account, chainId: chain.id, challengeDuration: BigInt(CONFIG.DEFAULT_CHALLENGE_TIMEOUT), // min diff --git a/sdk/src/client/index.ts b/sdk/src/client/index.ts index 8afe54915..0dae6edf3 100644 --- a/sdk/src/client/index.ts +++ b/sdk/src/client/index.ts @@ -21,6 +21,7 @@ import { ResizeChannelParams, State, } from './types'; +import { StateSigner } from './signer'; const CUSTODY_MIN_CHALLENGE_DURATION = 3600n; @@ -36,7 +37,7 @@ export class NitroliteClient { public readonly challengeDuration: bigint; public readonly txPreparer: NitroliteTransactionPreparer; public readonly chainId: number; - private readonly stateWalletClient: WalletClient>; + private readonly stateSigner: StateSigner; private readonly nitroliteService: NitroliteService; private readonly erc20Service: Erc20Service; private readonly sharedDeps: PreparerDependencies; @@ -57,8 +58,7 @@ export class NitroliteClient { this.publicClient = config.publicClient; this.walletClient = config.walletClient; - // Determine which wallet client to use for state signing - this.stateWalletClient = config.stateWalletClient ?? config.walletClient; + this.stateSigner = config.stateSigner; this.account = config.walletClient.account; this.addresses = config.addresses; this.challengeDuration = config.challengeDuration; @@ -79,7 +79,7 @@ export class NitroliteClient { account: this.account, walletClient: this.walletClient, challengeDuration: this.challengeDuration, - stateWalletClient: this.stateWalletClient, + stateSigner: this.stateSigner, chainId: this.chainId, }; diff --git a/sdk/src/client/prepare.ts b/sdk/src/client/prepare.ts index a0cda7c40..275f4a5fb 100644 --- a/sdk/src/client/prepare.ts +++ b/sdk/src/client/prepare.ts @@ -24,6 +24,7 @@ import { CreateChannelParams, ResizeChannelParams, } from './types'; +import { StateSigner } from './signer'; /** * Represents the data needed to construct a transaction or UserOperation call. @@ -41,7 +42,7 @@ export interface PreparerDependencies { addresses: ContractAddresses; account: ParseAccount; walletClient: WalletClient>; - stateWalletClient: WalletClient>; + stateSigner: StateSigner; challengeDuration: bigint; chainId: number; } diff --git a/sdk/src/client/signer.ts b/sdk/src/client/signer.ts new file mode 100644 index 000000000..f3189c5b9 --- /dev/null +++ b/sdk/src/client/signer.ts @@ -0,0 +1,92 @@ +import { Account, Address, Chain, Hex, ParseAccount, toHex, Transport, WalletClient } from 'viem'; +import { State } from './types'; +import { getPackedState, getStateHash } from '../utils'; +import { signRawECDSAMessage } from '../utils/sign'; +import { privateKeyToAccount } from 'viem/accounts'; + +// TODO: perhaps extend this interface with rpc signing methods and use it as universal signer interface + +/** + * Interface for signing protocol states. + * This interface is used to abstract the signing logic for state updates in the Nitrolite SDK. + * It allows for different implementations, such as using a wallet client or a session key. + * Also implementation could include data packing/encoding, which is crucial for some signatures (EIP-712, EIP-191) + */ +export interface StateSigner { + /** + * Get the address of the signer. + * @returns The address of the signer. + */ + getAddress(): Address; + /** + * Sign a state for a given channel ID. + * @param channelId The ID of the channel. + * @param state The state to sign. + * @returns A Promise that resolves to the signature as a Hex string. + */ + signState(channelId: Hex, state: State): Promise; + /** + * Sign a raw message. + * @param message The message to sign as a Hex string. + * @returns A Promise that resolves to the signature as a Hex string. + * @dev use viem's `toHex` to convert the message to Hex if needed. + */ + signRawMessage(message: Hex): Promise; +} + +/** + * Implementation of the StateSigner interface using a viem WalletClient. + * This class uses the wallet client to sign states and raw messages. + * It is suitable for use in scenarios where the wallet client is available and can sign messages, + * e.g. signing with MetaMask or other wallet providers. + */ +export class WalletStateSigner implements StateSigner { + private readonly walletClient: WalletClient>; + + constructor(walletClient: WalletClient>) { + this.walletClient = walletClient; + } + + getAddress(): Address { + return this.walletClient.account.address; + } + + async signState(channelId: Hex, state: State): Promise { + const packedState = getPackedState(channelId, state) + + return this.walletClient.signMessage({ message: { raw: packedState } }); + } + + async signRawMessage(message: Hex): Promise { + return this.walletClient.signMessage({ message: { raw: message } }); + } +} + +/** + * Implementation of the StateSigner interface using a session key. + * This class uses a session key to sign states and raw messages. + * It is suitable for scenarios where a session key is used for signing and private key could be exposed to application. + */ +export class SessionKeyStateSigner implements StateSigner { + private readonly sessionKey: Hex; + private readonly account: Account; + + constructor(sessionKey: Hex) { + this.sessionKey = sessionKey; + this.account = privateKeyToAccount(sessionKey); + } + + getAddress(): Address { + return this.account.address; + } + + async signState(channelId: Hex, state: State): Promise { + const packedState = getPackedState(channelId, state); + + return signRawECDSAMessage(packedState, this.sessionKey); + } + + async signRawMessage(message: Hex): Promise { + return signRawECDSAMessage(message, this.sessionKey); + } +} diff --git a/sdk/src/client/state.ts b/sdk/src/client/state.ts index 40878f2a7..188b22a53 100644 --- a/sdk/src/client/state.ts +++ b/sdk/src/client/state.ts @@ -1,6 +1,6 @@ -import { Address, encodeAbiParameters, encodePacked, Hex, keccak256 } from 'viem'; +import { Address, Hex } from 'viem'; import * as Errors from '../errors'; -import { generateChannelNonce, getChannelId, getPackedState, getStateHash, signChallengeState, signState } from '../utils'; +import { generateChannelNonce, getChannelId, getPackedChallengeState } from '../utils'; import { PreparerDependencies } from './prepare'; import { ChallengeChannelParams, @@ -38,7 +38,7 @@ export async function _prepareAndSignInitialState( const channelNonce = generateChannelNonce(deps.account.address); const participants: [Hex, Hex] = [deps.account.address, deps.addresses.guestAddress]; - const channelParticipants: [Hex, Hex] = [deps.stateWalletClient.account.address, deps.addresses.guestAddress]; + const channelParticipants: [Hex, Hex] = [deps.stateSigner.getAddress(), deps.addresses.guestAddress]; const adjudicatorAddress = deps.addresses.adjudicator; if (!adjudicatorAddress) { throw new Errors.MissingParameterError( @@ -77,8 +77,7 @@ export async function _prepareAndSignInitialState( sigs: [], }; - - const accountSignature = await signState(channelId, stateToSign, deps.stateWalletClient.signMessage); + const accountSignature = await deps.stateSigner.signState(channelId, stateToSign); const initialState: State = { ...stateToSign, sigs: [accountSignature], @@ -104,7 +103,8 @@ export async function _prepareAndSignChallengeState( challengerSig: Signature; }> { const { channelId, candidateState, proofStates = [] } = params; - const challengerSig = await signChallengeState(channelId, candidateState, deps.stateWalletClient.signMessage); + const challengeMsg = getPackedChallengeState(channelId, candidateState); + const challengerSig = await deps.stateSigner.signRawMessage(challengeMsg); return { channelId, candidateState, proofs: proofStates, challengerSig }; } @@ -136,7 +136,7 @@ export async function _prepareAndSignResizeState( sigs: [], }; - const accountSignature = await signState(channelId, stateToSign, deps.stateWalletClient.signMessage); + const accountSignature = await deps.stateSigner.signState(channelId, stateToSign); // Create a new state with signatures in the requested style const resizeStateWithSigs: State = { @@ -176,7 +176,7 @@ export async function _prepareAndSignFinalState( sigs: [], }; - const accountSignature = await signState(channelId, stateToSign, deps.stateWalletClient.signMessage); + const accountSignature = await deps.stateSigner.signState(channelId, stateToSign); // Create a new state with signatures in the requested style const finalStateWithSigs: State = { diff --git a/sdk/src/client/types.ts b/sdk/src/client/types.ts index 2bb5171ea..16edc1961 100644 --- a/sdk/src/client/types.ts +++ b/sdk/src/client/types.ts @@ -1,5 +1,6 @@ import { Account, Hex, PublicClient, WalletClient, Chain, Transport, ParseAccount, Address } from 'viem'; import { ContractAddresses } from '../abis'; +import { StateSigner } from './signer'; /** * Channel identifier @@ -125,14 +126,9 @@ export interface NitroliteClientConfig { walletClient: WalletClient>; /** - * Optional: A separate viem WalletClient used *only* for signing off-chain state updates (`signMessage`). - * Provide this if you want to use a different key (e.g., a "hot" key from localStorage) - * for state signing than the one used for on-chain transactions. - * If omitted, `walletClient` will be used for state signing. - * @dev Note that the client's `signMessage` function should NOT add an EIP-191 prefix to the message signed. See {@link SignMessageFn} for details. - * viem's `signMessage` can operate in `raw` mode, which suffice. + * Implementation of the StateSigner interface used for signing protocol states. */ - stateWalletClient?: WalletClient>; + stateSigner: StateSigner; /** Contract addresses required by the SDK. */ addresses: ContractAddresses; diff --git a/sdk/src/rpc/api.ts b/sdk/src/rpc/api.ts index 775b04ccf..3cde29a3b 100644 --- a/sdk/src/rpc/api.ts +++ b/sdk/src/rpc/api.ts @@ -1,4 +1,4 @@ -import { Address, Hex, keccak256, stringToBytes, WalletClient } from 'viem'; +import { Address, Hex, keccak256, stringToBytes, toHex, WalletClient } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { MessageSigner, @@ -28,6 +28,7 @@ import { GetLedgerTransactionsRequestParams, TransferRequestParams, } from './types/request'; +import { signRawECDSAMessage } from '../utils/sign'; /** * Creates the signed, stringified message body for an 'auth_request'. @@ -642,12 +643,9 @@ export function createEIP712AuthMessageSigner( export function createECDSAMessageSigner(privateKey: Hex): MessageSigner { return async (payload: RequestData | ResponsePayload): Promise => { try { - const messageBytes = keccak256( - stringToBytes(JSON.stringify(payload, (_, v) => (typeof v === 'bigint' ? v.toString() : v))), - ); - const flatSignature = await privateKeyToAccount(privateKey).sign({ hash: messageBytes }); + const message = toHex(JSON.stringify(payload, (_, v) => (typeof v === 'bigint' ? v.toString() : v))); - return flatSignature as Hex; + return signRawECDSAMessage(message, privateKey); } catch (error) { console.error('ECDSA signing failed:', error); throw new Error(`ECDSA signing failed: ${error}`); diff --git a/sdk/src/utils/sign.ts b/sdk/src/utils/sign.ts new file mode 100644 index 000000000..4065dedd7 --- /dev/null +++ b/sdk/src/utils/sign.ts @@ -0,0 +1,9 @@ +import { Hex, keccak256 } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +export const signRawECDSAMessage = async (message: Hex, privateKey: Hex): Promise => { + const hash = keccak256(message); + const flatSignature = await privateKeyToAccount(privateKey).sign({ hash }); + + return flatSignature; +}; diff --git a/sdk/src/utils/state.ts b/sdk/src/utils/state.ts index d02fabc16..db586b21c 100644 --- a/sdk/src/utils/state.ts +++ b/sdk/src/utils/state.ts @@ -45,65 +45,28 @@ export function getStateHash(channelId: ChannelId, state: State): StateHash { } /** - * Function type for signing messages, compatible with Viem's WalletClient or Account. - * @dev Signing should NOT add an EIP-191 prefix to the message. - * @param args An object containing the message to sign in the `{ message: { raw: Hex } }` format. - * @returns A promise that resolves to the signature as a Hex string. - * @throws If the signing fails. + * Get a packed challenge state for a channel. + * This function encodes the packed state and the challenge string.ß + * @param channelId The ID of the channel. + * @param state The state to calculate with. + * @returns The encoded and packed challenge state as a Hex string. */ -type SignMessageFn = (args: { message: { raw: Hex } }) => Promise; +export function getPackedChallengeState(channelId: ChannelId, state: State): Hex { + const packedState = getPackedState(channelId, state); + const encoded = encodePacked(['bytes', 'string'], [packedState, 'challenge']); -// TODO: extract into an interface and provide on NitroliteClient creation -/** - * Create a raw ECDSA signature for a hash over a packed state using a Viem WalletClient or Account compatible signer. - * Uses the locally defined parseSignature function. - * @dev `signMessage` function should NOT add an EIP-191 prefix to the stateHash. See {@link SignMessageFn}. - * @param stateHash The hash of the state to sign. - * @param signer An object with a `signMessage` method compatible with Viem's interface (e.g., WalletClient, Account). - * @returns The signature over the state hash. - * @throws If the signer cannot sign messages or signing/parsing fails. - */ -export async function signState( - channelId: ChannelId, - state: State, - signMessage: SignMessageFn, -): Promise { - const stateHash = getStateHash(channelId, state); - try { - return await signMessage({ message: { raw: stateHash } }); - } catch (error) { - console.error('Error signing state hash:', error); - throw new Error(`Failed to sign state hash: ${error instanceof Error ? error.message : String(error)}`); - } + return encoded; } /** - * Signs a challenge state for a channel. - * This function encodes the packed state and the challenge string, hashes it, and signs it. + * Calculate a challenge state for a channel. + * This function encodes the packed state and the challenge string and hashes it * @param channelId The ID of the channel. - * @param state The state to sign. - * @param signMessage The signing function compatible with Viem's WalletClient or Account. - * @returns The signature as a Hex string. - * @throws If signing fails. + * @param state The state to calculate with. + * @returns The challenge hash as a Hex string. */ -export async function signChallengeState( - channelId: ChannelId, - state: State, - signMessage: SignMessageFn, -): Promise { - const packedState = getPackedState(channelId, state); - const encoded = encodePacked( - [ 'bytes', 'string' ], - [packedState, 'challenge'], - ); - const challengeHash = keccak256(encoded) as Hex; - - try { - return await signMessage({ message: { raw: challengeHash } }); - } catch (error) { - console.error('Error signing challenge state:', error); - throw new Error(`Failed to sign challenge state: ${error instanceof Error ? error.message : String(error)}`); - } +export function getChallengeHash(channelId: ChannelId, state: State): Hex { + return keccak256(getPackedChallengeState(channelId, state)); } // TODO: extract into an interface and provide on NitroliteClient creation diff --git a/sdk/test/unit/client/index.test.ts b/sdk/test/unit/client/index.test.ts index b89d5c413..06d2d31e0 100644 --- a/sdk/test/unit/client/index.test.ts +++ b/sdk/test/unit/client/index.test.ts @@ -31,6 +31,12 @@ describe('NitroliteClient', () => { let mockNitroService: any; let mockErc20Service: any; + const stateSigner = { + getAddress: jest.fn(() => mockAccount.address), + signState: jest.fn(async (_1: Hex, _2: any) => mockSignature as Hex), + signRawMessage: jest.fn(async (_: Hex) => mockSignature as Hex), + } + beforeEach(() => { jest.restoreAllMocks(); client = new NitroliteClient({ @@ -39,7 +45,7 @@ describe('NitroliteClient', () => { addresses: mockAddresses, challengeDuration, chainId: chainId, - stateWalletClient: { ...mockWalletClient, signMessage: mockSignMessage }, + stateSigner, }); mockNitroService = { deposit: jest.fn(), diff --git a/sdk/test/unit/client/state.test.ts b/sdk/test/unit/client/state.test.ts index a4411998d..02d22cc94 100644 --- a/sdk/test/unit/client/state.test.ts +++ b/sdk/test/unit/client/state.test.ts @@ -6,7 +6,7 @@ import { Hex } from 'viem'; import { _prepareAndSignInitialState, _prepareAndSignFinalState } from '../../../src/client/state'; import * as utils from '../../../src/utils'; import { Errors } from '../../../src/errors'; -import { StateIntent } from '../../../src/client/types'; +import { State, StateIntent } from '../../../src/client/types'; // Mock utils jest.mock('../../../src/utils', () => ({ @@ -19,19 +19,21 @@ jest.mock('../../../src/utils', () => ({ })); describe('_prepareAndSignInitialState', () => { - let deps: any; + let deps; const guestAddress = '0xGUEST' as Hex; const tokenAddress = '0xTOKEN' as Hex; const adjudicatorAddress = '0xADJ' as Hex; - const challengeDuration = 123; + const challengeDuration = BigInt(123); + const stateSigner = { + getAddress: jest.fn(() => '0xOWNER' as Hex), + signState: jest.fn(async (_1: Hex, _2: State) => 'accSig'), + signRawMessage: jest.fn(async (_: Hex) => 'accSig'), + }; beforeEach(() => { deps = { account: { address: '0xOWNER' as Hex }, - stateWalletClient: { - account: { address: '0xOWNER' as Hex }, - signMessage: async (_: string) => 'walletSig', - }, + stateSigner, addresses: { guestAddress, adjudicator: adjudicatorAddress, @@ -73,13 +75,16 @@ describe('_prepareAndSignInitialState', () => { sigs: ['accSig'], }); // Signs the state - expect(utils.signState).toHaveBeenCalledWith('cid', { - data: 'customData', - intent: StateIntent.INITIALIZE, - allocations: expect.any(Array), - version: 0n, - sigs: [], - }, deps.stateWalletClient.signMessage); + expect(stateSigner.signState).toHaveBeenCalledWith( + 'cid', + { + data: 'customData', + intent: StateIntent.INITIALIZE, + allocations: expect.any(Array), + version: 0n, + sigs: [], + } + ); }); test('throws if no adjudicator', async () => { @@ -108,13 +113,15 @@ describe('_prepareAndSignFinalState', () => { const channelIdArg = 'cid' as Hex; const allocations = [{ destination: '0xA' as Hex, token: '0xT' as Hex, amount: 5n }]; const version = 7n; + const stateSigner = { + getAddress: jest.fn(async () => '0xOWNER' as Hex), + signState: jest.fn(async (_1: Hex, _2: State) => 'accSig'), + signRawMessage: jest.fn(async (_: Hex) => 'accSig'), + }; beforeEach(() => { deps = { - stateWalletClient: { - account: { address: '0xOWNER' as Hex }, - signMessage: async (_: string) => 'walletSig2', - }, + stateSigner, addresses: { /* not used */ }, @@ -147,13 +154,16 @@ describe('_prepareAndSignFinalState', () => { version, sigs: ['accSig', 'srvSig'], }); - expect(utils.signState).toHaveBeenCalledWith('cid', { - data: 'finalData', - intent: StateIntent.FINALIZE, - allocations, - version, - sigs: [], - }, deps.stateWalletClient.signMessage); + expect(stateSigner.signState).toHaveBeenCalledWith( + 'cid', + { + data: 'finalData', + intent: StateIntent.FINALIZE, + allocations, + version, + sigs: [], + } + ); }); test('throws if no stateData', async () => { diff --git a/sdk/test/unit/utils/state.test.ts b/sdk/test/unit/utils/state.test.ts index d66fc7795..9e05aa11f 100644 --- a/sdk/test/unit/utils/state.test.ts +++ b/sdk/test/unit/utils/state.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, jest } from '@jest/globals'; -import { getStateHash, signState, verifySignature } from '../../../src/utils/state'; +import { getStateHash, verifySignature } from '../../../src/utils/state'; import { type State, type Signature, type Allocation, StateIntent } from '../../../src/client/types'; import { Hex, Address, recoverMessageAddress, encodeAbiParameters, keccak256 } from 'viem'; @@ -53,40 +53,6 @@ describe('getStateHash', () => { }); }); -describe('signState', () => { - const channelId = '0xChannelId' as Hex; - const state: State = { - data: '0xdata' as Hex, - intent: StateIntent.INITIALIZE, - allocations: [ - { destination: '0xA' as Address, token: '0xT' as Address, amount: 10n }, - { destination: '0xB' as Address, token: '0xT' as Address, amount: 20n }, - ] as [Allocation, Allocation], - version: 0n, - sigs: [], - }; - const stateHash = getStateHash(channelId, state); - const expectedSignature = '0xrs1b' as Hex; - const signer = jest.fn(async ({ message }) => { - if (message.raw === stateHash) return expectedSignature; - throw new Error('sign fail'); - }); - - test('successfully signs and parses signature', async () => { - // @ts-ignore - const sig = await signState(channelId, state, signer); - expect(signer).toHaveBeenCalledWith({ message: { raw: stateHash } }); - expect(sig).toEqual(expectedSignature); - }); - - test('throws on signer error', async () => { - const badSigner = jest.fn(async () => { - throw new Error('bad'); - }); - await expect(signState(channelId, state, badSigner)).rejects.toThrow(/Failed to sign state hash: bad/); - }); -}); - describe('verifySignature', () => { const channelId = '0xChannelId' as Hex; const state: State = { From b71fe644194b547dbaba585a678a5ac22fb70a3f Mon Sep 17 00:00:00 2001 From: Dmytro Steblyna <80773046+dimast-x@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:49:53 +0100 Subject: [PATCH 03/18] feat: redesign and improve joining channels by broker, refactor endpoints (#290) * feat: custody updates feat(custody): add EIP-191, EIP-712 signature support (#257) * feat(custody): add EIP-191, EIP-712 support * feat(custody): correct EIP-712 sig impl * feat(utils): add POLA principle comment * feat(contract): add EIP712AdjudicatorBase * feat(contract): integrate EIP712AdjudicatorBase to supported adjs * fix(contract): tests with updated adjs * test(contract): EIP712AdjudicatorBase * test(contract): SimpleConsensus EIP191, EIP712 sigs * test(contract): Utils signatures * test(contract): Custody, SimpleConsensus integration tests for sigs refactor(contract): migrate sig to bytes (#279) * refactor(Types): replace Signature{v,r,s} with bytes * style(contract): add empty lines at the end refactor(clearnode): replace Signature{r,s,v} with []byte (#283) refactor(sdk): migrate `Signature{r,s,v}` to `Hex` (#284) * refactor(sdk): replace Signature{r,s,v} with Hex * fix(NitroliteService): perform convertStateFromContract correctly * fix(rpc): expect updated signature type from rpc * style(sdk/rpc): remove ServerSignatureSchema var * feat(sdk/rpc): add empty signatures array by default * feat(sdk): add build:force command * feat(sdk): remove npm build:force refactor(clearnode): define `Signature []byte` type (#287) * refactor(clearnode): define `Signature []byte` type * refactor(clearnode::RPCEntry): replace sig `[]string` with `[]Signature` * feat(clearnode/nitrolite): add Sigs2Strings, Strings2Sigs helpers fix(integration): tests sig migration (#289) * feat(docker-compose): add ability to pass logger visibility to clearnode * fix(clearnode): raw ECDSA sign and verify * fix(clearnode): try to extract req id on ummarshal error * fix(integration): update sig to new type * feat(sdk): change type of sig array to contain only Hex * refactor(clearnode): remove check on nil Req, zero-init instead * fix(clearnode:nitrolite): remove unintended side-effect of sig param modif feat(custody): add note about ephemeral final state style(contract): run forge fmt fix(clearnode/docs): remove legacy signature format feat(custody): eip-1271, eip-6492 sigs support (#293) * feat(contract): add ERC-1271,6492 support * test(contract/Utils.sol): add ERC-1271, 6492 tests * refactor(contract:Utils.t.sol): separate tests into contracts * refactor(UtilsHarness): do NOT expose constants * fix(contracts/adjudicators): use verifyStateSignature instead of just EOA sig * test(contract/Custody): add ERC-1271, 6492 sig to integration test * test(contract/Custody): add challenge with EIP-712, EIP-1271 tests * refactor(contract/Utils): reorder recoverStateEIP712Signer params for consistency * test(contract): remove console.logs * feat(contract): clarify Utils comments * refactor(contract): optimize Custody and Utils sig verification functions * style(contract): run forge fmt feat(custody): add ability to create channel w/ server sig feat(contract/Custody): add ability to broker deposit in create * feat: clearnode updates feat: improve and simplify channel joining by broker drop join by broker feat: add create channel endpoint draft feat: use proper state format feat: code improvements and fix tests add doc feat: use state as encode state arg address comments feat: add tests for request create method update docs feat: refactor channel service tests rename encode state into encode state data feat: update response feat: unify channel operation responses feat: alling app session service structure to channel service update docs improve errors consistency * feat(rpc): add create_channel method and unify channel response types (#296) * feat: remove state hash from channel operation response * update erc-7824 docs * feat: include channel in channel operation response * fix: use chain id as key in network config * fix(Custody::join): allow 2 signatures * feat: add an optional session key for create_channel request * return state data as an encoded string * fix(Custody::join): add Server wallet * test(custody::join): broker auto-join tests (#303) * test(Custody::join): broker auto-join * refactor(contract/test): signState usage * refactor(Custody.t.sol): enhance broker auto join test name readability * feat: add sdk and integration support for simplified join (#304) --------- Co-authored-by: Sazonov Nikita <35502225+nksazonov@users.noreply.github.com> Co-authored-by: Max Pushkarov Co-authored-by: nksazonov Co-authored-by: MaxMoskalenko --- clearnode/app_session_service.go | 55 +- clearnode/app_session_service_test.go | 20 +- clearnode/channel.go | 7 +- clearnode/channel_service.go | 259 +++++--- clearnode/channel_service_test.go | 672 +++++++++----------- clearnode/config.go | 6 +- clearnode/custody.go | 148 +---- clearnode/custody_test.go | 182 +----- clearnode/docs/API.md | 143 +++-- clearnode/eth_listener.go | 5 - clearnode/main.go | 11 +- clearnode/metrics.go | 2 +- clearnode/nitrolite/state.go | 7 +- clearnode/reconcile_cli.go | 11 +- clearnode/rpc_router.go | 1 + clearnode/rpc_router_private.go | 121 ++-- clearnode/rpc_router_private_test.go | 299 +++++++-- clearnode/rpc_router_public.go | 4 +- clearnode/rpc_router_public_test.go | 52 +- clearnode/rpc_router_test.go | 20 +- contract/src/Custody.sol | 22 +- contract/test/Custody.t.sol | 310 ++++++--- contract/test/Custody_integration.t.sol | 63 +- contract/test/TestUtils.sol | 15 +- erc7824-docs/docs/erc-7824.md | 38 +- integration/common/nitroliteClient.ts | 54 +- integration/common/ws.ts | 6 + integration/tests/challenge_channel.test.ts | 154 ----- integration/tests/close_channel.test.ts | 10 +- integration/tests/create_channel.test.ts | 60 +- integration/tests/lifecycle.test.ts | 40 +- integration/tests/resize_channel.test.ts | 70 +- sdk/src/client/index.ts | 11 +- sdk/src/client/prepare.ts | 13 +- sdk/src/client/signer.ts | 4 +- sdk/src/client/state.ts | 76 +-- sdk/src/client/types.ts | 7 +- sdk/src/rpc/api.ts | 12 + sdk/src/rpc/parse/channel.ts | 107 ++-- sdk/src/rpc/parse/parse.ts | 1 + sdk/src/rpc/types/index.ts | 2 +- sdk/src/rpc/types/request.ts | 25 + sdk/src/rpc/types/response.ts | 65 +- sdk/src/utils/channel.ts | 26 +- sdk/src/utils/state.ts | 6 +- sdk/test/unit/client/index.test.ts | 45 +- sdk/test/unit/client/prepare.test.ts | 71 ++- sdk/test/unit/client/state.test.ts | 72 ++- 48 files changed, 1830 insertions(+), 1580 deletions(-) delete mode 100644 integration/tests/challenge_channel.test.ts diff --git a/clearnode/app_session_service.go b/clearnode/app_session_service.go index 869685a29..a09045633 100644 --- a/clearnode/app_session_service.go +++ b/clearnode/app_session_service.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/json" - "errors" "fmt" "github.com/ethereum/go-ethereum/common" @@ -23,21 +22,21 @@ func NewAppSessionService(db *gorm.DB, wsNotifier *WSNotifier) *AppSessionServic return &AppSessionService{db: db, wsNotifier: wsNotifier} } -func (s *AppSessionService) CreateApplication(params *CreateAppSessionParams, rpcSigners map[string]struct{}) (*AppSession, error) { +func (s *AppSessionService) CreateApplication(params *CreateAppSessionParams, rpcSigners map[string]struct{}) (AppSessionResponse, error) { if len(params.Definition.ParticipantWallets) < 2 { - return nil, RPCErrorf("invalid number of participants") + return AppSessionResponse{}, RPCErrorf("invalid number of participants") } if len(params.Definition.Weights) != len(params.Definition.ParticipantWallets) { - return nil, RPCErrorf("number of weights must be equal to participants") + return AppSessionResponse{}, RPCErrorf("number of weights must be equal to participants") } if params.Definition.Nonce == 0 { - return nil, RPCErrorf("nonce is zero or not provided") + return AppSessionResponse{}, RPCErrorf("nonce is zero or not provided") } // Generate a unique ID for the virtual application appBytes, err := json.Marshal(params.Definition) if err != nil { - return nil, RPCErrorf("failed to generate app session ID") + return AppSessionResponse{}, RPCErrorf("failed to generate app session ID") } appSessionID := crypto.Keccak256Hash(appBytes).Hex() sessionAccountID := NewAccountID(appSessionID) @@ -82,7 +81,7 @@ func (s *AppSessionService) CreateApplication(params *CreateAppSessionParams, rp } _, err = RecordLedgerTransaction(tx, TransactionTypeAppDeposit, userAccountID, sessionAccountID, alloc.AssetSymbol, alloc.Amount) if err != nil { - return fmt.Errorf("failed to record transaction: %w", err) + return RPCErrorf("failed to record transaction: %w", err) } participants[walletAddress] = true } @@ -106,17 +105,21 @@ func (s *AppSessionService) CreateApplication(params *CreateAppSessionParams, rp }) if err != nil { - return nil, err + return AppSessionResponse{}, err } for participant := range participants { s.wsNotifier.Notify(NewBalanceNotification(participant, s.db)) } - return &AppSession{SessionID: appSessionID, Version: 1, Status: ChannelStatusOpen}, nil + return AppSessionResponse{ + AppSessionID: appSessionID, + Version: 1, + Status: string(ChannelStatusOpen), + }, nil } -func (s *AppSessionService) SubmitAppState(params *SubmitAppStateParams, rpcSigners map[string]struct{}) (uint64, error) { +func (s *AppSessionService) SubmitAppState(params *SubmitAppStateParams, rpcSigners map[string]struct{}) (AppSessionResponse, error) { var newVersion uint64 err := s.db.Transaction(func(tx *gorm.DB) error { appSession, participantWeights, err := verifyQuorum(tx, params.AppSessionID, rpcSigners) @@ -133,7 +136,7 @@ func (s *AppSessionService) SubmitAppState(params *SubmitAppStateParams, rpcSign allocationSum := map[string]decimal.Decimal{} for _, alloc := range params.Allocations { if alloc.Amount.IsNegative() { - return fmt.Errorf("negative allocation: %s for asset %s", alloc.Amount, alloc.AssetSymbol) + return RPCErrorf("negative allocation: %s for asset %s", alloc.Amount, alloc.AssetSymbol) } walletAddress := GetWalletBySigner(alloc.ParticipantWallet) @@ -149,7 +152,7 @@ func (s *AppSessionService) SubmitAppState(params *SubmitAppStateParams, rpcSign ledger := GetWalletLedger(tx, userAddress) balance, err := ledger.Balance(sessionAccountID, alloc.AssetSymbol) if err != nil { - return RPCErrorf("failed to get session balance") + return RPCErrorf("failed to get session balance for asset %s", alloc.AssetSymbol) } // Reset participant allocation in app session to the new amount @@ -181,16 +184,20 @@ func (s *AppSessionService) SubmitAppState(params *SubmitAppStateParams, rpcSign }) if err != nil { - return 0, err + return AppSessionResponse{}, err } - return newVersion, nil + return AppSessionResponse{ + AppSessionID: params.AppSessionID, + Version: newVersion, + Status: string(ChannelStatusOpen), + }, nil } // CloseApplication closes a virtual app session and redistributes funds to participants -func (s *AppSessionService) CloseApplication(params *CloseAppSessionParams, rpcSigners map[string]struct{}) (uint64, error) { +func (s *AppSessionService) CloseApplication(params *CloseAppSessionParams, rpcSigners map[string]struct{}) (AppSessionResponse, error) { if params.AppSessionID == "" || len(params.Allocations) == 0 { - return 0, errors.New("missing required parameters: app_id or allocations") + return AppSessionResponse{}, RPCErrorf("missing required parameters: app_id or allocations") } participants := make(map[string]bool) @@ -210,7 +217,7 @@ func (s *AppSessionService) CloseApplication(params *CloseAppSessionParams, rpcS allocationSum := map[string]decimal.Decimal{} for _, alloc := range params.Allocations { if alloc.Amount.IsNegative() { - return fmt.Errorf("negative allocation: %s for asset %s", alloc.Amount, alloc.AssetSymbol) + return RPCErrorf("negative allocation: %s for asset %s", alloc.Amount, alloc.AssetSymbol) } walletAddress := GetWalletBySigner(alloc.ParticipantWallet) @@ -227,7 +234,7 @@ func (s *AppSessionService) CloseApplication(params *CloseAppSessionParams, rpcS ledger := GetWalletLedger(tx, userAddress) balance, err := ledger.Balance(sessionAccountID, alloc.AssetSymbol) if err != nil { - return RPCErrorf("failed to get session balance") + return RPCErrorf("failed to get session balance for asset %s", alloc.AssetSymbol) } // Debit session, credit participant @@ -239,7 +246,7 @@ func (s *AppSessionService) CloseApplication(params *CloseAppSessionParams, rpcS } _, err = RecordLedgerTransaction(tx, TransactionTypeAppWithdrawal, sessionAccountID, userAccountID, alloc.AssetSymbol, alloc.Amount) if err != nil { - return fmt.Errorf("failed to record transaction: %w", err) + return RPCErrorf("failed to record transaction: %w", err) } if !alloc.Amount.IsZero() { @@ -265,17 +272,21 @@ func (s *AppSessionService) CloseApplication(params *CloseAppSessionParams, rpcS }) if err != nil { - return 0, err + return AppSessionResponse{}, err } for participant := range participants { s.wsNotifier.Notify(NewBalanceNotification(participant, s.db)) } - return newVersion, nil + return AppSessionResponse{ + AppSessionID: params.AppSessionID, + Version: newVersion, + Status: string(ChannelStatusClosed), + }, nil } -// getAppSessions finds all app sessions +// GetAppSessions finds all app sessions // If participantWallet is specified, it returns only sessions for that participant // If participantWallet is empty, it returns all sessions func (s *AppSessionService) GetAppSessions(participantWallet string, status string, options *ListOptions) ([]AppSession, error) { diff --git a/clearnode/app_session_service_test.go b/clearnode/app_session_service_test.go index abf871ea2..f8a4f9f6a 100644 --- a/clearnode/app_session_service_test.go +++ b/clearnode/app_session_service_test.go @@ -67,11 +67,11 @@ func TestAppSessionService_CreateApplication(t *testing.T) { appSession, err := service.CreateApplication(params, rpcSigners) require.NoError(t, err) assert.NotNil(t, appSession) - assert.NotEmpty(t, appSession.SessionID) + assert.NotEmpty(t, appSession.AppSessionID) assert.Equal(t, uint64(1), appSession.Version) - assert.Equal(t, ChannelStatusOpen, appSession.Status) + assert.Equal(t, ChannelStatusOpen, ChannelStatus(appSession.Status)) - sessionAccountID := NewAccountID(appSession.SessionID) + sessionAccountID := NewAccountID(appSession.AppSessionID) assert.Len(t, capturedNotifications, 2) assertNotifications(t, capturedNotifications, userAddressA.Hex(), 1) @@ -103,7 +103,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { expectedAmount, exists := expectedTxs[tx.FromAccount] assert.True(t, exists, "Unexpected destination of a transaction: %s", tx.FromAccount) assert.Equal(t, TransactionTypeAppDeposit, tx.Type, "Transaction type should be app deposit") - assert.Equal(t, appSession.SessionID, tx.ToAccount, "To account should be app session ID") + assert.Equal(t, appSession.AppSessionID, tx.ToAccount, "To account should be app session ID") assert.Equal(t, "usdc", tx.AssetSymbol, "Asset symbol should be usdc") assert.Equal(t, expectedAmount, tx.Amount, "Amount should match allocation") assert.False(t, tx.CreatedAt.IsZero(), "CreatedAt should be set") @@ -240,9 +240,9 @@ func TestAppSessionService_SubmitAppState(t *testing.T) { userAddressB.Hex(): {}, } - newVersion, err := service.SubmitAppState(params, rpcSigners) + resp, err := service.SubmitAppState(params, rpcSigners) require.NoError(t, err) - assert.Equal(t, uint64(2), newVersion) + assert.Equal(t, uint64(2), resp.Version) // Verify balances appBalA, err := ledgerA.Balance(sessionAccountID, "usdc") @@ -345,9 +345,9 @@ func TestAppSessionService_CloseApplication(t *testing.T) { userAddressB.Hex(): {}, } - newVersion, err := service.CloseApplication(params, rpcSigners) + resp, err := service.CloseApplication(params, rpcSigners) require.NoError(t, err) - assert.Equal(t, uint64(2), newVersion) + assert.Equal(t, uint64(2), resp.Version) assert.Len(t, capturedNotifications, 2) assertNotifications(t, capturedNotifications, userAddressA.Hex(), 1) @@ -433,9 +433,9 @@ func TestAppSessionService_CloseApplication(t *testing.T) { userAddressB.Hex(): {}, } - newVersion, err := service.CloseApplication(params, rpcSigners) + resp, err := service.CloseApplication(params, rpcSigners) require.NoError(t, err) - assert.Equal(t, uint64(2), newVersion) + assert.Equal(t, uint64(2), resp.Version) var closedSession AppSession require.NoError(t, db.First(&closedSession, "session_id = ?", session.SessionID).Error) diff --git a/clearnode/channel.go b/clearnode/channel.go index c88796ebb..5abf5ebc2 100644 --- a/clearnode/channel.go +++ b/clearnode/channel.go @@ -13,7 +13,6 @@ import ( type ChannelStatus string var ( - ChannelStatusJoining ChannelStatus = "joining" ChannelStatusOpen ChannelStatus = "open" ChannelStatusClosed ChannelStatus = "closed" ChannelStatusChallenged ChannelStatus = "challenged" @@ -44,15 +43,13 @@ func (Channel) TableName() string { } // CreateChannel creates a new channel in the database -// For real channels, participantB is always the broker application func CreateChannel(tx *gorm.DB, channelID, wallet, participantSigner string, nonce uint64, challenge uint64, adjudicator string, chainID uint32, tokenAddress string, amount decimal.Decimal) (Channel, error) { - channel := Channel{ ChannelID: channelID, Wallet: wallet, Participant: participantSigner, - ChainID: chainID, // Set the network ID for channels - Status: ChannelStatusJoining, + ChainID: chainID, + Status: ChannelStatusOpen, Nonce: nonce, Adjudicator: adjudicator, Challenge: challenge, diff --git a/clearnode/channel_service.go b/clearnode/channel_service.go index 01622100c..edaee92fa 100644 --- a/clearnode/channel_service.go +++ b/clearnode/channel_service.go @@ -2,6 +2,7 @@ package main import ( "math/big" + "time" "github.com/erc7824/nitrolite/clearnode/nitrolite" "github.com/ethereum/go-ethereum/accounts/abi" @@ -11,45 +12,132 @@ import ( "gorm.io/gorm" ) -// ChannelService handles the business logic for funding channels. +// ChannelService handles the business logic for channel operations. type ChannelService struct { - db *gorm.DB - signer *Signer + db *gorm.DB + networks map[uint32]*NetworkConfig + signer *Signer } -// NewAppSessionService creates a new AppSessionService. -func NewChannelService(db *gorm.DB, signer *Signer) *ChannelService { - return &ChannelService{db: db, signer: signer} +// NewChannelService creates a new ChannelService. +func NewChannelService(db *gorm.DB, networks map[uint32]*NetworkConfig, signer *Signer) *ChannelService { + return &ChannelService{db: db, networks: networks, signer: signer} } -func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParams, rpcSigners map[string]struct{}) (ResizeChannelResponse, error) { +func (s *ChannelService) RequestCreate(wallet common.Address, params *CreateChannelParams, rpcSigners map[string]struct{}, logger Logger) (ChannelOperationResponse, error) { + _, ok := rpcSigners[wallet.Hex()] + if !ok { + return ChannelOperationResponse{}, RPCErrorf("invalid signature") + } + + existingOpenChannel, err := CheckExistingChannels(s.db, wallet.Hex(), params.Token, params.ChainID) + if err != nil { + return ChannelOperationResponse{}, RPCErrorf("failed to check existing channels") + } + if existingOpenChannel != nil { + return ChannelOperationResponse{}, RPCErrorf("an open channel with broker already exists: %s", existingOpenChannel.ChannelID) + } + + if _, err := GetAssetByToken(s.db, params.Token, params.ChainID); err != nil { + return ChannelOperationResponse{}, RPCErrorf("token not supported: %s", params.Token) + } + + allocations := []nitrolite.Allocation{ + { + Destination: wallet, + Token: common.HexToAddress(params.Token), + Amount: params.Amount.BigInt(), + }, + { + Destination: s.signer.GetAddress(), + Token: common.HexToAddress(params.Token), + Amount: big.NewInt(0), + }, + } + + networkConfig, ok := s.networks[params.ChainID] + if !ok { + return ChannelOperationResponse{}, RPCErrorf("unsupported chain ID: %d", params.ChainID) + } + + userParticipant := wallet + if params.SessionKey != nil { + sessionKeyAddress := common.HexToAddress(*params.SessionKey) + if sessionKeyAddress == wallet { + return ChannelOperationResponse{}, RPCErrorf("session key cannot be the same as the wallet address") + } + userParticipant = sessionKeyAddress + } + + channel := nitrolite.Channel{ + Participants: []common.Address{userParticipant, s.signer.GetAddress()}, + Adjudicator: common.HexToAddress(networkConfig.AdjudicatorAddress), + Challenge: 3600, + Nonce: uint64(time.Now().UnixMilli()), + } + + channelIDHash, err := nitrolite.GetChannelID(channel, params.ChainID) + if err != nil { + logger.Error("failed to get channel ID", "error", err) + return ChannelOperationResponse{}, RPCErrorf("failed to get channel ID") + } + + stateDataHex := "0x" + stateDataBytes, err := hexutil.Decode(stateDataHex) + if err != nil { + logger.Error("failed to decode state data hex", "error", err) + return ChannelOperationResponse{}, RPCErrorf("failed to decode state data hex") + } + + state := nitrolite.State{ + Intent: uint8(nitrolite.IntentINITIALIZE), + Version: big.NewInt(0), + Data: stateDataBytes, + Allocations: allocations, + } + + packedState, err := nitrolite.PackState(channelIDHash, state) + if err != nil { + logger.Error("failed to pack state", "error", err) + return ChannelOperationResponse{}, RPCErrorf("failed to pack state") + } + sig, err := s.signer.Sign(packedState) + if err != nil { + logger.Error("failed to sign state", "error", err) + return ChannelOperationResponse{}, RPCErrorf("failed to sign state") + } + + return createChannelOperationResponse(channelIDHash.Hex(), state, &channel, sig), nil +} + +func (s *ChannelService) RequestResize(params *ResizeChannelParams, rpcSigners map[string]struct{}, logger Logger) (ChannelOperationResponse, error) { channel, err := GetChannelByID(s.db, params.ChannelID) if err != nil { logger.Error("failed to find channel", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to find channel: %s", params.ChannelID) + return ChannelOperationResponse{}, RPCErrorf("failed to find channel: %s", params.ChannelID) } if channel == nil { - return ResizeChannelResponse{}, RPCErrorf("channel %s not found", params.ChannelID) + return ChannelOperationResponse{}, RPCErrorf("channel %s not found", params.ChannelID) } if err = checkChallengedChannels(s.db, channel.Wallet); err != nil { logger.Error("failed to check challenged channels", "error", err) - return ResizeChannelResponse{}, err + return ChannelOperationResponse{}, err } if channel.Status != ChannelStatusOpen { - return ResizeChannelResponse{}, RPCErrorf("channel %s is not open: %s", params.ChannelID, channel.Status) + return ChannelOperationResponse{}, RPCErrorf("channel %s is not open: %s", params.ChannelID, channel.Status) } _, ok := rpcSigners[channel.Wallet] if !ok { - return ResizeChannelResponse{}, RPCErrorf("invalid signature") + return ChannelOperationResponse{}, RPCErrorf("invalid signature") } asset, err := GetAssetByToken(s.db, channel.Token, channel.ChainID) if err != nil { logger.Error("failed to find asset", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to find asset for token %s on chain %d", channel.Token, channel.ChainID) + return ChannelOperationResponse{}, RPCErrorf("failed to find asset for token %s on chain %d", channel.Token, channel.ChainID) } if params.ResizeAmount == nil { @@ -61,27 +149,25 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam // Prevent no-op resize operations if params.ResizeAmount.Cmp(decimal.Zero) == 0 && params.AllocateAmount.Cmp(decimal.Zero) == 0 { - return ResizeChannelResponse{}, RPCErrorf("resize operation requires non-zero ResizeAmount or AllocateAmount") + return ChannelOperationResponse{}, RPCErrorf("resize operation requires non-zero ResizeAmount or AllocateAmount") } - userAddress := common.HexToAddress(channel.Wallet) - userAccountID := NewAccountID(channel.Wallet) - ledger := GetWalletLedger(s.db, userAddress) - balance, err := ledger.Balance(userAccountID, asset.Symbol) + ledger := GetWalletLedger(s.db, common.HexToAddress(channel.Wallet)) + balance, err := ledger.Balance(NewAccountID(channel.Wallet), asset.Symbol) if err != nil { logger.Error("failed to check participant balance", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to check participant balance for asset %s", asset.Symbol) + return ChannelOperationResponse{}, RPCErrorf("failed to check participant balance for asset %s", asset.Symbol) } rawBalance := balance.Shift(int32(asset.Decimals)) newChannelRawAmount := channel.RawAmount.Add(*params.AllocateAmount) if rawBalance.Cmp(newChannelRawAmount) < 0 { - return ResizeChannelResponse{}, RPCErrorf("insufficient unified balance for channel %s: required %s, available %s", channel.ChannelID, newChannelRawAmount.String(), rawBalance.String()) + return ChannelOperationResponse{}, RPCErrorf("insufficient unified balance for channel %s: required %s, available %s", channel.ChannelID, newChannelRawAmount.String(), rawBalance.String()) } newChannelRawAmount = newChannelRawAmount.Add(*params.ResizeAmount) if newChannelRawAmount.Cmp(decimal.Zero) < 0 { - return ResizeChannelResponse{}, RPCErrorf("new channel amount must be positive: %s", newChannelRawAmount.String()) + return ChannelOperationResponse{}, RPCErrorf("new channel amount must be positive: %s", newChannelRawAmount.String()) } allocations := []nitrolite.Allocation{ @@ -101,94 +187,83 @@ func (s *ChannelService) RequestResize(logger Logger, params *ResizeChannelParam intentionType, err := abi.NewType("int256[]", "", nil) if err != nil { - logger.Fatal("failed to create intention type", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to create intention type") + logger.Error("failed to create intention type", "error", err) + return ChannelOperationResponse{}, RPCErrorf("failed to create intention type") } intentionArgs := abi.Arguments{{Type: intentionType}} encodedIntentions, err := intentionArgs.Pack(resizeAmounts) if err != nil { logger.Error("failed to pack resize amounts", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to pack resize amounts") + return ChannelOperationResponse{}, RPCErrorf("failed to pack resize amounts") + } + + state := nitrolite.State{ + Intent: uint8(nitrolite.IntentRESIZE), + Version: big.NewInt(int64(channel.Version) + 1), + Data: encodedIntentions, + Allocations: allocations, } - // 6) Encode & sign the new state channelIDHash := common.HexToHash(channel.ChannelID) - packedState, err := nitrolite.PackState(channelIDHash, nitrolite.IntentRESIZE, big.NewInt(int64(channel.Version)+1), encodedIntentions, allocations) + packedState, err := nitrolite.PackState(channelIDHash, state) if err != nil { logger.Error("failed to pack state", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to pack state") + return ChannelOperationResponse{}, RPCErrorf("failed to pack state") } sig, err := s.signer.Sign(packedState) if err != nil { logger.Error("failed to sign state", "error", err) - return ResizeChannelResponse{}, RPCErrorf("failed to sign state") + return ChannelOperationResponse{}, RPCErrorf("failed to sign state") } - resp := ResizeChannelResponse{ - ChannelID: channel.ChannelID, - Intent: uint8(nitrolite.IntentRESIZE), - Version: channel.Version + 1, - StateData: hexutil.Encode(encodedIntentions), - Signature: sig, - } - - for _, alloc := range allocations { - resp.Allocations = append(resp.Allocations, Allocation{ - Participant: alloc.Destination.Hex(), - TokenAddress: alloc.Token.Hex(), - RawAmount: decimal.NewFromBigInt(alloc.Amount, 0), - }) - } - return resp, nil + return createChannelOperationResponse(channel.ChannelID, state, nil, sig), nil } -func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams, rpcSigners map[string]struct{}) (CloseChannelResponse, error) { +func (s *ChannelService) RequestClose(params *CloseChannelParams, rpcSigners map[string]struct{}, logger Logger) (ChannelOperationResponse, error) { channel, err := GetChannelByID(s.db, params.ChannelID) if err != nil { logger.Error("failed to find channel", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to find channel") + return ChannelOperationResponse{}, RPCErrorf("failed to find channel: %s", params.ChannelID) } if channel == nil { - return CloseChannelResponse{}, RPCErrorf("channel not found") + return ChannelOperationResponse{}, RPCErrorf("channel %s not found", params.ChannelID) } if err = checkChallengedChannels(s.db, channel.Wallet); err != nil { logger.Error("failed to check challenged channels", "error", err) - return CloseChannelResponse{}, err + return ChannelOperationResponse{}, err } if channel.Status != ChannelStatusOpen { - return CloseChannelResponse{}, RPCErrorf("channel %s is not open: %s", params.ChannelID, channel.Status) + return ChannelOperationResponse{}, RPCErrorf("channel %s is not open: %s", params.ChannelID, channel.Status) } _, ok := rpcSigners[channel.Wallet] if !ok { - return CloseChannelResponse{}, RPCErrorf("invalid signature") + return ChannelOperationResponse{}, RPCErrorf("invalid signature") } asset, err := GetAssetByToken(s.db, channel.Token, channel.ChainID) if err != nil { logger.Error("failed to find asset", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to find asset for token %s on chain %d", channel.Token, channel.ChainID) + return ChannelOperationResponse{}, RPCErrorf("failed to find asset for token %s on chain %d", channel.Token, channel.ChainID) } - userAddress := common.HexToAddress(channel.Wallet) - userAccountID := NewAccountID(channel.Wallet) - ledger := GetWalletLedger(s.db, userAddress) - balance, err := ledger.Balance(userAccountID, asset.Symbol) + ledger := GetWalletLedger(s.db, common.HexToAddress(channel.Wallet)) + balance, err := ledger.Balance(NewAccountID(channel.Wallet), asset.Symbol) if err != nil { logger.Error("failed to check participant balance", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to check participant balance") + return ChannelOperationResponse{}, RPCErrorf("failed to check participant balance for asset %s", asset.Symbol) } if balance.IsNegative() { logger.Error("negative balance", "balance", balance.String()) - return CloseChannelResponse{}, RPCErrorf("negative balance") + return ChannelOperationResponse{}, RPCErrorf("negative balance") } rawBalance := balance.Shift(int32(asset.Decimals)).BigInt() channelRawAmount := channel.RawAmount.BigInt() if channelRawAmount.Cmp(rawBalance) < 0 { - return CloseChannelResponse{}, RPCErrorf("resize this channel first") + return ChannelOperationResponse{}, RPCErrorf("resize this channel first") } allocations := []nitrolite.Allocation{ @@ -208,36 +283,29 @@ func (s *ChannelService) RequestClose(logger Logger, params *CloseChannelParams, stateDataBytes, err := hexutil.Decode(stateDataHex) if err != nil { logger.Error("failed to decode state data hex", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to decode state data hex") + return ChannelOperationResponse{}, RPCErrorf("failed to decode state data hex") + } + + state := nitrolite.State{ + Intent: uint8(nitrolite.IntentFINALIZE), + Version: big.NewInt(int64(channel.Version) + 1), + Data: stateDataBytes, + Allocations: allocations, } - packedState, err := nitrolite.PackState(common.HexToHash(channel.ChannelID), nitrolite.IntentFINALIZE, big.NewInt(int64(channel.Version)+1), stateDataBytes, allocations) + + packedState, err := nitrolite.PackState(common.HexToHash(channel.ChannelID), state) if err != nil { logger.Error("failed to pack state", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to pack state") + return ChannelOperationResponse{}, RPCErrorf("failed to pack state") } sig, err := s.signer.Sign(packedState) + if err != nil { logger.Error("failed to sign state", "error", err) - return CloseChannelResponse{}, RPCErrorf("failed to sign state") - } - - resp := CloseChannelResponse{ - ChannelID: channel.ChannelID, - Intent: uint8(nitrolite.IntentFINALIZE), - Version: channel.Version + 1, - StateData: stateDataHex, - Signature: sig, - } - - for _, alloc := range allocations { - resp.FinalAllocations = append(resp.FinalAllocations, Allocation{ - Participant: alloc.Destination.Hex(), - TokenAddress: alloc.Token.Hex(), - RawAmount: decimal.NewFromBigInt(alloc.Amount, 0), - }) + return ChannelOperationResponse{}, RPCErrorf("failed to sign state") } - return resp, nil + return createChannelOperationResponse(channel.ChannelID, state, nil, sig), nil } // checkChallengedChannels checks if the participant has any channels in the challenged state. @@ -252,3 +320,36 @@ func checkChallengedChannels(tx *gorm.DB, wallet string) error { } return nil } + +func createChannelOperationResponse(channelID string, state nitrolite.State, channel *nitrolite.Channel, signature Signature) ChannelOperationResponse { + resp := ChannelOperationResponse{ + ChannelID: channelID, + State: UnsignedState{ + Intent: uint8(state.Intent), + Version: state.Version.Uint64(), + Data: hexutil.Encode(state.Data), + }, + StateSignature: signature, + } + for _, alloc := range state.Allocations { + resp.State.Allocations = append(resp.State.Allocations, Allocation{ + Participant: alloc.Destination.Hex(), + TokenAddress: alloc.Token.Hex(), + RawAmount: decimal.NewFromBigInt(alloc.Amount, 0), + }) + } + if channel != nil { + resp.Channel = &struct { + Participants [2]string `json:"participants"` + Adjudicator string `json:"adjudicator"` + Challenge uint64 `json:"challenge"` + Nonce uint64 `json:"nonce"` + }{ + Participants: [2]string{channel.Participants[0].Hex(), channel.Participants[1].Hex()}, + Adjudicator: channel.Adjudicator.Hex(), + Challenge: channel.Challenge, + Nonce: channel.Nonce, + } + } + return resp +} diff --git a/clearnode/channel_service_test.go b/clearnode/channel_service_test.go index 27e7124f9..ace1ccbbd 100644 --- a/clearnode/channel_service_test.go +++ b/clearnode/channel_service_test.go @@ -2,488 +2,446 @@ package main import ( "context" + "math/big" "testing" "github.com/ethereum/go-ethereum/crypto" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/gorm" ) -func TestChannelService_ResizeChannel(t *testing.T) { - t.Run("SuccessfulAllocation", func(t *testing.T) { +func seedAsset(t *testing.T, db *gorm.DB, token string, chainID uint32, symbol string, decimals uint8) Asset { + asset := Asset{Token: token, ChainID: chainID, Symbol: symbol, Decimals: decimals} + require.NoError(t, db.Create(&asset).Error) + return asset +} + +func seedChannel(t *testing.T, db *gorm.DB, channelID, participant, wallet, token string, chainID uint32, rawAmount decimal.Decimal, version uint64, status ChannelStatus) Channel { + ch := Channel{ + ChannelID: channelID, + Participant: participant, + Wallet: wallet, + Status: status, + Token: token, + ChainID: chainID, + RawAmount: rawAmount, + Version: version, + } + require.NoError(t, db.Create(&ch).Error) + return ch +} + +func getCreateChannelParams(chainID uint32, token string, amount decimal.Decimal) *CreateChannelParams { + return &CreateChannelParams{ + ChainID: chainID, + Token: token, + Amount: &amount, + } +} + +func getResizeChannelParams(channelID string, allocateAmount *decimal.Decimal, resizeAmount *decimal.Decimal, destination string) *ResizeChannelParams { + return &ResizeChannelParams{ + ChannelID: channelID, + AllocateAmount: allocateAmount, + ResizeAmount: resizeAmount, + FundsDestination: destination, + } +} + +func getCloseChannelParams(channelID, destination string) *CloseChannelParams { + return &CloseChannelParams{ + ChannelID: channelID, + FundsDestination: destination, + } +} + +func TestChannelService(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + signer := Signer{privateKey: key} + userAddress := signer.GetAddress() + userAccountID := NewAccountID(userAddress.Hex()) + + rpcSigners := map[string]struct{}{ + userAddress.Hex(): {}, + } + + tokenAddress := "0x1234567890123456789012345678901234567890" + tokenSymbol := "usdc" + channelID := "0xDefaultChannelID" + channelAmountRaw := decimal.NewFromInt(1000) + chainID := uint32(137) + + networks := map[uint32]*NetworkConfig{ + 137: { + Name: "polygon", + ChainID: chainID, + InfuraURL: "https://polygon-mainnet.infura.io/v3/test", + CustodyAddress: "0x2e189bd6f6FD3EB59fd97FcA03251d93Af4E522a", + AdjudicatorAddress: "0xdadB0d80178819F2319190D340ce9A924f783711", + }, + } + + t.Run("RequestResize_Success", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - userAccountID := NewAccountID(userAddress.Hex()) - - // Create asset - asset := Asset{Token: "0xTokenResize", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - // Create channel with initial amount 1000 - initialRawAmount := decimal.NewFromInt(1000) - ch := Channel{ - ChannelID: "0xChanResize", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: initialRawAmount, - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusOpen) // Fund participant ledger with 1500 USDC (enough for resize) ledger := GetWalletLedger(db, userAddress) - require.NoError(t, ledger.Record(userAccountID, "usdc", decimal.NewFromInt(1500))) + require.NoError(t, ledger.Record(userAccountID, tokenSymbol, decimal.NewFromInt(1500))) - // Verify initial balance - initialBalance, err := ledger.Balance(userAccountID, "usdc") + initialBalance, err := ledger.Balance(userAccountID, tokenSymbol) require.NoError(t, err) assert.Equal(t, decimal.NewFromInt(1500), initialBalance) - service := NewChannelService(db, &signer) - allocateAmount := decimal.NewFromInt(200) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{ - userAddress.Hex(): {}, - } + service := NewChannelService(db, nil, &signer) - response, err := service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + allocateAmount := decimal.NewFromInt(200) + params := getResizeChannelParams(ch.ChannelID, &allocateAmount, nil, userAddress.Hex()) + response, err := service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.NoError(t, err) // Validate response assert.Equal(t, ch.ChannelID, response.ChannelID) - assert.Equal(t, ch.Version+1, response.Version) + assert.Equal(t, ch.Version+1, response.State.Version) // New channel amount should be initial + 200 - expected := initialRawAmount.Add(decimal.NewFromInt(200)) - assert.Equal(t, 0, response.Allocations[0].RawAmount.Cmp(expected), "Allocated amount mismatch") - assert.Equal(t, 0, response.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") + expected := channelAmountRaw.Add(decimal.NewFromInt(200)) + assert.Equal(t, 0, response.State.Allocations[0].RawAmount.Cmp(expected), "Allocated amount mismatch") + assert.Equal(t, 0, response.State.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") // Verify channel state in database remains unchanged (no update until blockchain confirmation) - var unchangedChannel Channel - require.NoError(t, db.Where("channel_id = ?", ch.ChannelID).First(&unchangedChannel).Error) - assert.Equal(t, initialRawAmount, unchangedChannel.RawAmount) // Should remain unchanged - assert.Equal(t, ch.Version, unchangedChannel.Version) // Should remain unchanged - assert.Equal(t, ChannelStatusOpen, unchangedChannel.Status) + channel, err := GetChannelByID(db, ch.ChannelID) + require.NoError(t, err) + require.NotNil(t, channel) + assert.Equal(t, channelAmountRaw, channel.RawAmount) + assert.Equal(t, ch.Version, channel.Version) + assert.Equal(t, ChannelStatusOpen, channel.Status) // Verify ledger balance remains unchanged (no update until blockchain confirmation) - finalBalance, err := ledger.Balance(userAccountID, "usdc") + finalBalance, err := ledger.Balance(userAccountID, tokenSymbol) require.NoError(t, err) assert.Equal(t, decimal.NewFromInt(1500), finalBalance) // Should remain unchanged }) - t.Run("SuccessfulDeallocation", func(t *testing.T) { + t.Run("RequestResize_SuccessfulDeallocation", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - userAccountID := NewAccountID(userAddress.Hex()) - - asset := Asset{Token: "0xTokenResize2", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - initialRawAmount := decimal.NewFromInt(1000) - ch := Channel{ - ChannelID: "0xChanResize2", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: initialRawAmount, - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusOpen) ledger := GetWalletLedger(db, userAddress) - require.NoError(t, ledger.Record(userAccountID, "usdc", decimal.NewFromInt(500))) + require.NoError(t, ledger.Record(userAccountID, tokenSymbol, decimal.NewFromInt(500))) - service := NewChannelService(db, &signer) - allocateAmount := decimal.NewFromInt(-300) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{ - userAddress.Hex(): {}, - } + service := NewChannelService(db, nil, &signer) - response, err := service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + allocateAmount := decimal.NewFromInt(-300) + params := getResizeChannelParams(ch.ChannelID, &allocateAmount, nil, userAddress.Hex()) + response, err := service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.NoError(t, err) // Channel amount should decrease - expected := initialRawAmount.Sub(decimal.NewFromInt(300)) - assert.Equal(t, 0, response.Allocations[0].RawAmount.Cmp(expected), "Decreased amount mismatch") + expected := channelAmountRaw.Sub(decimal.NewFromInt(300)) + assert.Equal(t, 0, response.State.Allocations[0].RawAmount.Cmp(expected), "Decreased amount mismatch") // Verify ledger balance remains unchanged (no update until blockchain confirmation) - finalBalance, err := ledger.Balance(userAccountID, "usdc") + finalBalance, err := ledger.Balance(userAccountID, tokenSymbol) require.NoError(t, err) assert.Equal(t, decimal.NewFromInt(500), finalBalance) // Should remain unchanged }) - t.Run("ErrorInvalidChannelID", func(t *testing.T) { + t.Run("RequestResize_ErrorInvalidChannelID", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) - service := NewChannelService(db, &signer) allocateAmount := decimal.NewFromInt(100) - params := &ResizeChannelParams{ - ChannelID: "0xNonExistentChannel", - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{userAddress.Hex(): {}} - - _, err = service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + params := getResizeChannelParams("0xNonExistentChannel", &allocateAmount, nil, userAddress.Hex()) + _, err = service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.Error(t, err) + assert.Contains(t, err.Error(), "channel 0xNonExistentChannel not found") }) - t.Run("ErrorChannelClosed", func(t *testing.T) { + t.Run("RequestResize_ErrorChannelClosed", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - - asset := Asset{Token: "0xTokenClosed", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - ch := Channel{ - ChannelID: "0xChanClosed", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusClosed, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusClosed) + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) - service := NewChannelService(db, &signer) allocateAmount := decimal.NewFromInt(100) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{userAddress.Hex(): {}} - - _, err = service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + params := getResizeChannelParams(ch.ChannelID, &allocateAmount, nil, userAddress.Hex()) + _, err = service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.Error(t, err) - assert.Contains(t, err.Error(), "channel 0xChanClosed is not open: closed") - }) - t.Run("ErrorChannelJoining", func(t *testing.T) { - db, cleanup := setupTestDB(t) - t.Cleanup(cleanup) - - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - - asset := Asset{Token: "0xTokenJoining", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - ch := Channel{ - ChannelID: "0xChanJoining", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusJoining, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) - - service := NewChannelService(db, &signer) - allocateAmount := decimal.NewFromInt(100) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{userAddress.Hex(): {}} - - _, err = service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) - require.Error(t, err) - assert.Contains(t, err.Error(), "channel 0xChanJoining is not open: joining") + assert.Contains(t, err.Error(), "channel 0xDefaultChannelID is not open: closed") }) - t.Run("ErrorOtherChallengedChannel", func(t *testing.T) { + t.Run("RequestResize_ErrorOtherChallengedChannel", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - - asset := Asset{Token: "0xToken", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - require.NoError(t, db.Create(&Channel{ - ChannelID: "0xChanChallenged", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusChallenged, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - }).Error) - - ch := Channel{ - ChannelID: "0xChan", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusChallenged) + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) - service := NewChannelService(db, &signer) allocateAmount := decimal.NewFromInt(100) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{userAddress.Hex(): {}} - - _, err = service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + params := getResizeChannelParams(ch.ChannelID, &allocateAmount, nil, userAddress.Hex()) + _, err = service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.Error(t, err) + assert.Contains(t, err.Error(), "has challenged channels") }) - t.Run("ErrorInsufficientFunds", func(t *testing.T) { + t.Run("RequestResize_ErrorInsufficientFunds", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - userAccountID := NewAccountID(userAddress.Hex()) - - asset := Asset{Token: "0xTokenInsufficient", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - ch := Channel{ - ChannelID: "0xChanInsufficient", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusOpen) // Fund with very small amount (0.000001 USDC), but try to allocate 200 raw units // This will create insufficient balance when converted to raw units ledger := GetWalletLedger(db, userAddress) - require.NoError(t, ledger.Record(userAccountID, "usdc", decimal.NewFromFloat(0.000001))) + require.NoError(t, ledger.Record(userAccountID, tokenSymbol, decimal.NewFromFloat(0.000001))) - service := NewChannelService(db, &signer) - allocateAmount := decimal.NewFromInt(200) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{userAddress.Hex(): {}} + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) - _, err = service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + allocateAmount := decimal.NewFromInt(200) + params := getResizeChannelParams(ch.ChannelID, &allocateAmount, nil, userAddress.Hex()) + _, err = service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.Error(t, err) + assert.Contains(t, err.Error(), "insufficient unified balance") }) - t.Run("ErrorInvalidSignature", func(t *testing.T) { + t.Run("RequestResize_ErrorInvalidSignature", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - - asset := Asset{Token: "0xTokenSig", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - ch := Channel{ - ChannelID: "0xChanSig", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusOpen) + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) - service := NewChannelService(db, &signer) allocateAmount := decimal.NewFromInt(100) - params := &ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{} // Empty signers + params := getResizeChannelParams(ch.ChannelID, &allocateAmount, nil, userAddress.Hex()) + rpcSigners := map[string]struct{}{} // Empty signers map - _, err = service.RequestResize(LoggerFromContext(context.Background()), params, rpcSigners) + _, err = service.RequestResize(params, rpcSigners, LoggerFromContext(context.Background())) require.Error(t, err) + assert.Contains(t, err.Error(), "invalid signature") }) -} -func TestChannelService_CloseChannel(t *testing.T) { - t.Run("SuccessfulCloseChannel", func(t *testing.T) { + t.Run("RequestClose_Success", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - userAccountID := NewAccountID(userAddress.Hex()) - - // Create asset - asset := Asset{Token: "0xTokenClose", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - // Create channel with amount 500 - initialRawAmount := decimal.NewFromInt(500) - ch := Channel{ - ChannelID: "0xChanClose", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: initialRawAmount, - Version: 2, - } - require.NoError(t, db.Create(&ch).Error) + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 2, ChannelStatusOpen) // Fund participant ledger so that raw units match channel.Amount require.NoError(t, GetWalletLedger(db, userAddress).Record( userAccountID, - "usdc", - rawToDecimal(initialRawAmount.BigInt(), asset.Decimals), + tokenSymbol, + rawToDecimal(channelAmountRaw.BigInt(), asset.Decimals), )) - service := NewChannelService(db, &signer) - params := &CloseChannelParams{ - ChannelID: ch.ChannelID, - FundsDestination: userAddress.Hex(), - } - rpcSigners := map[string]struct{}{ - userAddress.Hex(): {}, - } + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) - response, err := service.RequestClose(LoggerFromContext(context.Background()), params, rpcSigners) + params := getCloseChannelParams(ch.ChannelID, userAddress.Hex()) + response, err := service.RequestClose(params, rpcSigners, LoggerFromContext(context.Background())) require.NoError(t, err) - // Validate response assert.Equal(t, ch.ChannelID, response.ChannelID) - assert.Equal(t, ch.Version+1, response.Version) + assert.Equal(t, ch.Version+1, response.State.Version) // Final allocation should send full balance to destination - assert.Equal(t, 0, response.FinalAllocations[0].RawAmount.Cmp(initialRawAmount), "Primary allocation mismatch") - assert.Equal(t, 0, response.FinalAllocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") + assert.Equal(t, 0, response.State.Allocations[0].RawAmount.Cmp(channelAmountRaw), "Primary allocation mismatch") + assert.Equal(t, 0, response.State.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") }) - t.Run("ErrorOtherChallengedChannel", func(t *testing.T) { + t.Run("RequestClose_ErrorOtherChallengedChannel", func(t *testing.T) { db, cleanup := setupTestDB(t) t.Cleanup(cleanup) - rawKey, err := crypto.GenerateKey() + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + ch := seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 2, ChannelStatusChallenged) + service := NewChannelService(db, map[uint32]*NetworkConfig{}, &signer) + + params := getCloseChannelParams(ch.ChannelID, userAddress.Hex()) + _, err = service.RequestClose(params, rpcSigners, LoggerFromContext(context.Background())) + require.Error(t, err) + + assert.Contains(t, err.Error(), "has challenged channels") + }) + + t.Run("RequestCreate_Success", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + service := NewChannelService(db, networks, &signer) + + amount := decimal.NewFromInt(1000000) // 1 USDC in raw units + params := getCreateChannelParams(chainID, asset.Token, amount) + response, err := service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - userAccountID := NewAccountID(userAddress.Hex()) - - // Create asset - asset := Asset{Token: "0xTokenClose", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - // Create channel with amount 500 - initialRawAmount := decimal.NewFromInt(500) - - // Seed other challenged channel - require.NoError(t, db.Create(&Channel{ - ChannelID: "0xChanChallenged", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusChallenged, - Token: asset.Token, - ChainID: 137, - RawAmount: initialRawAmount, - Version: 2, - }).Error) - - ch := Channel{ - ChannelID: "0xChanClose", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusOpen, - Token: asset.Token, - ChainID: 137, - RawAmount: initialRawAmount, - Version: 2, - } - require.NoError(t, db.Create(&ch).Error) - // Fund participant ledger so that raw units match channel.Amount - require.NoError(t, GetWalletLedger(db, userAddress).Record( - userAccountID, - "usdc", - rawToDecimal(initialRawAmount.BigInt(), asset.Decimals), - )) + // Validate response structure + assert.NotEmpty(t, response.ChannelID, "Channel ID should not be empty") + assert.NotNil(t, response.State, "State should not be nil") + + // Verify state structure + assert.Equal(t, uint8(1), response.State.Intent, "Intent should be INITIALIZE (1)") + assert.Equal(t, uint64(0), response.State.Version, "Version should be 0") + assert.Len(t, response.State.Allocations, 2, "Should have 2 allocations") + assert.NotEmpty(t, response.StateSignature, "Should have 1 signature") + + // Verify allocations + assert.Equal(t, userAddress.Hex(), response.State.Allocations[0].Participant, "First allocation should be for user") + assert.Equal(t, asset.Token, response.State.Allocations[0].TokenAddress, "Token address should match") + assert.Equal(t, amount, response.State.Allocations[0].RawAmount, "Amount should match") + + assert.Equal(t, signer.GetAddress().Hex(), response.State.Allocations[1].Participant, "Second allocation should be for broker") + assert.Equal(t, asset.Token, response.State.Allocations[1].TokenAddress, "Token address should match") + assert.True(t, response.State.Allocations[1].RawAmount.IsZero(), "Broker allocation should be zero") + assert.Equal(t, 2, len(response.Channel.Participants), "Expected 2 participants") + assert.Equal(t, networks[chainID].AdjudicatorAddress, response.Channel.Adjudicator, "Adjudicator address should match") + assert.Equal(t, uint64(3600), response.Channel.Challenge, "Challenge should match") + assert.NotEqual(t, uint64(0), response.Channel.Nonce, "Nonce should not be 0") + }) - service := NewChannelService(db, &signer) - params := &CloseChannelParams{ - ChannelID: ch.ChannelID, - FundsDestination: userAddress.Hex(), - } + t.Run("RequestCreate_ZeroAmount", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + service := NewChannelService(db, networks, &signer) + + amount := decimal.Zero // Zero amount channel + params := getCreateChannelParams(chainID, asset.Token, amount) + response, err := service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) + require.NoError(t, err) + + assert.True(t, response.State.Allocations[0].RawAmount.IsZero(), "User allocation should be zero") + assert.True(t, response.State.Allocations[1].RawAmount.IsZero(), "Broker allocation should be zero") + }) + + t.Run("RequestCreate_ErrorInvalidSignature", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + service := NewChannelService(db, networks, &signer) + + amount := decimal.NewFromInt(1000000) + params := getCreateChannelParams(chainID, asset.Token, amount) + rpcSigners := map[string]struct{}{} // Empty signers map + + _, err = service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) + require.Error(t, err) + + assert.Contains(t, err.Error(), "invalid signature") + }) + + t.Run("RequestCreate_ErrorExistingOpenChannel", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + _ = seedChannel(t, db, channelID, userAddress.Hex(), userAddress.Hex(), asset.Token, chainID, channelAmountRaw, 1, ChannelStatusOpen) + service := NewChannelService(db, networks, &signer) + + amount := decimal.NewFromInt(1000000) + params := getCreateChannelParams(chainID, asset.Token, amount) + _, err = service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) + require.Error(t, err) + + assert.Contains(t, err.Error(), "an open channel with broker already exists") + }) + + t.Run("RequestCreate_ErrorUnsupportedToken", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + // Don't create any assets in the database + service := NewChannelService(db, networks, &signer) + + amount := decimal.NewFromInt(1000000) + params := getCreateChannelParams(chainID, "0xUnsupportedToken1234567890123456789012", amount) + + _, err = service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) + require.Error(t, err) + + assert.Contains(t, err.Error(), "token not supported") + }) + + t.Run("RequestCreate_ErrorUnsupportedChainID", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + // Create asset for unsupported chain ID to pass asset check first + asset := seedAsset(t, db, tokenAddress, 999, tokenSymbol, 6) + service := NewChannelService(db, networks, &signer) + + amount := decimal.NewFromInt(1000000) + params := getCreateChannelParams(999, asset.Token, amount) + _, err = service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) + require.Error(t, err) + + assert.Contains(t, err.Error(), "unsupported chain ID") + }) + + t.Run("RequestCreate_LargeAmount", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + service := NewChannelService(db, networks, &signer) + + largeAmount := decimal.NewFromBigInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(30), nil), 0) // 10^30 + params := getCreateChannelParams(chainID, asset.Token, largeAmount) + response, err := service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) + require.NoError(t, err) + + assert.Equal(t, largeAmount, response.State.Allocations[0].RawAmount, "Large amount should be preserved") + }) + + t.Run("RequestCreate_ErrorDifferentUserSignature", func(t *testing.T) { + db, cleanup := setupTestDB(t) + t.Cleanup(cleanup) + + asset := seedAsset(t, db, tokenAddress, chainID, tokenSymbol, 6) + service := NewChannelService(db, networks, &signer) + + // Create a different user + differentKey, err := crypto.GenerateKey() + require.NoError(t, err) + differentSigner := Signer{privateKey: differentKey} + differentAddress := differentSigner.GetAddress() + + amount := decimal.NewFromInt(1000000) + params := getCreateChannelParams(chainID, asset.Token, amount) + + // Use different user's signature but pass userAddress as wallet rpcSigners := map[string]struct{}{ - userAddress.Hex(): {}, + differentAddress.Hex(): {}, } - _, err = service.RequestClose(LoggerFromContext(context.Background()), params, rpcSigners) + _, err = service.RequestCreate(userAddress, params, rpcSigners, LoggerFromContext(context.Background())) require.Error(t, err) - assert.Contains(t, err.Error(), "has challenged channels") + + assert.Contains(t, err.Error(), "invalid signature") }) } diff --git a/clearnode/config.go b/clearnode/config.go index 4f3267902..abe275b5b 100644 --- a/clearnode/config.go +++ b/clearnode/config.go @@ -39,7 +39,7 @@ type NetworkConfig struct { // Config represents the overall application configuration type Config struct { - networks map[string]*NetworkConfig + networks map[uint32]*NetworkConfig privateKeyHex string dbConf DatabaseConfig msgExpiryTime int // Time in seconds for message timestamp validation @@ -92,7 +92,7 @@ func LoadConfig(logger Logger) (*Config, error) { logger.Info("set message expiry time", "value", messageTimestampExpiry) config := Config{ - networks: make(map[string]*NetworkConfig), + networks: make(map[uint32]*NetworkConfig), privateKeyHex: privateKeyHex, dbConf: dbConf, msgExpiryTime: messageTimestampExpiry, @@ -137,7 +137,7 @@ func LoadConfig(logger Logger) (*Config, error) { // Only add network if both required variables are present if infuraURL != "" && custodyAddress != "" && adjudicatorAddress != "" && balanceCheckerAddress != "" { networkLower := strings.ToLower(network) - config.networks[networkLower] = &NetworkConfig{ + config.networks[chainID] = &NetworkConfig{ Name: networkLower, ChainID: chainID, InfuraURL: infuraURL, diff --git a/clearnode/custody.go b/clearnode/custody.go index 80ac6c95c..207630e84 100644 --- a/clearnode/custody.go +++ b/clearnode/custody.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "math/big" "time" @@ -20,8 +19,7 @@ import ( ) var ( - custodyAbi *abi.ABI - balanceCheckerAbi *abi.ABI + custodyAbi *abi.ABI ) var ErrCustodyEventAlreadyProcessed = errors.New("custody event already processed") @@ -60,8 +58,8 @@ func NewCustody(signer *Signer, db *gorm.DB, wsNotifier *WSNotifier, infuraURL, return nil, fmt.Errorf("failed to create transaction signer: %w", err) } - // TODO: remove as it is unused - auth.GasPrice = big.NewInt(30000000000) // 20 gwei. + // TODO: estimate on the go. + auth.GasPrice = big.NewInt(30000000000) // 30 gwei. auth.GasLimit = uint64(3000000) custodyAddress := common.HexToAddress(custodyAddressStr) @@ -72,7 +70,7 @@ func NewCustody(signer *Signer, db *gorm.DB, wsNotifier *WSNotifier, infuraURL, balanceChecker, err := nitrolite.NewBalanceChecker(common.HexToAddress(balanceCheckerAddr), client) if err != nil { - return nil, fmt.Errorf("failed to bind custody contract: %w", err) + return nil, fmt.Errorf("failed to bind balance checker contract: %w", err) } return &Custody{ @@ -109,34 +107,6 @@ func (c *Custody) ListenEvents(ctx context.Context) { listenEvents(ctx, c.client, c.custodyAddr, c.chainID, c.blockStep, lastBlock, lastIndex, c.handleBlockChainEvent, c.logger) } -// Join calls the join method on the custody contract -func (c *Custody) Join(channelID string, lastStateData []byte) (common.Hash, error) { - // Convert string channelID to bytes32 - channelIDBytes := common.HexToHash(channelID) - - // The broker will always join as participant with index 1 (second participant) - index := big.NewInt(1) - - sig, err := c.signer.Sign(lastStateData) - if err != nil { - return common.Hash{}, fmt.Errorf("failed to sign data: %w", err) - } - - gasPrice, err := c.client.SuggestGasPrice(context.Background()) - if err != nil { - return common.Hash{}, fmt.Errorf("failed to suggest gas price: %w", err) - } - - c.transactOpts.GasPrice = gasPrice.Add(gasPrice, gasPrice) - // Call the join method on the custody contract - tx, err := c.custody.Join(c.transactOpts, channelIDBytes, index, sig) - if err != nil { - return common.Hash{}, fmt.Errorf("failed to join channel: %w", err) - } - - return tx.Hash(), nil -} - // handleBlockChainEvent processes different event types received from the blockchain func (c *Custody) handleBlockChainEvent(ctx context.Context, l types.Log) { ctx = SetContextLogger(ctx, c.logger) @@ -153,12 +123,7 @@ func (c *Custody) handleBlockChainEvent(ctx context.Context, l types.Log) { } c.handleCreated(logger, ev) case custodyAbi.Events["Joined"].ID: - ev, err := c.custody.ParseJoined(l) - if err != nil { - logger.Warn("error parsing event", "error", err) - return - } - c.handleJoined(logger, ev) + return case custodyAbi.Events["Challenged"].ID: ev, err := c.custody.ParseChallenged(l) if err != nil { @@ -190,8 +155,8 @@ func (c *Custody) handleCreated(logger Logger, ev *nitrolite.CustodyCreated) { channelID := common.Hash(ev.ChannelId).Hex() logger.Debug("parsed event", "channelId", channelID, "wallet", ev.Wallet.Hex(), "channel", ev.Channel, "initial", ev.Initial) - if len(ev.Channel.Participants) < 2 { - logger.Warn("not enough participants in the channel") + if len(ev.Channel.Participants) != 2 { + logger.Warn("supported only 2 participants in the channel") return } @@ -222,7 +187,7 @@ func (c *Custody) handleCreated(logger Logger, ev *nitrolite.CustodyCreated) { // Check if channel was created with the broker. if broker != c.signer.GetAddress() { - logger.Warn("participantB is not Broker", "actual", c.signer.GetAddress().Hex(), "expected", broker) + logger.Warn("channel participant is not the broker", "actual", c.signer.GetAddress().Hex(), "expected", broker.Hex()) return } @@ -266,88 +231,21 @@ func (c *Custody) handleCreated(logger Logger, ev *nitrolite.CustodyCreated) { asset, err := GetAssetByToken(tx, tokenAddress, c.chainID) if err != nil { - return fmt.Errorf("DB error fetching asset: %w", err) + return fmt.Errorf("failed to get asset: %w", err) } + amount := rawToDecimal(rawAmount, asset.Decimals) walletAddress := ev.Wallet channelAccountID := NewAccountID(channelID) + walletAccountID := NewAccountID(walletAddress.Hex()) - amount := rawToDecimal(rawAmount, asset.Decimals) ledger := GetWalletLedger(tx, walletAddress) if err := ledger.Record(channelAccountID, asset.Symbol, amount); err != nil { return fmt.Errorf("error recording balance update for wallet: %w", err) } - - logger.Info("handled created event", "channelId", channelID) - return nil - }) - if errors.Is(err, ErrCustodyEventAlreadyProcessed) { - return - } else if err != nil { - logger.Error("error creating channel in database", "error", err) - return - } - - encodedState, err := nitrolite.PackState(ev.ChannelId, nitrolite.IntentINITIALIZE, big.NewInt(0), ev.Initial.Data, ev.Initial.Allocations) - if err != nil { - logger.Error("error encoding state hash", "error", err) - return - } - - txHash, err := c.Join(channelID, encodedState) - if err != nil { - logger.Error("error joining channel", "error", err) - return - } - - c.wsNotifier.Notify(NewChannelNotification(ch)) - - logger.Info("successfully initiated join for channel", "channelId", channelID, "txHash", txHash.Hex()) -} - -func (c *Custody) handleJoined(logger Logger, ev *nitrolite.CustodyJoined) { - logger = logger.With("event", "Joined") - channelID := common.Hash(ev.ChannelId).Hex() - logger.Debug("parsed event", "channelId", channelID, "index", ev.Index) - - var channel Channel - err := c.db.Transaction(func(tx *gorm.DB) error { - // Save event in DB - if err := c.saveContractEvent(tx, "joined", *ev, ev.Raw); err != nil { - return err - } - - result := tx.Where("channel_id = ?", channelID).First(&channel) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return fmt.Errorf("channel with ID %s not found", channelID) - } - return fmt.Errorf("error finding channel: %w", result.Error) - } - - // Update the channel status to "open" - channel.Status = ChannelStatusOpen - channel.UpdatedAt = time.Now() - if err := tx.Save(&channel).Error; err != nil { - return fmt.Errorf("failed to close channel: %w", err) - } - - asset, err := GetAssetByToken(tx, channel.Token, c.chainID) - if err != nil { - return fmt.Errorf("DB error fetching asset: %w", err) - } - - walletAddress := common.HexToAddress(channel.Wallet) - channelAccountID := NewAccountID(channelID) - walletAccountID := NewAccountID(channel.Wallet) - amount := rawToDecimal(channel.RawAmount.BigInt(), asset.Decimals) - // Transfer from channel account into user's unified account. - ledger := GetWalletLedger(tx, walletAddress) if err := ledger.Record(channelAccountID, asset.Symbol, amount.Neg()); err != nil { - log.Printf("[Joined] Error recording balance update for wallet: %v", err) - return err + return fmt.Errorf("error recording balance update for wallet: %w", err) } - ledger = GetWalletLedger(tx, walletAddress) if err := ledger.Record(walletAccountID, asset.Symbol, amount); err != nil { return fmt.Errorf("error recording balance update for wallet: %w", err) @@ -358,20 +256,18 @@ func (c *Custody) handleJoined(logger Logger, ev *nitrolite.CustodyJoined) { return fmt.Errorf("failed to record transaction: %w", err) } - logger.Info("handled joined event", "channelId", channelID) + logger.Info("handled created event", "channelId", channelID) return nil }) if errors.Is(err, ErrCustodyEventAlreadyProcessed) { return } else if err != nil { - logger.Error("failed to join channel", "channelId", channelID, "error", err) + logger.Error("error creating channel in database", "error", err) return } - c.wsNotifier.Notify( - NewBalanceNotification(channel.Wallet, c.db), - NewChannelNotification(channel), - ) + c.wsNotifier.Notify(NewChannelNotification(ch)) + c.wsNotifier.Notify(NewBalanceNotification(ch.Wallet, c.db)) } func (c *Custody) handleChallenged(logger Logger, ev *nitrolite.CustodyChallenged) { @@ -457,7 +353,7 @@ func (c *Custody) handleResized(logger Logger, ev *nitrolite.CustodyResized) { if resizeAmount.Cmp(big.NewInt(0)) != 0 { asset, err := GetAssetByToken(tx, channel.Token, c.chainID) if err != nil { - return fmt.Errorf("DB error fetching asset: %w", err) + return fmt.Errorf("failed to get asset: %w", err) } amount := rawToDecimal(resizeAmount, asset.Decimals) @@ -539,7 +435,7 @@ func (c *Custody) handleClosed(logger Logger, ev *nitrolite.CustodyClosed) { asset, err := GetAssetByToken(tx, channel.Token, c.chainID) if err != nil { - return fmt.Errorf("DB error fetching asset: %w", err) + return fmt.Errorf("failed to get asset: %w", err) } rawAmount := ev.FinalState.Allocations[0].Amount @@ -562,8 +458,7 @@ func (c *Custody) handleClosed(logger Logger, ev *nitrolite.CustodyClosed) { return fmt.Errorf("error recording balance update for participant: %w", err) } if err := ledger.Record(channelAccountID, asset.Symbol, amount); err != nil { - log.Printf("[Closed] Error recording balance update for wallet: %v", err) - return err + return fmt.Errorf("error recording balance update for wallet: %w", err) } _, err = RecordLedgerTransaction(tx, TransactionTypeWithdrawal, walletAccountID, channelAccountID, asset.Symbol, amount) if err != nil { @@ -573,8 +468,7 @@ func (c *Custody) handleClosed(logger Logger, ev *nitrolite.CustodyClosed) { // 2. Withdraw from the channel account. if err := ledger.Record(channelAccountID, asset.Symbol, amount.Neg()); err != nil { - log.Printf("[Closed] Error recording balance update for wallet: %v", err) - return err + return fmt.Errorf("error recording balance update for wallet: %w", err) } channel.Status = ChannelStatusClosed @@ -650,7 +544,7 @@ func (c *Custody) UpdateBalanceMetrics(ctx context.Context, assets []Asset, metr for i, asset := range assets { var available decimal.Decimal if len(availInfo) > 0 && i < len(availInfo[0]) { - available := rawToDecimal(availInfo[0][i], asset.Decimals) + available = rawToDecimal(availInfo[0][i], asset.Decimals) metrics.BrokerBalanceAvailable.With(prometheus.Labels{ "network": fmt.Sprintf("%d", c.chainID), diff --git a/clearnode/custody_test.go b/clearnode/custody_test.go index 88421e844..fb4940735 100644 --- a/clearnode/custody_test.go +++ b/clearnode/custody_test.go @@ -150,21 +150,6 @@ func createMockCreatedEvent(t *testing.T, signer *Signer, token string, amount * return &log, event } -func createMockJoinedEvent(t *testing.T) (*types.Log, *nitrolite.CustodyJoined) { - t.Helper() - - channelID := [32]byte{1, 2, 3, 4} - - event := &nitrolite.CustodyJoined{ - ChannelId: channelID, - Index: big.NewInt(1), - } - - log := createMockLog(custodyAbi.Events["Joined"].ID) - - return &log, event -} - func createMockClosedEvent(t *testing.T, signer *Signer, token string, amount *big.Int) (*types.Log, *nitrolite.CustodyClosed) { t.Helper() @@ -352,14 +337,15 @@ func TestHandleCreatedEvent(t *testing.T) { assert.Equal(t, dbChannel.Adjudicator, mockEvent.Channel.Adjudicator.Hex()) assert.Equal(t, dbChannel.RawAmount, decimal.NewFromBigInt(tc.amount, 0)) assert.Equal(t, dbChannel.Token, tokenAddress) - assert.Equal(t, dbChannel.Status, ChannelStatusJoining) + assert.Equal(t, dbChannel.Status, ChannelStatusOpen) var entries []Entry entriesErr := db.Where("wallet = ?", mockEvent.Wallet.Hex()).Find(&entries).Error require.NoError(t, entriesErr) assert.NotEmpty(t, entries) - assertNotifications(t, capturedNotifications, mockEvent.Wallet.Hex(), 1) + assertNotifications(t, capturedNotifications, mockEvent.Wallet.Hex(), 2) + assert.Equal(t, dbChannel.ChainID, uint32(custody.chainID)) assert.False(t, dbChannel.CreatedAt.IsZero()) assert.False(t, dbChannel.UpdatedAt.IsZero()) @@ -368,7 +354,7 @@ func TestHandleCreatedEvent(t *testing.T) { assert.WithinDuration(t, time.Now(), dbChannel.UpdatedAt, 2*time.Second) walletLedger := GetWalletLedger(db, mockEvent.Wallet) - balance, err := walletLedger.Balance(NewAccountID(channelIDStr), "usdc") + balance, err := walletLedger.Balance(NewAccountID(mockEvent.Wallet.Hex()), "usdc") require.NoError(t, err) assert.Equal(t, tc.amount.String(), balance.Mul(decimal.NewFromInt(10).Pow(decimal.NewFromInt(6))).String()) // 6 decimals for USDC default test token @@ -376,153 +362,6 @@ func TestHandleCreatedEvent(t *testing.T) { } } -func TestHandleJoinedEvent(t *testing.T) { - t.Run("Success", func(t *testing.T) { - custody, db, cleanup := setupMockCustody(t) - defer cleanup() - - amount := decimal.NewFromInt(1000000) - - channelID := "0x0102030400000000000000000000000000000000000000000000000000000000" - channelAccountID := NewAccountID(channelID) - walletAddr := newTestCommonAddress("0xWallet123") - walletAccountID := NewAccountID(walletAddr.Hex()) - participantAddr := newTestCommonAddress("0xParticipant1") - - initialChannel := Channel{ - ChannelID: channelID, - Wallet: walletAddr.Hex(), - Participant: participantAddr.Hex(), - Status: ChannelStatusJoining, - Token: tokenAddress, - ChainID: custody.chainID, - RawAmount: amount, - Nonce: 12345, - Version: 1, - Challenge: 3600, - Adjudicator: newTestCommonAddress("0xAdjudicatorAddress").Hex(), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - err := db.Create(&initialChannel).Error - require.NoError(t, err) - - asset, err := GetAssetByToken(db, tokenAddress, custody.chainID) - require.NoError(t, err) - - ledger := GetWalletLedger(db, walletAddr) - tokenAmountDecimal := decimal.NewFromBigInt(amount.BigInt(), 0).Div(decimal.NewFromInt(10).Pow(decimal.NewFromInt(int64(asset.Decimals)))) - err = ledger.Record(channelAccountID, asset.Symbol, tokenAmountDecimal) - require.NoError(t, err) - - _, mockEvent := createMockJoinedEvent(t) - - capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { - - capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ - userID: userID, - eventType: EventType(method), - data: params, - }) - } - - beforeUpdate := time.Now() - logger := custody.logger.With("event", "Joined") - custody.handleJoined(logger, mockEvent) - afterUpdate := time.Now() - - var updatedChannel Channel - err = db.Where("channel_id = ?", channelID).First(&updatedChannel).Error - require.NoError(t, err) - - assert.Equal(t, ChannelStatusOpen, updatedChannel.Status) - assert.Equal(t, initialChannel.RawAmount, updatedChannel.RawAmount, "Amount should not change") - assert.Equal(t, initialChannel.Nonce, updatedChannel.Nonce, "Nonce should not change") - assert.Equal(t, initialChannel.Challenge, updatedChannel.Challenge, "Challenge should not change") - assert.Equal(t, initialChannel.ChainID, updatedChannel.ChainID, "ChainID should not change") - assert.Equal(t, initialChannel.Token, updatedChannel.Token, "Token should not change") - - var entries []Entry - err = db.Where("wallet = ?", walletAddr.Hex()).Find(&entries).Error - require.NoError(t, err) - assert.NotEmpty(t, entries) - - assertNotifications(t, capturedNotifications, walletAddr.Hex(), 2) - assert.Equal(t, ChannelUpdateEventType, capturedNotifications[walletAddr.Hex()][1].eventType) - - assert.Equal(t, initialChannel.CreatedAt.Unix(), updatedChannel.CreatedAt.Unix()) - assert.True(t, updatedChannel.UpdatedAt.After(initialChannel.UpdatedAt)) - assert.True(t, updatedChannel.UpdatedAt.After(beforeUpdate) && updatedChannel.UpdatedAt.Before(afterUpdate)) - - channelBalance, err := ledger.Balance(channelAccountID, asset.Symbol) - require.NoError(t, err) - assert.True(t, channelBalance.IsZero(), "Channel balance should be zero after joined event") - - walletBalance, err := ledger.Balance(walletAccountID, asset.Symbol) - require.NoError(t, err) - assert.True(t, tokenAmountDecimal.Equal(walletBalance), - "Wallet balance should be %s, got %s", tokenAmountDecimal, walletBalance) - - // Verify transaction was recorded to the database - var transactions []LedgerTransaction - err = db.Where("from_account = ? AND to_account = ?", channelID, walletAddr.Hex()).Find(&transactions).Error - require.NoError(t, err) - assert.Len(t, transactions, 1, "Should have 1 deposit transaction recorded") - - tx := transactions[0] - assert.Equal(t, TransactionTypeDeposit, tx.Type, "Transaction type should be deposit") - assert.Equal(t, channelID, tx.FromAccount, "From account should be channel ID") - assert.Equal(t, walletAddr.Hex(), tx.ToAccount, "To account should be wallet address") - assert.Equal(t, asset.Symbol, tx.AssetSymbol, "Asset symbol should match") - assert.True(t, tokenAmountDecimal.Equal(tx.Amount), "Transaction amount should match deposited amount") - assert.False(t, tx.CreatedAt.IsZero(), "CreatedAt should be set") - }) - - t.Run("Channel Not Found", func(t *testing.T) { - custody, db, cleanup := setupMockCustody(t) - defer cleanup() - - // Create a different channel ID than the one in the event - initialChannel := Channel{ - ChannelID: "0xDifferentChannelId", - Wallet: "0xWallet123", - Status: ChannelStatusJoining, - Token: tokenAddress, - ChainID: custody.chainID, - RawAmount: decimal.NewFromInt(1000000), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - err := db.Create(&initialChannel).Error - require.NoError(t, err) - - _, mockEvent := createMockJoinedEvent(t) - - capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { - - capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ - userID: userID, - eventType: EventType(method), - data: params, - }) - } - - logger := custody.logger.With("event", "Joined") - custody.handleJoined(logger, mockEvent) - - // Event should be ignored, and no callbacks should be called - assert.Equal(t, 0, len(capturedNotifications), "No notifications should be sent") - - // Initial channel should remain unmodified - var checkChannel Channel - err = db.Where("channel_id = ?", initialChannel.ChannelID).First(&checkChannel).Error - require.NoError(t, err) - assert.Equal(t, ChannelStatusJoining, checkChannel.Status, "Status of other channel should not change") - }) -} - func TestHandleClosedEvent(t *testing.T) { t.Run("Success Smaller Final Amount", func(t *testing.T) { custody, db, cleanup := setupMockCustody(t) @@ -727,7 +566,7 @@ func TestHandleClosedEvent(t *testing.T) { ChannelID: channelID, Wallet: walletAddr.Hex(), Participant: participantAddr.Hex(), - Status: ChannelStatusJoining, + Status: ChannelStatusOpen, Token: tokenAddress, ChainID: custody.chainID, RawAmount: channelAmount, @@ -1086,17 +925,6 @@ func TestHandleResizedEvent(t *testing.T) { } func TestHandleEventWithInvalidChannel(t *testing.T) { - t.Run("Invalid Channel For Joined", func(t *testing.T) { - custody, _, cleanup := setupMockCustody(t) - defer cleanup() - - _, mockEvent := createMockJoinedEvent(t) - - logger := custody.logger.With("event", "Joined") - // Should not panic when channel doesn't exist - custody.handleJoined(logger, mockEvent) - }) - t.Run("Invalid Channel For Closed", func(t *testing.T) { custody, _, cleanup := setupMockCustody(t) defer cleanup() diff --git a/clearnode/docs/API.md b/clearnode/docs/API.md index d05db9624..f2936294a 100644 --- a/clearnode/docs/API.md +++ b/clearnode/docs/API.md @@ -22,8 +22,9 @@ | `create_app_session` | Creates a new virtual application on a ledger | Private | | `submit_app_state` | Submits an intermediate state into a virtual application | Private | | `close_app_session` | Closes a virtual application | Private | -| `close_channel` | Closes a payment channel | Private | -| `resize_channel` | Adjusts channel capacity | Private | +| `create_channel` | Returns data and Broker signature to open a channel | Private | +| `close_channel` | Returns data and Broker signature to close a channel | Private | +| `resize_channel` | Returns data and Broker signature to adjust channel capacity | Private | ## Authentication @@ -805,6 +806,73 @@ The optional `session_data` field can be used to provide final application-speci } ``` +### Create Channel + +Requests opening a channel with a Clearnode broker on a specific network. + +**Request:** + +```json +{ + "req": [1, "create_channel", [{ + "chain_id": 137, + "token": "0xeeee567890abcdef...", + "amount": "100000000", + "session_key": "0x1234567890abcdef..." // Optional + }], 1619123456789], + "sig": ["0x9876fedcba..."] +} +``` + +The request parameters are: +- `chain_id`: The blockchain network ID where the channel should be created +- `token`: The token contract address for the channel +- `amount`: The initial amount to deposit in the channel (in raw token units) +- `session_key`: An optional session key for channel operations + +**Response:** + +Returns signed initial state with the requested amounts ready to submit on Blockchain. + +```json +{ + "res": [1, "create_channel", [{ + "channel_id": "0x4567890123abcdef...", + "channel": { + "participants": ["0x1234567890abcdef...", "0xbbbb567890abcdef..."], + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 3600, + "nonce": 1619123456789 + }, + "state": { + "intent": 1, + "version": 0, + "state_data": "0xc0ffee", + "allocations": [ + { + "destination": "0x1234567890abcdef...", + "token": "0xeeee567890abcdef...", + "amount": "100000000" + }, + { + "destination": "0xbbbb567890abcdef...", + "token": "0xeeee567890abcdef...", + "amount": "0" + } + ] + }, + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" + }], 1619123456789], + "sig": ["0xabcd1234..."] +} +``` + +The response includes: +- `channel_id`: Unique identifier for the channel +- `channel`: Channel definition with participants, adjudicator, challenge period, and nonce +- `state`: Complete initial state structure containing intent, version, state_data, and allocations +- `server_signature`: Broker's signature of the state + ### Close Channel To close a channel, the user must request the final state signed by the broker and then submit it to the smart contract. @@ -830,22 +898,24 @@ In the request, the user must specify funds destination. After the channel is cl { "res": [1, "close_channel", [{ "channel_id": "0x4567890123abcdef...", - "intent": 3, // IntentFINALIZE - constant specifying that this is a final state - "version": 123, - "state_data": "0xdeadbeef", - "allocations": [ - { - "destination": "0x1234567890abcdef...", // Provided funds address - "token": "0xeeee567890abcdef...", - "amount": "50000" - }, - { - "destination": "0xbbbb567890abcdef...", // Broker address - "token": "0xeeee567890abcdef...", - "amount": "50000" - } - ], - "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c", + "state": { + "intent": 3, // IntentFINALIZE - constant specifying that this is a final state + "version": 123, + "state_data": "0xc0ffee", + "allocations": [ + { + "destination": "0x1234567890abcdef...", // Provided funds address + "token": "0xeeee567890abcdef...", + "amount": "50000" + }, + { + "destination": "0xbbbb567890abcdef...", // Broker address + "token": "0xeeee567890abcdef...", + "amount": "50000" + } + ] + }, + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" }], 1619123456789], "sig": ["0xabcd1234..."] } @@ -885,22 +955,24 @@ Example: { "res": [1, "resize_channel", [{ "channel_id": "0x4567890123abcdef...", - "state_data": "0xdeadbeef", - "intent": 2, // IntentRESIZE - "version": 5, - "allocations": [ - { - "destination": "0x1234567890abcdef...", - "token": "0xeeee567890abcdef...", - "amount": "100000" - }, - { - "destination": "0xbbbb567890abcdef...", // Broker address - "token": "0xeeee567890abcdef...", - "amount": "0" - } - ], - "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c", + "state": { + "intent": 2, // IntentRESIZE + "version": 5, + "state_data": "0xc0ffee", + "allocations": [ + { + "destination": "0x1234567890abcdef...", + "token": "0xeeee567890abcdef...", + "amount": "100000" + }, + { + "destination": "0xbbbb567890abcdef...", // Broker address + "token": "0xeeee567890abcdef...", + "amount": "0" + } + ] + }, + "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" }], 1619123456789], "sig": ["0xabcd1234..."] } @@ -1131,19 +1203,16 @@ Retrieves broker configuration information including supported networks. "broker_address": "0xbbbb567890abcdef...", "networks": [ { - "name": "polygon", "chain_id": 137, "custody_address": "0xCustodyContractAddress1...", "adjudicator_address":"0xCustodyContractAddress1..." }, { - "name": "celo", "chain_id": 42220, "custody_address": "0xCustodyContractAddress2...", "adjudicator_address":"0xCustodyContractAddress1..." }, { - "name": "base", "chain_id": 8453, "custody_address": "0xCustodyContractAddress3...", "adjudicator_address":"0xCustodyContractAddress1..." diff --git a/clearnode/eth_listener.go b/clearnode/eth_listener.go index fbf177e73..6f90fdade 100644 --- a/clearnode/eth_listener.go +++ b/clearnode/eth_listener.go @@ -55,11 +55,6 @@ func init() { if err != nil { panic(err) } - - balanceCheckerAbi, err = nitrolite.BalanceCheckerMetaData.GetAbi() - if err != nil { - panic(err) - } } type LogHandler func(ctx context.Context, l types.Log) diff --git a/clearnode/main.go b/clearnode/main.go index 5db8d48a5..9856744f0 100644 --- a/clearnode/main.go +++ b/clearnode/main.go @@ -49,7 +49,7 @@ func main() { // Initialize Prometheus metrics metrics := NewMetrics() // Map to store custody clients for later reference - custodyClients := make(map[string]*Custody) + custodyClients := make(map[uint32]*Custody) authManager, err := NewAuthManager(signer.GetPrivateKey()) if err != nil { @@ -58,9 +58,8 @@ func main() { rpcNode := NewRPCNode(signer, logger) wsNotifier := NewWSNotifier(rpcNode.Notify, logger) - appSessionService := NewAppSessionService(db, wsNotifier) - channelService := NewChannelService(db, signer) + channelService := NewChannelService(db, config.networks, signer) NewRPCRouter(rpcNode, config, signer, appSessionService, channelService, db, authManager, metrics, rpcStore, wsNotifier, logger) @@ -74,13 +73,13 @@ func main() { Handler: rpcMux, } - for name, network := range config.networks { + for chainID, network := range config.networks { client, err := NewCustody(signer, db, wsNotifier, network.InfuraURL, network.CustodyAddress, network.AdjudicatorAddress, network.BalanceCHeckerAddress, network.ChainID, network.BlockStep, logger) if err != nil { - logger.Warn("failed to initialize blockchain client", "network", name, "error", err) + logger.Warn("failed to initialize blockchain client", "chainID", chainID, "error", err) continue } - custodyClients[name] = client + custodyClients[chainID] = client go client.ListenEvents(context.Background()) } diff --git a/clearnode/metrics.go b/clearnode/metrics.go index 808cf17be..10812d102 100644 --- a/clearnode/metrics.go +++ b/clearnode/metrics.go @@ -154,7 +154,7 @@ func NewMetricsWithRegistry(registry prometheus.Registerer) *Metrics { return metrics } -func (m *Metrics) RecordMetricsPeriodically(db *gorm.DB, custodyClients map[string]*Custody, logger Logger) { +func (m *Metrics) RecordMetricsPeriodically(db *gorm.DB, custodyClients map[uint32]*Custody, logger Logger) { logger = logger.NewSystem("metrics") dbTicker := time.NewTicker(15 * time.Second) defer dbTicker.Stop() diff --git a/clearnode/nitrolite/state.go b/clearnode/nitrolite/state.go index 89ec62df1..c4ea23674 100644 --- a/clearnode/nitrolite/state.go +++ b/clearnode/nitrolite/state.go @@ -1,8 +1,6 @@ package nitrolite import ( - "math/big" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" ) @@ -16,8 +14,7 @@ const ( IntentFINALIZE Intent = 3 ) -// PackState encodes channel id and state into a byte array -func PackState(channelID common.Hash, intent Intent, version *big.Int, stateData []byte, allocations []Allocation) ([]byte, error) { +func PackState(channelID common.Hash, state State) ([]byte, error) { allocationType, err := abi.NewType("tuple[]", "", []abi.ArgumentMarshaling{ {Name: "destination", Type: "address"}, {Name: "token", Type: "address"}, @@ -44,7 +41,7 @@ func PackState(channelID common.Hash, intent Intent, version *big.Int, stateData {Type: allocationType}, // allocations (tuple[]) } - packed, err := args.Pack(channelID, uint8(intent), version, stateData, allocations) + packed, err := args.Pack(channelID, state.Intent, state.Version, state.Data, state.Allocations) if err != nil { return nil, err } diff --git a/clearnode/reconcile_cli.go b/clearnode/reconcile_cli.go index 98ccc1574..b3b546248 100644 --- a/clearnode/reconcile_cli.go +++ b/clearnode/reconcile_cli.go @@ -11,14 +11,17 @@ import ( ) // runReconcileCli is the entry point for the reconcile command line interface. -// Example: clearnode reconcile eth_mainnet 1000000 2000000 +// Example: clearnode reconcile 1 1000000 2000000 func runReconcileCli(logger Logger) { logger = logger.NewSystem("reconcile") if len(os.Args) < 5 { logger.Fatal("Usage: clearnode reconcile ") } - networkName := os.Args[2] + networkID, ok := new(big.Int).SetString(os.Args[2], 10) + if !ok { + logger.Fatal("Invalid network ID", "value", os.Args[2]) + } blockStart, ok := new(big.Int).SetString(os.Args[3], 10) if !ok { logger.Fatal("Invalid block start", "value", os.Args[3]) @@ -34,9 +37,9 @@ func runReconcileCli(logger Logger) { logger.Fatal("Failed to load configuration", "error", err) } - network, ok := config.networks[networkName] + network, ok := config.networks[uint32(networkID.Uint64())] if !ok { - logger.Fatal("Network is not configured", "network", networkName) + logger.Fatal("Network is not configured", "network", networkID.Uint64()) } client, err := ethclient.Dial(network.InfuraURL) diff --git a/clearnode/rpc_router.go b/clearnode/rpc_router.go index a21772862..7f0d13996 100644 --- a/clearnode/rpc_router.go +++ b/clearnode/rpc_router.go @@ -82,6 +82,7 @@ func NewRPCRouter( historyGroup := privGroup.NewGroup("") historyGroup.Use(r.HistoryMiddleware) + historyGroup.Handle("create_channel", r.HandleCreateChannel) historyGroup.Handle("resize_channel", r.HandleResizeChannel) historyGroup.Handle("close_channel", r.HandleCloseChannel) diff --git a/clearnode/rpc_router_private.go b/clearnode/rpc_router_private.go index ca2eb689f..ed9ca5e27 100644 --- a/clearnode/rpc_router_private.go +++ b/clearnode/rpc_router_private.go @@ -72,40 +72,48 @@ type AppSessionResponse struct { UpdatedAt string `json:"updated_at"` } +type CreateChannelParams struct { + ChainID uint32 `json:"chain_id" validate:"required"` + Token string `json:"token" validate:"required"` + Amount *decimal.Decimal `json:"amount" validate:"required,bigint"` + SessionKey *string `json:"session_key,omitempty" validate:"omitempty"` +} + type ResizeChannelParams struct { ChannelID string `json:"channel_id" validate:"required"` - AllocateAmount *decimal.Decimal `json:"allocate_amount,omitempty,string" validate:"omitempty,required_without=ResizeAmount,bigint"` - ResizeAmount *decimal.Decimal `json:"resize_amount,omitempty,string" validate:"omitempty,required_without=AllocateAmount,bigint"` + AllocateAmount *decimal.Decimal `json:"allocate_amount,omitempty" validate:"omitempty,required_without=ResizeAmount,bigint"` + ResizeAmount *decimal.Decimal `json:"resize_amount,omitempty" validate:"omitempty,required_without=AllocateAmount,bigint"` FundsDestination string `json:"funds_destination" validate:"required"` } -type ResizeChannelResponse struct { - ChannelID string `json:"channel_id"` - StateData string `json:"state_data"` +type CloseChannelParams struct { + ChannelID string `json:"channel_id"` + FundsDestination string `json:"funds_destination"` +} + +type ChannelOperationResponse struct { + ChannelID string `json:"channel_id"` + Channel *struct { + Participants [2]string `json:"participants"` + Adjudicator string `json:"adjudicator"` + Challenge uint64 `json:"challenge"` + Nonce uint64 `json:"nonce"` + } `json:"channel,omitempty"` + State UnsignedState `json:"state"` + StateSignature Signature `json:"server_signature"` +} + +type UnsignedState struct { Intent uint8 `json:"intent"` Version uint64 `json:"version"` + Data string `json:"state_data"` Allocations []Allocation `json:"allocations"` - Signature Signature `json:"server_signature"` } type Allocation struct { Participant string `json:"destination"` TokenAddress string `json:"token"` - RawAmount decimal.Decimal `json:"amount,string"` -} - -type CloseChannelParams struct { - ChannelID string `json:"channel_id"` - FundsDestination string `json:"funds_destination"` -} - -type CloseChannelResponse struct { - ChannelID string `json:"channel_id"` - Intent uint8 `json:"intent"` - Version uint64 `json:"version"` - StateData string `json:"state_data"` - FinalAllocations []Allocation `json:"allocations"` - Signature Signature `json:"server_signature"` + RawAmount decimal.Decimal `json:"amount"` } type ChannelResponse struct { @@ -114,7 +122,7 @@ type ChannelResponse struct { Status ChannelStatus `json:"status"` Token string `json:"token"` Wallet string `json:"wallet"` - RawAmount decimal.Decimal `json:"amount,string"` // Total amount in the channel (user + broker) + RawAmount decimal.Decimal `json:"amount"` // Total amount in the channel (user + broker) ChainID uint32 `json:"chain_id"` Adjudicator string `json:"adjudicator"` Challenge uint64 `json:"challenge"` @@ -359,21 +367,17 @@ func (r *RPCRouter) HandleCreateApplication(c *RPCContext) { return } - appSession, err := r.AppSessionService.CreateApplication(¶ms, rpcSigners) + resp, err := r.AppSessionService.CreateApplication(¶ms, rpcSigners) if err != nil { logger.Error("failed to create application session", "error", err) c.Fail(err, "failed to create application session") return } - c.Succeed(req.Method, AppSessionResponse{ - AppSessionID: appSession.SessionID, - Version: appSession.Version, - Status: string(ChannelStatusOpen), - }) + c.Succeed(req.Method, resp) logger.Info("application session created", "userID", c.UserID, - "sessionID", appSession.SessionID, + "sessionID", resp.AppSessionID, "protocol", params.Definition.Protocol, "participants", params.Definition.ParticipantWallets, "challenge", params.Definition.Challenge, @@ -401,22 +405,18 @@ func (r *RPCRouter) HandleSubmitAppState(c *RPCContext) { return } - newVersion, err := r.AppSessionService.SubmitAppState(¶ms, rpcSigners) + resp, err := r.AppSessionService.SubmitAppState(¶ms, rpcSigners) if err != nil { logger.Error("failed to submit app state", "error", err) c.Fail(err, "failed to submit app state") return } - c.Succeed(req.Method, AppSessionResponse{ - AppSessionID: params.AppSessionID, - Version: newVersion, - Status: string(ChannelStatusOpen), - }) + c.Succeed(req.Method, resp) logger.Info("application session state submitted", "userID", c.UserID, "sessionID", params.AppSessionID, - "newVersion", newVersion, + "newVersion", resp.Version, "allocations", params.Allocations, ) } @@ -440,26 +440,55 @@ func (r *RPCRouter) HandleCloseApplication(c *RPCContext) { return } - finalVersion, err := r.AppSessionService.CloseApplication(¶ms, rpcSigners) + resp, err := r.AppSessionService.CloseApplication(¶ms, rpcSigners) if err != nil { logger.Error("failed to close application session", "error", err) c.Fail(err, "failed to close application session") return } - c.Succeed(req.Method, AppSessionResponse{ - AppSessionID: params.AppSessionID, - Version: finalVersion, - Status: string(ChannelStatusClosed), - }) + c.Succeed(req.Method, resp) logger.Info("application session closed", "userID", c.UserID, "sessionID", params.AppSessionID, - "finalVersion", finalVersion, + "finalVersion", resp.Version, "allocations", params.Allocations, ) } +// HandleCreateChannel processes a request to create a payment channel with broker +func (r *RPCRouter) HandleCreateChannel(c *RPCContext) { + ctx := c.Context + logger := LoggerFromContext(ctx) + req := c.Message.Req + + var params CreateChannelParams + if err := parseParams(req.Params, ¶ms); err != nil { + c.Fail(err, "failed to parse parameters") + return + } + + rpcSigners, err := getWallets(&c.Message) + if err != nil { + logger.Error("failed to get wallets from RPC message", "error", err) + c.Fail(err, "failed to get wallets from RPC message") + return + } + + resp, err := r.ChannelService.RequestCreate(common.HexToAddress(c.UserID), ¶ms, rpcSigners, logger) + if err != nil { + logger.Error("failed to request channel create", "error", err) + c.Fail(err, "failed to request channel create") + return + } + + c.Succeed(req.Method, resp) + logger.Info("channel create requested", + "userID", c.UserID, + "channelID", resp.ChannelID, + ) +} + // HandleResizeChannel processes a request to resize a payment channel func (r *RPCRouter) HandleResizeChannel(c *RPCContext) { ctx := c.Context @@ -479,7 +508,7 @@ func (r *RPCRouter) HandleResizeChannel(c *RPCContext) { return } - resp, err := r.ChannelService.RequestResize(logger, ¶ms, rpcSigners) + resp, err := r.ChannelService.RequestResize(¶ms, rpcSigners, logger) if err != nil { logger.Error("failed to request channel resize", "error", err) c.Fail(err, "failed to request channel resize") @@ -490,7 +519,6 @@ func (r *RPCRouter) HandleResizeChannel(c *RPCContext) { logger.Info("channel resize requested", "userID", c.UserID, "channelID", resp.ChannelID, - "newVersion", resp.Version, "fundsDestination", params.FundsDestination, "resizeAmount", params.ResizeAmount.String(), "allocateAmount", params.AllocateAmount.String(), @@ -535,7 +563,7 @@ func (r *RPCRouter) HandleCloseChannel(c *RPCContext) { return } - resp, err := r.ChannelService.RequestClose(logger, ¶ms, rpcSigners) + resp, err := r.ChannelService.RequestClose(¶ms, rpcSigners, logger) if err != nil { logger.Error("failed to request channel closure", "error", err) c.Fail(err, "failed to request channel closure") @@ -546,7 +574,6 @@ func (r *RPCRouter) HandleCloseChannel(c *RPCContext) { logger.Info("channel close requested", "userID", c.UserID, "channelID", resp.ChannelID, - "newVersion", resp.Version, "fundsDestination", params.FundsDestination, ) } diff --git a/clearnode/rpc_router_private_test.go b/clearnode/rpc_router_private_test.go index 589b0b6f5..a09a2e1ac 100644 --- a/clearnode/rpc_router_private_test.go +++ b/clearnode/rpc_router_private_test.go @@ -950,15 +950,15 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) - require.Equal(t, ch.Version+1, resObj.Version) + require.Equal(t, ch.Version+1, resObj.State.Version) // New channel amount should be initial + 200 expected := initialRawAmount.Add(allocateAmount) - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(expected), "Allocated amount mismatch") - require.Equal(t, 0, resObj.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(expected), "Allocated amount mismatch") + require.Equal(t, 0, resObj.State.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") // Verify channel state in database remains unchanged (no update until blockchain confirmation) var unchangedChannel Channel @@ -1010,12 +1010,12 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok) // Channel amount should decrease expected := initialRawAmount.Add(allocateAmount) - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(expected), "Decreased amount mismatch") + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(expected), "Decreased amount mismatch") // Verify ledger balance remains unchanged (no update until blockchain confirmation) finalBalance, err := ledger.Balance(userAccountID, "usdc") @@ -1081,45 +1081,6 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { assertErrorResponse(t, ctx, "channel 0xChanClosed is not open: closed") }) - t.Run("ErrorChannelJoining", func(t *testing.T) { - t.Parallel() - - router, db, cleanup := setupTestRPCRouter(t) - t.Cleanup(cleanup) - - rawKey, err := crypto.GenerateKey() - require.NoError(t, err) - signer := Signer{privateKey: rawKey} - userAddress := signer.GetAddress() - - asset := Asset{Token: "0xTokenJoining", ChainID: 137, Symbol: "usdc", Decimals: 6} - require.NoError(t, db.Create(&asset).Error) - - ch := Channel{ - ChannelID: "0xChanJoining", - Participant: userAddress.Hex(), - Wallet: userAddress.Hex(), - Status: ChannelStatusJoining, - Token: asset.Token, - ChainID: 137, - RawAmount: decimal.NewFromInt(1000), - Version: 1, - } - require.NoError(t, db.Create(&ch).Error) - - allocateAmount := decimal.NewFromInt(100) - resizeParams := ResizeChannelParams{ - ChannelID: ch.ChannelID, - AllocateAmount: &allocateAmount, - FundsDestination: userAddress.Hex(), - } - - ctx := createSignedRPCContext(1, "resize_channel", resizeParams, userSigner) - router.HandleResizeChannel(ctx) - - assertErrorResponse(t, ctx, "channel 0xChanJoining is not open: joining") - }) - t.Run("ErrorOtherChallengedChannel", func(t *testing.T) { t.Parallel() @@ -1287,12 +1248,12 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok) // Should be initial amount (1000) + allocate amount (0) + resize amount (100) = 1100 expected := initialRawAmount.Add(allocateAmount) - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(expected)) + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(expected)) }) t.Run("SuccessfulResizeWithdrawal", func(t *testing.T) { @@ -1334,12 +1295,12 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok) // Should be initial amount (1000) + allocate amount (0) - resize amount (100) = 900 expected := initialRawAmount.Add(allocateAmount) - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(expected)) + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(expected)) }) t.Run("ErrorExcessiveDeallocation", func(t *testing.T) { @@ -1453,12 +1414,12 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok) // Verify the large allocation was processed correctly expectedAmount := decimal.NewFromInt(1000).Add(allocateAmount) // 1000 + 10^15 - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(expectedAmount)) + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(expectedAmount)) }) t.Run("SuccessfulAllocationWithResizeDeposit", func(t *testing.T) { @@ -1508,15 +1469,15 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) - require.Equal(t, ch.Version+1, resObj.Version) + require.Equal(t, ch.Version+1, resObj.State.Version) // New channel amount should be initial + AllocateAmount + ResizeAmount = 1000 + 150 + 100 = 1250 expected := initialRawAmount.Add(allocateAmount).Add(resizeAmount) - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(expected), "Combined allocation+resize amount mismatch") - require.Equal(t, 0, resObj.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(expected), "Combined allocation+resize amount mismatch") + require.Equal(t, 0, resObj.State.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") // Verify channel state in database remains unchanged (no update until blockchain confirmation) var unchangedChannel Channel @@ -1578,14 +1539,14 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ResizeChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) - require.Equal(t, ch.Version+1, resObj.Version) + require.Equal(t, ch.Version+1, resObj.State.Version) // New channel amount should be initial + AllocateAmount + ResizeAmount = 0 + 100 - 100 = 0 - require.Equal(t, 0, resObj.Allocations[0].RawAmount.Cmp(decimal.Zero), "Combined allocation+resize amount mismatch") - require.Equal(t, 0, resObj.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(decimal.Zero), "Combined allocation+resize amount mismatch") + require.Equal(t, 0, resObj.State.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") // Verify channel state in database remains unchanged (no update until blockchain confirmation) var unchangedChannel Channel @@ -1648,14 +1609,14 @@ func TestRPCRouterHandleCloseChannel(t *testing.T) { router.HandleCloseChannel(ctx) res := assertResponse(t, ctx, "close_channel") - resObj, ok := res.Params[0].(CloseChannelResponse) + resObj, ok := res.Params[0].(ChannelOperationResponse) require.True(t, ok, "Response should be CloseChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) - require.Equal(t, ch.Version+1, resObj.Version) + require.Equal(t, ch.Version+1, resObj.State.Version) // Final allocation should send full balance to destination - require.Equal(t, 0, resObj.FinalAllocations[0].RawAmount.Cmp(initialRawAmount), "Primary allocation mismatch") - require.Equal(t, 0, resObj.FinalAllocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") + require.Equal(t, 0, resObj.State.Allocations[0].RawAmount.Cmp(initialRawAmount), "Primary allocation mismatch") + require.Equal(t, 0, resObj.State.Allocations[1].RawAmount.Cmp(decimal.Zero), "Broker allocation should be zero") }) t.Run("ErrorOtherChannelInChallenge", func(t *testing.T) { t.Parallel() @@ -1712,3 +1673,215 @@ func TestRPCRouterHandleCloseChannel(t *testing.T) { assertErrorResponse(t, ctx, "has challenged channels") }) } + +func TestRPCRouterHandleCreateChannel(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + userSigner := Signer{privateKey: key} + userAddress := userSigner.GetAddress() + + t.Run("SuccessfulCreateChannel", func(t *testing.T) { + t.Parallel() + + router, db, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Seed asset with proper address format + asset := Asset{Token: "0x1234567890123456789012345678901234567890", ChainID: 137, Symbol: "usdc", Decimals: 6} + require.NoError(t, db.Create(&asset).Error) + + // Create channel params + amount := decimal.NewFromInt(1000000) // 1 USDC in raw units (6 decimals) + createParams := CreateChannelParams{ + ChainID: 137, + Token: asset.Token, + Amount: &amount, + } + + ctx := createSignedRPCContext(1, "create_channel", createParams, userSigner) + router.HandleCreateChannel(ctx) + + res := assertResponse(t, ctx, "create_channel") + resObj, ok := res.Params[0].(ChannelOperationResponse) + require.True(t, ok, "Response should be CreateChannelResponse") + + // Verify response structure + require.NotEmpty(t, resObj.ChannelID, "Channel ID should not be empty") + require.NotNil(t, resObj.State, "State should not be nil") + + // Verify state structure + require.Equal(t, uint8(1), resObj.State.Intent, "Intent should be INITIALIZE (1)") + require.Equal(t, uint64(0), resObj.State.Version, "Version should be 0") + require.Len(t, resObj.State.Allocations, 2, "Should have 2 allocations") + require.NotEmpty(t, resObj.StateSignature, "Should have 1 signature") + + // Verify allocations + require.Equal(t, userAddress.Hex(), resObj.State.Allocations[0].Participant, "First allocation should be for user") + require.Equal(t, asset.Token, resObj.State.Allocations[0].TokenAddress, "Token address should match") + require.Equal(t, amount, resObj.State.Allocations[0].RawAmount, "Amount should match") + + require.Equal(t, router.Signer.GetAddress().Hex(), resObj.State.Allocations[1].Participant, "Second allocation should be for broker") + require.Equal(t, asset.Token, resObj.State.Allocations[1].TokenAddress, "Token address should match") + require.True(t, resObj.State.Allocations[1].RawAmount.IsZero(), "Broker allocation should be zero") + }) + + t.Run("ErrorInvalidChainID", func(t *testing.T) { + t.Parallel() + + router, db, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Seed asset for unsupported chain ID to pass asset check first + asset := Asset{Token: "0xTokenCreate", ChainID: 999, Symbol: "usdc", Decimals: 6} + require.NoError(t, db.Create(&asset).Error) + + amount := decimal.NewFromInt(1000000) + createParams := CreateChannelParams{ + ChainID: 999, // Unsupported chain ID + Token: "0xTokenCreate", + Amount: &amount, + } + + ctx := createSignedRPCContext(1, "create_channel", createParams, userSigner) + router.HandleCreateChannel(ctx) + + assertErrorResponse(t, ctx, "unsupported chain ID") + }) + + t.Run("ErrorUnsupportedToken", func(t *testing.T) { + t.Parallel() + + router, _, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Don't seed any assets + amount := decimal.NewFromInt(1000000) + createParams := CreateChannelParams{ + ChainID: 137, + Token: "0xUnsupportedToken", + Amount: &amount, + } + + ctx := createSignedRPCContext(1, "create_channel", createParams, userSigner) + router.HandleCreateChannel(ctx) + + assertErrorResponse(t, ctx, "token not supported") + }) + + t.Run("ErrorExistingOpenChannel", func(t *testing.T) { + t.Parallel() + + router, db, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Seed asset + asset := Asset{Token: "0xTokenCreate", ChainID: 137, Symbol: "usdc", Decimals: 6} + require.NoError(t, db.Create(&asset).Error) + + // Create existing open channel + existingChannel := Channel{ + ChannelID: "0xExistingChannel", + Participant: userAddress.Hex(), + Wallet: userAddress.Hex(), + Status: ChannelStatusOpen, + Token: asset.Token, + ChainID: 137, + RawAmount: decimal.NewFromInt(500), + Version: 1, + } + require.NoError(t, db.Create(&existingChannel).Error) + + // Try to create another channel with same token/chain + amount := decimal.NewFromInt(1000000) + createParams := CreateChannelParams{ + ChainID: 137, + Token: asset.Token, + Amount: &amount, + } + + ctx := createSignedRPCContext(1, "create_channel", createParams, userSigner) + router.HandleCreateChannel(ctx) + + assertErrorResponse(t, ctx, "an open channel with broker already exists") + }) + + t.Run("ErrorInvalidSignature", func(t *testing.T) { + t.Parallel() + + router, db, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Seed asset + asset := Asset{Token: "0xTokenCreate", ChainID: 137, Symbol: "usdc", Decimals: 6} + require.NoError(t, db.Create(&asset).Error) + + // Create channel params + amount := decimal.NewFromInt(1000000) + createParams := CreateChannelParams{ + ChainID: 137, + Token: asset.Token, + Amount: &amount, + } + + // Create context without signature (empty signers) + ctx := createSignedRPCContext(1, "create_channel", createParams) + ctx.UserID = userAddress.Hex() // Set UserID but no signature + + router.HandleCreateChannel(ctx) + + assertErrorResponse(t, ctx, "invalid signature") + }) + + t.Run("SuccessfulZeroAmount", func(t *testing.T) { + t.Parallel() + + router, db, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Seed asset + asset := Asset{Token: "0xTokenCreate", ChainID: 137, Symbol: "usdc", Decimals: 6} + require.NoError(t, db.Create(&asset).Error) + + // Try to create channel with zero amount + amount := decimal.Zero + createParams := CreateChannelParams{ + ChainID: 137, + Token: asset.Token, + Amount: &amount, + } + + ctx := createSignedRPCContext(1, "create_channel", createParams, userSigner) + router.HandleCreateChannel(ctx) + + // This should work as zero amount channels are allowed + res := assertResponse(t, ctx, "create_channel") + resObj, ok := res.Params[0].(ChannelOperationResponse) + require.True(t, ok, "Response should be CreateChannelResponse") + require.True(t, resObj.State.Allocations[0].RawAmount.IsZero(), "User allocation should be zero") + }) + + t.Run("ErrorNegativeAmount", func(t *testing.T) { + t.Parallel() + + router, db, cleanup := setupTestRPCRouter(t) + t.Cleanup(cleanup) + + // Seed asset + asset := Asset{Token: "0xTokenCreate", ChainID: 137, Symbol: "usdc", Decimals: 6} + require.NoError(t, db.Create(&asset).Error) + + // Try to create channel with negative amount + amount := decimal.NewFromInt(-1000) + createParams := CreateChannelParams{ + ChainID: 137, + Token: asset.Token, + Amount: &amount, + } + + ctx := createSignedRPCContext(1, "create_channel", createParams, userSigner) + router.HandleCreateChannel(ctx) + + // This should fail during validation or processing + assertErrorResponse(t, ctx, "") + }) +} diff --git a/clearnode/rpc_router_public.go b/clearnode/rpc_router_public.go index 7b002dbe6..f78a32d07 100644 --- a/clearnode/rpc_router_public.go +++ b/clearnode/rpc_router_public.go @@ -68,7 +68,6 @@ type GetLedgerTransactionsParams struct { } type NetworkInfo struct { - Name string `json:"name"` ChainID uint32 `json:"chain_id"` CustodyAddress string `json:"custody_address"` AdjudicatorAddress string `json:"adjudicator_address"` @@ -99,9 +98,8 @@ func (r *RPCRouter) HandlePing(c *RPCContext) { func (r *RPCRouter) HandleGetConfig(c *RPCContext) { supportedNetworks := make([]NetworkInfo, 0, len(r.Config.networks)) - for name, networkConfig := range r.Config.networks { + for _, networkConfig := range r.Config.networks { supportedNetworks = append(supportedNetworks, NetworkInfo{ - Name: name, ChainID: networkConfig.ChainID, CustodyAddress: networkConfig.CustodyAddress, AdjudicatorAddress: networkConfig.AdjudicatorAddress, diff --git a/clearnode/rpc_router_public_test.go b/clearnode/rpc_router_public_test.go index 46ebefbdb..fa962d36c 100644 --- a/clearnode/rpc_router_public_test.go +++ b/clearnode/rpc_router_public_test.go @@ -52,10 +52,10 @@ func TestRPCRouterHandleGetConfig(t *testing.T) { defer cleanup() router.Config = &Config{ - networks: map[string]*NetworkConfig{ - "polygon": {Name: "polygon", ChainID: 137, InfuraURL: "https://polygon-mainnet.infura.io/v3/test", CustodyAddress: "0xCustodyAddress1"}, - "celo": {Name: "celo", ChainID: 42220, InfuraURL: "https://celo-mainnet.infura.io/v3/test", CustodyAddress: "0xCustodyAddress2"}, - "base": {Name: "base", ChainID: 8453, InfuraURL: "https://base-mainnet.infura.io/v3/test", CustodyAddress: "0xCustodyAddress3"}, + networks: map[uint32]*NetworkConfig{ + 137: {ChainID: 137, InfuraURL: "https://polygon-mainnet.infura.io/v3/test", CustodyAddress: "0xCustodyAddress1"}, + 42220: {ChainID: 42220, InfuraURL: "https://celo-mainnet.infura.io/v3/test", CustodyAddress: "0xCustodyAddress2"}, + 8453: {ChainID: 8453, InfuraURL: "https://base-mainnet.infura.io/v3/test", CustodyAddress: "0xCustodyAddress3"}, }, } @@ -68,17 +68,16 @@ func TestRPCRouterHandleGetConfig(t *testing.T) { assert.Equal(t, router.Signer.GetAddress().Hex(), configMap.BrokerAddress) require.Len(t, configMap.Networks, 3, "Should have 3 supported networks") - expectedNetworks := map[string]uint32{ - "polygon": 137, - "celo": 42220, - "base": 8453, + expectedNetworks := map[uint32]struct{}{ + 137: {}, + 42220: {}, + 8453: {}, } for _, network := range configMap.Networks { - expectedChainID, exists := expectedNetworks[network.Name] - assert.True(t, exists, "Network %s should be in expected networks", network.Name) - assert.Equal(t, expectedChainID, network.ChainID, "Chain ID should match for %s", network.Name) + _, exists := expectedNetworks[network.ChainID] + assert.True(t, exists, "Network %d should be in expected networks", network.ChainID) assert.Contains(t, network.CustodyAddress, "0xCustodyAddress", "Custody address should be present") - delete(expectedNetworks, network.Name) + delete(expectedNetworks, network.ChainID) } assert.Empty(t, expectedNetworks, "All expected networks should be found") } @@ -165,7 +164,7 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { ChannelID: "0xChannel3", Wallet: participantWallet, Participant: participantSigner, - Status: ChannelStatusJoining, + Status: ChannelStatusOpen, Nonce: 3, CreatedAt: baseTime.Add(2 * time.Hour), }, @@ -212,23 +211,18 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { { name: "Filter by status open", params: map[string]interface{}{"status": string(ChannelStatusOpen)}, - expectedChannelIDs: []string{"0xOtherChannel", "0xChannel1"}, + expectedChannelIDs: []string{"0xOtherChannel", "0xChannel3", "0xChannel1"}, }, { name: "Filter by participant and status open", params: map[string]interface{}{"participant": participantWallet, "status": string(ChannelStatusOpen)}, - expectedChannelIDs: []string{"0xChannel1"}, + expectedChannelIDs: []string{"0xChannel3", "0xChannel1"}, }, { name: "Filter by participant and status closed", params: map[string]interface{}{"participant": participantWallet, "status": string(ChannelStatusClosed)}, expectedChannelIDs: []string{"0xChannel2"}, }, - { - name: "Filter by participant and status joining", - params: map[string]interface{}{"participant": participantWallet, "status": string(ChannelStatusJoining)}, - expectedChannelIDs: []string{"0xChannel3"}, - }, { name: "Filter by status closed only", params: map[string]interface{}{"status": string(ChannelStatusClosed)}, @@ -268,14 +262,14 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { {Wallet: "0xWallet1", Participant: "0xParticipant1", Status: ChannelStatusOpen, Nonce: 1}, {Wallet: "0xWallet2", Participant: "0xParticipant2", Status: ChannelStatusClosed, Nonce: 2}, {Wallet: "0xWallet3", Participant: "0xParticipant3", Status: ChannelStatusOpen, Nonce: 3}, - {Wallet: "0xWallet4", Participant: "0xParticipant4", Status: ChannelStatusJoining, Nonce: 4}, - {Wallet: "0xWallet5", Participant: "0xParticipant5", Status: ChannelStatusOpen, Nonce: 5}, - {Wallet: "0xWallet6", Participant: "0xParticipant6", Status: ChannelStatusChallenged, Nonce: 6}, - {Wallet: "0xWallet7", Participant: "0xParticipant7", Status: ChannelStatusOpen, Nonce: 7}, - {Wallet: "0xWallet8", Participant: "0xParticipant8", Status: ChannelStatusClosed, Nonce: 8}, - {Wallet: "0xWallet9", Participant: "0xParticipant9", Status: ChannelStatusOpen, Nonce: 9}, - {Wallet: "0xWallet10", Participant: "0xParticipant10", Status: ChannelStatusJoining, Nonce: 10}, - {Wallet: "0xWallet11", Participant: "0xParticipant11", Status: ChannelStatusOpen, Nonce: 11}, + {Wallet: "0xWallet5", Participant: "0xParticipant5", Status: ChannelStatusOpen, Nonce: 4}, + {Wallet: "0xWallet6", Participant: "0xParticipant6", Status: ChannelStatusChallenged, Nonce: 5}, + {Wallet: "0xWallet7", Participant: "0xParticipant7", Status: ChannelStatusOpen, Nonce: 6}, + {Wallet: "0xWallet8", Participant: "0xParticipant8", Status: ChannelStatusClosed, Nonce: 7}, + {Wallet: "0xWallet9", Participant: "0xParticipant9", Status: ChannelStatusOpen, Nonce: 8}, + {Wallet: "0xWallet10", Participant: "0xParticipant10", Status: ChannelStatusOpen, Nonce: 9}, + {Wallet: "0xWallet11", Participant: "0xParticipant11", Status: ChannelStatusOpen, Nonce: 10}, + {Wallet: "0xWallet12", Participant: "0xParticipant12", Status: ChannelStatusOpen, Nonce: 11}, } for i := range testChannels { @@ -315,7 +309,7 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { }, {name: "Pagination with status filter", params: map[string]interface{}{"status": "open", "limit": float64(3)}, - expectedChannelIDs: []string{"0xChannel01", "0xChannel03", "0xChannel05"}, // Only open channels, first 3 + expectedChannelIDs: []string{"0xChannel01", "0xChannel03", "0xChannel04"}, // Only open channels, first 3 }, } diff --git a/clearnode/rpc_router_test.go b/clearnode/rpc_router_test.go index aafc21122..b4c5050cb 100644 --- a/clearnode/rpc_router_test.go +++ b/clearnode/rpc_router_test.go @@ -108,10 +108,28 @@ func setupTestRPCRouter(t *testing.T) (*RPCRouter, *gorm.DB, func()) { logger := NewLoggerIPFS("root.test") - channelService := NewChannelService(db, signer) node := NewRPCNode(signer, logger) wsNotifier := NewWSNotifier(node.Notify, logger) + networks := map[uint32]*NetworkConfig{ + 137: { + Name: "polygon", + ChainID: 137, + InfuraURL: "https://polygon-mainnet.infura.io/v3/test", + CustodyAddress: "0xCustodyAddress", + AdjudicatorAddress: "0xAdjudicatorAddress", + }, + 42220: { + Name: "celo", + ChainID: 42220, + InfuraURL: "https://celo-mainnet.infura.io/v3/test", + CustodyAddress: "0xCustodyAddress2", + AdjudicatorAddress: "0xAdjudicatorAddress2", + }, + } + + channelService := NewChannelService(db, networks, signer) + // Create an instance of RPCRouter router := &RPCRouter{ Node: node, diff --git a/contract/src/Custody.sol b/contract/src/Custody.sol index 3fb0e364b..279f528ce 100644 --- a/contract/src/Custody.sol +++ b/contract/src/Custody.sol @@ -238,7 +238,8 @@ contract Custody is IChannel, IDeposit, IChannelReader, EIP712 { channelId = Utils.getChannelId(ch); if (_channels[channelId].stage != ChannelStatus.VOID) revert InvalidStatus(); - if (initial.sigs.length != 1) revert InvalidStateSignatures(); + if (initial.sigs.length == 0 || initial.sigs.length > PART_NUM) revert InvalidStateSignatures(); + // TODO: later we can lift the restriction that first sig must be from CLIENT if ( !initial.verifyStateSignature( @@ -281,6 +282,25 @@ contract Custody is IChannel, IDeposit, IChannelReader, EIP712 { emit Created(channelId, wallet, ch, initial); + if (initial.sigs.length == PART_NUM) { + address serverAddress = ch.participants[SERVER_IDX]; + if (!initial.verifyStateSignature(channelId, _domainSeparatorV4(), initial.sigs[SERVER_IDX], serverAddress)) + { + revert InvalidStateSignatures(); + } + + meta.stage = ChannelStatus.ACTIVE; + Amount memory expectedDeposit = meta.expectedDeposits[SERVER_IDX]; + meta.actualDeposits[SERVER_IDX] = expectedDeposit; + meta.wallets[SERVER_IDX] = serverAddress; + _ledgers[serverAddress].channels.add(channelId); + + _lockAccountFundsToChannel(serverAddress, channelId, expectedDeposit.token, expectedDeposit.amount); + + emit Joined(channelId, SERVER_IDX); + emit Opened(channelId); + } + return channelId; } diff --git a/contract/test/Custody.t.sol b/contract/test/Custody.t.sol index 6e4bd82e0..cc40d1799 100644 --- a/contract/test/Custody.t.sol +++ b/contract/test/Custody.t.sol @@ -19,16 +19,17 @@ import {MessageHashUtils} from "lib/openzeppelin-contracts/contracts/utils/crypt import {ECDSA} from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {MockFlagERC1271} from "./mocks/MockFlagERC1271.sol"; // import to allow easier artifact fetching for `getCode` cheat code +import {FlagAdjudicator} from "./mocks/FlagAdjudicator.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; import {TestUtils} from "./TestUtils.sol"; + import {Custody} from "../src/Custody.sol"; import { Channel, State, Allocation, ChannelStatus, StateIntent, Amount, STATE_TYPEHASH } from "../src/interfaces/Types.sol"; +import {IChannel} from "../src/interfaces/IChannel.sol"; import {Utils} from "../src/Utils.sol"; -import {FlagAdjudicator} from "./mocks/FlagAdjudicator.sol"; -import {MockERC20} from "./mocks/MockERC20.sol"; - contract CustodyTest_Base is Test { Custody public custody; FlagAdjudicator public adjudicator; @@ -509,67 +510,253 @@ contract CustodyTest_challenge is CustodyTest_Base { } } -contract CustodyTest is CustodyTest_Base { - // ==================== TEST CASES ==================== +contract CustodyTest_create is CustodyTest_Base { + function setupChannelAndState() internal view returns (Channel memory chan, State memory initialState) { + chan = createTestChannelWithSK(); + initialState = createInitialStateWithSK(); - // ==== 1. Channel Creation and Joining ==== + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); + sigs[0] = hostSig; + initialState.sigs = sigs; + } - function test_Create() public { - // 1. Prepare channel and initial state - Channel memory chan = createTestChannelWithSK(); - State memory initialState = createInitialStateWithSK(); + function verifyChannelCreated(address creator, bytes32 channelId, Channel memory chan) internal view { + (uint256 available, uint256 channelCount) = getAvailableBalanceAndChannelCount(creator, address(token)); + assertEq(available, DEPOSIT_AMOUNT, "Host should have correct available balance"); + assertEq(channelCount, 1, "Host should have 1 channel"); + + bytes32 expectedChannelId = Utils.getChannelId(chan); + assertEq(channelId, expectedChannelId, "Channel ID is incorrect"); + + (, ChannelStatus status,,,) = custody.getChannelData(channelId); + assertTrue(status == ChannelStatus.INITIAL, "Channel should be INITIAL"); + } + + function verifyChannelActive(address[2] memory participants, bytes32 channelId, Channel memory chan) + internal + view + { + address[] memory participantsDyn = new address[](2); + participantsDyn[0] = participants[0]; + participantsDyn[1] = participants[1]; + bytes32[][] memory channels = custody.getOpenChannels(participantsDyn); + + assertEq(channels[0].length, 1, "Should have 1 channel for host"); + assertEq(channels[0][0], channelId, "Host's channel ID is incorrect"); + + assertEq(channels[1].length, 1, "Should have 1 channel for guest"); + assertEq(channels[1][0], channelId, "Guest's channel ID is incorrect"); + + bytes32 expectedChannelId = Utils.getChannelId(chan); + assertEq(channelId, expectedChannelId, "Channel ID is incorrect"); + + (, ChannelStatus status,,,) = custody.getChannelData(channelId); + assertTrue(status == ChannelStatus.ACTIVE, "Channel should be ACTIVE"); + } + + function verifyDeposited(address user, address tokenAddress, uint256 amount) internal view { + address[] memory users = new address[](1); + users[0] = user; + address[] memory tokens = new address[](1); + tokens[0] = tokenAddress; + uint256 available = custody.getAccountsBalances(users, tokens)[0][0]; + assertEq(available, amount, "User should have correct available balance"); + } + + function test_create() public { + (Channel memory chan, State memory initialState) = setupChannelAndState(); - // 2. Sign the state by the host vm.deal(hostSK, 1 ether); // Ensure host has ETH for gas - // Sign the state + vm.prank(hostSK); + custody.deposit(hostSK, address(token), DEPOSIT_AMOUNT * 2); + + vm.prank(hostSK); + bytes32 channelId = custody.create(chan, initialState); + + verifyChannelCreated(hostSK, channelId, chan); + } + + function test_depositAndCreate() public { + (Channel memory chan, State memory initialState) = setupChannelAndState(); + + vm.prank(hostSK); + bytes32 channelId = custody.depositAndCreate(address(token), DEPOSIT_AMOUNT * 2, chan, initialState); + + verifyChannelCreated(hostSK, channelId, chan); + } + + function test_create_revert_ifEmptyParticipants() public { + (Channel memory chan, State memory initialState) = setupChannelAndState(); + chan.participants = new address[](0); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); bytes[] memory sigs = new bytes[](1); sigs[0] = hostSig; initialState.sigs = sigs; - // 3. Deposit tokens for the host + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + vm.prank(hostSK); - custody.deposit(hostSK, address(token), DEPOSIT_AMOUNT * 2); + vm.expectRevert(Custody.InvalidParticipant.selector); + custody.create(chan, initialState); + } + + function test_create_revert_ifZeroAdjudicator() public { + (Channel memory chan, State memory initialState) = setupChannelAndState(); + chan.adjudicator = address(0); + + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); + sigs[0] = hostSig; + initialState.sigs = sigs; - // 4. Create the channel as host vm.prank(hostSK); - bytes32 channelId = custody.create(chan, initialState); + vm.expectRevert(Custody.InvalidAdjudicator.selector); + custody.create(chan, initialState); + } - // Verify the channel is created and in INITIAL state - (uint256 available, uint256 channelCount) = getAvailableBalanceAndChannelCount(hostSK, address(token)); - assertEq(available, DEPOSIT_AMOUNT, "Host should have correct available balance"); - assertEq(channelCount, 1, "Host should have 1 channel"); + function test_create_revert_ifZeroChallenge() public { + (Channel memory chan, State memory initialState) = setupChannelAndState(); + chan.challenge = 0; - // Also check that the channelId is consistent - bytes32 expectedChannelId = Utils.getChannelId(chan); - assertEq(channelId, expectedChannelId, "Channel ID is incorrect"); + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes[] memory sigs = new bytes[](1); + sigs[0] = hostSig; + initialState.sigs = sigs; + + vm.prank(hostSK); + vm.expectRevert(Custody.InvalidChallengePeriod.selector); + custody.create(chan, initialState); } - function test_depositAndCreate() public { - // 1. Prepare channel and initial state + function test_create_brokerAutoJoinWithTheirSig_zeroAllocation_rawECDSA() public { Channel memory chan = createTestChannelWithSK(); State memory initialState = createInitialStateWithSK(); + initialState.allocations[1].amount = 0; - // 2. Sign the state by the host bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); - bytes[] memory sigs = new bytes[](1); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes[] memory sigs = new bytes[](2); sigs[0] = hostSig; + sigs[1] = guestSig; initialState.sigs = sigs; - // 3. Create the channel as host + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + + bytes32 channelId = Utils.getChannelId(chan); + + vm.expectEmit(true, true, true, true); + emit IChannel.Joined(channelId, 1); + vm.expectEmit(true, true, true, true); + emit IChannel.Opened(channelId); vm.prank(hostSK); - bytes32 channelId = custody.depositAndCreate(address(token), DEPOSIT_AMOUNT * 2, chan, initialState); + custody.create(chan, initialState); - // Verify the channel is created and in INITIAL state - (uint256 available, uint256 channelCount) = getAvailableBalanceAndChannelCount(hostSK, address(token)); - assertEq(available, DEPOSIT_AMOUNT, "Host should have correct available balance"); - assertEq(channelCount, 1, "Host should have 1 channel"); + verifyChannelActive([hostSK, guestSK], channelId, chan); + verifyDeposited(hostSK, address(token), DEPOSIT_AMOUNT); + verifyDeposited(guestSK, address(token), 0); // No funds have been allocated to guest, so they keep their full deposit + } - // Also check that the channelId is consistent - bytes32 expectedChannelId = Utils.getChannelId(chan); - assertEq(channelId, expectedChannelId, "Channel ID is incorrect"); + function test_create_brokerAutoJoinWithTheirSig_zeroAllocation_EIP191() public { + Channel memory chan = createTestChannelWithSK(); + State memory initialState = createInitialStateWithSK(); + initialState.allocations[1].amount = 0; + + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes32 channelId = Utils.getChannelId(chan); + bytes memory guestSig = TestUtils.signStateEIP191(vm, channelId, initialState, guestSKPrivKey); + bytes[] memory sigs = new bytes[](2); + sigs[0] = hostSig; + sigs[1] = guestSig; + initialState.sigs = sigs; + + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + + vm.prank(hostSK); + custody.create(chan, initialState); + + verifyChannelActive([hostSK, guestSK], channelId, chan); + verifyDeposited(hostSK, address(token), DEPOSIT_AMOUNT); + verifyDeposited(guestSK, address(token), 0); // No funds have been allocated to guest, so they keep their full deposit + } + + function test_create_brokerAutoJoinWithTheirSig_zeroAllocation_EIP712() public { + Channel memory chan = createTestChannelWithSK(); + State memory initialState = createInitialStateWithSK(); + initialState.allocations[1].amount = 0; + + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes32 channelId = Utils.getChannelId(chan); + bytes memory guestSig = TestUtils.signStateEIP712( + vm, channelId, initialState, STATE_TYPEHASH, custodyDomainSeparator, guestSKPrivKey + ); + bytes[] memory sigs = new bytes[](2); + sigs[0] = hostSig; + sigs[1] = guestSig; + initialState.sigs = sigs; + + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + + vm.prank(hostSK); + custody.create(chan, initialState); + + verifyChannelActive([hostSK, guestSK], channelId, chan); + verifyDeposited(hostSK, address(token), DEPOSIT_AMOUNT); + verifyDeposited(guestSK, address(token), 0); // No funds have been allocated to guest, so they keep their full deposit + } + + function test_create_brokerAutoJoinWithTheirSig_nonZeroAllocation() public { + Channel memory chan = createTestChannelWithSK(); + State memory initialState = createInitialStateWithSK(); + + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes[] memory sigs = new bytes[](2); + sigs[0] = hostSig; + sigs[1] = guestSig; + initialState.sigs = sigs; + + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + depositTokens(guestSK, DEPOSIT_AMOUNT * 2); + + vm.prank(hostSK); + bytes32 channelId = custody.create(chan, initialState); + + verifyChannelActive([hostSK, guestSK], channelId, chan); + verifyDeposited(hostSK, address(token), DEPOSIT_AMOUNT); + verifyDeposited(guestSK, address(token), DEPOSIT_AMOUNT); + } + + function test_create_revert_whenBrokerAutoJoinWithTheirSig_insufficientFunds() public { + Channel memory chan = createTestChannelWithSK(); + State memory initialState = createInitialStateWithSK(); + + // Both CLIENT and SERVER sign the initial state + bytes memory hostSig = signState(chan, initialState, hostSKPrivKey); + bytes memory guestSig = signState(chan, initialState, guestSKPrivKey); + bytes[] memory sigs = new bytes[](2); + sigs[0] = hostSig; + sigs[1] = guestSig; + initialState.sigs = sigs; + + // CLIENT deposits enough, but SERVER doesn't have sufficient funds + depositTokens(hostSK, DEPOSIT_AMOUNT * 2); + depositTokens(guestSK, DEPOSIT_AMOUNT / 2); // Not enough for their DEPOSIT_AMOUNT allocation + + vm.prank(hostSK); + vm.expectRevert( + abi.encodeWithSelector(Custody.InsufficientBalance.selector, DEPOSIT_AMOUNT / 2, DEPOSIT_AMOUNT) + ); + custody.create(chan, initialState); } +} + +contract CustodyTest is CustodyTest_Base { + // ==================== TEST CASES ==================== + + // ==== 1. Channel Creation and Joining ==== function test_CompleteChannelFunding() public { // 1. Create channel with host @@ -609,57 +796,6 @@ contract CustodyTest is CustodyTest_Base { assertEq(guestAvailable, DEPOSIT_AMOUNT, "Guest should have correct available balance"); } - function test_InvalidChannelCreation() public { - // Create channel with invalid participant (empty array) - address[] memory invalidParticipants = new address[](1); - invalidParticipants[0] = hostSK; - Channel memory chanWithInvalidParticipants = Channel({ - participants: invalidParticipants, - adjudicator: address(adjudicator), - challenge: CHALLENGE_DURATION, - nonce: NONCE - }); - - State memory initialState = createInitialStateWithSK(); - bytes memory hostSig = signState(chanWithInvalidParticipants, initialState, hostSKPrivKey); - bytes[] memory sigs = new bytes[](1); - sigs[0] = hostSig; - initialState.sigs = sigs; - - depositTokens(hostSK, DEPOSIT_AMOUNT * 2); - - vm.startPrank(hostSK); - vm.expectRevert(Custody.InvalidParticipant.selector); - custody.create(chanWithInvalidParticipants, initialState); - vm.stopPrank(); - - // Test with zero address as adjudicator - address[] memory validParticipants = new address[](2); - validParticipants[0] = hostSK; - validParticipants[1] = guestSK; - - Channel memory chanWithZeroAdjudicator = Channel({ - participants: validParticipants, - adjudicator: address(0), - challenge: CHALLENGE_DURATION, - nonce: NONCE - }); - - vm.startPrank(hostSK); - vm.expectRevert(Custody.InvalidAdjudicator.selector); - custody.create(chanWithZeroAdjudicator, initialState); - vm.stopPrank(); - - // Test with zero challenge period - Channel memory chanWithZeroChallenge = - Channel({participants: validParticipants, adjudicator: address(adjudicator), challenge: 0, nonce: NONCE}); - - vm.startPrank(hostSK); - vm.expectRevert(Custody.InvalidChallengePeriod.selector); - custody.create(chanWithZeroChallenge, initialState); - vm.stopPrank(); - } - // ==== 2. Channel Closing ==== function test_CooperativeClose() public { diff --git a/contract/test/Custody_integration.t.sol b/contract/test/Custody_integration.t.sol index 753e31409..da0aa2be9 100644 --- a/contract/test/Custody_integration.t.sol +++ b/contract/test/Custody_integration.t.sol @@ -26,6 +26,8 @@ contract CustodyIntegrationTest_Signatures is Test { MockERC20 public token; CheatERC6492Factory public factory; + bytes32 custodyDomainSeparator; + // Test participants address public participant1; address public participant2; @@ -53,6 +55,8 @@ contract CustodyIntegrationTest_Signatures is Test { token = new MockERC20("Test Token", "TEST", 18); factory = new CheatERC6492Factory(); + custodyDomainSeparator = TestUtils.buildDomainSeparatorForContract(custody); + // Set up participants participant1PrivateKey = vm.createWallet("participant1").privateKey; participant2PrivateKey = vm.createWallet("participant2").privateKey; @@ -87,41 +91,6 @@ contract CustodyIntegrationTest_Signatures is Test { // ==================== SIGNATURE HELPERS ==================== - function _signStateRaw(State memory state, uint256 privateKey) internal view returns (bytes memory) { - bytes memory packedState = Utils.getPackedState(channelId, state); - return TestUtils.sign(vm, privateKey, packedState); - } - - function _signStateEIP191(State memory state, uint256 privateKey) internal view returns (bytes memory) { - bytes memory packedState = Utils.getPackedState(channelId, state); - return TestUtils.signEIP191(vm, privateKey, packedState); - } - - function _signStateEIP712(State memory state, uint256 privateKey) internal view returns (bytes memory) { - (, string memory name, string memory version, uint256 chainId, address verifyingContract,,) = - custody.eip712Domain(); - bytes32 domainSeparator = keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name)), - keccak256(bytes(version)), - chainId, - verifyingContract - ) - ); - bytes32 structHash = keccak256( - abi.encode( - STATE_TYPEHASH, - channelId, - state.intent, - state.version, - keccak256(state.data), - keccak256(abi.encode(state.allocations)) - ) - ); - return TestUtils.signEIP712(vm, privateKey, domainSeparator, structHash); - } - function _signStateEIP6492(address signer, State memory) internal view returns (bytes memory) { bytes memory signature = "dummy signature"; bool flag = true; // meaning each EIP-1271 signature is valid @@ -171,7 +140,7 @@ contract CustodyIntegrationTest_Signatures is Test { // Create initial state - participant1 uses EIP191 State memory initialState = _createInitialState(); initialState.sigs = new bytes[](1); - initialState.sigs[0] = _signStateEIP191(initialState, participant1PrivateKey); + initialState.sigs[0] = TestUtils.signStateEIP191(vm, channelId, initialState, participant1PrivateKey); vm.prank(participant1); custody.depositAndCreate(address(token), DEPOSIT_AMOUNT, channel, initialState); @@ -186,7 +155,7 @@ contract CustodyIntegrationTest_Signatures is Test { custody.deposit(participant2, address(token), DEPOSIT_AMOUNT); // Participant2 joins using raw ECDSA signature - bytes memory participant2JoinSig = _signStateRaw(initialState, participant2PrivateKey); + bytes memory participant2JoinSig = TestUtils.signStateRaw(vm, channelId, initialState, participant2PrivateKey); vm.prank(participant2); custody.join(channelId, 1, participant2JoinSig); @@ -200,8 +169,11 @@ contract CustodyIntegrationTest_Signatures is Test { // Create challenge state - participant1 uses EIP712, participant2 uses raw ECDSA State memory challengeState = _createOperateState(1, bytes("challenge data")); challengeState.sigs = new bytes[](2); - challengeState.sigs[PARTICIPANT_1] = _signStateEIP712(challengeState, participant1PrivateKey); - challengeState.sigs[PARTICIPANT_2] = _signStateRaw(challengeState, participant2PrivateKey); + challengeState.sigs[PARTICIPANT_1] = TestUtils.signStateEIP712( + vm, channelId, challengeState, STATE_TYPEHASH, custodyDomainSeparator, participant1PrivateKey + ); + challengeState.sigs[PARTICIPANT_2] = + TestUtils.signStateRaw(vm, channelId, challengeState, participant2PrivateKey); bytes memory challengerSig = _signChallenge(challengeState, participant1PrivateKey); @@ -219,8 +191,10 @@ contract CustodyIntegrationTest_Signatures is Test { // Create checkpoint state with higher version - participant1 uses raw ECDSA, participant2 uses raw ECDSA State memory checkpointState = _createOperateState(2, bytes("checkpoint data")); checkpointState.sigs = new bytes[](2); - checkpointState.sigs[PARTICIPANT_1] = _signStateRaw(checkpointState, participant1PrivateKey); - checkpointState.sigs[PARTICIPANT_2] = _signStateRaw(checkpointState, participant2PrivateKey); + checkpointState.sigs[PARTICIPANT_1] = + TestUtils.signStateRaw(vm, channelId, checkpointState, participant1PrivateKey); + checkpointState.sigs[PARTICIPANT_2] = + TestUtils.signStateRaw(vm, channelId, checkpointState, participant2PrivateKey); vm.prank(participant2); custody.checkpoint(channelId, checkpointState, new State[](0)); @@ -236,7 +210,8 @@ contract CustodyIntegrationTest_Signatures is Test { checkpointState = _createOperateState(3, bytes("checkpoint data")); checkpointState.sigs = new bytes[](2); checkpointState.sigs[PARTICIPANT_1] = _signStateEIP6492(participant1, checkpointState); - checkpointState.sigs[PARTICIPANT_2] = _signStateRaw(checkpointState, participant2PrivateKey); + checkpointState.sigs[PARTICIPANT_2] = + TestUtils.signStateRaw(vm, channelId, checkpointState, participant2PrivateKey); vm.prank(participant2); custody.checkpoint(channelId, checkpointState, new State[](0)); @@ -251,8 +226,8 @@ contract CustodyIntegrationTest_Signatures is Test { State memory finalState = _createFinalState(4); finalState.sigs = new bytes[](2); // as participant1 already has a contract at its address, we assume this contract expects EIP-191 signature - finalState.sigs[PARTICIPANT_1] = _signStateEIP191(finalState, participant1PrivateKey); - finalState.sigs[PARTICIPANT_2] = _signStateRaw(finalState, participant2PrivateKey); + finalState.sigs[PARTICIPANT_1] = TestUtils.signStateEIP191(vm, channelId, finalState, participant1PrivateKey); + finalState.sigs[PARTICIPANT_2] = TestUtils.signStateRaw(vm, channelId, finalState, participant2PrivateKey); vm.prank(participant1); custody.close(channelId, finalState, new State[](0)); diff --git a/contract/test/TestUtils.sol b/contract/test/TestUtils.sol index 733a976c0..bcccad1ea 100644 --- a/contract/test/TestUtils.sol +++ b/contract/test/TestUtils.sol @@ -53,12 +53,21 @@ library TestUtils { return abi.encodePacked(r, s, v); } - function signStateEIP191(Vm vm, Channel memory channel, State memory state, uint256 privateKey) + function signStateRaw(Vm vm, bytes32 channelId, State memory state, uint256 privateKey) internal - view + pure + returns (bytes memory) + { + bytes memory packedState = Utils.getPackedState(channelId, state); + return sign(vm, privateKey, packedState); + } + + function signStateEIP191(Vm vm, bytes32 channelId, State memory state, uint256 privateKey) + internal + pure returns (bytes memory) { - bytes memory packedState = Utils.getPackedState(Utils.getChannelId(channel), state); + bytes memory packedState = Utils.getPackedState(channelId, state); return TestUtils.signEIP191(vm, privateKey, packedState); } diff --git a/erc7824-docs/docs/erc-7824.md b/erc7824-docs/docs/erc-7824.md index a18b170f4..f58d4542d 100644 --- a/erc7824-docs/docs/erc-7824.md +++ b/erc7824-docs/docs/erc-7824.md @@ -197,7 +197,9 @@ interface IChannel { /** * @notice Creates a new channel and initializes funding - * @dev The creator must sign the funding state with StateIntent.INITIALIZE + * @dev Creator must sign the funding state with StateIntent.INITIALIZE. + * If both participants sign the initial state, channel opens immediately in ACTIVE status. + * If only creator signs, channel enters INITIAL status awaiting join(). * @param ch Channel configuration * @param initial Initial state with StateIntent.INITIALIZE and expected allocations * @return channelId Unique identifier for the created channel @@ -207,7 +209,8 @@ interface IChannel { /** * @notice Allows a participant to join a channel by signing the funding state - * @dev Participant must provide signature on the same funding state + * @dev Only needed when channel was created with single signature. + * Participant must provide signature on the same funding state. * @param channelId Unique identifier for the channel * @param index Index of the participant in the channel's participants array * @param sig Signature of the participant on the funding state @@ -300,16 +303,22 @@ interface IDeposit { ### Channel Lifecycle -1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE`. Second participant are able to join a channel immediately by providing a signature over initial state, and funds will be deducted from their account, if available. -2. **Joining**: Participants verify the channel and sign the same funding state. This step can be omitted by providing a signature over the initial state when creating the channel. Note, however, that this means that funds will be locked from the participant's balance, while `join(...)` allows to fund the channel from external account. -3. **Active**: Once fully funded, the channel transitions to active state for off-chain operation -4. **Off-chain Updates**: Participants exchange and sign state updates according to application logic -5. **Resolution**: +1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE`. The channel can be opened immediately in one transaction if both participants provide signatures over the initial state, with both participants' funds being deducted from their available balances. +2. **Active**: Once fully funded (either through single-transaction creation or separate join), the channel transitions to active state for off-chain operation +3. **Off-chain Updates**: Participants exchange and sign state updates according to application logic +4. **Resolution**: - **Cooperative Close**: All parties sign a final state with `StateIntent.FINALIZE` - **Challenge-Response**: Participant can post a state on-chain and initiate challenge period - **Checkpoint**: Record valid state on-chain without closing for future dispute resolution - **Resize**: Adjust allocations by agreement without closing the channel +#### Two-Phase Opening (Legacy) + +For scenarios requiring separate funding from external accounts: + +1. **Creation**: Creator calls `create()` with single signature, channel enters INITIAL status +2. **Joining**: Second participant calls `join()` with their signature, transitioning channel to ACTIVE status + ### Example Implementation: Remittance Adjudicator The Remittance adjudicator validates payment transfers between participants: @@ -406,10 +415,21 @@ State memory fundingState = State({ sigs: [aliceSignature, bobSignature] }); -// Alice creates and funds the channel +// Alice creates and funds the channel - opens immediately since both signatures provided bytes32 channelId = custody.create(channel, fundingState); -// Bob joins with his signature +// Alternative: Single-signature creation followed by join +State memory fundingStatePartial = State({ + intent: StateIntent.INITIALIZE, + version: 0, + data: "", + allocations: [ + Allocation(alice, tokenAddress, 100 ether), + Allocation(bob, tokenAddress, 50 ether) + ], + sigs: [aliceSignature] // Only Alice's signature +}); +bytes32 channelId = custody.create(channel, fundingStatePartial); custody.join(channelId, 1, bobSignature); // Off-chain: Alice pays Bob 10 tokens diff --git a/integration/common/nitroliteClient.ts b/integration/common/nitroliteClient.ts index 52b11494c..d7e316377 100644 --- a/integration/common/nitroliteClient.ts +++ b/integration/common/nitroliteClient.ts @@ -1,6 +1,9 @@ import { Allocation, + convertRPCToClientChannel, + convertRPCToClientState, createCloseChannelMessage, + createCreateChannelMessage, NitroliteClient, RPCChannelStatus, rpcResponseParser, @@ -8,7 +11,12 @@ import { import { Identity } from './identity'; import { Address, createPublicClient, Hex, http } from 'viem'; import { chain, CONFIG } from './setup'; -import { getChannelUpdatePredicateWithStatus, getCloseChannelPredicate, TestWebSocket } from './ws'; +import { + getChannelUpdatePredicateWithStatus, + getCloseChannelPredicate, + getCreateChannelPredicate, + TestWebSocket, +} from './ws'; export class TestNitroliteClient extends NitroliteClient { constructor(private identity: Identity) { @@ -35,9 +43,18 @@ export class TestNitroliteClient extends NitroliteClient { createAndWaitForChannel = async ( ws: TestWebSocket, - { tokenAddress, amount, depositAmount }: { tokenAddress: Address; amount: bigint, depositAmount?: bigint } + { tokenAddress, amount, depositAmount }: { tokenAddress: Address; amount: bigint; depositAmount?: bigint } ) => { - depositAmount = depositAmount ?? amount; + const msg = await createCreateChannelMessage(this.identity.messageSigner, { + chain_id: chain.id, + token: tokenAddress, + amount, + session_key: this.identity.sessionAddress, + }); + const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); + expect(createResponse).toBeDefined(); + + const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), @@ -45,9 +62,14 @@ export class TestNitroliteClient extends NitroliteClient { 5000 ); + depositAmount = depositAmount ?? amount; const { initialState } = await this.depositAndCreateChannel(tokenAddress, depositAmount, { - initialAllocationAmounts: [amount, BigInt(0)], - stateData: '0x', + unsignedInitialState: convertRPCToClientState( + createParsedResponseParams.state, + createParsedResponseParams.serverSignature + ), + channel: convertRPCToClientChannel(createParsedResponseParams.channel), + serverSignature: createParsedResponseParams.serverSignature, }); const openResponse = await openChannelPromise; @@ -76,25 +98,25 @@ export class TestNitroliteClient extends NitroliteClient { await this.closeChannel({ finalState: { - intent: closeParsedResponse.params.intent, - channelId: closeParsedResponse.params.channelId as Hex, - data: closeParsedResponse.params.stateData as Hex, + intent: closeParsedResponse.params.state.intent, + channelId: closeParsedResponse.params.channelId, + data: closeParsedResponse.params.state.stateData, allocations: [ { - destination: closeParsedResponse.params.allocations[0].destination as Address, - token: closeParsedResponse.params.allocations[0].token as Address, - amount: closeParsedResponse.params.allocations[0].amount, + destination: closeParsedResponse.params.state.allocations[0].destination as Address, + token: closeParsedResponse.params.state.allocations[0].token as Address, + amount: closeParsedResponse.params.state.allocations[0].amount, }, { - destination: closeParsedResponse.params.allocations[1].destination as Address, - token: closeParsedResponse.params.allocations[1].token as Address, - amount: closeParsedResponse.params.allocations[1].amount, + destination: closeParsedResponse.params.state.allocations[1].destination as Address, + token: closeParsedResponse.params.state.allocations[1].token as Address, + amount: closeParsedResponse.params.state.allocations[1].amount, }, ] as [Allocation, Allocation], - version: BigInt(closeParsedResponse.params.version), + version: BigInt(closeParsedResponse.params.state.version), serverSignature: closeParsedResponse.params.serverSignature, }, - stateData: closeParsedResponse.params.stateData as Hex, + stateData: closeParsedResponse.params.state.stateData, }); const closeChannelUpdateResponse = await closeChannelUpdateChannelPromise; diff --git a/integration/common/ws.ts b/integration/common/ws.ts index 9094d8459..1a2728b31 100644 --- a/integration/common/ws.ts +++ b/integration/common/ws.ts @@ -190,6 +190,12 @@ export const getChannelUpdatePredicateWithStatus = (status: RPCChannelStatus) => }; }; +export const getCreateChannelPredicate = () => { + return (data: string, reqId?: number): boolean => { + return genericPredicate(data, (r) => r.method === RPCMethod.CreateChannel, reqId); + } +}; + export const getCloseChannelPredicate = () => { return (data: string, reqId?: number): boolean => { return genericPredicate(data, (r) => r.method === RPCMethod.CloseChannel, reqId); diff --git a/integration/tests/challenge_channel.test.ts b/integration/tests/challenge_channel.test.ts deleted file mode 100644 index 29ea7578d..000000000 --- a/integration/tests/challenge_channel.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { createAuthSessionWithClearnode } from '@/auth'; -import { BlockchainUtils } from '@/blockchainUtils'; -import { DatabaseUtils } from '@/databaseUtils'; -import { Identity } from '@/identity'; -import { TestNitroliteClient } from '@/nitroliteClient'; -import { CONFIG } from '@/setup'; -import { getChannelUpdatePredicateWithStatus, TestWebSocket, getGetLedgerEntriesPredicate } from '@/ws'; -import { createGetLedgerEntriesMessage, RPCChannelStatus, rpcResponseParser } from '@erc7824/nitrolite'; -import { parseUnits, GetTxpoolContentReturnType, Hash } from 'viem'; - -describe('Close channel', () => { - const depositAmount = parseUnits('100', 6); // 100 USDC (decimals = 6) - const decimalDepositAmount = 100; - - let ws: TestWebSocket; - let identity: Identity; - let client: TestNitroliteClient; - let blockUtils: BlockchainUtils; - let databaseUtils: DatabaseUtils; - - beforeAll(async () => { - blockUtils = new BlockchainUtils(); - databaseUtils = new DatabaseUtils(); - identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); - ws = new TestWebSocket(CONFIG.CLEARNODE_URL, CONFIG.DEBUG_MODE); - - await blockUtils.resumeMining(); - }); - - beforeEach(async () => { - await ws.connect(); - await createAuthSessionWithClearnode(ws, identity); - await blockUtils.makeSnapshot(); - }); - - afterEach(async () => { - ws.close(); - await databaseUtils.cleanupDatabaseData(); - await blockUtils.resetSnapshot(); - }); - - afterAll(async () => { - databaseUtils.close(); - - await blockUtils.resumeMining(); - }); - - it('should create nitrolite client to challenge channels', async () => { - client = new TestNitroliteClient(identity); - - expect(client).toBeDefined(); - expect(client).toHaveProperty('depositAndCreateChannel'); - expect(client).toHaveProperty('challengeChannel'); - }); - - it('should challenge channel in joining state', async () => { - const joiningChannelPromise = ws.waitForMessage( - getChannelUpdatePredicateWithStatus(RPCChannelStatus.Joining), - undefined, - 5000 - ); - - const hash = await client.approveTokens(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, depositAmount); - await blockUtils.waitForTransaction(hash); - - await blockUtils.pauseMining(); - - const { channelId, txHash: createTxHash } = await client.depositAndCreateChannel( - CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, - depositAmount, - { - initialAllocationAmounts: [depositAmount, BigInt(0)], - stateData: '0x', - } - ); - - // Mine exactly one block to ensure the transaction is processed and join is not mined - const depositTxPromise = blockUtils.waitForTransaction(createTxHash); - await blockUtils.mineBlock(); - await depositTxPromise; - - const { lastValidState } = await client.getChannelData(channelId); - const poolWithJoin: GetTxpoolContentReturnType = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - clearInterval(interval); - reject(new Error('Timed out waiting for pending transaction in txpool')); - }, 5000); - - const interval = setInterval(async () => { - const pool = await blockUtils.readTxPool(); - if (Object.keys(pool.pending).length > 0) { - clearInterval(interval); - clearTimeout(timeout); - resolve(pool); - } - }, 200); - }); - - // TODO: this approach is very brittle, and could fail if there are multiple pending transactions - // which usually doesn't happen in tests, but still - const txKey = Object.keys(poolWithJoin.pending)[0]; - const txIndex = Object.keys(poolWithJoin.pending[txKey])[0]; - const joinTx = poolWithJoin.pending[txKey][txIndex]; - - await blockUtils.dropTxFromPool(joinTx.hash as Hash); - - const challengeTxHash = await client.challengeChannel({ - channelId, - candidateState: lastValidState, - }); - - const challengeTxPromise = blockUtils.waitForTransaction(challengeTxHash); - await blockUtils.mineBlock(); - await challengeTxPromise; - - const joinTxHash = await blockUtils.sendRawTransactionAs( - CONFIG.IDENTITIES[0].WALLET_PK, - { - chainId: Number(BigInt(joinTx.chainId)), - nonce: Number(BigInt(joinTx.nonce)), - gasPrice: BigInt(joinTx.gasPrice), - gas: BigInt(joinTx.gas), - to: joinTx.to, - value: BigInt(joinTx.value), - data: joinTx.input, - }, - { - v: BigInt(joinTx.v), - r: joinTx.r, - s: joinTx.s, - } - ); - - const joinTxPromise = blockUtils.waitForTransaction(joinTxHash); - await blockUtils.mineBlock(); - await joinTxPromise; - - const channelData = await client.getChannelData(channelId); - expect(channelData).toBeDefined(); - - const joiningResponse = await joiningChannelPromise; - expect(joiningResponse).toBeDefined(); - - const msg = await createGetLedgerEntriesMessage(identity.messageSigner, channelId); - const response = await ws.sendAndWaitForResponse(msg, getGetLedgerEntriesPredicate(), 5000); - - const { params: parsedResponseParams } = rpcResponseParser.getLedgerEntries(response); - expect(parsedResponseParams).toBeDefined(); - - expect(parsedResponseParams).toHaveLength(2); - expect(+parsedResponseParams[0].debit + +parsedResponseParams[1].debit).toEqual(decimalDepositAmount); - expect(+parsedResponseParams[0].credit + +parsedResponseParams[1].credit).toEqual(decimalDepositAmount); - }); -}); diff --git a/integration/tests/close_channel.test.ts b/integration/tests/close_channel.test.ts index 8ecd11d5e..40e8934b8 100644 --- a/integration/tests/close_channel.test.ts +++ b/integration/tests/close_channel.test.ts @@ -75,14 +75,14 @@ describe('Close channel', () => { const closeChannelTxHash = await client.closeChannel({ finalState: { - intent: closeParsedResponse.params.intent, + intent: closeParsedResponse.params.state.intent, channelId: closeParsedResponse.params.channelId, - data: closeParsedResponse.params.stateData as Hex, - allocations: closeParsedResponse.params.allocations, - version: BigInt(closeParsedResponse.params.version), + data: closeParsedResponse.params.state.stateData as Hex, + allocations: closeParsedResponse.params.state.allocations, + version: BigInt(closeParsedResponse.params.state.version), serverSignature: closeParsedResponse.params.serverSignature, }, - stateData: closeParsedResponse.params.stateData as Hex, + stateData: closeParsedResponse.params.state.stateData as Hex, }); expect(closeChannelTxHash).toBeDefined(); diff --git a/integration/tests/create_channel.test.ts b/integration/tests/create_channel.test.ts index 04d9ecda0..01365fb40 100644 --- a/integration/tests/create_channel.test.ts +++ b/integration/tests/create_channel.test.ts @@ -3,9 +3,15 @@ import { BlockchainUtils } from '@/blockchainUtils'; import { DatabaseUtils } from '@/databaseUtils'; import { Identity } from '@/identity'; import { TestNitroliteClient } from '@/nitroliteClient'; -import { CONFIG } from '@/setup'; -import { getChannelUpdatePredicateWithStatus, TestWebSocket } from '@/ws'; -import { RPCChannelStatus, rpcResponseParser } from '@erc7824/nitrolite'; +import { chain, CONFIG } from '@/setup'; +import { getChannelUpdatePredicateWithStatus, getCreateChannelPredicate, TestWebSocket } from '@/ws'; +import { + convertRPCToClientChannel, + convertRPCToClientState, + createCreateChannelMessage, + RPCChannelStatus, + rpcResponseParser, +} from '@erc7824/nitrolite'; import { parseUnits } from 'viem'; describe('Create channel', () => { @@ -56,11 +62,17 @@ describe('Create channel', () => { identity.walletAddress ); - const joiningChannelPromise = ws.waitForMessage( - getChannelUpdatePredicateWithStatus(RPCChannelStatus.Joining), - undefined, - 5000 - ); + const msg = await createCreateChannelMessage(identity.messageSigner, { + chain_id: chain.id, + token: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, + amount: depositAmount, + session_key: identity.sessionAddress, + }); + const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); + expect(createResponse).toBeDefined(); + + const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), undefined, @@ -71,8 +83,12 @@ describe('Create channel', () => { CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, depositAmount, { - initialAllocationAmounts: [depositAmount, BigInt(0)], - stateData: '0x', + unsignedInitialState: convertRPCToClientState( + createParsedResponseParams.state, + createParsedResponseParams.serverSignature + ), + channel: convertRPCToClientChannel(createParsedResponseParams.channel), + serverSignature: createParsedResponseParams.serverSignature, } ); @@ -83,9 +99,6 @@ describe('Create channel', () => { const receipt = await blockUtils.waitForTransaction(txHash); expect(receipt).toBeDefined(); - const joiningResponse = await joiningChannelPromise; - expect(joiningResponse).toBeDefined(); - const openResponse = await openChannelPromise; expect(openResponse).toBeDefined(); @@ -133,6 +146,17 @@ describe('Create channel', () => { expect(postBalance.rawBalance).toBe(prevBalance.rawBalance - depositAmount); + const msg = await createCreateChannelMessage(identity.messageSigner, { + chain_id: chain.id, + token: CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, + amount: depositAmount, + session_key: identity.sessionAddress, + }); + const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); + expect(createResponse).toBeDefined(); + + const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), undefined, @@ -143,9 +167,13 @@ describe('Create channel', () => { txHash: createChannelTxHash, channelId, initialState, - } = await client.createChannel(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS, { - initialAllocationAmounts: [depositAmount, BigInt(0)], - stateData: '0x', + } = await client.createChannel({ + unsignedInitialState: convertRPCToClientState( + createParsedResponseParams.state, + createParsedResponseParams.serverSignature + ), + channel: convertRPCToClientChannel(createParsedResponseParams.channel), + serverSignature: createParsedResponseParams.serverSignature, }); expect(channelId).toBeDefined(); diff --git a/integration/tests/lifecycle.test.ts b/integration/tests/lifecycle.test.ts index 020b48f61..caabce687 100644 --- a/integration/tests/lifecycle.test.ts +++ b/integration/tests/lifecycle.test.ts @@ -387,14 +387,14 @@ describe('Close channel', () => { const { params: closeResponseParams } = rpcResponseParser.closeChannel(closeResponse); const closeChannelTxHash = await client.closeChannel({ finalState: { - intent: closeResponseParams.intent, + intent: closeResponseParams.state.intent, channelId: closeResponseParams.channelId, - data: closeResponseParams.stateData as Hex, - allocations: closeResponseParams.allocations, - version: BigInt(closeResponseParams.version), + data: closeResponseParams.state.stateData as Hex, + allocations: closeResponseParams.state.allocations, + version: BigInt(closeResponseParams.state.version), serverSignature: closeResponseParams.serverSignature, }, - stateData: closeResponseParams.stateData as Hex, + stateData: closeResponseParams.state.stateData as Hex, }); expect(closeChannelTxHash).toBeDefined(); @@ -417,22 +417,22 @@ describe('Close channel', () => { const resizeResponse = await cpWS.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); - expect(resizeResponseParams.allocations).toBeDefined(); - expect(resizeResponseParams.allocations).toHaveLength(2); - expect(String(resizeResponseParams.allocations[0].destination)).toBe(cpIdentity.walletAddress); - expect(String(resizeResponseParams.allocations[0].amount)).toBe( + expect(resizeResponseParams.state.allocations).toBeDefined(); + expect(resizeResponseParams.state.allocations).toHaveLength(2); + expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(cpIdentity.walletAddress); + expect(String(resizeResponseParams.state.allocations[0].amount)).toBe( (depositAmount * BigInt(11)).toString() // 1000 + 100 ); - expect(String(resizeResponseParams.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); - expect(String(resizeResponseParams.allocations[1].amount)).toBe('0'); + expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); + expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); const resizeChannelTxHash = await cpClient.resizeChannel({ resizeState: { channelId: resizeResponseParams.channelId as Hex, - intent: resizeResponseParams.intent, - version: BigInt(resizeResponseParams.version), - data: resizeResponseParams.stateData as Hex, - allocations: resizeResponseParams.allocations, + intent: resizeResponseParams.state.intent, + version: BigInt(resizeResponseParams.state.version), + data: resizeResponseParams.state.stateData as Hex, + allocations: resizeResponseParams.state.allocations, serverSignature: resizeResponseParams.serverSignature, }, proofStates: [ @@ -472,14 +472,14 @@ describe('Close channel', () => { const { params: closeResponseParams } = rpcResponseParser.closeChannel(closeResponse); const closeChannelTxHash = await cpClient.closeChannel({ finalState: { - intent: closeResponseParams.intent, + intent: closeResponseParams.state.intent, channelId: closeResponseParams.channelId, - data: closeResponseParams.stateData as Hex, - allocations: closeResponseParams.allocations, - version: BigInt(closeResponseParams.version), + data: closeResponseParams.state.stateData as Hex, + allocations: closeResponseParams.state.allocations, + version: BigInt(closeResponseParams.state.version), serverSignature: closeResponseParams.serverSignature, }, - stateData: closeResponseParams.stateData as Hex, + stateData: closeResponseParams.state.stateData as Hex, }); expect(closeChannelTxHash).toBeDefined(); diff --git a/integration/tests/resize_channel.test.ts b/integration/tests/resize_channel.test.ts index 41e8e5ea0..7ba5f7dea 100644 --- a/integration/tests/resize_channel.test.ts +++ b/integration/tests/resize_channel.test.ts @@ -76,30 +76,30 @@ describe('Resize channel', () => { const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); expect(resizeResponseParams.channelId).toBe(createResponseParams.channelId); - expect(resizeResponseParams.stateData).toBeDefined(); - expect(resizeResponseParams.intent).toBe(2); // StateIntent.RESIZE // TODO: add enum to sdk - expect(resizeResponseParams.version).toBe(createResponseParams.version + 1); + expect(resizeResponseParams.state.stateData).toBeDefined(); + expect(resizeResponseParams.state.intent).toBe(2); // StateIntent.RESIZE // TODO: add enum to sdk + expect(resizeResponseParams.state.version).toBe(createResponseParams.version + 1); expect(resizeResponseParams.serverSignature).toBeDefined(); - expect(resizeResponseParams.allocations).toBeDefined(); - expect(resizeResponseParams.allocations).toHaveLength(2); - expect(String(resizeResponseParams.allocations[0].destination)).toBe(identity.walletAddress); - expect(String(resizeResponseParams.allocations[0].token)).toBe(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(String(resizeResponseParams.allocations[0].amount)).toBe( + expect(resizeResponseParams.state.allocations).toBeDefined(); + expect(resizeResponseParams.state.allocations).toHaveLength(2); + expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); + expect(String(resizeResponseParams.state.allocations[0].token)).toBe(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); + expect(String(resizeResponseParams.state.allocations[0].amount)).toBe( (depositAmount * BigInt(6)).toString() // 500 + 100 ); - expect(String(resizeResponseParams.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); - expect(String(resizeResponseParams.allocations[1].token)).toBe(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); - expect(String(resizeResponseParams.allocations[1].amount)).toBe('0'); + expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); + expect(String(resizeResponseParams.state.allocations[1].token)).toBe(CONFIG.ADDRESSES.USDC_TOKEN_ADDRESS); + expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); const resizeChannelTxHash = await client.resizeChannel({ resizeState: { channelId: resizeResponseParams.channelId as Hex, - intent: resizeResponseParams.intent, - version: BigInt(resizeResponseParams.version), - data: resizeResponseParams.stateData as Hex, - allocations: resizeResponseParams.allocations, + intent: resizeResponseParams.state.intent, + version: BigInt(resizeResponseParams.state.version), + data: resizeResponseParams.state.stateData as Hex, + allocations: resizeResponseParams.state.allocations, serverSignature: resizeResponseParams.serverSignature, }, proofStates: [ @@ -166,22 +166,22 @@ describe('Resize channel', () => { const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); - expect(resizeResponseParams.allocations).toBeDefined(); - expect(resizeResponseParams.allocations).toHaveLength(2); - expect(String(resizeResponseParams.allocations[0].destination)).toBe(identity.walletAddress); - expect(String(resizeResponseParams.allocations[0].amount)).toBe( + expect(resizeResponseParams.state.allocations).toBeDefined(); + expect(resizeResponseParams.state.allocations).toHaveLength(2); + expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); + expect(String(resizeResponseParams.state.allocations[0].amount)).toBe( (depositAmount * BigInt(4)).toString() // 500 - 100 ); - expect(String(resizeResponseParams.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); - expect(String(resizeResponseParams.allocations[1].amount)).toBe('0'); + expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); + expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); const resizeChannelTxHash = await client.resizeChannel({ resizeState: { channelId: resizeResponseParams.channelId as Hex, - intent: resizeResponseParams.intent, - version: BigInt(resizeResponseParams.version), - data: resizeResponseParams.stateData as Hex, - allocations: resizeResponseParams.allocations, + intent: resizeResponseParams.state.intent, + version: BigInt(resizeResponseParams.state.version), + data: resizeResponseParams.state.stateData as Hex, + allocations: resizeResponseParams.state.allocations, serverSignature: resizeResponseParams.serverSignature, }, proofStates: [ @@ -248,22 +248,22 @@ describe('Resize channel', () => { const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); - expect(resizeResponseParams.allocations).toBeDefined(); - expect(resizeResponseParams.allocations).toHaveLength(2); - expect(String(resizeResponseParams.allocations[0].destination)).toBe(identity.walletAddress); - expect(String(resizeResponseParams.allocations[0].amount)).toBe( + expect(resizeResponseParams.state.allocations).toBeDefined(); + expect(resizeResponseParams.state.allocations).toHaveLength(2); + expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); + expect(String(resizeResponseParams.state.allocations[0].amount)).toBe( (depositAmount * BigInt(4)).toString() // 500 - 100 ); - expect(String(resizeResponseParams.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); - expect(String(resizeResponseParams.allocations[1].amount)).toBe('0'); + expect(String(resizeResponseParams.state.allocations[1].destination)).toBe(CONFIG.ADDRESSES.GUEST_ADDRESS); + expect(String(resizeResponseParams.state.allocations[1].amount)).toBe('0'); const resizeChannelTxHash = await client.resizeChannel({ resizeState: { channelId: resizeResponseParams.channelId as Hex, - intent: resizeResponseParams.intent, - version: BigInt(resizeResponseParams.version), - data: resizeResponseParams.stateData as Hex, - allocations: resizeResponseParams.allocations, + intent: resizeResponseParams.state.intent, + version: BigInt(resizeResponseParams.state.version), + data: resizeResponseParams.state.stateData as Hex, + allocations: resizeResponseParams.state.allocations, serverSignature: resizeResponseParams.serverSignature, }, proofStates: [ diff --git a/sdk/src/client/index.ts b/sdk/src/client/index.ts index 0dae6edf3..d2ab365d1 100644 --- a/sdk/src/client/index.ts +++ b/sdk/src/client/index.ts @@ -128,16 +128,14 @@ export class NitroliteClient { * @returns The channel ID, the signed initial state, and the transaction hash. */ async createChannel( - tokenAddress: Address, params: CreateChannelParams, ): Promise<{ channelId: ChannelId; initialState: State; txHash: Hash }> { try { - const { channel, initialState, channelId } = await _prepareAndSignInitialState( - tokenAddress, + const { initialState, channelId } = await _prepareAndSignInitialState( this.sharedDeps, params, ); - const txHash = await this.nitroliteService.createChannel(channel, initialState); + const txHash = await this.nitroliteService.createChannel(params.channel, initialState); return { channelId, initialState, txHash }; } catch (err) { @@ -161,8 +159,7 @@ export class NitroliteClient { try { const owner = this.account.address; const spender = this.addresses.custody; - const { channel, initialState, channelId } = await _prepareAndSignInitialState( - tokenAddress, + const { initialState, channelId } = await _prepareAndSignInitialState( this.sharedDeps, params, ); @@ -183,7 +180,7 @@ export class NitroliteClient { const txHash = await this.nitroliteService.depositAndCreateChannel( tokenAddress, depositAmount, - channel, + params.channel, initialState, ); diff --git a/sdk/src/client/prepare.ts b/sdk/src/client/prepare.ts index 275f4a5fb..4c40fd565 100644 --- a/sdk/src/client/prepare.ts +++ b/sdk/src/client/prepare.ts @@ -108,14 +108,11 @@ export class NitroliteTransactionPreparer { * @param params Parameters for channel creation. See {@link CreateChannelParams}. * @returns The prepared transaction data ({ to, data, value }). */ - async prepareCreateChannelTransaction( - tokenAddress: Address, - params: CreateChannelParams, - ): Promise { + async prepareCreateChannelTransaction(params: CreateChannelParams): Promise { try { - const { channel, initialState } = await _prepareAndSignInitialState(tokenAddress, this.deps, params); + const { initialState } = await _prepareAndSignInitialState(this.deps, params); - return await this.deps.nitroliteService.prepareCreateChannel(channel, initialState); + return await this.deps.nitroliteService.prepareCreateChannel(params.channel, initialState); } catch (err) { if (err instanceof Errors.NitroliteError) throw err; throw new Errors.ContractCallError('prepareCreateChannelTransaction', err as Error, { params }); @@ -156,11 +153,11 @@ export class NitroliteTransactionPreparer { } try { - const { channel, initialState } = await _prepareAndSignInitialState(tokenAddress, this.deps, params); + const { initialState } = await _prepareAndSignInitialState(this.deps, params); const depositTx = await this.deps.nitroliteService.prepareDepositAndCreateChannel( tokenAddress, amount, - channel, + params.channel, initialState, ); transactions.push(depositTx); diff --git a/sdk/src/client/signer.ts b/sdk/src/client/signer.ts index f3189c5b9..a8981b0cb 100644 --- a/sdk/src/client/signer.ts +++ b/sdk/src/client/signer.ts @@ -1,5 +1,5 @@ import { Account, Address, Chain, Hex, ParseAccount, toHex, Transport, WalletClient } from 'viem'; -import { State } from './types'; +import { State, UnsignedState } from './types'; import { getPackedState, getStateHash } from '../utils'; import { signRawECDSAMessage } from '../utils/sign'; import { privateKeyToAccount } from 'viem/accounts'; @@ -24,7 +24,7 @@ export interface StateSigner { * @param state The state to sign. * @returns A Promise that resolves to the signature as a Hex string. */ - signState(channelId: Hex, state: State): Promise; + signState(channelId: Hex, state: UnsignedState): Promise; /** * Sign a raw message. * @param message The message to sign as a Hex string. diff --git a/sdk/src/client/state.ts b/sdk/src/client/state.ts index 188b22a53..050879cbe 100644 --- a/sdk/src/client/state.ts +++ b/sdk/src/client/state.ts @@ -1,6 +1,12 @@ import { Address, Hex } from 'viem'; import * as Errors from '../errors'; -import { generateChannelNonce, getChannelId, getPackedChallengeState } from '../utils'; +import { + generateChannelNonce, + getChannelId, + getPackedChallengeState, + getPackedState, + getStateHash, +} from '../utils'; import { PreparerDependencies } from './prepare'; import { ChallengeChannelParams, @@ -17,73 +23,51 @@ import { /** * Shared logic for preparing the channel object, initial state, and signing it. * Used by both direct execution (createChannel) and preparation (prepareCreateChannelTransaction). - * @param tokenAddress The address of the token for the channel. * @param deps - The dependencies needed (account, addresses, walletClient, challengeDuration). See {@link PreparerDependencies}. * @param params - Parameters for channel creation. See {@link CreateChannelParams}. - * @returns An object containing the channel object, the signed initial state, and the channel ID. + * @returns An object containing the signed initial state, and the channel ID. * @throws {Errors.MissingParameterError} If the default adjudicator address is missing. * @throws {Errors.InvalidParameterError} If participants are invalid. */ export async function _prepareAndSignInitialState( - tokenAddress: Address, deps: PreparerDependencies, params: CreateChannelParams, -): Promise<{ channel: Channel; initialState: State; channelId: ChannelId }> { - const { initialAllocationAmounts, stateData } = params; +): Promise<{ initialState: State; channelId: ChannelId }> { + const { channel, unsignedInitialState, serverSignature } = params; - if (!stateData) { - throw new Errors.MissingParameterError('State data is required for creating the channel'); + if (!unsignedInitialState) { + throw new Errors.MissingParameterError('Initial state is required for creating the channel'); } - const channelNonce = generateChannelNonce(deps.account.address); - - const participants: [Hex, Hex] = [deps.account.address, deps.addresses.guestAddress]; - const channelParticipants: [Hex, Hex] = [deps.stateSigner.getAddress(), deps.addresses.guestAddress]; - const adjudicatorAddress = deps.addresses.adjudicator; - if (!adjudicatorAddress) { - throw new Errors.MissingParameterError( - 'Default adjudicator address is not configured in addresses.adjudicator', - ); + if (!unsignedInitialState.data) { + throw new Errors.MissingParameterError('State data is required for creating the channel'); } - const challengeDuration = deps.challengeDuration; + if (!unsignedInitialState.allocations || unsignedInitialState.allocations.length !== 2) { + throw new Errors.InvalidParameterError('Initial allocation amounts must be provided for both participants.'); + } - if (!participants || participants.length !== 2) { - throw new Errors.InvalidParameterError('Channel must have two participants.'); + if (!channel) { + throw new Errors.MissingParameterError("Channel's fixed part is required for creating the channel"); } - if (!initialAllocationAmounts || initialAllocationAmounts.length !== 2) { - throw new Errors.InvalidParameterError('Initial allocation amounts must be provided for both participants.'); + if (!channel?.adjudicator) { + throw new Errors.MissingParameterError('Adjudicator address is required for creating the channel'); } - const channel: Channel = { - participants: channelParticipants, - adjudicator: adjudicatorAddress, - challenge: challengeDuration, - nonce: channelNonce, - }; + if (!channel.participants || channel.participants.length !== 2) { + throw new Errors.InvalidParameterError('Channel must have exactly two participants.'); + } const channelId = getChannelId(channel, deps.chainId); - - const stateToSign: State = { - data: stateData, - intent: StateIntent.INITIALIZE, - allocations: [ - { destination: participants[0], token: tokenAddress, amount: initialAllocationAmounts[0] }, - { destination: participants[1], token: tokenAddress, amount: initialAllocationAmounts[1] }, - ], - // The state version is set to 0 for the initial state. - version: 0n, - sigs: [], - }; - - const accountSignature = await deps.stateSigner.signState(channelId, stateToSign); - const initialState: State = { - ...stateToSign, - sigs: [accountSignature], + const accountSignature = await deps.stateSigner.signState(channelId, unsignedInitialState); + const signedInitialState: State = { + ...unsignedInitialState, + // TODO: remove assumption, that current signer will always be the first participant + sigs: [accountSignature, serverSignature], }; - return { channel, initialState, channelId }; + return { initialState: signedInitialState, channelId }; } /** diff --git a/sdk/src/client/types.ts b/sdk/src/client/types.ts index 16edc1961..9486429e3 100644 --- a/sdk/src/client/types.ts +++ b/sdk/src/client/types.ts @@ -69,7 +69,7 @@ export interface ChannelData { lastValidState: State; // Last valid state of the channel recorded on-chain } -interface UnsignedState { +export interface UnsignedState { intent: StateIntent; // Intent of the state (uint8 enum in contract) version: bigint; // Version of the state (uint256 in contract) data: Hex; // Application data encoded (bytes in contract) @@ -144,8 +144,9 @@ export interface NitroliteClientConfig { * Parameters required for creating a new state channel. */ export interface CreateChannelParams { - initialAllocationAmounts: [bigint, bigint]; - stateData?: Hex; + channel: Channel; + unsignedInitialState: UnsignedState; + serverSignature: Signature; } /** diff --git a/sdk/src/rpc/api.ts b/sdk/src/rpc/api.ts index 3cde29a3b..285c61219 100644 --- a/sdk/src/rpc/api.ts +++ b/sdk/src/rpc/api.ts @@ -27,6 +27,7 @@ import { GetLedgerTransactionsFilters, GetLedgerTransactionsRequestParams, TransferRequestParams, + CreateChannelRequestParams, } from './types/request'; import { signRawECDSAMessage } from '../utils/sign'; @@ -410,6 +411,17 @@ export async function createApplicationMessage( return JSON.stringify(signedRequest); } +export async function createCreateChannelMessage( + signer: MessageSigner, + params: CreateChannelRequestParams, + requestId: RequestID = generateRequestId(), + timestamp: Timestamp = getCurrentTimestamp(), +): Promise { + const request = NitroliteRPC.createRequest(requestId, RPCMethod.CreateChannel, [params], timestamp); + const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); + return JSON.stringify(signedRequest, (_, value) => (typeof value === 'bigint' ? value.toString() : value)); +} + /** * Creates the signed, stringified message body for a 'close_channel' request. * diff --git a/sdk/src/rpc/parse/channel.ts b/sdk/src/rpc/parse/channel.ts index 5ffe9833e..ee5c3094f 100644 --- a/sdk/src/rpc/parse/channel.ts +++ b/sdk/src/rpc/parse/channel.ts @@ -2,6 +2,8 @@ import { z } from 'zod'; import { Address, Hex } from 'viem'; import { RPCMethod, + ChannelOperationResponseParams, + CreateChannelResponseParams, ResizeChannelResponseParams, CloseChannelResponseParams, GetChannelsResponseParams, @@ -14,69 +16,79 @@ import { hexSchema, addressSchema, statusEnum, ParamsParser } from './common'; const RPCAllocationSchema = z.object({ destination: addressSchema, token: addressSchema, - amount: z.string().transform((a) => BigInt(a)), + amount: z.string(), }); -const ResizeChannelParamsSchema = z - .array( - z - .object({ - channel_id: hexSchema, - state_data: hexSchema, - intent: z.number(), - version: z.number(), - allocations: z.array(RPCAllocationSchema), - server_signature: hexSchema, - }) - .transform( - (raw) => - ({ - channelId: raw.channel_id as Hex, - stateData: raw.state_data as Hex, - intent: raw.intent, - version: raw.version, - allocations: raw.allocations.map((a) => ({ - destination: a.destination as Address, - token: a.token as Address, - amount: a.amount, - })), - serverSignature: raw.server_signature, - }) as ResizeChannelResponseParams, - ), - ) +const ChannelOperationObject = z.object({ + channel_id: hexSchema, + state: z.object({ + intent: z.number(), + version: z.number(), + state_data: z.string(), + allocations: z.array(RPCAllocationSchema), + }), + server_signature: hexSchema, +}); + +const ChannelOperationObjectSchema = ChannelOperationObject.transform( + (raw) => + ({ + channelId: raw.channel_id as Hex, + state: { + intent: raw.state.intent, + version: raw.state.version, + stateData: raw.state.state_data as Hex, + allocations: raw.state.allocations.map((a) => ({ + destination: a.destination as Address, + token: a.token as Address, + amount: BigInt(a.amount), + })), + }, + serverSignature: raw.server_signature, + }) as ChannelOperationResponseParams, +); + +const ChannelOperationParamsSchema = z + .array(ChannelOperationObjectSchema) .refine((arr) => arr.length === 1) .transform((arr) => arr[0]); -const CloseChannelParamsSchema = z +const CreateChannelParamsSchema = z .array( z .object({ - channel_id: hexSchema, - state_data: hexSchema, - intent: z.number(), - version: z.number(), - allocations: z.array(RPCAllocationSchema), - server_signature: hexSchema, + ...ChannelOperationObject.shape, + channel: z.object({ + participants: z.array(addressSchema), + adjudicator: addressSchema, + challenge: z.number(), + nonce: z.number(), + }), }) .transform( - (raw) => + (params) => ({ - channelId: raw.channel_id as Hex, - stateData: raw.state_data as Hex, - intent: raw.intent, - version: raw.version, - allocations: raw.allocations.map((a) => ({ - destination: a.destination as Address, - token: a.token as Address, - amount: a.amount, - })), - serverSignature: raw.server_signature, - }) as CloseChannelResponseParams, + ...ChannelOperationObjectSchema.parse(params), + channel: { + participants: params.channel.participants, + adjudicator: params.channel.adjudicator, + challenge: params.channel.challenge, + nonce: params.channel.nonce, + }, + }) as CreateChannelResponseParams, ), ) .refine((arr) => arr.length === 1) .transform((arr) => arr[0]); +const ResizeChannelParamsSchema = ChannelOperationParamsSchema.transform( + (params) => params as ResizeChannelResponseParams, +); + +const CloseChannelParamsSchema = ChannelOperationParamsSchema.transform( + (params) => params as CloseChannelResponseParams, +); + const ChannelUpdateObjectSchema = z .object({ channel_id: hexSchema, @@ -129,6 +141,7 @@ const ChannelsUpdateParamsSchema = z .transform((arr) => arr[0]); export const channelParamsParsers: Record> = { + [RPCMethod.CreateChannel]: (params) => CreateChannelParamsSchema.parse(params), [RPCMethod.ResizeChannel]: (params) => ResizeChannelParamsSchema.parse(params), [RPCMethod.CloseChannel]: (params) => CloseChannelParamsSchema.parse(params), [RPCMethod.GetChannels]: (params) => GetChannelsParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/parse.ts b/sdk/src/rpc/parse/parse.ts index 104a5db48..e88e388a9 100644 --- a/sdk/src/rpc/parse/parse.ts +++ b/sdk/src/rpc/parse/parse.ts @@ -82,6 +82,7 @@ export const rpcResponseParser = { closeAppSession: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseAppSession), getAppDefinition: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppDefinition), getAppSessions: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppSessions), + createChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateChannel), resizeChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ResizeChannel), closeChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseChannel), getChannels: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetChannels), diff --git a/sdk/src/rpc/types/index.ts b/sdk/src/rpc/types/index.ts index c84491143..543f0daec 100644 --- a/sdk/src/rpc/types/index.ts +++ b/sdk/src/rpc/types/index.ts @@ -20,7 +20,6 @@ export type ResponseData = [RequestID, RPCMethod, object[], Timestamp?]; /** Represents the status of a channel. */ export enum RPCChannelStatus { - Joining = 'joining', Open = 'open', Closed = 'closed', Challenged = 'challenged', @@ -326,6 +325,7 @@ export enum RPCMethod { CloseAppSession = 'close_app_session', GetAppDefinition = 'get_app_definition', GetAppSessions = 'get_app_sessions', + CreateChannel = 'create_channel', ResizeChannel = 'resize_channel', CloseChannel = 'close_channel', GetChannels = 'get_channels', diff --git a/sdk/src/rpc/types/request.ts b/sdk/src/rpc/types/request.ts index 513ad0810..6d1cfb0a2 100644 --- a/sdk/src/rpc/types/request.ts +++ b/sdk/src/rpc/types/request.ts @@ -258,6 +258,21 @@ export interface GetAppSessionsRequest extends GenericRPCMessage { params: [GetAppSessionsRequestParams]; } +/** + * Represents the request parameters for the 'create_channel' RPC method. + */ +export interface CreateChannelRequestParams { + /** The blockchain network ID where the channel should be created. */ + chain_id: number; + /** The token contract address for the channel. */ + token: Address; + /** The initial amount to deposit in the channel (in raw token units). */ + amount: bigint; + /** Optional session key address for the channel. */ + session_key?: Hex; +} +export type CreateChannelRPCRequestParams = CreateChannelRequestParams; // for backward compatibility + /** * Represents the request parameters for the 'resize_channel' RPC method. */ @@ -273,6 +288,14 @@ export type ResizeChannelRequestParams = { }; export type ResizeChannelRPCRequestParams = ResizeChannelRequestParams; // for backward compatibility +/** + * Represents the request structure for the 'create_channel' RPC method. + */ +export interface CreateChannelRequest extends GenericRPCMessage { + method: RPCMethod.CreateChannel; + params: [CreateChannelRequestParams]; +} + /** * Represents the request structure for the 'resize_channel' RPC method. */ @@ -449,6 +472,7 @@ export type RPCRequest = | CloseAppSessionRequest | GetAppDefinitionRequest | GetAppSessionsRequest + | CreateChannelRequest | ResizeChannelRequest | CloseChannelRequest | GetChannelsRequest @@ -476,6 +500,7 @@ export type RPCRequestParamsByMethod = { [RPCMethod.CloseAppSession]: CloseAppSessionRequestParams; [RPCMethod.GetAppDefinition]: GetAppDefinitionRequestParams; [RPCMethod.GetAppSessions]: GetAppSessionsRequestParams; + [RPCMethod.CreateChannel]: CreateChannelRequestParams; [RPCMethod.ResizeChannel]: ResizeChannelRequestParams; [RPCMethod.CloseChannel]: CloseChannelRequestParams; [RPCMethod.GetChannels]: GetChannelsRequestParams; diff --git a/sdk/src/rpc/types/response.ts b/sdk/src/rpc/types/response.ts index cf9fd697a..620f1c1d3 100644 --- a/sdk/src/rpc/types/response.ts +++ b/sdk/src/rpc/types/response.ts @@ -220,41 +220,59 @@ export interface RPCAllocation { } /** - * Represents the parameters for the 'resize_channel' RPC method. + * Represents the state object within channel operation responses. */ -export interface ResizeChannelResponseParams { - /** The unique identifier for the channel. */ - channelId: Hex; - /** The encoded state data for the channel. */ - stateData: Hex; +export interface ChannelOperationState { /** The intent type for the state update. */ intent: number; /** The version number of the channel. */ version: number; + /** The encoded state data for the channel. */ + stateData: Hex; /** The list of allocations for the channel. */ allocations: RPCAllocation[]; - /** The server's signature for the state update. */ - serverSignature: ServerSignature; } -export type ResizeChannelRPCResponseParams = ResizeChannelResponseParams; // for backward compatibility /** - * Represents the parameters for the 'close_channel' RPC method. + * Represents the fixed part of a channel, containing essential metadata. */ -export interface CloseChannelResponseParams { +export interface RPCChannel { + participants: Address[]; + adjudicator: Address; + challenge: number; + nonce: number; +} + +/** + * Represents the unified parameters for channel operations (create, resize, close). + */ +export interface ChannelOperationResponseParams { /** The unique identifier for the channel. */ channelId: Hex; - /** The intent type for the state update. */ - intent: number; - /** The version number of the channel. */ - version: number; - /** The encoded state data for the channel. */ - stateData: Hex; - /** The list of final allocations for the channel. */ - allocations: RPCAllocation[]; + /** The channel state object. */ + state: ChannelOperationState; /** The server's signature for the state update. */ serverSignature: ServerSignature; } + +/** + * Represents the parameters for the 'create_channel' RPC method. + */ +export interface CreateChannelResponseParams extends ChannelOperationResponseParams { + channel: RPCChannel; +} +export type CreateChannelRPCResponseParams = CreateChannelResponseParams; // for backward compatibility + +/** + * Represents the parameters for the 'resize_channel' RPC method. + */ +export interface ResizeChannelResponseParams extends ChannelOperationResponseParams {} +export type ResizeChannelRPCResponseParams = ResizeChannelResponseParams; // for backward compatibility + +/** + * Represents the parameters for the 'close_channel' RPC method. + */ +export interface CloseChannelResponseParams extends ChannelOperationResponseParams {} export type CloseChannelRPCResponseParams = CloseChannelResponseParams; // for backward compatibility /** @@ -391,6 +409,14 @@ export interface GetAppSessionsResponse extends GenericRPCMessage { params: GetAppSessionsResponseParams[]; } +/** + * Represents the response structure for the 'create_channel' RPC method. + */ +export interface CreateChannelResponse extends GenericRPCMessage { + method: RPCMethod.CreateChannel; + params: CreateChannelResponseParams; +} + /** * Represents the response structure for the 'resize_channel' RPC method. */ @@ -624,6 +650,7 @@ export type RPCResponse = | CloseAppSessionResponse | GetAppDefinitionResponse | GetAppSessionsResponse + | CreateChannelResponse | ResizeChannelResponse | CloseChannelResponse | GetChannelsResponse diff --git a/sdk/src/utils/channel.ts b/sdk/src/utils/channel.ts index 35b98a3f8..0ff7f162d 100644 --- a/sdk/src/utils/channel.ts +++ b/sdk/src/utils/channel.ts @@ -1,5 +1,6 @@ import { keccak256, encodeAbiParameters, Address } from 'viem'; -import { Channel, ChannelId } from '../client/types'; // Updated import path +import { Channel, ChannelId, State } from '../client/types'; // Updated import path +import { ChannelOperationState, RPCChannel, ServerSignature } from '../rpc'; /** * Compute the unique identifier for a channel based on its configuration. @@ -55,3 +56,26 @@ export function generateChannelNonce(address?: Address): bigint { return nonce; } + +export function convertRPCToClientChannel(ch: RPCChannel): Channel { + return { + participants: ch.participants, + adjudicator: ch.adjudicator, + challenge: BigInt(ch.challenge), + nonce: BigInt(ch.nonce), + }; +} + +export function convertRPCToClientState(s: ChannelOperationState, sig: ServerSignature): State { + return { + intent: s.intent, + version: BigInt(s.version), + data: s.stateData, + allocations: s.allocations.map((a) => ({ + token: a.token, + destination: a.destination, + amount: a.amount, + })), + sigs: [sig], + }; +} diff --git a/sdk/src/utils/state.ts b/sdk/src/utils/state.ts index db586b21c..29963c4d1 100644 --- a/sdk/src/utils/state.ts +++ b/sdk/src/utils/state.ts @@ -1,5 +1,5 @@ import { keccak256, encodeAbiParameters, Address, Hex, recoverMessageAddress, encodePacked } from 'viem'; -import { State, StateHash, Signature, ChannelId } from '../client/types'; // Updated import path +import { UnsignedState, State, StateHash, Signature, ChannelId } from '../client/types'; // Updated import path /** * Packs a channel state into a canonical format for hashing and signing. @@ -7,7 +7,7 @@ import { State, StateHash, Signature, ChannelId } from '../client/types'; // Upd * @param state The state to pack. * @returns The packed state as Hex. */ -export function getPackedState(channelId: ChannelId, state: State): Hex { +export function getPackedState(channelId: ChannelId, state: UnsignedState): Hex { return encodeAbiParameters( [ { name: 'channelId', type: 'bytes32' }, @@ -40,7 +40,7 @@ export function getPackedState(channelId: ChannelId, state: State): Hex { * @param state The state struct * @returns The state hash as Hex */ -export function getStateHash(channelId: ChannelId, state: State): StateHash { +export function getStateHash(channelId: ChannelId, state: UnsignedState): StateHash { return keccak256(getPackedState(channelId, state)) as StateHash; } diff --git a/sdk/test/unit/client/index.test.ts b/sdk/test/unit/client/index.test.ts index 06d2d31e0..d170e7337 100644 --- a/sdk/test/unit/client/index.test.ts +++ b/sdk/test/unit/client/index.test.ts @@ -3,7 +3,14 @@ import { NitroliteClient } from '../../../src/client/index'; import { Errors } from '../../../src/errors'; import { Address, Hash, Hex } from 'viem'; import * as stateModule from '../../../src/client/state'; -import { Allocation, Channel, ChannelId, ChannelStatus, StateIntent } from '../../../src/client/types'; +import { + Allocation, + Channel, + ChannelId, + ChannelStatus, + CreateChannelParams, + StateIntent, +} from '../../../src/client/types'; describe('NitroliteClient', () => { let client: NitroliteClient; @@ -12,9 +19,7 @@ describe('NitroliteClient', () => { } as any; const mockAccount = { address: '0x1234567890123456789012345678901234567890' as Address }; const mockSignature = '0x' + '1234567890abcdef'.repeat(8) + '1b'; // 128 hex chars, v = 27 - const mockSignMessage = jest.fn(() => - Promise.resolve(mockSignature), - ); + const mockSignMessage = jest.fn(() => Promise.resolve(mockSignature)); const mockWalletClient = { account: mockAccount, signMessage: mockSignMessage, @@ -116,20 +121,14 @@ describe('NitroliteClient', () => { }); describe('createChannel', () => { - const params = { - initialAllocationAmounts: [1n, 2n] as [bigint, bigint], - stateData: '0x00' as any, - }; - - test('success', async () => { - const channel: Channel = { + const params: CreateChannelParams = { + channel: { participants: ['0x0', '0x1'], // List of participants in the channel [Host, Guest] adjudicator: mockAddresses.adjudicator, // Address of the contract that validates final states challenge: challengeDuration, // Duration in seconds for challenge period nonce: 1n, // Unique per channel with same participants and adjudicator - }; - - const initialState = { + }, + initialState: { data: '0x00' as Hex, intent: StateIntent.INITIALIZE, allocations: [ @@ -146,33 +145,34 @@ describe('NitroliteClient', () => { ] as [Allocation, Allocation], version: 0n, sigs: [], - }; + }, + }; + + test('success', async () => { const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex; jest.spyOn(stateModule, '_prepareAndSignInitialState').mockResolvedValue({ - channel, - initialState, + initialState: params.initialState, channelId, }); mockNitroService.createChannel.mockResolvedValue('0xCRE' as Hash); - const result = await client.createChannel(tokenAddress, params); + const result = await client.createChannel(params); expect(stateModule._prepareAndSignInitialState).toHaveBeenCalledWith( - tokenAddress, expect.anything(), params, ); - expect(mockNitroService.createChannel).toHaveBeenCalledWith(channel, initialState); + expect(mockNitroService.createChannel).toHaveBeenCalledWith(params.channel, params.initialState); expect(result).toEqual({ channelId, - initialState, + initialState: params.initialState, txHash: '0xCRE', }); }); test('failure throws ContractCallError', async () => { jest.spyOn(stateModule, '_prepareAndSignInitialState').mockRejectedValue(new Error('fail')); - await expect(client.createChannel(tokenAddress, params)).rejects.toThrow(Errors.ContractCallError); + await expect(client.createChannel(params)).rejects.toThrow(Errors.ContractCallError); }); }); @@ -188,7 +188,6 @@ describe('NitroliteClient', () => { }; jest.spyOn(stateModule, '_prepareAndSignInitialState').mockResolvedValue({ - channel: {} as any, initialState, channelId, }); diff --git a/sdk/test/unit/client/prepare.test.ts b/sdk/test/unit/client/prepare.test.ts index 440020e54..6678890c4 100644 --- a/sdk/test/unit/client/prepare.test.ts +++ b/sdk/test/unit/client/prepare.test.ts @@ -15,7 +15,7 @@ import { import { NitroliteService, Erc20Service } from '../../../src/client/services'; import { ContractAddresses } from '../../../src/abis'; import * as Errors from '../../../src/errors'; -import { CreateChannelParams, CheckpointChannelParams, Allocation } from '../../../src/client/types'; +import { CreateChannelParams, CheckpointChannelParams, Allocation, StateIntent } from '../../../src/client/types'; // TODO: remove ts-ignore describe('NitroliteTransactionPreparer', () => { @@ -106,34 +106,67 @@ describe('NitroliteTransactionPreparer', () => { }); describe('prepareCreateChannelTransaction', () => { - const params: CreateChannelParams = { initialAllocationAmounts: [1n, 2n], stateData: '0xA' as any }; + const params: CreateChannelParams = { + channel: { + participants: [accountAddress, guestAddress], + adjudicator: '0xadj', + challenge: 123n, + nonce: 999n, + }, + initialState: { + data: '0xcustomData', + intent: StateIntent.INITIALIZE, + allocations: [ + { destination: accountAddress, token: tokenAddress, amount: 10n }, + { destination: guestAddress, token: tokenAddress, amount: 20n }, + ], + version: 0n, + sigs: [], + }, + }; test('success', async () => { const fake = { channel: {}, initialState: {} }; // @ts-ignore (_prepareAndSignInitialState as jest.Mock).mockResolvedValue(fake); mockNitro.prepareCreateChannel.mockResolvedValue({ to: '0xC', data: '0xC' } as any); - const tx = await prep.prepareCreateChannelTransaction(tokenAddress, params); - expect(_prepareAndSignInitialState).toHaveBeenCalledWith(tokenAddress, deps, params); + const tx = await prep.prepareCreateChannelTransaction(params); + expect(_prepareAndSignInitialState).toHaveBeenCalledWith(deps, params); expect(tx).toEqual({ to: '0xC', data: '0xC' }); }); test('wraps non-NitroliteError', async () => { // @ts-ignore (_prepareAndSignInitialState as jest.Mock).mockRejectedValue(new Error('oops')); - await expect(prep.prepareCreateChannelTransaction(tokenAddress, params)).rejects.toThrow( - Errors.ContractCallError, - ); + await expect(prep.prepareCreateChannelTransaction(params)).rejects.toThrow(Errors.ContractCallError); }); test('rethrows NitroliteError from state', async () => { const nitroliteError = new Errors.MissingParameterError('x'); // @ts-ignore (_prepareAndSignInitialState as jest.Mock).mockRejectedValueOnce(nitroliteError); - await expect(prep.prepareCreateChannelTransaction(tokenAddress, params)).rejects.toBe(nitroliteError); + await expect(prep.prepareCreateChannelTransaction(params)).rejects.toBe(nitroliteError); }); }); describe('prepareDepositAndCreateChannelTransactions', () => { + const params: CreateChannelParams = { + channel: { + participants: [accountAddress, guestAddress], + adjudicator: '0xadj', + challenge: 123n, + nonce: 999n, + }, + initialState: { + data: '0xcustomData', + intent: StateIntent.INITIALIZE, + allocations: [ + { destination: accountAddress, token: tokenAddress, amount: 10n }, + { destination: guestAddress, token: tokenAddress, amount: 20n }, + ], + version: 0n, + sigs: [], + }, + }; test('combines flows', async () => { // Test case where approval is needed mockERC20.getTokenAllowance.mockResolvedValue(5n); // Less than deposit amount @@ -142,10 +175,7 @@ describe('NitroliteTransactionPreparer', () => { (_prepareAndSignInitialState as jest.Mock).mockResolvedValue({ channel: {}, initialState: {} }); mockNitro.prepareDepositAndCreateChannel.mockResolvedValue({ to: '0xD', data: '0xD' }); - const all = await prep.prepareDepositAndCreateChannelTransactions(tokenAddress, 10n, { - initialAllocationAmounts: [1n, 2n], - stateData: '0x00' as any, - }); + const all = await prep.prepareDepositAndCreateChannelTransactions(tokenAddress, 10n, params); expect(all).toHaveLength(2); expect(all[0]).toEqual({ to: '0xA', data: '0xA' }); // Approval transaction @@ -154,10 +184,7 @@ describe('NitroliteTransactionPreparer', () => { // Verify the correct calls were made expect(mockERC20.getTokenAllowance).toHaveBeenCalledWith(tokenAddress, accountAddress, custody); expect(mockERC20.prepareApprove).toHaveBeenCalledWith(tokenAddress, custody, 10n); - expect(_prepareAndSignInitialState).toHaveBeenCalledWith(tokenAddress, deps, { - initialAllocationAmounts: [1n, 2n], - stateData: '0x00' as any, - }); + expect(_prepareAndSignInitialState).toHaveBeenCalledWith(deps, params); }); test('rethrows NitroliteError from deposit prepare', async () => { @@ -230,7 +257,7 @@ describe('NitroliteTransactionPreparer', () => { await expect( prep.prepareCloseChannelTransaction({ finalState: { - stateData: '0xA' as any, + data: '0xA' as any, channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex, allocations: [ { destination: '0x0' as Hex, token: tokenAddress, amount: 10n }, @@ -238,6 +265,7 @@ describe('NitroliteTransactionPreparer', () => { ] as [Allocation, Allocation], version: 0n, serverSignature: [] as any, + intent: StateIntent.FINALIZE, }, }), ).resolves.toEqual({ to: '0xX', data: '0xX' }); @@ -249,7 +277,7 @@ describe('NitroliteTransactionPreparer', () => { await expect( prep.prepareCloseChannelTransaction({ finalState: { - stateData: '0xA' as any, + data: '0xA' as any, channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex, allocations: [ { destination: '0x0' as Hex, token: tokenAddress, amount: 10n }, @@ -257,6 +285,7 @@ describe('NitroliteTransactionPreparer', () => { ] as [Allocation, Allocation], version: 0n, serverSignature: [] as any, + intent: StateIntent.FINALIZE, }, }), ).rejects.toThrow(Errors.ContractCallError); @@ -273,7 +302,7 @@ describe('NitroliteTransactionPreparer', () => { await expect( prep.prepareCloseChannelTransaction({ finalState: { - stateData: '0xA' as any, + data: '0xA' as any, channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex, allocations: [ { destination: '0x0' as Hex, token: tokenAddress, amount: 10n }, @@ -281,6 +310,7 @@ describe('NitroliteTransactionPreparer', () => { ] as [Allocation, Allocation], version: 0n, serverSignature: [] as any, + intent: StateIntent.FINALIZE, }, }), ).rejects.toThrow(Errors.ContractCallError); @@ -297,7 +327,7 @@ describe('NitroliteTransactionPreparer', () => { await expect( prep.prepareCloseChannelTransaction({ finalState: { - stateData: '0xA' as any, + data: '0xA' as any, channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex, allocations: [ { destination: '0x0' as Hex, token: tokenAddress, amount: 10n }, @@ -305,6 +335,7 @@ describe('NitroliteTransactionPreparer', () => { ] as [Allocation, Allocation], version: 0n, serverSignature: [] as any, + intent: StateIntent.FINALIZE, }, }), ).rejects.toBe(ne); diff --git a/sdk/test/unit/client/state.test.ts b/sdk/test/unit/client/state.test.ts index 02d22cc94..e1fd5d3a0 100644 --- a/sdk/test/unit/client/state.test.ts +++ b/sdk/test/unit/client/state.test.ts @@ -6,7 +6,7 @@ import { Hex } from 'viem'; import { _prepareAndSignInitialState, _prepareAndSignFinalState } from '../../../src/client/state'; import * as utils from '../../../src/utils'; import { Errors } from '../../../src/errors'; -import { State, StateIntent } from '../../../src/client/types'; +import { State, Channel, CreateChannelParams, StateIntent } from '../../../src/client/types'; // Mock utils jest.mock('../../../src/utils', () => ({ @@ -19,7 +19,9 @@ jest.mock('../../../src/utils', () => ({ })); describe('_prepareAndSignInitialState', () => { - let deps; + let deps: any; + let defaultChannel: Channel; + let defaultState: State; const guestAddress = '0xGUEST' as Hex; const tokenAddress = '0xTOKEN' as Hex; const adjudicatorAddress = '0xADJ' as Hex; @@ -40,45 +42,52 @@ describe('_prepareAndSignInitialState', () => { }, challengeDuration, }; - }); - - test('success with explicit stateData', async () => { - const params = { - initialAllocationAmounts: [10n, 20n], - stateData: 'customData', - }; - const { channel, initialState, channelId } = await _prepareAndSignInitialState( - tokenAddress, - deps, - params as any, - ); - // Channel fields - expect(utils.generateChannelNonce).toHaveBeenCalledWith(deps.account.address); - expect(channel).toEqual({ + defaultChannel = { participants: [deps.account.address, guestAddress], adjudicator: adjudicatorAddress, challenge: challengeDuration, nonce: 999n, - }); + }; + + defaultState = { + data: '0xcustomData', + intent: StateIntent.INITIALIZE, + allocations: [ + { destination: deps.account.address, token: tokenAddress, amount: 10n }, + { destination: guestAddress, token: tokenAddress, amount: 20n }, + ], + version: 0n, + sigs: [], + }; + }); + + test('success with explicit stateData', async () => { + const params: CreateChannelParams = { + channel: defaultChannel, + unsignedInitialState: defaultState, + serverSignature: '0xSRVSIG', + }; + const { initialState, channelId } = await _prepareAndSignInitialState(deps, params); + // channelId is stubbed expect(channelId).toBe('cid'); // State fields expect(initialState).toEqual({ - data: 'customData', + data: '0xcustomData', intent: StateIntent.INITIALIZE, allocations: [ { destination: deps.account.address, token: tokenAddress, amount: 10n }, { destination: guestAddress, token: tokenAddress, amount: 20n }, ], version: 0n, - sigs: ['accSig'], + sigs: ['accSig', '0xSRVSIG'], }); // Signs the state expect(stateSigner.signState).toHaveBeenCalledWith( 'cid', { - data: 'customData', + data: '0xcustomData', intent: StateIntent.INITIALIZE, allocations: expect.any(Array), version: 0n, @@ -88,21 +97,26 @@ describe('_prepareAndSignInitialState', () => { }); test('throws if no adjudicator', async () => { - deps.addresses.adjudicator = undefined; + const localChannel = { ...defaultChannel, adjudicator: undefined } as any; + await expect( - _prepareAndSignInitialState(tokenAddress, deps, { - initialAllocationAmounts: [1n, 2n], - stateData: '0xdata', + _prepareAndSignInitialState(deps, { + channel: localChannel, + unsignedInitialState: defaultState, + serverSignature: '0xSRVSIG', }), ).rejects.toThrow(Errors.MissingParameterError); }); test('throws if bad allocations length', async () => { + const localState = { ...defaultState, allocations: [] } as any; + await expect( - _prepareAndSignInitialState(tokenAddress, deps, { - initialAllocationAmounts: [1n], - stateData: 'd', - } as any), + _prepareAndSignInitialState(deps, { + channel: defaultChannel, + unsignedInitialState: localState, + serverSignature: '0xSRVSIG', + }), ).rejects.toThrow(Errors.InvalidParameterError); }); }); From 3216007541f91db45f4314c4c2554ae3e1c7fef2 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Wed, 6 Aug 2025 16:10:00 +0300 Subject: [PATCH 04/18] refactor: API protocol (#285) * refactor: API protocol * fix: resolve issues after rebase --- clearnode/app_session_service_test.go | 19 +- clearnode/custody_test.go | 14 +- clearnode/docs/API.md | 796 ++++++++++-------- clearnode/docs/Clearnode.protocol.md | 40 +- clearnode/notification.go | 8 +- clearnode/reconcile_cli.go | 2 +- clearnode/rpc.go | 15 +- clearnode/rpc_node.go | 20 +- clearnode/rpc_node_test.go | 125 ++- clearnode/rpc_router.go | 34 +- clearnode/rpc_router_auth.go | 108 +-- clearnode/rpc_router_callback.go | 66 ++ clearnode/rpc_router_private.go | 45 +- clearnode/rpc_router_private_test.go | 78 +- clearnode/rpc_router_public.go | 68 +- clearnode/rpc_router_public_test.go | 96 +-- integration/common/auth.ts | 10 +- integration/common/nitroliteClient.ts | 12 +- integration/tests/clearnode_auth.test.ts | 23 +- integration/tests/close_channel.test.ts | 4 +- integration/tests/create_channel.test.ts | 11 +- integration/tests/get_user_tag.test.ts | 10 +- integration/tests/ledger_transactions.test.ts | 86 +- integration/tests/lifecycle.test.ts | 119 +-- integration/tests/resize_channel.test.ts | 50 +- sdk/src/rpc/api.ts | 270 ++++-- sdk/src/rpc/nitrolite.ts | 140 +-- sdk/src/rpc/parse/app.ts | 185 ++-- sdk/src/rpc/parse/asset.ts | 52 +- sdk/src/rpc/parse/auth.ts | 53 +- sdk/src/rpc/parse/channel.ts | 199 +++-- sdk/src/rpc/parse/common.ts | 37 +- sdk/src/rpc/parse/ledger.ts | 157 ++-- sdk/src/rpc/parse/misc.ts | 147 ++-- sdk/src/rpc/parse/parse.ts | 134 ++- sdk/src/rpc/types/common.ts | 295 +++++++ sdk/src/rpc/types/filters.ts | 17 + sdk/src/rpc/types/index.ts | 155 +--- sdk/src/rpc/types/request.ts | 558 ++++++------ sdk/src/rpc/types/response.ts | 695 ++++++--------- sdk/src/utils/channel.ts | 6 +- sdk/test/unit/rpc/api.test.ts | 191 +++-- sdk/test/unit/rpc/nitrolite.test.ts | 152 +--- sdk/test/unit/rpc/utils.test.ts | 46 +- 44 files changed, 2730 insertions(+), 2618 deletions(-) create mode 100644 clearnode/rpc_router_callback.go create mode 100644 sdk/src/rpc/types/common.ts create mode 100644 sdk/src/rpc/types/filters.ts diff --git a/clearnode/app_session_service_test.go b/clearnode/app_session_service_test.go index f8a4f9f6a..e61e8d30e 100644 --- a/clearnode/app_session_service_test.go +++ b/clearnode/app_session_service_test.go @@ -36,7 +36,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { require.NoError(t, GetWalletLedger(db, userAddressB).Record(userAccountIDB, "usdc", decimal.NewFromInt(200))) capturedNotifications := make(map[string][]Notification) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) { + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -117,7 +117,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { require.NoError(t, db.Create(&SignerWallet{Signer: userAddressA.Hex(), Wallet: userAddressA.Hex()}).Error) require.NoError(t, GetWalletLedger(db, userAddressA).Record(userAccountIDA, "usdc", decimal.NewFromInt(50))) // Not enough - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) params := &CreateAppSessionParams{ Definition: AppDefinition{ Protocol: "test-proto", @@ -148,7 +148,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { Status: ChannelStatusChallenged, }).Error) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) params := &CreateAppSessionParams{ Definition: AppDefinition{ Protocol: "test-proto", @@ -175,7 +175,7 @@ func TestAppSessionService_CreateApplication(t *testing.T) { require.NoError(t, db.Create(&SignerWallet{Signer: userAddressA.Hex(), Wallet: userAddressA.Hex()}).Error) require.NoError(t, GetWalletLedger(db, userAddressA).Record(userAccountIDA, "usdc", decimal.NewFromInt(100))) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) params := &CreateAppSessionParams{ Definition: AppDefinition{ Protocol: "test-proto", @@ -208,7 +208,7 @@ func TestAppSessionService_SubmitAppState(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) session := &AppSession{ SessionID: "test-session", Protocol: "test-proto", @@ -258,7 +258,7 @@ func TestAppSessionService_SubmitAppState(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) session := &AppSession{ SessionID: "test-session-negative", Protocol: "test-proto", @@ -307,8 +307,7 @@ func TestAppSessionService_CloseApplication(t *testing.T) { defer cleanup() capturedNotifications := make(map[string][]Notification) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) { - + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, eventType: EventType(method), @@ -395,7 +394,7 @@ func TestAppSessionService_CloseApplication(t *testing.T) { defer cleanup() capturedNotifications := make(map[string][]Notification) - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) { + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -457,7 +456,7 @@ func TestAppSessionService_CloseApplication(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params ...any) {}, nil)) + service := NewAppSessionService(db, NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, nil)) session := &AppSession{ SessionID: "test-session-close-negative", Protocol: "test-proto", diff --git a/clearnode/custody_test.go b/clearnode/custody_test.go index fb4940735..b16b3a34a 100644 --- a/clearnode/custody_test.go +++ b/clearnode/custody_test.go @@ -85,7 +85,7 @@ func setupMockCustody(t *testing.T) (*Custody, *gorm.DB, func()) { custody: contract, chainID: uint32(chainID.Int64()), adjudicatorAddress: newTestCommonAddress("0xAdjudicatorAddress"), - wsNotifier: NewWSNotifier(func(userID string, method string, params ...any) {}, logger), + wsNotifier: NewWSNotifier(func(userID string, method string, params RPCDataParams) {}, logger), logger: logger, } @@ -310,7 +310,7 @@ func TestHandleCreatedEvent(t *testing.T) { } capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { if capturedNotifications[userID] == nil { capturedNotifications[userID] = make([]Notification, 0) } @@ -406,7 +406,7 @@ func TestHandleClosedEvent(t *testing.T) { _, mockEvent := createMockClosedEvent(t, custody.signer, tokenAddress, finalAmount) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -594,7 +594,7 @@ func TestHandleClosedEvent(t *testing.T) { _, mockEvent := createMockClosedEvent(t, custody.signer, tokenAddress, channelAmount.BigInt()) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -665,7 +665,7 @@ func TestHandleChallengedEvent(t *testing.T) { _, mockEvent := createMockChallengedEvent(t, custody.signer, tokenAddress, amount.BigInt()) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -741,7 +741,7 @@ func TestHandleResizedEvent(t *testing.T) { _, mockEvent := createMockResizedEvent(t, deltaAmount.BigInt()) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, @@ -901,7 +901,7 @@ func TestHandleResizedEvent(t *testing.T) { _, mockEvent := createMockResizedEvent(t, big.NewInt(500000)) capturedNotifications := make(map[string][]Notification) - custody.wsNotifier.notify = func(userID string, method string, params ...any) { + custody.wsNotifier.notify = func(userID string, method string, params RPCDataParams) { capturedNotifications[userID] = append(capturedNotifications[userID], Notification{ userID: userID, diff --git a/clearnode/docs/API.md b/clearnode/docs/API.md index f2936294a..5a55be407 100644 --- a/clearnode/docs/API.md +++ b/clearnode/docs/API.md @@ -2,29 +2,29 @@ ## API Endpoints -| Method | Description | Access | -|--------|-------------|------------| -| `auth_request` | Initiates authentication with the server | Public | -| `auth_challenge` | Server response with authentication challenge | Public | -| `auth_verify` | Completes authentication with a challenge response | Public | -| `ping` | Simple connectivity check | Public | -| `get_config` | Retrieves broker configuration and supported networks | Public | -| `get_assets` | Retrieves all supported assets (optionally filtered by chain_id) | Public | -| `get_channels` | Lists all channels for a participant with their status across all chains | Public | -| `get_app_definition` | Retrieves application definition for a ledger account | Public | -| `get_app_sessions` | Lists virtual applications for a participant with optional status filter | Public | -| `get_ledger_entries` | Retrieves detailed ledger entries for a participant | Public | -| `get_ledger_transactions` | Retrieves transaction history with optional filtering | Public | -| `get_user_tag` | Retrieves user's tag| Private | -| `get_rpc_history` | Retrieves all RPC message history for a participant | Private | -| `get_ledger_balances` | Lists participants and their balances for a ledger account | Private | -| `transfer` | Transfers funds from user's unified balance to another account | Private | -| `create_app_session` | Creates a new virtual application on a ledger | Private | -| `submit_app_state` | Submits an intermediate state into a virtual application | Private | -| `close_app_session` | Closes a virtual application | Private | -| `create_channel` | Returns data and Broker signature to open a channel | Private | -| `close_channel` | Returns data and Broker signature to close a channel | Private | -| `resize_channel` | Returns data and Broker signature to adjust channel capacity | Private | +| Method | Description | Access | +| ------------------------- | ------------------------------------------------------------------------ | ------- | +| `auth_request` | Initiates authentication with the server | Public | +| `auth_challenge` | Server response with authentication challenge | Public | +| `auth_verify` | Completes authentication with a challenge response | Public | +| `ping` | Simple connectivity check | Public | +| `get_config` | Retrieves broker configuration and supported networks | Public | +| `get_assets` | Retrieves all supported assets (optionally filtered by chain_id) | Public | +| `get_channels` | Lists all channels for a participant with their status across all chains | Public | +| `get_app_definition` | Retrieves application definition for a ledger account | Public | +| `get_app_sessions` | Lists virtual applications for a participant with optional status filter | Public | +| `get_ledger_entries` | Retrieves detailed ledger entries for a participant | Public | +| `get_ledger_transactions` | Retrieves transaction history with optional filtering | Public | +| `get_user_tag` | Retrieves user's tag | Private | +| `get_rpc_history` | Retrieves all RPC message history for a participant | Private | +| `get_ledger_balances` | Lists participants and their balances for a ledger account | Private | +| `transfer` | Transfers funds from user's unified balance to another account | Private | +| `create_app_session` | Creates a new virtual application on a ledger | Private | +| `submit_app_state` | Submits an intermediate state into a virtual application | Private | +| `close_app_session` | Closes a virtual application | Private | +| `create_channel` | Returns data and Broker signature to open a channel | Private | +| `close_channel` | Returns data and Broker signature to close a channel | Private | +| `resize_channel` | Returns data and Broker signature to adjust channel capacity | Private | ## Authentication @@ -36,20 +36,20 @@ Initiates authentication with the server. ```json { - "req": [1, "auth_request", [{ + "req": [1, "auth_request", { "address": "0x1234567890abcdef...", "session_key": "0x9876543210fedcba...", // If specified, enables delegation to this key "app_name": "Example App", // Application name for analytics "allowances": [ // Asset allowances for the session - [ - "usdc", - "100.0" - ] + { + "asset": "usdc", + "amount": "100.0" + } ], "scope": "app.create", // Permission scope (e.g., "app.create", "ledger.readonly") "expire": "3600", // Session expiration "application": "0xApp1234567890abcdef..." // Application public address - }], 1619123456789], + }, 1619123456789], "sig": ["0x5432abcdef..."] // Client's signature of the entire 'req' object } ``` @@ -62,9 +62,9 @@ Server response with a challenge token for the client to sign. ```json { - "res": [1, "auth_challenge", [{ + "res": [1, "auth_challenge", { "challenge_message": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] // Server's signature of the entire 'res' object } ``` @@ -77,9 +77,9 @@ Completes authentication with a challenge response. ```json { - "req": [2, "auth_verify", [{ + "req": [2, "auth_verify", { "challenge": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x2345bcdef..."] // Client's EIP-712 signatures of the challenge data object } ``` @@ -88,11 +88,11 @@ Completes authentication with a challenge response. ```json { - "res": [2, "auth_verify", [{ + "res": [2, "auth_verify", { "address": "0x1234567890abcdef...", "success": true, "jwt_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." // JWT token for subsequent requests - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] // Server's signature of the entire 'res' object } ``` @@ -113,7 +113,7 @@ The JWT token has a default validity period of 24 hours and must be refreshed by ### Get Channels Retrieves all channels for a participant (both open, closed, and joining), ordered by creation date (newest first). This method returns channels across all supported chains. If no participant is specified, it returns all channels. -Supports pagination and sorting. +Supports pagination and sorting by providing optional request parameters and metadata fields in response. > Sorted descending by `created_at` by default. @@ -121,9 +121,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_channels", [{ - "participant": "0x1234567890abcdef...", - }], 1619123456789], + "req": [1, "get_channels", {}, 1619123456789], "sig": [] } ``` @@ -132,13 +130,13 @@ Supports pagination and sorting. ```json { - "req": [1, "get_channels", [{ - "participant": "0x1234567890abcdef...", + "req": [1, "get_channels", { + "participant": "0x1234567890abcdef...", // Optional: filter by participant "status":"open", // Optional filter "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of channels to return "sort": "desc" // Optional: sort asc or desc by created_at - }], 1619123456789], + }, 1619123456789], "sig": [] } ``` @@ -147,38 +145,46 @@ Supports pagination and sorting. ```json { - "res": [1, "get_channels", [[ - { - "channel_id": "0xfedcba9876543210...", - "participant": "0x1234567890abcdef...", - "wallet": "0x1234567890abcdef...", - "status": "open", - "token": "0xeeee567890abcdef...", - "amount": "100000", - "chain_id": 137, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 2, - "created_at": "2023-05-01T12:00:00Z", - "updated_at": "2023-05-01T12:30:00Z" - }, - { - "channel_id": "0xabcdef1234567890...", - "participant": "0x1234567890abcdef...", - "wallet": "0x1234567890abcdef...", - "status": "closed", - "token": "0xeeee567890abcdef...", - "amount": "50000", - "chain_id": 42220, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 3, - "created_at": "2023-04-15T10:00:00Z", - "updated_at": "2023-04-20T14:30:00Z" + "res": [1, "get_channels", { + "channels" : [ + { + "channel_id": "0xfedcba9876543210...", + "participant": "0x1234567890abcdef...", + "wallet": "0x1234567890abcdef...", + "status": "open", + "token": "0xeeee567890abcdef...", + "amount": "100000", + "chain_id": 137, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 2, + "created_at": "2023-05-01T12:00:00Z", + "updated_at": "2023-05-01T12:30:00Z" + }, + { + "channel_id": "0xabcdef1234567890...", + "participant": "0x1234567890abcdef...", + "wallet": "0x1234567890abcdef...", + "status": "closed", + "token": "0xeeee567890abcdef...", + "amount": "50000", + "chain_id": 42220, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 3, + "created_at": "2023-04-15T10:00:00Z", + "updated_at": "2023-04-20T14:30:00Z" + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -200,6 +206,13 @@ Each channel response includes: - `version`: Current version of the channel state - `created_at`: When the channel was created (ISO 8601 format) - `updated_at`: When the channel was last updated (ISO 8601 format) + +Metadata fields provide pagination information: + +- `page`: Current page number +- `per_page`: Number of channels per page +- `total_count`: Total number of channels available +- `page_count`: Total number of pages based on the `per_page` limit ### Get App Definition @@ -209,9 +222,9 @@ Retrieves the application definition for a specific ledger account. ```json { - "req": [1, "get_app_definition", [{ + "req": [1, "get_app_definition", { "app_session_id": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -220,19 +233,17 @@ Retrieves the application definition for a specific ledger account. ```json { - "res": [1, "get_app_definition", [ - { - "protocol": "NitroRPC/0.2", - "participants": [ - "0xAaBbCcDdEeFf0011223344556677889900aAbBcC", - "0x00112233445566778899AaBbCcDdEeFf00112233" - ], - "weights": [50, 50], - "quorum": 100, - "challenge": 86400, - "nonce": 1 - } - ], 1619123456789], + "res": [1, "get_app_definition", { + "protocol": "NitroRPC/0.2", + "participants": [ + "0xAaBbCcDdEeFf0011223344556677889900aAbBcC", + "0x00112233445566778899AaBbCcDdEeFf00112233" + ], + "weights": [50, 50], + "quorum": 100, + "challenge": 86400, + "nonce": 1 + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -248,9 +259,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_app_sessions", [{ - "participant": "0x1234567890abcdef..." - }], 1619123456789], + "req": [1, "get_app_sessions", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -259,13 +268,13 @@ Supports pagination and sorting. ```json { - "req": [1, "get_app_sessions", [{ - "participant": "0x1234567890abcdef...", + "req": [1, "get_app_sessions", { + "participant": "0x1234567890abcdef...", // Optional: filter by participant "status": "open", // Optional: filter by status "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of sessions to return "sort": "asc", // Optional: sort asc or desc - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -274,38 +283,46 @@ Supports pagination and sorting. ```json { - "res": [1, "get_app_sessions", [[ - { - "app_session_id": "0x3456789012abcdef...", - "status": "open", - "participants": [ - "0x1234567890abcdef...", - "0x00112233445566778899AaBbCcDdEeFf00112233" - ], - "session_data": "{\"gameType\":\"rps\",\"rounds\":5,\"currentRound\":3,\"scores\":{\"0x1234567890abcdef\":2,\"0x00112233445566778899AaBbCcDdEeFf00112233\":1}}", - "protocol": "NitroAura", - "challenge": 86400, - "weights": [50, 50], - "quorum": 100, - "version": 1, - "nonce": 123456789 - }, - { - "app_session_id": "0x7890123456abcdef...", - "status": "open", - "participants": [ - "0x1234567890abcdef...", - "0xAaBbCcDdEeFf0011223344556677889900aAbBcC" - ], - "session_data": "{\"gameType\":\"snake\",\"boardSize\":20,\"snakeLength\":5,\"score\":150,\"level\":3,\"gameState\":\"active\"}", - "protocol": "NitroSnake", - "challenge": 86400, - "weights": [70, 30], - "quorum": 100, - "version": 1, - "nonce": 123456790 + "res": [1, "get_app_sessions", { + "app_sessions" : [ + { + "app_session_id": "0x3456789012abcdef...", + "status": "open", + "participants": [ + "0x1234567890abcdef...", + "0x00112233445566778899AaBbCcDdEeFf00112233" + ], + "session_data": "{\"gameType\":\"rps\",\"rounds\":5,\"currentRound\":3,\"scores\":{\"0x1234567890abcdef\":2,\"0x00112233445566778899AaBbCcDdEeFf00112233\":1}}", + "protocol": "NitroAura", + "challenge": 86400, + "weights": [50, 50], + "quorum": 100, + "version": 1, + "nonce": 123456789 + }, + { + "app_session_id": "0x7890123456abcdef...", + "status": "open", + "participants": [ + "0x1234567890abcdef...", + "0xAaBbCcDdEeFf0011223344556677889900aAbBcC" + ], + "session_data": "{\"gameType\":\"snake\",\"boardSize\":20,\"snakeLength\":5,\"score\":150,\"level\":3,\"gameState\":\"active\"}", + "protocol": "NitroSnake", + "challenge": 86400, + "weights": [70, 30], + "quorum": 100, + "version": 1, + "nonce": 123456790 + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -318,11 +335,11 @@ Retrieves the balances of all participants in a specific ledger account. ```json { - "req": [1, "get_ledger_balances", [{ + "req": [1, "get_ledger_balances", { "participant": "0x1234567890abcdef...", // TO BE DEPRECATED // OR "account_id": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -334,16 +351,18 @@ To get balance in a specific virtual app session, specify `app_session_id` as ac ```json { - "res": [1, "get_ledger_balances", [[ - { - "asset": "usdc", - "amount": "100.0" - }, - { - "asset": "eth", - "amount": "0.5" - } - ]], 1619123456789], + "res": [1, "get_ledger_balances", { + "ledger_balances": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -356,7 +375,7 @@ Retrieves the user's tag, which can be used for transfer operations. The tag is ```json { - "req": [1, "get_user_tag", [], 1619123456789], + "req": [1, "get_user_tag", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -365,13 +384,13 @@ Retrieves the user's tag, which can be used for transfer operations. The tag is ```json { - "res": [1, "get_user_tag", [ - { - "tag": "UX123D", - }], 1619123456789], + "res": [1, "get_user_tag", { + "tag": "UX123D", + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` + ### Transfer Funds This method allows a user to transfer assets from their unified balance to another account. The user must have sufficient funds for each asset being transferred. The operation will fail if any of the specified assets have insufficient funds. @@ -385,7 +404,7 @@ Currently, `Transfer` supports ledger account of another user as destination (wa ```json { - "req": [1, "transfer", [{ + "req": [1, "transfer", { "destination": "0x9876543210abcdef...", "allocations": [ { @@ -397,14 +416,14 @@ Currently, `Transfer` supports ledger account of another user as destination (wa "amount": "0.1" } ] - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } // OR { - "req": [1, "transfer", [{ + "req": [1, "transfer", { "destination_user_tag": "UX123D", "allocations": [ { @@ -416,7 +435,7 @@ Currently, `Transfer` supports ledger account of another user as destination (wa "amount": "0.1" } ] - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -425,34 +444,37 @@ Currently, `Transfer` supports ledger account of another user as destination (wa ```json { - "res": [1, "transfer", [[ - { - "id": "1", - "tx_type": "transfer", - "from_account": "0x1234567890abcdef...", - "from_account_tag": "NQKO7C", - "to_account": "0x9876543210abcdef...", - "to_account_tag": "UX123D", - "asset": "usdc", - "amount": "50.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": "2", - "tx_type": "transfer", - "from_account": "0x1234567890abcdef...", - "from_account_tag": "NQKO7C", - "to_account": "0x9876543210abcdef...", - "to_account_tag": "UX123D", - "asset": "eth", - "amount": "0.1", - "created_at": "2023-05-01T12:00:00Z" - } - ]], 1619123456789], + "res": [1, "transfer", { + "transactions" : [ + { + "id": "1", + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": "2", + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "eth", + "amount": "0.1", + "created_at": "2023-05-01T12:00:00Z" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` -The response returns an array of transaction objects, with one transaction for each asset being transferred. + +The response returns an array of transaction objects, with one transaction for each asset being transferred. Each transaction includes: @@ -477,7 +499,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_entries", [], 1619123456789], + "req": [1, "get_ledger_entries", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -486,14 +508,14 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_entries", [{ - "account_id": "0x1234567890abcdef...", + "req": [1, "get_ledger_entries", { + "account_id": "0x1234567890abcdef...", // Optional: filter by account ID "wallet": "0x1234567890abcdef...", // Optional: filter by participant "asset": "usdc", // Optional: filter by asset "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of entries to return "sort": "desc" // Optional: sort asc or desc by created_at - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -502,28 +524,36 @@ Supports pagination and sorting. ```json { - "res": [1, "get_ledger_entries", [[ - { - "id": 123, - "account_id": "0x1234567890abcdef...", - "account_type": 0, - "asset": "usdc", - "participant": "0x1234567890abcdef...", - "credit": "100.0", - "debit": "0.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": 124, - "account_id": "0x1234567890abcdef...", - "account_type": 0, - "asset": "usdc", - "participant": "0x1234567890abcdef...", - "credit": "0.0", - "debit": "25.0", - "created_at": "2023-05-01T14:30:00Z" + "res": [1, "get_ledger_entries", { + "ledger_entries": [ + { + "id": 123, + "account_id": "0x1234567890abcdef...", + "account_type": 0, + "asset": "usdc", + "participant": "0x1234567890abcdef...", + "credit": "100.0", + "debit": "0.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": 124, + "account_id": "0x1234567890abcdef...", + "account_type": 0, + "asset": "usdc", + "participant": "0x1234567890abcdef...", + "credit": "0.0", + "debit": "25.0", + "created_at": "2023-05-01T14:30:00Z" + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -536,6 +566,7 @@ Supports pagination and sorting. > Sorted descending by `created_at` by default. **Available Transaction Types:** + - `transfer`: Direct transfers between unified accounts - `deposit`: Funds deposited to a unified account - `withdrawal`: Funds withdrawn from a unified account @@ -546,11 +577,7 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_transactions", [{ - "account_id": "0x1234567890abcdef...", - "asset": "usdc", // Optional: filter by asset - "tx_type": "transfer" // Optional: filter by transaction type - }], 1619123456789], + "req": [1, "get_ledger_transactions", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -559,14 +586,14 @@ Supports pagination and sorting. ```json { - "req": [1, "get_ledger_transactions", [{ - "account_id": "0x1234567890abcdef...", + "req": [1, "get_ledger_transactions", { + "account_id": "0x1234567890abcdef...", // Optional: filter by account ID "asset": "usdc", // Optional: filter by asset "tx_type": "transfer", // Optional: filter by transaction type "offset": 42, // Optional: pagination offset "limit": 10, // Optional: number of transactions to return "sort": "desc" // Optional: sort asc or desc by created_at - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -575,35 +602,44 @@ Supports pagination and sorting. ```json { - "res": [1, "get_ledger_transactions", [[ - { - "id": "1", - "tx_type": "transfer", - "from_account": "0x1234567890abcdef...", - "from_account_tag": "NQKO7C", - "to_account": "0x9876543210abcdef...", - "to_account_tag": "UX123D", - "asset": "usdc", - "amount": "50.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": "2", - "tx_type": "deposit", - "from_account": "0x9876543210abcdef...", // Channel account - "from_account_tag": "", // Channel accounts does not have tags - "to_account": "0x1234567890abcdef...", - "to_account_tag": "UX123D", - "asset": "usdc", - "amount": "25.0", - "created_at": "2023-05-01T10:30:00Z" + "res": [1, "get_ledger_transactions", { + "ledger_transactions":[ + { + "id": "1", + "tx_type": "transfer", + "from_account": "0x1234567890abcdef...", + "from_account_tag": "NQKO7C", + "to_account": "0x9876543210abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": "2", + "tx_type": "deposit", + "from_account": "0x9876543210abcdef...", // Channel account + "from_account_tag": "", // Channel accounts does not have tags + "to_account": "0x1234567890abcdef...", + "to_account_tag": "UX123D", + "asset": "usdc", + "amount": "25.0", + "created_at": "2023-05-01T10:30:00Z" + } + ], + "metadata": { + "page": 5, + "per_page": 10, + "total_count": 56, + "page_count": 6 } - ]], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` Each transaction response includes: + - `id`: Unique transaction id reference - `tx_type`: Transaction type (transfer/deposit/withdrawal/app_deposit/app_withdrawal) - `from_account`: The account that sent the funds @@ -624,7 +660,7 @@ Retrieves all RPC messages history for a participant, ordered by timestamp (newe ```json { - "req": [4, "get_rpc_history", [], 1619123456789], + "req": [4, "get_rpc_history", {}, 1619123456789], "sig": [] } ``` @@ -633,30 +669,32 @@ Retrieves all RPC messages history for a participant, ordered by timestamp (newe ```json { - "res": [4, "get_rpc_history", [[ - { - "id": 123, - "sender": "0x1234567890abcdef...", - "req_id": 42, - "method": "get_channels", - "params": "[{\"participant\":\"0x1234567890abcdef...\"}]", - "timestamp": 1619123456789, - "req_sig": ["0x9876fedcba..."], - "response": "{\"res\":[42,\"get_channels\",[[...]],1619123456799]}", - "res_sig": ["0xabcd1234..."] - }, - { - "id": 122, - "sender": "0x1234567890abcdef...", - "req_id": 41, - "method": "ping", - "params": "[null]", - "timestamp": 1619123446789, - "req_sig": ["0x8765fedcba..."], - "response": "{\"res\":[41,\"pong\",[],1619123446799]}", - "res_sig": ["0xdcba4321..."] - } - ]], 1619123456789], + "res": [4, "get_rpc_history", { + "rpc_entries": [ + { + "id": 123, + "sender": "0x1234567890abcdef...", + "req_id": 42, + "method": "get_channels", + "params": "[{\"participant\":\"0x1234567890abcdef...\"}]", + "timestamp": 1619123456789, + "req_sig": ["0x9876fedcba..."], + "response": "{\"res\":[42,\"get_channels\",[[...]],1619123456799]}", + "res_sig": ["0xabcd1234..."] + }, + { + "id": 122, + "sender": "0x1234567890abcdef...", + "req_id": 41, + "method": "ping", + "params": "[null]", + "timestamp": 1619123446789, + "req_sig": ["0x8765fedcba..."], + "response": "{\"res\":[41,\"pong\",[],1619123446799]}", + "res_sig": ["0xdcba4321..."] + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -674,7 +712,7 @@ The optional `session_data` field can be used to store application-specific data ```json { - "req": [1, "create_app_session", [{ + "req": [1, "create_app_session", { "definition": { "protocol": "NitroRPC/0.2", "participants": [ @@ -699,7 +737,7 @@ The optional `session_data` field can be used to store application-specific data } ], "session_data": "{\"gameType\":\"chess\",\"timeControl\":{\"initial\":600,\"increment\":5},\"maxPlayers\":2,\"gameState\":\"waiting\"}" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -708,14 +746,15 @@ The optional `session_data` field can be used to store application-specific data ```json { - "res": [1, "create_app_session", [{ + "res": [1, "create_app_session", { "app_session_id": "0x3456789012abcdef...", "version": "1", "status": "open" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` + ### Submit Application State Submits an intermediate state into a virtual application and redistributes funds in an app session. @@ -728,7 +767,7 @@ The optional `session_data` field can be used to update application-specific dat ```json { - "req": [1, "submit_app_state", [{ + "req": [1, "submit_app_state", { "app_session_id": "0x3456789012abcdef...", "allocations": [ { @@ -743,7 +782,7 @@ The optional `session_data` field can be used to update application-specific dat } ], "session_data": "{\"gameType\":\"chess\",\"timeControl\":{\"initial\":600,\"increment\":5},\"maxPlayers\":2,\"gameState\":\"finished\",\"winner\":\"0x00112233445566778899AaBbCcDdEeFf00112233\",\"endCondition\":\"checkmate\"}" // Optional - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba...", "0x8765fedcba..."] } ``` @@ -752,11 +791,11 @@ The optional `session_data` field can be used to update application-specific dat ```json { - "res": [1, "submit_app_state", [{ + "res": [1, "submit_app_state", { "app_session_id": "0x3456789012abcdef...", "version": "567", "status": "open" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -773,7 +812,7 @@ The optional `session_data` field can be used to provide final application-speci ```json { - "req": [1, "close_app_session", [{ + "req": [1, "close_app_session", { "app_session_id": "0x3456789012abcdef...", "allocations": [ { @@ -788,7 +827,7 @@ The optional `session_data` field can be used to provide final application-speci } ], "session_data": "{\"gameType\":\"chess\",\"timeControl\":{\"initial\":600,\"increment\":5},\"maxPlayers\":2,\"gameState\":\"closed\",\"winner\":\"0x00112233445566778899AaBbCcDdEeFf00112233\",\"endCondition\":\"checkmate\",\"moveHistory\":[\"e2e4\",\"e7e5\",\"Nf3\",\"Nc6\"]}" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba...", "0x8765fedcba..."] } ``` @@ -797,11 +836,11 @@ The optional `session_data` field can be used to provide final application-speci ```json { - "res": [1, "close_app_session", [{ + "res": [1, "close_app_session", { "app_session_id": "0x3456789012abcdef...", "version": "3", "status": "closed" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -876,16 +915,16 @@ The response includes: ### Close Channel To close a channel, the user must request the final state signed by the broker and then submit it to the smart contract. -Only an open channel can be closed. In case the user does not agree with the final state provided by the broker, they can call the `challenge` method directly on the smart contract. +Only an open channel can be closed. In case the user does not agree with the final state provided by the broker, they can call the `challenge` method directly on the smart contract. **Request:** ```json { - "req": [1, "close_channel", [{ + "req": [1, "close_channel", { "channel_id": "0x4567890123abcdef...", "funds_destination": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -896,7 +935,7 @@ In the request, the user must specify funds destination. After the channel is cl ```json { - "res": [1, "close_channel", [{ + "res": [1, "close_channel", { "channel_id": "0x4567890123abcdef...", "state": { "intent": 3, // IntentFINALIZE - constant specifying that this is a final state @@ -916,7 +955,7 @@ In the request, the user must specify funds destination. After the channel is cl ] }, "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -929,20 +968,21 @@ Adjusts the capacity of a channel. ```json { - "req": [1, "resize_channel", [{ + "req": [1, "resize_channel", { "channel_id": "0x4567890123abcdef...", "allocate_amount": "200000000", "resize_amount": "1000000000", "funds_destination": "0x1234567890abcdef..." - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` `allocate_amount` is how much more token user wants to allocate to this token-network specific channel from his unified balance. -`resize_amount` is how much user wants to deposit or withdraw from a token-network speecific channel. +`resize_amount` is how much user wants to deposit or withdraw from a token-network specific channel. Example: + - Initial state: user an open channel on Polygon with 20 usdc, and a channel on Celo with 5 usdc. - User wants to deposit 75 usdc on Celo. User calls `resize_channel`, with `allocate_amount=0` and `resize_amount=75`. - Now user's unified balance is 100 usdc (20 on Polygon and 80 on Celo). @@ -953,7 +993,7 @@ Example: ```json { - "res": [1, "resize_channel", [{ + "res": [1, "resize_channel", { "channel_id": "0x4567890123abcdef...", "state": { "intent": 2, // IntentRESIZE @@ -973,7 +1013,7 @@ Example: ] }, "server_signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1c" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -990,9 +1030,9 @@ Sends a message to all participants in a virtual app session. ```json { - "req": [1, "your_custom_method", [{ + "req": [1, "your_custom_method", { "your_custom_field": "Hello, application participants!" - }], 1619123456789], + }, 1619123456789], "sid": "0x3456789012abcdef...", // Virtual App Session ID "sig": ["0x9876fedcba..."] } @@ -1004,9 +1044,9 @@ Responses can also be forwarded to all participants in a virtual application by ```json { - "res": [1, "your_custom_method", [{ + "res": [1, "your_custom_method", { "your_custom_field": "I confirm that I have received your message!" - }], 1619123456789], + }, 1619123456789], "sid": "0x3456789012abcdef...", // Virtual App Session ID "sig": ["0x9876fedcba..."] } @@ -1022,7 +1062,7 @@ Simple ping to check connectivity. ```json { - "req": [1, "ping", [], 1619123456789], + "req": [1, "ping", {}, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -1031,7 +1071,7 @@ Simple ping to check connectivity. ```json { - "res": [1, "pong", [], 1619123456789], + "res": [1, "pong", {}, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1039,6 +1079,7 @@ Simple ping to check connectivity. ### Balance Updates The server automatically sends balance updates to clients in these scenarios: + 1. After successful authentication (as a welcome message) 2. After channel operations (open, close, resize) 3. After application operations (create, close) @@ -1047,16 +1088,18 @@ Balance updates are sent as unsolicited server messages with the "bu" method: ```json { - "res": [1234567890123, "bu", [[ - { - "asset": "usdc", - "amount": "100.0" - }, - { - "asset": "eth", - "amount": "0.5" - } - ]], 1619123456789], + "res": [1234567890123, "bu", { + "balance_updates": [ + { + "asset": "usdc", + "amount": "100.0" + }, + { + "asset": "eth", + "amount": "0.5" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1069,36 +1112,38 @@ The server automatically sends all open channels as a batch update to clients af ```json { - "res": [1234567890123, "channels", [[ - { - "channel_id": "0xfedcba9876543210...", - "participant": "0x1234567890abcdef...", - "status": "open", - "token": "0xeeee567890abcdef...", - "amount": "100000", - "chain_id": 137, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 2, - "created_at": "2023-05-01T12:00:00Z", - "updated_at": "2023-05-01T12:30:00Z" - }, - { - "channel_id": "0xabcdef1234567890...", - "participant": "0x1234567890abcdef...", - "status": "open", - "token": "0xeeee567890abcdef...", - "amount": "50000", - "chain_id": 42220, - "adjudicator": "0xAdjudicatorContractAddress...", - "challenge": 86400, - "nonce": 1, - "version": 3, - "created_at": "2023-04-15T10:00:00Z", - "updated_at": "2023-04-20T14:30:00Z" - } - ]], 1619123456789], + "res": [1234567890123, "channels", { + "channels": [ + { + "channel_id": "0xfedcba9876543210...", + "participant": "0x1234567890abcdef...", + "status": "open", + "token": "0xeeee567890abcdef...", + "amount": "100000", + "chain_id": 137, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 2, + "created_at": "2023-05-01T12:00:00Z", + "updated_at": "2023-05-01T12:30:00Z" + }, + { + "channel_id": "0xabcdef1234567890...", + "participant": "0x1234567890abcdef...", + "status": "open", + "token": "0xeeee567890abcdef...", + "amount": "50000", + "chain_id": 42220, + "adjudicator": "0xAdjudicatorContractAddress...", + "challenge": 86400, + "nonce": 1, + "version": 3, + "created_at": "2023-04-15T10:00:00Z", + "updated_at": "2023-04-20T14:30:00Z" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1106,6 +1151,7 @@ The server automatically sends all open channels as a batch update to clients af ### Channel Updates For channel updates, the server sends them in these scenarios: + 1. When a channel is created 2. When a channel's status changes (open, joined, closed) 3. When a channel is resized @@ -1114,7 +1160,7 @@ Individual channel updates are sent as unsolicited server messages with the "cu" ```json { - "res": [1234567890123, "cu", [{ + "res": [1234567890123, "cu", { "channel_id": "0xfedcba9876543210...", "participant": "0x1234567890abcdef...", "status": "open", @@ -1127,7 +1173,7 @@ Individual channel updates are sent as unsolicited server messages with the "cu" "version": 2, "created_at": "2023-05-01T12:00:00Z", "updated_at": "2023-05-01T12:30:00Z" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1142,30 +1188,32 @@ Transfer notifications are sent as unsolicited server messages with the "transfe ```json { - "res": [1234567890123, "tr", [[ - { - "id": "1", - "tx_type": "transfer", - "from_account": "0x9876543210abcdef...", - "from_account_tag": "ABC123", - "to_account": "0x1234567890abcdef...", - "to_account_tag": "XYZ789", - "asset": "usdc", - "amount": "50.0", - "created_at": "2023-05-01T12:00:00Z" - }, - { - "id": "2", - "tx_type": "transfer", - "from_account": "0x9876543210abcdef...", - "from_account_tag": "ABC123", - "to_account": "0x1234567890abcdef...", - "to_account_tag": "XYZ789", - "asset": "weth", - "amount": "0.1", - "created_at": "2023-05-01T12:00:00Z" - } - ]], 1619123456789], + "res": [1234567890123, "tr", { + "transactions": [ + { + "id": "1", + "tx_type": "transfer", + "from_account": "0x9876543210abcdef...", + "from_account_tag": "ABC123", + "to_account": "0x1234567890abcdef...", + "to_account_tag": "XYZ789", + "asset": "usdc", + "amount": "50.0", + "created_at": "2023-05-01T12:00:00Z" + }, + { + "id": "2", + "tx_type": "transfer", + "from_account": "0x9876543210abcdef...", + "from_account_tag": "ABC123", + "to_account": "0x1234567890abcdef...", + "to_account_tag": "XYZ789", + "asset": "weth", + "amount": "0.1", + "created_at": "2023-05-01T12:00:00Z" + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1190,7 +1238,7 @@ Retrieves broker configuration information including supported networks. ```json { - "req": [1, "get_config", [], 1619123456789], + "req": [1, "get_config", {}, 1619123456789], "sig": [] } ``` @@ -1199,7 +1247,7 @@ Retrieves broker configuration information including supported networks. ```json { - "res": [1, "get_config", [{ + "res": [1, "get_config", { "broker_address": "0xbbbb567890abcdef...", "networks": [ { @@ -1218,7 +1266,7 @@ Retrieves broker configuration information including supported networks. "adjudicator_address":"0xCustodyContractAddress1..." } ] - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1233,7 +1281,7 @@ Retrieves all supported assets. Optionally, you can filter the assets by chain_i ```json { - "req": [1, "get_assets", [], 1619123456789], + "req": [1, "get_assets", {}, 1619123456789], "sig": [] } ``` @@ -1242,9 +1290,9 @@ Retrieves all supported assets. Optionally, you can filter the assets by chain_i ```json { - "req": [1, "get_assets", [{ + "req": [1, "get_assets", { "chain_id": 137 - }], 1619123456789], + }, 1619123456789], "sig": [] } ``` @@ -1253,24 +1301,28 @@ Retrieves all supported assets. Optionally, you can filter the assets by chain_i ```json { - "res": [1, "get_assets", [[{ - "token": "0xeeee567890abcdef...", - "chain_id": 137, - "symbol": "usdc", - "decimals": 6 - }, - { - "token": "0xffff567890abcdef...", - "chain_id": 137, - "symbol": "weth", - "decimals": 18 - }, - { - "token": "0xaaaa567890abcdef...", - "chain_id": 42220, - "symbol": "celo", - "decimals": 18 - }]], 1619123456789], + "res": [1, "get_assets", { + "assets": [ + { + "token": "0xeeee567890abcdef...", + "chain_id": 137, + "symbol": "usdc", + "decimals": 6 + }, + { + "token": "0xffff567890abcdef...", + "chain_id": 137, + "symbol": "weth", + "decimals": 18 + }, + { + "token": "0xaaaa567890abcdef...", + "chain_id": 42220, + "symbol": "celo", + "decimals": 18 + } + ] + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` @@ -1281,9 +1333,9 @@ When an error occurs, the server responds with an error message: ```json { - "res": [REQUEST_ID, "error", [{ + "res": [REQUEST_ID, "error", { "error": "Error message describing what went wrong" - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` diff --git a/clearnode/docs/Clearnode.protocol.md b/clearnode/docs/Clearnode.protocol.md index 0c3545eaf..200d12842 100644 --- a/clearnode/docs/Clearnode.protocol.md +++ b/clearnode/docs/Clearnode.protocol.md @@ -1,11 +1,13 @@ # Clearnode Protocol Specification ## Overview + The Clearnode protocol is a system for managing payment channels and virtual applications between participants. It provides a secure, efficient way to conduct transactions off-chain while retaining the ability to settle on-chain when required, with support for multiple blockchain networks. ## Protocol Flow ### 1. Blockchain Channels and Credit + - The protocol accepts blockchain channels to credit participants' balances in the database ledger - Participants create on-chain channels through custody contracts (supported on multiple chains including Polygon, Celo, and Base) - Channel creation events from the blockchain are received through webhooks and processed by the `EventHandler` @@ -13,6 +15,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app - Each participant has an `Account` in the ledger tied to their address. ### 2. Virtual Application Creation + - After being credited from on-chain channels, participants can create virtual applications with other participants - Virtual applications allow participants to allocate a portion of their balance for peer-to-peer transactions without requiring on-chain operations - The broker validates that: @@ -25,11 +28,13 @@ The Clearnode protocol is a system for managing payment channels and virtual app - The broker sets up message routing between participants ### 3. Virtual Application Operations + - Participants send both requests and responses to each other through virtual applications using WebSocket connections - Any message (request or response) with an AppID specified is forwarded to all other participants - The broker maintains a real-time bidirectional communication layer for message routing ### 4. Virtual Application Closure and Settlement + - When participants wish to close a virtual application, authorized signers must provide signatures that meet the quorum threshold - The broker validates the signatures against the list of authorized signers and their weights registered during application creation - The broker validates the final allocation of funds between participants @@ -43,6 +48,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app ## Security Features ### Authentication and Authorization + - All operations are authenticated using cryptographic signatures - The system uses ECDSA signatures compatible with Ethereum accounts - Virtual applications implement a multi-signature scheme: @@ -61,6 +67,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app - Tokens use ES256 signatures for verification ### Multi-Chain Support + - The system supports multiple blockchain networks (currently Polygon, Celo, and Base) - Each network has its own custody contract address and connection details - Chain IDs are tracked with channels to ensure proper chain association @@ -72,6 +79,7 @@ The Clearnode protocol is a system for managing payment channels and virtual app - The `get_channels` method returns all channels for a participant across all supported chains ## Benefits + - Efficient, low-cost transactions by keeping most operations off-chain - Security guarantees of blockchain when needed - Participants can freely transact within their allocated funds in virtual applications @@ -89,7 +97,7 @@ All messages exchanged between clients and clearnodes follow this standardized f ```json { - "req": [REQUEST_ID, METHOD, [PARAMETERS], TIMESTAMP], + "req": [REQUEST_ID, METHOD, PARAMETERS, TIMESTAMP], "sid": "APP_SESSION_ID", // AppId for Virtual Ledgers for Internal Communication "sig": ["SIGNATURE"] // Client's signature of the entire "req" object } @@ -102,7 +110,7 @@ All messages exchanged between clients and clearnodes follow this standardized f ```json { - "res": [REQUEST_ID, METHOD, [RESPONSE_DATA], TIMESTAMP], + "res": [REQUEST_ID, METHOD, RESPONSE_DATA, TIMESTAMP], "sid": "APP_SESSION_ID", // AppId for Virtual Ledgers for Internal Communication "sig": ["SIGNATURE"] } @@ -111,14 +119,13 @@ All messages exchanged between clients and clearnodes follow this standardized f - The `sid` field serves as both the subject and destination pubsub topic for the message. There is a one-to-one mapping between topics and ledger accounts. - The `sig` field contains one or more signatures, of the `res` data. - The structure breakdown: - `REQUEST_ID`: A unique identifier for the request/response pair (`uint64`) - `METHOD`: The name of the method being called (`string`) -- `PARAMETERS`/`RESPONSE_DATA`: An array of parameters/response data (`[]any`) +- `PARAMETERS`/`RESPONSE_DATA`: An object of parameters/response data (`map[string]any`) - `TIMESTAMP`: Unix timestamp of the request/response in milliseconds (`uint64`) -- `APP_SESSION_ID` (`sid`): If specified, the message gets forwarded to all participants of a virtual app with thosn AppSessionID. +- `APP_SESSION_ID` (`sid`): If specified, the message gets forwarded to all participants of a virtual app with those AppSessionID. - `SIGNATURE`: Cryptographic signatures of the message (`[]string`). Multiple signatures may be required for certain operations. ## Data Types @@ -149,7 +156,7 @@ The client initiates authentication by sending an `auth_request` request with th ```json { - "req": [1, "auth_request", [{ + "req": [1, "auth_request", { "address": "0x1234567890abcdef...", "session_key": "0x9876543210fedcba...", // Optional: If specified, enables delegation "app_name": "Example App", // Optional: Application name @@ -162,7 +169,7 @@ The client initiates authentication by sending an `auth_request` request with th "scope": "app.create", // Optional: Permission scope "expire": "24h", // Optional: Session expiration time "application": "0xApplication1234..." // Optional: Application public address - }], 1619123456789], + }, 1619123456789], "sig": ["0x5432abcdef..."] } ``` @@ -173,9 +180,9 @@ The server responds with a random string challenge token. ```json { - "res": [1, "auth_challenge", [{ + "res": [1, "auth_challenge", { "challenge_message": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x9876fedcba..."] } ``` @@ -186,10 +193,10 @@ The client sends a verification request with the challenge token signed by the c ```json { - "req": [2, "auth_verify", [{ + "req": [2, "auth_verify", { "address": "0x1234567890abcdef...", "challenge": "550e8400-e29b-41d4-a716-446655440000" - }], 1619123456789], + }, 1619123456789], "sig": ["0x2345bcdef..."] } ``` @@ -206,16 +213,17 @@ If authentication is successful, the server responds with a success confirmation ```json { - "res": [2, "auth_verify", [{ + "res": [2, "auth_verify", { "address": "0x1234567890abcdef...", "success": true, "jwt_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..." // JWT token for session management - }], 1619123456789], + }, 1619123456789], "sig": ["0xabcd1234..."] } ``` The JWT token contains: + - Policy information (wallet, participant, scope, application) - Asset allowances (if specified) - Standard JWT claims (expiration, issuer, etc.) @@ -255,9 +263,9 @@ When an error occurs, the server responds with an error message: ```json { - "res": [REQUEST_ID, "error", [{ + "res": [REQUEST_ID, "error", { "error": "Error message describing what went wrong" - }], TIMESTAMP], + }, TIMESTAMP], "sig": ["SIGNATURE"] } ``` @@ -304,4 +312,4 @@ When an error occurs, the server responds with an error message: - Never reuse signatures across different sessions or services - Verify all message signatures from the server before processing - Ensure your private key is securely stored and never exposed - - Generate a fresh unique identifier client-side for each request ID \ No newline at end of file + - Generate a fresh unique identifier client-side for each request ID diff --git a/clearnode/notification.go b/clearnode/notification.go index 1a6eed696..6d7569343 100644 --- a/clearnode/notification.go +++ b/clearnode/notification.go @@ -9,11 +9,11 @@ import ( ) type WSNotifier struct { - notify func(userID string, method string, params ...any) + notify func(userID string, method string, params RPCDataParams) logger Logger } -func NewWSNotifier(notifyFunc func(userID string, method string, params ...any), logger Logger) *WSNotifier { +func NewWSNotifier(notifyFunc func(userID string, method string, params RPCDataParams), logger Logger) *WSNotifier { return &WSNotifier{ notify: notifyFunc, logger: logger, @@ -55,7 +55,7 @@ func NewBalanceNotification(wallet string, db *gorm.DB) *Notification { return &Notification{ userID: wallet, eventType: BalanceUpdateEventType, - data: balances, + data: BalanceUpdatesResponse{BalanceUpdates: balances}, } } @@ -82,7 +82,7 @@ func NewChannelNotification(channel Channel) *Notification { } // NewTransferNotification creates a notification for a transfer event -func NewTransferNotification(wallet string, transferredAllocations []TransactionResponse) *Notification { +func NewTransferNotification(wallet string, transferredAllocations TransferResponse) *Notification { return &Notification{ userID: wallet, eventType: TransferEventType, diff --git a/clearnode/reconcile_cli.go b/clearnode/reconcile_cli.go index b3b546248..230adee07 100644 --- a/clearnode/reconcile_cli.go +++ b/clearnode/reconcile_cli.go @@ -60,7 +60,7 @@ func runReconcileCli(logger Logger) { custody, err := NewCustody( signer, db, - NewWSNotifier(func(userID, method string, params ...any) {}, logger), + NewWSNotifier(func(userID, method string, params RPCDataParams) {}, logger), network.InfuraURL, network.CustodyAddress, network.AdjudicatorAddress, diff --git a/clearnode/rpc.go b/clearnode/rpc.go index 9e373853c..3a4d8686b 100644 --- a/clearnode/rpc.go +++ b/clearnode/rpc.go @@ -38,13 +38,16 @@ func (r RPCMessage) GetRequestSignersMap() (map[string]struct{}, error) { return recoveredAddresses, nil } +// TODO: ensure that it accepts only structs or maps, and prevent passing primitive (and other DS) types +type RPCDataParams = any + // RPCData represents the common structure for both requests and responses // Format: [request_id, method, params, ts] type RPCData struct { - RequestID uint64 `json:"request_id" validate:"required"` - Method string `json:"method" validate:"required"` - Params []any `json:"params" validate:"required"` - Timestamp uint64 `json:"ts" validate:"required"` + RequestID uint64 `json:"request_id" validate:"required"` + Method string `json:"method" validate:"required"` + Params RPCDataParams `json:"params" validate:"required"` + Timestamp uint64 `json:"ts" validate:"required"` rawBytes []byte } @@ -66,7 +69,7 @@ func (m *RPCData) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(rawArr[1], &m.Method); err != nil { return fmt.Errorf("invalid method: %w", err) } - // Element 2: []any Params + // Element 2: RPCDataParams Params if err := json.Unmarshal(rawArr[2], &m.Params); err != nil { return fmt.Errorf("invalid params: %w", err) } @@ -92,7 +95,7 @@ func (m RPCData) MarshalJSON() ([]byte, error) { } // CreateResponse is unchanged. It simply constructs an RPCMessage with a "res" array. -func CreateResponse(id uint64, method string, responseParams []any) *RPCMessage { +func CreateResponse(id uint64, method string, responseParams RPCDataParams) *RPCMessage { return &RPCMessage{ Res: &RPCData{ RequestID: id, diff --git a/clearnode/rpc_node.go b/clearnode/rpc_node.go index 48399dfbc..88376f529 100644 --- a/clearnode/rpc_node.go +++ b/clearnode/rpc_node.go @@ -245,7 +245,7 @@ type RPCHandler func(c *RPCContext) // SendRPCMessageFunc is a function type for sending RPC notifications to a connection. // It's provided to event handlers to allow server-initiated messages. -type SendRPCMessageFunc func(method string, params ...any) +type SendRPCMessageFunc func(method string, params RPCDataParams) // RPCContext contains all the information about an RPC request and provides // methods for handlers to process and respond to the request. @@ -279,7 +279,7 @@ func (c *RPCContext) Next() { // Succeed sets a successful response with the given method and parameters. // This should be called by handlers to indicate successful processing. -func (c *RPCContext) Succeed(method string, params ...any) { +func (c *RPCContext) Succeed(method string, params RPCDataParams) { c.Message.Res = &RPCData{ RequestID: c.Message.Req.RequestID, Method: method, @@ -329,7 +329,7 @@ func (c *RPCContext) Fail(err error, fallbackMessage string) { c.Message.Res = &RPCData{ RequestID: c.Message.Req.RequestID, Method: "error", - Params: []any{message}, + Params: ErrorResponse{Error: message}, Timestamp: uint64(time.Now().UnixMilli()), } } @@ -371,9 +371,9 @@ func prepareRawRPCResponse(signer *Signer, data *RPCData) ([]byte, error) { // prepareRawNotification creates a signed server-initiated notification message. // Unlike responses, notifications don't correspond to a specific request. -func prepareRawNotification(signer *Signer, method string, params ...any) ([]byte, error) { +func prepareRawNotification(signer *Signer, method string, params RPCDataParams) ([]byte, error) { if params == nil { - params = []any{} + params = struct{}{} } data := &RPCData{ @@ -468,8 +468,8 @@ func (wn *RPCNode) OnAuthenticated(handler func(userID string, send SendRPCMessa // Notify sends a server-initiated notification to a specific authenticated user. // If the user is not connected, the notification is silently dropped. -func (wn *RPCNode) Notify(userID, method string, params ...any) { - message, err := prepareRawNotification(wn.signer, method, params...) +func (wn *RPCNode) Notify(userID, method string, params RPCDataParams) { + message, err := prepareRawNotification(wn.signer, method, params) if err != nil { wn.logger.Error("failed to prepare notification message", "error", err, "userID", userID, "method", method) return @@ -481,8 +481,8 @@ func (wn *RPCNode) Notify(userID, method string, params ...any) { // getSendMessageFunc creates a SendRPCMessageFunc for a specific connection. // The returned function can be used to send notifications to that connection. func (wn *RPCNode) getSendMessageFunc(conn *RPCConnection) SendRPCMessageFunc { - return func(method string, params ...any) { - message, err := prepareRawNotification(wn.signer, method, params...) + return func(method string, params RPCDataParams) { + message, err := prepareRawNotification(wn.signer, method, params) if err != nil { wn.logger.Error("failed to prepare notification message", "error", err, "method", method) return @@ -511,7 +511,7 @@ func (wn *RPCNode) sendErrorResponse(conn *RPCConnection, requestID uint64, mess data := &RPCData{ RequestID: requestID, Method: "error", - Params: []any{message}, + Params: ErrorResponse{Error: message}, Timestamp: uint64(time.Now().UnixMilli()), } diff --git a/clearnode/rpc_node_test.go b/clearnode/rpc_node_test.go index bd013069c..e33fbabb9 100644 --- a/clearnode/rpc_node_test.go +++ b/clearnode/rpc_node_test.go @@ -37,6 +37,15 @@ func TestRPCNode(t *testing.T) { previousExecMethodKey := "previous_exec_method" authMethod := "auth.test" + paramsUserIDKey := "userID" + paramsPrevMethodKey := "prev" + paramsRootMwKey := "rootMw" + paramsGroupAMwKey := "groupA" + paramsGroupBMwKey := "groupB" + paramsErrorKey := "error" + paramsOnConnectCounts := "onConnectCounts" + paramsOnAuthCounts := "onAuthCounts" + onConnectMethod := "on_connect.test" onConnectCounts := 0 node.OnConnect(func(send SendRPCMessageFunc) { @@ -44,7 +53,10 @@ func TestRPCNode(t *testing.T) { defer mu.Unlock() onConnectCounts++ - send(onConnectMethod, onConnectCounts) + params := map[string]any{ + paramsOnConnectCounts: onConnectCounts, + } + send(onConnectMethod, params) }) onDisconnectSignal := newTestSignal() @@ -65,7 +77,11 @@ func TestRPCNode(t *testing.T) { defer mu.Unlock() onAuthenticatedCounts++ - send(onAuthenticatedMethod, onAuthenticatedCounts, userID) + params := map[string]any{ + paramsOnAuthCounts: onAuthenticatedCounts, + paramsUserIDKey: userID, + } + send(onAuthenticatedMethod, params) }) onMessageSentSignal := newTestSignal() @@ -110,7 +126,14 @@ func TestRPCNode(t *testing.T) { groupBMwValue = false } } - c.Succeed(method, c.UserID, prevMethod, rootMwValue, groupAMwValue, groupBMwValue) + params := map[string]any{ + paramsUserIDKey: c.UserID, + paramsPrevMethodKey: prevMethod, + paramsRootMwKey: rootMwValue, + paramsGroupAMwKey: groupAMwValue, + paramsGroupBMwKey: groupBMwValue, + } + c.Succeed(method, params) c.Storage.Set(previousExecMethodKey, method) } } @@ -128,7 +151,10 @@ func TestRPCNode(t *testing.T) { node.Handle(rootMethod, createDummyHandler(rootMethod)) node.Handle(authMethod, func(c *RPCContext) { logger.Debug("executing auth handler") - c.Succeed(authMethod, authenticatedUserID) + params := map[string]any{ + paramsUserIDKey: authenticatedUserID, + } + c.Succeed(authMethod, params) c.UserID = authenticatedUserID // Simulate authenticated user }) @@ -177,7 +203,7 @@ func TestRPCNode(t *testing.T) { } // Helper function to send request and receive response - sendAndReceive := func(t *testing.T, RequestID uint64, method string, params ...interface{}) *RPCMessage { + sendAndReceive := func(t *testing.T, RequestID uint64, method string, params RPCDataParams) *RPCMessage { if params == nil { params = []interface{}{} } @@ -218,7 +244,7 @@ func TestRPCNode(t *testing.T) { // Test root handler t.Run("root handler", func(t *testing.T) { - resp := sendAndReceive(t, 1, rootMethod) + resp := sendAndReceive(t, 1, rootMethod, nil) mu.Lock() defer mu.Unlock() @@ -227,17 +253,21 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, rootMethod, resp.Res.Method) assert.Len(t, resp.Res.Params, 5) assert.Len(t, resp.Sig, 1) - assert.Equal(t, "", resp.Res.Params[0]) // not authenticated - assert.Equal(t, "", resp.Res.Params[1]) // previous dummy method empty - assert.Equal(t, true, resp.Res.Params[2]) // root middleware executed - assert.Equal(t, false, resp.Res.Params[3]) // group A middleware not executed - assert.Equal(t, false, resp.Res.Params[4]) // group B middleware not executed - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, "", params[paramsUserIDKey]) // not authenticated + assert.Equal(t, "", params[paramsPrevMethodKey]) // previous dummy method empty + assert.Equal(t, true, params[paramsRootMwKey]) // root middleware executed + assert.Equal(t, false, params[paramsGroupAMwKey]) // group A middleware not executed + assert.Equal(t, false, params[paramsGroupBMwKey]) // group B middleware not executed + assert.True(t, onMessageSentSignal.await()) // on message sent signal }) // Test auth handler t.Run("auth handler", func(t *testing.T) { - resp := sendAndReceive(t, 1, authMethod) + resp := sendAndReceive(t, 1, authMethod, nil) // So we definitely receive both authMethod and onAuthenticatedMethod time.Sleep(100 * time.Millisecond) @@ -247,8 +277,12 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, authMethod, resp.Res.Method) assert.Len(t, resp.Res.Params, 1) assert.Len(t, resp.Sig, 1) - assert.Equal(t, authenticatedUserID, resp.Res.Params[0]) // authenticated user ID - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // authenticated user ID + assert.True(t, onMessageSentSignal.await()) // on message sent signal mu.Unlock() // on authenticated method executed @@ -259,15 +293,20 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, onAuthenticatedMethod, resp.Res.Method) assert.Len(t, resp.Res.Params, 2) assert.Len(t, resp.Sig, 1) - assert.Equal(t, 1, onAuthenticatedCounts) // on authenticated counts - assert.Equal(t, authenticatedUserID, resp.Res.Params[1]) // authenticated user ID - assert.True(t, onMessageSentSignal.await()) // on message sent signal + assert.Equal(t, 1, onAuthenticatedCounts) // on authenticated counts + + params, ok = resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // authenticated user ID + assert.True(t, onMessageSentSignal.await()) // on message sent signal + mu.Unlock() }) // Test group handler 1 t.Run("group handler 1", func(t *testing.T) { - resp := sendAndReceive(t, 2, groupMethodA) + resp := sendAndReceive(t, 2, groupMethodA, nil) mu.Lock() defer mu.Unlock() @@ -276,17 +315,21 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, groupMethodA, resp.Res.Method) assert.Len(t, resp.Res.Params, 5) assert.Len(t, resp.Sig, 1) - assert.Equal(t, authenticatedUserID, resp.Res.Params[0]) // this method - assert.Equal(t, rootMethod, resp.Res.Params[1]) // previous dummy method root - assert.Equal(t, true, resp.Res.Params[2]) // root middleware executed - assert.Equal(t, true, resp.Res.Params[3]) // group A middleware executed - assert.Equal(t, false, resp.Res.Params[4]) // group B middleware not executed - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // this method + assert.Equal(t, rootMethod, params[paramsPrevMethodKey]) // previous dummy method root + assert.Equal(t, true, params[paramsRootMwKey]) // root middleware executed + assert.Equal(t, true, params[paramsGroupAMwKey]) // group A middleware executed + assert.Equal(t, false, params[paramsGroupBMwKey]) // group B middleware not executed + assert.True(t, onMessageSentSignal.await()) // on message sent signal }) // Test group handler 2 t.Run("group handler 2", func(t *testing.T) { - resp := sendAndReceive(t, 3, groupMethodB) + resp := sendAndReceive(t, 3, groupMethodB, nil) mu.Lock() defer mu.Unlock() @@ -295,17 +338,21 @@ func TestRPCNode(t *testing.T) { assert.Equal(t, groupMethodB, resp.Res.Method) assert.Len(t, resp.Res.Params, 5) assert.Len(t, resp.Sig, 1) - assert.Equal(t, authenticatedUserID, resp.Res.Params[0]) // this method - assert.Equal(t, groupMethodA, resp.Res.Params[1]) // previous dummy method root - assert.Equal(t, true, resp.Res.Params[2]) // root middleware executed - assert.Equal(t, true, resp.Res.Params[3]) // group A middleware executed - assert.Equal(t, true, resp.Res.Params[4]) // group B middleware executed - assert.True(t, onMessageSentSignal.await()) // on message sent signal + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Equal(t, authenticatedUserID, params[paramsUserIDKey]) // this method + assert.Equal(t, groupMethodA, params[paramsPrevMethodKey]) // previous dummy method root + assert.Equal(t, true, params[paramsRootMwKey]) // root middleware executed + assert.Equal(t, true, params[paramsGroupAMwKey]) // group A middleware executed + assert.Equal(t, true, params[paramsGroupBMwKey]) // group B middleware executed + assert.True(t, onMessageSentSignal.await()) // on message sent signal }) // Test unknown method t.Run("unknown method", func(t *testing.T) { - resp := sendAndReceive(t, 4, "unknown.method") + resp := sendAndReceive(t, 4, "unknown.method", nil) mu.Lock() defer mu.Unlock() @@ -313,7 +360,11 @@ func TestRPCNode(t *testing.T) { require.NotNil(t, resp.Res) assert.Equal(t, "error", resp.Res.Method) assert.Len(t, resp.Res.Params, 1) - assert.Contains(t, resp.Res.Params[0], "unknown method") + + params, ok := resp.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Contains(t, params[paramsErrorKey], "unknown method") assert.True(t, onMessageSentSignal.await()) // on message sent signal }) @@ -333,7 +384,11 @@ func TestRPCNode(t *testing.T) { require.NotNil(t, respMsg.Res) assert.Equal(t, "error", respMsg.Res.Method) - assert.Contains(t, respMsg.Res.Params[0], "invalid message format") + + params, ok := respMsg.Res.Params.(map[string]any) + require.True(t, ok, "params should be a map[string]any") + + assert.Contains(t, params[paramsErrorKey], "invalid message format") assert.True(t, onMessageSentSignal.await()) // on message sent signal }) diff --git a/clearnode/rpc_router.go b/clearnode/rpc_router.go index 7f0d13996..c4d2fbbb1 100644 --- a/clearnode/rpc_router.go +++ b/clearnode/rpc_router.go @@ -109,12 +109,12 @@ func (r *RPCRouter) HandleConnect(send SendRPCMessageFunc) { } // Convert to AssetResponse format - response := make([]GetAssetsResponse, 0, len(assets)) + respAssets := make([]AssetResponse, 0, len(assets)) for _, asset := range assets { - response = append(response, GetAssetsResponse(asset)) + respAssets = append(respAssets, AssetResponse(asset)) } - send("assets", response) + send("assets", AssetsResponse{Assets: respAssets}) } func (r *RPCRouter) HandleDisconnect(userID string) { @@ -130,9 +130,9 @@ func (r *RPCRouter) HandleAuthenticated(userID string, send SendRPCMessageFunc) r.lg.Error("error retrieving channels for participant", "error", err) } - resp := []ChannelResponse{} + respChannels := []ChannelResponse{} for _, ch := range channels { - resp = append(resp, ChannelResponse{ + respChannels = append(respChannels, ChannelResponse{ ChannelID: ch.ChannelID, Participant: ch.Participant, Status: ch.Status, @@ -149,7 +149,7 @@ func (r *RPCRouter) HandleAuthenticated(userID string, send SendRPCMessageFunc) } // Send channel updates - send("channels", resp) + send("channels", ChannelsResponse{Channels: respChannels}) // Send initial balances balances, err := GetWalletLedger(r.DB, common.HexToAddress(walletAddress)).GetBalances(NewAccountID(walletAddress)) @@ -157,7 +157,7 @@ func (r *RPCRouter) HandleAuthenticated(userID string, send SendRPCMessageFunc) r.lg.Error("error getting balances", "sender", walletAddress, "error", err) return } - send("bu", balances) + send("bu", BalanceUpdatesResponse{BalanceUpdates: balances}) } func (r *RPCRouter) HandleMessageSent() { @@ -236,16 +236,16 @@ func (r *RPCRouter) HistoryMiddleware(c *RPCContext) { } } -func parseParams(params []any, unmarshalTo any) error { - if len(params) > 0 { - paramsJSON, err := json.Marshal(params[0]) - if err != nil { - return fmt.Errorf("failed to parse parameters: %w", err) - } - err = json.Unmarshal(paramsJSON, &unmarshalTo) - if err != nil { - return err - } +func parseParams(params RPCDataParams, unmarshalTo any) error { + paramsJSON, err := json.Marshal(params) + if err != nil { + return fmt.Errorf("failed to parse parameters: %w", err) } + + err = json.Unmarshal(paramsJSON, &unmarshalTo) + if err != nil { + return err + } + return getValidator().Struct(unmarshalTo) } diff --git a/clearnode/rpc_router_auth.go b/clearnode/rpc_router_auth.go index 16fd29e9b..7e2fa0c47 100644 --- a/clearnode/rpc_router_auth.go +++ b/clearnode/rpc_router_auth.go @@ -10,13 +10,13 @@ import ( ) type AuthRequestParams struct { - Address string `json:"address"` // The wallet address requesting authentication - SessionKey string `json:"session_key"` // The session key for the authentication - AppName string `json:"app_name"` // The name of the application requesting authentication - Allowances []Allowance `json:"allowances"` // Allowances for the application - Expire string `json:"expire"` // Expiration time for the authentication - Scope string `json:"scope"` // Scope of the authentication - ApplicationAddress string `json:"application_address"` // The address of the application requesting authentication + Address string `json:"address"` // The wallet address requesting authentication + SessionKey string `json:"session_key"` // The session key for the authentication + AppName string `json:"app_name"` // The name of the application requesting authentication + Allowances []Allowance `json:"allowances"` // Allowances for the application + Expire string `json:"expire"` // Expiration time for the authentication + Scope string `json:"scope"` // Scope of the authentication + ApplicationAddress string `json:"application"` // The address of the application requesting authentication } // AuthResponse represents the server's challenge response @@ -40,69 +40,11 @@ func (r *RPCRouter) HandleAuthRequest(c *RPCContext) { // Parse the parameters var authParams AuthRequestParams - // TODO: change params format to a single object and use the commented code below - // if err := parseParams(req.Params, &authParams); err != nil { - // c.Fail(err.Error()) - // return - // } - - if len(req.Params) < 7 { - c.Fail(nil, "invalid parameters: expected 7 parameters") - return - } - - addr, ok := req.Params[0].(string) - if !ok || addr == "" { - c.Fail(nil, fmt.Sprintf("invalid address: %v", req.Params[0])) - return - } - - sessionKey, ok := req.Params[1].(string) - if !ok || sessionKey == "" { - c.Fail(nil, fmt.Sprintf("invalid session key: %v", req.Params[1])) - return - } - - appName, ok := req.Params[2].(string) - if !ok || appName == "" { - c.Fail(nil, fmt.Sprintf("invalid application name: %v", req.Params[2])) - return - } - - rawAllowances := req.Params[3] - allowances, err := parseAllowances(rawAllowances) - if err != nil { - c.Fail(err, fmt.Sprintf("invalid allowances: %s", err.Error())) - return - } - - expire, ok := req.Params[4].(string) - if !ok { - c.Fail(nil, fmt.Sprintf("invalid expiration time: %v", req.Params[4])) - return - } - - scope, ok := req.Params[5].(string) - if !ok { - c.Fail(nil, fmt.Sprintf("invalid scope: %v", req.Params[5])) + if err := parseParams(req.Params, &authParams); err != nil { + c.Fail(err, "failed to parse auth parameters") return } - applicationAddress, ok := req.Params[6].(string) - if !ok { - c.Fail(nil, fmt.Sprintf("invalid application address: %v", req.Params[6])) - } - - authParams = AuthRequestParams{ - Address: addr, - SessionKey: sessionKey, - AppName: appName, - Allowances: allowances, - Expire: expire, - Scope: scope, - ApplicationAddress: applicationAddress, - } - logger.Debug("incoming auth request", "addr", authParams.Address, "sessionKey", authParams.SessionKey, @@ -309,35 +251,3 @@ func ValidateTimestamp(ts uint64, expirySeconds int) error { } return nil } - -func parseAllowances(rawAllowances any) ([]Allowance, error) { - outerSlice, ok := rawAllowances.([]interface{}) - if !ok { - return nil, fmt.Errorf("input is not a list of allowances") - } - - result := make([]Allowance, len(outerSlice)) - - for i, item := range outerSlice { - innerSlice, ok := item.([]interface{}) - if !ok { - return nil, fmt.Errorf("allowance at index %d is not a list", i) - } - if len(innerSlice) != 2 { - return nil, fmt.Errorf("allowance at index %d must have exactly 2 elements (asset, amount)", i) - } - - asset, ok1 := innerSlice[0].(string) - amount, ok2 := innerSlice[1].(string) - if !ok1 || !ok2 { - return nil, fmt.Errorf("allowance at index %d has non-string asset or amount", i) - } - - result[i] = Allowance{ - Asset: asset, - Amount: amount, - } - } - - return result, nil -} diff --git a/clearnode/rpc_router_callback.go b/clearnode/rpc_router_callback.go new file mode 100644 index 000000000..ce5c29dfd --- /dev/null +++ b/clearnode/rpc_router_callback.go @@ -0,0 +1,66 @@ +package main + +import ( + "time" + + "github.com/ethereum/go-ethereum/common" +) + +type BalanceUpdatesResponse struct { + BalanceUpdates []Balance `json:"balance_updates"` +} + +type ChannelsResponse struct { + Channels []ChannelResponse `json:"channels"` +} + +type AssetsResponse struct { + Assets []AssetResponse `json:"assets"` +} + +// SendBalanceUpdate sends balance updates to the client +func (r *RPCRouter) SendBalanceUpdate(destinationWallet string) { + senderAddress := common.HexToAddress(destinationWallet) + senderAccountID := NewAccountID(destinationWallet) + balances, err := GetWalletLedger(r.DB, senderAddress).GetBalances(senderAccountID) + if err != nil { + r.lg.Error("error getting balances", "userID", destinationWallet, "error", err) + return + } + + r.Node.Notify(destinationWallet, "bu", BalanceUpdatesResponse{BalanceUpdates: balances}) + r.lg.Info("balance update sent", "userID", destinationWallet, "balances", balances) +} + +// SendChannelUpdate sends a single channel update to the client +func (r *RPCRouter) SendChannelUpdate(channel Channel) { + channelResponse := ChannelResponse{ + ChannelID: channel.ChannelID, + Participant: channel.Participant, + Status: channel.Status, + Token: channel.Token, + RawAmount: channel.RawAmount, + ChainID: channel.ChainID, + Adjudicator: channel.Adjudicator, + Challenge: channel.Challenge, + Nonce: channel.Nonce, + Version: channel.Version, + CreatedAt: channel.CreatedAt.Format(time.RFC3339), + UpdatedAt: channel.UpdatedAt.Format(time.RFC3339), + } + + r.Node.Notify(channel.Wallet, "cu", channelResponse) + r.lg.Info("channel update sent", + "userID", channel.Wallet, + "channelID", channel.ChannelID, + "participant", channel.Participant, + "status", channel.Status, + ) +} + +// TODO: make adequate notifications response/type +// SendTransferNotification sends a transfer notification to the client +func (r *RPCRouter) SendTransferNotification(destinationWallet string, transferredAllocations TransferResponse) { + r.Node.Notify(destinationWallet, "tr", transferredAllocations) + r.lg.Info("transfer notification sent", "userID", destinationWallet, "transferred allocations", transferredAllocations) +} diff --git a/clearnode/rpc_router_private.go b/clearnode/rpc_router_private.go index ed9ca5e27..09f351fcd 100644 --- a/clearnode/rpc_router_private.go +++ b/clearnode/rpc_router_private.go @@ -10,8 +10,7 @@ import ( ) type GetLedgerBalancesParams struct { - Participant string `json:"participant,omitempty"` // Optional participant address to filter balances - AccountID string `json:"account_id,omitempty"` // Optional account ID to filter balances + AccountID string `json:"account_id,omitempty"` // Optional account ID to filter balances } type GetRPCHistoryParams struct { @@ -137,6 +136,18 @@ type Balance struct { Amount decimal.Decimal `json:"amount"` } +type GetLedgerBalancesResponse struct { + LedgerBalances []Balance `json:"ledger_balances"` +} + +type TransferResponse struct { + Transactions []TransactionResponse `json:"transactions"` +} + +type GetRPCHistoryResponse struct { + RPCEntries []RPCEntry `json:"rpc_entries"` +} + func (r *RPCRouter) BalanceUpdateMiddleware(c *RPCContext) { logger := LoggerFromContext(c.Context) userAddress := common.HexToAddress(c.UserID) @@ -150,7 +161,7 @@ func (r *RPCRouter) BalanceUpdateMiddleware(c *RPCContext) { logger.Error("error getting balances", "sender", userAddress.Hex(), "error", err) return } - r.Node.Notify(c.UserID, "bu", balances) + r.Node.Notify(c.UserID, "bu", BalanceUpdatesResponse{BalanceUpdates: balances}) // TODO: notify other participants } @@ -171,8 +182,6 @@ func (r *RPCRouter) HandleGetLedgerBalances(c *RPCContext) { userAccountID := NewAccountID(c.UserID) if params.AccountID != "" { userAccountID = NewAccountID(params.AccountID) - } else if params.Participant != "" { - userAccountID = NewAccountID(params.Participant) } ledger := GetWalletLedger(r.DB, userAddress) @@ -183,7 +192,11 @@ func (r *RPCRouter) HandleGetLedgerBalances(c *RPCContext) { return } - c.Succeed(req.Method, balances) + resp := GetLedgerBalancesResponse{ + LedgerBalances: balances, + } + + c.Succeed(req.Method, resp) logger.Info("ledger balances retrieved", "userID", c.UserID, "accountID", userAccountID) } @@ -271,7 +284,7 @@ func (r *RPCRouter) HandleTransfer(c *RPCContext) { return } - var resp []TransactionResponse + var respTransactions []TransactionResponse err = r.DB.Transaction(func(tx *gorm.DB) error { if wallet := GetWalletBySigner(fromWallet); wallet != "" { fromWallet = wallet @@ -322,7 +335,7 @@ func (r *RPCRouter) HandleTransfer(c *RPCContext) { if err != nil { return fmt.Errorf("failed to format transactions: %w", err) } - resp = formattedTransactions + respTransactions = formattedTransactions return nil }) if err != nil { @@ -332,6 +345,10 @@ func (r *RPCRouter) HandleTransfer(c *RPCContext) { return } + resp := TransferResponse{ + Transactions: respTransactions, + } + r.wsNotifier.Notify( NewBalanceNotification(fromWallet, r.DB), NewTransferNotification(fromWallet, resp), @@ -597,7 +614,7 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { return } - response := make([]RPCEntry, 0, len(rpcHistory)) + respRPCEntries := make([]RPCEntry, 0, len(rpcHistory)) for _, record := range rpcHistory { reqSigs, err := nitrolite.SignaturesFromStrings(record.ReqSig) if err != nil { @@ -613,7 +630,7 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { return } - response = append(response, RPCEntry{ + respRPCEntries = append(respRPCEntries, RPCEntry{ ID: record.ID, Sender: record.Sender, ReqID: record.ReqID, @@ -626,8 +643,12 @@ func (r *RPCRouter) HandleGetRPCHistory(c *RPCContext) { }) } - c.Succeed(req.Method, response) - logger.Info("RPC history retrieved", "userID", c.UserID, "entryCount", len(response)) + resp := GetRPCHistoryResponse{ + RPCEntries: respRPCEntries, + } + + c.Succeed(req.Method, resp) + logger.Info("RPC history retrieved", "userID", c.UserID, "entryCount", len(respRPCEntries)) } func verifyAllocations(appSessionBalance, allocationSum map[string]decimal.Decimal) error { diff --git a/clearnode/rpc_router_private_test.go b/clearnode/rpc_router_private_test.go index a09a2e1ac..40fa836c8 100644 --- a/clearnode/rpc_router_private_test.go +++ b/clearnode/rpc_router_private_test.go @@ -44,8 +44,11 @@ func assertErrorResponse(t *testing.T, ctx *RPCContext, expectedContains string) res := ctx.Message.Res require.NotNil(t, res) require.Equal(t, "error", res.Method) - require.Len(t, res.Params, 1) - require.Contains(t, res.Params[0], expectedContains) + + errorParams, ok := res.Params.(ErrorResponse) + require.True(t, ok, "Response parameter should be an ErrorResponse") + + require.Contains(t, errorParams.Error, expectedContains) } func TestRPCRouterHandleGetRPCHistory(t *testing.T) { @@ -157,13 +160,12 @@ func TestRPCRouterHandleGetRPCHistory(t *testing.T) { res := assertResponse(t, ctx, "get_rpc_history") require.Equal(t, uint64(idx+100), res.RequestID) - require.Len(t, res.Params, 1, "Response should contain an array") - rpcHistory, ok := res.Params[0].([]RPCEntry) - require.True(t, ok, "Response parameter should be a slice of RPCEntry") - assert.Len(t, rpcHistory, tc.expectedRecordCount, "Should return expected number of records") + rpcHistory, ok := res.Params.(GetRPCHistoryResponse) + require.True(t, ok, "Response parameter should be a GetRPCHistoryResponse") + assert.Len(t, rpcHistory.RPCEntries, tc.expectedRecordCount, "Should return expected number of records") // Check records are in expected order - for i, record := range rpcHistory { + for i, record := range rpcHistory.RPCEntries { if i < len(tc.expectedReqIDs) { assert.Equal(t, tc.expectedReqIDs[i], record.ReqID, "Record %d should have expected ReqID", i) assert.Equal(t, userAddress, record.Sender, "All records should belong to the requesting participant") @@ -192,7 +194,8 @@ func TestRPCRouterHandleGetLedgerBalances(t *testing.T) { router.HandleGetLedgerBalances(ctx) res := assertResponse(t, ctx, "get_ledger_balances") - balancesArray, ok := res.Params[0].([]Balance) + balancesResp, ok := res.Params.(GetLedgerBalancesResponse) + balancesArray := balancesResp.LedgerBalances require.True(t, ok) require.Len(t, balancesArray, 1) require.Equal(t, "usdc", balancesArray[0].Asset) @@ -219,7 +222,7 @@ func TestRPCRouterHandleGetUserTag(t *testing.T) { router.HandleGetUserTag(ctx) assertResponse(t, ctx, "get_user_tag") - getTagResponse, ok := ctx.Message.Res.Params[0].(GetUserTagResponse) + getTagResponse, ok := ctx.Message.Res.Params.(GetUserTagResponse) require.True(t, ok, "Response should be a GetUserTagResponse") require.Equal(t, userTag.Tag, getTagResponse.Tag) }) @@ -236,6 +239,7 @@ func TestRPCRouterHandleGetUserTag(t *testing.T) { assertErrorResponse(t, ctx, "failed to get user tag") }) } + func TestRPCRouterHandleTransfer(t *testing.T) { senderKey, _ := crypto.GenerateKey() senderSigner := Signer{privateKey: senderKey} @@ -269,18 +273,21 @@ func TestRPCRouterHandleTransfer(t *testing.T) { router.HandleTransfer(ctx) res := assertResponse(t, ctx, "transfer") - transferResp, ok := res.Params[0].([]TransactionResponse) + transferResp, ok := res.Params.(TransferResponse) + require.Len(t, transferResp.Transactions, 2, "Response should contain 2 transaction objects") + + transferTransaction := transferResp.Transactions[0] require.True(t, ok, "Response should be a slice of TransactionResponse") - require.Equal(t, senderAddr.Hex(), transferResp[0].FromAccount) - require.Equal(t, recipientAddr.Hex(), transferResp[0].ToAccount) - require.False(t, transferResp[0].CreatedAt.IsZero(), "CreatedAt should be set") + require.Equal(t, senderAddr.Hex(), transferTransaction.FromAccount) + require.Equal(t, recipientAddr.Hex(), transferTransaction.ToAccount) + require.False(t, transferTransaction.CreatedAt.IsZero(), "CreatedAt should be set") // Verify user tags are empty (since no tags were created for these wallets) - require.Empty(t, transferResp[0].FromAccountTag, "FromAccountTag should be empty when no tag exists") - require.Empty(t, transferResp[0].ToAccountTag, "ToAccountTag should be empty when no tag exists") + require.Empty(t, transferTransaction.FromAccountTag, "FromAccountTag should be empty when no tag exists") + require.Empty(t, transferTransaction.ToAccountTag, "ToAccountTag should be empty when no tag exists") // Verify that all transactions in response have the tag fields - for _, tx := range transferResp { + for _, tx := range transferResp.Transactions { require.Equal(t, senderAddr.Hex(), tx.FromAccount) require.Equal(t, recipientAddr.Hex(), tx.ToAccount) require.Empty(t, tx.FromAccountTag, "FromAccountTag should be empty when no tag exists") @@ -331,8 +338,7 @@ func TestRPCRouterHandleTransfer(t *testing.T) { } // Verify response transactions match database transactions - require.Len(t, transferResp, 2, "Response should contain 2 transaction objects") - for _, responseTx := range transferResp { + for _, responseTx := range transferResp.Transactions { // Find matching transaction in database var dbTx LedgerTransaction err = db.Where("id = ?", responseTx.Id).First(&dbTx).Error @@ -374,12 +380,12 @@ func TestRPCRouterHandleTransfer(t *testing.T) { router.HandleTransfer(ctx) res := assertResponse(t, ctx, "transfer") - transactionResponse, ok := res.Params[0].([]TransactionResponse) + transactionResponse, ok := res.Params.(TransferResponse) require.True(t, ok, "Response should be a TransactionResponse") + require.Len(t, transactionResponse.Transactions, 2, "Should have 2 transaction entries for the transfer") - targetTransaction := transactionResponse[0] + targetTransaction := transactionResponse.Transactions[0] - require.Len(t, transactionResponse, 2, "Should have 2 transaction entries for the transfer") require.Equal(t, senderAddr.Hex(), targetTransaction.FromAccount) require.Equal(t, recipientAddr.Hex(), targetTransaction.ToAccount) require.False(t, targetTransaction.CreatedAt.IsZero(), "CreatedAt should be set") @@ -389,7 +395,7 @@ func TestRPCRouterHandleTransfer(t *testing.T) { require.Equal(t, recipientTag.Tag, targetTransaction.ToAccountTag, "ToAccountTag should match recipient's tag") // Verify all transactions have correct tag information - for _, tx := range transactionResponse { + for _, tx := range transactionResponse.Transactions { require.Equal(t, senderAddr.Hex(), tx.FromAccount) require.Equal(t, recipientAddr.Hex(), tx.ToAccount) require.Empty(t, tx.FromAccountTag, "FromAccountTag should be empty since sender has no tag") @@ -647,7 +653,7 @@ func TestRPCRouterHandleCreateAppSession(t *testing.T) { router.HandleCreateApplication(ctx) res := assertResponse(t, ctx, "create_app_session") - appResp, ok := res.Params[0].(AppSessionResponse) + appResp, ok := res.Params.(AppSessionResponse) require.True(t, ok) require.Equal(t, string(ChannelStatusOpen), appResp.Status) require.Equal(t, uint64(1), appResp.Version) @@ -784,7 +790,7 @@ func TestRPCRouterHandleSubmitAppState(t *testing.T) { router.HandleSubmitAppState(ctx) res := assertResponse(t, ctx, "submit_app_state") - appResp, ok := res.Params[0].(AppSessionResponse) + appResp, ok := res.Params.(AppSessionResponse) require.True(t, ok) require.Equal(t, string(ChannelStatusOpen), appResp.Status) require.Equal(t, uint64(2), appResp.Version) @@ -871,7 +877,7 @@ func TestRPCRouterHandleCloseAppSession(t *testing.T) { router.HandleCloseApplication(ctx) res := assertResponse(t, ctx, "close_app_session") - appResp, ok := res.Params[0].(AppSessionResponse) + appResp, ok := res.Params.(AppSessionResponse) require.True(t, ok) require.Equal(t, string(ChannelStatusClosed), appResp.Status) require.Equal(t, uint64(3), appResp.Version) @@ -950,8 +956,8 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) - require.True(t, ok, "Response should be ResizeChannelResponse") + resObj, ok := res.Params.(ChannelOperationResponse) + require.True(t, ok, "Response should be ChannelOperationResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1010,7 +1016,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Channel amount should decrease @@ -1248,7 +1254,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Should be initial amount (1000) + allocate amount (0) + resize amount (100) = 1100 @@ -1295,7 +1301,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Should be initial amount (1000) + allocate amount (0) - resize amount (100) = 900 @@ -1414,7 +1420,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok) // Verify the large allocation was processed correctly @@ -1469,7 +1475,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1539,7 +1545,7 @@ func TestRPCRouterHandleResizeChannel(t *testing.T) { router.HandleResizeChannel(ctx) res := assertResponse(t, ctx, "resize_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be ResizeChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1609,7 +1615,7 @@ func TestRPCRouterHandleCloseChannel(t *testing.T) { router.HandleCloseChannel(ctx) res := assertResponse(t, ctx, "close_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be CloseChannelResponse") require.Equal(t, ch.ChannelID, resObj.ChannelID) require.Equal(t, ch.Version+1, resObj.State.Version) @@ -1702,7 +1708,7 @@ func TestRPCRouterHandleCreateChannel(t *testing.T) { router.HandleCreateChannel(ctx) res := assertResponse(t, ctx, "create_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be CreateChannelResponse") // Verify response structure @@ -1855,7 +1861,7 @@ func TestRPCRouterHandleCreateChannel(t *testing.T) { // This should work as zero amount channels are allowed res := assertResponse(t, ctx, "create_channel") - resObj, ok := res.Params[0].(ChannelOperationResponse) + resObj, ok := res.Params.(ChannelOperationResponse) require.True(t, ok, "Response should be CreateChannelResponse") require.True(t, resObj.State.Allocations[0].RawAmount.IsZero(), "User allocation should be zero") }) diff --git a/clearnode/rpc_router_public.go b/clearnode/rpc_router_public.go index f78a32d07..166ef22df 100644 --- a/clearnode/rpc_router_public.go +++ b/clearnode/rpc_router_public.go @@ -11,13 +11,17 @@ type GetAssetsParams struct { ChainID *uint32 `json:"chain_id,omitempty"` // Optional chain ID to filter assets } -type GetAssetsResponse struct { +type AssetResponse struct { Token string `json:"token"` ChainID uint32 `json:"chain_id"` Symbol string `json:"symbol"` Decimals uint8 `json:"decimals"` } +type GetAssetsResponse struct { + Assets []AssetResponse `json:"assets"` +} + type GetChannelsParams struct { ListOptions Participant string `json:"participant,omitempty"` // Optional participant wallet to filter channels @@ -90,8 +94,28 @@ type TransactionResponse struct { CreatedAt time.Time `json:"created_at"` } +type GetChannelsResponse struct { + Channels []ChannelResponse `json:"channels"` +} + +type GetAppSessionsResponse struct { + AppSessions []AppSessionResponse `json:"app_sessions"` +} + +type GetLedgerEntriesResponse struct { + LedgerEntries []LedgerEntryResponse `json:"ledger_entries"` +} + +type GetLedgerTransactionsResponse struct { + LedgerTransactions []TransactionResponse `json:"ledger_transactions"` +} + +type ErrorResponse struct { + Error string `json:"error"` // The error message to send back to the client +} + func (r *RPCRouter) HandlePing(c *RPCContext) { - c.Succeed("pong") + c.Succeed("pong", nil) } // HandleGetConfig returns the broker configuration @@ -134,9 +158,13 @@ func (r *RPCRouter) HandleGetAssets(c *RPCContext) { return } - resp := make([]GetAssetsResponse, 0, len(assets)) + respAssets := make([]AssetResponse, 0, len(assets)) for _, asset := range assets { - resp = append(resp, GetAssetsResponse(asset)) + respAssets = append(respAssets, AssetResponse(asset)) + } + + resp := GetAssetsResponse{ + Assets: respAssets, } c.Succeed(req.Method, resp) @@ -166,9 +194,9 @@ func (r *RPCRouter) HandleGetChannels(c *RPCContext) { return } - response := make([]ChannelResponse, 0, len(channels)) + respChannels := make([]ChannelResponse, 0, len(channels)) for _, channel := range channels { - response = append(response, ChannelResponse{ + respChannels = append(respChannels, ChannelResponse{ ChannelID: channel.ChannelID, Participant: channel.Participant, Status: channel.Status, @@ -185,7 +213,11 @@ func (r *RPCRouter) HandleGetChannels(c *RPCContext) { }) } - c.Succeed(req.Method, response) + resp := GetChannelsResponse{ + Channels: respChannels, + } + + c.Succeed(req.Method, resp) logger.Info("channels retrieved", "participant", params.Participant, "status", params.Status) } @@ -243,9 +275,9 @@ func (r *RPCRouter) HandleGetAppSessions(c *RPCContext) { } // TODO: update response format accordingly to create struct - resp := make([]AppSessionResponse, len(sessions)) + respAppSessions := make([]AppSessionResponse, len(sessions)) for i, session := range sessions { - resp[i] = AppSessionResponse{ + respAppSessions[i] = AppSessionResponse{ AppSessionID: session.SessionID, Status: string(session.Status), ParticipantWallets: session.ParticipantWallets, @@ -261,6 +293,10 @@ func (r *RPCRouter) HandleGetAppSessions(c *RPCContext) { } } + resp := GetAppSessionsResponse{ + AppSessions: respAppSessions, + } + c.Succeed(req.Method, resp) logger.Info("application sessions retrieved", "participant", params.Participant, "status", params.Status) } @@ -293,9 +329,9 @@ func (r *RPCRouter) HandleGetLedgerEntries(c *RPCContext) { return } - resp := make([]LedgerEntryResponse, len(entries)) + respLedgerEntries := make([]LedgerEntryResponse, len(entries)) for i, entry := range entries { - resp[i] = LedgerEntryResponse{ + respLedgerEntries[i] = LedgerEntryResponse{ ID: entry.ID, AccountID: entry.AccountID, AccountType: entry.AccountType, @@ -307,6 +343,10 @@ func (r *RPCRouter) HandleGetLedgerEntries(c *RPCContext) { } } + resp := GetLedgerEntriesResponse{ + LedgerEntries: respLedgerEntries, + } + c.Succeed(req.Method, resp) logger.Info("ledger entries retrieved", "accountID", userAccountID, "asset", params.Asset, "wallet", userAddress) } @@ -341,13 +381,17 @@ func (r *RPCRouter) HandleGetLedgerTransactions(c *RPCContext) { return } - resp, err := FormatTransactions(r.DB, transactions) + respLedgerTransactions, err := FormatTransactions(r.DB, transactions) if err != nil { logger.Error("failed to format transactions", "error", err) c.Fail(err, "failed to return transactions") return } + resp := GetLedgerTransactionsResponse{ + LedgerTransactions: respLedgerTransactions, + } + c.Succeed(req.Method, resp) logger.Info("transactions retrieved", "count", len(transactions), "accountID", params.AccountID, "asset", params.Asset, "txType", params.TxType) } diff --git a/clearnode/rpc_router_public_test.go b/clearnode/rpc_router_public_test.go index fa962d36c..0b3860694 100644 --- a/clearnode/rpc_router_public_test.go +++ b/clearnode/rpc_router_public_test.go @@ -12,11 +12,9 @@ import ( "github.com/stretchr/testify/require" ) -func createRPCContext(id int, method string, params any) *RPCContext { - var rpcParams []any - if params != nil { - paramsJSON, _ := json.Marshal(params) - rpcParams = []any{json.RawMessage(paramsJSON)} +func createRPCContext(id int, method string, params RPCDataParams) *RPCContext { + if params == nil { + params = struct{}{} } return &RPCContext{ @@ -25,7 +23,7 @@ func createRPCContext(id int, method string, params any) *RPCContext { Req: &RPCData{ RequestID: uint64(id), Method: method, - Params: rpcParams, + Params: params, Timestamp: uint64(time.Now().Unix()), }, Sig: []Signature{Signature([]byte("dummy-signature"))}, @@ -63,7 +61,7 @@ func TestRPCRouterHandleGetConfig(t *testing.T) { router.HandleGetConfig(ctx) res := assertResponse(t, ctx, "get_config") - configMap, ok := res.Params[0].(BrokerConfig) + configMap, ok := res.Params.(BrokerConfig) require.True(t, ok, "Response should contain a BrokerConfig") assert.Equal(t, router.Signer.GetAddress().Hex(), configMap.BrokerAddress) require.Len(t, configMap.Networks, 3, "Should have 3 supported networks") @@ -116,12 +114,11 @@ func TestRPCRouterHandleGetAssets(t *testing.T) { router.HandleGetAssets(ctx) res := assertResponse(t, ctx, "get_assets") - require.Len(t, res.Params, 1, "Response should contain an array of AssetResponse") - responseAssets, ok := res.Params[0].([]GetAssetsResponse) - require.True(t, ok, "Response parameter should be a slice of AssetResponse") - assert.Len(t, responseAssets, len(tc.expectedTokenNames), "Should return expected number of assets") + responseAssets, ok := res.Params.(GetAssetsResponse) + require.True(t, ok, "Response parameter should be a GetAssetsResponse") + assert.Len(t, responseAssets.Assets, len(tc.expectedTokenNames), "Should return expected number of assets") - for idx, asset := range responseAssets { + for idx, asset := range responseAssets.Assets { assert.True(t, asset.Token == tc.expectedTokenNames[idx], "Should include token %s", tc.expectedTokenNames[idx]) } }) @@ -236,12 +233,11 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { router.HandleGetChannels(ctx) res := assertResponse(t, ctx, "get_channels") - require.Len(t, res.Params, 1, "Response should contain a slice of ChannelResponse") - responseChannels, ok := res.Params[0].([]ChannelResponse) - require.True(t, ok, "Response parameter should be a slice of ChannelResponse") - assert.Len(t, responseChannels, len(tc.expectedChannelIDs), "Should return expected number of channels") + responseChannels, ok := res.Params.(GetChannelsResponse) + require.True(t, ok, "Response parameter should be a GetChannelsResponse") + assert.Len(t, responseChannels.Channels, len(tc.expectedChannelIDs), "Should return expected number of channels") - for idx, channel := range responseChannels { + for idx, channel := range responseChannels.Channels { assert.True(t, channel.ChannelID == tc.expectedChannelIDs[idx], "%d-th result (%s) should equal %s", idx, channel.ChannelID, tc.expectedChannelIDs[idx]) } }) @@ -319,13 +315,12 @@ func TestRPCRouterHandleGetChannels(t *testing.T) { router.HandleGetChannels(ctx) res := assertResponse(t, ctx, "get_channels") - require.Len(t, res.Params, 1, "Response should contain an array of ChannelResponse") - responseChannels, ok := res.Params[0].([]ChannelResponse) - require.True(t, ok, "Response parameter should be a slice of ChannelResponse") - assert.Len(t, responseChannels, len(tc.expectedChannelIDs), "Should return expected number of channels") + responseChannels, ok := res.Params.(GetChannelsResponse) + require.True(t, ok, "Response parameter should be a GetChannelsResponse") + assert.Len(t, responseChannels.Channels, len(tc.expectedChannelIDs), "Should return expected number of channels") // Check channel IDs are included in expected order - for idx, channel := range responseChannels { + for idx, channel := range responseChannels.Channels { assert.Equal(t, tc.expectedChannelIDs[idx], channel.ChannelID, "Should include channel %s at position %d", tc.expectedChannelIDs[idx], idx) } }) @@ -356,7 +351,7 @@ func TestRPCRouterHandleGetAppDefinition(t *testing.T) { router.HandleGetAppDefinition(ctx) res := assertResponse(t, ctx, "get_app_definition") - def, ok := res.Params[0].(AppDefinition) + def, ok := res.Params.(AppDefinition) require.True(t, ok) assert.Equal(t, session.Protocol, def.Protocol) assert.EqualValues(t, session.ParticipantWallets, def.ParticipantWallets) @@ -503,13 +498,12 @@ func TestRPCRouterHandleGetAppSessions(t *testing.T) { res := assertResponse(t, ctx, "get_app_sessions") assert.Equal(t, uint64(idx), res.RequestID) - require.Len(t, res.Params, 1, "Response should contain an array of AppSessionResponse") - sessionResponses, ok := res.Params[0].([]AppSessionResponse) - require.True(t, ok, "Response parameter should be a slice of AppSessionResponse") - assert.Len(t, sessionResponses, len(tc.expectedSessionIDs), "Should return expected number of app sessions") + sessionResponses, ok := res.Params.(GetAppSessionsResponse) + require.True(t, ok, "Response parameter should be a GetAppSessionsResponse") + assert.Len(t, sessionResponses.AppSessions, len(tc.expectedSessionIDs), "Should return expected number of app sessions") - for idx, sessionResponse := range sessionResponses { + for idx, sessionResponse := range sessionResponses.AppSessions { assert.True(t, sessionResponse.AppSessionID == tc.expectedSessionIDs[idx], "Should include session %s", tc.expectedSessionIDs[idx]) } }) @@ -591,13 +585,12 @@ func TestRPCRouterHandleGetAppSessions(t *testing.T) { router.HandleGetAppSessions(ctx) res := assertResponse(t, ctx, "get_app_sessions") - require.Len(t, res.Params, 1, "Response should contain an array of AppSessionResponse") - responseSessions, ok := res.Params[0].([]AppSessionResponse) - require.True(t, ok, "Response parameter should be a slice of AppSessionResponse") - assert.Len(t, responseSessions, len(tc.expectedSessionIDs), "Should return expected number of sessions") + responseSessions, ok := res.Params.(GetAppSessionsResponse) + require.True(t, ok, "Response parameter should be a GetAppSessionsResponse") + assert.Len(t, responseSessions.AppSessions, len(tc.expectedSessionIDs), "Should return expected number of sessions") // Check session IDs are in expected order - for idx, session := range responseSessions { + for idx, session := range responseSessions.AppSessions { assert.True(t, session.AppSessionID == tc.expectedSessionIDs[idx], "Retrieved %d-th session ID should be equal %s", idx, tc.expectedSessionIDs[idx]) } }) @@ -741,13 +734,12 @@ func TestRPCRouterHandleGetLedgerEntries(t *testing.T) { res := assertResponse(t, ctx, "get_ledger_entries") assert.Equal(t, uint64(idx+1), res.RequestID) - require.Len(t, res.Params, 1, "Response should contain an array of Entry objects") - entries, ok := res.Params[0].([]LedgerEntryResponse) - require.True(t, ok, "Response parameter should be a slice of Entry") - assert.Len(t, entries, tc.expectedCount, "Should return expected number of entries") + entries, ok := res.Params.(GetLedgerEntriesResponse) + require.True(t, ok, "Response parameter should be a GetLedgerEntriesResponse") + assert.Len(t, entries.LedgerEntries, tc.expectedCount, "Should return expected number of entries") - tc.validateFunc(t, entries) + tc.validateFunc(t, entries.LedgerEntries) }) } }) @@ -825,13 +817,12 @@ func TestRPCRouterHandleGetLedgerEntries(t *testing.T) { router.HandleGetLedgerEntries(c) res := assertResponse(t, c, "get_ledger_entries") - require.Len(t, res.Params, 1, "Response should contain an array of LedgerEntryResponse") - responseEntries, ok := res.Params[0].([]LedgerEntryResponse) - require.True(t, ok, "Response parameter should be a slice of LedgerEntryResponse") - assert.Len(t, responseEntries, len(tc.expectedToken), "Should return expected number of entries") + responseEntries, ok := res.Params.(GetLedgerEntriesResponse) + require.True(t, ok, "Response parameter should be a GetLedgerEntriesResponse") + assert.Len(t, responseEntries.LedgerEntries, len(tc.expectedToken), "Should return expected number of entries") // Check token names are included in expected order - for idx, entry := range responseEntries { + for idx, entry := range responseEntries.LedgerEntries { assert.Equal(t, tc.expectedToken[idx], entry.Asset, "Should include token %s at position %d", tc.expectedToken[idx], idx) } }) @@ -992,16 +983,17 @@ func TestRPCRouterHandleGetTransactions(t *testing.T) { router.HandleGetLedgerTransactions(c) res := assertResponse(t, c, "get_ledger_transactions") - require.Len(t, res.Params, 1, "Response should contain one parameter") // Unmarshal the actual transaction data - var transactions []TransactionResponse + var resp GetLedgerTransactionsResponse + require.NotNil(t, res.Params, "Response parameter should not be nil") // We need to marshal the interface{} back to JSON, then unmarshal into our concrete type. - respBytes, err := json.Marshal(res.Params[0]) + respBytes, err := json.Marshal(res.Params) require.NoError(t, err) - err = json.Unmarshal(respBytes, &transactions) - require.NoError(t, err, "Response parameter should be a slice of TransactionResponse") + err = json.Unmarshal(respBytes, &resp) + require.NoError(t, err, "Response parameter should be a GetLedgerTransactionsResponse") + transactions := resp.LedgerTransactions // Assert the expected number of transactions were returned assert.Len(t, transactions, tc.expectedLen) @@ -1114,14 +1106,14 @@ func TestRPCRouterHandleGetTransactions(t *testing.T) { router.HandleGetLedgerTransactions(ctx) res := assertResponse(t, ctx, "get_ledger_transactions") - require.Len(t, res.Params, 1, "Response should contain an array of TransactionResponse") - var transactions []TransactionResponse - respBytes, err := json.Marshal(res.Params[0]) + var resp GetLedgerTransactionsResponse + respBytes, err := json.Marshal(res.Params) require.NoError(t, err) - err = json.Unmarshal(respBytes, &transactions) + err = json.Unmarshal(respBytes, &resp) require.NoError(t, err) + transactions := resp.LedgerTransactions assert.Len(t, transactions, tc.expectedCount, "Should return expected number of transactions") // For non-filter tests, verify order diff --git a/integration/common/auth.ts b/integration/common/auth.ts index 8b71a5900..fb3e1bbca 100644 --- a/integration/common/auth.ts +++ b/integration/common/auth.ts @@ -3,7 +3,7 @@ import { createAuthRequestMessage, createAuthVerifyMessage, AuthRequestParams, - rpcResponseParser, + parseAuthChallengeResponse, } from '@erc7824/nitrolite'; import { Identity } from './identity'; import { getAuthChallengePredicate, getAuthVerifyPredicate, TestWebSocket } from './ws'; @@ -14,8 +14,8 @@ export const createAuthSessionWithClearnode = async ( authRequestParams?: AuthRequestParams ) => { authRequestParams = authRequestParams || { - wallet: identity.walletAddress, - participant: identity.sessionAddress, + address: identity.walletAddress, + session_key: identity.sessionAddress, app_name: 'Test Domain', expire: String(Math.floor(Date.now() / 1000) + 3600), // 1 hour expiration scope: 'console', @@ -28,7 +28,7 @@ export const createAuthSessionWithClearnode = async ( { scope: authRequestParams.scope, application: authRequestParams.application, - participant: authRequestParams.participant, + participant: authRequestParams.session_key, expire: authRequestParams.expire, allowances: authRequestParams.allowances, }, @@ -40,7 +40,7 @@ export const createAuthSessionWithClearnode = async ( const authRequestMsg = await createAuthRequestMessage(authRequestParams); const authRequestResponse = await ws.sendAndWaitForResponse(authRequestMsg, getAuthChallengePredicate(), 1000); - const authRequestParsedResponse = rpcResponseParser.authChallenge(authRequestResponse); + const authRequestParsedResponse = parseAuthChallengeResponse(authRequestResponse); const authVerifyMsg = await createAuthVerifyMessage(eip712MessageSigner, authRequestParsedResponse); await ws.sendAndWaitForResponse(authVerifyMsg, getAuthVerifyPredicate(), 1000); diff --git a/integration/common/nitroliteClient.ts b/integration/common/nitroliteClient.ts index d7e316377..4295b11f4 100644 --- a/integration/common/nitroliteClient.ts +++ b/integration/common/nitroliteClient.ts @@ -5,8 +5,10 @@ import { createCloseChannelMessage, createCreateChannelMessage, NitroliteClient, + parseChannelUpdateResponse, + parseCloseChannelResponse, + parseCreateChannelResponse, RPCChannelStatus, - rpcResponseParser, } from '@erc7824/nitrolite'; import { Identity } from './identity'; import { Address, createPublicClient, Hex, http } from 'viem'; @@ -54,7 +56,7 @@ export class TestNitroliteClient extends NitroliteClient { const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); expect(createResponse).toBeDefined(); - const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), @@ -74,7 +76,7 @@ export class TestNitroliteClient extends NitroliteClient { const openResponse = await openChannelPromise; - const openParsedResponse = rpcResponseParser.channelUpdate(openResponse); + const openParsedResponse = parseChannelUpdateResponse(openResponse); const responseChannel = openParsedResponse.params; return { params: responseChannel, initialState }; @@ -88,7 +90,7 @@ export class TestNitroliteClient extends NitroliteClient { ); const closeResponse = await ws.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); - const closeParsedResponse = rpcResponseParser.closeChannel(closeResponse); + const closeParsedResponse = parseCloseChannelResponse(closeResponse); const closeChannelUpdateChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Closed), @@ -120,7 +122,7 @@ export class TestNitroliteClient extends NitroliteClient { }); const closeChannelUpdateResponse = await closeChannelUpdateChannelPromise; - const closeChannelUpdateParsedResponse = rpcResponseParser.channelUpdate(closeChannelUpdateResponse); + const closeChannelUpdateParsedResponse = parseChannelUpdateResponse(closeChannelUpdateResponse); const responseChannel = closeChannelUpdateParsedResponse.params; return { params: responseChannel }; diff --git a/integration/tests/clearnode_auth.test.ts b/integration/tests/clearnode_auth.test.ts index 8fbc338c9..df0baf8e0 100644 --- a/integration/tests/clearnode_auth.test.ts +++ b/integration/tests/clearnode_auth.test.ts @@ -9,7 +9,8 @@ import { createAuthVerifyMessage, createAuthVerifyMessageWithJWT, createEIP712AuthMessageSigner, - rpcResponseParser, + parseAuthChallengeResponse, + parseAuthVerifyResponse, } from '@erc7824/nitrolite'; describe('Clearnode Authentication', () => { @@ -25,8 +26,8 @@ describe('Clearnode Authentication', () => { const identity = new Identity(CONFIG.IDENTITIES[0].WALLET_PK, CONFIG.IDENTITIES[0].SESSION_PK); const authRequestParams: AuthRequestParams = { - wallet: identity.walletAddress, - participant: identity.sessionAddress, + address: identity.walletAddress, + session_key: identity.sessionAddress, app_name: 'Test Domain', expire: String(Math.floor(Date.now() / 1000) + 3600), // 1 hour expiration scope: 'console', @@ -39,7 +40,7 @@ describe('Clearnode Authentication', () => { { scope: authRequestParams.scope, application: authRequestParams.application, - participant: authRequestParams.participant, + participant: authRequestParams.session_key, expire: authRequestParams.expire, allowances: authRequestParams.allowances, }, @@ -59,7 +60,7 @@ describe('Clearnode Authentication', () => { const response = await ws.sendAndWaitForResponse(msg, getAuthChallengePredicate(), 1000); expect(response).toBeDefined(); - parsedChallengeResponse = rpcResponseParser.authChallenge(response); + parsedChallengeResponse = parseAuthChallengeResponse(response); expect(parsedChallengeResponse.params.challengeMessage).toBeDefined(); }); @@ -81,11 +82,11 @@ describe('Clearnode Authentication', () => { const response = await ws.sendAndWaitForResponse(msg, getAuthVerifyPredicate(), 1000); expect(response).toBeDefined(); - const parsedAuthVerifyResponse = rpcResponseParser.authVerify(response); + const parsedAuthVerifyResponse = parseAuthVerifyResponse(response); expect(parsedAuthVerifyResponse.params.success).toBe(true); - expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.participant); - expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.wallet); + expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.session_key); + expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.address); expect(parsedAuthVerifyResponse.params.jwtToken).toBeDefined(); jwtToken = parsedAuthVerifyResponse.params.jwtToken; @@ -101,11 +102,11 @@ describe('Clearnode Authentication', () => { const response = await ws.sendAndWaitForResponse(msg, getAuthVerifyPredicate(), 1000); expect(response).toBeDefined(); - const parsedAuthVerifyResponse = rpcResponseParser.authVerify(response); + const parsedAuthVerifyResponse = parseAuthVerifyResponse(response); expect(parsedAuthVerifyResponse.params.success).toBe(true); - expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.participant); - expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.wallet); + expect(parsedAuthVerifyResponse.params.sessionKey).toBe(authRequestParams.session_key); + expect(parsedAuthVerifyResponse.params.address).toBe(authRequestParams.address); expect(parsedAuthVerifyResponse.params.jwtToken).toBeUndefined(); }); }); diff --git a/integration/tests/close_channel.test.ts b/integration/tests/close_channel.test.ts index 40e8934b8..d168f57c6 100644 --- a/integration/tests/close_channel.test.ts +++ b/integration/tests/close_channel.test.ts @@ -5,7 +5,7 @@ import { Identity } from '@/identity'; import { TestNitroliteClient } from '@/nitroliteClient'; import { CONFIG } from '@/setup'; import { getCloseChannelPredicate, TestWebSocket } from '@/ws'; -import { createCloseChannelMessage, rpcResponseParser } from '@erc7824/nitrolite'; +import { createCloseChannelMessage, parseCloseChannelResponse } from '@erc7824/nitrolite'; import { Hex, parseUnits } from 'viem'; describe('Close channel', () => { @@ -71,7 +71,7 @@ describe('Close channel', () => { const closeResponse = await ws.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); expect(closeResponse).toBeDefined(); - const closeParsedResponse = rpcResponseParser.closeChannel(closeResponse); + const closeParsedResponse = parseCloseChannelResponse(closeResponse); const closeChannelTxHash = await client.closeChannel({ finalState: { diff --git a/integration/tests/create_channel.test.ts b/integration/tests/create_channel.test.ts index 01365fb40..1294e8dc4 100644 --- a/integration/tests/create_channel.test.ts +++ b/integration/tests/create_channel.test.ts @@ -9,8 +9,9 @@ import { convertRPCToClientChannel, convertRPCToClientState, createCreateChannelMessage, + parseChannelUpdateResponse, + parseCreateChannelResponse, RPCChannelStatus, - rpcResponseParser, } from '@erc7824/nitrolite'; import { parseUnits } from 'viem'; @@ -71,7 +72,7 @@ describe('Create channel', () => { const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); expect(createResponse).toBeDefined(); - const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), @@ -102,7 +103,7 @@ describe('Create channel', () => { const openResponse = await openChannelPromise; expect(openResponse).toBeDefined(); - const openParsedResponse = rpcResponseParser.channelUpdate(openResponse); + const openParsedResponse = parseChannelUpdateResponse(openResponse); const responseChannel = openParsedResponse.params; expect(responseChannel.adjudicator).toBe(CONFIG.ADDRESSES.DUMMY_ADJUDICATOR_ADDRESS); @@ -155,7 +156,7 @@ describe('Create channel', () => { const createResponse = await ws.sendAndWaitForResponse(msg, getCreateChannelPredicate(), 5000); expect(createResponse).toBeDefined(); - const { params: createParsedResponseParams } = rpcResponseParser.createChannel(createResponse); + const { params: createParsedResponseParams } = parseCreateChannelResponse(createResponse); const openChannelPromise = ws.waitForMessage( getChannelUpdatePredicateWithStatus(RPCChannelStatus.Open), @@ -186,7 +187,7 @@ describe('Create channel', () => { const openResponse = await openChannelPromise; expect(openResponse).toBeDefined(); - const openParsedResponse = rpcResponseParser.channelUpdate(openResponse); + const openParsedResponse = parseChannelUpdateResponse(openResponse); const responseChannel = openParsedResponse.params; expect(responseChannel.adjudicator).toBe(CONFIG.ADDRESSES.DUMMY_ADJUDICATOR_ADDRESS); diff --git a/integration/tests/get_user_tag.test.ts b/integration/tests/get_user_tag.test.ts index 661afeb90..716915663 100644 --- a/integration/tests/get_user_tag.test.ts +++ b/integration/tests/get_user_tag.test.ts @@ -3,7 +3,7 @@ import { DatabaseUtils } from '@/databaseUtils'; import { Identity } from '@/identity'; import { CONFIG } from '@/setup'; import { getGetUserTagPredicate, TestWebSocket } from "@/ws"; -import { createGetUserTagMessage, rpcResponseParser } from "@erc7824/nitrolite"; +import { createGetUserTagMessage, parseGetUserTagResponse } from "@erc7824/nitrolite"; describe('Get User Tag Integration', () => { let ws: TestWebSocket; @@ -43,7 +43,7 @@ describe('Get User Tag Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getUserTag(response); + const parsedResponse = parseGetUserTagResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); expect(parsedResponse.params.tag).toBeDefined(); @@ -55,12 +55,12 @@ describe('Get User Tag Integration', () => { // First request const msg1 = await createGetUserTagMessage(identity.messageSigner); const response1 = await ws.sendAndWaitForResponse(msg1, getGetUserTagPredicate(), 5000); - const parsedResponse1 = rpcResponseParser.getUserTag(response1); + const parsedResponse1 = parseGetUserTagResponse(response1); // Second request const msg2 = await createGetUserTagMessage(identity.messageSigner); const response2 = await ws.sendAndWaitForResponse(msg2, getGetUserTagPredicate(), 5000); - const parsedResponse2 = rpcResponseParser.getUserTag(response2); + const parsedResponse2 = parseGetUserTagResponse(response2); expect(parsedResponse1.params.tag).toBe(parsedResponse2.params.tag); }); @@ -68,7 +68,7 @@ describe('Get User Tag Integration', () => { it('should return valid tag format', async () => { const msg = await createGetUserTagMessage(identity.messageSigner); const response = await ws.sendAndWaitForResponse(msg, getGetUserTagPredicate(), 5000); - const parsedResponse = rpcResponseParser.getUserTag(response); + const parsedResponse = parseGetUserTagResponse(response); // Verify the tag format matches expected pattern (e.g., 'UX123D8C') expect(parsedResponse.params.tag).toMatch(/^[A-Z0-9]+$/); diff --git a/integration/tests/ledger_transactions.test.ts b/integration/tests/ledger_transactions.test.ts index 325049ad8..f2d0d5c08 100644 --- a/integration/tests/ledger_transactions.test.ts +++ b/integration/tests/ledger_transactions.test.ts @@ -3,7 +3,12 @@ import { DatabaseUtils } from '@/databaseUtils'; import { Identity } from '@/identity'; import { CONFIG } from '@/setup'; import { getGetLedgerTransactionsPredicate, TestWebSocket } from '@/ws'; -import { createGetLedgerTransactionsMessage, rpcResponseParser, GetLedgerTransactionsFilters, TxType } from '@erc7824/nitrolite'; +import { + createGetLedgerTransactionsMessage, + GetLedgerTransactionsFilters, + parseGetLedgerTransactionsResponse, + RPCTxType, +} from '@erc7824/nitrolite'; describe('Ledger Transactions Integration', () => { let ws: TestWebSocket; @@ -44,10 +49,9 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); }); it('should successfully request ledger transactions with asset filter', async () => { @@ -62,14 +66,16 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // If there are transactions, they should all be for usdc - if (parsedResponse.params.length > 0) { - parsedResponse.params.forEach((transaction) => { + if (ledgerTransactions.length > 0) { + ledgerTransactions.forEach((transaction) => { expect(transaction.asset).toBe('usdc'); }); } @@ -78,7 +84,7 @@ describe('Ledger Transactions Integration', () => { it('should successfully request ledger transactions with tx_type filter', async () => { const accountId = identity.walletAddress; const filters: GetLedgerTransactionsFilters = { - tx_type: TxType.Deposit, + tx_type: RPCTxType.Deposit, }; const msg = await createGetLedgerTransactionsMessage(identity.messageSigner, accountId, filters); @@ -87,15 +93,17 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // If there are transactions, they should all be of type 'deposit' - if (parsedResponse.params.length > 0) { - parsedResponse.params.forEach((transaction) => { - expect(transaction.txType).toBe(TxType.Deposit); + if (ledgerTransactions.length > 0) { + ledgerTransactions.forEach((transaction) => { + expect(transaction.txType).toBe(RPCTxType.Deposit); }); } }); @@ -113,13 +121,15 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // Should not return more than the limit - expect(parsedResponse.params.length).toBeLessThanOrEqual(5); + expect(ledgerTransactions.length).toBeLessThanOrEqual(5); }); it('should successfully request ledger transactions with sort order', async () => { @@ -135,16 +145,18 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // If there are multiple transactions, they should be sorted by createdAt in descending order - if (parsedResponse.params.length > 1) { - for (let i = 0; i < parsedResponse.params.length - 1; i++) { - const currentDate = parsedResponse.params[i].createdAt; - const nextDate = parsedResponse.params[i + 1].createdAt; + if (ledgerTransactions.length > 1) { + for (let i = 0; i < ledgerTransactions.length - 1; i++) { + const currentDate = ledgerTransactions[i].createdAt; + const nextDate = ledgerTransactions[i + 1].createdAt; expect(currentDate.getTime()).toBeGreaterThanOrEqual(nextDate.getTime()); } } @@ -154,7 +166,7 @@ describe('Ledger Transactions Integration', () => { const accountId = identity.walletAddress; const filters: GetLedgerTransactionsFilters = { asset: 'usdc', - tx_type: TxType.Deposit, + tx_type: RPCTxType.Deposit, offset: 0, limit: 3, sort: 'desc', @@ -166,18 +178,20 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // Should not return more than the limit - expect(parsedResponse.params.length).toBeLessThanOrEqual(3); + expect(ledgerTransactions.length).toBeLessThanOrEqual(3); // All transactions should match the filters - parsedResponse.params.forEach((transaction) => { + ledgerTransactions.forEach((transaction) => { expect(transaction.asset).toBe('usdc'); - expect(transaction.txType).toBe(TxType.Deposit); + expect(transaction.txType).toBe(RPCTxType.Deposit); }); }); @@ -193,11 +207,13 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); - expect(parsedResponse.params.length).toBe(0); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); + expect(ledgerTransactions.length).toBe(0); }); it('should handle invalid account ID gracefully', async () => { @@ -209,12 +225,14 @@ describe('Ledger Transactions Integration', () => { expect(response).toBeDefined(); - const parsedResponse = rpcResponseParser.getLedgerTransactions(response); + const parsedResponse = parseGetLedgerTransactionsResponse(response); expect(parsedResponse).toBeDefined(); expect(parsedResponse.params).toBeDefined(); - expect(Array.isArray(parsedResponse.params)).toBe(true); + + const ledgerTransactions = parsedResponse.params.ledgerTransactions; + expect(Array.isArray(ledgerTransactions)).toBe(true); // Should return empty array for invalid/non-existent account - expect(parsedResponse.params.length).toBe(0); + expect(ledgerTransactions.length).toBe(0); }); }); }); diff --git a/integration/tests/lifecycle.test.ts b/integration/tests/lifecycle.test.ts index caabce687..f479f642d 100644 --- a/integration/tests/lifecycle.test.ts +++ b/integration/tests/lifecycle.test.ts @@ -15,8 +15,8 @@ import { getCloseChannelPredicate, } from '@/ws'; import { - AppDefinition, - AppSessionAllocation, + RPCAppDefinition, + RPCAppSessionAllocation, createAppSessionMessage, createCloseAppSessionMessage, createCloseChannelMessage, @@ -25,7 +25,13 @@ import { createResizeChannelMessage, createSubmitAppStateMessage, RPCChannelStatus, - rpcResponseParser, + parseCloseAppSessionResponse, + parseCloseChannelResponse, + parseCreateAppSessionResponse, + parseGetAppSessionsResponse, + parseGetLedgerBalancesResponse, + parseResizeChannelResponse, + parseSubmitAppStateResponse, } from '@erc7824/nitrolite'; import { Hex, parseUnits } from 'viem'; @@ -63,11 +69,11 @@ describe('Close channel', () => { 1000 ); - const getAppSessionsParsedResponse = rpcResponseParser.getAppSessions(getAppSessionsResponse); + const getAppSessionsParsedResponse = parseGetAppSessionsResponse(getAppSessionsResponse); expect(getAppSessionsParsedResponse).toBeDefined(); - expect(getAppSessionsParsedResponse.params).toHaveLength(1); + expect(getAppSessionsParsedResponse.params.appSessions).toHaveLength(1); - const appSession = getAppSessionsParsedResponse.params[0]; + const appSession = getAppSessionsParsedResponse.params.appSessions[0]; expect(appSession.appSessionId).toBe(appSessionId); expect(appSession.sessionData).toBeDefined(); @@ -103,17 +109,15 @@ describe('Close channel', () => { }; const submitAppStateUpdate = async ( - allocations: AppSessionAllocation[], + allocations: RPCAppSessionAllocation[], sessionData: object, expectedVersion: number ) => { - const submitAppStateMsg = await createSubmitAppStateMessage(appIdentity.messageSigner, [ - { - app_session_id: appSessionId as Hex, - allocations, - session_data: JSON.stringify(sessionData), - }, - ]); + const submitAppStateMsg = await createSubmitAppStateMessage(appIdentity.messageSigner, { + app_session_id: appSessionId as Hex, + allocations, + session_data: JSON.stringify(sessionData), + }); const submitAppStateResponse = await appWS.sendAndWaitForResponse( submitAppStateMsg, @@ -121,7 +125,7 @@ describe('Close channel', () => { 1000 ); - const submitAppStateParsedResponse = rpcResponseParser.submitAppState(submitAppStateResponse); + const submitAppStateParsedResponse = parseSubmitAppStateResponse(submitAppStateResponse); expect(submitAppStateParsedResponse).toBeDefined(); expect(submitAppStateParsedResponse.params.appSessionId).toBe(appSessionId); expect(submitAppStateParsedResponse.params.status).toBe(RPCChannelStatus.Open); @@ -131,17 +135,15 @@ describe('Close channel', () => { }; const closeAppSessionWithState = async ( - allocations: AppSessionAllocation[], + allocations: RPCAppSessionAllocation[], sessionData: object, expectedVersion: number ) => { - const closeAppSessionMsg = await createCloseAppSessionMessage(appIdentity.messageSigner, [ - { - app_session_id: appSessionId as Hex, - allocations, - session_data: JSON.stringify(sessionData), - }, - ]); + const closeAppSessionMsg = await createCloseAppSessionMessage(appIdentity.messageSigner, { + app_session_id: appSessionId as Hex, + allocations, + session_data: JSON.stringify(sessionData), + }); const closeAppSessionResponse = await appWS.sendAndWaitForResponse( closeAppSessionMsg, @@ -151,7 +153,7 @@ describe('Close channel', () => { expect(closeAppSessionResponse).toBeDefined(); - const closeAppSessionParsedResponse = rpcResponseParser.closeAppSession(closeAppSessionResponse); + const closeAppSessionParsedResponse = parseCloseAppSessionResponse(closeAppSessionResponse); expect(closeAppSessionParsedResponse).toBeDefined(); expect(closeAppSessionParsedResponse.params.appSessionId).toBe(appSessionId); expect(closeAppSessionParsedResponse.params.status).toBe(RPCChannelStatus.Closed); @@ -215,8 +217,8 @@ describe('Close channel', () => { it('should create app session with allowance for participant to deposit', async () => { await createAuthSessionWithClearnode(appWS, appIdentity, { - wallet: appIdentity.walletAddress, - participant: appIdentity.sessionAddress, + address: appIdentity.walletAddress, + session_key: appIdentity.sessionAddress, app_name: 'App Domain', expire: String(Math.floor(Date.now() / 1000) + 3600), // 1 hour expiration scope: 'console', @@ -241,16 +243,17 @@ describe('Close channel', () => { 1000 ); - const getLedgerBalancesParsedResponse = rpcResponseParser.getLedgerBalances(getLedgerBalancesResponse); + const getLedgerBalancesParsedResponse = parseGetLedgerBalancesResponse(getLedgerBalancesResponse); expect(getLedgerBalancesParsedResponse).toBeDefined(); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params[0].amount).toBe((decimalDepositAmount * BigInt(10)).toString()); - expect(getLedgerBalancesParsedResponse.params[0].asset).toBe('USDC'); + + const ledgerBalances = getLedgerBalancesParsedResponse.params.ledgerBalances; + expect(ledgerBalances).toHaveLength(1); + expect(ledgerBalances[0].amount).toBe((decimalDepositAmount * BigInt(10)).toString()); + expect(ledgerBalances[0].asset).toBe('USDC'); }); it('should create app session', async () => { - const definition: AppDefinition = { + const definition: RPCAppDefinition = { protocol: 'nitroliterpc', participants: [appIdentity.walletAddress, appCPIdentity.walletAddress], weights: [100, 0], @@ -272,20 +275,18 @@ describe('Close channel', () => { }, ]; - const createAppSessionMsg = await createAppSessionMessage(appIdentity.messageSigner, [ - { - definition, - allocations, - session_data: JSON.stringify(SESSION_DATA_WAITING), - }, - ]); + const createAppSessionMsg = await createAppSessionMessage(appIdentity.messageSigner, { + definition, + allocations, + session_data: JSON.stringify(SESSION_DATA_WAITING), + }); const createAppSessionResponse = await appWS.sendAndWaitForResponse( createAppSessionMsg, getCreateAppSessionPredicate(), 1000 ); - const createAppSessionParsedResponse = rpcResponseParser.createAppSession(createAppSessionResponse); + const createAppSessionParsedResponse = parseCreateAppSessionResponse(createAppSessionResponse); expect(createAppSessionParsedResponse).toBeDefined(); expect(createAppSessionParsedResponse.params.appSessionId).toBeDefined(); @@ -353,11 +354,13 @@ describe('Close channel', () => { 1000 ); - const getLedgerBalancesParsedResponse = rpcResponseParser.getLedgerBalances(getLedgerBalancesResponse); + const getLedgerBalancesParsedResponse = parseGetLedgerBalancesResponse(getLedgerBalancesResponse); expect(getLedgerBalancesParsedResponse).toBeDefined(); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params[0].amount).toBe((decimalDepositAmount * BigInt(9)).toString()); // 1000 - 100 - expect(getLedgerBalancesParsedResponse.params[0].asset).toBe('USDC'); + + const ledgerBalances = getLedgerBalancesParsedResponse.params.ledgerBalances; + expect(ledgerBalances).toHaveLength(1); + expect(ledgerBalances[0].amount).toBe((decimalDepositAmount * BigInt(9)).toString()); // 1000 - 100 + expect(ledgerBalances[0].asset).toBe('USDC'); }); it('should update ledger balances for receiving side', async () => { @@ -371,11 +374,13 @@ describe('Close channel', () => { 1000 ); - const getLedgerBalancesParsedResponse = rpcResponseParser.getLedgerBalances(getLedgerBalancesResponse); + const getLedgerBalancesParsedResponse = parseGetLedgerBalancesResponse(getLedgerBalancesResponse); expect(getLedgerBalancesParsedResponse).toBeDefined(); - expect(getLedgerBalancesParsedResponse.params).toHaveLength(1); - expect(getLedgerBalancesParsedResponse.params[0].amount).toBe((decimalDepositAmount * BigInt(11)).toString()); // 1000 + 100 - expect(getLedgerBalancesParsedResponse.params[0].asset).toBe('USDC'); + + const ledgerBalances = getLedgerBalancesParsedResponse.params.ledgerBalances; + expect(ledgerBalances).toHaveLength(1); + expect(ledgerBalances[0].amount).toBe((decimalDepositAmount * BigInt(11)).toString()); // 1000 + 100 + expect(ledgerBalances[0].asset).toBe('USDC'); }); it('should close channel and withdraw without app funds', async () => { @@ -384,7 +389,7 @@ describe('Close channel', () => { const closeResponse = await ws.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); expect(closeResponse).toBeDefined(); - const { params: closeResponseParams } = rpcResponseParser.closeChannel(closeResponse); + const { params: closeResponseParams } = parseCloseChannelResponse(closeResponse); const closeChannelTxHash = await client.closeChannel({ finalState: { intent: closeResponseParams.state.intent, @@ -406,16 +411,14 @@ describe('Close channel', () => { }); it('should resize channel by withdrawing received funds from app to channel', async () => { - const msg = await createResizeChannelMessage(cpIdentity.messageSigner, [ - { - channel_id: cpChannelId, - allocate_amount: depositAmount, - funds_destination: cpIdentity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(cpIdentity.messageSigner, { + channel_id: cpChannelId, + allocate_amount: depositAmount, + funds_destination: cpIdentity.walletAddress, + }); const resizeResponse = await cpWS.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.state.allocations).toBeDefined(); expect(resizeResponseParams.state.allocations).toHaveLength(2); @@ -469,7 +472,7 @@ describe('Close channel', () => { const closeResponse = await cpWS.sendAndWaitForResponse(msg, getCloseChannelPredicate(), 1000); expect(closeResponse).toBeDefined(); - const { params: closeResponseParams } = rpcResponseParser.closeChannel(closeResponse); + const { params: closeResponseParams } = parseCloseChannelResponse(closeResponse); const closeChannelTxHash = await cpClient.closeChannel({ finalState: { intent: closeResponseParams.state.intent, diff --git a/integration/tests/resize_channel.test.ts b/integration/tests/resize_channel.test.ts index 7ba5f7dea..49843a98f 100644 --- a/integration/tests/resize_channel.test.ts +++ b/integration/tests/resize_channel.test.ts @@ -5,7 +5,7 @@ import { Identity } from '@/identity'; import { TestNitroliteClient } from '@/nitroliteClient'; import { CONFIG } from '@/setup'; import { getResizeChannelPredicate, TestWebSocket } from '@/ws'; -import { createResizeChannelMessage, rpcResponseParser } from '@erc7824/nitrolite'; +import { createResizeChannelMessage, parseResizeChannelResponse } from '@erc7824/nitrolite'; import { Hex, parseUnits } from 'viem'; describe('Resize channel', () => { @@ -64,17 +64,15 @@ describe('Resize channel', () => { ); expect(preResizeChannelBalance).toBe(depositAmount * BigInt(5)); // 500 - const msg = await createResizeChannelMessage(identity.messageSigner, [ - { - channel_id: createResponseParams.channelId, - resize_amount: depositAmount, - allocate_amount: parseUnits('0', 6), - funds_destination: identity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(identity.messageSigner, { + channel_id: createResponseParams.channelId, + resize_amount: depositAmount, + allocate_amount: parseUnits('0', 6), + funds_destination: identity.walletAddress, + }); const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.channelId).toBe(createResponseParams.channelId); expect(resizeResponseParams.state.stateData).toBeDefined(); expect(resizeResponseParams.state.intent).toBe(2); // StateIntent.RESIZE // TODO: add enum to sdk @@ -155,17 +153,15 @@ describe('Resize channel', () => { ); expect(preResizeChannelBalance).toBe(depositAmount * BigInt(5)); // 500 - const msg = await createResizeChannelMessage(identity.messageSigner, [ - { - channel_id: createResponseParams.channelId, - resize_amount: -depositAmount, - allocate_amount: parseUnits('0', 6), - funds_destination: identity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(identity.messageSigner, { + channel_id: createResponseParams.channelId, + resize_amount: -depositAmount, + allocate_amount: parseUnits('0', 6), + funds_destination: identity.walletAddress, + }); const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.state.allocations).toBeDefined(); expect(resizeResponseParams.state.allocations).toHaveLength(2); expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); @@ -237,17 +233,15 @@ describe('Resize channel', () => { ); expect(preResizeChannelBalance).toBe(depositAmount * BigInt(5)); // 500 - const msg = await createResizeChannelMessage(identity.messageSigner, [ - { - channel_id: createResponseParams.channelId, - resize_amount: parseUnits('0', 6), - allocate_amount: -depositAmount, - funds_destination: identity.walletAddress, - }, - ]); + const msg = await createResizeChannelMessage(identity.messageSigner, { + channel_id: createResponseParams.channelId, + resize_amount: parseUnits('0', 6), + allocate_amount: -depositAmount, + funds_destination: identity.walletAddress, + }); const resizeResponse = await ws.sendAndWaitForResponse(msg, getResizeChannelPredicate(), 1000); - const { params: resizeResponseParams } = rpcResponseParser.resizeChannel(resizeResponse); + const { params: resizeResponseParams } = parseResizeChannelResponse(resizeResponse); expect(resizeResponseParams.state.allocations).toBeDefined(); expect(resizeResponseParams.state.allocations).toHaveLength(2); expect(String(resizeResponseParams.state.allocations[0].destination)).toBe(identity.walletAddress); diff --git a/sdk/src/rpc/api.ts b/sdk/src/rpc/api.ts index 285c61219..99f564fa5 100644 --- a/sdk/src/rpc/api.ts +++ b/sdk/src/rpc/api.ts @@ -12,10 +12,10 @@ import { EIP712AuthDomain, EIP712AuthMessage, AuthChallengeResponse, - RequestData, RPCMethod, + RPCData, + GetLedgerTransactionsFilters, RPCChannelStatus, - ResponsePayload, } from './types'; import { NitroliteRPC } from './nitrolite'; import { generateRequestId, getCurrentTimestamp } from './utils'; @@ -24,7 +24,6 @@ import { CreateAppSessionRequestParams, SubmitAppStateRequestParams, ResizeChannelRequestParams, - GetLedgerTransactionsFilters, GetLedgerTransactionsRequestParams, TransferRequestParams, CreateChannelRequestParams, @@ -45,17 +44,12 @@ export async function createAuthRequestMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const allowances = Object.values(params.allowances || {}).map((v) => [v.asset, v.amount]); - const paramsArray = [ - params.wallet, - params.participant, - params.app_name, - allowances, - params.expire ?? '', - params.scope ?? '', - params.application ?? '', - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthRequest, paramsArray, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthRequest, + params, + requestId, + timestamp, + }); return JSON.stringify(request); } @@ -76,9 +70,14 @@ export async function createAuthVerifyMessageFromChallenge( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ challenge: challenge }]; - - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthVerify, [params], timestamp); + const params = { challenge: challenge }; + + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthVerify, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -101,8 +100,13 @@ export async function createAuthVerifyMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ challenge: challenge.params.challengeMessage }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthVerify, params, timestamp); + const params = { challenge: challenge.params.challengeMessage }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthVerify, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); } @@ -121,8 +125,13 @@ export async function createAuthVerifyMessageWithJWT( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ jwt: jwtToken }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.AuthVerify, params, timestamp); + const params = { jwt: jwtToken }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.AuthVerify, + params, + requestId, + timestamp, + }); return JSON.stringify(request); } @@ -139,7 +148,12 @@ export async function createPingMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.Ping, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.Ping, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -158,7 +172,12 @@ export async function createGetConfigMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetConfig, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetConfig, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -177,7 +196,12 @@ export async function createGetUserTagMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetUserTag, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetUserTag, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -198,8 +222,13 @@ export async function createGetLedgerBalancesMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ participant: participant }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetLedgerBalances, params, timestamp); + const params = { participant: participant }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetLedgerBalances, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -222,13 +251,16 @@ export async function createGetLedgerEntriesMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - account_id: accountId, - ...(asset ? { asset } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetLedgerEntries, params, timestamp); + const params = { + account_id: accountId, + ...(asset ? { asset } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetLedgerEntries, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -261,13 +293,17 @@ export async function createGetLedgerTransactionsMessage( }); } - const paramsObj: GetLedgerTransactionsRequestParams = { + const params: GetLedgerTransactionsRequestParams = { account_id: accountId, ...filteredParams, }; - const params = [paramsObj]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetLedgerTransactions, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetLedgerTransactions, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -288,8 +324,13 @@ export async function createGetAppDefinitionMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ app_session_id: appSessionId }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetAppDefinition, params, timestamp); + const params = { app_session_id: appSessionId }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetAppDefinition, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -308,17 +349,20 @@ export async function createGetAppDefinitionMessage( export async function createGetAppSessionsMessage( signer: MessageSigner, participant: Address, - status?: string, + status?: RPCChannelStatus, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - participant, - ...(status ? { status } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetAppSessions, params, timestamp); + const params = { + participant, + ...(status ? { status } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetAppSessions, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -335,11 +379,16 @@ export async function createGetAppSessionsMessage( */ export async function createAppSessionMessage( signer: MessageSigner, - params: CreateAppSessionRequestParams[], + params: CreateAppSessionRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CreateAppSession, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CreateAppSession, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -356,11 +405,16 @@ export async function createAppSessionMessage( */ export async function createSubmitAppStateMessage( signer: MessageSigner, - params: SubmitAppStateRequestParams[], + params: SubmitAppStateRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.SubmitAppState, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.SubmitAppState, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -378,11 +432,16 @@ export async function createSubmitAppStateMessage( */ export async function createCloseAppSessionMessage( signer: MessageSigner, - params: CloseAppSessionRequestParams[], + params: CloseAppSessionRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CloseAppSession, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CloseAppSession, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -401,11 +460,19 @@ export async function createCloseAppSessionMessage( export async function createApplicationMessage( signer: MessageSigner, appSessionId: Hex, - messageParams: any[], + messageParams: any, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createAppRequest(requestId, RPCMethod.Message, messageParams, timestamp, appSessionId); + const request = NitroliteRPC.createAppRequest( + { + method: RPCMethod.Message, + params: messageParams, + requestId, + timestamp, + }, + appSessionId, + ); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -417,7 +484,12 @@ export async function createCreateChannelMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CreateChannel, [params], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CreateChannel, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest, (_, value) => (typeof value === 'bigint' ? value.toString() : value)); } @@ -439,8 +511,13 @@ export async function createCloseChannelMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [{ channel_id: channelId, funds_destination: fundDestination }]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.CloseChannel, params, timestamp); + const params = { channel_id: channelId, funds_destination: fundDestination }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.CloseChannel, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -457,11 +534,16 @@ export async function createCloseChannelMessage( */ export async function createResizeChannelMessage( signer: MessageSigner, - params: ResizeChannelRequestParams[], + params: ResizeChannelRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.ResizeChannel, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.ResizeChannel, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest, (_, value) => (typeof value === 'bigint' ? value.toString() : value)); @@ -484,13 +566,16 @@ export async function createGetChannelsMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - ...(participant ? { participant } : {}), - ...(status ? { status } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetChannels, params, timestamp); + const params = { + ...(participant ? { participant } : {}), + ...(status ? { status } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetChannels, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); } @@ -508,7 +593,12 @@ export async function createGetRPCHistoryMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetRPCHistory, [], timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetRPCHistory, + params: {}, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -529,12 +619,15 @@ export async function createGetAssetsMessage( requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { - const params = [ - { - ...(chainId ? { chain_id: chainId } : {}), - }, - ]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.GetAssets, params, timestamp); + const params = { + ...(chainId ? { chain_id: chainId } : {}), + }; + const request = NitroliteRPC.createRequest({ + method: RPCMethod.GetAssets, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -551,13 +644,13 @@ export async function createGetAssetsMessage( */ export async function createTransferMessage( signer: MessageSigner, - transferParams: TransferRequestParams, + params: TransferRequestParams, requestId: RequestID = generateRequestId(), timestamp: Timestamp = getCurrentTimestamp(), ): Promise { // Validate that exactly one destination type is provided (XOR logic) - const hasDestination = !!transferParams.destination; - const hasDestinationTag = !!transferParams.destination_user_tag; + const hasDestination = !!params.destination; + const hasDestinationTag = !!params.destination_user_tag; if (hasDestination === hasDestinationTag) { throw new Error( @@ -567,8 +660,12 @@ export async function createTransferMessage( ); } - const params = [transferParams]; - const request = NitroliteRPC.createRequest(requestId, RPCMethod.Transfer, params, timestamp); + const request = NitroliteRPC.createRequest({ + method: RPCMethod.Transfer, + params, + requestId, + timestamp, + }); const signedRequest = await NitroliteRPC.signRequestMessage(request, signer); return JSON.stringify(signedRequest); @@ -587,7 +684,7 @@ export function createEIP712AuthMessageSigner( partialMessage: PartialEIP712AuthMessage, domain: EIP712AuthDomain, ): MessageSigner { - return async (payload: RequestData | ResponsePayload): Promise => { + return async (payload: RPCData): Promise => { const address = walletClient.account?.address; if (!address) { throw new Error('Wallet client is not connected or does not have an account.'); @@ -603,19 +700,12 @@ export function createEIP712AuthMessageSigner( // Safely extract the challenge from the payload for an AuthVerify request. // The expected structure is `[id, 'auth_verify', [{ challenge: '...' }], ts]` const params = payload[2]; - const firstParam = Array.isArray(params) ? params[0] : undefined; - - if ( - typeof firstParam !== 'object' || - firstParam === null || - !('challenge' in firstParam) || - typeof firstParam.challenge !== 'string' - ) { + if (!('challenge' in params) || typeof params.challenge !== 'string') { throw new Error('Invalid payload for AuthVerify: The challenge string is missing or malformed.'); } - // After the check, TypeScript knows `firstParam` is an object with a `challenge` property of type string. - const challengeUUID: string = firstParam.challenge; + // After the check, TypeScript knows `params` is an object with a `challenge` property of type string. + const challengeUUID: string = params.challenge; const message: EIP712AuthMessage = { ...partialMessage, @@ -653,7 +743,7 @@ export function createEIP712AuthMessageSigner( * @returns A MessageSigner function that signs the payload using ECDSA. */ export function createECDSAMessageSigner(privateKey: Hex): MessageSigner { - return async (payload: RequestData | ResponsePayload): Promise => { + return async (payload: RPCData): Promise => { try { const message = toHex(JSON.stringify(payload, (_, v) => (typeof v === 'bigint' ? v.toString() : v))); diff --git a/sdk/src/rpc/nitrolite.ts b/sdk/src/rpc/nitrolite.ts index e93f2fa1f..2d4c6586b 100644 --- a/sdk/src/rpc/nitrolite.ts +++ b/sdk/src/rpc/nitrolite.ts @@ -1,16 +1,12 @@ import { Address, Hex } from 'viem'; import { NitroliteRPCMessage, - RequestData, - NitroliteRPCErrorDetail, MessageSigner, SingleMessageVerifier, MultiMessageVerifier, - ParsedResponse, - ResponsePayload, ApplicationRPCMessage, - RPCResponse, - RPCMethod, + RPCRequest, + RPCData, } from './types'; import { getCurrentTimestamp, generateRequestId } from './utils'; @@ -28,14 +24,15 @@ export class NitroliteRPC { * @param timestamp - Timestamp for the request. Defaults to the current time. * @returns A formatted NitroliteRPCMessage object for the request. */ - static createRequest( - requestId: number = generateRequestId(), - method: RPCMethod, - params: any[] = [], - timestamp: number = getCurrentTimestamp(), - ): NitroliteRPCMessage { - const requestData: RequestData = [requestId, method, params, timestamp]; - const message: NitroliteRPCMessage = { req: requestData, sig: [] }; + static createRequest({ + method, + params = {}, + requestId = generateRequestId(), + timestamp = getCurrentTimestamp(), + signatures = [], + }: RPCRequest): NitroliteRPCMessage { + const requestData: RPCData = [requestId, method, params, timestamp]; + const message: NitroliteRPCMessage = { req: requestData, sig: signatures }; return message; } @@ -50,123 +47,14 @@ export class NitroliteRPC { * @returns A formatted NitroliteRPCMessage object for the request. */ static createAppRequest( - requestId: number = generateRequestId(), - method: RPCMethod, - params: any[] = [], - timestamp: number = getCurrentTimestamp(), + { requestId = generateRequestId(), method, params = {}, timestamp = getCurrentTimestamp() }: RPCRequest, sid: Hex, ): ApplicationRPCMessage { - const requestData: RequestData = [requestId, method, params, timestamp]; + const requestData: RPCData = [requestId, method, params, timestamp]; const message: ApplicationRPCMessage = { req: requestData, sid }; return message; } - /** - * Parses a raw message string or object received from the broker, - * validating its structure as a Nitrolite RPC response. Handles both - * messages with and without the top-level 'sid' field. - * Does NOT verify the signature. - * - * @param rawMessage - The raw JSON string or pre-parsed object received. - * @returns A ParsedResponse object containing the extracted data and validation status. - */ - static parseResponse(rawMessage: string | object): ParsedResponse { - // TODO: either merge or replace it with parseRPCResponse from utils.ts - let message: any; - - try { - message = typeof rawMessage === 'string' ? JSON.parse(rawMessage) : rawMessage; - } catch (e) { - console.error('Failed to parse incoming message:', e); - return { - isValid: false, - error: 'Message parsing failed', - }; - } - - if ( - !message || - typeof message !== 'object' || - !message.res || - !Array.isArray(message.res) || - message.res.length !== 4 - ) { - return { - isValid: false, - error: "Invalid message structure: Missing or invalid 'res' array.", - }; - } - - const [requestId, method, dataPayload, timestamp] = message.res; - const sid = typeof message.sid === 'string' ? message.sid : undefined; - - if ( - typeof requestId !== 'number' || - typeof method !== 'string' || - !Object.values(RPCMethod).includes(method as RPCMethod) || - !Array.isArray(dataPayload) || - typeof timestamp !== 'number' - ) { - return { - isValid: false, - requestId, - method, - sid, - timestamp, - error: "Invalid 'res' payload structure or types.", - }; - } - - let data: any[] | NitroliteRPCErrorDetail; - let isError = false; - - if (method === RPCMethod.Error) { - isError = true; - if ( - dataPayload.length === 1 && - typeof dataPayload[0] === 'object' && - dataPayload[0] !== null && - 'error' in dataPayload[0] - ) { - data = dataPayload[0] as NitroliteRPCErrorDetail; - } else { - return { - isValid: false, - requestId, - method, - sid, - timestamp, - error: 'Malformed error response payload.', - }; - } - } else { - data = dataPayload; - } - - return { - isValid: true, - isError, - requestId, - method: method as RPCMethod, - data, - sid, - timestamp, - }; - } - - /** - * Type guard to check if a response is a specific RPC response type. - * @param response - The response to check - * @param method - The method name to check against - * @returns True if the response is of the specified type - */ - static isResponseType( - response: ParsedResponse, - method: T['method'], - ): response is ParsedResponse & { data: T['params'] } { - return response.isValid && !response.isError && response.method === method; - } - /** * Extracts the payload (req or res array) from a message for signing or verification. * @@ -175,7 +63,7 @@ export class NitroliteRPC { * @throws Error if the message doesn't contain a 'req' or 'res' field. * @private */ - private static getMessagePayload(message: NitroliteRPCMessage): RequestData | ResponsePayload { + private static getMessagePayload(message: NitroliteRPCMessage): RPCData { if (message.req) return message.req; if (message.res) return message.res; throw new Error("Message must contain either 'req' or 'res' field"); diff --git a/sdk/src/rpc/parse/app.ts b/sdk/src/rpc/parse/app.ts index 7bfce8bbb..9e4a8da84 100644 --- a/sdk/src/rpc/parse/app.ts +++ b/sdk/src/rpc/parse/app.ts @@ -7,118 +7,101 @@ import { CloseAppSessionResponseParams, GetAppDefinitionResponseParams, GetAppSessionsResponseParams, - RPCChannelStatus, + RPCAppSession, } from '../types'; -import { hexSchema, addressSchema, statusEnum, ParamsParser } from './common'; +import { hexSchema, addressSchema, statusEnum, ParamsParser, dateSchema } from './common'; + +const AppSessionObjectSchema = z + .object({ + app_session_id: hexSchema, + status: statusEnum, + participants: z.array(addressSchema), + protocol: z.string(), + challenge: z.number(), + weights: z.array(z.number()), + quorum: z.number(), + version: z.number(), + nonce: z.number(), + created_at: dateSchema, + updated_at: dateSchema, + session_data: z.string().optional(), + }) + .transform( + (raw): RPCAppSession => ({ + appSessionId: raw.app_session_id, + status: raw.status, + participants: raw.participants, + protocol: raw.protocol, + challenge: raw.challenge, + weights: raw.weights, + quorum: raw.quorum, + version: raw.version, + nonce: raw.nonce, + createdAt: raw.created_at, + updatedAt: raw.updated_at, + sessionData: raw.session_data, + }), + ); const CreateAppSessionParamsSchema = z - .array( - z.object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }).transform( - (raw) => - ({ - appSessionId: raw.app_session_id as `0x${string}`, - version: raw.version, - status: raw.status as RPCChannelStatus, - }) as CreateAppSessionResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }) + .transform( + (raw): CreateAppSessionResponseParams => ({ + appSessionId: raw.app_session_id, + version: raw.version, + status: raw.status, + }), + ); const SubmitAppStateParamsSchema = z - .array( - z.object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }).transform( - (raw) => - ({ - appSessionId: raw.app_session_id as `0x${string}`, - version: raw.version, - status: raw.status as RPCChannelStatus, - }) as SubmitAppStateResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }) + .transform( + (raw): SubmitAppStateResponseParams => ({ + appSessionId: raw.app_session_id, + version: raw.version, + status: raw.status, + }), + ); const CloseAppSessionParamsSchema = z - .array( - z.object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }).transform( - (raw) => - ({ - appSessionId: raw.app_session_id as `0x${string}`, - version: raw.version, - status: raw.status as RPCChannelStatus, - }) as CloseAppSessionResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ app_session_id: hexSchema, version: z.number(), status: statusEnum }) + .transform( + (raw): CloseAppSessionResponseParams => ({ + appSessionId: raw.app_session_id, + version: raw.version, + status: raw.status, + }), + ); const GetAppDefinitionParamsSchema = z - .array( - z - .object({ - protocol: z.string(), - participants: z.array(addressSchema), - weights: z.array(z.number()), - quorum: z.number(), - challenge: z.number(), - nonce: z.number(), - }) - .transform( - (raw) => - ({ - protocol: raw.protocol, - participants: raw.participants as Address[], - weights: raw.weights, - quorum: raw.quorum, - challenge: raw.challenge, - nonce: raw.nonce, - }) as GetAppDefinitionResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + protocol: z.string(), + participants: z.array(addressSchema), + weights: z.array(z.number()), + quorum: z.number(), + challenge: z.number(), + nonce: z.number(), + }) + .transform( + (raw): GetAppDefinitionResponseParams => ({ + protocol: raw.protocol, + participants: raw.participants as Address[], + weights: raw.weights, + quorum: raw.quorum, + challenge: raw.challenge, + nonce: raw.nonce, + }), + ); const GetAppSessionsParamsSchema = z - .array( - z.array( - z - .object({ - app_session_id: hexSchema, - status: statusEnum, - participants: z.array(addressSchema), - protocol: z.string(), - challenge: z.number(), - weights: z.array(z.number()), - quorum: z.number(), - version: z.number(), - nonce: z.number(), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - updated_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - session_data: z.string().optional(), - }) - .transform( - (s) => - ({ - appSessionId: s.app_session_id as `0x${string}`, - status: s.status as RPCChannelStatus, - participants: s.participants as Address[], - protocol: s.protocol, - challenge: s.challenge, - weights: s.weights, - quorum: s.quorum, - version: s.version, - nonce: s.nonce, - createdAt: s.created_at, - updatedAt: s.updated_at, - sessionData: s.session_data, - }) as GetAppSessionsResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetAppSessionsResponseParams[]); + .object({ + app_sessions: z.array(AppSessionObjectSchema), + }) + .transform( + (raw): GetAppSessionsResponseParams => ({ + appSessions: raw.app_sessions, + }), + ); export const appParamsParsers: Record> = { [RPCMethod.CreateAppSession]: (params) => CreateAppSessionParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/asset.ts b/sdk/src/rpc/parse/asset.ts index 0b63e9451..cae0fc50e 100644 --- a/sdk/src/rpc/parse/asset.ts +++ b/sdk/src/rpc/parse/asset.ts @@ -1,29 +1,39 @@ import { z } from 'zod'; -import { Address } from 'viem'; -import { RPCMethod, GetAssetsResponseParams } from '../types'; +import { RPCMethod, GetAssetsResponseParams, RPCAsset, AssetsResponseParams } from '../types'; import { addressSchema, ParamsParser } from './common'; +const AssetObjectSchema = z + .object({ token: addressSchema, chain_id: z.number(), symbol: z.string(), decimals: z.number() }) + .transform( + (raw): RPCAsset => ({ + token: raw.token, + chainId: raw.chain_id, + symbol: raw.symbol, + decimals: raw.decimals, + }), + ); + const GetAssetsParamsSchema = z - .array( - z.array( - z - .object({ token: addressSchema, chain_id: z.number(), symbol: z.string(), decimals: z.number() }) - .transform( - (a) => - ({ - token: a.token as Address, - chainId: a.chain_id, - symbol: a.symbol, - decimals: a.decimals, - }) as GetAssetsResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetAssetsResponseParams[]); + .object({ + assets: z.array(AssetObjectSchema), + }) + .transform( + (raw): GetAssetsResponseParams => ({ + assets: raw.assets, + }), + ); + +const AssetsParamsSchema = z + .object({ + assets: z.array(AssetObjectSchema), + }) + .transform( + (raw): AssetsResponseParams => ({ + assets: raw.assets, + }), + ); export const assetParamsParsers: Record> = { [RPCMethod.GetAssets]: (params) => GetAssetsParamsSchema.parse(params), - [RPCMethod.Assets]: (params) => GetAssetsParamsSchema.parse(params), // Alias + [RPCMethod.Assets]: (params) => AssetsParamsSchema.parse(params), }; diff --git a/sdk/src/rpc/parse/auth.ts b/sdk/src/rpc/parse/auth.ts index fd16cdcd4..a12b1f5d8 100644 --- a/sdk/src/rpc/parse/auth.ts +++ b/sdk/src/rpc/parse/auth.ts @@ -1,47 +1,30 @@ import { z } from 'zod'; -import { Address } from 'viem'; import { RPCMethod, AuthChallengeResponseParams, AuthVerifyResponseParams, AuthRequestResponseParams } from '../types'; import { addressSchema, ParamsParser } from './common'; const AuthChallengeParamsSchema = z - .array( - z - .object({ challenge_message: z.string() }) - .transform((raw) => ({ challengeMessage: raw.challenge_message }) as AuthChallengeResponseParams), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ challenge_message: z.string() }) + .transform((raw): AuthChallengeResponseParams => ({ challengeMessage: raw.challenge_message })); const AuthVerifyParamsSchema = z - .array( - z - .object({ - address: addressSchema, - session_key: addressSchema, - success: z.boolean(), - jwt_token: z.string().optional(), - }) - .transform( - (raw) => - ({ - address: raw.address as Address, - sessionKey: raw.session_key as Address, - success: raw.success, - jwtToken: raw.jwt_token, - }) as AuthVerifyResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + address: addressSchema, + session_key: addressSchema, + success: z.boolean(), + jwt_token: z.string().optional(), + }) + .transform( + (raw): AuthVerifyResponseParams => ({ + address: raw.address, + sessionKey: raw.session_key, + success: raw.success, + jwtToken: raw.jwt_token, + }), + ); const AuthRequestParamsSchema = z - .array( - z - .object({ challenge_message: z.string() }) - .transform((raw) => ({ challengeMessage: raw.challenge_message }) as AuthRequestResponseParams), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ challenge_message: z.string() }) + .transform((raw): AuthRequestResponseParams => ({ challengeMessage: raw.challenge_message })); export const authParamsParsers: Record> = { [RPCMethod.AuthChallenge]: (params) => AuthChallengeParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/channel.ts b/sdk/src/rpc/parse/channel.ts index ee5c3094f..33d3601e0 100644 --- a/sdk/src/rpc/parse/channel.ts +++ b/sdk/src/rpc/parse/channel.ts @@ -1,22 +1,22 @@ import { z } from 'zod'; -import { Address, Hex } from 'viem'; import { RPCMethod, - ChannelOperationResponseParams, - CreateChannelResponseParams, ResizeChannelResponseParams, CloseChannelResponseParams, GetChannelsResponseParams, ChannelUpdateResponseParams, - RPCChannelStatus, - ChannelUpdate, + RPCChannelUpdate, + ChannelsUpdateResponseParams, + RPCChannelUpdateWithWallet, + CreateChannelResponseParams, + RPCChannelOperation, } from '../types'; -import { hexSchema, addressSchema, statusEnum, ParamsParser } from './common'; +import { hexSchema, addressSchema, statusEnum, ParamsParser, bigIntSchema, dateSchema } from './common'; const RPCAllocationSchema = z.object({ destination: addressSchema, token: addressSchema, - amount: z.string(), + amount: bigIntSchema, }); const ChannelOperationObject = z.object({ @@ -24,121 +24,120 @@ const ChannelOperationObject = z.object({ state: z.object({ intent: z.number(), version: z.number(), - state_data: z.string(), + state_data: hexSchema, allocations: z.array(RPCAllocationSchema), }), server_signature: hexSchema, }); const ChannelOperationObjectSchema = ChannelOperationObject.transform( - (raw) => - ({ - channelId: raw.channel_id as Hex, - state: { - intent: raw.state.intent, - version: raw.state.version, - stateData: raw.state.state_data as Hex, - allocations: raw.state.allocations.map((a) => ({ - destination: a.destination as Address, - token: a.token as Address, - amount: BigInt(a.amount), - })), - }, - serverSignature: raw.server_signature, - }) as ChannelOperationResponseParams, + (raw): RPCChannelOperation => ({ + channelId: raw.channel_id, + state: { + intent: raw.state.intent, + version: raw.state.version, + stateData: raw.state.state_data, + allocations: raw.state.allocations.map((a) => ({ + destination: a.destination, + token: a.token, + amount: BigInt(a.amount), + })), + }, + serverSignature: raw.server_signature, + }), ); -const ChannelOperationParamsSchema = z - .array(ChannelOperationObjectSchema) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); - const CreateChannelParamsSchema = z - .array( - z - .object({ - ...ChannelOperationObject.shape, - channel: z.object({ - participants: z.array(addressSchema), - adjudicator: addressSchema, - challenge: z.number(), - nonce: z.number(), - }), - }) - .transform( - (params) => - ({ - ...ChannelOperationObjectSchema.parse(params), - channel: { - participants: params.channel.participants, - adjudicator: params.channel.adjudicator, - challenge: params.channel.challenge, - nonce: params.channel.nonce, - }, - }) as CreateChannelResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + ...ChannelOperationObject.shape, + channel: z.object({ + participants: z.array(addressSchema), + adjudicator: addressSchema, + challenge: z.number(), + nonce: z.number(), + }), + }) + .transform( + (raw): CreateChannelResponseParams => ({ + ...ChannelOperationObjectSchema.parse(raw), + channel: { + participants: raw.channel.participants, + adjudicator: raw.channel.adjudicator, + challenge: raw.channel.challenge, + nonce: raw.channel.nonce, + }, + }), + ); -const ResizeChannelParamsSchema = ChannelOperationParamsSchema.transform( - (params) => params as ResizeChannelResponseParams, -); +const ResizeChannelParamsSchema = ChannelOperationObjectSchema + // Validate received type with linter + .transform((raw): ResizeChannelResponseParams => raw); -const CloseChannelParamsSchema = ChannelOperationParamsSchema.transform( - (params) => params as CloseChannelResponseParams, +const CloseChannelParamsSchema = ChannelOperationObjectSchema + // Validate received type with linter + .transform((raw): CloseChannelResponseParams => raw); + +const ChannelUpdateObject = z.object({ + channel_id: hexSchema, + participant: addressSchema, + status: statusEnum, + token: addressSchema, + amount: bigIntSchema, + chain_id: z.number(), + adjudicator: addressSchema, + challenge: z.number(), + nonce: z.number(), + version: z.number(), + created_at: dateSchema, + updated_at: dateSchema, +}); + +const ChannelUpdateObjectSchema = ChannelUpdateObject.transform( + (raw): RPCChannelUpdate => ({ + channelId: raw.channel_id, + participant: raw.participant, + status: raw.status, + token: raw.token, + amount: BigInt(raw.amount), + chainId: raw.chain_id, + adjudicator: raw.adjudicator, + challenge: raw.challenge, + nonce: raw.nonce, + version: raw.version, + createdAt: raw.created_at, + updatedAt: raw.updated_at, + }), ); -const ChannelUpdateObjectSchema = z +const ChannelUpdateWithWalletObjectSchema = z .object({ - channel_id: hexSchema, - participant: addressSchema, - status: statusEnum, - token: addressSchema, - wallet: z.union([addressSchema, z.literal('')]), - amount: z.union([z.string(), z.number()]).transform((a) => BigInt(a)), - chain_id: z.number(), - adjudicator: addressSchema, - challenge: z.number(), - nonce: z.union([z.string(), z.number()]).transform((n) => BigInt(n)), - version: z.number(), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - updated_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), + ...ChannelUpdateObject.shape, + wallet: addressSchema, }) .transform( - (c) => - ({ - channelId: c.channel_id as Hex, - participant: c.participant as Address, - status: c.status as RPCChannelStatus, - token: c.token as Address, - wallet: c.wallet as Address, - amount: c.amount, - chainId: c.chain_id, - adjudicator: c.adjudicator as Address, - challenge: c.challenge, - nonce: c.nonce, - version: c.version, - createdAt: c.created_at, - updatedAt: c.updated_at, - }) as ChannelUpdateResponseParams, + (raw): RPCChannelUpdateWithWallet => ({ + ...ChannelUpdateObjectSchema.parse(raw), + wallet: raw.wallet, + }), ); const GetChannelsParamsSchema = z - .array(z.array(ChannelUpdateObjectSchema)) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetChannelsResponseParams); + .object({ + channels: z.array(ChannelUpdateWithWalletObjectSchema), + }) + // Validate received type with linter + .transform((raw): GetChannelsResponseParams => raw); -const ChannelUpdateParamsSchema = z - .array(ChannelUpdateObjectSchema) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); +const ChannelUpdateParamsSchema = ChannelUpdateObjectSchema + // Validate received type with linter + .transform((raw): ChannelUpdateResponseParams => raw); const ChannelsUpdateParamsSchema = z - .array(z.array(ChannelUpdateObjectSchema)) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + channels: z.array(ChannelUpdateObjectSchema), + }) + // Validate received type with linter + .transform((raw): ChannelsUpdateResponseParams => raw); export const channelParamsParsers: Record> = { [RPCMethod.CreateChannel]: (params) => CreateChannelParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/common.ts b/sdk/src/rpc/parse/common.ts index 774bc88eb..c5a38940b 100644 --- a/sdk/src/rpc/parse/common.ts +++ b/sdk/src/rpc/parse/common.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { RPCChannelStatus, RPCMethod } from '../types'; +import { Address, Hex } from 'viem'; // --- Shared Interfaces & Classes --- @@ -16,15 +17,33 @@ export class ParserParamsMissingError extends Error { // --- Shared Zod Schemas --- -export const hexSchema = z.string().refine((val) => /^0x[0-9a-fA-F]*$/.test(val), { - message: 'Must be a 0x-prefixed hex string', -}); - -export const addressSchema = z.string().refine((val) => /^0x[0-9a-fA-F]{40}$/.test(val), { - message: 'Must be a 0x-prefixed hex string of 40 hex chars (EVM address)', -}); - -export const statusEnum = z.enum(Object.values(RPCChannelStatus) as [string, ...string[]]); +export const hexSchema = z + .string() + .refine((val) => /^0x[0-9a-fA-F]*$/.test(val), { + message: 'Must be a 0x-prefixed hex string', + }) + .transform((v: string) => v as Hex); + +export const addressSchema = z + .string() + .refine((val) => /^0x[0-9a-fA-F]{40}$/.test(val), { + message: 'Must be a 0x-prefixed hex string of 40 hex chars (EVM address)', + }) + .transform((v: string) => v as Address); + +// TODO: add more validation for bigints if needed +export const bigIntSchema = z.string(); + +export const dateSchema = z.union([z.string(), z.date()]).transform((v) => new Date(v)); + +export const decimalSchema = z + .union([z.string(), z.number()]) + .transform((v) => v.toString()) + .refine((val) => /^[+-]?((\d+(\.\d*)?)|(\.\d+))$/.test(val), { + message: 'Must be a valid decimal string', + }); + +export const statusEnum = z.nativeEnum(RPCChannelStatus); // --- Shared Parser Functions --- diff --git a/sdk/src/rpc/parse/ledger.ts b/sdk/src/rpc/parse/ledger.ts index d41f47eda..5e7cc2382 100644 --- a/sdk/src/rpc/parse/ledger.ts +++ b/sdk/src/rpc/parse/ledger.ts @@ -6,60 +6,67 @@ import { GetLedgerEntriesResponseParams, BalanceUpdateResponseParams, GetLedgerTransactionsResponseParams, - TxType, - Transaction, + RPCTxType, + RPCTransaction, TransferNotificationResponseParams, TransferResponseParams, + RPCBalance, + RPCLedgerEntry, } from '../types'; -import { addressSchema, ParamsParser } from './common'; +import { addressSchema, dateSchema, decimalSchema, ParamsParser } from './common'; + +const BalanceObjectSchema = z + .object({ + asset: z.string(), + amount: decimalSchema, + }) + .transform((b): RPCBalance => b); const GetLedgerBalancesParamsSchema = z - .array( - z.array( - z.object({ - asset: z.string(), - amount: z.union([z.string(), z.number()]).transform((a) => a.toString()), - }), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetLedgerBalancesResponseParams[]); + .object({ + ledger_balances: z.array(BalanceObjectSchema), + }) + .transform( + (raw): GetLedgerBalancesResponseParams => ({ + ledgerBalances: raw.ledger_balances, + }), + ); + +const LedgerEntryObjectSchema = z + .object({ + id: z.number(), + account_id: z.string(), + account_type: z.number(), + asset: z.string(), + participant: addressSchema, + credit: decimalSchema, + debit: decimalSchema, + created_at: dateSchema, + }) + .transform( + (e): RPCLedgerEntry => ({ + id: e.id, + accountId: e.account_id, + accountType: e.account_type, + asset: e.asset, + participant: e.participant, + credit: e.credit, + debit: e.debit, + createdAt: e.created_at, + }), + ); const GetLedgerEntriesParamsSchema = z - .array( - z.array( - z - .object({ - id: z.number(), - account_id: z.string(), - account_type: z.number(), - asset: z.string(), - participant: addressSchema, - credit: z.union([z.string(), z.number()]).transform((v) => v.toString()), - debit: z.union([z.string(), z.number()]).transform((v) => v.toString()), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), - }) - .transform( - (e) => - ({ - id: e.id, - accountId: e.account_id, - accountType: e.account_type, - asset: e.asset, - participant: e.participant as Address, - credit: e.credit, - debit: e.debit, - createdAt: e.created_at, - }) as GetLedgerEntriesResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetLedgerEntriesResponseParams[]); + .object({ + ledger_entries: z.array(LedgerEntryObjectSchema), + }) + .transform( + (raw): GetLedgerEntriesResponseParams => ({ + ledgerEntries: raw.ledger_entries, + }), + ); -export const txTypeEnum = z.nativeEnum(TxType); +export const txTypeEnum = z.nativeEnum(RPCTxType); export const TransactionSchema = z .object({ @@ -71,15 +78,15 @@ export const TransactionSchema = z to_account_tag: z.string().optional(), asset: z.string(), amount: z.string(), - created_at: z.union([z.string(), z.date()]).transform((v) => new Date(v)), + created_at: dateSchema, }) .transform( - (raw): Transaction => ({ + (raw): RPCTransaction => ({ id: raw.id, txType: raw.tx_type, - fromAccount: raw.from_account as Address, + fromAccount: raw.from_account, fromAccountTag: raw.from_account_tag, - toAccount: raw.to_account as Address, + toAccount: raw.to_account, toAccountTag: raw.to_account_tag, asset: raw.asset, amount: raw.amount, @@ -88,30 +95,44 @@ export const TransactionSchema = z ); const GetLedgerTransactionsParamsSchema = z - .array(z.array(TransactionSchema)) - .refine((arr) => arr.length === 1) - .transform((arr): GetLedgerTransactionsResponseParams => arr[0]); + .object({ + ledger_transactions: z.array(TransactionSchema), + }) + .transform( + (raw): GetLedgerTransactionsResponseParams => ({ + ledgerTransactions: raw.ledger_transactions, + }), + ); const BalanceUpdateParamsSchema = z - .array( - z.array( - z - .object({ asset: z.string(), amount: z.union([z.string(), z.number()]).transform((a) => a.toString()) }) - .transform((b) => ({ asset: b.asset, amount: b.amount }) as BalanceUpdateResponseParams), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + balance_updates: z.array(BalanceObjectSchema), + }) + .transform( + (raw): BalanceUpdateResponseParams => ({ + balanceUpdates: raw.balance_updates, + }), + ); const TransferParamsSchema = z - .array(z.array(TransactionSchema)) - .refine((arr) => arr.length === 1) - .transform((arr): TransferResponseParams => arr[0]); + .object({ + transactions: z.array(TransactionSchema), + }) + .transform( + (raw): TransferResponseParams => ({ + transactions: raw.transactions, + }), + ); const TransferNotificationParamsSchema = z - .array(z.array(TransactionSchema)) - .refine((arr) => arr.length === 1) - .transform((arr): TransferNotificationResponseParams => arr[0]); + .object({ + transactions: z.array(TransactionSchema), + }) + .transform( + (raw): TransferNotificationResponseParams => ({ + transactions: raw.transactions, + }), + ); export const ledgerParamsParsers: Record> = { [RPCMethod.GetLedgerBalances]: (params) => GetLedgerBalancesParamsSchema.parse(params), diff --git a/sdk/src/rpc/parse/misc.ts b/sdk/src/rpc/parse/misc.ts index 6b4e96f8f..a3a44fbfa 100644 --- a/sdk/src/rpc/parse/misc.ts +++ b/sdk/src/rpc/parse/misc.ts @@ -1,97 +1,92 @@ import { z } from 'zod'; -import { Address } from 'viem'; import { RPCMethod, GetConfigResponseParams, ErrorResponseParams, GetRPCHistoryResponseParams, - UserTagParams, + RPCNetworkInfo, + RPCHistoryEntry, + GetUserTagResponseParams, } from '../types'; -import { hexSchema, addressSchema, ParamsParser, ParserParamsMissingError } from './common'; +import { hexSchema, addressSchema, ParamsParser } from './common'; -const NetworkInfoSchema = z.object({ - name: z.string(), - chain_id: z.number(), - custody_address: addressSchema, - adjudicator_address: addressSchema, -}); +const NetworkInfoObjectSchema = z + .object({ + name: z.string(), + chain_id: z.number(), + custody_address: addressSchema, + adjudicator_address: addressSchema, + }) + .transform( + (raw): RPCNetworkInfo => ({ + name: raw.name, + chainId: raw.chain_id, + custodyAddress: raw.custody_address, + adjudicatorAddress: raw.adjudicator_address, + }), + ); const GetConfigParamsSchema = z - .array( - z - .object({ broker_address: addressSchema, networks: z.array(NetworkInfoSchema) }) - .strict() - .transform( - (raw) => - ({ - brokerAddress: raw.broker_address as Address, - networks: raw.networks.map((n) => ({ - name: n.name, - chainId: n.chain_id, - custodyAddress: n.custody_address as Address, - adjudicatorAddress: n.adjudicator_address as Address, - })), - }) as GetConfigResponseParams, - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ broker_address: addressSchema, networks: z.array(NetworkInfoObjectSchema) }) + .strict() + .transform( + (raw): GetConfigResponseParams => ({ + brokerAddress: raw.broker_address, + networks: raw.networks, + }), + ); const ErrorParamsSchema = z - .array(z.string().transform((raw) => ({ error: raw }) as ErrorResponseParams)) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ error: z.string() }) + // Validate received type with linter + .transform((raw): ErrorResponseParams => raw); + +const RPCEntryObjectSchema = z + .object({ + id: z.number(), + sender: addressSchema, + req_id: z.number(), + method: z.string(), + params: z.string(), + timestamp: z.number(), + req_sig: z.array(hexSchema), + res_sig: z.array(hexSchema), + response: z.string(), + }) + .transform( + (raw): RPCHistoryEntry => ({ + id: raw.id, + sender: raw.sender, + reqId: raw.req_id, + method: raw.method, + params: raw.params, + timestamp: raw.timestamp, + reqSig: raw.req_sig, + resSig: raw.res_sig, + response: raw.response, + }), + ); const GetRPCHistoryParamsSchema = z - .array( - z.array( - z - .object({ - id: z.number(), - sender: addressSchema, - req_id: z.number(), - method: z.string(), - params: z.string(), - timestamp: z.number(), - req_sig: z.array(hexSchema), - res_sig: z.array(hexSchema), - response: z.string(), - }) - .transform( - (h) => - ({ - id: h.id, - sender: h.sender as Address, - reqId: h.req_id, - method: h.method, - params: h.params, - timestamp: h.timestamp, - reqSig: h.req_sig as any, - resSig: h.res_sig as any, - response: h.response, - }) as GetRPCHistoryResponseParams, - ), - ), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]) - .transform((arr) => arr as GetRPCHistoryResponseParams[]); + .object({ + rpc_entries: z.array(RPCEntryObjectSchema), + }) + .transform( + (raw): GetRPCHistoryResponseParams => ({ + rpcEntries: raw.rpc_entries, + }), + ); const GetUserTagParamsSchema = z - .array( - z - .object({ - tag: z.string(), - }) - .strict() - .transform((raw) => ({ tag: raw.tag }) as UserTagParams), - ) - .refine((arr) => arr.length === 1) - .transform((arr) => arr[0]); + .object({ + tag: z.string(), + }) + .strict() + // Validate received type with linter + .transform((raw): GetUserTagResponseParams => raw); const parseMessageParams: ParamsParser = (params) => { - if (!Array.isArray(params) || params.length === 0) throw new ParserParamsMissingError(RPCMethod.Message); - return params[0]; + return params; }; export const miscParamsParsers: Record> = { diff --git a/sdk/src/rpc/parse/parse.ts b/sdk/src/rpc/parse/parse.ts index e88e388a9..3a4e17da2 100644 --- a/sdk/src/rpc/parse/parse.ts +++ b/sdk/src/rpc/parse/parse.ts @@ -56,45 +56,95 @@ const _parseSpecificRPCResponse = ( return result as SpecificRPCResponse; }; -/** - * The main RPC response parsing utility. - * This object provides a collection of type-safe parsers for each specific RPC method. - * It offers the best developer experience, returning a fully typed object - * without the need for manual type guards. - * - * @example - * const result = rpcResponseParser.authChallenge(rawResponse); - * // `result` is now fully typed as AuthChallengeResponse - * console.log(result.params.challengeMessage); - */ -export const rpcResponseParser = { - authChallenge: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthChallenge), - authVerify: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthVerify), - authRequest: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthRequest), - error: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Error), - getConfig: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetConfig), - getLedgerBalances: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerBalances), - getLedgerEntries: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerEntries), - getLedgerTransactions: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerTransactions), - getUserTag: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetUserTag), - createAppSession: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateAppSession), - submitAppState: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.SubmitAppState), - closeAppSession: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseAppSession), - getAppDefinition: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppDefinition), - getAppSessions: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppSessions), - createChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateChannel), - resizeChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ResizeChannel), - closeChannel: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseChannel), - getChannels: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetChannels), - getRPCHistory: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetRPCHistory), - getAssets: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAssets), - assets: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Assets), - message: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Message), - balanceUpdate: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.BalanceUpdate), - channelsUpdate: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelsUpdate), - channelUpdate: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelUpdate), - ping: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Ping), - pong: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Pong), - transfer: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Transfer), - transferNotification: (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.TransferNotification), -}; +/** Parses `auth_challenge` response */ +export const parseAuthChallengeResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthChallenge); + +/** Parses `auth_verify` response */ +export const parseAuthVerifyResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthVerify); + +/** Parses `auth_request` response */ +export const parseAuthRequestResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.AuthRequest); + +/** Parses `error` response */ +export const parseErrorResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Error); + +/** Parses `get_config` response */ +export const parseGetConfigResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetConfig); + +/** Parses `get_ledger_balances` response */ +export const parseGetLedgerBalancesResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerBalances); + +/** Parses `get_ledger_entries` response */ +export const parseGetLedgerEntriesResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerEntries); + +/** Parses `get_ledger_transactions` response */ +export const parseGetLedgerTransactionsResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetLedgerTransactions); + +/** Parses `get_user_tag` response */ +export const parseGetUserTagResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetUserTag); + +/** Parses `create_app_session` response */ +export const parseCreateAppSessionResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.CreateAppSession); + +/** Parses `submit_app_state` response */ +export const parseSubmitAppStateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.SubmitAppState); + +/** Parses `close_app_session` response */ +export const parseCloseAppSessionResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseAppSession); + +/** Parses `get_app_definition` response */ +export const parseGetAppDefinitionResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.GetAppDefinition); + +/** Parses `get_app_sessions` response */ +export const parseGetAppSessionsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAppSessions); + +/** Parses `create_channel` response */ +export const parseCreateChannelResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CreateChannel); + +/** Parses `resize_channel` response */ +export const parseResizeChannelResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ResizeChannel); + +/** Parses `close_channel` response */ +export const parseCloseChannelResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.CloseChannel); + +/** Parses `get_channels` response */ +export const parseGetChannelsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetChannels); + +/** Parses `get_rpc_history` response */ +export const parseGetRPCHistoryResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetRPCHistory); + +/** Parses `get_assets` response */ +export const parseGetAssetsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.GetAssets); + +/** Parses `assets` response */ +export const parseAssetsResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Assets); + +/** Parses `message` response */ +export const parseMessageResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Message); + +/** Parses `bu` response */ +export const parseBalanceUpdateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.BalanceUpdate); + +/** Parses `channels` response */ +export const parseChannelsUpdateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelsUpdate); + +/** Parses `cu` response */ +export const parseChannelUpdateResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.ChannelUpdate); + +/** Parses `ping` response */ +export const parsePingResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Ping); + +/** Parses `pong` response */ +export const parsePongResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Pong); + +/** Parses `transfer` response */ +export const parseTransferResponse = (raw: string) => _parseSpecificRPCResponse(raw, RPCMethod.Transfer); + +/** Parses `tr` response */ +export const parseTransferNotificationResponse = (raw: string) => + _parseSpecificRPCResponse(raw, RPCMethod.TransferNotification); diff --git a/sdk/src/rpc/types/common.ts b/sdk/src/rpc/types/common.ts new file mode 100644 index 000000000..c567c7d7a --- /dev/null +++ b/sdk/src/rpc/types/common.ts @@ -0,0 +1,295 @@ +import { Hex, Address } from 'viem'; + +/** Represents the status of a channel. */ +export enum RPCChannelStatus { + Open = 'open', + Closed = 'closed', + Challenged = 'challenged', +} + +/** + * Represents the request parameters for the 'get_transactions' RPC method. + */ +export enum RPCTxType { + Transfer = 'transfer', + Deposit = 'deposit', + Withdrawal = 'withdrawal', + AppDeposit = 'app_deposit', + AppWithdrawal = 'app_withdrawal', +} + +/** + * Defines the structure of an application definition used when creating an application. + */ +export interface RPCAppDefinition { + /** The protocol identifier or name for the application logic (e.g., "NitroRPC/0.2"). */ + protocol: string; + /** An array of participant addresses (Ethereum addresses) involved in the application. Must have at least 2 participants. */ + participants: Hex[]; + /** An array representing the relative weights or stakes of participants, often used for dispute resolution or allocation calculations. Order corresponds to the participants array. */ + weights: number[]; + /** The number of participants required to reach consensus or approve state updates. */ + quorum: number; + /** A parameter related to the challenge period or mechanism within the application's protocol, in seconds. */ + challenge: number; + /** A unique number used once, often for preventing replay attacks or ensuring uniqueness of the application instance. Must be non-zero. */ + nonce?: number; +} + +/** + * Represents a channel update message sent over the RPC protocol. + */ +export interface RPCChannelUpdate { + /** The unique identifier for the channel. */ + channelId: Hex; + /** The Ethereum address of the participant. */ + participant: Address; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + /** The token contract address. */ + token: Address; + /** The total amount in the channel. */ + amount: BigInt; + /** The chain ID where the channel exists. */ + chainId: number; + /** The adjudicator contract address. */ + adjudicator: Address; + /** The challenge period in seconds. */ + challenge: number; + /** The nonce value for the channel. */ + nonce: number; + /** The version number of the channel. */ + version: number; + /** The timestamp when the channel was created. */ + createdAt: Date; + /** The timestamp when the channel was last updated. */ + updatedAt: Date; +} + +export interface RPCChannelUpdateWithWallet extends RPCChannelUpdate { + /** The Ethereum address of the wallet associated with the channel. */ + wallet: Address; +} + +/** + * Represents the network information for the 'get_config' RPC method. + */ +export interface RPCNetworkInfo { + /** The name of the network (e.g., "Ethereum", "Polygon"). */ + name: string; + /** The chain ID of the network. */ + chainId: number; + /** The custody contract address for the network. */ + custodyAddress: Address; + /** The adjudicator contract address for the network. */ + adjudicatorAddress: Address; +} + +/** + * Represents the balance information from clearnode. + */ +export interface RPCBalance { + /** The asset symbol (e.g., "ETH", "USDC"). */ + asset: string; + /** The balance amount. */ + amount: string; +} + +/** + * Represents a single entry in the ledger. + */ +export interface RPCLedgerEntry { + /** Unique identifier for the ledger entry. */ + id: number; + /** The account identifier associated with the entry. */ + accountId: string; + /** The type of account (e.g., "wallet", "channel"). */ + accountType: number; + /** The asset symbol for the entry. */ + asset: string; + /** The Ethereum address of the participant. */ + participant: Address; + /** The credit amount. */ + credit: string; + /** The debit amount. */ + debit: string; + /** The timestamp when the entry was created. */ + createdAt: Date; +} + +/** + * Represents the app session information. + */ +export interface RPCAppSession { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + /** List of participant Ethereum addresses. */ + participants: Address[]; + /** The protocol identifier for the application. */ + protocol: string; + /** The challenge period in seconds. */ + challenge: number; + /** The signature weights for each participant. */ + weights: number[]; + /** The minimum number of signatures required for state updates. */ + quorum: number; + /** The version number of the session. */ + version: number; + /** The nonce value for the session. */ + nonce: number; + /** The timestamp when the session was created. */ + createdAt: Date; + /** The timestamp when the session was last updated. */ + updatedAt: Date; + /** Optional session data as a JSON string that stores application-specific state or metadata. */ + sessionData?: string; +} + +/** + * Represents RPC entry in the history. + */ +export interface RPCHistoryEntry { + /** Unique identifier for the RPC entry. */ + id: number; + /** The Ethereum address of the sender. */ + sender: Address; + /** The request ID for the RPC call. */ + reqId: number; + /** The RPC method name. */ + method: string; + /** The JSON string of the request parameters. */ + params: string; + /** The timestamp of the RPC call. */ + timestamp: number; + /** Array of request signatures. */ + reqSig: Hex[]; + /** Array of response signatures. */ + resSig: Hex[]; + /** The JSON string of the response. */ + response: string; +} + +/** + * Represents Asset information received from the clearnode. + */ +export interface RPCAsset { + /** The token contract address. */ + token: Address; + /** The chain ID where the asset exists. */ + chainId: number; + /** The asset symbol (e.g., "ETH", "USDC"). */ + symbol: string; + /** The number of decimal places for the asset. */ + decimals: number; +} + +/** + * Represents the parameters for the transfer transaction. + */ +export interface RPCTransaction { + /** Unique identifier for the transfer. */ + id: number; + /** The type of transaction. */ + txType: RPCTxType; + /** The source address from which assets were transferred. */ + fromAccount: Address; + /** The user tag for the source account (optional). */ + fromAccountTag?: string; + /** The destination address to which assets were transferred. */ + toAccount: Address; + /** The user tag for the destination account (optional). */ + toAccountTag?: string; + /** The asset symbol that was transferred. */ + asset: string; + /** The amount that was transferred. */ + amount: string; + /** The timestamp when the transfer was created. */ + createdAt: Date; +} + +/** + * Represents a generic RPC message structure that includes common fields. + * This interface is extended by specific RPC request and response types. + */ +export interface RPCAllowance { + /** The symbol of the asset (e.g., "USDC", "USDT"). */ + asset: string; + /** The amount of the asset that is allowed to be spent. */ + amount: string; +} + +// TODO: create single domain allocation type + +/** + * Represents the allocation of assets within an application session. + * This structure is used to define the initial allocation of assets among participants. + * It includes the participant's address, the asset (usdc, usdt, etc) being allocated, and the amount. + */ +export interface RPCAppSessionAllocation { + /** The symbol of the asset (e.g., "USDC", "USDT", "ETH"). */ + asset: string; + /** The amount of the asset. Must be a positive number. */ + amount: string; + /** The Ethereum address of the participant receiving the allocation. */ + participant: Address; +} + +/** + * Represents the allocation of assets for an RPC transfer. + * This structure is used to define the asset and amount being transferred to a specific destination address. + */ +export interface RPCChannelAllocation { + /** The destination address for the allocation. */ + destination: Address; + /** The token contract address for the asset being allocated. */ + token: Address; + /** The amount of the asset being allocated. */ + amount: bigint; +} + +/** + * Represents the allocation of assets for an RPC transfer. + * This structure is used to define the asset and amount being transferred. + */ +export interface RPCTransferAllocation { + /** The symbol of the asset (e.g., "USDC", "USDT", "ETH"). */ + asset: string; + /** The amount of the asset being transferred. */ + amount: string; +} + +/** + * Represents the state of a channel operation. + * This structure is used to define the intent, version, state data, and allocations for a channel operation. + */ +export interface RPCChannelOperationState { + /** The intent type for the state update. */ + intent: number; + /** The version number of the channel. */ + version: number; + /** The encoded state data for the channel. */ + stateData: Hex; + /** The list of allocations for the channel. */ + allocations: RPCChannelAllocation[]; +} + +export interface RPCChannelOperation { + /** The unique identifier for the channel. */ + channelId: Hex; + /** The channel state object. */ + state: RPCChannelOperationState; + /** The server's signature for the state update. */ + serverSignature: Hex; +} + +/** + * Represents the fixed part of a channel, containing essential metadata. + */ +export interface RPCChannel { + participants: Address[]; + adjudicator: Address; + challenge: number; + nonce: number; +} diff --git a/sdk/src/rpc/types/filters.ts b/sdk/src/rpc/types/filters.ts new file mode 100644 index 000000000..acdcb38e6 --- /dev/null +++ b/sdk/src/rpc/types/filters.ts @@ -0,0 +1,17 @@ +import { RPCTxType } from './common'; + +export interface PaginationFilters { + /** Pagination offset. */ + offset?: number; + /** Number of transactions to return. */ + limit?: number; + /** Sort order by created_at. */ + sort?: 'asc' | 'desc'; +} + +export interface GetLedgerTransactionsFilters extends PaginationFilters { + /** Filter by transaction type. */ + tx_type?: RPCTxType; + /** Filter by asset symbol. */ + asset?: string; +} diff --git a/sdk/src/rpc/types/index.ts b/sdk/src/rpc/types/index.ts index 543f0daec..76028ee2c 100644 --- a/sdk/src/rpc/types/index.ts +++ b/sdk/src/rpc/types/index.ts @@ -1,7 +1,10 @@ import { Address, Hex } from 'viem'; +import { RPCAllowance } from './common'; export * from './request'; export * from './response'; +export * from './filters'; +export * from './common'; /** Type alias for Request ID (uint64) */ export type RequestID = number; @@ -12,76 +15,19 @@ export type Timestamp = number; /** Type alias for Account ID (channelId or appId) */ export type AccountID = Hex; -/** Represents the data payload within a request message: [requestId, method, params, timestamp?]. */ -export type RequestData = [RequestID, RPCMethod, object[], Timestamp?]; - -/** Represents the data payload within a successful response message: [requestId, method, result, timestamp?]. */ -export type ResponseData = [RequestID, RPCMethod, object[], Timestamp?]; - -/** Represents the status of a channel. */ -export enum RPCChannelStatus { - Open = 'open', - Closed = 'closed', - Challenged = 'challenged', -} +/** Represents the data payload within a request or response message: [requestId, method, params, timestamp?]. */ +export type RPCData = [RequestID, RPCMethod, object, Timestamp?]; /** * Represents a generic RPC message structure that includes common fields. * This interface is extended by specific RPC request and response types. */ export interface GenericRPCMessage { - requestId: RequestID; + requestId?: RequestID; timestamp?: Timestamp; signatures?: Hex[]; } -/** Base type for asset allocations with common asset and amount fields. */ -export type AssetAllocation = { - /** The symbol of the asset (e.g., "USDC", "USDT", "ETH"). */ - asset: string; - /** The amount of the asset. Must be a positive number. */ - amount: string; -}; - -/** - * Represents a generic RPC message structure that includes common fields. - * This interface is extended by specific RPC request and response types. - */ -export type Allowance = { - /** The symbol of the asset (e.g., "USDC", "USDT"). */ - asset: string; - /** The amount of the asset that is allowed to be spent. */ - amount: string; -}; - -/** Represents the allocation of assets within an application session. - * This structure is used to define the initial allocation of assets among participants. - * It includes the participant's address, the asset (usdc, usdt, etc) being allocated, and the amount. - */ -export type AppSessionAllocation = AssetAllocation & { - /** The Ethereum address of the participant receiving the allocation. */ - participant: Address; -}; - -/** Represents the allocation of assets for a transfer. - * This structure is used to define the asset and amount being transferred. - */ -export type TransferAllocation = AssetAllocation; - -/** - * Represents the structure of an error object within an error response payload. - */ -export interface NitroliteRPCErrorDetail { - /** The error message describing what went wrong. */ - error: string; -} - -/** Represents the data payload for an error response: [requestId, "error", [errorDetail], timestamp?]. */ -export type ErrorResponseData = [RequestID, 'error', [NitroliteRPCErrorDetail], Timestamp?]; - -/** Union type for the 'res' payload, covering both success and error responses. */ -export type ResponsePayload = ResponseData | ErrorResponseData; - /** * Defines the wire format for Nitrolite RPC messages, based on NitroRPC principles * as adapted for the Clearnet protocol. @@ -89,9 +35,9 @@ export type ResponsePayload = ResponseData | ErrorResponseData; */ export interface NitroliteRPCMessage { /** Contains the request payload if this is a request message. */ - req?: RequestData; + req?: RPCData; /** Contains the response or error payload if this is a response message. */ - res?: ResponsePayload; + res?: RPCData; /** Optional cryptographic signature(s) for message authentication. */ sig?: Hex[]; } @@ -108,79 +54,6 @@ export interface ApplicationRPCMessage extends NitroliteRPCMessage { sid: Hex; } -/** - * Represents the result of parsing an incoming Nitrolite RPC response message. - * Contains extracted fields and validation status. - */ -export interface ParsedResponse { - /** Indicates if the message was successfully parsed and passed basic structural validation. */ - isValid: boolean; - /** If isValid is false, contains a description of the parsing or validation error. */ - error?: string; - /** Indicates if the parsed response represents an error (method === "error"). Undefined if isValid is false. */ - isError?: boolean; - /** The Request ID from the response payload. Undefined if structure is invalid. */ - requestId?: RequestID; - /** The method name from the response payload. Undefined if structure is invalid. */ - method?: RPCMethod; - /** The extracted data payload (result array for success, error detail object for error). Undefined if structure is invalid or error payload malformed. */ - data?: object[] | NitroliteRPCErrorDetail; - /** The Application Session ID from the message envelope. Undefined if structure is invalid. */ - sid?: Hex; - /** The Timestamp from the response payload. Undefined if structure is invalid. */ - timestamp?: Timestamp; -} - -/** - * Defines the structure of an application definition used when creating an application. - */ -export interface AppDefinition { - /** The protocol identifier or name for the application logic (e.g., "NitroRPC/0.2"). */ - protocol: string; - /** An array of participant addresses (Ethereum addresses) involved in the application. Must have at least 2 participants. */ - participants: Hex[]; - /** An array representing the relative weights or stakes of participants, often used for dispute resolution or allocation calculations. Order corresponds to the participants array. */ - weights: number[]; - /** The number of participants required to reach consensus or approve state updates. */ - quorum: number; - /** A parameter related to the challenge period or mechanism within the application's protocol, in seconds. */ - challenge: number; - /** A unique number used once, often for preventing replay attacks or ensuring uniqueness of the application instance. Must be non-zero. */ - nonce?: number; -} - -/** - * Represents a channel update message sent over the RPC protocol. - */ -export interface ChannelUpdate { - /** The unique identifier for the channel. */ - channelId: Hex; - /** The Ethereum address of the participant. */ - participant: Address; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; - /** The token contract address. */ - token: Address; - /** The wallet address associated with the channel. */ - wallet: Address; - /** The total amount in the channel. */ - amount: BigInt; - /** The chain ID where the channel exists. */ - chainId: number; - /** The adjudicator contract address. */ - adjudicator: Address; - /** The challenge period in seconds. */ - challenge: number; - /** The nonce value for the channel. */ - nonce: BigInt; - /** The version number of the channel. */ - version: number; - /** The timestamp when the channel was created. */ - createdAt: Date; - /** The timestamp when the channel was last updated. */ - updatedAt: Date; -} - /** * Defines standard error codes for the Nitrolite RPC protocol. * Includes standard JSON-RPC codes and custom codes for specific errors. @@ -217,7 +90,7 @@ export enum NitroliteErrorCode { * @param payload - The RequestData or ResponsePayload object (array) to sign. * @returns A Promise that resolves to the cryptographic signature as a Hex string. */ -export type MessageSigner = (payload: RequestData | ResponsePayload) => Promise; +export type MessageSigner = (payload: RPCData) => Promise; /** * Defines the function signature for signing challenge state data. @@ -236,7 +109,7 @@ export type ChallengeStateSigner = (stateHash: Hex) => Promise; * @returns A Promise that resolves to true if the signature is valid for the given payload and address, false otherwise. */ export type SingleMessageVerifier = ( - payload: RequestData | ResponsePayload, + payload: RPCData, signature: Hex, address: Address, ) => Promise; @@ -250,7 +123,7 @@ export type SingleMessageVerifier = ( * @returns A Promise that resolves to true if all required signatures from the expected signers are present and valid, false otherwise. */ export type MultiMessageVerifier = ( - payload: RequestData | ResponsePayload, + payload: RPCData, signatures: Hex[], expectedSigners: Address[], ) => Promise; @@ -265,11 +138,7 @@ export interface PartialEIP712AuthMessage { application: Address; participant: Address; expire: string; - // TODO: use Allowance type after replacing symbol with asset - allowances: { - asset: string; - amount: string; - }[]; + allowances: RPCAllowance[]; } /** diff --git a/sdk/src/rpc/types/request.ts b/sdk/src/rpc/types/request.ts index 6d1cfb0a2..33fe5d091 100644 --- a/sdk/src/rpc/types/request.ts +++ b/sdk/src/rpc/types/request.ts @@ -1,44 +1,40 @@ import { Address, Hex } from 'viem'; -import { RPCMethod, GenericRPCMessage, AppDefinition, RPCChannelStatus, TransferAllocation } from '.'; - -/** - * Represents the request parameters for the 'auth_challenge' RPC method. - */ -export interface AuthChallengeRequestParams { - /** The challenge message to be signed by the client for authentication. */ - challenge_message: string; -} -export type AuthChallengeRPCRequestParams = AuthChallengeRequestParams; // for backward compatibility +import { + RPCMethod, + GenericRPCMessage, + RPCAppDefinition, + RPCChannelStatus, + RPCTransferAllocation, + RPCAppSessionAllocation, + RPCAllowance, + GetLedgerTransactionsFilters, +} from '.'; /** * Represents the request structure for the 'auth_challenge' RPC method. */ export interface AuthChallengeRequest extends GenericRPCMessage { method: RPCMethod.AuthChallenge; - params: AuthChallengeRequestParams[]; + params: { + /** The challenge message to be signed by the client for authentication. */ + challenge_message: string; + }; } -/** - * Represents the request parameters for the 'auth_verify' RPC method. - * Either JWT or challenge must be provided. JWT takes precedence over challenge. - */ -export type AuthVerifyRequestParams = - | { - /** JSON Web Token for authentication. */ - jwt: string; - } - | { - /** The challenge token received from auth_challenge response. Used to verify the client's signature and prevent replay attacks. */ - challenge: string; - }; -export type AuthVerifyRPCRequestParams = AuthVerifyRequestParams; // for backward compatibility - /** * Represents the request structure for the 'auth_verify' RPC method. */ export interface AuthVerifyRequest extends GenericRPCMessage { method: RPCMethod.AuthVerify; - params: AuthVerifyRequestParams[]; + params: + | { + /** JSON Web Token for authentication. */ + jwt: string; + } + | { + /** The challenge token received from auth_challenge response. Used to verify the client's signature and prevent replay attacks. */ + challenge: string; + }; } /** @@ -46,254 +42,139 @@ export interface AuthVerifyRequest extends GenericRPCMessage { */ export interface GetConfigRequest extends GenericRPCMessage { method: RPCMethod.GetConfig; - params: []; + params: {}; } -/** - * Represents the request parameters for the 'get_ledger_balances' RPC method. - */ -export interface GetLedgerBalancesRequestParams { - /** The participant address to filter balances. */ - participant: Address; - /** Optional account ID to filter balances. If provided, overrides the participant address. */ - account_id?: string; -} -export type GetLedgerBalancesRPCRequestParams = GetLedgerBalancesRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_ledger_balances' RPC method. */ export interface GetLedgerBalancesRequest extends GenericRPCMessage { method: RPCMethod.GetLedgerBalances; - params: [GetLedgerBalancesRequestParams]; + params: { + /** The participant address to filter balances. */ + participant: Address; + /** Optional account ID to filter balances. If provided, overrides the participant address. */ + account_id?: string; + }; } -/** - * Represents the request parameters for the 'get_ledger_entries' RPC method. - */ -export interface GetLedgerEntriesRequestParams { - /** The account ID to filter ledger entries. */ - account_id?: string; - /** The asset symbol to filter ledger entries. */ - asset?: string; - /** Optional wallet address to filter ledger entries. If provided, overrides the authenticated wallet. */ - wallet?: Address; -} -export type GetLedgerEntriesRPCRequestParams = GetLedgerEntriesRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_ledger_entries' RPC method. */ export interface GetLedgerEntriesRequest extends GenericRPCMessage { method: RPCMethod.GetLedgerEntries; - params: [GetLedgerEntriesRequestParams]; -} - -/** - * Represents the request parameters for the 'get_transactions' RPC method. - */ -export enum TxType { - Transfer = 'transfer', - Deposit = 'deposit', - Withdrawal = 'withdrawal', - AppDeposit = 'app_deposit', - AppWithdrawal = 'app_withdrawal', + params: { + /** The account ID to filter ledger entries. */ + account_id?: string; + /** The asset symbol to filter ledger entries. */ + asset?: string; + /** Optional wallet address to filter ledger entries. If provided, overrides the authenticated wallet. */ + wallet?: Address; + }; } -/** - * Represents the request parameters for the 'get_transactions' RPC method. - */ -export interface GetLedgerTransactionsFilters { - /** The asset symbol to filter transactions. */ - asset?: string; - /** The transaction type to filter transactions. */ - tx_type?: TxType; - /** Pagination offset. */ - offset?: number; - /** Number of transactions to return. */ - limit?: number; - /** Sort order by created_at. */ - sort?: 'asc' | 'desc'; -} - -export interface GetLedgerTransactionsRequestParams extends GetLedgerTransactionsFilters { - /** The account ID to filter transactions. */ - account_id: string; -} -export type GetLedgerTransactionsRPCRequestParams = GetLedgerTransactionsRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_transactions' RPC method. */ export interface GetLedgerTransactionsRequest extends GenericRPCMessage { method: RPCMethod.GetLedgerTransactions; - params: GetLedgerTransactionsRequestParams; + params: GetLedgerTransactionsFilters & { + account_id: string; + }; } -/** - * Represents the request parameters for the 'get_user_tag' RPC method. - */ -export interface GetUserTagRequestParams { - // This method takes no parameters - empty object -} -export type GetUserTagRPCRequestParams = GetUserTagRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_user_tag' RPC method. */ export interface GetUserTagRequest extends GenericRPCMessage { method: RPCMethod.GetUserTag; - params: []; + params: {}; } -/** Represents the allocation of assets within an application session. - * This structure is used to define allocation of assets among participants. - * It includes the participant's address, the asset (usdc, usdt, etc) being allocated, and the amount. - */ -export type AppSessionAllocation = { - /** The Ethereum address of the participant receiving the allocation. */ - participant: Address; - /** The symbol of the asset being allocated (e.g., "USDC", "USDT"). */ - asset: string; - /** The amount of the asset being allocated. Must be a positive number. */ - amount: string; -}; - -/** - * Represents the request parameters for the 'create_app_session' RPC method. - */ -export interface CreateAppSessionRequestParams { - /** The detailed definition of the application being created, including protocol, participants, weights, and quorum. */ - definition: AppDefinition; - /** The initial allocation distribution among participants. Each participant must have sufficient balance for their allocation. */ - allocations: AppSessionAllocation[]; - /** Optional session data as a JSON string that can store application-specific state or metadata. */ - session_data?: string; -} -export type CreateAppSessionRPCRequestParams = CreateAppSessionRequestParams; // for backward compatibility - /** * Represents the request structure for the 'create_app_session' RPC method. */ export interface CreateAppSessionRequest extends GenericRPCMessage { method: RPCMethod.CreateAppSession; - params: [CreateAppSessionRequestParams]; -} - -/** - * Represents the request parameters for the 'submit_app_state' RPC method. - */ -export interface SubmitAppStateRequestParams { - /** The unique identifier of the application session to update. */ - app_session_id: Hex; - /** The new allocation distribution among participants. Must include all participants and maintain total balance. */ - allocations: AppSessionAllocation[]; - /** Optional session data as a JSON string that can store application-specific state or metadata. */ - session_data?: string; + params: { + /** The detailed definition of the application being created, including protocol, participants, weights, and quorum. */ + definition: RPCAppDefinition; + /** The initial allocation distribution among participants. Each participant must have sufficient balance for their allocation. */ + allocations: RPCAppSessionAllocation[]; + /** Optional session data as a JSON string that can store application-specific state or metadata. */ + session_data?: string; + }; } -export type SubmitAppStateRPCRequestParams = SubmitAppStateRequestParams; // for backward compatibility /** * Represents the request structure for the 'submit_app_state' RPC method. */ export interface SubmitAppStateRequest extends GenericRPCMessage { method: RPCMethod.SubmitAppState; - params: [SubmitAppStateRequestParams]; + params: { + /** The unique identifier of the application session to update. */ + app_session_id: Hex; + /** The new allocation distribution among participants. Must include all participants and maintain total balance. */ + allocations: RPCAppSessionAllocation[]; + /** Optional session data as a JSON string that can store application-specific state or metadata. */ + session_data?: string; + }; } -/** - * Represents the request parameters for the 'close_app_session' RPC method. - */ -export interface CloseAppSessionRequestParams { - /** The unique identifier of the application session to close. */ - app_session_id: Hex; - /** The final allocation distribution among participants upon closing. Must include all participants and maintain total balance. */ - allocations: AppSessionAllocation[]; - /** Optional session data as a JSON string that can store application-specific state or metadata. */ - session_data?: string; -} -export type CloseAppSessionRPCRequestParams = CloseAppSessionRequestParams; // for backward compatibility - /** * Represents the request structure for the 'close_app_session' RPC method. */ export interface CloseAppSessionRequest extends GenericRPCMessage { method: RPCMethod.CloseAppSession; - params: [CloseAppSessionRequestParams]; -} - -/** - * Represents the request parameters for the 'get_app_definition' RPC method. - */ -export interface GetAppDefinitionRequestParams { - /** The unique identifier of the application session to retrieve. */ - app_session_id: Hex; + params: { + /** The unique identifier of the application session to close. */ + app_session_id: Hex; + /** The final allocation distribution among participants upon closing. Must include all participants and maintain total balance. */ + allocations: RPCAppSessionAllocation[]; + /** Optional session data as a JSON string that can store application-specific state or metadata. */ + session_data?: string; + }; } -export type GetAppDefinitionRPCRequestParams = GetAppDefinitionRequestParams; // for backward compatibility /** * Represents the request structure for the 'get_app_definition' RPC method. */ export interface GetAppDefinitionRequest extends GenericRPCMessage { method: RPCMethod.GetAppDefinition; - params: [GetAppDefinitionRequestParams]; + params: { + /** The unique identifier of the application session to retrieve the definition for. */ + app_session_id: Hex; + }; } -/** - * Represents the request parameters for the 'get_app_sessions' RPC method. - */ -export interface GetAppSessionsRequestParams { - /** The participant address to filter application sessions. */ - participant: Address; - /** The status to filter application sessions (e.g., "open", "closed"). */ - status: RPCChannelStatus; -} -export type GetAppSessionsRPCRequestParams = GetAppSessionsRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_app_sessions' RPC method. */ export interface GetAppSessionsRequest extends GenericRPCMessage { method: RPCMethod.GetAppSessions; - params: [GetAppSessionsRequestParams]; + params: { + /** Optional, The participant address to filter application sessions. */ + participant?: Address; + /** Optional, The status to filter application sessions (e.g., "open", "closed"). */ + status?: RPCChannelStatus; + }; } -/** - * Represents the request parameters for the 'create_channel' RPC method. - */ -export interface CreateChannelRequestParams { - /** The blockchain network ID where the channel should be created. */ - chain_id: number; - /** The token contract address for the channel. */ - token: Address; - /** The initial amount to deposit in the channel (in raw token units). */ - amount: bigint; - /** Optional session key address for the channel. */ - session_key?: Hex; -} -export type CreateChannelRPCRequestParams = CreateChannelRequestParams; // for backward compatibility - -/** - * Represents the request parameters for the 'resize_channel' RPC method. - */ -export type ResizeChannelRequestParams = { - /** The unique identifier of the channel to resize. */ - channel_id: Hex; - /** Amount to resize the channel by (can be positive or negative). Required if allocate_amount is not provided. */ - resize_amount?: bigint; - /** Amount to allocate from the unified balance to the channel. Required if resize_amount is not provided. */ - allocate_amount?: bigint; - /** The address where the resized funds will be sent. */ - funds_destination: Address; -}; -export type ResizeChannelRPCRequestParams = ResizeChannelRequestParams; // for backward compatibility - /** * Represents the request structure for the 'create_channel' RPC method. */ export interface CreateChannelRequest extends GenericRPCMessage { method: RPCMethod.CreateChannel; - params: [CreateChannelRequestParams]; + params: { + /** The blockchain network ID where the channel should be created. */ + chain_id: number; + /** The token contract address for the channel. */ + token: Address; + /** The initial amount to deposit in the channel (in raw token units). */ + amount: bigint; + /** Optional session key address for the channel. */ + session_key?: Hex; + }; } /** @@ -301,45 +182,42 @@ export interface CreateChannelRequest extends GenericRPCMessage { */ export interface ResizeChannelRequest extends GenericRPCMessage { method: RPCMethod.ResizeChannel; - params: [ResizeChannelRequestParams]; -} - -/** - * Represents the request parameters for the 'close_channel' RPC method. - */ -export interface CloseChannelRequestParams { - /** The unique identifier of the channel to close. */ - channel_id: Hex; - /** The address where the channel funds will be sent upon closing. */ - funds_destination: Address; + params: { + /** The unique identifier of the channel to resize. */ + channel_id: Hex; + /** Amount to resize the channel by (can be positive or negative). Required if allocate_amount is not provided. */ + resize_amount?: bigint; + /** Amount to allocate from the unified balance to the channel. Required if resize_amount is not provided. */ + allocate_amount?: bigint; + /** The address where the resized funds will be sent. */ + funds_destination: Address; + }; } -export type CloseChannelRPCRequestParams = CloseChannelRequestParams; // for backward compatibility /** * Represents the request structure for the 'close_channel' RPC method. */ export interface CloseChannelRequest extends GenericRPCMessage { method: RPCMethod.CloseChannel; - params: [CloseChannelRequestParams]; -} - -/** - * Represents the request parameters for the 'get_channels' RPC method. - */ -export interface GetChannelsRequestParams { - /** The participant address to filter channels. */ - participant: Address; - /** The status to filter channels (e.g., "open", "closed"). */ - status: RPCChannelStatus; + params: { + /** The unique identifier of the channel to close. */ + channel_id: Hex; + /** The address where the channel funds will be sent upon closing. */ + funds_destination: Address; + }; } -export type GetChannelsRPCRequestParams = GetChannelsRequestParams; // for backward compatibility /** * Represents the request structure for the 'get_channels' RPC method. */ export interface GetChannelsRequest extends GenericRPCMessage { method: RPCMethod.GetChannels; - params: [GetChannelsRequestParams]; + params: { + /** Optional, The participant address to filter channels. */ + participant?: Address; + /** Optional, The status to filter channels (e.g., "open", "closed"). */ + status?: RPCChannelStatus; + }; } /** @@ -347,63 +225,41 @@ export interface GetChannelsRequest extends GenericRPCMessage { */ export interface GetRPCHistoryRequest extends GenericRPCMessage { method: RPCMethod.GetRPCHistory; - params: []; + params: {}; } -/** - * Represents the request parameters for the 'get_assets' RPC method. - */ -export interface GetAssetsRequestParams { - /** Optional chain ID to filter assets by network. If not provided, returns assets from all networks. */ - chain_id?: number; -} -export type GetAssetsRPCRequestParams = GetAssetsRequestParams; // for backward compatibility - /** * Represents the request structure for the 'get_assets' RPC method. */ export interface GetAssetsRequest extends GenericRPCMessage { method: RPCMethod.GetAssets; - params: [GetAssetsRequestParams]; + params: { + /** Optional chain ID to filter assets by network. If not provided, returns assets from all networks. */ + chain_id?: number; + }; } -/** Represents a single allowance for an asset, used in application sessions. - * This structure defines the symbol of the asset and the amount that is allowed to be spent. - */ -export type Allowance = { - /** The symbol of the asset (e.g., "USDC", "USDT"). */ - asset: string; - /** The amount of the asset that is allowed to be spent. */ - amount: string; -}; - -/** - * Represents the request parameters for the 'auth_request' RPC method. - */ -export interface AuthRequestParams { - /** The Ethereum address of the wallet being authorized. */ - wallet: Address; - /** The session key address associated with the authentication attempt. */ - participant: Address; - /** The name of the application being authorized. */ - app_name: string; - /** The allowances for the connection. */ - allowances: Allowance[]; - /** The expiration timestamp for the authorization. */ - expire: string; - /** The scope of the authorization. */ - scope: string; - /** The application address being authorized. */ - application: Address; -} -export type AuthRequestRPCRequestParams = AuthRequestParams; // for backward compatibility - /** * Represents the request structure for the 'auth_request' RPC method. */ export interface AuthRequest extends GenericRPCMessage { method: RPCMethod.AuthRequest; - params: AuthRequestParams[]; + params: { + /** The Ethereum address of the wallet being authorized. */ + address: Address; + /** The session key address associated with the authentication attempt. */ + session_key: Address; + /** The name of the application being authorized. */ + app_name: string; + /** The allowances for the connection. */ + allowances: RPCAllowance[]; + /** The expiration timestamp for the authorization. */ + expire: string; + /** The scope of the authorization. */ + scope: string; + /** The application address being authorized. */ + application: Address; + }; } /** @@ -412,7 +268,7 @@ export interface AuthRequest extends GenericRPCMessage { export interface MessageRequest extends GenericRPCMessage { method: RPCMethod.Message; /** The message parameters are handled by the virtual application */ - params: any[]; + params: any; } /** @@ -420,8 +276,7 @@ export interface MessageRequest extends GenericRPCMessage { */ export interface PingRequest extends GenericRPCMessage { method: RPCMethod.Ping; - /** No parameters needed for ping */ - params: []; + params: {}; } /** @@ -429,20 +284,7 @@ export interface PingRequest extends GenericRPCMessage { */ export interface PongRequest extends GenericRPCMessage { method: RPCMethod.Pong; - /** No parameters needed for pong */ - params: []; -} - -/** - * Represents the request parameters for the 'transfer' RPC method. - */ -export interface TransferRequestParams { - /** The destination address to transfer assets to. Required if destination_user_tag is not provided. */ - destination?: Address; - /** The destination user tag to transfer assets to. Required if destination is not provided. */ - destination_user_tag?: string; - /** The assets and amounts to transfer. */ - allocations: TransferAllocation[]; + params: {}; } /** @@ -450,9 +292,129 @@ export interface TransferRequestParams { */ export interface TransferRequest extends GenericRPCMessage { method: RPCMethod.Transfer; - params: TransferRequestParams; + params: { + /** The destination address to transfer assets to. Required if destination_user_tag is not provided. */ + destination?: Address; + /** The destination user tag to transfer assets to. Required if destination is not provided. */ + destination_user_tag?: string; + /** The assets and amounts to transfer. */ + allocations: RPCTransferAllocation[]; + }; } -export type TransferRPCRequestParams = TransferRequestParams; // for backward compatibility + +/** Represents the request parameters for the 'auth_challenge' RPC method. */ +export type AuthChallengeRequestParams = AuthChallengeRequest['params']; + +/** + * Represents the request parameters for the 'auth_verify' RPC method. + * Either JWT or challenge must be provided. JWT takes precedence over challenge. + */ +export type AuthVerifyRequestParams = AuthVerifyRequest['params']; + +/** + * Represents the request parameters for the 'get_config' RPC method. + */ +export type GetConfigRequestParams = GetConfigRequest['params']; + +/** + * Represents the request parameters for the 'get_ledger_balances' RPC method. + */ +export type GetLedgerBalancesRequestParams = GetLedgerBalancesRequest['params']; + +/** + * Represents the request parameters for the 'get_ledger_entries' RPC method. + */ +export type GetLedgerEntriesRequestParams = GetLedgerEntriesRequest['params']; + +/** + * Represents the request parameters for the 'get_ledger_transactions' RPC method. + */ +export type GetLedgerTransactionsRequestParams = GetLedgerTransactionsRequest['params']; + +/** + * Represents the request parameters for the 'get_user_tag' RPC method. + */ +export type GetUserTagRequestParams = GetUserTagRequest['params']; + +/** + * Represents the request parameters for the 'create_app_session' RPC method. + */ +export type CreateAppSessionRequestParams = CreateAppSessionRequest['params']; + +/** + * Represents the request parameters for the 'submit_app_state' RPC method. + */ +export type SubmitAppStateRequestParams = SubmitAppStateRequest['params']; + +/** + * Represents the request parameters for the 'close_app_session' RPC method. + */ +export type CloseAppSessionRequestParams = CloseAppSessionRequest['params']; + +/** + * Represents the request parameters for the 'get_app_definition' RPC method. + */ +export type GetAppDefinitionRequestParams = GetAppDefinitionRequest['params']; + +/** + * Represents the request parameters for the 'get_app_sessions' RPC method. + */ +export type GetAppSessionsRequestParams = GetAppSessionsRequest['params']; + +/** + * Represents the request parameters for the 'create_channel' RPC method. + */ +export type CreateChannelRequestParams = CreateChannelRequest['params']; + +/** + * Represents the request parameters for the 'resize_channel' RPC method. + */ +export type ResizeChannelRequestParams = ResizeChannelRequest['params']; + +/** + * Represents the request parameters for the 'close_channel' RPC method. + */ +export type CloseChannelRequestParams = CloseChannelRequest['params']; + +/** + * Represents the request parameters for the 'get_channels' RPC method. + */ +export type GetChannelsRequestParams = GetChannelsRequest['params']; + +/** + * Represents the request parameters for the 'get_rpc_history' RPC method. + */ +export type GetRPCHistoryParams = GetRPCHistoryRequest['params']; + +/** + * Represents the request parameters for the 'get_assets' RPC method. + */ +export type GetAssetsRequestParams = GetAssetsRequest['params']; + +/** + * Represents the request parameters for the 'auth_request' RPC method. + */ +export type AuthRequestParams = AuthRequest['params']; + +/** + * Represents the request parameters for the 'message' RPC method. + */ +export type MessageRequestParams = MessageRequest['params']; + +/** + * Represents the request parameters for the 'ping' RPC method. + */ +export type PingRequestParams = PingRequest['params']; + +/** + * Represents the request parameters for the 'pong' RPC method. + */ +export type PongRequestParams = PongRequest['params']; + +/** + * Represents the request parameters for the 'transfer' RPC method. + */ +export type TransferRequestParams = TransferRequest['params']; /** * Union type for all possible RPC request types. @@ -490,11 +452,11 @@ export type RPCRequestParamsByMethod = { [RPCMethod.AuthChallenge]: AuthChallengeRequestParams; [RPCMethod.AuthVerify]: AuthVerifyRequestParams; [RPCMethod.AuthRequest]: AuthRequestParams; - [RPCMethod.GetConfig]: []; + [RPCMethod.GetConfig]: GetConfigRequestParams; [RPCMethod.GetLedgerBalances]: GetLedgerBalancesRequestParams; [RPCMethod.GetLedgerEntries]: GetLedgerEntriesRequestParams; [RPCMethod.GetLedgerTransactions]: GetLedgerTransactionsRequestParams; - [RPCMethod.GetUserTag]: []; + [RPCMethod.GetUserTag]: GetUserTagRequestParams; [RPCMethod.CreateAppSession]: CreateAppSessionRequestParams; [RPCMethod.SubmitAppState]: SubmitAppStateRequestParams; [RPCMethod.CloseAppSession]: CloseAppSessionRequestParams; @@ -504,10 +466,10 @@ export type RPCRequestParamsByMethod = { [RPCMethod.ResizeChannel]: ResizeChannelRequestParams; [RPCMethod.CloseChannel]: CloseChannelRequestParams; [RPCMethod.GetChannels]: GetChannelsRequestParams; - [RPCMethod.GetRPCHistory]: []; + [RPCMethod.GetRPCHistory]: GetRPCHistoryParams; [RPCMethod.GetAssets]: GetAssetsRequestParams; - [RPCMethod.Ping]: []; - [RPCMethod.Pong]: []; - [RPCMethod.Message]: any[]; + [RPCMethod.Ping]: PingRequestParams; + [RPCMethod.Pong]: PongRequestParams; + [RPCMethod.Message]: any; [RPCMethod.Transfer]: TransferRequestParams; }; diff --git a/sdk/src/rpc/types/response.ts b/sdk/src/rpc/types/response.ts index 620f1c1d3..35b55aced 100644 --- a/sdk/src/rpc/types/response.ts +++ b/sdk/src/rpc/types/response.ts @@ -2,634 +2,481 @@ import { Address, Hex } from 'viem'; import { RPCMethod, GenericRPCMessage, - AppDefinition, + RPCAppDefinition, RPCChannelStatus, - AuthVerifyRequestParams, - TransferAllocation, - ChannelUpdate, - TxType, + RPCChannelUpdate, + RPCNetworkInfo, + RPCBalance, + RPCLedgerEntry, + RPCAppSession, + RPCHistoryEntry, + RPCAsset, + RPCTransaction, + RPCChannelUpdateWithWallet, + RPCChannelOperation, + RPCChannel, } from '.'; -/** - * Represents the parameters for the 'auth_challenge' RPC method. - */ -export interface AuthChallengeResponseParams { - /** The challenge message to be signed by the client for authentication. */ - challengeMessage: string; -} -export type AuthChallengeRPCResponseParams = AuthChallengeResponseParams; // for backward compatibility - /** * Represents the response structure for the 'auth_challenge' RPC method. */ export interface AuthChallengeResponse extends GenericRPCMessage { method: RPCMethod.AuthChallenge; - params: AuthChallengeResponseParams; + params: { + /** The challenge message to be signed by the client for authentication. */ + challengeMessage: string; + }; } /** - * Represents the parameters for the 'auth_verify' RPC method. - */ -export type AuthVerifyResponseParams = { - address: Address; - sessionKey: Address; - success: boolean; -} & { - /** Available only if challenge auth method was used in {@link AuthVerifyRequestParams} during the call to {@link RPCMethod.AuthRequest} */ - jwtToken: string; -}; -export type AuthVerifyRPCResponseParams = AuthVerifyResponseParams; // for backward compatibility - -/** - * Represents the parameters for the 'error' RPC method. + * Represents the response structure for an error response. */ -export interface ErrorResponseParams { - /** The error message describing what went wrong. */ - error: string; +export interface ErrorResponse extends GenericRPCMessage { + method: RPCMethod.Error; + params: { + /** The error message describing what went wrong. */ + error: string; + }; } -export type ErrorRPCResponseParams = ErrorResponseParams; // for backward compatibility /** - * Represents the network information for the 'get_config' RPC method. + * Represents the response structure for the 'get_config' RPC method. */ -export interface NetworkInfo { - /** The name of the network (e.g., "Ethereum", "Polygon"). */ - name: string; - /** The chain ID of the network. */ - chainId: number; - /** The custody contract address for the network. */ - custodyAddress: Address; - /** The adjudicator contract address for the network. */ - adjudicatorAddress: Address; +export interface GetConfigResponse extends GenericRPCMessage { + method: RPCMethod.GetConfig; + params: { + /** The Ethereum address of the broker. */ + brokerAddress: Address; + /** List of supported networks and their configurations. */ + networks: RPCNetworkInfo[]; + }; } /** - * Represents the parameters for the 'get_config' RPC method. + * Represents the response structure for the 'get_ledger_balances' RPC method. */ -export interface GetConfigResponseParams { - /** The Ethereum address of the broker. */ - brokerAddress: Address; - /** List of supported networks and their configurations. */ - networks: NetworkInfo[]; +export interface GetLedgerBalancesResponse extends GenericRPCMessage { + method: RPCMethod.GetLedgerBalances; + params: { + /** List of balances for each asset in the ledger. */ + ledgerBalances: RPCBalance[]; + }; } -export type GetConfigRPCResponseParams = GetConfigResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_ledger_balances' RPC method. + * Represents the response structure for the 'get_ledger_entries' RPC method. */ -export interface GetLedgerBalancesResponseParams { - /** The asset symbol (e.g., "ETH", "USDC"). */ - asset: string; - /** The balance amount. */ - amount: string; +export interface GetLedgerEntriesResponse extends GenericRPCMessage { + method: RPCMethod.GetLedgerEntries; + params: { + /** List of ledger entries containing transaction details. */ + ledgerEntries: RPCLedgerEntry[]; + }; } -export type GetLedgerBalancesRPCResponseParams = GetLedgerBalancesResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_ledger_entries' RPC method. + * Represents the response structure for the 'get_transactions' RPC method. */ -export interface GetLedgerEntriesResponseParams { - /** Unique identifier for the ledger entry. */ - id: number; - /** The account identifier associated with the entry. */ - accountId: string; - /** The type of account (e.g., "wallet", "channel"). */ - accountType: number; - /** The asset symbol for the entry. */ - asset: string; - /** The Ethereum address of the participant. */ - participant: Address; - /** The credit amount. */ - credit: string; - /** The debit amount. */ - debit: string; - /** The timestamp when the entry was created. */ - createdAt: Date; +export interface GetLedgerTransactionsResponse extends GenericRPCMessage { + method: RPCMethod.GetLedgerTransactions; + params: { + /** List of transactions in the ledger. */ + ledgerTransactions: RPCTransaction[]; + }; } -export type GetLedgerEntriesRPCResponseParams = GetLedgerEntriesResponseParams; // for backward compatibility - -export type GetLedgerTransactionsResponseParams = Transaction[]; /** - * Represents the parameters for the 'get_user_tag' RPC method. + * Represents the response structure for the 'get_user_tag' RPC method. */ -export interface UserTagParams { - /** The user's unique tag identifier. */ - tag: string; +export interface GetUserTagResponse extends GenericRPCMessage { + method: RPCMethod.GetUserTag; + params: { + /** The user's unique tag identifier. */ + tag: string; + }; } -export type GetUserTagRPCResponseParams = UserTagParams; // for backward compatibility /** - * Represents the parameters for the 'create_app_session' RPC method. + * Represents the response structure for the 'create_app_session' RPC method. */ -export interface CreateAppSessionResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The version number of the session. */ - version: number; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; +export interface CreateAppSessionResponse extends GenericRPCMessage { + method: RPCMethod.CreateAppSession; + params: { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The version number of the session. */ + version: number; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + }; } -export type CreateAppSessionRPCResponseParams = CreateAppSessionResponseParams; // for backward compatibility /** - * Represents the parameters for the 'submit_app_state' RPC method. + * Represents the response structure for the 'submit_app_state' RPC method. */ -export interface SubmitAppStateResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The version number of the session. */ - version: number; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; +export interface SubmitAppStateResponse extends GenericRPCMessage { + method: RPCMethod.SubmitAppState; + params: { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The version number of the session. */ + version: number; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + }; } -export type SubmitAppStateRPCResponseParams = SubmitAppStateResponseParams; // for backward compatibility /** - * Represents the parameters for the 'close_app_session' RPC method. + * Represents the response structure for the 'close_app_session' RPC method. */ -export interface CloseAppSessionResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The version number of the session. */ - version: number; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; +export interface CloseAppSessionResponse extends GenericRPCMessage { + method: RPCMethod.CloseAppSession; + params: { + /** The unique identifier for the application session. */ + appSessionId: Hex; + /** The version number of the session. */ + version: number; + /** The current status of the channel (e.g., "open", "closed"). */ + status: RPCChannelStatus; + }; } -export type CloseAppSessionRPCResponseParams = CloseAppSessionResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_app_definition' RPC method. + * Represents the response structure for the 'get_app_definition' RPC method. */ -export interface GetAppDefinitionResponseParams extends AppDefinition { - /** The protocol identifier for the application (e.g., "payment", "swap"). */ - protocol: string; - /** List of Ethereum addresses of participants in the application session. */ - participants: Address[]; - /** Array of signature weights for each participant, used for quorum calculations. */ - weights: number[]; - /** The minimum number of signatures required for state updates. */ - quorum: number; - /** The challenge period in seconds for state updates. */ - challenge: number; - /** A unique nonce value for the application session to prevent replay attacks. */ - nonce: number; +export interface GetAppDefinitionResponse extends GenericRPCMessage { + method: RPCMethod.GetAppDefinition; + params: RPCAppDefinition & { + /** A unique nonce value for the application session to prevent replay attacks. */ + nonce: number; + }; } -export type GetAppDefinitionRPCResponseParams = GetAppDefinitionResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_app_sessions' RPC method. + * Represents the response structure for the 'get_app_sessions' RPC method. */ -export interface GetAppSessionsResponseParams { - /** The unique identifier for the application session. */ - appSessionId: Hex; - /** The current status of the channel (e.g., "open", "closed"). */ - status: RPCChannelStatus; - /** List of participant Ethereum addresses. */ - participants: Address[]; - /** The protocol identifier for the application. */ - protocol: string; - /** The challenge period in seconds. */ - challenge: number; - /** The signature weights for each participant. */ - weights: number[]; - /** The minimum number of signatures required for state updates. */ - quorum: number; - /** The version number of the session. */ - version: number; - /** The nonce value for the session. */ - nonce: number; - /** The timestamp when the session was created. */ - createdAt: Date; - /** The timestamp when the session was last updated. */ - updatedAt: Date; - /** Optional session data as a JSON string that stores application-specific state or metadata. */ - sessionData?: string; -} -export type GetAppSessionsRPCResponseParams = GetAppSessionsResponseParams; // for backward compatibility - -export type ServerSignature = Hex; - -export interface RPCAllocation { - /** The destination address for the allocation. */ - destination: Address; - /** The token contract address. */ - token: Address; - /** The amount to allocate. */ - amount: bigint; +export interface GetAppSessionsResponse extends GenericRPCMessage { + method: RPCMethod.GetAppSessions; + params: { + appSessions: RPCAppSession[]; + }; } /** - * Represents the state object within channel operation responses. + * Represents the response structure for the 'create_channel' RPC method. */ -export interface ChannelOperationState { - /** The intent type for the state update. */ - intent: number; - /** The version number of the channel. */ - version: number; - /** The encoded state data for the channel. */ - stateData: Hex; - /** The list of allocations for the channel. */ - allocations: RPCAllocation[]; +export interface CreateChannelResponse extends GenericRPCMessage { + method: RPCMethod.CreateChannel; + params: RPCChannelOperation & { + channel: RPCChannel; + }; } /** - * Represents the fixed part of a channel, containing essential metadata. + * Represents the response structure for the 'resize_channel' RPC method. */ -export interface RPCChannel { - participants: Address[]; - adjudicator: Address; - challenge: number; - nonce: number; +export interface ResizeChannelResponse extends GenericRPCMessage { + method: RPCMethod.ResizeChannel; + params: RPCChannelOperation; } /** - * Represents the unified parameters for channel operations (create, resize, close). + * Represents the response structure for the 'close_channel' RPC method. */ -export interface ChannelOperationResponseParams { - /** The unique identifier for the channel. */ - channelId: Hex; - /** The channel state object. */ - state: ChannelOperationState; - /** The server's signature for the state update. */ - serverSignature: ServerSignature; +export interface CloseChannelResponse extends GenericRPCMessage { + method: RPCMethod.CloseChannel; + params: RPCChannelOperation; } /** - * Represents the parameters for the 'create_channel' RPC method. + * Represents the response structure for the 'get_channels' RPC method. */ -export interface CreateChannelResponseParams extends ChannelOperationResponseParams { - channel: RPCChannel; +export interface GetChannelsResponse extends GenericRPCMessage { + method: RPCMethod.GetChannels; + params: { + /** List of channel updates containing information about each channel. */ + channels: RPCChannelUpdateWithWallet[]; + }; } -export type CreateChannelRPCResponseParams = CreateChannelResponseParams; // for backward compatibility /** - * Represents the parameters for the 'resize_channel' RPC method. + * Represents the response structure for the 'get_rpc_history' RPC method. */ -export interface ResizeChannelResponseParams extends ChannelOperationResponseParams {} -export type ResizeChannelRPCResponseParams = ResizeChannelResponseParams; // for backward compatibility +export interface GetRPCHistoryResponse extends GenericRPCMessage { + method: RPCMethod.GetRPCHistory; + params: { + /** List of RPC entries containing historical RPC calls and their responses. */ + rpcEntries: RPCHistoryEntry[]; + }; +} /** - * Represents the parameters for the 'close_channel' RPC method. + * Represents the response structure for the 'get_assets' RPC method. */ -export interface CloseChannelResponseParams extends ChannelOperationResponseParams {} -export type CloseChannelRPCResponseParams = CloseChannelResponseParams; // for backward compatibility +export interface GetAssetsResponse extends GenericRPCMessage { + method: RPCMethod.GetAssets; + params: { + /** List of assets available in the clearnode. */ + assets: RPCAsset[]; + }; +} /** - * Represents the parameters for the 'get_channels' RPC method. + * Represents the response structure for the 'assets' RPC method. */ -export type GetChannelsResponseParams = ChannelUpdate[]; -export type GetChannelsRPCResponseParams = GetChannelsResponseParams; // for backward compatibility +export interface AssetsResponse extends GenericRPCMessage { + method: RPCMethod.Assets; + params: { + /** List of assets available in the clearnode. */ + assets: RPCAsset[]; + }; +} /** - * Represents the parameters for the 'get_rpc_history' RPC method. + * Represents the response structure for the 'auth_verify' RPC method. */ -export interface GetRPCHistoryResponseParams { - /** Unique identifier for the RPC entry. */ - id: number; - /** The Ethereum address of the sender. */ - sender: Address; - /** The request ID for the RPC call. */ - reqId: number; - /** The RPC method name. */ - method: string; - /** The JSON string of the request parameters. */ - params: string; - /** The timestamp of the RPC call. */ - timestamp: number; - /** Array of request signatures. */ - reqSig: Hex[]; - /** Array of response signatures. */ - resSig: Hex[]; - /** The JSON string of the response. */ - response: string; +export interface AuthVerifyResponse extends GenericRPCMessage { + method: RPCMethod.AuthVerify; + params: { + address: Address; + sessionKey: Address; + success: boolean; + /** Available only if challenge auth method was used in {@link AuthVerifyRequest} during the call to {@link RPCMethod.AuthRequest} */ + jwtToken?: string; + }; } -export type GetRPCHistoryRPCResponseParams = GetRPCHistoryResponseParams; // for backward compatibility /** - * Represents the parameters for the 'get_assets' RPC method. + * Represents the response structure for the 'auth_request' RPC method. */ -export interface GetAssetsResponseParams { - /** The token contract address. */ - token: Address; - /** The chain ID where the asset exists. */ - chainId: number; - /** The asset symbol (e.g., "ETH", "USDC"). */ - symbol: string; - /** The number of decimal places for the asset. */ - decimals: number; +export interface AuthRequestResponse extends GenericRPCMessage { + method: RPCMethod.AuthRequest; + params: { + /** The challenge message to be signed by the client for authentication. */ + challengeMessage: string; + }; } -export type GetAssetsRPCResponseParams = GetAssetsResponseParams; // for backward compatibility /** - * Represents the response structure for an error response. + * Represents the response structure for the 'message' RPC method. */ -export interface ErrorResponse extends GenericRPCMessage { - method: RPCMethod.Error; - params: ErrorResponseParams; +export interface MessageResponse extends GenericRPCMessage { + method: RPCMethod.Message; + params: {}; } /** - * Represents the response structure for the 'get_config' RPC method. + * Represents the response structure for the 'bu' RPC method. */ -export interface GetConfigResponse extends GenericRPCMessage { - method: RPCMethod.GetConfig; - params: GetConfigResponseParams; +export interface BalanceUpdateResponse extends GenericRPCMessage { + method: RPCMethod.BalanceUpdate; + params: { + /** List of balance updates. */ + balanceUpdates: RPCBalance[]; + }; } /** - * Represents the response structure for the 'get_ledger_balances' RPC method. + * Represents the response structure for the 'channels' RPC method. */ -export interface GetLedgerBalancesResponse extends GenericRPCMessage { - method: RPCMethod.GetLedgerBalances; - params: GetLedgerBalancesResponseParams[]; +export interface ChannelsUpdateResponse extends GenericRPCMessage { + method: RPCMethod.ChannelsUpdate; + params: { + /** List of channel updates. */ + channels: RPCChannelUpdate[]; + }; } /** - * Represents the response structure for the 'get_ledger_entries' RPC method. + * Represents the response structure for the 'cu' RPC method. */ -export interface GetLedgerEntriesResponse extends GenericRPCMessage { - method: RPCMethod.GetLedgerEntries; - params: GetLedgerEntriesResponseParams[]; +export interface ChannelUpdateResponse extends GenericRPCMessage { + method: RPCMethod.ChannelUpdate; + params: RPCChannelUpdate; } /** - * Represents the response structure for the 'get_transactions' RPC method. + * Represents the response structure for the 'ping' RPC method. */ -export interface GetLedgerTransactionsResponse extends GenericRPCMessage { - method: RPCMethod.GetLedgerTransactions; - params: GetLedgerTransactionsResponseParams; +export interface PingResponse extends GenericRPCMessage { + method: RPCMethod.Ping; + params: {}; } /** - * Represents the response structure for the 'get_user_tag' RPC method. + * Represents the response structure for the 'pong' RPC method. */ -export interface GetUserTagResponse extends GenericRPCMessage { - method: RPCMethod.GetUserTag; - params: UserTagParams; +export interface PongResponse extends GenericRPCMessage { + method: RPCMethod.Pong; + params: {}; } /** - * Represents the response structure for the 'create_app_session' RPC method. + * Represents the response structure for the 'transfer' RPC method. */ -export interface CreateAppSessionResponse extends GenericRPCMessage { - method: RPCMethod.CreateAppSession; - params: CreateAppSessionResponseParams; +export interface TransferResponse extends GenericRPCMessage { + method: RPCMethod.Transfer; + params: { + /** List of transactions representing transfers. */ + transactions: RPCTransaction[]; + }; } /** - * Represents the response structure for the 'submit_app_state' RPC method. + * Represents the response structure for the 'transfer_notification' RPC method. */ -export interface SubmitAppStateResponse extends GenericRPCMessage { - method: RPCMethod.SubmitAppState; - params: SubmitAppStateResponseParams; +export interface TransferNotificationResponse extends GenericRPCMessage { + method: RPCMethod.TransferNotification; + params: { + /** List of transactions representing transfers. */ + transactions: RPCTransaction[]; + }; } /** - * Represents the response structure for the 'close_app_session' RPC method. + * Represents the parameters for the 'auth_challenge' RPC method. */ -export interface CloseAppSessionResponse extends GenericRPCMessage { - method: RPCMethod.CloseAppSession; - params: CloseAppSessionResponseParams; -} +export type AuthChallengeResponseParams = AuthChallengeResponse['params']; /** - * Represents the response structure for the 'get_app_definition' RPC method. + * Represents the parameters for the 'auth_verify' RPC method. */ -export interface GetAppDefinitionResponse extends GenericRPCMessage { - method: RPCMethod.GetAppDefinition; - params: GetAppDefinitionResponseParams; -} +export type AuthVerifyResponseParams = AuthVerifyResponse['params']; /** - * Represents the response structure for the 'get_app_sessions' RPC method. + * Represents the parameters for the 'error' RPC method. */ -export interface GetAppSessionsResponse extends GenericRPCMessage { - method: RPCMethod.GetAppSessions; - params: GetAppSessionsResponseParams[]; -} +export type ErrorResponseParams = ErrorResponse['params']; /** - * Represents the response structure for the 'create_channel' RPC method. + * Represents the parameters for the 'get_config' RPC method. */ -export interface CreateChannelResponse extends GenericRPCMessage { - method: RPCMethod.CreateChannel; - params: CreateChannelResponseParams; -} +export type GetConfigResponseParams = GetConfigResponse['params']; /** - * Represents the response structure for the 'resize_channel' RPC method. + * Represents the parameters for the 'get_ledger_balances' RPC method. */ -export interface ResizeChannelResponse extends GenericRPCMessage { - method: RPCMethod.ResizeChannel; - params: ResizeChannelResponseParams; -} +export type GetLedgerBalancesResponseParams = GetLedgerBalancesResponse['params']; /** - * Represents the response structure for the 'close_channel' RPC method. + * Represents the parameters for the 'get_ledger_entries' RPC method. */ -export interface CloseChannelResponse extends GenericRPCMessage { - method: RPCMethod.CloseChannel; - params: CloseChannelResponseParams; -} +export type GetLedgerEntriesResponseParams = GetLedgerEntriesResponse['params']; /** - * Represents the response structure for the 'get_channels' RPC method. + * Represents the parameters for the 'get_ledger_transactions' RPC method. */ -export interface GetChannelsResponse extends GenericRPCMessage { - method: RPCMethod.GetChannels; - params: GetChannelsResponseParams; -} +export type GetLedgerTransactionsResponseParams = GetLedgerTransactionsResponse['params']; /** - * Represents the response structure for the 'get_rpc_history' RPC method. + * Represents the parameters for the 'get_user_tag' RPC method. */ -export interface GetRPCHistoryResponse extends GenericRPCMessage { - method: RPCMethod.GetRPCHistory; - params: GetRPCHistoryResponseParams[]; -} +export type GetUserTagResponseParams = GetUserTagResponse['params']; /** - * Represents the response structure for the 'get_assets' RPC method. + * Represents the parameters for the 'create_app_session' RPC method. */ -export interface GetAssetsResponse extends GenericRPCMessage { - method: RPCMethod.GetAssets; - params: GetAssetsResponseParams[]; -} - -export interface AssetsResponse extends GenericRPCMessage { - method: RPCMethod.Assets; - params: GetAssetsResponseParams[]; -} +export type CreateAppSessionResponseParams = CreateAppSessionResponse['params']; /** - * Represents the response structure for the 'auth_verify' RPC method. + * Represents the parameters for the 'submit_app_state' RPC method. */ -export interface AuthVerifyResponse extends GenericRPCMessage { - method: RPCMethod.AuthVerify; - params: AuthVerifyResponseParams; -} +export type SubmitAppStateResponseParams = SubmitAppStateResponse['params']; /** - * Represents the parameters for the 'auth_request' RPC method. + * Represents the parameters for the 'close_app_session' RPC method. */ -export interface AuthRequestResponseParams { - /** The challenge message to be signed by the client for authentication. */ - challengeMessage: string; -} -export type AuthRequestRPCResponseParams = AuthRequestResponseParams; // for backward compatibility +export type CloseAppSessionResponseParams = CloseAppSessionResponse['params']; /** - * Represents the response structure for the 'auth_request' RPC method. + * Represents the parameters for the 'get_app_definition' RPC method. */ -export interface AuthRequestResponse extends GenericRPCMessage { - method: RPCMethod.AuthRequest; - params: AuthRequestResponseParams; -} +export type GetAppDefinitionResponseParams = GetAppDefinitionResponse['params']; /** - * Represents the response parameters for the 'message' RPC method. + * Represents the parameters for the 'get_app_sessions' RPC method. */ -export interface MessageResponseParams { - // Message response parameters are handled by the application -} -export type MessageRPCResponseParams = MessageResponseParams; // for backward compatibility +export type GetAppSessionsResponseParams = GetAppSessionsResponse['params']; /** - * Represents the response structure for the 'message' RPC method. + * Represents the parameters for the 'create_channel' RPC method. */ -export interface MessageResponse extends GenericRPCMessage { - method: RPCMethod.Message; - params: MessageResponseParams; -} +export type CreateChannelResponseParams = CreateChannelResponse['params']; /** - * Represents the parameters for the 'bu' RPC method. + * Represents the parameters for the 'resize_channel' RPC method. */ -export interface BalanceUpdateResponseParams { - /** The asset symbol (e.g., "ETH", "USDC"). */ - asset: string; - /** The balance amount. */ - amount: string; -} -export type BalanceUpdateRPCResponseParams = BalanceUpdateResponseParams; // for backward compatibility +export type ResizeChannelResponseParams = ResizeChannelResponse['params']; /** - * Represents the response structure for the 'bu' RPC method. + * Represents the parameters for the 'close_channel' RPC method. */ -export interface BalanceUpdateResponse extends GenericRPCMessage { - method: RPCMethod.BalanceUpdate; - params: BalanceUpdateResponseParams[]; -} +export type CloseChannelResponseParams = CloseChannelResponse['params']; /** - * Represents the parameters for the 'channels' RPC method. + * Represents the parameters for the 'get_channels' RPC method. */ -export type ChannelsUpdateResponseParams = ChannelUpdate; +export type GetChannelsResponseParams = GetChannelsResponse['params']; /** - * Represents the response structure for the 'channels_update' RPC method. + * Represents the parameters for the 'get_rpc_history' RPC method. */ -export interface ChannelsUpdateResponse extends GenericRPCMessage { - method: RPCMethod.ChannelsUpdate; - params: ChannelsUpdateResponseParams; -} +export type GetRPCHistoryResponseParams = GetRPCHistoryResponse['params']; /** - * Represents the parameters for the 'cu' RPC method. + * Represents the parameters for the 'get_assets' RPC method. */ -export type ChannelUpdateResponseParams = ChannelUpdate; -export type ChannelUpdateRPCResponseParams = ChannelUpdateResponseParams; // for backward compatibility +export type GetAssetsResponseParams = GetAssetsResponse['params']; /** - * Represents the response structure for the 'cu' RPC method. + * Represents the parameters for the 'assets' RPC method. */ -export interface ChannelUpdateResponse extends GenericRPCMessage { - method: RPCMethod.ChannelUpdate; - params: ChannelUpdateResponseParams; -} +export type AssetsResponseParams = AssetsResponse['params']; /** - * Represents the parameters for the 'ping' RPC method. + * Represents the parameters for the 'auth_request' RPC method. */ -export interface PingResponseParams { - // No parameters needed for ping -} -export type PingRPCResponseParams = PingResponseParams; // for backward compatibility +export type AuthRequestResponseParams = AuthRequestResponse['params']; /** - * Represents the response structure for the 'ping' RPC method. + * Represents the parameters for the 'message' RPC method. */ -export interface PingResponse extends GenericRPCMessage { - method: RPCMethod.Ping; - params: PingResponseParams; -} +export type MessageResponseParams = MessageResponse['params']; /** - * Represents the parameters for the 'pong' RPC method. + * Represents the parameters for the 'bu' RPC method. */ -export interface PongResponseParams { - // No parameters needed for pong -} -export type PongRPCResponseParams = PongResponseParams; // for backward compatibility +export type BalanceUpdateResponseParams = BalanceUpdateResponse['params']; /** - * Represents the response structure for the 'pong' RPC method. + * Represents the parameters for the 'channels' RPC method. */ -export interface PongResponse extends GenericRPCMessage { - method: RPCMethod.Pong; - params: PongResponseParams; -} +export type ChannelsUpdateResponseParams = ChannelsUpdateResponse['params']; /** - * Represents the parameters for the transfer transaction. - */ -export interface Transaction { - /** Unique identifier for the transfer. */ - id: number; - /** The type of transaction. */ - txType: TxType; - /** The source address from which assets were transferred. */ - fromAccount: Address; - /** The user tag for the source account (optional). */ - fromAccountTag?: string; - /** The destination address to which assets were transferred. */ - toAccount: Address; - /** The user tag for the destination account (optional). */ - toAccountTag?: string; - /** The asset symbol that was transferred. */ - asset: string; - /** The amount that was transferred. */ - amount: string; - /** The timestamp when the transfer was created. */ - createdAt: Date; -} + * Represents the parameters for the 'cu' RPC method. + */ +export type ChannelUpdateResponseParams = ChannelUpdateResponse['params']; /** - * Represents the parameters for the 'transfer' RPC method. + * Represents the parameters for the 'ping' RPC method. */ -export type TransferResponseParams = Transaction[]; +export type PingResponseParams = PingResponse['params']; /** - * Represents the response structure for the 'transfer' RPC method. + * Represents the parameters for the 'pong' RPC method. */ -export interface TransferResponse extends GenericRPCMessage { - method: RPCMethod.Transfer; - params: TransferResponseParams; -} +export type PongResponseParams = PongResponse['params']; /** - * Represents the parameters for the 'transfer_notification' RPC method. + * Represents the parameters for the 'transfer' RPC method. */ -export type TransferNotificationResponseParams = Transaction[]; +export type TransferResponseParams = TransferResponse['params']; /** - * Represents the response structure for the 'transfer_notification' RPC method. + * Represents the parameters for the 'tr' RPC method. */ -export interface TransferNotificationResponse extends GenericRPCMessage { - method: RPCMethod.TransferNotification; - params: TransferNotificationResponseParams; -} +export type TransferNotificationResponseParams = TransferNotificationResponse['params']; /** * Union type for all possible RPC response types. diff --git a/sdk/src/utils/channel.ts b/sdk/src/utils/channel.ts index 0ff7f162d..3c9a2905c 100644 --- a/sdk/src/utils/channel.ts +++ b/sdk/src/utils/channel.ts @@ -1,6 +1,6 @@ -import { keccak256, encodeAbiParameters, Address } from 'viem'; +import { keccak256, encodeAbiParameters, Address, Hex } from 'viem'; import { Channel, ChannelId, State } from '../client/types'; // Updated import path -import { ChannelOperationState, RPCChannel, ServerSignature } from '../rpc'; +import { RPCChannel, RPCChannelOperationState } from '../rpc'; /** * Compute the unique identifier for a channel based on its configuration. @@ -66,7 +66,7 @@ export function convertRPCToClientChannel(ch: RPCChannel): Channel { }; } -export function convertRPCToClientState(s: ChannelOperationState, sig: ServerSignature): State { +export function convertRPCToClientState(s: RPCChannelOperationState, sig: Hex): State { return { intent: s.intent, version: BigInt(s.version), diff --git a/sdk/test/unit/rpc/api.test.ts b/sdk/test/unit/rpc/api.test.ts index 650e1f712..572305757 100644 --- a/sdk/test/unit/rpc/api.test.ts +++ b/sdk/test/unit/rpc/api.test.ts @@ -21,17 +21,16 @@ import { createECDSAMessageSigner, } from '../../../src/rpc/api'; import { - CreateAppSessionRequest, MessageSigner, AuthChallengeResponse, RPCMethod, - RPCChannelStatus, - RequestData, - TransferAllocation, ResizeChannelRequestParams, AuthRequestParams, CloseAppSessionRequestParams, - TxType, + RPCChannelStatus, + RPCTransferAllocation, + RPCTxType, + RPCData, } from '../../../src/rpc/types'; describe('API message creators', () => { @@ -49,8 +48,8 @@ describe('API message creators', () => { test('createAuthRequestMessage', async () => { const authRequest: AuthRequestParams = { - wallet: clientAddress, - participant: clientAddress, + address: clientAddress, + session_key: clientAddress, app_name: 'test-app', allowances: [], expire: '', @@ -64,7 +63,15 @@ describe('API message creators', () => { req: [ requestId, RPCMethod.AuthRequest, - [clientAddress, clientAddress, 'test-app', [], '', '', clientAddress], + { + address: clientAddress, + session_key: clientAddress, + app_name: 'test-app', + allowances: [], + expire: '', + scope: '', + application: clientAddress, + }, timestamp, ], sig: [], @@ -74,10 +81,10 @@ describe('API message creators', () => { test('createAuthVerifyMessageFromChallenge', async () => { const challenge = 'challenge123'; const msgStr = await createAuthVerifyMessageFromChallenge(signer, challenge, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, [[{ challenge }]], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, { challenge }, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.AuthVerify, [[{ challenge }]], timestamp], + req: [requestId, RPCMethod.AuthVerify, { challenge }, timestamp], sig: ['0xsig'], }); }); @@ -98,10 +105,10 @@ describe('API message creators', () => { test('successful challenge flow', async () => { const msgStr = await createAuthVerifyMessage(signer, rawResponse, requestId, timestamp); const challenge = 'c8261773-2619-4fbe-9514-96392f87e7b2'; - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, [{ challenge }], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.AuthVerify, { challenge }, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.AuthVerify, [{ challenge }], timestamp], + req: [requestId, RPCMethod.AuthVerify, { challenge }, timestamp], sig: ['0xsig'], }); }); @@ -109,37 +116,37 @@ describe('API message creators', () => { test('createPingMessage', async () => { const msgStr = await createPingMessage(signer, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Ping, [], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Ping, {}, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.Ping, [], timestamp], + req: [requestId, RPCMethod.Ping, {}, timestamp], sig: ['0xsig'], }); }); test('createGetConfigMessage', async () => { const msgStr = await createGetConfigMessage(signer, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetConfig, [], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetConfig, {}, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.GetConfig, [], timestamp], + req: [requestId, RPCMethod.GetConfig, {}, timestamp], sig: ['0xsig'], }); }); test('createGetUserTagMessage', async () => { const msgStr = await createGetUserTagMessage(signer, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetUserTag, [], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetUserTag, {}, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.GetUserTag, [], timestamp], + req: [requestId, RPCMethod.GetUserTag, {}, timestamp], sig: ['0xsig'], }); }); test('createGetLedgerBalancesMessage', async () => { const participant = '0x0123124124124100000000000000000000000000' as Address; - const ledgerParams = [{ participant }]; + const ledgerParams = { participant }; const msgStr = await createGetLedgerBalancesMessage(signer, participant, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerBalances, ledgerParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -150,7 +157,7 @@ describe('API message creators', () => { }); test('createGetAppDefinitionMessage', async () => { - const appParams = [{ app_session_id: appId }]; + const appParams = { app_session_id: appId }; const msgStr = await createGetAppDefinitionMessage(signer, appId, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetAppDefinition, appParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -161,30 +168,28 @@ describe('API message creators', () => { }); test('createAppSessionMessage', async () => { - const params = [ - { - definition: { - protocol: 'p', - participants: [], - weights: [], - quorum: 0, - challenge: 0, - nonce: 0, - }, - allocations: [ - { - participant: '0xAaBbCcDdEeFf0011223344556677889900aAbBcC' as Address, - asset: 'usdc', - amount: '0.0', - }, - { - participant: '0x00112233445566778899AaBbCcDdEeFf00112233' as Address, - asset: 'usdc', - amount: '200.0', - }, - ], + const params = { + definition: { + protocol: 'p', + participants: [], + weights: [], + quorum: 0, + challenge: 0, + nonce: 0, }, - ]; + allocations: [ + { + participant: '0xAaBbCcDdEeFf0011223344556677889900aAbBcC' as Address, + asset: 'usdc', + amount: '0.0', + }, + { + participant: '0x00112233445566778899AaBbCcDdEeFf00112233' as Address, + asset: 'usdc', + amount: '200.0', + }, + ], + }; const msgStr = await createAppSessionMessage(signer, params, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.CreateAppSession, params, timestamp]); const parsed = JSON.parse(msgStr); @@ -195,7 +200,7 @@ describe('API message creators', () => { }); test('createCloseAppSessionMessage', async () => { - const closeParams: CloseAppSessionRequestParams[] = [{ app_session_id: appId, allocations: [] }]; + const closeParams: CloseAppSessionRequestParams = { app_session_id: appId, allocations: [] }; const msgStr = await createCloseAppSessionMessage(signer, closeParams, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.CloseAppSession, closeParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -206,7 +211,7 @@ describe('API message creators', () => { }); test('createApplicationMessage', async () => { - const messageParams = ['hello']; + const messageParams = 'hello'; const msgStr = await createApplicationMessage(signer, appId, messageParams, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Message, messageParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -222,7 +227,7 @@ describe('API message creators', () => { expect(signer).toHaveBeenCalledWith([ requestId, RPCMethod.CloseChannel, - [{ channel_id: channelId, funds_destination: fundDestination }], + { channel_id: channelId, funds_destination: fundDestination }, timestamp, ]); const parsed = JSON.parse(msgStr); @@ -230,7 +235,7 @@ describe('API message creators', () => { req: [ requestId, RPCMethod.CloseChannel, - [{ channel_id: channelId, funds_destination: fundDestination }], + { channel_id: channelId, funds_destination: fundDestination }, timestamp, ], sig: ['0xsig'], @@ -243,28 +248,26 @@ describe('API message creators', () => { expect(signer).not.toHaveBeenCalled(); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.AuthVerify, [{ jwt: jwtToken }], timestamp], + req: [requestId, RPCMethod.AuthVerify, { jwt: jwtToken }, timestamp], sig: [], }); }); test('createResizeChannelMessage', async () => { - const resizeParams: ResizeChannelRequestParams[] = [ - { - channel_id: channelId, - funds_destination: fundDestination, - resize_amount: 1000n, - }, - ]; + const resizeParams: ResizeChannelRequestParams = { + channel_id: channelId, + funds_destination: fundDestination, + resize_amount: 1000n, + }; const msgStr = await createResizeChannelMessage(signer, resizeParams, requestId, timestamp); // The signer should be called with the original bigint value expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.ResizeChannel, resizeParams, timestamp]); const parsed = JSON.parse(msgStr); // The parsed message should have the stringified bigint - const resizeParamsExpected = resizeParams.map((param) => ({ - ...param, - resize_amount: param.resize_amount?.toString(), - })); + const resizeParamsExpected = { + ...resizeParams, + resize_amount: resizeParams.resize_amount?.toString(), + }; expect(parsed).toEqual({ req: [requestId, RPCMethod.ResizeChannel, resizeParamsExpected, timestamp], sig: ['0xsig'], @@ -277,19 +280,19 @@ describe('API message creators', () => { expect(signer).toHaveBeenCalledWith([ requestId, RPCMethod.GetChannels, - [{ participant, status: RPCChannelStatus.Open }], + { participant, status: RPCChannelStatus.Open }, timestamp, ]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.GetChannels, [{ participant, status: RPCChannelStatus.Open }], timestamp], + req: [requestId, RPCMethod.GetChannels, { participant, status: RPCChannelStatus.Open }, timestamp], sig: ['0xsig'], }); }); test('createTransferMessage with destination address', async () => { const destination = '0x1234567890123456789012345678901234567890' as Address; - const allocations: TransferAllocation[] = [ + const allocations: RPCTransferAllocation[] = [ { asset: 'usdc', amount: '100.5', @@ -301,17 +304,17 @@ describe('API message creators', () => { ]; const transferParams = { destination, allocations }; const msgStr = await createTransferMessage(signer, transferParams, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, [transferParams], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, transferParams, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.Transfer, [transferParams], timestamp], + req: [requestId, RPCMethod.Transfer, transferParams, timestamp], sig: ['0xsig'], }); }); test('createTransferMessage with destination_user_tag', async () => { const destination_user_tag = 'UX123D8C'; - const allocations: TransferAllocation[] = [ + const allocations: RPCTransferAllocation[] = [ { asset: 'usdc', amount: '100.5', @@ -319,16 +322,16 @@ describe('API message creators', () => { ]; const transferParams = { destination_user_tag, allocations }; const msgStr = await createTransferMessage(signer, transferParams, requestId, timestamp); - expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, [transferParams], timestamp]); + expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.Transfer, transferParams, timestamp]); const parsed = JSON.parse(msgStr); expect(parsed).toEqual({ - req: [requestId, RPCMethod.Transfer, [transferParams], timestamp], + req: [requestId, RPCMethod.Transfer, transferParams, timestamp], sig: ['0xsig'], }); }); test('createTransferMessage validates destination parameters', async () => { - const allocations: TransferAllocation[] = [{ asset: 'usdc', amount: '100.5' }]; + const allocations: RPCTransferAllocation[] = [{ asset: 'usdc', amount: '100.5' }]; // Test missing both parameters await expect(createTransferMessage(signer, { allocations }, requestId, timestamp)).rejects.toThrow( @@ -345,7 +348,7 @@ describe('API message creators', () => { test('createGetLedgerTransactionsMessage with no filters', async () => { const accountId = 'test-account'; - const expectedParams = [{ account_id: accountId }]; + const expectedParams = { account_id: accountId }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, undefined, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -359,21 +362,19 @@ describe('API message creators', () => { const accountId = 'test-account'; const filters = { asset: 'usdc', - tx_type: TxType.Transfer, + tx_type: RPCTxType.Transfer, offset: 10, limit: 20, sort: 'desc' as const, }; - const expectedParams = [ - { - account_id: accountId, - asset: 'usdc', - tx_type: TxType.Transfer, - offset: 10, - limit: 20, - sort: 'desc', - }, - ]; + const expectedParams = { + account_id: accountId, + asset: 'usdc', + tx_type: RPCTxType.Transfer, + offset: 10, + limit: 20, + sort: 'desc', + }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, filters, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -389,13 +390,11 @@ describe('API message creators', () => { asset: 'eth', limit: 5, }; - const expectedParams = [ - { - account_id: accountId, - asset: 'eth', - limit: 5, - }, - ]; + const expectedParams = { + account_id: accountId, + asset: 'eth', + limit: 5, + }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, filters, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -409,18 +408,16 @@ describe('API message creators', () => { const accountId = 'test-account'; const filters = { asset: '', - tx_type: TxType.Transfer, + tx_type: RPCTxType.Transfer, offset: 0, limit: undefined, sort: null as any, }; - const expectedParams = [ - { - account_id: accountId, - tx_type: TxType.Transfer, - offset: 0, - }, - ]; + const expectedParams = { + account_id: accountId, + tx_type: RPCTxType.Transfer, + offset: 0, + }; const msgStr = await createGetLedgerTransactionsMessage(signer, accountId, filters, requestId, timestamp); expect(signer).toHaveBeenCalledWith([requestId, RPCMethod.GetLedgerTransactions, expectedParams, timestamp]); const parsed = JSON.parse(msgStr); @@ -432,12 +429,12 @@ describe('API message creators', () => { test('createECDSAMessageSigner', async () => { const privateKey = '0xb482c8fa261c29eaaa646703948e2cc2a2ff54411cc42d8fce9a161035dfb3dc'; - const payload = [42, 'ping', [{ p1: 4337, p2: 7702 }], 20] as unknown as RequestData; + const payload: RPCData = [42, RPCMethod.Ping, { p1: 4337, p2: 7702 }, 20]; const signer = createECDSAMessageSigner(privateKey); const signature = await signer(payload); expect(signature).toBeDefined(); expect(signature).toEqual( - '0xebf96c7d3d64ab9195a341d3c922e2cb88ea592d2e229aa64d27e024f895e5720e68786c8b34a61d34a0b6f5e0f65dbe95f0a46dee9b7055df3e33f3209ea0d21b', + '0x7263178cbb9b9820491b3add77f83ebbab7df700fc30734a659b69bf0268073a2494ccbcf0ee3e98a9321f88385013a88aabe6a640d6411cda19fbbc197d38ac1c', ); }); }); diff --git a/sdk/test/unit/rpc/nitrolite.test.ts b/sdk/test/unit/rpc/nitrolite.test.ts index 3fc49b943..4c49551fc 100644 --- a/sdk/test/unit/rpc/nitrolite.test.ts +++ b/sdk/test/unit/rpc/nitrolite.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, jest } from '@jest/globals'; +import { describe, test, expect, jest, beforeAll, afterAll } from '@jest/globals'; import { Address, Hex } from 'viem'; import { NitroliteRPC } from '../../../src/rpc/nitrolite'; import { @@ -6,9 +6,8 @@ import { MessageSigner, SingleMessageVerifier, MultiMessageVerifier, - RequestData, - ResponsePayload, RPCMethod, + RPCData, } from '../../../src/rpc/types'; describe('NitroliteRPC', () => { @@ -22,10 +21,18 @@ describe('NitroliteRPC', () => { test('should create a valid request message', () => { const requestId = 12345; const method = RPCMethod.Ping; - const params = ['param1', 'param2']; + const params = { + param1: 'value1', + param2: 'value2', + }; const timestamp = 1619876543210; - const result = NitroliteRPC.createRequest(requestId, method, params, timestamp); + const result = NitroliteRPC.createRequest({ + requestId, + method, + params, + timestamp, + }); expect(result).toEqual({ req: [requestId, method, params, timestamp], @@ -35,12 +42,15 @@ describe('NitroliteRPC', () => { test('should use default values when not provided', () => { jest.spyOn(global.Date, 'now').mockReturnValue(1619876543210); - const result = NitroliteRPC.createRequest(undefined, RPCMethod.Ping); + const result = NitroliteRPC.createRequest({ + method: RPCMethod.Ping, + params: {}, + }); expect(result.req).toBeDefined(); expect(result.req![0]).toBeGreaterThan(0); expect(result.req![1]).toBe(RPCMethod.Ping); - expect(result.req![2]).toEqual([]); + expect(result.req![2]).toEqual({}); expect(result.req![3]).toBe(1619876543210); }); }); @@ -49,11 +59,22 @@ describe('NitroliteRPC', () => { test('should create a valid application request message', () => { const requestId = 12345; const method = RPCMethod.Ping; - const params = ['param1', 'param2']; + const params = { + param1: 'value1', + param2: 'value2', + }; const timestamp = 1619876543210; const accountId = '0xaccountId' as Hex; - const result = NitroliteRPC.createAppRequest(requestId, method, params, timestamp, accountId); + const result = NitroliteRPC.createAppRequest( + { + requestId, + method, + params, + timestamp, + }, + accountId, + ); expect(result).toEqual({ req: [requestId, method, params, timestamp], @@ -62,121 +83,10 @@ describe('NitroliteRPC', () => { }); }); - describe('parseResponse', () => { - test('should parse a valid response message string', () => { - const responseStr = JSON.stringify({ - res: [12345, RPCMethod.Ping, ['result1', 'result2'], 1619876543210], - }); - - const result = NitroliteRPC.parseResponse(responseStr); - - expect(result).toEqual({ - isValid: true, - isError: false, - requestId: 12345, - method: RPCMethod.Ping, - data: ['result1', 'result2'], - timestamp: 1619876543210, - }); - }); - - test('should parse a valid response message object', () => { - const responseObj = { - res: [12345, RPCMethod.Ping, ['result1', 'result2'], 1619876543210], - }; - - const result = NitroliteRPC.parseResponse(responseObj); - - expect(result).toEqual({ - isValid: true, - isError: false, - requestId: 12345, - method: RPCMethod.Ping, - data: ['result1', 'result2'], - timestamp: 1619876543210, - }); - }); - - test('should parse a valid response message with sid field', () => { - const responseObj = { - res: [12345, RPCMethod.Ping, ['result1', 'result2'], 1619876543210], - sid: '0xaccountId' as Hex, - }; - - const result = NitroliteRPC.parseResponse(responseObj); - - expect(result).toEqual({ - isValid: true, - isError: false, - requestId: 12345, - method: RPCMethod.Ping, - data: ['result1', 'result2'], - sid: '0xaccountId', - timestamp: 1619876543210, - }); - }); - - test('should handle error responses correctly', () => { - const errorResponse = { - res: [12345, 'error', [{ error: 'Something went wrong' }], 1619876543210], - }; - - const result = NitroliteRPC.parseResponse(errorResponse); - - expect(result).toEqual({ - isValid: true, - isError: true, - requestId: 12345, - method: 'error', - data: { error: 'Something went wrong' }, - timestamp: 1619876543210, - }); - }); - - test('should return invalid for malformed JSON', () => { - const result = NitroliteRPC.parseResponse('invalid json'); - - expect(result.isValid).toBe(false); - expect(result.error).toBe('Message parsing failed'); - }); - - test('should return invalid for missing res field', () => { - const result = NitroliteRPC.parseResponse({ something: 'else' }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("Invalid message structure: Missing or invalid 'res' array."); - }); - - test('should return invalid for incorrectly sized res array', () => { - const result = NitroliteRPC.parseResponse({ res: [1, 2, 3] }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("Invalid message structure: Missing or invalid 'res' array."); - }); - - test('should return invalid for incorrect types in res array', () => { - const result = NitroliteRPC.parseResponse({ - res: ['not-a-number', 123, 'not-an-array', 'not-a-number'], - }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe("Invalid 'res' payload structure or types."); - }); - - test('should return invalid for malformed error response', () => { - const result = NitroliteRPC.parseResponse({ - res: [12345, 'error', ['not an error object'], 1619876543210], - }); - - expect(result.isValid).toBe(false); - expect(result.error).toBe('Malformed error response payload.'); - }); - }); - describe('signRequestMessage', () => { test('should sign a request message and add signature to the message', async () => { const mockSigner = jest - .fn<(data: RequestData | ResponsePayload) => Promise>() + .fn<(data: RPCData) => Promise>() .mockResolvedValue('0xsignature' as Hex); const request: NitroliteRPCMessage = { req: [12345, RPCMethod.Ping, ['param1', 'param2'], 1619876543210], diff --git a/sdk/test/unit/rpc/utils.test.ts b/sdk/test/unit/rpc/utils.test.ts index 88b39ba1f..a3d91d82d 100644 --- a/sdk/test/unit/rpc/utils.test.ts +++ b/sdk/test/unit/rpc/utils.test.ts @@ -12,7 +12,7 @@ import { isValidResponseTimestamp, isValidResponseRequestId, } from '../../../src/rpc/utils'; -import { rpcResponseParser } from '../../../src/rpc/parse/parse'; +import { parseAuthChallengeResponse, parseCreateAppSessionResponse, parseGetConfigResponse, parseGetLedgerBalancesResponse, parsePingResponse } from '../../../src/rpc/parse/parse'; import { NitroliteRPCMessage, RPCMethod, RPCChannelStatus } from '../../../src/rpc/types'; describe('RPC Utils', () => { @@ -201,14 +201,14 @@ describe('RPC Utils', () => { }); }); -describe('rpcResponseParser', () => { +describe('rpc response parsers', () => { test('should parse auth_challenge response correctly', () => { const rawResponse = JSON.stringify({ - res: [123, RPCMethod.AuthChallenge, [{ challenge_message: 'test-challenge' }], 456], + res: [123, RPCMethod.AuthChallenge, { challenge_message: 'test-challenge' }, 456], sig: ['0x123'], }); - const result = rpcResponseParser.authChallenge(rawResponse); + const result = parseAuthChallengeResponse(rawResponse); expect(result.method).toBe(RPCMethod.AuthChallenge); expect(result.requestId).toBe(123); expect(result.timestamp).toBe(456); @@ -217,24 +217,26 @@ describe('rpcResponseParser', () => { }); test('should parse get_ledger_balances response correctly', () => { - const balances = [ - [ + const balances = { + ledger_balances: [ { asset: 'eth', amount: 1.5 }, { asset: 'usdc', amount: 1000 }, ], - ]; + }; const rawResponse = JSON.stringify({ res: [123, RPCMethod.GetLedgerBalances, balances, 456], sig: ['0x123'], }); - const result = rpcResponseParser.getLedgerBalances(rawResponse); + const result = parseGetLedgerBalancesResponse(rawResponse); expect(result.method).toBe(RPCMethod.GetLedgerBalances); - expect(result.params).toEqual([ - { asset: 'eth', amount: '1.5' }, - { asset: 'usdc', amount: '1000' }, - ]); + expect(result.params).toEqual({ + ledgerBalances: [ + { asset: 'eth', amount: '1.5' }, + { asset: 'usdc', amount: '1000' }, + ], + }); }); test('should parse get_config response correctly', () => { @@ -251,11 +253,11 @@ describe('rpcResponseParser', () => { }; const rawResponse = JSON.stringify({ - res: [123, RPCMethod.GetConfig, [config], 456], + res: [123, RPCMethod.GetConfig, config, 456], sig: ['0x123'], }); - const result = rpcResponseParser.getConfig(rawResponse); + const result = parseGetConfigResponse(rawResponse); expect(result.method).toBe(RPCMethod.GetConfig); expect(result.params).toEqual({ brokerAddress: '0x1234567890123456789012345678901234567890', @@ -272,11 +274,11 @@ describe('rpcResponseParser', () => { test('should parse ping response correctly', () => { const rawResponse = JSON.stringify({ - res: [123, RPCMethod.Ping, [], 456], + res: [123, RPCMethod.Ping, {}, 456], sig: ['0x123'], }); - const result = rpcResponseParser.ping(rawResponse); + const result = parsePingResponse(rawResponse); expect(result.method).toBe(RPCMethod.Ping); expect(result.requestId).toBe(123); expect(result.params).toEqual({}); @@ -290,11 +292,11 @@ describe('rpcResponseParser', () => { }; const rawResponse = JSON.stringify({ - res: [123, RPCMethod.CreateAppSession, [params], 456], + res: [123, RPCMethod.CreateAppSession, params, 456], sig: ['0x123'], }); - const result = rpcResponseParser.createAppSession(rawResponse); + const result = parseCreateAppSessionResponse(rawResponse); expect(result.method).toBe(RPCMethod.CreateAppSession); expect(result.params).toEqual({ @@ -309,22 +311,22 @@ describe('rpcResponseParser', () => { res: [123, RPCMethod.Ping, 456], // Missing one element }); - expect(() => rpcResponseParser.ping(invalidResponse)).toThrow('Invalid RPC response format'); + expect(() => parsePingResponse(invalidResponse)).toThrow('Invalid RPC response format'); }); test('should throw error for invalid JSON', () => { const invalidJSON = 'this is not json'; - expect(() => rpcResponseParser.ping(invalidJSON)).toThrow(/Failed to parse RPC response/); + expect(() => parsePingResponse(invalidJSON)).toThrow(/Failed to parse RPC response/); }); test('should throw error when parsing with the wrong method', () => { const rawResponse = JSON.stringify({ - res: [123, RPCMethod.AuthChallenge, [{ challenge_message: 'test-challenge' }], 456], + res: [123, RPCMethod.AuthChallenge, { challenge_message: 'test-challenge' }, 456], sig: ['0x123'], }); // Try to parse an auth_challenge response with the ping parser - expect(() => rpcResponseParser.ping(rawResponse)).toThrow( + expect(() => parsePingResponse(rawResponse)).toThrow( "Expected RPC method to be 'ping', but received 'auth_challenge'", ); }); From 46dbe0d7de133a18575e89dc921294de8660366d Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 6 Aug 2025 16:14:04 +0300 Subject: [PATCH 05/18] feat(Custody): update domain version to 0.3.0 --- contract/src/Custody.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract/src/Custody.sol b/contract/src/Custody.sol index 279f528ce..06b04b67f 100644 --- a/contract/src/Custody.sol +++ b/contract/src/Custody.sol @@ -78,8 +78,8 @@ contract Custody is IChannel, IDeposit, IChannelReader, EIP712 { mapping(address account => Ledger ledger) internal _ledgers; // ========== Constructor ========== - // TODO: make sure the last nitrolite sdk npm package version used - constructor() EIP712("Nitrolite:Custody", "0.2.24") { + + constructor() EIP712("Nitrolite:Custody", "0.3.0") { // No state initialization needed } From 9ddecfffd8a2fb73ac39b13c54b40bb283290a9c Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 6 Aug 2025 17:02:49 +0300 Subject: [PATCH 06/18] config(contract): set optimizer runs to 45 000 --- contract/foundry.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contract/foundry.toml b/contract/foundry.toml index 0a891e2a2..58d6b4747 100644 --- a/contract/foundry.toml +++ b/contract/foundry.toml @@ -4,7 +4,8 @@ out = "out" libs = ["lib"] via_ir = true optimizer = true -optimizer_runs = 2000 +# As per v0.3.0 Custody size becomes a concern. The runs value should not be raised higher. +optimizer_runs = 45000 [rpc_endpoints] unichain_sepolia = "https://sepolia.unichain.org" From 1fbcf321009875a06eb3b6672c4890c4d9190cfc Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 6 Aug 2025 17:03:10 +0300 Subject: [PATCH 07/18] docs(contract/deployments): add UAT 137, 11155111, PROD 137 --- .../Custody.sol:Custody/2025-08-06T14:01:37.json | 11 +++++++++++ .../2025-08-06T14:02:01.json | 14 ++++++++++++++ .../Custody.sol:Custody/2025-08-06T13:58:12.json | 11 +++++++++++ .../Custody.sol:Custody/2025-08-06T13:59:17.json | 11 +++++++++++ .../2025-08-06T13:58:31.json | 14 ++++++++++++++ .../2025-08-06T14:01:03.json | 14 ++++++++++++++ 6 files changed, 75 insertions(+) create mode 100644 contract/deployments/11155111/Custody.sol:Custody/2025-08-06T14:01:37.json create mode 100644 contract/deployments/11155111/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:02:01.json create mode 100644 contract/deployments/137/Custody.sol:Custody/2025-08-06T13:58:12.json create mode 100644 contract/deployments/137/Custody.sol:Custody/2025-08-06T13:59:17.json create mode 100644 contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T13:58:31.json create mode 100644 contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:01:03.json diff --git a/contract/deployments/11155111/Custody.sol:Custody/2025-08-06T14:01:37.json b/contract/deployments/11155111/Custody.sol:Custody/2025-08-06T14:01:37.json new file mode 100644 index 000000000..cf2de1b5e --- /dev/null +++ b/contract/deployments/11155111/Custody.sol:Custody/2025-08-06T14:01:37.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6", + "transactionHash": "0xe371df18ff8a017d330b1d04836b6f470afdedcfc086cf0183253a7e87de4127", + "commit": "6cc315209fae6057553845c49df6a4bf02c35413", + "timestamp": 1754488897, + "chainId": 11155111, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "UAT v0.3.0" +} diff --git a/contract/deployments/11155111/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:02:01.json b/contract/deployments/11155111/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:02:01.json new file mode 100644 index 000000000..f2b29096a --- /dev/null +++ b/contract/deployments/11155111/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:02:01.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C", + "transactionHash": "0x433fe775ff88b3f4da2f78f112da8691a34becfbe57d1d3674c089aed9fccd57", + "commit": "6cc315209fae6057553845c49df6a4bf02c35413", + "timestamp": 1754488921, + "chainId": 11155111, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + ], + "comment": "UAT v0.3.0" +} diff --git a/contract/deployments/137/Custody.sol:Custody/2025-08-06T13:58:12.json b/contract/deployments/137/Custody.sol:Custody/2025-08-06T13:58:12.json new file mode 100644 index 000000000..c3938c959 --- /dev/null +++ b/contract/deployments/137/Custody.sol:Custody/2025-08-06T13:58:12.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f", + "transactionHash": "0x31a8195cbb50a80ad04bfff2b66b457be25c9076721ac115be1ac33dba18541a", + "commit": "6cc315209fae6057553845c49df6a4bf02c35413", + "timestamp": 1754488692, + "chainId": 137, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/137/Custody.sol:Custody/2025-08-06T13:59:17.json b/contract/deployments/137/Custody.sol:Custody/2025-08-06T13:59:17.json new file mode 100644 index 000000000..baf795658 --- /dev/null +++ b/contract/deployments/137/Custody.sol:Custody/2025-08-06T13:59:17.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025e9c0f85e34a2b141e99557355c5b3ea1e244", + "deployedTo": "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6", + "transactionHash": "0x251dbd9903f0b0e874f3021a76b4787cc9b0acf69b9c7fb0d39a0147bee2f58f", + "commit": "6cc315209fae6057553845c49df6a4bf02c35413", + "timestamp": 1754488757, + "chainId": 137, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "UAT v0.3.0" +} diff --git a/contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T13:58:31.json b/contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T13:58:31.json new file mode 100644 index 000000000..d383e20cc --- /dev/null +++ b/contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T13:58:31.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x14980dF216722f14c42CA7357b06dEa7eB408b10", + "transactionHash": "0x474f645793adc8af9c5c3fa826be142aa9f3c8ab2ab9a8ceadc553f4dc63e619", + "commit": "6cc315209fae6057553845c49df6a4bf02c35413", + "timestamp": 1754488711, + "chainId": 137, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f" + ], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:01:03.json b/contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:01:03.json new file mode 100644 index 000000000..d134f956e --- /dev/null +++ b/contract/deployments/137/SimpleConsensus.sol:SimpleConsensus/2025-08-06T14:01:03.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C", + "transactionHash": "0xd82dab282d1a8d312f351763763b0357b091ae9d5a8d678cf8e10da8ccaf0d11", + "commit": "6cc315209fae6057553845c49df6a4bf02c35413", + "timestamp": 1754488863, + "chainId": 137, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + ], + "comment": "UAT v0.3.0" +} From 347a0b2174d2a155b022fdd1fe87bfb31b6fdfc9 Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Wed, 6 Aug 2025 17:49:13 +0300 Subject: [PATCH 08/18] fix: add missing exports --- sdk/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 3cfc98be6..4495e1c37 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -5,5 +5,6 @@ export * from './client'; export * from './client/services'; export * from './client/types'; export * from './client/state'; +export * from './client/signer'; export * from './rpc'; export * from './abis'; From f36ae348dfae054b28251304bbe7938257e6f4ab Mon Sep 17 00:00:00 2001 From: MaxMoskalenko Date: Fri, 8 Aug 2025 15:08:22 +0300 Subject: [PATCH 09/18] bump sdk version --- sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/package.json b/sdk/package.json index 39fa09ab9..9eda3811e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@erc7824/nitrolite", - "version": "0.2.31", + "version": "0.3.0", "description": "The Nitrolite SDK empowers developers to build high-performance, scalable web3 applications using state channels. It's designed to provide near-instant transactions and significantly improved user experiences by minimizing direct blockchain interactions.", "main": "dist/index.js", "types": "dist/index.d.ts", From c5c3febeff7b2a1929d6a1002f7880a426225290 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Tue, 12 Aug 2025 12:22:48 +0300 Subject: [PATCH 10/18] fix(sdk::RPCNetworkInfo): remove chain name --- sdk/src/rpc/parse/misc.ts | 2 -- sdk/src/rpc/types/common.ts | 2 -- sdk/test/unit/rpc/utils.test.ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/sdk/src/rpc/parse/misc.ts b/sdk/src/rpc/parse/misc.ts index a3a44fbfa..3536ebdc2 100644 --- a/sdk/src/rpc/parse/misc.ts +++ b/sdk/src/rpc/parse/misc.ts @@ -12,14 +12,12 @@ import { hexSchema, addressSchema, ParamsParser } from './common'; const NetworkInfoObjectSchema = z .object({ - name: z.string(), chain_id: z.number(), custody_address: addressSchema, adjudicator_address: addressSchema, }) .transform( (raw): RPCNetworkInfo => ({ - name: raw.name, chainId: raw.chain_id, custodyAddress: raw.custody_address, adjudicatorAddress: raw.adjudicator_address, diff --git a/sdk/src/rpc/types/common.ts b/sdk/src/rpc/types/common.ts index c567c7d7a..d8037b0fe 100644 --- a/sdk/src/rpc/types/common.ts +++ b/sdk/src/rpc/types/common.ts @@ -75,8 +75,6 @@ export interface RPCChannelUpdateWithWallet extends RPCChannelUpdate { * Represents the network information for the 'get_config' RPC method. */ export interface RPCNetworkInfo { - /** The name of the network (e.g., "Ethereum", "Polygon"). */ - name: string; /** The chain ID of the network. */ chainId: number; /** The custody contract address for the network. */ diff --git a/sdk/test/unit/rpc/utils.test.ts b/sdk/test/unit/rpc/utils.test.ts index a3d91d82d..cff57fe2d 100644 --- a/sdk/test/unit/rpc/utils.test.ts +++ b/sdk/test/unit/rpc/utils.test.ts @@ -244,7 +244,6 @@ describe('rpc response parsers', () => { broker_address: '0x1234567890123456789012345678901234567890', networks: [ { - name: 'Ethereum', chain_id: 1, custody_address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', adjudicator_address: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', @@ -263,7 +262,6 @@ describe('rpc response parsers', () => { brokerAddress: '0x1234567890123456789012345678901234567890', networks: [ { - name: 'Ethereum', chainId: 1, custodyAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', adjudicatorAddress: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', From ad4721817fe0936fe15ccb022ce09011c1cdeb2e Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 12 Aug 2025 13:59:04 +0300 Subject: [PATCH 11/18] update uat release values --- clearnode/chart/config/uat/clearnode.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clearnode/chart/config/uat/clearnode.yaml b/clearnode/chart/config/uat/clearnode.yaml index 65fd72548..3d7642315 100644 --- a/clearnode/chart/config/uat/clearnode.yaml +++ b/clearnode/chart/config/uat/clearnode.yaml @@ -10,17 +10,17 @@ config: envSecret: "" extraEnvs: CLEARNODE_LOG_LEVEL: "debug" - POLYGON_CUSTODY_CONTRACT_ADDRESS: "0xE98e30523278aeFC909Fb8EF80FdF463a8BA6dEa" - POLYGON_ADJUDICATOR_ADDRESS: "0x132C865E708D53A0e26E134157Ef08cb6cC41624" + POLYGON_CUSTODY_CONTRACT_ADDRESS: "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + POLYGON_ADJUDICATOR_ADDRESS: "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C" POLYGON_BALANCE_CHECKER_ADDRESS: "0x2352c63A83f9Fd126af8676146721Fa00924d7e4" - ETH_SEPOLIA_CUSTODY_CONTRACT_ADDRESS: "0xE98e30523278aeFC909Fb8EF80FdF463a8BA6dEa" - ETH_SEPOLIA_ADJUDICATOR_ADDRESS: "0x132C865E708D53A0e26E134157Ef08cb6cC41624" + ETH_SEPOLIA_CUSTODY_CONTRACT_ADDRESS: "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + ETH_SEPOLIA_ADJUDICATOR_ADDRESS: "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C" ETH_SEPOLIA_BALANCE_CHECKER_ADDRESS: "0xBfbCed302deD369855fc5f7668356e123ca4B329" MSG_EXPIRY_TIME: "60" image: repository: ghcr.io/erc7824/nitrolite/clearnode - tag: 0.0.2-rc.12 + tag: c9c4d4c service: http: From 30ae48922b52fc2a945d618f2e16f0f0d0694f4b Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 19 Aug 2025 12:50:50 +0300 Subject: [PATCH 12/18] feat: migrate cerebro to v0.3.0 (#306) --- examples/cerebro/channel_ops.go | 173 ++++---- examples/cerebro/clearnet/auth.go | 91 ++--- examples/cerebro/clearnet/clearnode_client.go | 173 +++++--- examples/cerebro/clearnet/clearnode_rpc.go | 12 +- examples/cerebro/clearnet/events.go | 42 +- examples/cerebro/config.go | 5 +- examples/cerebro/custody/custody_binding.go | 368 ++++++++++++++---- examples/cerebro/custody/custody_client.go | 53 +-- examples/cerebro/custody_ledger.go | 52 ++- examples/cerebro/import.go | 15 +- examples/cerebro/list.go | 10 +- examples/cerebro/operator.go | 17 +- examples/cerebro/unisig/ecdsa.go | 6 +- examples/cerebro/unisig/signer.go | 28 +- 14 files changed, 633 insertions(+), 412 deletions(-) diff --git a/examples/cerebro/channel_ops.go b/examples/cerebro/channel_ops.go index 8668c34d1..7fced8a19 100644 --- a/examples/cerebro/channel_ops.go +++ b/examples/cerebro/channel_ops.go @@ -1,7 +1,6 @@ package main import ( - "context" "fmt" "math/big" "os" @@ -9,7 +8,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/ethclient" "github.com/jedib0t/go-pretty/v6/table" "github.com/shopspring/decimal" @@ -28,80 +26,71 @@ func (o *Operator) handleOpenChannel(args []string) { return } - chainName := args[2] - network := o.config.GetNetworkByName(chainName) + chainIDStr := args[2] + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + fmt.Printf("Invalid chain ID: %s.\n", chainIDStr) + return + } + + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { - fmt.Printf("Chain %s is not supported by the broker.\n", chainName) + fmt.Printf("Unknown chain: %s.\n", chainIDStr) return } assetSymbol := args[3] asset := network.GetAssetBySymbol(assetSymbol) if asset == nil { - fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainName) + fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainID.String()) return } if asset.IsEnabled() { - fmt.Printf("Channel is already opened for asset %s on %s: %s.\n", assetSymbol, chainName, asset.ChannelID) + fmt.Printf("Channel is already opened for asset %s on %s: %s.\n", assetSymbol, chainID.String(), asset.ChannelID) return } - chainRPCDTOs, err := o.store.GetChainRPCs(network.ChainID) + creationRes, err := o.clearnode.RequestChannelCreation(network.ChainID, o.config.Signer.Address(), asset.Token) if err != nil { - fmt.Printf("Failed to get RPCs for chain %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to request channel creation for chain %s: %s\n", chainID.String(), err.Error()) return } - if len(chainRPCDTOs) == 0 { - fmt.Printf("No RPCs found for chain %s. Please import an RPC first.\n", chainName) + + if creationRes.Channel == nil { + fmt.Printf("Response from Clearnode doesn't have a channel data: %v\n", creationRes) return } - chainRPCIndex := -1 - for i := len(chainRPCDTOs) - 1; i >= 0; i-- { - ethClient, err := ethclient.Dial(chainRPCDTOs[i].URL) - if err != nil { - continue - } - - // Check if the chain ID matches - chainIDFromRPC, err := ethClient.ChainID(context.Background()) - if err != nil { - continue - } - if chainIDFromRPC.Uint64() != uint64(network.ChainID) { - continue - } - - chainRPCIndex = i - break - } - if chainRPCIndex < 0 { - fmt.Printf("No valid RPC found for chain %s. Please import a valid RPC first.\n", chainName) + chainRPC, err := o.getChainRPC(network.ChainID) + if err != nil { + fmt.Printf("Failed to get RPC for chain %s: %s\n", chainID.String(), err.Error()) return } - fmt.Printf("Opening custody channel on %s...\n", chainName) + fmt.Printf("Opening custody channel on %s...\n", chainID.String()) channelID, err := o.custody.OpenChannel( o.config.Wallet, o.config.Signer, - network.ChainID, chainRPCDTOs[chainRPCIndex].URL, + network.ChainID, chainRPC, network.CustodyAddress, network.AdjudicatorAddress, o.config.BrokerAddress, asset.Token, - 0, + creationRes.Channel.Challenge, + creationRes.Channel.Nonce, + creationRes.StateSignature, ) if err != nil { - fmt.Printf("Failed to open custody channel on %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to open custody channel on %s: %s\n", chainID.String(), err.Error()) return } - if err := o.store.UpdateChainRPCUsage(chainRPCDTOs[chainRPCIndex].URL); err != nil { + if err := o.store.UpdateChainRPCUsage(chainRPC); err != nil { fmt.Printf("Failed to update chain RPC usage: %s\n", err.Error()) return } - fmt.Printf("Successfully opened custody channel (%s) on %s!\n", channelID, chainName) + fmt.Printf("Successfully opened custody channel (%s) on %s!\n", channelID, chainID.String()) } func (o *Operator) handleCloseChannel(args []string) { @@ -115,59 +104,59 @@ func (o *Operator) handleCloseChannel(args []string) { return } - chainName := args[2] - network := o.config.GetNetworkByName(chainName) + chainIDStr := args[2] + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + fmt.Printf("Invalid chain ID: %s.\n", chainIDStr) + return + } + + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { - fmt.Printf("Chain %s is not supported by the broker.\n", chainName) + fmt.Printf("Unknown chain: %s.\n", chainIDStr) return } assetSymbol := args[3] asset := network.GetAssetBySymbol(assetSymbol) if asset == nil { - fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainName) + fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainID.String()) return } if !asset.IsEnabled() { - fmt.Printf("There are no opened channels for %s on %s.\n", assetSymbol, chainName) + fmt.Printf("There are no opened channels for %s on %s.\n", assetSymbol, chainID.String()) return } closureRes, err := o.clearnode.RequestChannelClosure(o.config.Wallet.Address(), asset.ChannelID) if err != nil { - fmt.Printf("Failed to request channel closure for chain %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to request channel closure for chain %s: %s\n", chainID.String(), err.Error()) return } chainRPC, err := o.getChainRPC(network.ChainID) if err != nil { - fmt.Printf("Failed to get RPC for chain %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to get RPC for chain %s: %s\n", chainID.String(), err.Error()) return } - fmt.Printf("Closing custody channel (%s) on %s...\n", asset.ChannelID, chainName) + fmt.Printf("Closing custody channel (%s) on %s...\n", asset.ChannelID, chainID.String()) - allocations := make([]custody.Allocation, len(closureRes.FinalAllocations)) - for i, alloc := range closureRes.FinalAllocations { + allocations := make([]custody.Allocation, len(closureRes.State.Allocations)) + for i, alloc := range closureRes.State.Allocations { allocations[i] = convertAllocationRes(alloc) } - brokerSig, err := convertSignatureRes(closureRes.Signature) - if err != nil { - fmt.Printf("Failed to convert broker signature: %s\n", err.Error()) - return - } - if err := o.custody.CloseChannel( o.config.Wallet, o.config.Signer, network.ChainID, chainRPC, network.CustodyAddress, common.HexToHash(closureRes.ChannelID), - new(big.Int).SetUint64(closureRes.Version), + new(big.Int).SetUint64(closureRes.State.Version), allocations, - brokerSig, + closureRes.StateSignature, ); err != nil { - fmt.Printf("Failed to close custody channel on %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to close custody channel on %s: %s\n", chainID.String(), err.Error()) return } @@ -177,7 +166,7 @@ func (o *Operator) handleCloseChannel(args []string) { } unlockedAmount := decimal.NewFromBigInt(allocations[0].Amount, -int32(asset.Decimals)) - fmt.Printf("Successfully closed custody channel (%s) on %s with unlocked %s %s!\n", asset.ChannelID, chainName, fmtDec(unlockedAmount), strings.ToUpper(asset.Symbol)) + fmt.Printf("Successfully closed custody channel (%s) on %s with unlocked %s %s!\n", asset.ChannelID, chainID.String(), fmtDec(unlockedAmount), strings.ToUpper(asset.Symbol)) } func (o *Operator) handleResizeChannel(args []string) { @@ -191,27 +180,33 @@ func (o *Operator) handleResizeChannel(args []string) { return } - chainName := args[2] - network := o.config.GetNetworkByName(chainName) + chainIDStr := args[2] + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + fmt.Printf("Invalid chain ID: %s.\n", chainIDStr) + return + } + + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { - fmt.Printf("Chain %s is not supported by the broker.\n", chainName) + fmt.Printf("Unknown chain: %s.\n", chainIDStr) return } assetSymbol := args[3] asset := network.GetAssetBySymbol(assetSymbol) if asset == nil { - fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainName) + fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainID.String()) return } if !asset.IsEnabled() { - fmt.Printf("There are no opened channels for %s on %s.\n", assetSymbol, chainName) + fmt.Printf("There are no opened channels for %s on %s.\n", assetSymbol, chainID.String()) return } chainRPC, err := o.getChainRPC(network.ChainID) if err != nil { - fmt.Printf("Failed to get RPC for chain %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to get RPC for chain %s: %s\n", chainID.String(), err.Error()) return } @@ -219,7 +214,7 @@ func (o *Operator) handleResizeChannel(args []string) { network.ChainID, chainRPC, network.CustodyAddress, o.config.Wallet.Address(), asset.Token) if err != nil { - fmt.Printf("Failed to get balance for asset %s on %s: %s\n", assetSymbol, chainName, err.Error()) + fmt.Printf("Failed to get balance for asset %s on %s: %s\n", assetSymbol, chainID.String(), err.Error()) return } custodyBalance := decimal.NewFromBigInt(rawCustodyBalance, -int32(asset.Decimals)) @@ -265,7 +260,7 @@ func (o *Operator) handleResizeChannel(args []string) { fmtDec(custodyBalance), assetSymbol) return } - rawResizeAmount := resizeAmount.Shift(int32(asset.Decimals)).BigInt() + rawResizeAmount := resizeAmount.Shift(int32(asset.Decimals)) fmt.Printf("How much %s do you want to allocate (+)into/(-)out channel?\n", assetSymbol) fmt.Println("That's the amount moved between unified balance and channel.") @@ -282,7 +277,7 @@ func (o *Operator) handleResizeChannel(args []string) { fmtDec(unifiedBalance), assetSymbol) return } - rawAllocateAmount := allocateAmount.Shift(int32(asset.Decimals)).BigInt() + rawAllocateAmount := allocateAmount.Shift(int32(asset.Decimals)) if newChannelBalance := channelBalance.Add(allocateAmount).Add(resizeAmount); newChannelBalance.LessThan(decimal.Zero) { fmt.Printf("New channel amount must not be negative after resize: %s\n", fmtDec(newChannelBalance)) @@ -291,40 +286,34 @@ func (o *Operator) handleResizeChannel(args []string) { resizeRes, err := o.clearnode.RequestChannelResize(o.config.Wallet.Address(), asset.ChannelID, rawAllocateAmount, rawResizeAmount) if err != nil { - fmt.Printf("Failed to request channel resize on %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to request channel resize on %s: %s\n", chainID.String(), err.Error()) return } - fmt.Printf("Resizing custody channel (%s) on %s...\n", asset.ChannelID, chainName) + fmt.Printf("Resizing custody channel (%s) on %s...\n", asset.ChannelID, chainID.String()) - stateData, err := hexutil.Decode(resizeRes.StateData) + stateData, err := hexutil.Decode(resizeRes.State.Data) if err != nil { fmt.Printf("Failed to decode state data: %s\n", err.Error()) return } - allocations := make([]custody.Allocation, len(resizeRes.Allocations)) - for i, alloc := range resizeRes.Allocations { + allocations := make([]custody.Allocation, len(resizeRes.State.Allocations)) + for i, alloc := range resizeRes.State.Allocations { allocations[i] = convertAllocationRes(alloc) } - brokerSig, err := convertSignatureRes(resizeRes.Signature) - if err != nil { - fmt.Printf("Failed to convert broker signature: %s\n", err.Error()) - return - } - if err := o.custody.Resize( o.config.Wallet, o.config.Signer, network.ChainID, chainRPC, network.CustodyAddress, common.HexToHash(resizeRes.ChannelID), - new(big.Int).SetUint64(resizeRes.Version), + new(big.Int).SetUint64(resizeRes.State.Version), stateData, allocations, - brokerSig, + resizeRes.StateSignature, ); err != nil { - fmt.Printf("Failed to resize custody channel on %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to resize custody channel on %s: %s\n", chainID.String(), err.Error()) return } @@ -333,31 +322,13 @@ func (o *Operator) handleResizeChannel(args []string) { return } - fmt.Printf("Successfully resized custody channel (%s) on %s!\n", asset.ChannelID, chainName) + fmt.Printf("Successfully resized custody channel (%s) on %s!\n", asset.ChannelID, chainID.String()) } func convertAllocationRes(a clearnet.AllocationRes) custody.Allocation { return custody.Allocation{ Destination: common.HexToAddress(a.Destination), Token: common.HexToAddress(a.Token), - Amount: a.Amount, - } -} - -func convertSignatureRes(sig clearnet.SignatureRes) (custody.Signature, error) { - r, err := hexutil.Decode(sig.R) - if err != nil { - return custody.Signature{}, fmt.Errorf("failed to decode R: %w", err) + Amount: a.Amount.BigInt(), } - - s, err := hexutil.Decode(sig.S) - if err != nil { - return custody.Signature{}, fmt.Errorf("failed to decode S: %w", err) - } - - return custody.Signature{ - V: sig.V, - R: [32]byte(r), - S: [32]byte(s), - }, nil } diff --git a/examples/cerebro/clearnet/auth.go b/examples/cerebro/clearnet/auth.go index fb5fa0644..970c14834 100644 --- a/examples/cerebro/clearnet/auth.go +++ b/examples/cerebro/clearnet/auth.go @@ -4,22 +4,20 @@ import ( "encoding/json" "fmt" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/erc7824/nitrolite/examples/cerebro/unisig" ) -type AuthChallenge struct { - AppName string - AppAddress string - Token string - Scope string - Wallet string - Participant string - Expire string - Allowances []any +type AuthChallengeParams struct { + Address string `json:"address"` + SessionKey string `json:"session_key"` + AppName string `json:"app_name"` + Allowances []any `json:"allowances"` + Expire string `json:"expire"` + Scope string `json:"scope"` + ApplicationAddress string `json:"application"` } func (c *ClearnodeClient) Authenticate(wallet, signer unisig.Signer) (string, error) { @@ -27,75 +25,66 @@ func (c *ClearnodeClient) Authenticate(wallet, signer unisig.Signer) (string, er return "", nil // Already authenticated } - ch := AuthChallenge{ - Wallet: wallet.Address().Hex(), - Participant: signer.Address().Hex(), // Using address as session key for simplicity - AppName: "Cerebro CLI", - Allowances: []any{}, // No allowances for now - Expire: "", // No expiration for now - Scope: "", // No specific scope for now - AppAddress: wallet.Address().Hex(), // Using address as app address for simplicity - } - res, err := c.request("auth_request", nil, - ch.Wallet, - ch.Participant, - ch.AppName, - ch.Allowances, - ch.Expire, - ch.Scope, - ch.AppAddress, - ) + ch := AuthChallengeParams{ + Address: wallet.Address().Hex(), + SessionKey: signer.Address().Hex(), // Using address as session key for simplicity + AppName: "Cerebro CLI", + Allowances: []any{}, // No allowances for now + Expire: "", // No expiration for now + Scope: "", // No specific scope for now + ApplicationAddress: wallet.Address().Hex(), // Using address as app address for simplicity + } + res, err := c.request("auth_request", nil, ch) if err != nil { return "", fmt.Errorf("authentication request failed: %w", err) } - if res.Res.Method != "auth_challenge" || len(res.Res.Params) < 1 { + if res.Res.Method != "auth_challenge" { return "", fmt.Errorf("unexpected response to auth_request: %v", res.Res) } - challengeMap, ok := res.Res.Params[0].(map[string]any) + challengeMap, ok := res.Res.Params.(map[string]any) if !ok { - return "", fmt.Errorf("invalid auth_challenge response format: %v", res.Res.Params[0]) + return "", fmt.Errorf("invalid auth_challenge response format: %v", res.Res.Params) } challengeToken, ok := challengeMap["challenge_message"].(string) if !ok { return "", fmt.Errorf("challenge_message not found in auth_challenge response: %v", challengeMap) } - ch.Token = challengeToken - chSig, err := signChallenge(wallet, ch) + chSig, err := signChallenge(wallet, ch, challengeToken) if err != nil { return "", fmt.Errorf("failed to sign challenge: %w", err) } authVerifyChallenge := map[string]any{ "challenge": challengeToken, } - res, err = c.request("auth_verify", []string{hexutil.Encode(chSig)}, authVerifyChallenge) + res, err = c.request("auth_verify", []unisig.Signature{chSig}, authVerifyChallenge) if err != nil { return "", fmt.Errorf("authentication verification failed: %w", err) } - if res.Res.Method != "auth_verify" || len(res.Res.Params) < 1 { + if res.Res.Method != "auth_verify" { return "", fmt.Errorf("unexpected response to auth_verify: %v", res.Res) } - verifyMap, ok := res.Res.Params[0].(map[string]any) + verifyMap, ok := res.Res.Params.(map[string]any) if !ok { - return "", fmt.Errorf("invalid auth_verify response format: %v", res.Res.Params[0]) + return "", fmt.Errorf("invalid auth_verify response format: %v", res.Res.Params) } if authSuccess, _ := verifyMap["success"].(bool); !authSuccess { return "", fmt.Errorf("authentication failed: %v", verifyMap) } - res, err = c.request("get_user_tag", nil) + res, err = c.request("get_user_tag", nil, nil) if err != nil { return "", fmt.Errorf("failed to get user tag: %w", err) } - if res.Res.Method != "get_user_tag" || len(res.Res.Params) < 1 { + if res.Res.Method != "get_user_tag" { return "", fmt.Errorf("unexpected response to get_user_tag: %v", res.Res) } - tagMap, ok := res.Res.Params[0].(map[string]any) + tagMap, ok := res.Res.Params.(map[string]any) if !ok { - return "", fmt.Errorf("invalid auth_verify response format: %v", res.Res.Params[0]) + return "", fmt.Errorf("invalid auth_verify response format: %v", res.Res.Params) } userTag, _ := tagMap["tag"].(string) @@ -103,7 +92,7 @@ func (c *ClearnodeClient) Authenticate(wallet, signer unisig.Signer) (string, er return userTag, nil } -func signChallenge(s unisig.Signer, c AuthChallenge) ([]byte, error) { +func signChallenge(s unisig.Signer, c AuthChallengeParams, token string) (unisig.Signature, error) { typedData := apitypes.TypedData{ Types: apitypes.Types{ "EIP712Domain": { @@ -127,11 +116,11 @@ func signChallenge(s unisig.Signer, c AuthChallenge) ([]byte, error) { Name: c.AppName, }, Message: map[string]interface{}{ - "challenge": c.Token, + "challenge": token, "scope": c.Scope, - "wallet": c.Wallet, - "application": c.AppAddress, - "participant": c.Participant, + "wallet": c.Address, + "application": c.ApplicationAddress, + "participant": c.SessionKey, "expire": c.Expire, "allowances": c.Allowances, }, @@ -139,27 +128,27 @@ func signChallenge(s unisig.Signer, c AuthChallenge) ([]byte, error) { hash, _, err := apitypes.TypedDataAndHash(typedData) if err != nil { - return nil, err + return unisig.Signature{}, err } signature, err := s.Sign(hash) if err != nil { - return nil, fmt.Errorf("failed to sign challenge: %w", err) + return unisig.Signature{}, fmt.Errorf("failed to sign challenge: %w", err) } return signature, nil } -func signRPCData(signer unisig.Signer, rpcData RPCData) ([]byte, error) { +func signRPCData(signer unisig.Signer, rpcData RPCData) (unisig.Signature, error) { dataBytes, err := json.Marshal(rpcData) if err != nil { - return nil, fmt.Errorf("failed to marshal RPC data: %w", err) + return unisig.Signature{}, fmt.Errorf("failed to marshal RPC data: %w", err) } dataHash := crypto.Keccak256(dataBytes) signature, err := signer.Sign(dataHash) if err != nil { - return nil, fmt.Errorf("failed to sign RPC data: %w", err) + return unisig.Signature{}, fmt.Errorf("failed to sign RPC data: %w", err) } return signature, nil diff --git a/examples/cerebro/clearnet/clearnode_client.go b/examples/cerebro/clearnet/clearnode_client.go index 594f4f8d0..3bc6b419d 100644 --- a/examples/cerebro/clearnet/clearnode_client.go +++ b/examples/cerebro/clearnet/clearnode_client.go @@ -3,13 +3,11 @@ package clearnet import ( "encoding/json" "fmt" - "math/big" "net" "sync" "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gorilla/websocket" "github.com/shopspring/decimal" @@ -32,7 +30,6 @@ type ClearnodeClient struct { } type NetworkInfo struct { - Name string `json:"name"` ChainID uint32 `json:"chain_id"` CustodyAddress string `json:"custody_address"` AdjudicatorAddress string `json:"adjudicator_address"` @@ -66,15 +63,15 @@ func NewClearnodeClient(wsURL string) (*ClearnodeClient, error) { } func (c *ClearnodeClient) GetConfig() (*BrokerConfig, error) { - res, err := c.request("get_config", nil) + res, err := c.request("get_config", nil, nil) if err != nil { return nil, fmt.Errorf("failed to fetch config: %w", err) } - if res.Res.Method != "get_config" || len(res.Res.Params) < 1 { + if res.Res.Method != "get_config" { return nil, fmt.Errorf("unexpected response to config request: %v", res.Res) } - configJSON, err := json.Marshal(res.Res.Params[0]) + configJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal config data: %w", err) } @@ -87,26 +84,42 @@ func (c *ClearnodeClient) GetConfig() (*BrokerConfig, error) { return &config, nil } +type GetAssetsReq struct { + ChainID *uint32 `json:"chain_id"` +} + +type GetAssetsRes struct { + Assets []AssetRes `json:"assets"` +} + func (c *ClearnodeClient) GetSupportedAssets() ([]AssetRes, error) { - res, err := c.request("get_assets", nil) + res, err := c.request("get_assets", nil, GetAssetsReq{ChainID: nil}) if err != nil { return nil, fmt.Errorf("failed to fetch assets: %w", err) } - if res.Res.Method != "get_assets" || len(res.Res.Params) < 1 { + if res.Res.Method != "get_assets" { return nil, fmt.Errorf("unexpected response to assets request: %v", res.Res) } - assetsJSON, err := json.Marshal(res.Res.Params[0]) + assetsJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal assets data: %w", err) } - var assets []AssetRes - if err := json.Unmarshal(assetsJSON, &assets); err != nil { - return nil, fmt.Errorf("failed to parse assets: %w", err) + var assetsRes GetAssetsRes + if err := json.Unmarshal(assetsJSON, &assetsRes); err != nil { + return nil, fmt.Errorf("failed to parse assets: %w, %s", err, string(assetsJSON)) } - return assets, nil + return assetsRes.Assets, nil +} + +type GetLedgerBalancesReq struct { + AccountID string `json:"account_id,omitempty"` +} + +type GetLedgerBalancesRes struct { + LedgerBalances []BalanceRes `json:"ledger_balances"` } type BalanceRes struct { @@ -115,25 +128,29 @@ type BalanceRes struct { } func (c *ClearnodeClient) GetLedgerBalances() ([]BalanceRes, error) { - res, err := c.request("get_ledger_balances", nil) + res, err := c.request("get_ledger_balances", nil, GetLedgerBalancesReq{}) if err != nil { return nil, fmt.Errorf("failed to fetch balances: %w", err) } - if res.Res.Method != "get_ledger_balances" || len(res.Res.Params) < 1 { + if res.Res.Method != "get_ledger_balances" { return nil, fmt.Errorf("unexpected response to get_ledger_balances request: %v", res.Res) } - assetsJSON, err := json.Marshal(res.Res.Params[0]) + assetsJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal assets data: %w", err) } - var balances []BalanceRes - if err := json.Unmarshal(assetsJSON, &balances); err != nil { + var balancesRes GetLedgerBalancesRes + if err := json.Unmarshal(assetsJSON, &balancesRes); err != nil { return nil, fmt.Errorf("failed to parse assets: %w", err) } - return balances, nil + return balancesRes.LedgerBalances, nil +} + +type GetChannelsRes struct { + Channels []ChannelRes `json:"channels"` } func (c *ClearnodeClient) GetChannels(participant, status string) ([]ChannelRes, error) { @@ -146,45 +163,82 @@ func (c *ClearnodeClient) GetChannels(participant, status string) ([]ChannelRes, if err != nil { return nil, fmt.Errorf("failed to fetch channels: %w", err) } - if res.Res.Method != "get_channels" || len(res.Res.Params) < 1 { + if res.Res.Method != "get_channels" { return nil, fmt.Errorf("unexpected response to channels request: %v", res.Res) } - channelsJSON, err := json.Marshal(res.Res.Params[0]) + channelsJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal channels data: %w", err) } - var channels []ChannelRes - if err := json.Unmarshal(channelsJSON, &channels); err != nil { + var channelsRes GetChannelsRes + if err := json.Unmarshal(channelsJSON, &channelsRes); err != nil { return nil, fmt.Errorf("failed to parse channels: %w", err) } - return channels, nil + return channelsRes.Channels, nil +} + +type ChannelOperationRes struct { + ChannelID string `json:"channel_id"` + Channel *struct { + Participants [2]string `json:"participants"` + Adjudicator string `json:"adjudicator"` + Challenge uint64 `json:"challenge"` + Nonce uint64 `json:"nonce"` + } `json:"channel,omitempty"` + State UnsignedState `json:"state"` + StateSignature unisig.Signature `json:"server_signature"` } -type ChannelClosureRes struct { - ChannelID string `json:"channel_id"` - Intent uint8 `json:"intent"` - Version uint64 `json:"version"` - StateData string `json:"state_data"` - FinalAllocations []AllocationRes `json:"allocations"` - Signature SignatureRes `json:"server_signature"` +type UnsignedState struct { + Intent uint8 `json:"intent"` + Version uint64 `json:"version"` + Data string `json:"state_data"` + Allocations []AllocationRes `json:"allocations"` } type AllocationRes struct { - Destination string `json:"destination"` - Token string `json:"token"` - Amount *big.Int `json:"amount"` + Destination string `json:"destination"` + Token string `json:"token"` + Amount decimal.Decimal `json:"amount"` } -type SignatureRes struct { - V uint8 `json:"v,string"` - R string `json:"r"` - S string `json:"s"` +func (c *ClearnodeClient) RequestChannelCreation(chainID uint32, signerAddress, assetAddress common.Address) (*ChannelOperationRes, error) { + if c.signer == nil { + return nil, fmt.Errorf("client not authenticated") + } + + params := map[string]any{ + "chain_id": chainID, + "session_key": c.signer.Address().Hex(), + "token": assetAddress.Hex(), + "amount": decimal.NewFromInt(0), // Default to 0 + } + + res, err := c.request("create_channel", nil, params) + if err != nil { + return nil, fmt.Errorf("failed to request channel creation: %w", err) + } + if res.Res.Method != "create_channel" { + return nil, fmt.Errorf("unexpected response to create_channel: %v", res.Res) + } + + opResJSON, err := json.Marshal(res.Res.Params) + if err != nil { + return nil, fmt.Errorf("failed to marshal creation response: %w", err) + } + + var opRes ChannelOperationRes + if err := json.Unmarshal(opResJSON, &opRes); err != nil { + return nil, fmt.Errorf("failed to parse channels: %w", err) + } + + return &opRes, nil } -func (c *ClearnodeClient) RequestChannelClosure(walletAddress common.Address, channelID string) (*ChannelClosureRes, error) { +func (c *ClearnodeClient) RequestChannelClosure(walletAddress common.Address, channelID string) (*ChannelOperationRes, error) { if c.signer == nil { return nil, fmt.Errorf("client not authenticated") } @@ -198,33 +252,24 @@ func (c *ClearnodeClient) RequestChannelClosure(walletAddress common.Address, ch if err != nil { return nil, fmt.Errorf("failed to request channel closure: %w", err) } - if res.Res.Method != "close_channel" || len(res.Res.Params) < 1 { + if res.Res.Method != "close_channel" { return nil, fmt.Errorf("unexpected response to close_channel: %v", res.Res) } - closureResJSON, err := json.Marshal(res.Res.Params[0]) + opResJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal closure response: %w", err) } - var closureRes ChannelClosureRes - if err := json.Unmarshal(closureResJSON, &closureRes); err != nil { + var opRes ChannelOperationRes + if err := json.Unmarshal(opResJSON, &opRes); err != nil { return nil, fmt.Errorf("failed to parse channels: %w", err) } - return &closureRes, nil -} - -type ChannelResizeRes struct { - ChannelID string `json:"channel_id"` - Intent uint8 `json:"intent"` - Version uint64 `json:"version"` - StateData string `json:"state_data"` - Allocations []AllocationRes `json:"allocations"` - Signature SignatureRes `json:"server_signature"` + return &opRes, nil } -func (c *ClearnodeClient) RequestChannelResize(walletAddress common.Address, channelID string, allocateAmount, resizeAmount *big.Int) (*ChannelResizeRes, error) { +func (c *ClearnodeClient) RequestChannelResize(walletAddress common.Address, channelID string, allocateAmount, resizeAmount decimal.Decimal) (*ChannelOperationRes, error) { if c.signer == nil { return nil, fmt.Errorf("client not authenticated") } @@ -240,21 +285,21 @@ func (c *ClearnodeClient) RequestChannelResize(walletAddress common.Address, cha if err != nil { return nil, fmt.Errorf("failed to request channel resize: %w", err) } - if res.Res.Method != "resize_channel" || len(res.Res.Params) < 1 { + if res.Res.Method != "resize_channel" { return nil, fmt.Errorf("unexpected response to resize_channel: %v", res.Res) } - resizeResJSON, err := json.Marshal(res.Res.Params[0]) + opResJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal closure response: %w", err) } - var resizeRes ChannelResizeRes - if err := json.Unmarshal(resizeResJSON, &resizeRes); err != nil { + var opRes ChannelOperationRes + if err := json.Unmarshal(opResJSON, &opRes); err != nil { return nil, fmt.Errorf("failed to parse channels: %w", err) } - return &resizeRes, nil + return &opRes, nil } type TransferReq struct { @@ -303,11 +348,11 @@ func (c *ClearnodeClient) Transfer(transferByTag bool, destinationValue string, if err != nil { return nil, fmt.Errorf("failed to transfer: %w", err) } - if res.Res.Method != "transfer" || len(res.Res.Params) < 1 { + if res.Res.Method != "transfer" { return nil, fmt.Errorf("unexpected response to transfer: %v", res.Res) } - resizeResJSON, err := json.Marshal(res.Res.Params[0]) + resizeResJSON, err := json.Marshal(res.Res.Params) if err != nil { return nil, fmt.Errorf("failed to marshal closure response: %w", err) } @@ -359,7 +404,7 @@ func (c *ClearnodeClient) pingPeriodically() { defer c.exit() // Ensure exit channel is closed when done for range ticker.C { - res, err := c.request("ping", nil) + res, err := c.request("ping", nil, nil) if err != nil { fmt.Printf("Error sending ping: %s\n", err.Error()) return @@ -372,7 +417,7 @@ func (c *ClearnodeClient) pingPeriodically() { } } -func (c *ClearnodeClient) request(method string, sigs []string, params ...any) (*RPCResponse, error) { +func (c *ClearnodeClient) request(method string, sigs []unisig.Signature, params any) (*RPCResponse, error) { if params == nil { params = []any{} // Ensure params is never nil } @@ -390,7 +435,7 @@ func (c *ClearnodeClient) request(method string, sigs []string, params ...any) ( if err != nil { return nil, fmt.Errorf("error signing RPC data: %w", err) } - sigs = []string{hexutil.Encode(sig)} + sigs = []unisig.Signature{sig} } req := RPCRequest{ diff --git a/examples/cerebro/clearnet/clearnode_rpc.go b/examples/cerebro/clearnet/clearnode_rpc.go index 5c5a17134..7fbe9500f 100644 --- a/examples/cerebro/clearnet/clearnode_rpc.go +++ b/examples/cerebro/clearnet/clearnode_rpc.go @@ -3,16 +3,18 @@ package clearnet import ( "encoding/json" "fmt" + + "github.com/erc7824/nitrolite/examples/cerebro/unisig" ) type RPCRequest struct { - Req RPCData `json:"req"` - Sig []string `json:"sig"` + Req RPCData `json:"req"` + Sig []unisig.Signature `json:"sig"` } type RPCResponse struct { - Res RPCData `json:"res"` - Sig []string `json:"sig"` + Res RPCData `json:"res"` + Sig []unisig.Signature `json:"sig"` } // RPCData represents the common structure for both requests and responses @@ -20,7 +22,7 @@ type RPCResponse struct { type RPCData struct { RequestID uint64 `json:"request_id" validate:"required"` Method string `json:"method" validate:"required"` - Params []any `json:"params" validate:"required"` + Params any `json:"params" validate:"required"` Timestamp uint64 `json:"ts" validate:"required"` } diff --git a/examples/cerebro/clearnet/events.go b/examples/cerebro/clearnet/events.go index 684eac770..c30088750 100644 --- a/examples/cerebro/clearnet/events.go +++ b/examples/cerebro/clearnet/events.go @@ -3,8 +3,9 @@ package clearnet import ( "encoding/json" "fmt" - "math/big" "sort" + + "github.com/shopspring/decimal" ) func (c *ClearnodeClient) handleEvent(event RPCData) { @@ -25,24 +26,19 @@ func (c *ClearnodeClient) handleEvent(event RPCData) { } type ChannelRes struct { - ChannelID string `json:"channel_id"` - Participant string `json:"participant"` - Status string `json:"status"` - Token string `json:"token"` - RawAmount *big.Int `json:"amount"` // Total amount in the channel (user + broker) - ChainID uint32 `json:"chain_id"` - Adjudicator string `json:"adjudicator"` - Challenge uint64 `json:"challenge"` - UpdatedAt string `json:"updated_at"` + ChannelID string `json:"channel_id"` + Participant string `json:"participant"` + Status string `json:"status"` + Token string `json:"token"` + RawAmount decimal.Decimal `json:"amount"` // Total amount in the channel (user + broker) + ChainID uint32 `json:"chain_id"` + Adjudicator string `json:"adjudicator"` + Challenge uint64 `json:"challenge"` + UpdatedAt string `json:"updated_at"` } func (c *ClearnodeClient) handleChannelsEvent(event RPCData) { - if len(event.Params) < 1 { - fmt.Println("Invalid channels event format") - return - } - - channelsData, err := json.Marshal(event.Params[0]) + channelsData, err := json.Marshal(event.Params) if err != nil { fmt.Printf("Failed to marshal channels data: %s\n", err.Error()) return @@ -74,12 +70,7 @@ func (c *ClearnodeClient) handleChannelsEvent(event RPCData) { } func (c *ClearnodeClient) handleBalancesEvent(event RPCData) { - if len(event.Params) < 1 { - fmt.Println("Invalid channels event format") - return - } - - channelsData, err := json.Marshal(event.Params[0]) + channelsData, err := json.Marshal(event.Params) if err != nil { fmt.Printf("Failed to marshal channels data: %s\n", err.Error()) return @@ -110,12 +101,7 @@ type AssetRes struct { } func (c *ClearnodeClient) handleAssetsEvent(event RPCData) { - if len(event.Params) < 1 { - fmt.Println("Invalid assets event format") - return - } - - assetsData, err := json.Marshal(event.Params[0]) + assetsData, err := json.Marshal(event.Params) if err != nil { fmt.Printf("Failed to marshal assets data: %s\n", err.Error()) return diff --git a/examples/cerebro/config.go b/examples/cerebro/config.go index f64de2067..a039ba46e 100644 --- a/examples/cerebro/config.go +++ b/examples/cerebro/config.go @@ -15,9 +15,9 @@ type OperatorConfig struct { Signer unisig.Signer } -func (c OperatorConfig) GetNetworkByName(name string) *NetworkConfig { +func (c OperatorConfig) GetNetworkByID(chainID uint32) *NetworkConfig { for _, network := range c.Networks { - if network.ChainName == name { + if network.ChainID == chainID { return &network } } @@ -39,7 +39,6 @@ func (c OperatorConfig) GetSymbolsOfEnabledAssets() []string { } type NetworkConfig struct { - ChainName string ChainID uint32 AdjudicatorAddress common.Address CustodyAddress common.Address diff --git a/examples/cerebro/custody/custody_binding.go b/examples/cerebro/custody/custody_binding.go index b818bf6e3..9cadd8506 100644 --- a/examples/cerebro/custody/custody_binding.go +++ b/examples/cerebro/custody/custody_binding.go @@ -44,26 +44,19 @@ type Channel struct { Nonce uint64 } -// Signature is an auto generated low-level Go binding around an user-defined struct. -type Signature struct { - V uint8 - R [32]byte - S [32]byte -} - // State is an auto generated low-level Go binding around an user-defined struct. type State struct { Intent uint8 Version *big.Int Data []byte Allocations []Allocation - Sigs []Signature + Sigs [][]byte } // CustodyMetaData contains all meta data concerning the Custody contract. var CustodyMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature\",\"name\":\"challengerSig\",\"type\":\"tuple\"}],\"name\":\"challenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"checkpoint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"name\":\"close\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"create\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"depositAndCreate\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getAccountsBalances\",\"outputs\":[{\"internalType\":\"uint256[][]\",\"name\":\"\",\"type\":\"uint256[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getChannelBalances\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"balances\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"getChannelData\",\"outputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"internalType\":\"enumChannelStatus\",\"name\":\"status\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"wallets\",\"type\":\"address[]\"},{\"internalType\":\"uint256\",\"name\":\"challengeExpiry\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"lastValidState\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"}],\"name\":\"getOpenChannels\",\"outputs\":[{\"internalType\":\"bytes32[][]\",\"name\":\"\",\"type\":\"bytes32[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature\",\"name\":\"sig\",\"type\":\"tuple\"}],\"name\":\"join\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"resize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"expiration\",\"type\":\"uint256\"}],\"name\":\"Challenged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"}],\"name\":\"Checkpointed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"finalState\",\"type\":\"tuple\"}],\"name\":\"Closed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"indexed\":false,\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"components\":[{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"internalType\":\"structSignature[]\",\"name\":\"sigs\",\"type\":\"tuple[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"Created\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposited\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"Joined\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"Opened\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"int256[]\",\"name\":\"deltaAllocations\",\"type\":\"int256[]\"}],\"name\":\"Resized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdrawn\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"ChallengeNotExpired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ChannelNotFinal\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"ChannelNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DepositAlreadyFulfilled\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expectedFulfilled\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"actualFulfilled\",\"type\":\"uint256\"}],\"name\":\"DepositsNotFulfilled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ECDSAInvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"ECDSAInvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"ECDSAInvalidSignatureS\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"available\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"required\",\"type\":\"uint256\"}],\"name\":\"InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAdjudicator\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAllocations\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAmount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengePeriod\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengerSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidParticipant\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidState\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStateSignatures\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidValue\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TransferFailed\",\"type\":\"error\"}]", - Bin: "0x0x608080604052346015576149a0908161001a8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063259311c9146118b05780632f33c4d6146116f75780635a9eb80e146116275780638340f549146115f2578063925bc4791461157c578063a22b823d14611054578063bc7b456f14610fc7578063d0cce1e814610b9f578063d37ff7b514610b33578063d710e92f146109a5578063de22731f146103a1578063e617208c1461023b5763f3fef3a3146100a8575f80fd5b34610237576040600319360112610237576100c161211f565b60243590335f52600160205260405f20906001600160a01b0381165f528160205260405f205491838310610207576001600160a01b0392508282165f5260205260405f20610110848254612691565b90551690816101b5575f80808084335af13d156101b0573d61013181612313565b9061013f60405192836120e4565b81525f60203d92013e5b1561017d575b6040519081527fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb60203392a3005b907fbf182be8000000000000000000000000000000000000000000000000000000005f526004523360245260445260645ffd5b610149565b6102026040517fa9059cbb000000000000000000000000000000000000000000000000000000006020820152336024820152826044820152604481526101fc6064826120e4565b83614666565b61014f565b50507fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5f80fd5b34610237576020600319360112610237575f606060405161025b81612090565b818152826020820152826040820152015261027461269e565b506004355f525f60205260405f2061028b81612602565b60ff60038301541691604051906102a36060836120e4565b600282526040366020840137600481015f5b600281106103735750506102d0600f600e8301549201612bdb565b6040519460a0865267ffffffffffffffff60606102fa8751608060a08b01526101208a01906121ae565b966001600160a01b0360208201511660c08a01528260408201511660e08a0152015116610100870152600581101561035f57859461035b9461034892602088015286820360408801526121ae565b9160608501528382036080850152612219565b0390f35b634e487b7160e01b5f52602160045260245ffd5b806001600160a01b0361038860019385612dcf565b90549060031b1c1661039a82876126f9565b52016102b5565b34610237576103af3661201d565b5050815f525f60205260405f2060ff600382015416600581101561035f57801561099257600281036106c8575081356004811015610237576103f0816121ea565b600381036106b957602083013580156106b95760808401916002610414848761258c565b9050036106915761043761042785612602565b6104313688612439565b90613fdc565b156106915761044990600f85016127ef565b60108301556011820161045f604085018561265e565b9067ffffffffffffffff821161067d576104838261047d8554612808565b85612856565b5f90601f8311600114610619576104b192915f918361060e575b50508160011b915f199060031b1c19161790565b90555b601282016104c5606085018561258c565b91906104d1838361289b565b905f5260205f205f915b8383106105a857505050506104f460138301918461258c565b906104ff828461291d565b915f5260205f205f925b82841061056857505050507f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c9109161054f6105496012610563945b01612b5a565b85614283565b604051918291602083526020830190612e13565b0390a2005b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290610509565b60036060826001600160a01b036105c0600195612909565b166001600160a01b03198654161785556105dc60208201612909565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906104db565b01359050888061049d565b601f19831691845f5260205f20925f5b818110610665575090846001959493921061064c575b505050811b0190556104b4565b01355f19600384901b60f8161c1916905587808061063f565b91936020600181928787013581550195019201610629565b634e487b7160e01b5f52604160045260245ffd5b7f773a750f000000000000000000000000000000000000000000000000000000005f5260045ffd5b63baf3f0f760e01b5f5260045ffd5b60030361096a57600e810180544210156109365782356004811015610237576106f0816121ea565b600381036106b95761070e61070484612602565b6104313687612439565b15610691575f6107229255600f83016127ef565b602082013560108201556011810161073d604084018461265e565b9067ffffffffffffffff821161067d5761075b8261047d8554612808565b5f90601f83116001146108d25761078892915f91836108c75750508160011b915f199060031b1c19161790565b90555b6012810161079c606084018461258c565b91906107a8838361289b565b905f5260205f205f915b8383106108615750505050601381016107ce608084018461258c565b906107d9828461291d565b915f5260205f205f925b82841061082157505050507f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c9109161054f610549601261056394610543565b80359060ff8216820361023757606060039160ff6001941660ff1986541617855560208101358486015560408101356002860155019201930192906107e3565b60036060826001600160a01b03610879600195612909565b166001600160a01b031986541617855561089560208201612909565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906107b2565b01359050878061049d565b601f19831691845f5260205f20925f5b81811061091e5750908460019594939210610905575b505050811b01905561078b565b01355f19600384901b60f8161c191690558680806108f8565b919360206001819287870135815501950192016108e2565b507f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c9109161054f610549601261056394610543565b7ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b836379c1d89f60e11b5f5260045260245ffd5b346102375760206003193601126102375760043567ffffffffffffffff8111610237576109d6903690600401612149565b8051906109fb6109e583612107565b926109f360405194856120e4565b808452612107565b90610a0e601f196020850193018361298b565b5f5b8151811015610aa2576001600160a01b03610a2b82846126f9565b51165f526001602052600160405f20016040519081602082549182815201915f5260205f20905f905b808210610a8a5750505090610a6e816001949303826120e4565b610a7882876126f9565b52610a8381866126f9565b5001610a10565b90919260016020819286548152019401920190610a54565b50509060405191829160208301906020845251809152604083019060408160051b85010192915f905b828210610ada57505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b808210610b1b575050506020806001929601920192018594939192610acb565b90919260208060019286518152019401920190610afb565b346102375760406003193601126102375760043567ffffffffffffffff811161023757608060031982360301126102375760243567ffffffffffffffff81116102375760a0600319823603011261023757602091610b9791600401906004016137da565b604051908152f35b3461023757610bad3661201d565b835f525f60205260405f2091600383019160ff83541690600582101561035f578115610fb4576004821461096a57853592600484101561023757610bf0846121ea565b836106b957600f86019260ff84541690600181145f14610c32577ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b600203610f0157506001600160a01b0360018701541691610c66610c56368a612439565b610c5f86612bdb565b9085614482565b156106b957610c90926020926040518095819482936305b959ef60e01b84528d8d60048601612f8f565b03915afa908115610ef6575f91610ec7575b50156106b957610cbd925b600260ff198254161790556127ef565b6020820135601082015560118101610cd8604084018461265e565b9067ffffffffffffffff821161067d57610cf68261047d8554612808565b5f90601f8311600114610e6357610d2392915f91836108c75750508160011b915f199060031b1c19161790565b90555b60128101610d37606084018461258c565b9190610d43838361289b565b905f5260205f205f915b838310610dfd57868660138701610d67608083018361258c565b90610d72828461291d565b915f5260205f205f925b828410610dbd57857fa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d61056387604051918291602083526020830190612e13565b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290610d7c565b60036060826001600160a01b03610e15600195612909565b166001600160a01b0319865416178555610e3160208201612909565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190610d4d565b601f19831691845f5260205f20925f5b818110610eaf5750908460019594939210610e96575b505050811b019055610d26565b01355f19600384901b60f8161c19169055868080610e89565b91936020600181928787013581550195019201610e73565b610ee9915060203d602011610eef575b610ee181836120e4565b81019061270d565b87610ca2565b503d610ed7565b6040513d5f823e3d90fd5b90916020610f35916001600160a01b0360018a0154169460405193849283926305b959ef60e01b84528d8d60048601612f8f565b0381865afa908115610ef6575f91610f95575b50156106b957610f57816121ea565b15610f6d575b50610cbd925f600e860155610cad565b610f8a90610f7b3688612439565b610f8484612bdb565b91614482565b156106b95786610f5d565b610fae915060203d602011610eef57610ee181836120e4565b89610f48565b866379c1d89f60e11b5f5260045260245ffd5b346102375760c06003193601126102375760243567ffffffffffffffff81116102375760a060031982360301126102375760443567ffffffffffffffff811161023757611018903690600401611fec565b9060607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c3601126102375761105292600401600435613027565b005b346102375760a060031936011261023757600435606060431936011261023757805f525f60205260405f2090600382019160ff835416600581101561035f578015611569575f190161096a5760016024350361154157600d81015461151957600f8101906110d36110c482612602565b6110cd84612bdb565b9061406d565b6001600160a01b03611100816110e885612d3a565b90549060031b1c16926110fa366123c5565b906146d3565b16036106915761110f82612bdb565b936040519461111f6060876120e4565b6002865260405f5b8181106114f0575050608081019561113f87516126c8565b51611149826126c8565b52611153816126c8565b5061115d366123c5565b611166826126e9565b52611170816126e9565b50865260018301546001600160a01b031660405190602061119181846120e4565b5f8352601f19015f5b8181106114d95750506111c79160209160405180809581946305b959ef60e01b8352888b6004850161276c565b03915afa908115610ef6575f916114ba575b50156106b9576112456111ee60088501612d67565b9461122086600c87019060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b6005850180546001600160a01b03191633179055825190611240826121ea565b6127ef565b6020810151601084015560118301604082015180519067ffffffffffffffff821161067d576112788261047d8554612808565b602090601f8311600114611457576112a692915f918361144c5750508160011b915f199060031b1c19161790565b90555b6060601284019101519060208251926112c2848461289b565b01905f5260205f205f915b8383106113e9575050505060138201945160208151916112ed838961291d565b01955f5260205f20955f905b8282106113b157835460ff19166002178455602087611359886001600160a01b036113238a612d3a565b90549060031b1c165f526001845261134183600160405f20016146fc565b50836001600160a01b03825116910151908333614154565b807fe8e915db7b3549b9e9e9b3e2ec2dc3edd1f76961504366998824836401f6846a8360405160018152a260405190807fd087f17acc177540af5f382bc30c65363705b90855144d285a822536ee11fdd15f80a28152f35b60036020826040600194518c60ff1960ff8084511616915416178d558c8685830151910155015160028c0155019801910190966112f9565b60036020826040600194516001600160a01b0380825116166001600160a01b03198854161787556001600160a01b0384820151166001600160a01b0387890191166001600160a01b031982541617905501516002860155019201920191906112cd565b015190508a8061049d565b90601f19831691845f52815f20925f5b8181106114a2575090846001959493921061148a575b505050811b0190556112a9565b01515f1960f88460031b161c1916905589808061147d565b92936020600181928786015181550195019301611467565b6114d3915060203d602011610eef57610ee181836120e4565b876111d9565b6020906114e461269e565b8282870101520161119a565b6020906040516114ff81612074565b5f81525f838201525f604082015282828b01015201611127565b7f1b136079000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa145c43e000000000000000000000000000000000000000000000000000000005f5260045ffd5b826379c1d89f60e11b5f5260045260245ffd5b60806003193601126102375761159061211f565b6044359067ffffffffffffffff82116102375760806003198336030112610237576064359167ffffffffffffffff83116102375760a06003198436030112610237576020926115e6610b979360243590336129e8565b600401906004016137da565b60606003193601126102375761160661211f565b602435906001600160a01b03821682036102375761105291604435916129e8565b346102375760406003193601126102375760043560243567ffffffffffffffff81116102375761165b903690600401612149565b61166581516129a7565b5f5b82518110156116b257600190845f525f602052601460405f20016001600160a01b038061169484886126f9565b5116165f5260205260405f20546116ab82856126f9565b5201611667565b506040518091602082016020835281518091526020604084019201905f5b8181106116de575050500390f35b82518452859450602093840193909201916001016116d0565b346102375760406003193601126102375760043567ffffffffffffffff811161023757611728903690600401611fec565b60243567ffffffffffffffff811161023757611748903690600401611fec565b91909261175482612107565b9361176260405195866120e4565b82855261176e83612107565b93611781601f196020880196018661298b565b5f5b84811061181a57858760405191829160208301906020845251809152604083019060408160051b85010192915f905b8282106117c157505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b8082106118025750505060208060019296019201920185949391926117b2565b909192602080600192865181520194019201906117e2565b611823826129a7565b61182d82896126f9565b5261183881886126f9565b505f5b82811061184b5750600101611783565b6001906001600160a01b03611869611864858a8a6129d8565b612909565b165f528160205260405f206001600160a01b0361188a61186484888a6129d8565b165f5260205260405f20546118a9826118a3868d6126f9565b516126f9565b520161183b565b34610237576118be3661201d565b9190835f525f60205260405f209360ff600386015416600581101561035f578015611fd9577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0161096a5783156106b957823592600484101561023757611924846121ea565b600284036106b957823593609e198436030194858112156102375761194c9085013690612439565b966020830135602089015160018101809111611b405781036106b95760608901936119778551613fa6565b606081019161199861199361198c858561258c565b369161232f565b613fa6565b6119ae6119a485612602565b6104313685612439565b15610691576119c0604083018361265e565b810195906020818803126102375780359067ffffffffffffffff821161023757019686601f89011215610237578735966119f988612107565b98611a076040519a8b6120e4565b888a5260208a01906020829a60051b82010192831161023757602001905b828210611fc95750505051611a3d61198c868661258c565b9060028951036106b957611a6f816040611a6681611a5d611aa6966126c8565b510151926126e9565b5101519061257f565b91611aa0611a90611a7f8c6126c8565b51611a898d6126e9565b5190614052565b916040611a6681611a5d846126c8565b92614052565b03611fa1575f198b019b8b8d11611b4057611ac08d612107565b9c806040519e8f90611ad290826120e4565b52611adc90612107565b601f19018d5f5b828110611f885750505060015b8c811015611b54578060051b8b01358c811215610237575f19820191908c01818311611b40578f92600193611b29611b39933690612439565b611b3383836126f9565b526126f9565b5001611af0565b634e487b7160e01b5f52601160045260245ffd5b5060208d611b88926001600160a01b0360018a015416906040518095819482936305b959ef60e01b84528d6004850161276c565b03915afa908115610ef6575f91611f69575b50156106b957611bad90600f86016127ef565b601084015560118301611bc3604083018361265e565b9067ffffffffffffffff821161067d57611be18261047d8554612808565b5f90601f8311600114611f0557611c0e92915f9183611efa5750508160011b915f199060031b1c19161790565b90555b60128301611c1f838361258c565b9190611c2b838361289b565b905f5260205f205f915b838310611e94575050505060138301611c51608083018361258c565b90611c5c828461291d565b915f5260205f205f925b828410611e545750505050611c7e9161198c9161258c565b9060068101916001600160a01b038354165f5b60028110611dfb57505f5b60028110611d5b575050600a5f9201915b60028110611d18575050505060405191602083019060208452518091526040830191905f5b818110611d0257857ff3b6c524f73df7344d9fcf2f960a57aba7fba7e292d8b79ed03d786f7b2b112f86860387a2005b8251845260209384019390920191600101611cd2565b806040611d27600193856126f9565b51015182611d358388612b47565b5001556040611d4482856126f9565b51015182611d528387612b47565b50015501611cad565b5f611d6982899796976126f9565b5112611d7b575b600101939293611c9c565b6001600160a01b03611d908260048801612dcf565b90549060031b1c1690611da381896126f9565b517f80000000000000000000000000000000000000000000000000000000000000008114611b4057600192611df49160405191611ddf83612074565b82528560208301525f0360408201528a6145b3565b9050611d70565b805f611e0c6001938a9897986126f9565b5113611e1c575b01939293611c91565b611e4f6001600160a01b03611e348360048a01612dcf565b90549060031b1c16848b611e48858d6126f9565b5192614154565b611e13565b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290611c66565b60036060826001600160a01b03611eac600195612909565b166001600160a01b0319865416178555611ec860208201612909565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190611c35565b013590508a8061049d565b601f19831691845f5260205f20925f5b818110611f515750908460019594939210611f38575b505050811b019055611c11565b01355f19600384901b60f8161c19169055898080611f2b565b91936020600181928787013581550195019201611f15565b611f82915060203d602011610eef57610ee181836120e4565b89611b9a565b6020918282611f9561269e565b92010152018e90611ae3565b7f52e4cb1c000000000000000000000000000000000000000000000000000000005f5260045ffd5b8135815260209182019101611a25565b506379c1d89f60e11b5f5260045260245ffd5b9181601f840112156102375782359167ffffffffffffffff8311610237576020808501948460051b01011161023757565b6060600319820112610237576004359160243567ffffffffffffffff81116102375760a0600319828503011261023757600401916044359067ffffffffffffffff82116102375761207091600401611fec565b9091565b6060810190811067ffffffffffffffff82111761067d57604052565b6080810190811067ffffffffffffffff82111761067d57604052565b60a0810190811067ffffffffffffffff82111761067d57604052565b6040810190811067ffffffffffffffff82111761067d57604052565b90601f601f19910116810190811067ffffffffffffffff82111761067d57604052565b67ffffffffffffffff811161067d5760051b60200190565b600435906001600160a01b038216820361023757565b35906001600160a01b038216820361023757565b9080601f8301121561023757813561216081612107565b9261216e60405194856120e4565b81845260208085019260051b82010192831161023757602001905b8282106121965750505090565b602080916121a384612135565b815201910190612189565b90602080835192838152019201905f5b8181106121cb5750505090565b82516001600160a01b03168452602093840193909201916001016121be565b6004111561035f57565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b8051612224816121ea565b825260208101516020830152612249604082015160a0604085015260a08401906121f4565b906060810151918381036060850152602080845192838152019301905f5b8181106122cb5750505060800151916080818303910152602080835192838152019201905f5b81811061229a5750505090565b909192602060606001926040875160ff8151168352848101518584015201516040820152019401910191909161228d565b909193602061230960019287519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b9501929101612267565b67ffffffffffffffff811161067d57601f01601f191660200190565b92919261233b82612107565b9361234960405195866120e4565b606060208685815201930282019181831161023757925b82841061236d5750505050565b60608483031261023757602060609160405161238881612074565b61239187612135565b815261239e838801612135565b8382015260408701356040820152815201930192612360565b359060ff8216820361023757565b604319606091011261023757604051906123de82612074565b8160443560ff8116810361023757815260643560208201526040608435910152565b91908260609103126102375760405161241881612074565b6040808294612426816123b7565b8452602081013560208501520135910152565b919060a08382031261023757604051612451816120ac565b80938035600481101561023757825260208101356020830152604081013567ffffffffffffffff811161023757810183601f8201121561023757803561249681612313565b916124a460405193846120e4565b818352856020838301011161023757815f92602080930183860137830101526040830152606081013567ffffffffffffffff811161023757810183601f8201121561023757838160206124f99335910161232f565b606083015260808101359067ffffffffffffffff8211610237570182601f8201121561023757803561252a81612107565b9361253860405195866120e4565b8185526020606081870193028401019281841161023757602001915b838310612565575050505060800152565b60206060916125748486612400565b815201920191612554565b91908201809211611b4057565b903590601e1981360301821215610237570180359067ffffffffffffffff82116102375760200191606082023603831361023757565b90602082549182815201915f5260205f20905f5b8181106125e35750505090565b82546001600160a01b03168452602090930192600192830192016125d6565b9060405161260f81612090565b606067ffffffffffffffff600283956040516126368161262f81856125c2565b03826120e4565b85528260018201546001600160a01b038116602088015260a01c166040860152015416910152565b903590601e1981360301821215610237570180359067ffffffffffffffff82116102375760200191813603831361023757565b91908203918211611b4057565b604051906126ab826120ac565b60606080835f81525f602082015282604082015282808201520152565b8051156126d55760200190565b634e487b7160e01b5f52603260045260245ffd5b8051600110156126d55760400190565b80518210156126d55760209160051b010190565b90816020910312610237575180151581036102375790565b9060808152606067ffffffffffffffff600261274460808501866125c2565b948260018201546001600160a01b038116602088015260a01c16604086015201541691015290565b9161278261279092606085526060850190612725565b908382036020850152612219565b906040818303910152815180825260208201916020808360051b8301019401925f915b8383106127c257505050505090565b90919293946020806127e083601f1986600196030187528951612219565b970193019301919392906127b3565b906127f9816121ea565b60ff60ff198354169116179055565b90600182811c92168015612836575b602083101461282257565b634e487b7160e01b5f52602260045260245ffd5b91607f1691612817565b81811061284b575050565b5f8155600101612840565b9190601f811161286557505050565b61288f925f5260205f20906020601f840160051c83019310612891575b601f0160051c0190612840565b565b9091508190612882565b9068010000000000000000811161067d578154918181558282106128be57505050565b82600302926003840403611b405781600302916003830403611b40575f5260205f2091820191015b8181106128f1575050565b805f600392555f60018201555f6002820155016128e6565b356001600160a01b03811681036102375790565b9068010000000000000000811161067d5781549181815582821061294057505050565b82600302926003840403611b405781600302916003830403611b40575f5260205f2091820191015b818110612973575050565b805f600392555f60018201555f600282015501612968565b5f5b82811061299957505050565b60608282015260200161298d565b906129b182612107565b6129be60405191826120e4565b828152601f196129ce8294612107565b0190602036910137565b91908110156126d55760051b0190565b908215612b1f576001600160a01b0316918215918215612ae857813403612ac0577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b036020925b1693845f526001835260405f20865f52835260405f20612a5883825461257f565b905515612a69575b604051908152a3565b612abb6040517f23b872dd000000000000000000000000000000000000000000000000000000008482015233602482015230604482015282606482015260648152612ab56084826120e4565b86614666565b612a60565b7faa7feadc000000000000000000000000000000000000000000000000000000005f5260045ffd5b34612ac0577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b03602092612a37565b7f2c5211c6000000000000000000000000000000000000000000000000000000005f5260045ffd5b9060028110156126d55760011b01905f90565b908154612b6681612107565b92612b7460405194856120e4565b81845260208401905f5260205f205f915b838310612b925750505050565b60036020600192604051612ba581612074565b6001600160a01b0386541681526001600160a01b0385870154168382015260028601546040820152815201920192019190612b85565b90604051612be8816120ac565b809260ff815416612bf8816121ea565b8252600181015460208301526040516002820180545f91612c1882612808565b8085529160018116908115612d135750600114612ccf575b505090612c42816004949303826120e4565b6040840152612c5360038201612b5a565b606084015201908154612c6581612107565b92612c7360405194856120e4565b81845260208401905f5260205f205f915b838310612c95575050505060800152565b60036020600192604051612ca881612074565b60ff8654168152848601548382015260028601546040820152815201920192019190612c84565b5f9081526020812094939250905b808210612cf75750919250908101602001612c4282612c30565b9192936001816020925483858801015201910190939291612cdd565b60ff191660208087019190915292151560051b85019092019250612c429150839050612c30565b8054600110156126d5575f52600160205f2001905f90565b80548210156126d5575f5260205f2001905f90565b90604051612d74816120c8565b6020600182946001600160a01b0381541684520154910152565b9190612dbc576020816001600160a01b03806001945116166001600160a01b03198554161784550151910155565b634e487b7160e01b5f525f60045260245ffd5b60028210156126d55701905f90565b9035601e198236030181121561023757016020813591019167ffffffffffffffff821161023757606082023603831361023757565b8035600481101561023757612e27816121ea565b8252602081013560208301526040810135601e198236030181121561023757810160208135910167ffffffffffffffff82116102375781360381136102375781601f1992601f9260a060408801528160a088015260c08701375f60c08287010152011682019060c082019160e0612ea16060840184612dde565b86840360c0016060880152948590529101925f5b818110612f2757505050612ecf8160806020930190612dde565b92909360808183039101528281520191905f5b818110612eef5750505090565b90919260608060019260ff612f03886123b7565b16815260208701356020820152604087013560408201520194019101919091612ee2565b9091936060806001926001600160a01b03612f4189612135565b1681526001600160a01b03612f5860208a01612135565b16602082015260408881013590820152019501929101612eb5565b929190612f8a602091604086526040860190612e13565b930152565b91612fa5612fb392606085526060850190612725565b908382036020850152612e13565b906040818303910152828152602081019260208160051b83010193835f91609e1982360301945b848410612feb575050505050505090565b90919293949596601f1982820301835287358781121561023757602061301660019387839401612e13565b990193019401929195949390612fda565b91939290825f525f60205260405f20600381019560ff87541690600582101561035f5781156136e6575f946003831480156136d9575b61096a578435916004831015968761023757613078846121ea565b600384146106b95761309661308c87612602565b6110cd368a612439565b6040516130a2816120c8565b8754156126d557875f526001600160a01b0360205f20541681526001600160a01b0380613142816130d28c612d3a565b90549060031b1c1694602085019586526130ed366064612400565b9060405160208101918252604080820152600960608201527f6368616c6c656e6765000000000000000000000000000000000000000000000060808201526080815261313a60a0826120e4565b5190206146d3565b92511691169081141591826136c4575b505061369c57600f86019460ff8654169161035f576001146135f5578790613179816121ea565b600181036134a257506102375761318f836121ea565b6001830361340e5750506131b56131a63686612439565b6131af84612bdb565b9061420e565b156106b9575b6131d667ffffffffffffffff600185015460a01c164261257f565b9485600e850155610237576131ea916127ef565b6020820135601082015560118101613205604084018461265e565b9067ffffffffffffffff821161067d576132238261047d8554612808565b5f90601f83116001146133aa5761325092915f918361339f5750508160011b915f199060031b1c19161790565b90555b60128101613264606084018461258c565b9190613270838361289b565b905f5260205f205f915b838310613339575050505060130194613296608083018361258c565b906132a1828961291d565b965f5260205f205f975b8289106132f9575050507f2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d0375693949550600360ff198254161790556132f460405192839283612f73565b0390a2565b80359060ff8216820361023757606060039160ff6001941660ff1986541617855560208101358486015560408101356002860155019201980197906132ab565b60036060826001600160a01b03613351600195612909565b166001600160a01b031986541617855561336d60208201612909565b6001600160a01b0385870191166001600160a01b0319825416179055604081013560028601550192019201919061327a565b013590505f8061049d565b601f19831691845f5260205f20925f5b8181106133f657509084600195949392106133dd575b505050811b019055613253565b01355f19600384901b60f8161c191690555f80806133d0565b919360206001819287870135815501950192016133ba565b6001600160a01b036001860154169161343361342a3689612439565b610c5f87612bdb565b156106b95761345d926020926040518095819482936305b959ef60e01b84528c8c60048601612f8f565b03915afa908115610ef6575f91613483575b506131bb5763baf3f0f760e01b5f5260045ffd5b61349c915060203d602011610eef57610ee181836120e4565b5f61346f565b90506134ad816121ea565b8061356e575086610237576134c1836121ea565b826106b9576134dc6134d33688612439565b6131af86612bdb565b156134ea575b50505b6131bb565b6001600160a01b036001860154169161350661342a3689612439565b156106b957613530926020926040518095819482936305b959ef60e01b84528c8c60048601612f8f565b03915afa908115610ef6575f9161354f575b50156106b9575f806134e2565b613568915060203d602011610eef57610ee181836120e4565b5f613542565b600291975061357c816121ea565b036106b95761358a826121ea565b600182146106b9575f9561359d836121ea565b826135be576001600160a01b036001860154169161343361342a3689612439565b505093505f936135cd816121ea565b600281036106b9576135e26131a63686612439565b6134e55763baf3f0f760e01b5f5260045ffd5b5050505091939495505061361791506131af6136113685612439565b91612bdb565b156106b9576132f48161365461054961198c60607f3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c91096018461258c565b837f2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d0375660405180613685428683612f73565b0390a2604051918291602083526020830190612e13565b7f61a44f6e000000000000000000000000000000000000000000000000000000005f5260045ffd5b516001600160a01b0316141590505f80613152565b505f95506004831461305d565b856379c1d89f60e11b5f5260045260245ffd5b903590601e1981360301821215610237570180359067ffffffffffffffff821161023757602001918160051b3603831361023757565b3567ffffffffffffffff811681036102375790565b359067ffffffffffffffff8216820361023757565b919091608081840312610237576040519061377382612090565b819381359067ffffffffffffffff8211610237578261379b606094926137c594869401612149565b85526137a960208201612135565b60208601526137ba60408201613744565b604086015201613744565b910152565b91908110156126d5576060020190565b9060026137e783806136f9565b905014801590613f7e575b8015613f50575b8015613f07575b61154157602082016001600160a01b0361381982612909565b1615613edf576040830192610e1067ffffffffffffffff6138398661372f565b1610613eb7578235600481101561023757613853816121ea565b600181036106b957602084013594856106b9576138786138733685613759565b614547565b95865f525f60205260ff600360405f20015416600581101561035f5761096a576138af6138a53686613759565b6110cd3689612439565b90608087019160016138c1848a61258c565b905003610691576138d2838961258c565b919091156126d5576138e487806136f9565b919091156126d55761390e6001600160a01b0392916110fa6139068594612909565b953690612400565b92169116036106915760608701916002613928848a61258c565b905003611fa157885f525f60205260405f209161394587806136f9565b9067ffffffffffffffff821161067d5768010000000000000000821161067d578454828655808310613e9b575b50845f5260205f205f5b838110613e805750505050600183016001600160a01b0361399c8a612909565b166001600160a01b03198254161781556139b58661372f565b7fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff7bffffffffffffffff000000000000000000000000000000000000000083549260a01b169116179055613a736002840196606089019767ffffffffffffffff613a1e8a61372f565b82547fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000016911617905560038501805460ff191660011790556004850180546001600160a01b03191633179055600f85016127ef565b601083015560118201613a8960408a018a61265e565b9067ffffffffffffffff821161067d57613aa78261047d8554612808565b5f90601f8311600114613e1c57613ad492915f918361339f5750508160011b915f199060031b1c19161790565b90555b60128201613ae5848a61258c565b9190613af1838361289b565b905f5260205f205f915b838310613db65750505050613b1460138301918961258c565b90613b1f828461291d565b915f5260205f205f925b828410613d7657505050505f91600a600683019201925b60028110613ce9575050613b56613b8191612d67565b80929060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b613b8b84806136f9565b919091156126d5576001600160a01b03613ba7613bd893612909565b165f526001602052613bbf88600160405f20016146fc565b5060206001600160a01b03825116910151908833614154565b604051936040855260c08501938035601e198236030181121561023757016020813591019467ffffffffffffffff8211610237578160051b36038613610237576080604088015281905260e0860195889590949392915f5b818110613cb5575050509467ffffffffffffffff613c9a859482613c8f613caf966001600160a01b03613c847f7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c398429c9d612135565b1660608a0152613744565b166080870152613744565b1660a084015282810360208401523396612e13565b0390a390565b91965091929394966020806001926001600160a01b03613cd48b612135565b16815201970191019189969795949392613c30565b80613d4e8a6040613d1f84613d0d88613d196020613d1360019b613d0d858b61258c565b906137ca565b01612909565b9561258c565b01356001600160a01b0360405192613d36846120c8565b1682526020820152613d488387612b47565b90612d8e565b613d70604051613d5d816120c8565b5f81525f6020820152613d488388612b47565b01613b40565b80359060ff8216820361023757606060039160ff6001941660ff198654161785556020810135848601556040810135600286015501920193019290613b29565b60036060826001600160a01b03613dce600195612909565b166001600160a01b0319865416178555613dea60208201612909565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190613afb565b601f19831691845f5260205f20925f5b818110613e685750908460019594939210613e4f575b505050811b019055613ad7565b01355f19600384901b60f8161c191690555f8080613e42565b91936020600181928787013581550195019201613e2c565b6001906020613e8e85612909565b940193818401550161397c565b613eb190865f528360205f209182019101612840565b5f613972565b7fb4e12433000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fea9e70ce000000000000000000000000000000000000000000000000000000005f5260045ffd5b50613f1282806136f9565b156126d557613f2090612909565b613f2a83806136f9565b600110156126d5576001600160a01b03613f476020829301612909565b16911614613800565b50613f5b82806136f9565b600110156126d557613f7760206001600160a01b039201612909565b16156137f9565b50613f8982806136f9565b156126d557613f9f6001600160a01b0391612909565b16156137f2565b60028151036106b9576001600160a01b036020613fd18282613fc7866126c8565b51015116936126e9565b51015116036106b957565b906080613fe9828461406d565b91019160028351510361404b575f5b600281106140095750505050600190565b6140148185516126f9565b516001600160a01b036140358161402c8587516126f9565b511692866146d3565b160361404357600101613ff8565b505050505f90565b5050505f90565b9190915f8382019384129112908015821691151617611b4057565b61407690614547565b90805190614083826121ea565b6020810151916140c86060604084015193015192604051948593602085019788526140ad816121ea565b6040850152606084015260a0608084015260c08301906121f4565b91601f198284030160a0830152602080825194858152019101925f5b818110614106575050614100925003601f1981018352826120e4565b51902090565b91600191935061414560209186519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b940191019184929391936140e4565b8315614208576001600160a01b03165f52600160205260405f206001600160a01b0383165f528060205260405f20548481106141d8578461419491612691565b906001600160a01b0384165f5260205260405f20555f525f6020526001600160a01b03601460405f200191165f526020526141d460405f2091825461257f565b9055565b84907fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b50505050565b6040516142398161422b6020820194602086526040830190612219565b03601f1981018352826120e4565b5190209060405161425a8161422b6020820194602086526040830190612219565b5190201490565b60048101905b818110614272575050565b5f8082556001820155600201614267565b90815f525f60205260405f209060038201600460ff1982541617905560028151036106b9575f5b600281106144655750505f5b6002811061442c5750505f525f60205260405f2080545f825580614412575b505f60018201555f60028201555f6003820155614301600682016142fc8160048501612840565b614261565b61430d600a8201614261565b5f600e8201555f600f8201555f60108201556011810161432d8154612808565b90816143cf575b5050601281018054905f815581614396575b50506013018054905f81558161435a575050565b81600302916003830403611b40575f5260205f20908101905b81811061437e575050565b805f600392555f60018201555f600282015501614373565b81600302916003830403611b40575f5260205f20908101905b8181101561434657805f600392555f60018201555f6002820155016143af565b81601f5f93116001146143e65750555b5f80614334565b8183526020832061440291601f0160051c810190600101612840565b80825281602081209155556143df565b61442690825f5260205f2090810190612840565b5f6142d5565b806001600160a01b0361444160019385612d52565b90549060031b1c165f528160205261445e848360405f2001614764565b50016142b6565b8061447c614475600193856126f9565b51866145b3565b016142aa565b602060405180927fcc2a842d00000000000000000000000000000000000000000000000000000000825260406004830152816001600160a01b03816144df6144cd604483018a612219565b6003198382030160248401528a612219565b0392165afa5f918161450a575b506144ff57506020809101519101511090565b90505f8092500b1390565b9091506020813d60201161453f575b81614526602093836120e4565b810103126102375751805f0b810361023757905f6144ec565b3d9150614519565b8051906141006001600160a01b036020830151169167ffffffffffffffff606081604084015116920151169260405193849261458f602085019760a0895260c08601906121ae565b926040850152606084015260808301524660a083015203601f1981018352826120e4565b906040810191825115614661575f525f602052601460405f20019160208201916001600160a01b0380845116165f528360205260405f205493841561465a576001600160a01b0392518086115f1461464f57614610908096612691565b908380865116165f5260205260405f205551165f5260016020526001600160a01b038060405f20925116165f526020526141d460405f2091825461257f565b506146108580612691565b5050505050565b505050565b905f602091828151910182855af115610ef6575f513d6146ca57506001600160a01b0381163b155b6146955750565b6001600160a01b03907f5274afe7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b6001141561468e565b6146f0906146f99260ff8151166040602083015192015192614821565b909291926148a3565b90565b6001810190825f528160205260405f2054155f1461404b5780546801000000000000000081101561067d5761475161473b826001879401855584612d52565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b906001820191815f528260205260405f20548015155f14614043575f198101818111611b405782545f19810191908211611b40578181036147ec575b505050805480156147d8575f1901906147b98282612d52565b8154905f199060031b1b19169055555f526020525f6040812055600190565b634e487b7160e01b5f52603160045260245ffd5b61480c6147fc61473b9386612d52565b90549060031b1c92839286612d52565b90555f528360205260405f20555f80806147a0565b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411614898579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610ef6575f516001600160a01b0381161561488e57905f905f90565b505f906001905f90565b5050505f9160039190565b6148ac816121ea565b806148b5575050565b6148be816121ea565b600181036148ee577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b6148f7816121ea565b6002810361492b57507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b600390614937816121ea565b1461493f5750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffdfea26469706673582212201a22c42288f1ba0e9314d3b1e6e8b7aa4d4a62d815a181d9b949bb05ac25096564736f6c634300081b0033", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes\",\"name\":\"challengerSig\",\"type\":\"bytes\"}],\"name\":\"challenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"checkpoint\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"name\":\"close\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"create\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"deposit\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"ch\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"depositAndCreate\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"eip712Domain\",\"outputs\":[{\"internalType\":\"bytes1\",\"name\":\"fields\",\"type\":\"bytes1\"},{\"internalType\":\"string\",\"name\":\"name\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"version\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"chainId\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"verifyingContract\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"salt\",\"type\":\"bytes32\"},{\"internalType\":\"uint256[]\",\"name\":\"extensions\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getAccountsBalances\",\"outputs\":[{\"internalType\":\"uint256[][]\",\"name\":\"\",\"type\":\"uint256[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"address[]\",\"name\":\"tokens\",\"type\":\"address[]\"}],\"name\":\"getChannelBalances\",\"outputs\":[{\"internalType\":\"uint256[]\",\"name\":\"balances\",\"type\":\"uint256[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"getChannelData\",\"outputs\":[{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"internalType\":\"enumChannelStatus\",\"name\":\"status\",\"type\":\"uint8\"},{\"internalType\":\"address[]\",\"name\":\"wallets\",\"type\":\"address[]\"},{\"internalType\":\"uint256\",\"name\":\"challengeExpiry\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"lastValidState\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"accounts\",\"type\":\"address[]\"}],\"name\":\"getOpenChannels\",\"outputs\":[{\"internalType\":\"bytes32[][]\",\"name\":\"\",\"type\":\"bytes32[][]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"sig\",\"type\":\"bytes\"}],\"name\":\"join\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState\",\"name\":\"candidate\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"internalType\":\"structState[]\",\"name\":\"proofs\",\"type\":\"tuple[]\"}],\"name\":\"resize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"expiration\",\"type\":\"uint256\"}],\"name\":\"Challenged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"state\",\"type\":\"tuple\"}],\"name\":\"Checkpointed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"finalState\",\"type\":\"tuple\"}],\"name\":\"Closed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"components\":[{\"internalType\":\"address[]\",\"name\":\"participants\",\"type\":\"address[]\"},{\"internalType\":\"address\",\"name\":\"adjudicator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"challenge\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"nonce\",\"type\":\"uint64\"}],\"indexed\":false,\"internalType\":\"structChannel\",\"name\":\"channel\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"enumStateIntent\",\"name\":\"intent\",\"type\":\"uint8\"},{\"internalType\":\"uint256\",\"name\":\"version\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"destination\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"internalType\":\"structAllocation[]\",\"name\":\"allocations\",\"type\":\"tuple[]\"},{\"internalType\":\"bytes[]\",\"name\":\"sigs\",\"type\":\"bytes[]\"}],\"indexed\":false,\"internalType\":\"structState\",\"name\":\"initial\",\"type\":\"tuple\"}],\"name\":\"Created\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Deposited\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"EIP712DomainChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"Joined\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"Opened\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"int256[]\",\"name\":\"deltaAllocations\",\"type\":\"int256[]\"}],\"name\":\"Resized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"wallet\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Withdrawn\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"ChallengeNotExpired\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ChannelNotFinal\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"channelId\",\"type\":\"bytes32\"}],\"name\":\"ChannelNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DepositAlreadyFulfilled\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"expectedFulfilled\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"actualFulfilled\",\"type\":\"uint256\"}],\"name\":\"DepositsNotFulfilled\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ECDSAInvalidSignature\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"length\",\"type\":\"uint256\"}],\"name\":\"ECDSAInvalidSignatureLength\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"ECDSAInvalidSignatureS\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"available\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"required\",\"type\":\"uint256\"}],\"name\":\"InsufficientBalance\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAdjudicator\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAllocations\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidAmount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengePeriod\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChallengerSignature\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidParticipant\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidShortString\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidState\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStateSignatures\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStatus\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidValue\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"name\":\"SafeERC20FailedOperation\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"str\",\"type\":\"string\"}],\"name\":\"StringTooLong\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"TransferFailed\",\"type\":\"error\"}]", + Bin: "0x0x610160604052346101365760405161001860408261013a565b601181526020810190704e6974726f6c6974653a437573746f647960781b82526040519161004760408461013a565b600683526020830191650c0b8c8b8c8d60d21b835261006581610171565b6101205261007284610307565b61014052519020918260e05251902080610100524660a0526040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a081526100db60c08261013a565b5190206080523060c0526040516153549081610440823960805181614a97015260a05181614b4e015260c05181614a68015260e05181614ae601526101005181614b0c01526101205181611068015261014051816110940152f35b5f80fd5b601f909101601f19168101906001600160401b0382119082101761015d57604052565b634e487b7160e01b5f52604160045260245ffd5b908151602081105f146101eb575090601f8151116101ab57602081519101516020821061019c571790565b5f198260200360031b1b161790565b604460209160405192839163305a27a960e01b83528160048401528051918291826024860152018484015e5f828201840152601f01601f19168101030190fd5b6001600160401b03811161015d575f54600181811c911680156102fd575b60208210146102e957601f81116102b7575b50602092601f821160011461025857928192935f9261024d575b50508160011b915f199060031b1c1916175f5560ff90565b015190505f80610235565b601f198216935f8052805f20915f5b86811061029f5750836001959610610287575b505050811b015f5560ff90565b01515f1960f88460031b161c191690555f808061027a565b91926020600181928685015181550194019201610267565b5f8052601f60205f20910160051c810190601f830160051c015b8181106102de575061021b565b5f81556001016102d1565b634e487b7160e01b5f52602260045260245ffd5b90607f1690610209565b908151602081105f14610332575090601f8151116101ab57602081519101516020821061019c571790565b6001600160401b03811161015d57600154600181811c91168015610435575b60208210146102e957601f8111610402575b50602092601f82116001146103a157928192935f92610396575b50508160011b915f199060031b1c19161760015560ff90565b015190505f8061037d565b601f1982169360015f52805f20915f5b8681106103ea57508360019596106103d2575b505050811b0160015560ff90565b01515f1960f88460031b161c191690555f80806103c4565b919260206001819286850151815501940192016103b1565b60015f52601f60205f20910160051c810190601f830160051c015b81811061042a5750610363565b5f815560010161041d565b90607f169061035156fe60806040526004361015610011575f80fd5b5f3560e01c8062e2bb2c146123555780631474e410146122d1578063183b499814611af05780632f33c4d6146119365780634a7e7798146118ca5780635a9eb80e146118285780637f9ebbd7146111705780638340f5491461113957806384b0196e14611050578063bab3290a14610a6e578063d710e92f146108e0578063e617208c1461078d578063ecf668fd146102455763f3fef3a3146100b2575f80fd5b34610241576040600319360112610241576100cb6123cb565b60243590335f52600360205260405f20906001600160a01b0381165f528160205260405f205491838310610211576001600160a01b0392508282165f5260205260405f2061011a8482546137c5565b90551690816101bf575f80808084335af13d156101ba573d61013b8161275c565b90610149604051928361251b565b81525f60203d92013e5b15610187575b6040519081527fd1c19fbcd4551a5edfb66d43d2e337c04837afda3482b42bdf569a8fccdae5fb60203392a3005b907fbf182be8000000000000000000000000000000000000000000000000000000005f526004523360245260445260645ffd5b610153565b61020c6040517fa9059cbb0000000000000000000000000000000000000000000000000000000060208201523360248201528260448201526044815261020660648261251b565b83614fc1565b610159565b50507fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b5f80fd5b346102415761025336612454565b835f52600260205260405f2091600383019160ff835416906005821015610779578115610766576004821461073e578535926004841015610241576102978461264f565b8361064257600f86019260ff84541690600181145f146102d9577ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b60020361068b57506001600160a01b036001870154169161030d6102fd368a612854565b61030686612af3565b9085614858565b15610642576103379260209260405180958194829363030232af60e21b84528d8d60048601612e43565b03915afa908115610680575f91610651575b501561064257610364925b600260ff19825416179055612ee8565b602082013560108201556011810161037f6040840184612f01565b9067ffffffffffffffff8211610559576103a38261039d85546129b8565b85612f4a565b5f90601f83116001146105de576103d192915f91836105d3575b50508160011b915f199060031b1c19161790565b90555b601281016103e56060840184612bb1565b91906103f18383612f8f565b905f5260205f205f915b83831061056d578686601387016104156080830183613011565b90916104218282613095565b5f9081526020812092805b83831061047257867f8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c361046d88604051918291602083526020830190612c6e565b0390a2005b61047c8183612f01565b9067ffffffffffffffff8211610559576104a08261049a89546129b8565b89612f4a565b5f90601f83116001146104ef57926104d5836001959460209487965f926104e45750508160011b915f199060031b1c19161790565b88555b0195019201919361042c565b013590508d806103bd565b601f19831691885f5260205f20925f5b8181106105415750936020936001969387969383889510610528575b505050811b0188556104d8565b01355f19600384901b60f8161c191690558c808061051b565b919360206001819287870135815501950192016104ff565b634e487b7160e01b5f52604160045260245ffd5b60036060826001600160a01b03610585600195612ffd565b166001600160a01b03198654161785556105a160208201612ffd565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906103fb565b0135905087806103bd565b601f19831691845f5260205f20925f5b81811061062a5750908460019594939210610611575b505050811b0190556103d4565b01355f19600384901b60f8161c19169055868080610604565b919360206001819287870135815501950192016105ee565b63baf3f0f760e01b5f5260045ffd5b610673915060203d602011610679575b61066b818361251b565b810190612de4565b87610349565b503d610661565b6040513d5f823e3d90fd5b909160206106bf916001600160a01b0360018a01541694604051938492839263030232af60e21b84528d8d60048601612e43565b0381865afa908115610680575f9161071f575b5015610642576106e18161264f565b156106f7575b50610364925f600e860155610354565b610714906107053688612854565b61070e84612af3565b91614858565b1561064257866106e7565b610738915060203d6020116106795761066b818361251b565b896106d2565b7ff525e320000000000000000000000000000000000000000000000000000000005f5260045ffd5b866379c1d89f60e11b5f5260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b34610241576020600319360112610241575f60606040516107ad816124c7565b81815282602082015282604082015201526107c66137d2565b506004355f52600260205260405f206107de81613770565b60ff60038301541691604051906107f660608361251b565b600282526040366020840137600481015f5b600281106108b2575050610823600f600e8301549201612af3565b6040519460a0865267ffffffffffffffff606061084d8751608060a08b01526101208a0190612613565b966001600160a01b0360208201511660c08a01528260408201511660e08a015201511661010087015260058110156107795785946108ae9461089b9260208801528682036040880152612613565b9160608501528382036080850152612659565b0390f35b806001600160a01b036108c76001938561394b565b90549060031b1c166108d9828761382d565b5201610808565b346102415760206003193601126102415760043567ffffffffffffffff811161024157610911903690600401612556565b8051906109366109208361253e565b9261092e604051948561251b565b80845261253e565b90610949601f19602085019301836138c4565b5f5b81518110156109dd576001600160a01b03610966828461382d565b51165f526003602052600160405f20016040519081602082549182815201915f5260205f20905f905b8082106109c557505050906109a98160019493038261251b565b6109b3828761382d565b526109be818661382d565b500161094b565b9091926001602081928654815201940192019061098f565b50509060405191829160208301906020845251809152604083019060408160051b85010192915f905b828210610a1557505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b808210610a56575050506020806001929601920192018594939192610a06565b90919260208060019286518152019401920190610a36565b346102415760606003193601126102415760043560443567ffffffffffffffff811161024157610aa2903690600401612426565b825f93929352600260205260405f2090600382019060ff825416600581101561077957801561103d575f190161073e5760016024350361101557600d830154610fed57600f830194610b2b610af687612af3565b610afe614a5e565b906001600160a01b03610b10886143d0565b90549060031b1c169188610b25368888612778565b92614b74565b15610fc557610b3986612af3565b90610b8960405191610b4c60608461251b565b60028352610b5e6040602085016138c4565b6080840194610b6d86516137fc565b51610b77856137fc565b52610b81846137fc565b503691612778565b610b928261381d565b52610b9c8161381d565b50825260018401546001600160a01b0316604051906020610bbd818461251b565b5f8352601f19015f5b818110610fae575050610bf391602091604051808095819463030232af60e21b8352888c60048501613841565b03915afa908115610680575f91610f8f575b501561064257610c71610c1a600886016139be565b96610c4c88600c88019060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b6005860180546001600160a01b03191633179055825190610c6c8261264f565b612ee8565b6020810151601085015560118401604082015180519067ffffffffffffffff821161055957610ca48261039d85546129b8565b602090601f8311600114610f2c57610cd292915f9183610f215750508160011b915f199060031b1c19161790565b90555b606060128501910151906020825192610cee8484612f8f565b01905f5260205f205f915b838310610ebe57505050506013830190516020815191610d198385613095565b01915f5260205f20915f905b828210610ddd57845460ff19166002178555602087610d858a6001600160a01b03610d4f8b6143d0565b90549060031b1c165f5260038452610d6d83600160405f2001614f59565b50836001600160a01b03825116910151908333614d06565b807fe8e915db7b3549b9e9e9b3e2ec2dc3edd1f76961504366998824836401f6846a8360405160018152a260405190807fd087f17acc177540af5f382bc30c65363705b90855144d285a822536ee11fdd15f80a28152f35b805180519067ffffffffffffffff821161055957610e0582610dff88546129b8565b88612f4a565b602090601f8311600114610e555792610e3b836001959460209487965f92610e4a5750508160011b915f199060031b1c19161790565b87555b01940191019092610d25565b015190508e806103bd565b90601f19831691875f52815f20925f5b818110610ea65750936020936001969387969383889510610e8e575b505050811b018755610e3e565b01515f1960f88460031b161c191690558d8080610e81565b92936020600181928786015181550195019301610e65565b60036020826040600194516001600160a01b0380825116166001600160a01b03198854161787556001600160a01b0384820151166001600160a01b0387890191166001600160a01b03198254161790550151600286015501920192019190610cf9565b015190508a806103bd565b90601f19831691845f52815f20925f5b818110610f775750908460019594939210610f5f575b505050811b019055610cd5565b01515f1960f88460031b161c19169055898080610f52565b92936020600181928786015181550195019301610f3c565b610fa8915060203d6020116106795761066b818361251b565b87610c05565b602090610fb96137d2565b82828701015201610bc6565b7f773a750f000000000000000000000000000000000000000000000000000000005f5260045ffd5b7f1b136079000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fa145c43e000000000000000000000000000000000000000000000000000000005f5260045ffd5b846379c1d89f60e11b5f5260045260245ffd5b34610241575f6003193601126102415761110b61108c7f000000000000000000000000000000000000000000000000000000000000000061502e565b6108ae6110b87f000000000000000000000000000000000000000000000000000000000000000061509f565b611119604051916110ca60208461251b565b5f83525f3681376040519586957f0f00000000000000000000000000000000000000000000000000000000000000875260e0602088015260e08701906125ee565b9085820360408701526125ee565b904660608501523060808501525f60a085015283820360c08501526125bb565b60606003193601126102415761114d6123cb565b602435906001600160a01b03821682036102415761116e9160443591614271565b005b346102415761117e36612454565b5050815f52600260205260405f2060ff600382015416600581101561077957801561181557600281036114e5575081356004811015610241576111c08161264f565b60038103610642576020830135801561064257608084019160026111e48487613011565b905003610fc5576112076111f785613770565b6112013688612854565b90614953565b15610fc55761121990600f8501612ee8565b60108301556011820161122f6040850185612f01565b9067ffffffffffffffff82116105595761124d8261039d85546129b8565b5f90601f83116001146114815761127a92915f91836114765750508160011b915f199060031b1c19161790565b90555b6012820161128e6060850185612bb1565b919061129a8383612f8f565b905f5260205f205f915b83831061141057505050506112bd601383019184613011565b90916112c98282613095565b5f9081526020812092805b83831061132f5750505050507fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a704969161131b611315601261046d945b01612a72565b856146b8565b604051918291602083526020830190612c6e565b6113398183612f01565b9067ffffffffffffffff8211610559576113578261049a89546129b8565b5f90601f83116001146113a6579261138c836001959460209487965f9261139b5750508160011b915f199060031b1c19161790565b88555b019501920191936112d4565b013590508e806103bd565b601f19831691885f5260205f20925f5b8181106113f857509360209360019693879693838895106113df575b505050811b01885561138f565b01355f19600384901b60f8161c191690558d80806113d2565b919360206001819287870135815501950192016113b6565b60036060826001600160a01b03611428600195612ffd565b166001600160a01b031986541617855561144460208201612ffd565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906112a4565b0135905088806103bd565b601f19831691845f5260205f20925f5b8181106114cd57509084600195949392106114b4575b505050811b01905561127d565b01355f19600384901b60f8161c191690558780806114a7565b91936020600181928787013581550195019201611491565b60030361073e57600e810180544210156117e157823560048110156102415761150d8161264f565b600381036106425761152b61152184613770565b6112013687612854565b15610fc5575f61153f9255600f8301612ee8565b602082013560108201556011810161155a6040840184612f01565b9067ffffffffffffffff8211610559576115788261039d85546129b8565b5f90601f831160011461177d576115a592915f91836105d35750508160011b915f199060031b1c19161790565b90555b601281016115b96060840184612bb1565b91906115c58383612f8f565b905f5260205f205f915b8383106117175750505050601381016115eb6080840184613011565b90916115f78282613095565b5f9081526020812092805b8383106116415750505050507fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a704969161131b611315601261046d9461130f565b61164b8183612f01565b9067ffffffffffffffff8211610559576116698261049a89546129b8565b5f90601f83116001146116ad579261169e836001959460209487965f9261139b5750508160011b915f199060031b1c19161790565b88555b01950192019193611602565b601f19831691885f5260205f20925f5b8181106116ff57509360209360019693879693838895106116e6575b505050811b0188556116a1565b01355f19600384901b60f8161c191690558d80806116d9565b919360206001819287870135815501950192016116bd565b60036060826001600160a01b0361172f600195612ffd565b166001600160a01b031986541617855561174b60208201612ffd565b6001600160a01b0385870191166001600160a01b031982541617905560408101356002860155019201920191906115cf565b601f19831691845f5260205f20925f5b8181106117c957509084600195949392106117b0575b505050811b0190556115a8565b01355f19600384901b60f8161c191690558680806117a3565b9193602060018192878701358155019501920161178d565b507fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a704969161131b611315601261046d9461130f565b836379c1d89f60e11b5f5260045260245ffd5b346102415760406003193601126102415760043560243567ffffffffffffffff81116102415761185c903690600401612556565b61186681516138e0565b5f5b82518110156118b457600190845f526002602052601460405f20016001600160a01b0380611896848861382d565b5116165f5260205260405f20546118ad828561382d565b5201611868565b604051602080825281906108ae908201856125bb565b346102415760406003193601126102415760043567ffffffffffffffff811161024157608060031982360301126102415760243567ffffffffffffffff81116102415760a060031982360301126102415760209161192e91600401906004016139e5565b604051908152f35b346102415760406003193601126102415760043567ffffffffffffffff8111610241576119679036906004016123f5565b60243567ffffffffffffffff8111610241576119879036906004016123f5565b9190926119938261253e565b936119a1604051958661251b565b8285526119ad8361253e565b936119c0601f19602088019601866138c4565b5f5b848110611a5957858760405191829160208301906020845251809152604083019060408160051b85010192915f905b828210611a0057505050500390f35b9193909294603f19908203018252845190602080835192838152019201905f905b808210611a415750505060208060019296019201920185949391926119f1565b90919260208060019286518152019401920190611a21565b611a62826138e0565b611a6c828961382d565b52611a77818861382d565b505f5b828110611a8a57506001016119c2565b6001906001600160a01b03611aa8611aa3858a8a613911565b612ffd565b165f52600360205260405f206001600160a01b03611aca611aa384888a613911565b165f5260205260405f2054611ae982611ae3868d61382d565b5161382d565b5201611a7a565b3461024157611afe36612454565b9190835f52600260205260405f209360ff60038601541660058110156107795780156122be577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0161073e57831561064257823592600484101561024157611b658461264f565b6002840361064257823593609e1984360301948581121561024157611b8d9085013690612854565b966020830135602089015160018101809111611d81578103610642576060890193611bb8855161491d565b6060810191611bd9611bd4611bcd8585612bb1565b36916127cc565b61491d565b611bef611be585613770565b6112013685612854565b15610fc557611c016040830183612f01565b810195906020818803126102415780359067ffffffffffffffff821161024157019686601f8901121561024157873596611c3a8861253e565b98611c486040519a8b61251b565b888a5260208a01906020829a60051b82010192831161024157602001905b8282106122ae5750505051611c7e611bcd8686612bb1565b90600289510361064257611cb0816040611ca781611c9e611ce7966137fc565b5101519261381d565b51015190612edb565b91611ce1611cd1611cc08c6137fc565b51611cca8d61381d565b51906149d1565b916040611ca781611c9e846137fc565b926149d1565b03612286575f198b019b8b8d11611d8157611d018d61253e565b9c806040519e8f90611d13908261251b565b52611d1d9061253e565b601f19018d5f5b82811061226d5750505060015b8c811015611d95578060051b8b01358c811215610241575f19820191908c01818311611d81578f92600193611d6a611d7a933690612854565b611d74838361382d565b5261382d565b5001611d31565b634e487b7160e01b5f52601160045260245ffd5b5060208d611dc9926001600160a01b0360018a0154169060405180958194829363030232af60e21b84528d60048501613841565b03915afa908115610680575f9161224e575b501561064257611dee90600f8601612ee8565b601084015560118301611e046040830183612f01565b9067ffffffffffffffff821161055957611e228261039d85546129b8565b5f90601f83116001146121ea57611e4f92915f91836121df5750508160011b915f199060031b1c19161790565b90555b60128301611e608383612bb1565b9190611e6c8383612f8f565b905f5260205f205f915b838310612179575050505060138301611e926080830183613011565b9091611e9e8282613095565b5f9081526020812092805b838310612098575050505050611ec291611bcd91612bb1565b9060068101916001600160a01b038354165f5b6002811061203f57505f5b60028110611f9f575050600a5f9201915b60028110611f5c575050505060405191602083019060208452518091526040830191905f5b818110611f4657857ff3b6c524f73df7344d9fcf2f960a57aba7fba7e292d8b79ed03d786f7b2b112f86860387a2005b8251845260209384019390920191600101611f16565b806040611f6b6001938561382d565b51015182611f79838861396a565b5001556040611f88828561382d565b51015182611f96838761396a565b50015501611ef1565b5f611fad828997969761382d565b5112611fbf575b600101939293611ee0565b6001600160a01b03611fd4826004880161394b565b90549060031b1c1690611fe7818961382d565b517f80000000000000000000000000000000000000000000000000000000000000008114611d81576001926120389160405191612023836124ab565b82528560208301525f0360408201528a614ea5565b9050611fb4565b805f6120506001938a98979861382d565b5113612060575b01939293611ed5565b6120936001600160a01b036120788360048a0161394b565b90549060031b1c16848b61208c858d61382d565b5192614d06565b612057565b6120a28183612f01565b9067ffffffffffffffff8211610559576120c08261049a89546129b8565b5f90601f831160011461210f57926120f5836001959460209487965f926121045750508160011b915f199060031b1c19161790565b88555b01950192019193611ea9565b013590505f806103bd565b601f19831691885f5260205f20925f5b8181106121615750936020936001969387969383889510612148575b505050811b0188556120f8565b01355f19600384901b60f8161c191690555f808061213b565b9193602060018192878701358155019501920161211f565b60036060826001600160a01b03612191600195612ffd565b166001600160a01b03198654161785556121ad60208201612ffd565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190611e76565b013590508a806103bd565b601f19831691845f5260205f20925f5b818110612236575090846001959493921061221d575b505050811b019055611e52565b01355f19600384901b60f8161c19169055898080612210565b919360206001819287870135815501950192016121fa565b612267915060203d6020116106795761066b818361251b565b89611ddb565b602091828261227a6137d2565b92010152018e90611d24565b7f52e4cb1c000000000000000000000000000000000000000000000000000000005f5260045ffd5b8135815260209182019101611c66565b506379c1d89f60e11b5f5260045260245ffd5b346102415760806003193601126102415760243567ffffffffffffffff81116102415760a060031982360301126102415760443567ffffffffffffffff8111610241576123229036906004016123f5565b916064359267ffffffffffffffff84116102415761234761116e943690600401612426565b9390926004016004356130e1565b6080600319360112610241576123696123cb565b6044359067ffffffffffffffff82116102415760806003198336030112610241576064359167ffffffffffffffff83116102415760a06003198436030112610241576020926123bf61192e936024359033614271565b600401906004016139e5565b600435906001600160a01b038216820361024157565b35906001600160a01b038216820361024157565b9181601f840112156102415782359167ffffffffffffffff8311610241576020808501948460051b01011161024157565b9181601f840112156102415782359167ffffffffffffffff8311610241576020838186019501011161024157565b6060600319820112610241576004359160243567ffffffffffffffff81116102415760a0600319828503011261024157600401916044359067ffffffffffffffff8211610241576124a7916004016123f5565b9091565b6060810190811067ffffffffffffffff82111761055957604052565b6080810190811067ffffffffffffffff82111761055957604052565b60a0810190811067ffffffffffffffff82111761055957604052565b6040810190811067ffffffffffffffff82111761055957604052565b90601f601f19910116810190811067ffffffffffffffff82111761055957604052565b67ffffffffffffffff81116105595760051b60200190565b9080601f8301121561024157813561256d8161253e565b9261257b604051948561251b565b81845260208085019260051b82010192831161024157602001905b8282106125a35750505090565b602080916125b0846123e1565b815201910190612596565b90602080835192838152019201905f5b8181106125d85750505090565b82518452602093840193909201916001016125cb565b90601f19601f602080948051918291828752018686015e5f8582860101520116010190565b90602080835192838152019201905f5b8181106126305750505090565b82516001600160a01b0316845260209384019390920191600101612623565b6004111561077957565b80516126648161264f565b825260208101516020830152612689604082015160a0604085015260a08401906125ee565b906060810151918381036060850152602080845192838152019301905f5b8181106127145750505060800151916080818303910152815180825260208201916020808360051b8301019401925f915b8383106126e757505050505090565b909192939460208061270583601f19866001960301875289516125ee565b970193019301919392906126d8565b909193602061275260019287519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b95019291016126a7565b67ffffffffffffffff811161055957601f01601f191660200190565b9291926127848261275c565b91612792604051938461251b565b829481845281830111610241578281602093845f960137010152565b9080601f83011215610241578160206127c993359101612778565b90565b9291926127d88261253e565b936127e6604051958661251b565b606060208685815201930282019181831161024157925b82841061280a5750505050565b606084830312610241576020606091604051612825816124ab565b61282e876123e1565b815261283b8388016123e1565b83820152604087013560408201528152019301926127fd565b919060a0838203126102415760405161286c816124e3565b80938035600481101561024157825260208101356020830152604081013567ffffffffffffffff811161024157836128a59183016127ae565b6040830152606081013567ffffffffffffffff811161024157810183601f8201121561024157838160206128db933591016127cc565b606083015260808101359067ffffffffffffffff821161024157019180601f8401121561024157823561290d8161253e565b9361291b604051958661251b565b81855260208086019260051b820101918383116102415760208201905b83821061294a57505050505060800152565b813567ffffffffffffffff81116102415760209161296d878480948801016127ae565b815201910190612938565b90602082549182815201915f5260205f20905f5b8181106129995750505090565b82546001600160a01b031684526020909301926001928301920161298c565b90600182811c921680156129e6575b60208310146129d257565b634e487b7160e01b5f52602260045260245ffd5b91607f16916129c7565b5f92918154916129ff836129b8565b8083529260018116908115612a545750600114612a1b57505050565b5f9081526020812093945091925b838310612a3a575060209250010190565b600181602092949394548385870101520191019190612a29565b9050602094955060ff1991509291921683830152151560051b010190565b908154612a7e8161253e565b92612a8c604051948561251b565b81845260208401905f5260205f205f915b838310612aaa5750505050565b60036020600192604051612abd816124ab565b6001600160a01b0386541681526001600160a01b0385870154168382015260028601546040820152815201920192019190612a9d565b90604051612b00816124e3565b6004819360ff815416612b128161264f565b835260018101546020840152604051612b3981612b3281600286016129f0565b038261251b565b6040840152612b4a60038201612a72565b606084015201908154612b5c8161253e565b92612b6a604051948561251b565b81845260208401905f5260205f205f915b838310612b8c575050505060800152565b600160208192604051612ba381612b3281896129f0565b815201920192019190612b7b565b903590601e1981360301821215610241570180359067ffffffffffffffff82116102415760200191606082023603831361024157565b9035601e198236030181121561024157016020813591019167ffffffffffffffff821161024157813603831361024157565b601f8260209493601f1993818652868601375f8582860101520116010190565b9035601e198236030181121561024157016020813591019167ffffffffffffffff8211610241578160051b3603831361024157565b8035600481101561024157612c828161264f565b825260208101356020830152612caf612c9e6040830183612be7565b60a0604086015260a0850191612c19565b906060810135601e19823603018112156102415781016020813591019267ffffffffffffffff82116102415760608202360384136102415784810360608601528181526020019392905f5b818110612d7a57505050806080612d12920190612c39565b90916080818503910152808352602083019260208260051b82010193835f925b848410612d425750505050505090565b909192939495602080612d6a83601f198660019603018852612d648b88612be7565b90612c19565b9801940194019294939190612d32565b909193946060806001926001600160a01b03612d95896123e1565b1681526001600160a01b03612dac60208a016123e1565b1660208201526040888101359082015201969501929101612cfa565b929190612ddf602091604086526040860190612c6e565b930152565b90816020910312610241575180151581036102415790565b9060808152606067ffffffffffffffff6002612e1b6080850186612978565b948260018201546001600160a01b038116602088015260a01c16604086015201541691015290565b91612e59612e6792606085526060850190612dfc565b908382036020850152612c6e565b906040818303910152828152602081019260208160051b83010193835f91609e1982360301945b848410612e9f575050505050505090565b90919293949596601f19828203018352873587811215610241576020612eca60019387839401612c6e565b990193019401929195949390612e8e565b91908201809211611d8157565b90612ef28161264f565b60ff60ff198354169116179055565b903590601e1981360301821215610241570180359067ffffffffffffffff82116102415760200191813603831361024157565b818110612f3f575050565b5f8155600101612f34565b9190601f8111612f5957505050565b612f83925f5260205f20906020601f840160051c83019310612f85575b601f0160051c0190612f34565b565b9091508190612f76565b9068010000000000000000811161055957815491818155828210612fb257505050565b82600302926003840403611d815781600302916003830403611d81575f5260205f2091820191015b818110612fe5575050565b805f600392555f60018201555f600282015501612fda565b356001600160a01b03811681036102415790565b903590601e1981360301821215610241570180359067ffffffffffffffff821161024157602001918160051b3603831361024157565b61305181546129b8565b908161305b575050565b81601f5f931160011461306c575055565b8183526020832061308891601f0160051c810190600101612f34565b8082528160208120915555565b90680100000000000000008111610559578154918181558282106130b857505050565b5f5260205f2091820191015b8181106130cf575050565b806130db600192613047565b016130c4565b93909195949295845f52600260205260405f2091600383019760ff89541692600584101561077957831561375d575f600385148015613751575b61073e5786359360048510159889610241576131368661264f565b600386146106425761316361317161317893613152368d612854565b9361316a8c60405195868092612978565b038561251b565b3691612778565b918c6143fd565b600f86019460ff86541691610779576001146136aa5787906131998161264f565b600181036135575750610241576131af8361264f565b600183036134c35750506131d56131c63686612854565b6131cf84612af3565b90614651565b15610642575b6131f667ffffffffffffffff600185015460a01c1642612edb565b9485600e8501556102415761320a91612ee8565b60208201356010820155601181016132256040840184612f01565b9067ffffffffffffffff8211610559576132438261039d85546129b8565b5f90601f831160011461345f5761327092915f91836121045750508160011b915f199060031b1c19161790565b90555b601281016132846060840184612bb1565b91906132908383612f8f565b905f5260205f205f915b8383106133f95750505050601301946132b66080830183613011565b90966132c28282613095565b5f9081526020812097805b83831061331c57505050507f44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e1793949550600360ff1982541617905561331760405192839283612dc8565b0390a2565b6133268183612f01565b9067ffffffffffffffff82116105595761334b828d61334581546129b8565b90612f4a565b5f90601f831160011461338f5792613380836001959460209487965f926121045750508160011b915f199060031b1c19161790565b8d555b019a01920191986132cd565b601f198316918d5f5260205f20925f5b8181106133e157509360209360019693879693838895106133c8575b505050811b018d55613383565b01355f19600384901b60f8161c191690555f80806133bb565b9193602060018192878701358155019501920161339f565b60036060826001600160a01b03613411600195612ffd565b166001600160a01b031986541617855561342d60208201612ffd565b6001600160a01b0385870191166001600160a01b0319825416179055604081013560028601550192019201919061329a565b601f19831691845f5260205f20925f5b8181106134ab5750908460019594939210613492575b505050811b019055613273565b01355f19600384901b60f8161c191690555f8080613485565b9193602060018192878701358155019501920161346f565b6001600160a01b03600186015416916134e86134df3689612854565b61030687612af3565b15610642576135129260209260405180958194829363030232af60e21b84528c8c60048601612e43565b03915afa908115610680575f91613538575b506131db5763baf3f0f760e01b5f5260045ffd5b613551915060203d6020116106795761066b818361251b565b5f613524565b90506135628161264f565b80613623575086610241576135768361264f565b82610642576135916135883688612854565b6131cf86612af3565b1561359f575b50505b6131db565b6001600160a01b03600186015416916135bb6134df3689612854565b15610642576135e59260209260405180958194829363030232af60e21b84528c8c60048601612e43565b03915afa908115610680575f91613604575b5015610642575f80613597565b61361d915060203d6020116106795761066b818361251b565b5f6135f7565b60029197506136318161264f565b036106425761363f8261264f565b60018214610642575f956136528361264f565b82613673576001600160a01b03600186015416916134e86134df3689612854565b505093505f936136828161264f565b60028103610642576136976131c63686612854565b61359a5763baf3f0f760e01b5f5260045ffd5b505050509193949550506136cc91506131cf6136c63685612854565b91612af3565b156106425761331781613709611315611bcd60607fd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496960184612bb1565b837f44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e176040518061373a428683612dc8565b0390a2604051918291602083526020830190612c6e565b50505f6004851461311b565b876379c1d89f60e11b5f5260045260245ffd5b9060405161377d816124c7565b606067ffffffffffffffff6002839560405161379d81612b328185612978565b85528260018201546001600160a01b038116602088015260a01c166040860152015416910152565b91908203918211611d8157565b604051906137df826124e3565b60606080835f81525f602082015282604082015282808201520152565b8051156138095760200190565b634e487b7160e01b5f52603260045260245ffd5b8051600110156138095760400190565b80518210156138095760209160051b010190565b9161385761386592606085526060850190612dfc565b908382036020850152612659565b906040818303910152815180825260208201916020808360051b8301019401925f915b83831061389757505050505090565b90919293946020806138b583601f1986600196030187528951612659565b97019301930191939290613888565b5f5b8281106138d257505050565b6060828201526020016138c6565b906138ea8261253e565b6138f7604051918261251b565b828152601f19613907829461253e565b0190602036910137565b91908110156138095760051b0190565b3567ffffffffffffffff811681036102415790565b359067ffffffffffffffff8216820361024157565b60028210156138095701905f90565b9190811015613809576060020190565b9060028110156138095760011b01905f90565b91906139ab576020816001600160a01b03806001945116166001600160a01b03198554161784550151910155565b634e487b7160e01b5f525f60045260245ffd5b906040516139cb816124ff565b6020600182946001600160a01b0381541684520154910152565b9060026139f28380613011565b905014801590614249575b801561421b575b80156141d2575b61101557602082016001600160a01b03613a2482612ffd565b16156141aa576040830192610e1067ffffffffffffffff613a4486613921565b161061418257823593600485101561024157613a5f8561264f565b600185036106425760208401359081610642576080833603126102415760405191613a89836124c7565b833567ffffffffffffffff811161024157613aa79036908601612556565b8352613ab2856123e1565b6020840152613ac082613936565b6040840152613ae06060850193613ad685613936565b60608201526149ec565b96875f52600260205260ff600360405f2001541660058110156107795761073e57608087016001613b11828a613011565b905003610fc557613b223689612854565b613b2a614a5e565b90613b35838b613011565b156138095780613b4491612f01565b919092613b518a80613011565b9490941561380957610b258e91613b6a613b7297612ffd565b953691612778565b15610fc55760608801926002613b88858b612bb1565b90500361228657895f52600260205260405f2092613ba68880613011565b9067ffffffffffffffff821161055957680100000000000000008211610559578554828755808310614166575b50855f5260205f205f5b83811061414b5750505050613ccf90600185016001600160a01b03613c018c612ffd565b166001600160a01b0319825416178155613c1a88613921565b7fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff7bffffffffffffffff000000000000000000000000000000000000000083549260a01b1691161790556002850167ffffffffffffffff613c7a8a613921565b82547fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000016911617905560038501805460ff191660011790556004850180546001600160a01b03191633179055600f8501612ee8565b601083015560118201613ce560408a018a612f01565b9067ffffffffffffffff821161055957613d038261039d85546129b8565b5f90601f83116001146140e757613d3092915f91836121045750508160011b915f199060031b1c19161790565b90555b60128201613d41848a612bb1565b9190613d4d8383612f8f565b905f5260205f205f915b8383106140815750505050613d70601383019189613011565b9091613d7c8282613095565b5f9081526020812092805b838310613fab5750505050505f91600a600683019201925b60028110613f1e575050613db5613de0916139be565b80929060206001916001600160a01b0380825116166001600160a01b03198554161784550151910155565b613dea8480613011565b9190911561380957613e3d906001600160a01b03613e0c8a9798969594612ffd565b165f526003602052613e2486600160405f2001614f59565b5060206001600160a01b03825116910151908633614d06565b6040519260408452613e5360c085019680612c39565b608060408701529687905260e08501965f5b818110613eea575050509467ffffffffffffffff613ecf859482613ec4613ee4966001600160a01b03613eb97f4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc79c9d6123e1565b1660608a0152613936565b166080870152613936565b1660a084015282810360208401523396612c6e565b0390a390565b91965091929394966020806001926001600160a01b03613f098b6123e1565b16815201970191019189969795949392613e65565b80613f838a6040613f5484613f4288613f4e6020613f4860019b613f42858b612bb1565b9061395a565b01612ffd565b95612bb1565b01356001600160a01b0360405192613f6b846124ff565b1682526020820152613f7d838761396a565b9061397d565b613fa5604051613f92816124ff565b5f81525f6020820152613f7d838861396a565b01613d9f565b613fb58183612f01565b9067ffffffffffffffff821161055957613fd38261049a89546129b8565b5f90601f83116001146140175792614008836001959460209487965f926121045750508160011b915f199060031b1c19161790565b88555b01950192019193613d87565b601f19831691885f5260205f20925f5b8181106140695750936020936001969387969383889510614050575b505050811b01885561400b565b01355f19600384901b60f8161c191690555f8080614043565b91936020600181928787013581550195019201614027565b60036060826001600160a01b03614099600195612ffd565b166001600160a01b03198654161785556140b560208201612ffd565b6001600160a01b0385870191166001600160a01b03198254161790556040810135600286015501920192019190613d57565b601f19831691845f5260205f20925f5b818110614133575090846001959493921061411a575b505050811b019055613d33565b01355f19600384901b60f8161c191690555f808061410d565b919360206001819287870135815501950192016140f7565b600190602061415985612ffd565b9401938184015501613bdd565b61417c90875f528360205f209182019101612f34565b5f613bd3565b7fb4e12433000000000000000000000000000000000000000000000000000000005f5260045ffd5b7fea9e70ce000000000000000000000000000000000000000000000000000000005f5260045ffd5b506141dd8280613011565b15613809576141eb90612ffd565b6141f58380613011565b60011015613809576001600160a01b036142126020829301612ffd565b16911614613a0b565b506142268280613011565b600110156138095761424260206001600160a01b039201612ffd565b1615613a04565b506142548280613011565b156138095761426a6001600160a01b0391612ffd565b16156139fd565b9082156143a8576001600160a01b031691821591821561437157813403614349577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b036020925b1693845f526003835260405f20865f52835260405f206142e1838254612edb565b9055156142f2575b604051908152a3565b6143446040517f23b872dd00000000000000000000000000000000000000000000000000000000848201523360248201523060448201528260648201526064815261433e60848261251b565b86614fc1565b6142e9565b7faa7feadc000000000000000000000000000000000000000000000000000000005f5260045ffd5b34614349577f8752a472e571a816aea92eec8dae9baf628e840f4929fbcc2d155e6233ff68a7916001600160a01b036020926142c0565b7f2c5211c6000000000000000000000000000000000000000000000000000000005f5260045ffd5b805460011015613809575f52600160205f2001905f90565b8054821015613809575f5260205f2001905f90565b919092825f52600260205261441c61441760405f20613770565b6149ec565b9084516144288161264f565b6020860194855193604088019461446586519461445760608c0196875190604051958694602086019889614e26565b03601f19810183528261251b565b51902060405160208101918252604080820152600960608201527f6368616c6c656e676500000000000000000000000000000000000000000000006080820152608081526144b460a08261251b565b5190206144d66144d06144c7868461519b565b909291926151d5565b87614e66565b614647577f19457468657265756d205369676e6564204d6573736167653a0a3332000000005f52601c526145196145136144c785603c5f2061519b565b86614e66565b61463e576146129661460d9661460495604294614534614a5e565b9351926145408461264f565b519151602081519101209051604051614569816144576020820194602086526040830190614dc1565b5190209160405193602085019586527f74875af04779d70f933aef147d5751a32a32b3fa275f5022499f396ea394cf5360408601526145a78161264f565b6060850152608084015260a083015260c082015260c081526145ca60e08261251b565b519020604051917f19010000000000000000000000000000000000000000000000000000000000008352600283015260228201522061519b565b909391936151d5565b614e66565b612f83577f61a44f6e000000000000000000000000000000000000000000000000000000005f5260045ffd5b50505050505050565b5050505050505050565b60405161466e816144576020820194602086526040830190612659565b5190209060405161468f816144576020820194602086526040830190612659565b5190201490565b60048101905b8181106146a7575050565b5f808255600182015560020161469c565b90815f52600260205260405f209060038201600460ff198254161790556002815103610642575f5b6002811061483b5750505f5b600281106148015750505f52600260205260405f2080545f8255806147e7575b505f60018201555f60028201555f6003820155614738600682016147338160048501612f34565b614696565b614744600a8201614696565b5f600e8201555f600f8201555f601082015561476260118201613047565b601281018054905f8155816147ae575b50506013018054905f815581614786575050565b5f5260205f20908101905b81811061479c575050565b806147a8600192613047565b01614791565b81600302916003830403611d81575f5260205f20908101905b8181101561477257805f600392555f60018201555f6002820155016147c7565b6147fb90825f5260205f2090810190612f34565b5f61470c565b806001600160a01b03614816600193856143e8565b90549060031b1c165f526003602052614834848360405f20016150d6565b50016146ec565b8061485261484b6001938561382d565b5186614ea5565b016146e0565b602060405180927f7f7d6ab500000000000000000000000000000000000000000000000000000000825260406004830152816001600160a01b03816148b56148a3604483018a612659565b6003198382030160248401528a612659565b0392165afa5f91816148e0575b506148d557506020809101519101511090565b90505f8092500b1390565b9091506020813d602011614915575b816148fc6020938361251b565b810103126102415751805f0b810361024157905f6148c2565b3d91506148ef565b6002815103610642576001600160a01b036020614948828261493e866137fc565b510151169361381d565b510151160361064257565b60808201916002835151036149ca5761496b826149ec565b915f5b60028110614980575050505050600190565b6149b461498b614a5e565b61499683885161382d565b516001600160a01b036149aa85875161382d565b5116918787614b74565b156149c15760010161496e565b50505050505f90565b5050505f90565b9190915f8382019384129112908015821691151617611d8157565b805190614a586001600160a01b036020830151169167ffffffffffffffff6060816040840151169201511692604051938492614a34602085019760a0895260c0860190612613565b926040850152606084015260808301524660a083015203601f19810183528261251b565b51902090565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016301480614b4b575b15614ab9577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a08152614a5860c08261251b565b507f00000000000000000000000000000000000000000000000000000000000000004614614a90565b926001600160a01b03949193805193614b8c8561264f565b6020820195865194614bd58960408601988951614bbe606089019a6144578c5160405194859360208501978c89614e26565b51902092614bcc888561519b565b909491946151d5565b1699168914614cf8577f19457468657265756d205369676e6564204d6573736167653a0a3332000000005f52601c52876001600160a01b03614c1d6144c787603c5f2061519b565b1614614ceb577fdeeda0875e527d63774890b89d23bff91e0ec84761e45d2b578ee592780095bb8214614cdf576001600160a01b0396614cd0966144c7966042955192614c698461264f565b519151602081519101209051604051614c92816144576020820194602086526040830190614dc1565b519020916040519360208501957fb02e61f8dbbfba070321cdff64845b04358ee41db88ba372ef47e352446f4b9c875260408601526145a78161264f565b1614614cda575f90565b600190565b50505050505050505f90565b5050505050505050600190565b505050505050505050600190565b8315614dbb576001600160a01b03165f52600360205260405f206001600160a01b0383165f528060205260405f2054848110614d8b5784614d46916137c5565b906001600160a01b0384165f5260205260405f20555f5260026020526001600160a01b03601460405f200191165f52602052614d8760405f20918254612edb565b9055565b84907fcf479181000000000000000000000000000000000000000000000000000000005f5260045260245260445ffd5b50505050565b90602080835192838152019201905f5b818110614dde5750505090565b9091926020614e1c60019286519060406060926001600160a01b0381511683526001600160a01b036020820151166020840152015160408201520190565b9401929101614dd1565b939092614e58926127c996948652614e3d8161264f565b6020860152604085015260a0606085015260a08401906125ee565b916080818403910152614dc1565b905f5b82518110156149ca576001600160a01b03614e84828561382d565b51166001600160a01b03831614614e9d57600101614e69565b505050600190565b906040810191825115614f54575f526002602052601460405f20019160208201916001600160a01b0380845116165f528360205260405f2054938415614f4d576001600160a01b0392518086115f14614f4257614f039080966137c5565b908380865116165f5260205260405f205551165f5260036020526001600160a01b038060405f20925116165f52602052614d8760405f20918254612edb565b50614f0385806137c5565b5050505050565b505050565b6001810190825f528160205260405f2054155f146149ca5780546801000000000000000081101561055957614fae614f988260018794018555846143e8565b819391549060031b91821b915f19901b19161790565b905554915f5260205260405f2055600190565b905f602091828151910182855af115610680575f513d61502557506001600160a01b0381163b155b614ff05750565b6001600160a01b03907f5274afe7000000000000000000000000000000000000000000000000000000005f521660045260245ffd5b60011415614fe9565b60ff811461508d5760ff811690601f8211615065576040519161505260408461251b565b6020808452838101919036833783525290565b7fb3512b0c000000000000000000000000000000000000000000000000000000005f5260045ffd5b506040516127c981612b32815f6129f0565b60ff81146150c35760ff811690601f8211615065576040519161505260408461251b565b506040516127c981612b328160016129f0565b906001820191815f528260205260405f20548015155f14615193575f198101818111611d815782545f19810191908211611d815781810361515e575b5050508054801561514a575f19019061512b82826143e8565b8154905f199060031b1b19169055555f526020525f6040812055600190565b634e487b7160e01b5f52603160045260245ffd5b61517e61516e614f9893866143e8565b90549060031b1c928392866143e8565b90555f528360205260405f20555f8080615112565b505050505f90565b81519190604183036151cb576151c49250602082015190606060408401519301515f1a9061529c565b9192909190565b50505f9160029190565b6151de8161264f565b806151e7575050565b6151f08161264f565b60018103615220577ff645eedf000000000000000000000000000000000000000000000000000000005f5260045ffd5b6152298161264f565b6002810361525d57507ffce698f7000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b6003906152698161264f565b146152715750565b7fd78bce0c000000000000000000000000000000000000000000000000000000005f5260045260245ffd5b91907f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411615313579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610680575f516001600160a01b0381161561530957905f905f90565b505f906001905f90565b5050505f916003919056fea2646970667358221220a42ae78844a10ed8f2b9c143b570a4d8ecbb65dda4c532c3bdd322fa72ea304064736f6c634300081b0033", } // CustodyABI is the input ABI used to generate the binding from. @@ -233,6 +226,76 @@ func (_Custody *CustodyTransactorRaw) Transact(opts *bind.TransactOpts, method s return _Custody.Contract.contract.Transact(opts, method, params...) } +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_Custody *CustodyCaller) Eip712Domain(opts *bind.CallOpts) (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + var out []interface{} + err := _Custody.contract.Call(opts, &out, "eip712Domain") + + outstruct := new(struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.Fields = *abi.ConvertType(out[0], new([1]byte)).(*[1]byte) + outstruct.Name = *abi.ConvertType(out[1], new(string)).(*string) + outstruct.Version = *abi.ConvertType(out[2], new(string)).(*string) + outstruct.ChainId = *abi.ConvertType(out[3], new(*big.Int)).(**big.Int) + outstruct.VerifyingContract = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Salt = *abi.ConvertType(out[5], new([32]byte)).(*[32]byte) + outstruct.Extensions = *abi.ConvertType(out[6], new([]*big.Int)).(*[]*big.Int) + + return *outstruct, err + +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_Custody *CustodySession) Eip712Domain() (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + return _Custody.Contract.Eip712Domain(&_Custody.CallOpts) +} + +// Eip712Domain is a free data retrieval call binding the contract method 0x84b0196e. +// +// Solidity: function eip712Domain() view returns(bytes1 fields, string name, string version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] extensions) +func (_Custody *CustodyCallerSession) Eip712Domain() (struct { + Fields [1]byte + Name string + Version string + ChainId *big.Int + VerifyingContract common.Address + Salt [32]byte + Extensions []*big.Int +}, error) { + return _Custody.Contract.Eip712Domain(&_Custody.CallOpts) +} + // GetAccountsBalances is a free data retrieval call binding the contract method 0x2f33c4d6. // // Solidity: function getAccountsBalances(address[] accounts, address[] tokens) view returns(uint256[][]) @@ -297,7 +360,7 @@ func (_Custody *CustodyCallerSession) GetChannelBalances(channelId [32]byte, tok // GetChannelData is a free data retrieval call binding the contract method 0xe617208c. // -// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) lastValidState) +// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) lastValidState) func (_Custody *CustodyCaller) GetChannelData(opts *bind.CallOpts, channelId [32]byte) (struct { Channel Channel Status uint8 @@ -331,7 +394,7 @@ func (_Custody *CustodyCaller) GetChannelData(opts *bind.CallOpts, channelId [32 // GetChannelData is a free data retrieval call binding the contract method 0xe617208c. // -// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) lastValidState) +// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) lastValidState) func (_Custody *CustodySession) GetChannelData(channelId [32]byte) (struct { Channel Channel Status uint8 @@ -344,7 +407,7 @@ func (_Custody *CustodySession) GetChannelData(channelId [32]byte) (struct { // GetChannelData is a free data retrieval call binding the contract method 0xe617208c. // -// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) lastValidState) +// Solidity: function getChannelData(bytes32 channelId) view returns((address[],address,uint64,uint64) channel, uint8 status, address[] wallets, uint256 challengeExpiry, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) lastValidState) func (_Custody *CustodyCallerSession) GetChannelData(channelId [32]byte) (struct { Channel Channel Status uint8 @@ -386,86 +449,86 @@ func (_Custody *CustodyCallerSession) GetOpenChannels(accounts []common.Address) return _Custody.Contract.GetOpenChannels(&_Custody.CallOpts, accounts) } -// Challenge is a paid mutator transaction binding the contract method 0xbc7b456f. +// Challenge is a paid mutator transaction binding the contract method 0x1474e410. // -// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs, (uint8,bytes32,bytes32) challengerSig) returns() -func (_Custody *CustodyTransactor) Challenge(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State, challengerSig Signature) (*types.Transaction, error) { +// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs, bytes challengerSig) returns() +func (_Custody *CustodyTransactor) Challenge(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State, challengerSig []byte) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "challenge", channelId, candidate, proofs, challengerSig) } -// Challenge is a paid mutator transaction binding the contract method 0xbc7b456f. +// Challenge is a paid mutator transaction binding the contract method 0x1474e410. // -// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs, (uint8,bytes32,bytes32) challengerSig) returns() -func (_Custody *CustodySession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig Signature) (*types.Transaction, error) { +// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs, bytes challengerSig) returns() +func (_Custody *CustodySession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig []byte) (*types.Transaction, error) { return _Custody.Contract.Challenge(&_Custody.TransactOpts, channelId, candidate, proofs, challengerSig) } -// Challenge is a paid mutator transaction binding the contract method 0xbc7b456f. +// Challenge is a paid mutator transaction binding the contract method 0x1474e410. // -// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs, (uint8,bytes32,bytes32) challengerSig) returns() -func (_Custody *CustodyTransactorSession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig Signature) (*types.Transaction, error) { +// Solidity: function challenge(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs, bytes challengerSig) returns() +func (_Custody *CustodyTransactorSession) Challenge(channelId [32]byte, candidate State, proofs []State, challengerSig []byte) (*types.Transaction, error) { return _Custody.Contract.Challenge(&_Custody.TransactOpts, channelId, candidate, proofs, challengerSig) } -// Checkpoint is a paid mutator transaction binding the contract method 0xd0cce1e8. +// Checkpoint is a paid mutator transaction binding the contract method 0xecf668fd. // -// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactor) Checkpoint(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "checkpoint", channelId, candidate, proofs) } -// Checkpoint is a paid mutator transaction binding the contract method 0xd0cce1e8. +// Checkpoint is a paid mutator transaction binding the contract method 0xecf668fd. // -// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodySession) Checkpoint(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Checkpoint(&_Custody.TransactOpts, channelId, candidate, proofs) } -// Checkpoint is a paid mutator transaction binding the contract method 0xd0cce1e8. +// Checkpoint is a paid mutator transaction binding the contract method 0xecf668fd. // -// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function checkpoint(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactorSession) Checkpoint(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Checkpoint(&_Custody.TransactOpts, channelId, candidate, proofs) } -// Close is a paid mutator transaction binding the contract method 0xde22731f. +// Close is a paid mutator transaction binding the contract method 0x7f9ebbd7. // -// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] ) returns() +// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] ) returns() func (_Custody *CustodyTransactor) Close(opts *bind.TransactOpts, channelId [32]byte, candidate State, arg2 []State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "close", channelId, candidate, arg2) } -// Close is a paid mutator transaction binding the contract method 0xde22731f. +// Close is a paid mutator transaction binding the contract method 0x7f9ebbd7. // -// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] ) returns() +// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] ) returns() func (_Custody *CustodySession) Close(channelId [32]byte, candidate State, arg2 []State) (*types.Transaction, error) { return _Custody.Contract.Close(&_Custody.TransactOpts, channelId, candidate, arg2) } -// Close is a paid mutator transaction binding the contract method 0xde22731f. +// Close is a paid mutator transaction binding the contract method 0x7f9ebbd7. // -// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] ) returns() +// Solidity: function close(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] ) returns() func (_Custody *CustodyTransactorSession) Close(channelId [32]byte, candidate State, arg2 []State) (*types.Transaction, error) { return _Custody.Contract.Close(&_Custody.TransactOpts, channelId, candidate, arg2) } -// Create is a paid mutator transaction binding the contract method 0xd37ff7b5. +// Create is a paid mutator transaction binding the contract method 0x4a7e7798. // -// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) returns(bytes32 channelId) +// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) returns(bytes32 channelId) func (_Custody *CustodyTransactor) Create(opts *bind.TransactOpts, ch Channel, initial State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "create", ch, initial) } -// Create is a paid mutator transaction binding the contract method 0xd37ff7b5. +// Create is a paid mutator transaction binding the contract method 0x4a7e7798. // -// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) returns(bytes32 channelId) +// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) returns(bytes32 channelId) func (_Custody *CustodySession) Create(ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.Create(&_Custody.TransactOpts, ch, initial) } -// Create is a paid mutator transaction binding the contract method 0xd37ff7b5. +// Create is a paid mutator transaction binding the contract method 0x4a7e7798. // -// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) returns(bytes32 channelId) +// Solidity: function create((address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) returns(bytes32 channelId) func (_Custody *CustodyTransactorSession) Create(ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.Create(&_Custody.TransactOpts, ch, initial) } @@ -491,65 +554,65 @@ func (_Custody *CustodyTransactorSession) Deposit(account common.Address, token return _Custody.Contract.Deposit(&_Custody.TransactOpts, account, token, amount) } -// DepositAndCreate is a paid mutator transaction binding the contract method 0x925bc479. +// DepositAndCreate is a paid mutator transaction binding the contract method 0x00e2bb2c. // -// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) payable returns(bytes32) +// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) payable returns(bytes32) func (_Custody *CustodyTransactor) DepositAndCreate(opts *bind.TransactOpts, token common.Address, amount *big.Int, ch Channel, initial State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "depositAndCreate", token, amount, ch, initial) } -// DepositAndCreate is a paid mutator transaction binding the contract method 0x925bc479. +// DepositAndCreate is a paid mutator transaction binding the contract method 0x00e2bb2c. // -// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) payable returns(bytes32) +// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) payable returns(bytes32) func (_Custody *CustodySession) DepositAndCreate(token common.Address, amount *big.Int, ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.DepositAndCreate(&_Custody.TransactOpts, token, amount, ch, initial) } -// DepositAndCreate is a paid mutator transaction binding the contract method 0x925bc479. +// DepositAndCreate is a paid mutator transaction binding the contract method 0x00e2bb2c. // -// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) payable returns(bytes32) +// Solidity: function depositAndCreate(address token, uint256 amount, (address[],address,uint64,uint64) ch, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) payable returns(bytes32) func (_Custody *CustodyTransactorSession) DepositAndCreate(token common.Address, amount *big.Int, ch Channel, initial State) (*types.Transaction, error) { return _Custody.Contract.DepositAndCreate(&_Custody.TransactOpts, token, amount, ch, initial) } -// Join is a paid mutator transaction binding the contract method 0xa22b823d. +// Join is a paid mutator transaction binding the contract method 0xbab3290a. // -// Solidity: function join(bytes32 channelId, uint256 index, (uint8,bytes32,bytes32) sig) returns(bytes32) -func (_Custody *CustodyTransactor) Join(opts *bind.TransactOpts, channelId [32]byte, index *big.Int, sig Signature) (*types.Transaction, error) { +// Solidity: function join(bytes32 channelId, uint256 index, bytes sig) returns(bytes32) +func (_Custody *CustodyTransactor) Join(opts *bind.TransactOpts, channelId [32]byte, index *big.Int, sig []byte) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "join", channelId, index, sig) } -// Join is a paid mutator transaction binding the contract method 0xa22b823d. +// Join is a paid mutator transaction binding the contract method 0xbab3290a. // -// Solidity: function join(bytes32 channelId, uint256 index, (uint8,bytes32,bytes32) sig) returns(bytes32) -func (_Custody *CustodySession) Join(channelId [32]byte, index *big.Int, sig Signature) (*types.Transaction, error) { +// Solidity: function join(bytes32 channelId, uint256 index, bytes sig) returns(bytes32) +func (_Custody *CustodySession) Join(channelId [32]byte, index *big.Int, sig []byte) (*types.Transaction, error) { return _Custody.Contract.Join(&_Custody.TransactOpts, channelId, index, sig) } -// Join is a paid mutator transaction binding the contract method 0xa22b823d. +// Join is a paid mutator transaction binding the contract method 0xbab3290a. // -// Solidity: function join(bytes32 channelId, uint256 index, (uint8,bytes32,bytes32) sig) returns(bytes32) -func (_Custody *CustodyTransactorSession) Join(channelId [32]byte, index *big.Int, sig Signature) (*types.Transaction, error) { +// Solidity: function join(bytes32 channelId, uint256 index, bytes sig) returns(bytes32) +func (_Custody *CustodyTransactorSession) Join(channelId [32]byte, index *big.Int, sig []byte) (*types.Transaction, error) { return _Custody.Contract.Join(&_Custody.TransactOpts, channelId, index, sig) } -// Resize is a paid mutator transaction binding the contract method 0x259311c9. +// Resize is a paid mutator transaction binding the contract method 0x183b4998. // -// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactor) Resize(opts *bind.TransactOpts, channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.contract.Transact(opts, "resize", channelId, candidate, proofs) } -// Resize is a paid mutator transaction binding the contract method 0x259311c9. +// Resize is a paid mutator transaction binding the contract method 0x183b4998. // -// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodySession) Resize(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Resize(&_Custody.TransactOpts, channelId, candidate, proofs) } -// Resize is a paid mutator transaction binding the contract method 0x259311c9. +// Resize is a paid mutator transaction binding the contract method 0x183b4998. // -// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[])[] proofs) returns() +// Solidity: function resize(bytes32 channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) candidate, (uint8,uint256,bytes,(address,address,uint256)[],bytes[])[] proofs) returns() func (_Custody *CustodyTransactorSession) Resize(channelId [32]byte, candidate State, proofs []State) (*types.Transaction, error) { return _Custody.Contract.Resize(&_Custody.TransactOpts, channelId, candidate, proofs) } @@ -650,9 +713,9 @@ type CustodyChallenged struct { Raw types.Log // Blockchain specific contextual infos } -// FilterChallenged is a free log retrieval operation binding the contract event 0x2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d03756. +// FilterChallenged is a free log retrieval operation binding the contract event 0x44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e17. // -// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state, uint256 expiration) +// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state, uint256 expiration) func (_Custody *CustodyFilterer) FilterChallenged(opts *bind.FilterOpts, channelId [][32]byte) (*CustodyChallengedIterator, error) { var channelIdRule []interface{} @@ -667,9 +730,9 @@ func (_Custody *CustodyFilterer) FilterChallenged(opts *bind.FilterOpts, channel return &CustodyChallengedIterator{contract: _Custody.contract, event: "Challenged", logs: logs, sub: sub}, nil } -// WatchChallenged is a free log subscription operation binding the contract event 0x2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d03756. +// WatchChallenged is a free log subscription operation binding the contract event 0x44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e17. // -// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state, uint256 expiration) +// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state, uint256 expiration) func (_Custody *CustodyFilterer) WatchChallenged(opts *bind.WatchOpts, sink chan<- *CustodyChallenged, channelId [][32]byte) (event.Subscription, error) { var channelIdRule []interface{} @@ -709,9 +772,9 @@ func (_Custody *CustodyFilterer) WatchChallenged(opts *bind.WatchOpts, sink chan }), nil } -// ParseChallenged is a log parse operation binding the contract event 0x2cce3a04acfb5f7911860de30611c13af2df5880b4a1f829fa7b4f2a26d03756. +// ParseChallenged is a log parse operation binding the contract event 0x44c1980976c3af1eb75b2a3b7d8c7e01f69168c0fe45dd229faf143233722e17. // -// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state, uint256 expiration) +// Solidity: event Challenged(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state, uint256 expiration) func (_Custody *CustodyFilterer) ParseChallenged(log types.Log) (*CustodyChallenged, error) { event := new(CustodyChallenged) if err := _Custody.contract.UnpackLog(event, "Challenged", log); err != nil { @@ -795,9 +858,9 @@ type CustodyCheckpointed struct { Raw types.Log // Blockchain specific contextual infos } -// FilterCheckpointed is a free log retrieval operation binding the contract event 0xa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d. +// FilterCheckpointed is a free log retrieval operation binding the contract event 0x8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c3. // -// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state) +// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state) func (_Custody *CustodyFilterer) FilterCheckpointed(opts *bind.FilterOpts, channelId [][32]byte) (*CustodyCheckpointedIterator, error) { var channelIdRule []interface{} @@ -812,9 +875,9 @@ func (_Custody *CustodyFilterer) FilterCheckpointed(opts *bind.FilterOpts, chann return &CustodyCheckpointedIterator{contract: _Custody.contract, event: "Checkpointed", logs: logs, sub: sub}, nil } -// WatchCheckpointed is a free log subscription operation binding the contract event 0xa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d. +// WatchCheckpointed is a free log subscription operation binding the contract event 0x8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c3. // -// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state) +// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state) func (_Custody *CustodyFilterer) WatchCheckpointed(opts *bind.WatchOpts, sink chan<- *CustodyCheckpointed, channelId [][32]byte) (event.Subscription, error) { var channelIdRule []interface{} @@ -854,9 +917,9 @@ func (_Custody *CustodyFilterer) WatchCheckpointed(opts *bind.WatchOpts, sink ch }), nil } -// ParseCheckpointed is a log parse operation binding the contract event 0xa876bb57c3d3b4b0363570fd7443e30dfe18d4b422fe9898358262d78485325d. +// ParseCheckpointed is a log parse operation binding the contract event 0x8cade4fe25d72146dc0dbe08ea2712bdcca7e2c996e2dce1e69f20e30ee1c5c3. // -// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) state) +// Solidity: event Checkpointed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) state) func (_Custody *CustodyFilterer) ParseCheckpointed(log types.Log) (*CustodyCheckpointed, error) { event := new(CustodyCheckpointed) if err := _Custody.contract.UnpackLog(event, "Checkpointed", log); err != nil { @@ -940,9 +1003,9 @@ type CustodyClosed struct { Raw types.Log // Blockchain specific contextual infos } -// FilterClosed is a free log retrieval operation binding the contract event 0x3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c910. +// FilterClosed is a free log retrieval operation binding the contract event 0xd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496. // -// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) finalState) +// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) finalState) func (_Custody *CustodyFilterer) FilterClosed(opts *bind.FilterOpts, channelId [][32]byte) (*CustodyClosedIterator, error) { var channelIdRule []interface{} @@ -957,9 +1020,9 @@ func (_Custody *CustodyFilterer) FilterClosed(opts *bind.FilterOpts, channelId [ return &CustodyClosedIterator{contract: _Custody.contract, event: "Closed", logs: logs, sub: sub}, nil } -// WatchClosed is a free log subscription operation binding the contract event 0x3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c910. +// WatchClosed is a free log subscription operation binding the contract event 0xd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496. // -// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) finalState) +// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) finalState) func (_Custody *CustodyFilterer) WatchClosed(opts *bind.WatchOpts, sink chan<- *CustodyClosed, channelId [][32]byte) (event.Subscription, error) { var channelIdRule []interface{} @@ -999,9 +1062,9 @@ func (_Custody *CustodyFilterer) WatchClosed(opts *bind.WatchOpts, sink chan<- * }), nil } -// ParseClosed is a log parse operation binding the contract event 0x3646844802330633cc652490829391a0e9ddb82143a86a7e39ca148dfb05c910. +// ParseClosed is a log parse operation binding the contract event 0xd3fa0f35ad809781b5c95d9f324b2621475e3d03254a60808cf804b663a70496. // -// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) finalState) +// Solidity: event Closed(bytes32 indexed channelId, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) finalState) func (_Custody *CustodyFilterer) ParseClosed(log types.Log) (*CustodyClosed, error) { event := new(CustodyClosed) if err := _Custody.contract.UnpackLog(event, "Closed", log); err != nil { @@ -1087,9 +1150,9 @@ type CustodyCreated struct { Raw types.Log // Blockchain specific contextual infos } -// FilterCreated is a free log retrieval operation binding the contract event 0x7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c39842. +// FilterCreated is a free log retrieval operation binding the contract event 0x4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc7. // -// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) +// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) func (_Custody *CustodyFilterer) FilterCreated(opts *bind.FilterOpts, channelId [][32]byte, wallet []common.Address) (*CustodyCreatedIterator, error) { var channelIdRule []interface{} @@ -1108,9 +1171,9 @@ func (_Custody *CustodyFilterer) FilterCreated(opts *bind.FilterOpts, channelId return &CustodyCreatedIterator{contract: _Custody.contract, event: "Created", logs: logs, sub: sub}, nil } -// WatchCreated is a free log subscription operation binding the contract event 0x7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c39842. +// WatchCreated is a free log subscription operation binding the contract event 0x4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc7. // -// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) +// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) func (_Custody *CustodyFilterer) WatchCreated(opts *bind.WatchOpts, sink chan<- *CustodyCreated, channelId [][32]byte, wallet []common.Address) (event.Subscription, error) { var channelIdRule []interface{} @@ -1154,9 +1217,9 @@ func (_Custody *CustodyFilterer) WatchCreated(opts *bind.WatchOpts, sink chan<- }), nil } -// ParseCreated is a log parse operation binding the contract event 0x7044488f9b947dc40d596a71992214b1050317a18ab1dced28e9d22320c39842. +// ParseCreated is a log parse operation binding the contract event 0x4dd0384c1acc40a5edb69575b4a1caa43c2c2852ef96f7ecfc4a6705ddb8ccc7. // -// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],(uint8,bytes32,bytes32)[]) initial) +// Solidity: event Created(bytes32 indexed channelId, address indexed wallet, (address[],address,uint64,uint64) channel, (uint8,uint256,bytes,(address,address,uint256)[],bytes[]) initial) func (_Custody *CustodyFilterer) ParseCreated(log types.Log) (*CustodyCreated, error) { event := new(CustodyCreated) if err := _Custody.contract.UnpackLog(event, "Created", log); err != nil { @@ -1320,6 +1383,139 @@ func (_Custody *CustodyFilterer) ParseDeposited(log types.Log) (*CustodyDeposite return event, nil } +// CustodyEIP712DomainChangedIterator is returned from FilterEIP712DomainChanged and is used to iterate over the raw logs and unpacked data for EIP712DomainChanged events raised by the Custody contract. +type CustodyEIP712DomainChangedIterator struct { + Event *CustodyEIP712DomainChanged // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *CustodyEIP712DomainChangedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(CustodyEIP712DomainChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(CustodyEIP712DomainChanged) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *CustodyEIP712DomainChangedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *CustodyEIP712DomainChangedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// CustodyEIP712DomainChanged represents a EIP712DomainChanged event raised by the Custody contract. +type CustodyEIP712DomainChanged struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterEIP712DomainChanged is a free log retrieval operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_Custody *CustodyFilterer) FilterEIP712DomainChanged(opts *bind.FilterOpts) (*CustodyEIP712DomainChangedIterator, error) { + + logs, sub, err := _Custody.contract.FilterLogs(opts, "EIP712DomainChanged") + if err != nil { + return nil, err + } + return &CustodyEIP712DomainChangedIterator{contract: _Custody.contract, event: "EIP712DomainChanged", logs: logs, sub: sub}, nil +} + +// WatchEIP712DomainChanged is a free log subscription operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_Custody *CustodyFilterer) WatchEIP712DomainChanged(opts *bind.WatchOpts, sink chan<- *CustodyEIP712DomainChanged) (event.Subscription, error) { + + logs, sub, err := _Custody.contract.WatchLogs(opts, "EIP712DomainChanged") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(CustodyEIP712DomainChanged) + if err := _Custody.contract.UnpackLog(event, "EIP712DomainChanged", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseEIP712DomainChanged is a log parse operation binding the contract event 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31. +// +// Solidity: event EIP712DomainChanged() +func (_Custody *CustodyFilterer) ParseEIP712DomainChanged(log types.Log) (*CustodyEIP712DomainChanged, error) { + event := new(CustodyEIP712DomainChanged) + if err := _Custody.contract.UnpackLog(event, "EIP712DomainChanged", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // CustodyJoinedIterator is returned from FilterJoined and is used to iterate over the raw logs and unpacked data for Joined events raised by the Custody contract. type CustodyJoinedIterator struct { Event *CustodyJoined // Event containing the contract specifics and raw log @@ -1906,4 +2102,4 @@ func (_Custody *CustodyFilterer) ParseWithdrawn(log types.Log) (*CustodyWithdraw } event.Raw = log return event, nil -} +} \ No newline at end of file diff --git a/examples/cerebro/custody/custody_client.go b/examples/cerebro/custody/custody_client.go index 8a20aa2a4..682c6544c 100644 --- a/examples/cerebro/custody/custody_client.go +++ b/examples/cerebro/custody/custody_client.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "math/big" - "time" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" @@ -31,7 +30,8 @@ func (c *CustodyClient) OpenChannel( wallet, signer unisig.Signer, chainID uint32, chainRPC string, custodyAddress, adjudicatorAddress, brokerAddress, tokenAddress common.Address, - challenge uint64, + challenge, nonce uint64, + brokerSig unisig.Signature, ) (string, error) { client, err := ethclient.Dial(chainRPC) if err != nil { @@ -53,7 +53,7 @@ func (c *CustodyClient) OpenChannel( Adjudicator: adjudicatorAddress, Participants: []common.Address{signer.Address(), brokerAddress}, Challenge: challenge, - Nonce: generateNonce(), + Nonce: nonce, } initial := State{ Intent: 1, // IntentINITIALIZE @@ -83,11 +83,12 @@ func (c *CustodyClient) OpenChannel( return "", fmt.Errorf("failed to encode initial state: %w", err) } - sig, err := signNitroData(signer, initialStateData) + dataHash := crypto.Keccak256Hash(initialStateData) + userSig, err := signer.Sign(dataHash.Bytes()) if err != nil { return "", fmt.Errorf("failed to sign initial state: %w", err) } - initial.Sigs = []Signature{sig} + initial.Sigs = [][]byte{userSig, brokerSig} txOpts := signerTxOpts(wallet, chainID) gasPrice, err := client.SuggestGasPrice(context.Background()) @@ -116,7 +117,7 @@ func (c *CustodyClient) CloseChannel( channelID common.Hash, version *big.Int, allocations []Allocation, - brokerSig Signature, + brokerSig unisig.Signature, ) error { client, err := ethclient.Dial(chainRPC) if err != nil { @@ -139,11 +140,12 @@ func (c *CustodyClient) CloseChannel( return fmt.Errorf("failed to encode initial state: %w", err) } - userSig, err := signNitroData(signer, stateData) + dataHash := crypto.Keccak256Hash(stateData) + userSig, err := signer.Sign(dataHash.Bytes()) if err != nil { return fmt.Errorf("failed to sign initial state: %w", err) } - state.Sigs = []Signature{userSig, brokerSig} + state.Sigs = [][]byte{userSig, brokerSig} txOpts := signerTxOpts(wallet, chainID) gasPrice, err := client.SuggestGasPrice(context.Background()) @@ -173,7 +175,7 @@ func (c *CustodyClient) Resize( version *big.Int, data []byte, allocations []Allocation, - brokerSig Signature, + brokerSig unisig.Signature, ) error { client, err := ethclient.Dial(chainRPC) if err != nil { @@ -201,11 +203,12 @@ func (c *CustodyClient) Resize( return fmt.Errorf("failed to encode initial state: %w", err) } - userSig, err := signNitroData(signer, stateData) + dataHash := crypto.Keccak256Hash(stateData) + userSig, err := signer.Sign(dataHash.Bytes()) if err != nil { return fmt.Errorf("failed to sign initial state: %w", err) } - state.Sigs = []Signature{userSig, brokerSig} + state.Sigs = [][]byte{userSig, brokerSig} txOpts := signerTxOpts(wallet, chainID) gasPrice, err := client.SuggestGasPrice(context.Background()) @@ -424,30 +427,6 @@ func GetChannelID(ch Channel, chainID uint32) (common.Hash, error) { return crypto.Keccak256Hash(packed), nil } -// generateNonce generates a nonce based on the current time in nanoseconds. -// it is totally suitable for use in custody channels as a unique identifier. -func generateNonce() uint64 { - return uint64(time.Now().UnixNano()) -} - -func signNitroData(s unisig.Signer, data []byte) (Signature, error) { - message := crypto.Keccak256(data) - sig, err := s.Sign(message) - if err != nil { - return Signature{}, err - } - - if sig[64] < 27 { - sig[64] += 27 // Adjust signature version - } - - return Signature{ - V: sig[64], - R: [32]byte(sig[:32]), - S: [32]byte(sig[32:64]), - }, nil -} - func signerTxOpts(signer unisig.Signer, chainID uint32) *bind.TransactOpts { bigChainID := big.NewInt(int64(chainID)) signingMethod := types.LatestSignerForChainID(bigChainID) @@ -462,6 +441,10 @@ func signerTxOpts(signer unisig.Signer, chainID uint32) *bind.TransactOpts { return nil, err } + if sig[64] >= 27 { + sig[64] -= 27 + } + return tx.WithSignature(signingMethod, sig) } diff --git a/examples/cerebro/custody_ledger.go b/examples/cerebro/custody_ledger.go index 66656b5f5..2c1e16da4 100644 --- a/examples/cerebro/custody_ledger.go +++ b/examples/cerebro/custody_ledger.go @@ -16,36 +16,42 @@ func (o *Operator) handleDepositCustody(args []string) { fmt.Println("Usage: deposit custody ") return } + chainIDStr := args[2] - chainName := args[2] - network := o.config.GetNetworkByName(chainName) + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + fmt.Printf("Invalid chain ID: %s.\n", chainIDStr) + return + } + + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { - fmt.Printf("Chain %s is not supported by the broker.\n", chainName) + fmt.Printf("Unknown chain: %s.\n", chainIDStr) return } assetSymbol := args[3] asset := network.GetAssetBySymbol(assetSymbol) if asset == nil { - fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainName) + fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainID.String()) return } chainRPC, err := o.getChainRPC(network.ChainID) if err != nil { - fmt.Printf("Failed to get RPC for chain %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to get RPC for chain %s: %s\n", chainID.String(), err.Error()) return } rawTokenBalance, err := custody.GetTokenBalance(network.ChainID, chainRPC, asset.Token, o.config.Wallet.Address()) if err != nil { - fmt.Printf("Failed to get token balance for asset %s on chain %s: %s\n", assetSymbol, chainName, err.Error()) + fmt.Printf("Failed to get token balance for asset %s on chain %s: %s\n", assetSymbol, chainID.String(), err.Error()) return } tokenBalance := decimal.NewFromBigInt(rawTokenBalance, -int32(asset.Decimals)) fmt.Printf("Your current balance for asset %s on chain %s is: %s\n", - assetSymbol, chainName, fmtDec(tokenBalance)) + assetSymbol, chainID.String(), fmtDec(tokenBalance)) fmt.Printf("How much %s do you want to deposit?\n", assetSymbol) amountStr := o.readExtraArg("deposit_amount") @@ -70,7 +76,7 @@ func (o *Operator) handleDepositCustody(args []string) { if err := custody.ApproveAllowance(o.config.Wallet, network.ChainID, chainRPC, asset.Token, network.CustodyAddress, rawAmount); err != nil { - fmt.Printf("Failed to approve allowance for %s on chain %s: %s\n", assetSymbol, chainName, err.Error()) + fmt.Printf("Failed to approve allowance for %s on chain %s: %s\n", assetSymbol, chainID.String(), err.Error()) return } @@ -80,12 +86,12 @@ func (o *Operator) handleDepositCustody(args []string) { network.CustodyAddress, asset.Token, rawAmount, ); err != nil { - fmt.Printf("Failed to deposit %s on chain %s: %s\n", assetSymbol, chainName, err.Error()) + fmt.Printf("Failed to deposit %s on chain %s: %s\n", assetSymbol, chainID.String(), err.Error()) return } fmt.Printf("Successfully deposited %s %s to custody on chain %s.\n", - fmtDec(amount), assetSymbol, chainName) + fmtDec(amount), assetSymbol, chainID.String()) } func (o *Operator) handleWithdrawCustody(args []string) { @@ -93,24 +99,30 @@ func (o *Operator) handleWithdrawCustody(args []string) { fmt.Println("Usage: withdraw custody ") return } + chainIDStr := args[2] + + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + fmt.Printf("Invalid chain ID: %s.\n", chainIDStr) + return + } - chainName := args[2] - network := o.config.GetNetworkByName(chainName) + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { - fmt.Printf("Chain %s is not supported by the broker.\n", chainName) + fmt.Printf("Unknown chain: %s.\n", chainIDStr) return } assetSymbol := args[3] asset := network.GetAssetBySymbol(assetSymbol) if asset == nil { - fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainName) + fmt.Printf("Asset %s is not supported on %s.\n", assetSymbol, chainID.String()) return } chainRPC, err := o.getChainRPC(network.ChainID) if err != nil { - fmt.Printf("Failed to get RPC for chain %s: %s\n", chainName, err.Error()) + fmt.Printf("Failed to get RPC for chain %s: %s\n", chainID.String(), err.Error()) return } @@ -118,17 +130,17 @@ func (o *Operator) handleWithdrawCustody(args []string) { network.ChainID, chainRPC, network.CustodyAddress, o.config.Wallet.Address(), asset.Token) if err != nil { - fmt.Printf("Failed to get custody balance for asset %s on %s: %s\n", assetSymbol, chainName, err.Error()) + fmt.Printf("Failed to get custody balance for asset %s on %s: %s\n", assetSymbol, chainID.String(), err.Error()) return } if rawCustodyBalance == nil || rawCustodyBalance.Cmp(new(big.Int)) <= 0 { - fmt.Printf("Insufficient custody balance for asset %s on %s.\n", assetSymbol, chainName) + fmt.Printf("Insufficient custody balance for asset %s on %s.\n", assetSymbol, chainID.String()) return } custodyBalance := decimal.NewFromBigInt(rawCustodyBalance, -int32(asset.Decimals)) fmt.Printf("Your current custody balance for asset %s on %s is: %s\n", - assetSymbol, chainName, fmtDec(custodyBalance)) + assetSymbol, chainID.String(), fmtDec(custodyBalance)) fmt.Printf("How much %s do you want to withdraw?\n", assetSymbol) amountStr := o.readExtraArg("withdraw_amount") @@ -156,12 +168,12 @@ func (o *Operator) handleWithdrawCustody(args []string) { network.CustodyAddress, asset.Token, rawAmount, ); err != nil { - fmt.Printf("Failed to withdraw %s on %s: %s\n", assetSymbol, chainName, err.Error()) + fmt.Printf("Failed to withdraw %s on %s: %s\n", assetSymbol, chainID.String(), err.Error()) return } fmt.Printf("Successfully withdrawn %s %s on %s.\n", - fmtDec(amount), assetSymbol, chainName) + fmtDec(amount), assetSymbol, chainID.String()) } func (o *Operator) getChainRPC(chainID uint32) (string, error) { diff --git a/examples/cerebro/import.go b/examples/cerebro/import.go index 90cfe4182..8fa76c510 100644 --- a/examples/cerebro/import.go +++ b/examples/cerebro/import.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "math/big" "syscall" "golang.org/x/term" @@ -44,11 +45,17 @@ func (o *Operator) handleImportRPC(args []string) { fmt.Println("Usage: import rpc ") return } - chainName := args[2] + chainIDStr := args[2] - network := o.config.GetNetworkByName(chainName) + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + fmt.Printf("Invalid chain ID: %s.\n", chainIDStr) + return + } + + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { - fmt.Printf("Unknown chain: %s.\n", chainName) + fmt.Printf("Unknown chain: %s.\n", chainIDStr) return } @@ -63,5 +70,5 @@ func (o *Operator) handleImportRPC(args []string) { fmt.Printf("Failed to import chain RPC: %s\n", err.Error()) return } - fmt.Printf("RPC URL for %s imported successfully!\n", chainName) + fmt.Printf("RPC URL for chain %s imported successfully!\n", chainID.String()) } diff --git a/examples/cerebro/list.go b/examples/cerebro/list.go index 76b55e5e1..45e751a28 100644 --- a/examples/cerebro/list.go +++ b/examples/cerebro/list.go @@ -17,7 +17,7 @@ func (o *Operator) handleListChains() { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"Name", "ID", "Asset", "RPCs", "Last Used"}) + t.AppendHeader(table.Row{"ID", "Asset", "RPCs", "Last Used"}) t.AppendSeparator() for _, network := range o.config.Networks { @@ -34,13 +34,12 @@ func (o *Operator) handleListChains() { } for _, asset := range network.Assets { - t.AppendRow(table.Row{network.ChainName, network.ChainID, asset.Symbol, numRPCs, lastUsed.Format(time.RFC3339)}) + t.AppendRow(table.Row{network.ChainID, asset.Symbol, numRPCs, lastUsed.Format(time.RFC3339)}) } } t.SetColumnConfigs( []table.ColumnConfig{ {Number: 1, AutoMerge: true}, - {Number: 2, AutoMerge: true}, }, ) t.Render() @@ -54,7 +53,7 @@ func (o *Operator) handleListChannels() { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"Chain", "Asset", "ID", "Balance"}) + t.AppendHeader(table.Row{"ChainID", "Asset", "ID", "Balance"}) t.AppendSeparator() for _, network := range o.config.Networks { @@ -66,13 +65,12 @@ func (o *Operator) handleListChannels() { channelBalance = decimal.NewFromBigInt(asset.RawChannelBalance, -int32(asset.Decimals)) } - t.AppendRow(table.Row{network.ChainName, asset.Symbol, channelID, fmtDec(channelBalance)}) + t.AppendRow(table.Row{network.ChainID, asset.Symbol, channelID, fmtDec(channelBalance)}) } } t.SetColumnConfigs( []table.ColumnConfig{ {Number: 1, AutoMerge: true}, - {Number: 2, AutoMerge: true}, }, ) t.Render() diff --git a/examples/cerebro/operator.go b/examples/cerebro/operator.go index 8647ef106..b91337715 100644 --- a/examples/cerebro/operator.go +++ b/examples/cerebro/operator.go @@ -274,7 +274,6 @@ func (o *Operator) reloadConfig() { } o.config.Networks = append(o.config.Networks, NetworkConfig{ - ChainName: network.Name, ChainID: network.ChainID, CustodyAddress: common.HexToAddress(network.CustodyAddress), AdjudicatorAddress: common.HexToAddress(network.AdjudicatorAddress), @@ -294,8 +293,7 @@ func (o *Operator) getChainSuggestions(filterEnabled int) []prompt.Suggest { if include { suggestions = append(suggestions, prompt.Suggest{ - Text: network.ChainName, - Description: fmt.Sprintf("Chain %s (ID: %d)", network.ChainName, network.ChainID), + Text: fmt.Sprintf("%d", network.ChainID), }) } } @@ -303,10 +301,10 @@ func (o *Operator) getChainSuggestions(filterEnabled int) []prompt.Suggest { } // getAssetSuggestions returns a list of asset suggestions for a specific chain. -// chainName is the name of the chain, and filterEnabled can be 0 (all assets), +// chainIDStr is the ID of the chain, and filterEnabled can be 0 (all assets), // >0 (only enabled assets), or <0 (only disabled assets). -func (o *Operator) getAssetSuggestions(chainName string, filterEnabled int) []prompt.Suggest { - if chainName == "" { +func (o *Operator) getAssetSuggestions(chainIDStr string, filterEnabled int) []prompt.Suggest { + if chainIDStr == "" { assetSymbols := o.config.GetSymbolsOfEnabledAssets() suggestions := make([]prompt.Suggest, len(assetSymbols)) for i, symbol := range assetSymbols { @@ -319,7 +317,12 @@ func (o *Operator) getAssetSuggestions(chainName string, filterEnabled int) []pr return suggestions } - network := o.config.GetNetworkByName(chainName) + chainID, ok := new(big.Int).SetString(chainIDStr, 10) + if !ok { + return nil + } + + network := o.config.GetNetworkByID(uint32(chainID.Uint64())) if network == nil { return nil } diff --git a/examples/cerebro/unisig/ecdsa.go b/examples/cerebro/unisig/ecdsa.go index dd608b1e7..498d6a775 100644 --- a/examples/cerebro/unisig/ecdsa.go +++ b/examples/cerebro/unisig/ecdsa.go @@ -31,7 +31,7 @@ func (s *EcdsaSigner) Address() common.Address { } // Sign creates an ECDSA signature for the provided message -func (s *EcdsaSigner) Sign(msg []byte) ([]byte, error) { +func (s *EcdsaSigner) Sign(msg []byte) (Signature, error) { signature, err := crypto.Sign(msg, s.privateKey) if err != nil { return nil, fmt.Errorf("failed to sign data: %w", err) @@ -41,6 +41,10 @@ func (s *EcdsaSigner) Sign(msg []byte) ([]byte, error) { return nil, fmt.Errorf("invalid signature length: got %d, want 65", len(signature)) } + if signature[64] < 27 { + signature[64] += 27 + } + return signature, nil } diff --git a/examples/cerebro/unisig/signer.go b/examples/cerebro/unisig/signer.go index 8ddb904d8..0bf206506 100644 --- a/examples/cerebro/unisig/signer.go +++ b/examples/cerebro/unisig/signer.go @@ -1,10 +1,36 @@ package unisig import ( + "encoding/json" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ) type Signer interface { Address() common.Address - Sign(data []byte) ([]byte, error) + Sign(msg []byte) (Signature, error) +} + +type Signature []byte + +func (s Signature) MarshalJSON() ([]byte, error) { + return json.Marshal(hexutil.Encode(s)) +} + +func (s *Signature) UnmarshalJSON(data []byte) error { + var hexStr string + if err := json.Unmarshal(data, &hexStr); err != nil { + return err + } + decoded, err := hexutil.Decode(hexStr) + if err != nil { + return err + } + *s = decoded + return nil +} + +func (s Signature) String() string { + return hexutil.Encode(s) } From 7a92a35e0625faa3f1bff063c44b14975e48c6c1 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Tue, 19 Aug 2025 12:52:15 +0300 Subject: [PATCH 13/18] fix: channel amount --- examples/cerebro/operator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cerebro/operator.go b/examples/cerebro/operator.go index b91337715..7fab51854 100644 --- a/examples/cerebro/operator.go +++ b/examples/cerebro/operator.go @@ -258,7 +258,7 @@ func (o *Operator) reloadConfig() { for _, channel := range channels { if channel.ChainID == network.ChainID && channel.Token == asset.Token { channelID = channel.ChannelID - rawChannelBalance = channel.RawAmount + rawChannelBalance = channel.RawAmount.BigInt() break } } From 703c84c3b900b621fcbc22dc1e6c6ad03c9204cd Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Aug 2025 09:17:29 +0300 Subject: [PATCH 14/18] feat(examples): remove snake, tictactoe, aura --- examples/snake/.gitignore | 30 - examples/snake/README.md | 118 - examples/snake/client/index.html | 13 - examples/snake/client/nixpacks.toml | 5 - examples/snake/client/package-lock.json | 2062 --- examples/snake/client/package.json | 28 - examples/snake/client/public/vite.svg | 1 - examples/snake/client/src/App.vue | 355 - .../snake/client/src/components/GameRoom.vue | 717 - .../snake/client/src/components/Lobby.vue | 671 - examples/snake/client/src/config/index.ts | 13 - examples/snake/client/src/crypto/index.ts | 94 - examples/snake/client/src/main.ts | 5 - .../client/src/services/ClearNetService.ts | 354 - .../snake/client/src/services/GameService.ts | 630 - .../client/src/services/authentication.ts | 235 - examples/snake/client/src/style.css | 34 - examples/snake/client/src/vite-env.d.ts | 7 - examples/snake/client/tsconfig.json | 28 - examples/snake/client/vite.config.ts | 16 - examples/snake/server/config/index.ts | 6 - examples/snake/server/interfaces/index.ts | 57 - .../server/middlewares/authMiddleware.ts | 48 - examples/snake/server/middlewares/index.ts | 1 - examples/snake/server/nixpacks.toml | 2 - examples/snake/server/package-lock.json | 1880 --- examples/snake/server/package.json | 31 - examples/snake/server/server.ts | 42 - .../server/services/appSessionService.ts | 322 - .../snake/server/services/brokerService.ts | 711 - examples/snake/server/services/gameService.ts | 204 - .../snake/server/services/stateService.ts | 83 - .../snake/server/services/websocketService.ts | 882 -- examples/snake/server/tsconfig.json | 21 - examples/snake/server/utils/shutdown.ts | 66 - examples/snake/tsconfig.json | 8 - examples/tictactoe/Readme.md | 54 - examples/tictactoe/client/.gitignore | 24 - examples/tictactoe/client/README.md | 41 - examples/tictactoe/client/eslint.config.js | 29 - examples/tictactoe/client/index.html | 21 - examples/tictactoe/client/package-lock.json | 11197 ---------------- examples/tictactoe/client/package.json | 55 - examples/tictactoe/client/postcss.config.js | 6 - .../client/public/android-chrome-192x192.png | Bin 28617 -> 0 bytes .../client/public/android-chrome-512x512.png | Bin 129468 -> 0 bytes .../client/public/apple-touch-icon.png | Bin 26135 -> 0 bytes examples/tictactoe/client/public/banner.webp | Bin 300104 -> 0 bytes .../tictactoe/client/public/banner_v2.webp | Bin 37730 -> 0 bytes .../tictactoe/client/public/favicon-16x16.png | Bin 688 -> 0 bytes .../tictactoe/client/public/favicon-32x32.png | Bin 1839 -> 0 bytes examples/tictactoe/client/public/favicon.ico | Bin 15406 -> 0 bytes examples/tictactoe/client/public/logo.png | Bin 272311 -> 0 bytes examples/tictactoe/client/public/logo.webp | Bin 28244 -> 0 bytes examples/tictactoe/client/public/vite.svg | 1 - examples/tictactoe/client/src/App.css | 94 - examples/tictactoe/client/src/App.tsx | 245 - .../tictactoe/client/src/assets/react.svg | 1 - .../src/components/BackgroundAnimation.tsx | 261 - .../tictactoe/client/src/components/Board.tsx | 56 - .../tictactoe/client/src/components/Cell.tsx | 131 - .../client/src/components/CellAuraEffect.tsx | 175 - .../client/src/components/CellMarkEffect.tsx | 132 - .../client/src/components/ChannelDeposit.tsx | 125 - .../src/components/ChannelRequiredModal.tsx | 246 - .../client/src/components/ErrorModal.tsx | 59 - .../src/components/GameLobbyIntegrated.tsx | 136 - .../client/src/components/GameOver.tsx | 115 - .../client/src/components/GameScreen.tsx | 190 - .../client/src/components/GameStatus.tsx | 136 - .../tictactoe/client/src/components/Lobby.tsx | 459 - .../src/components/OnlinePlayersCounter.tsx | 25 - .../src/components/README_AURA_EFFECTS.md | 82 - .../client/src/components/RoomInfo.tsx | 72 - .../client/src/components/ui/badge.tsx | 40 - .../client/src/components/ui/button.tsx | 70 - .../client/src/components/ui/card.tsx | 76 - .../client/src/components/ui/dialog.tsx | 146 - .../client/src/components/ui/input.tsx | 71 - .../components/ui/styled-wallet-button.tsx | 31 - .../client/src/components/ui/tabs.tsx | 53 - .../src/context/NitroliteClientWrapper.tsx | 237 - .../client/src/context/WebSocketContext.tsx | 346 - examples/tictactoe/client/src/context/app.ts | 35 - .../client/src/context/createSigner.ts | 94 - .../src/hooks/useAppSessionSignature.ts | 100 - .../tictactoe/client/src/hooks/useChannel.ts | 203 - .../client/src/hooks/useGameState.ts | 225 - .../tictactoe/client/src/hooks/useMetaMask.ts | 108 - .../src/hooks/useNitroliteIntegration.ts | 70 - .../client/src/hooks/useSoundEffects.ts | 107 - .../client/src/hooks/useWebSocket.ts | 135 - .../client/src/hooks/useWebSocketNitrolite.ts | 35 - .../client/src/hooks/utils/tokenDecimals.ts | 130 - examples/tictactoe/client/src/index.css | 217 - examples/tictactoe/client/src/lib/utils.ts | 6 - examples/tictactoe/client/src/main.tsx | 20 - .../providers/RainbowkitConnectProvider.tsx | 64 - examples/tictactoe/client/src/store/index.ts | 121 - .../tictactoe/client/src/store/storeUtils.ts | 69 - examples/tictactoe/client/src/types/index.ts | 222 - .../tictactoe/client/src/utils/metamask.ts | 25 - examples/tictactoe/client/src/vite-env.d.ts | 1 - .../tictactoe/client/src/websocket/index.ts | 2 - .../client/src/websocket/websocket.ts | 648 - examples/tictactoe/client/tailwind.config.js | 190 - examples/tictactoe/client/tsconfig.app.json | 27 - examples/tictactoe/client/tsconfig.json | 7 - examples/tictactoe/client/tsconfig.node.json | 25 - examples/tictactoe/client/vite.config.ts | 7 - examples/tictactoe/server/.env | 18 - examples/tictactoe/server/.gitignore | 24 - examples/tictactoe/server/README.md | 132 - examples/tictactoe/server/package-lock.json | 1734 --- examples/tictactoe/server/package.json | 27 - .../tictactoe/server/src/config/websocket.js | 53 - .../tictactoe/server/src/routes/gameRoutes.js | 222 - .../tictactoe/server/src/routes/roomRoutes.js | 142 - examples/tictactoe/server/src/server.js | 317 - .../server/src/services/appSessions.js | 691 - .../tictactoe/server/src/services/index.js | 31 - .../server/src/services/nitroliteOnChain.js | 81 - .../server/src/services/nitroliteRPC.js | 549 - .../server/src/services/roomManager.js | 268 - .../server/src/services/ticTacToe.js | 137 - examples/tictactoe/server/src/utils/logger.js | 50 - .../tictactoe/server/src/utils/validators.js | 103 - examples/viper-duel/.gitignore | 1 - .../viper-duel/GAME_DEVELOPMENT_PROMPTS.md | 596 - examples/viper-duel/Readme.md | 0 examples/viper-duel/client/.gitignore | 24 - examples/viper-duel/client/README.md | 54 - examples/viper-duel/client/eslint.config.js | 29 - examples/viper-duel/client/index.html | 21 - examples/viper-duel/client/nixpacks.toml | 2 - examples/viper-duel/client/package-lock.json | 10526 --------------- examples/viper-duel/client/package.json | 51 - examples/viper-duel/client/postcss.config.js | 6 - .../client/public/android-chrome-192x192.png | Bin 16670 -> 0 bytes .../client/public/android-chrome-512x512.png | Bin 89257 -> 0 bytes .../client/public/apple-touch-icon.png | Bin 15027 -> 0 bytes .../client/public/favicon-16x16.png | Bin 474 -> 0 bytes .../client/public/favicon-32x32.png | Bin 1205 -> 0 bytes examples/viper-duel/client/public/favicon.ico | Bin 15406 -> 0 bytes examples/viper-duel/client/public/vite.svg | 1 - examples/viper-duel/client/src/App.css | 94 - examples/viper-duel/client/src/App.tsx | 245 - .../viper-duel/client/src/assets/react.svg | 1 - .../src/components/BackgroundAnimation.tsx | 398 - .../client/src/components/Board.tsx | 94 - .../viper-duel/client/src/components/Cell.tsx | 131 - .../client/src/components/CellAuraEffect.tsx | 175 - .../client/src/components/CellMarkEffect.tsx | 132 - .../client/src/components/ChannelDeposit.tsx | 125 - .../src/components/ChannelRequiredModal.tsx | 246 - .../client/src/components/ErrorModal.tsx | 59 - .../src/components/GameLobbyIntegrated.tsx | 136 - .../client/src/components/GameOver.tsx | 132 - .../client/src/components/GameScreen.tsx | 228 - .../client/src/components/GameStatus.tsx | 136 - .../client/src/components/Lobby.tsx | 505 - .../src/components/OnlinePlayersCounter.tsx | 25 - .../src/components/README_AURA_EFFECTS.md | 82 - .../client/src/components/RoomInfo.tsx | 72 - .../client/src/components/SnakeCanvas.tsx | 279 - .../client/src/components/ui/badge.tsx | 40 - .../client/src/components/ui/button.tsx | 72 - .../client/src/components/ui/card.tsx | 76 - .../client/src/components/ui/dialog.tsx | 146 - .../client/src/components/ui/input.tsx | 71 - .../components/ui/styled-wallet-button.tsx | 71 - .../client/src/components/ui/tabs.tsx | 53 - .../src/context/NitroliteClientWrapper.tsx | 213 - .../client/src/context/WebSocketContext.tsx | 346 - examples/viper-duel/client/src/context/app.ts | 35 - .../client/src/context/createSigner.ts | 94 - .../src/hooks/useAppSessionSignature.ts | 100 - .../viper-duel/client/src/hooks/useChannel.ts | 203 - .../client/src/hooks/useGameState.ts | 257 - .../client/src/hooks/useMetaMask.ts | 108 - .../src/hooks/useNitroliteIntegration.ts | 70 - .../client/src/hooks/useSoundEffects.ts | 107 - .../client/src/hooks/useWebSocket.ts | 135 - .../client/src/hooks/useWebSocketNitrolite.ts | 35 - .../client/src/hooks/utils/tokenDecimals.ts | 130 - examples/viper-duel/client/src/index.css | 267 - examples/viper-duel/client/src/lib/utils.ts | 6 - examples/viper-duel/client/src/main.tsx | 20 - .../providers/RainbowkitConnectProvider.tsx | 68 - examples/viper-duel/client/src/store/index.ts | 120 - .../viper-duel/client/src/store/storeUtils.ts | 69 - examples/viper-duel/client/src/types/index.ts | 259 - .../viper-duel/client/src/utils/metamask.ts | 25 - examples/viper-duel/client/src/vite-env.d.ts | 1 - .../viper-duel/client/src/websocket/index.ts | 2 - .../client/src/websocket/websocket.ts | 644 - examples/viper-duel/client/tailwind.config.js | 214 - examples/viper-duel/client/tsconfig.app.json | 27 - examples/viper-duel/client/tsconfig.json | 7 - examples/viper-duel/client/tsconfig.node.json | 25 - examples/viper-duel/client/vite.config.ts | 7 - examples/viper-duel/server/.env | 18 - examples/viper-duel/server/.gitignore | 24 - examples/viper-duel/server/README.md | 132 - examples/viper-duel/server/package-lock.json | 1734 --- examples/viper-duel/server/package.json | 27 - .../viper-duel/server/src/config/websocket.js | 53 - .../server/src/routes/gameRoutes.js | 368 - .../server/src/routes/roomRoutes.js | 150 - examples/viper-duel/server/src/server.js | 322 - .../server/src/services/appSessions.js | 723 - .../viper-duel/server/src/services/index.js | 31 - .../server/src/services/nitroliteOnChain.js | 81 - .../server/src/services/nitroliteRPC.js | 552 - .../server/src/services/roomManager.js | 318 - .../viper-duel/server/src/services/snake.js | 502 - .../viper-duel/server/src/utils/logger.js | 50 - .../viper-duel/server/src/utils/validators.js | 119 - 218 files changed, 56423 deletions(-) delete mode 100644 examples/snake/.gitignore delete mode 100644 examples/snake/README.md delete mode 100644 examples/snake/client/index.html delete mode 100644 examples/snake/client/nixpacks.toml delete mode 100644 examples/snake/client/package-lock.json delete mode 100644 examples/snake/client/package.json delete mode 100644 examples/snake/client/public/vite.svg delete mode 100644 examples/snake/client/src/App.vue delete mode 100644 examples/snake/client/src/components/GameRoom.vue delete mode 100644 examples/snake/client/src/components/Lobby.vue delete mode 100644 examples/snake/client/src/config/index.ts delete mode 100644 examples/snake/client/src/crypto/index.ts delete mode 100644 examples/snake/client/src/main.ts delete mode 100644 examples/snake/client/src/services/ClearNetService.ts delete mode 100644 examples/snake/client/src/services/GameService.ts delete mode 100644 examples/snake/client/src/services/authentication.ts delete mode 100644 examples/snake/client/src/style.css delete mode 100644 examples/snake/client/src/vite-env.d.ts delete mode 100644 examples/snake/client/tsconfig.json delete mode 100644 examples/snake/client/vite.config.ts delete mode 100644 examples/snake/server/config/index.ts delete mode 100644 examples/snake/server/interfaces/index.ts delete mode 100644 examples/snake/server/middlewares/authMiddleware.ts delete mode 100644 examples/snake/server/middlewares/index.ts delete mode 100644 examples/snake/server/nixpacks.toml delete mode 100644 examples/snake/server/package-lock.json delete mode 100644 examples/snake/server/package.json delete mode 100644 examples/snake/server/server.ts delete mode 100644 examples/snake/server/services/appSessionService.ts delete mode 100644 examples/snake/server/services/brokerService.ts delete mode 100644 examples/snake/server/services/gameService.ts delete mode 100644 examples/snake/server/services/stateService.ts delete mode 100644 examples/snake/server/services/websocketService.ts delete mode 100644 examples/snake/server/tsconfig.json delete mode 100644 examples/snake/server/utils/shutdown.ts delete mode 100644 examples/snake/tsconfig.json delete mode 100644 examples/tictactoe/Readme.md delete mode 100644 examples/tictactoe/client/.gitignore delete mode 100644 examples/tictactoe/client/README.md delete mode 100644 examples/tictactoe/client/eslint.config.js delete mode 100644 examples/tictactoe/client/index.html delete mode 100644 examples/tictactoe/client/package-lock.json delete mode 100644 examples/tictactoe/client/package.json delete mode 100644 examples/tictactoe/client/postcss.config.js delete mode 100644 examples/tictactoe/client/public/android-chrome-192x192.png delete mode 100644 examples/tictactoe/client/public/android-chrome-512x512.png delete mode 100644 examples/tictactoe/client/public/apple-touch-icon.png delete mode 100644 examples/tictactoe/client/public/banner.webp delete mode 100644 examples/tictactoe/client/public/banner_v2.webp delete mode 100644 examples/tictactoe/client/public/favicon-16x16.png delete mode 100644 examples/tictactoe/client/public/favicon-32x32.png delete mode 100644 examples/tictactoe/client/public/favicon.ico delete mode 100644 examples/tictactoe/client/public/logo.png delete mode 100644 examples/tictactoe/client/public/logo.webp delete mode 100644 examples/tictactoe/client/public/vite.svg delete mode 100644 examples/tictactoe/client/src/App.css delete mode 100644 examples/tictactoe/client/src/App.tsx delete mode 100644 examples/tictactoe/client/src/assets/react.svg delete mode 100644 examples/tictactoe/client/src/components/BackgroundAnimation.tsx delete mode 100644 examples/tictactoe/client/src/components/Board.tsx delete mode 100644 examples/tictactoe/client/src/components/Cell.tsx delete mode 100644 examples/tictactoe/client/src/components/CellAuraEffect.tsx delete mode 100644 examples/tictactoe/client/src/components/CellMarkEffect.tsx delete mode 100644 examples/tictactoe/client/src/components/ChannelDeposit.tsx delete mode 100644 examples/tictactoe/client/src/components/ChannelRequiredModal.tsx delete mode 100644 examples/tictactoe/client/src/components/ErrorModal.tsx delete mode 100644 examples/tictactoe/client/src/components/GameLobbyIntegrated.tsx delete mode 100644 examples/tictactoe/client/src/components/GameOver.tsx delete mode 100644 examples/tictactoe/client/src/components/GameScreen.tsx delete mode 100644 examples/tictactoe/client/src/components/GameStatus.tsx delete mode 100644 examples/tictactoe/client/src/components/Lobby.tsx delete mode 100644 examples/tictactoe/client/src/components/OnlinePlayersCounter.tsx delete mode 100644 examples/tictactoe/client/src/components/README_AURA_EFFECTS.md delete mode 100644 examples/tictactoe/client/src/components/RoomInfo.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/badge.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/button.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/card.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/dialog.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/input.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/styled-wallet-button.tsx delete mode 100644 examples/tictactoe/client/src/components/ui/tabs.tsx delete mode 100644 examples/tictactoe/client/src/context/NitroliteClientWrapper.tsx delete mode 100644 examples/tictactoe/client/src/context/WebSocketContext.tsx delete mode 100644 examples/tictactoe/client/src/context/app.ts delete mode 100644 examples/tictactoe/client/src/context/createSigner.ts delete mode 100644 examples/tictactoe/client/src/hooks/useAppSessionSignature.ts delete mode 100644 examples/tictactoe/client/src/hooks/useChannel.ts delete mode 100644 examples/tictactoe/client/src/hooks/useGameState.ts delete mode 100644 examples/tictactoe/client/src/hooks/useMetaMask.ts delete mode 100644 examples/tictactoe/client/src/hooks/useNitroliteIntegration.ts delete mode 100644 examples/tictactoe/client/src/hooks/useSoundEffects.ts delete mode 100644 examples/tictactoe/client/src/hooks/useWebSocket.ts delete mode 100644 examples/tictactoe/client/src/hooks/useWebSocketNitrolite.ts delete mode 100644 examples/tictactoe/client/src/hooks/utils/tokenDecimals.ts delete mode 100644 examples/tictactoe/client/src/index.css delete mode 100644 examples/tictactoe/client/src/lib/utils.ts delete mode 100644 examples/tictactoe/client/src/main.tsx delete mode 100644 examples/tictactoe/client/src/providers/RainbowkitConnectProvider.tsx delete mode 100644 examples/tictactoe/client/src/store/index.ts delete mode 100644 examples/tictactoe/client/src/store/storeUtils.ts delete mode 100644 examples/tictactoe/client/src/types/index.ts delete mode 100644 examples/tictactoe/client/src/utils/metamask.ts delete mode 100644 examples/tictactoe/client/src/vite-env.d.ts delete mode 100644 examples/tictactoe/client/src/websocket/index.ts delete mode 100644 examples/tictactoe/client/src/websocket/websocket.ts delete mode 100644 examples/tictactoe/client/tailwind.config.js delete mode 100644 examples/tictactoe/client/tsconfig.app.json delete mode 100644 examples/tictactoe/client/tsconfig.json delete mode 100644 examples/tictactoe/client/tsconfig.node.json delete mode 100644 examples/tictactoe/client/vite.config.ts delete mode 100644 examples/tictactoe/server/.env delete mode 100644 examples/tictactoe/server/.gitignore delete mode 100644 examples/tictactoe/server/README.md delete mode 100644 examples/tictactoe/server/package-lock.json delete mode 100644 examples/tictactoe/server/package.json delete mode 100644 examples/tictactoe/server/src/config/websocket.js delete mode 100644 examples/tictactoe/server/src/routes/gameRoutes.js delete mode 100644 examples/tictactoe/server/src/routes/roomRoutes.js delete mode 100644 examples/tictactoe/server/src/server.js delete mode 100644 examples/tictactoe/server/src/services/appSessions.js delete mode 100644 examples/tictactoe/server/src/services/index.js delete mode 100644 examples/tictactoe/server/src/services/nitroliteOnChain.js delete mode 100644 examples/tictactoe/server/src/services/nitroliteRPC.js delete mode 100644 examples/tictactoe/server/src/services/roomManager.js delete mode 100644 examples/tictactoe/server/src/services/ticTacToe.js delete mode 100644 examples/tictactoe/server/src/utils/logger.js delete mode 100644 examples/tictactoe/server/src/utils/validators.js delete mode 100644 examples/viper-duel/.gitignore delete mode 100644 examples/viper-duel/GAME_DEVELOPMENT_PROMPTS.md delete mode 100644 examples/viper-duel/Readme.md delete mode 100644 examples/viper-duel/client/.gitignore delete mode 100644 examples/viper-duel/client/README.md delete mode 100644 examples/viper-duel/client/eslint.config.js delete mode 100644 examples/viper-duel/client/index.html delete mode 100644 examples/viper-duel/client/nixpacks.toml delete mode 100644 examples/viper-duel/client/package-lock.json delete mode 100644 examples/viper-duel/client/package.json delete mode 100644 examples/viper-duel/client/postcss.config.js delete mode 100644 examples/viper-duel/client/public/android-chrome-192x192.png delete mode 100644 examples/viper-duel/client/public/android-chrome-512x512.png delete mode 100644 examples/viper-duel/client/public/apple-touch-icon.png delete mode 100644 examples/viper-duel/client/public/favicon-16x16.png delete mode 100644 examples/viper-duel/client/public/favicon-32x32.png delete mode 100644 examples/viper-duel/client/public/favicon.ico delete mode 100644 examples/viper-duel/client/public/vite.svg delete mode 100644 examples/viper-duel/client/src/App.css delete mode 100644 examples/viper-duel/client/src/App.tsx delete mode 100644 examples/viper-duel/client/src/assets/react.svg delete mode 100644 examples/viper-duel/client/src/components/BackgroundAnimation.tsx delete mode 100644 examples/viper-duel/client/src/components/Board.tsx delete mode 100644 examples/viper-duel/client/src/components/Cell.tsx delete mode 100644 examples/viper-duel/client/src/components/CellAuraEffect.tsx delete mode 100644 examples/viper-duel/client/src/components/CellMarkEffect.tsx delete mode 100644 examples/viper-duel/client/src/components/ChannelDeposit.tsx delete mode 100644 examples/viper-duel/client/src/components/ChannelRequiredModal.tsx delete mode 100644 examples/viper-duel/client/src/components/ErrorModal.tsx delete mode 100644 examples/viper-duel/client/src/components/GameLobbyIntegrated.tsx delete mode 100644 examples/viper-duel/client/src/components/GameOver.tsx delete mode 100644 examples/viper-duel/client/src/components/GameScreen.tsx delete mode 100644 examples/viper-duel/client/src/components/GameStatus.tsx delete mode 100644 examples/viper-duel/client/src/components/Lobby.tsx delete mode 100644 examples/viper-duel/client/src/components/OnlinePlayersCounter.tsx delete mode 100644 examples/viper-duel/client/src/components/README_AURA_EFFECTS.md delete mode 100644 examples/viper-duel/client/src/components/RoomInfo.tsx delete mode 100644 examples/viper-duel/client/src/components/SnakeCanvas.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/badge.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/button.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/card.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/dialog.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/input.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/styled-wallet-button.tsx delete mode 100644 examples/viper-duel/client/src/components/ui/tabs.tsx delete mode 100644 examples/viper-duel/client/src/context/NitroliteClientWrapper.tsx delete mode 100644 examples/viper-duel/client/src/context/WebSocketContext.tsx delete mode 100644 examples/viper-duel/client/src/context/app.ts delete mode 100644 examples/viper-duel/client/src/context/createSigner.ts delete mode 100644 examples/viper-duel/client/src/hooks/useAppSessionSignature.ts delete mode 100644 examples/viper-duel/client/src/hooks/useChannel.ts delete mode 100644 examples/viper-duel/client/src/hooks/useGameState.ts delete mode 100644 examples/viper-duel/client/src/hooks/useMetaMask.ts delete mode 100644 examples/viper-duel/client/src/hooks/useNitroliteIntegration.ts delete mode 100644 examples/viper-duel/client/src/hooks/useSoundEffects.ts delete mode 100644 examples/viper-duel/client/src/hooks/useWebSocket.ts delete mode 100644 examples/viper-duel/client/src/hooks/useWebSocketNitrolite.ts delete mode 100644 examples/viper-duel/client/src/hooks/utils/tokenDecimals.ts delete mode 100644 examples/viper-duel/client/src/index.css delete mode 100644 examples/viper-duel/client/src/lib/utils.ts delete mode 100644 examples/viper-duel/client/src/main.tsx delete mode 100644 examples/viper-duel/client/src/providers/RainbowkitConnectProvider.tsx delete mode 100644 examples/viper-duel/client/src/store/index.ts delete mode 100644 examples/viper-duel/client/src/store/storeUtils.ts delete mode 100644 examples/viper-duel/client/src/types/index.ts delete mode 100644 examples/viper-duel/client/src/utils/metamask.ts delete mode 100644 examples/viper-duel/client/src/vite-env.d.ts delete mode 100644 examples/viper-duel/client/src/websocket/index.ts delete mode 100644 examples/viper-duel/client/src/websocket/websocket.ts delete mode 100644 examples/viper-duel/client/tailwind.config.js delete mode 100644 examples/viper-duel/client/tsconfig.app.json delete mode 100644 examples/viper-duel/client/tsconfig.json delete mode 100644 examples/viper-duel/client/tsconfig.node.json delete mode 100644 examples/viper-duel/client/vite.config.ts delete mode 100644 examples/viper-duel/server/.env delete mode 100644 examples/viper-duel/server/.gitignore delete mode 100644 examples/viper-duel/server/README.md delete mode 100644 examples/viper-duel/server/package-lock.json delete mode 100644 examples/viper-duel/server/package.json delete mode 100644 examples/viper-duel/server/src/config/websocket.js delete mode 100644 examples/viper-duel/server/src/routes/gameRoutes.js delete mode 100644 examples/viper-duel/server/src/routes/roomRoutes.js delete mode 100644 examples/viper-duel/server/src/server.js delete mode 100644 examples/viper-duel/server/src/services/appSessions.js delete mode 100644 examples/viper-duel/server/src/services/index.js delete mode 100644 examples/viper-duel/server/src/services/nitroliteOnChain.js delete mode 100644 examples/viper-duel/server/src/services/nitroliteRPC.js delete mode 100644 examples/viper-duel/server/src/services/roomManager.js delete mode 100644 examples/viper-duel/server/src/services/snake.js delete mode 100644 examples/viper-duel/server/src/utils/logger.js delete mode 100644 examples/viper-duel/server/src/utils/validators.js diff --git a/examples/snake/.gitignore b/examples/snake/.gitignore deleted file mode 100644 index 289ac982e..000000000 --- a/examples/snake/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -dist-server -*.local - -# Environment variables -.env -.env.* - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -*.tsbuildinfo diff --git a/examples/snake/README.md b/examples/snake/README.md deleted file mode 100644 index df2538f13..000000000 --- a/examples/snake/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Nitro-Snake - -An online two-player snake game built with Vue and WebSocket, featuring state channels integration for secure, off-chain gameplay payments. - -## Features - -- Realtime multiplayer gameplay with WebSocket -- Random room ID generation for private games -- Interactive lobby to create or join games -- Player nicknames and scores -- Collision detection and game physics with wrapping edges -- Clean, responsive UI -- Nitrolite integration for state channels -- Secure payment handling through state channels -- Real-time state signing and submission -- Automated channel finalization on game over - -## Stack - -- Vue 3 + TypeScript + Vite -- Node.js + Express for backend API -- WebSocket for realtime communication -- Canvas-based game rendering -- @erc7824/nitrolite for state channel management -- Ethereum wallet integration with ethers.js and viem - -## How to Play - -1. Connect your wallet to the game -2. Enter your nickname -3. Create or join a payment channel with your deposit -4. Create a new game room or join an existing one with a room ID -5. Share the room ID with your friend -6. Use arrow keys or WASD to control your snake -7. Eat food to grow your snake and earn points -8. Avoid collisions with your own tail or the other player's snake -9. When the game is over, the winner gets a larger share of the deposited funds -10. Withdraw your funds back to your wallet - -## Game Rules - -- Each player controls a snake on a 40x30 grid -- Eat food to grow your snake and earn points -- If you collide with another snake or your own tail, your snake dies -- The game has "wrapping" edges - snakes can move off one edge and appear on the opposite side -- Game ends when a player collides with any snake segment -- The player with the highest score wins - -## Running Locally - -```bash -# Install dependencies -npm install - -# Set up environment variables (create .env file) -echo "BROKER_WS_URL=wss://broker.example.com/ws # see docs for the URL -SERVER_PRIVATE_KEY=your_server_private_key # used to sign the state updates in game server to broker comms -WALLET_PRIVATE_KEY=your_wallet_private_key # used to fund the state channel with the broker -POLYGON_RPC_URL=your_polygon_rpc_url -PORT=3001" > .env - -# Start the WebSocket server -npm run server - -# In another terminal, start the frontend -npm run dev -``` - -Visit `http://localhost:5173` in your browser to play the game. - -## Environment Variables - -- `BROKER_WS_URL`: WebSocket URL for Nitrolite broker -- `SERVER_PRIVATE_KEY`: Private key for server -- `WALLET_PRIVATE_KEY`: Private key for wallet -- `POLYGON_RPC_URL`: RPC URL for Polygon blockchain -- `PORT`: Port number for the WebSocket server (default: 3001) -- `CUSTODY_ADDRESS`: Address of the custody contract (optional) -- `ADJUDICATOR_ADDRESS`: Address of the adjudicator contract (optional) -- `GUEST_ADDRESS`: Address of the guest wallet (optional) -- `TOKEN_ADDRESS`: Address of the token contract (optional) - -## Communication Flow - -1. Player connects their wallet and creates a state channel with a deposit -2. First player creates a room with a unique ID -3. Second player joins the room using the same ID and creates/joins the channel -4. When both players are connected, the game starts -5. Players control their snakes using keyboard inputs -6. Game state is synchronized between all clients in real-time -7. State updates are signed and submitted to the Nitrolite broker -8. When the game ends, the final state is submitted and the channel is finalized -9. Funds are distributed according to the game outcome -10. Players can withdraw their funds back to their wallets - -## State Channel Integration - -The game uses Nitrolite state channels for: - -1. **Secure payments**: Players deposit funds when creating or joining a channel -2. **Real-time state updates**: Game state is signed by all participants -3. **Off-chain transactions**: All gameplay happens off-chain for better performance -4. **Verifiable outcomes**: Game results are cryptographically secured -5. **Fair fund distribution**: Funds are distributed based on game outcome - -## API Endpoints - -- `/api/contract-addresses` - Get contract addresses for channel setup -- `/api/rooms` - Get active rooms -- `/api/rooms/:roomId` - Get room details -- `/api/rooms/:roomId/channel` - Get channel info for a specific room -- `/api/game-sessions` - Create a new game session -- `/api/game-sessions/:sessionId/state` - Get the state of a game session -- `/api/auth-status` - Check authentication status -- `/api/sign-state` - Sign game state -- `/api/verify-signature` - Verify signatures -- `/api/auth/challenge` - Generate auth challenge -- `/api/auth/verify` - Verify auth challenge diff --git a/examples/snake/client/index.html b/examples/snake/client/index.html deleted file mode 100644 index 60dd2691c..000000000 --- a/examples/snake/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Nitro Snake - - -
- - - diff --git a/examples/snake/client/nixpacks.toml b/examples/snake/client/nixpacks.toml deleted file mode 100644 index e374f181c..000000000 --- a/examples/snake/client/nixpacks.toml +++ /dev/null @@ -1,5 +0,0 @@ -[phases.install] -cmds = ['npm install'] - -[start] -cmd = 'npm run preview' diff --git a/examples/snake/client/package-lock.json b/examples/snake/client/package-lock.json deleted file mode 100644 index 4f9dbe2c4..000000000 --- a/examples/snake/client/package-lock.json +++ /dev/null @@ -1,2062 +0,0 @@ -{ - "name": "nitro-snake-client", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nitro-snake-client", - "version": "0.0.0", - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "axios": "^1.9.0", - "ethers": "6.7.1", - "viem": "2.29.3", - "vue": "^3.5.13" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.2.3", - "@vue/tsconfig": "^0.7.0", - "typescript": "~5.8.3", - "vite": "^6.3.5", - "vue-tsc": "^2.2.8" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@erc7824/nitrolite": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@erc7824/nitrolite/-/nitrolite-0.2.20.tgz", - "integrity": "sha512-iYzgHh6Acd3Pq1tjQ+yzEiZQh68xHZ4P+r5UMrweqBMU93Fr39pHEYe/PkdAGppREOHoXvuIDDp6S7ORTxfkug==", - "license": "MIT", - "dependencies": { - "abitype": "^0.10.3", - "viem": "^2.5.0", - "zod": "^3.25.67" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@noble/curves": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", - "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.2" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", - "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", - "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", - "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", - "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", - "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", - "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", - "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", - "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", - "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", - "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", - "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", - "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", - "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", - "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", - "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", - "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", - "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", - "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", - "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", - "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@scure/base": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", - "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-vue": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", - "vue": "^3.2.25" - } - }, - "node_modules/@volar/language-core": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz", - "integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.14" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz", - "integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.14", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz", - "integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.14", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz", - "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.2", - "@vue/shared": "3.5.14", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz", - "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.14", - "@vue/shared": "3.5.14" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz", - "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.2", - "@vue/compiler-core": "3.5.14", - "@vue/compiler-dom": "3.5.14", - "@vue/compiler-ssr": "3.5.14", - "@vue/shared": "3.5.14", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.17", - "postcss": "^8.5.3", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz", - "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.14", - "@vue/shared": "3.5.14" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/@vue/language-core": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz", - "integrity": "sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "~2.4.11", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^1.0.3", - "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", - "integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.14" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz", - "integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.14", - "@vue/shared": "3.5.14" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz", - "integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.14", - "@vue/runtime-core": "3.5.14", - "@vue/shared": "3.5.14", - "csstype": "^3.1.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz", - "integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.14", - "@vue/shared": "3.5.14" - }, - "peerDependencies": { - "vue": "3.5.14" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz", - "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==", - "license": "MIT" - }, - "node_modules/@vue/tsconfig": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", - "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": "5.x", - "vue": "^3.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/abitype": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.3.tgz", - "integrity": "sha512-tRN+7XIa7J9xugdbRzFv/95ka5ivR/sRe01eiWvM0HWWjHuigSZEACgKa0sj4wGuekTDtghCx+5Izk/cOi78pQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT" - }, - "node_modules/alien-signals": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", - "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/ethers": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", - "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.9.2", - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.7.1", - "@types/node": "18.15.13", - "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/@adraffy/ens-normalize": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", - "license": "MIT" - }, - "node_modules/ethers/node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", - "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.1", - "@rollup/rollup-android-arm64": "4.41.1", - "@rollup/rollup-darwin-arm64": "4.41.1", - "@rollup/rollup-darwin-x64": "4.41.1", - "@rollup/rollup-freebsd-arm64": "4.41.1", - "@rollup/rollup-freebsd-x64": "4.41.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", - "@rollup/rollup-linux-arm-musleabihf": "4.41.1", - "@rollup/rollup-linux-arm64-gnu": "4.41.1", - "@rollup/rollup-linux-arm64-musl": "4.41.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-musl": "4.41.1", - "@rollup/rollup-linux-s390x-gnu": "4.41.1", - "@rollup/rollup-linux-x64-gnu": "4.41.1", - "@rollup/rollup-linux-x64-musl": "4.41.1", - "@rollup/rollup-win32-arm64-msvc": "4.41.1", - "@rollup/rollup-win32-ia32-msvc": "4.41.1", - "@rollup/rollup-win32-x64-msvc": "4.41.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/viem": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.29.3.tgz", - "integrity": "sha512-9D/nO+4S3Kk0P8vh6yXUA8OJ0mli9nzoY22qyh8iYHf1Q0GIejUnyq0QGjoDbkTcVzCVD5iA9KBkSxw9Tp3vUg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.2", - "@noble/hashes": "1.7.2", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.9", - "ws": "8.18.1" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vue": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz", - "integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.14", - "@vue/compiler-sfc": "3.5.14", - "@vue/runtime-dom": "3.5.14", - "@vue/server-renderer": "3.5.14", - "@vue/shared": "3.5.14" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vue-tsc": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.10.tgz", - "integrity": "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "~2.4.11", - "@vue/language-core": "2.2.10" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, - "node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/examples/snake/client/package.json b/examples/snake/client/package.json deleted file mode 100644 index 4bfdad792..000000000 --- a/examples/snake/client/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "nitro-snake-client", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vue-tsc -b && vite build", - "preview": "vite preview --host 0.0.0.0" - }, - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "axios": "^1.9.0", - "ethers": "6.7.1", - "viem": "2.29.3", - "vue": "^3.5.13" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.2.3", - "@vue/tsconfig": "^0.7.0", - "typescript": "~5.8.3", - "vite": "^6.3.5", - "vue-tsc": "^2.2.8" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/examples/snake/client/public/vite.svg b/examples/snake/client/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/examples/snake/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/snake/client/src/App.vue b/examples/snake/client/src/App.vue deleted file mode 100644 index b9bcfa2c6..000000000 --- a/examples/snake/client/src/App.vue +++ /dev/null @@ -1,355 +0,0 @@ - - - - - diff --git a/examples/snake/client/src/components/GameRoom.vue b/examples/snake/client/src/components/GameRoom.vue deleted file mode 100644 index c1030813b..000000000 --- a/examples/snake/client/src/components/GameRoom.vue +++ /dev/null @@ -1,717 +0,0 @@ - - - - - diff --git a/examples/snake/client/src/components/Lobby.vue b/examples/snake/client/src/components/Lobby.vue deleted file mode 100644 index 138c0518a..000000000 --- a/examples/snake/client/src/components/Lobby.vue +++ /dev/null @@ -1,671 +0,0 @@ - - - - - diff --git a/examples/snake/client/src/config/index.ts b/examples/snake/client/src/config/index.ts deleted file mode 100644 index fc8fdf6f4..000000000 --- a/examples/snake/client/src/config/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -const getEnvVar = (key: string, defaultValue: string): string => { - try { - const envValue = (import.meta.env?.[`VITE_${key}`] || window?.__ENV__?.[key] || null) as string | null; - return envValue || defaultValue; - } catch (e) { - console.warn(`Could not access environment variable ${key}, using default value`); - return defaultValue; - } -}; - -export const BROKER_WS_URL = getEnvVar("BROKER_WS_URL", ""); -export const GAMESERVER_WS_URL = getEnvVar("GAMESERVER_WS_URL", "ws://localhost:3001"); -export const CHAIN_ID = parseInt(getEnvVar("CHAIN_ID", "137"), 10); diff --git a/examples/snake/client/src/crypto/index.ts b/examples/snake/client/src/crypto/index.ts deleted file mode 100644 index c0450b214..000000000 --- a/examples/snake/client/src/crypto/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type Hex } from "viem"; -import { ethers } from "ethers"; -import { type MessageSigner, type RequestData, type ResponsePayload } from "@erc7824/nitrolite"; - -/** - * Interface for a cryptographic keypair - */ -export interface CryptoKeypair { - /** Private key in hexadecimal format */ - privateKey: Hex; - /** Optional Ethereum address derived from the public key */ - address?: Hex; -} - -/** - * Interface for a wallet signer that can sign messages - */ -export interface WalletSigner { - /** Optional Ethereum address derived from the public key */ - address: Hex; - /** Function to sign a message and return a hex signature */ - sign: MessageSigner; -} - -/** - * Creates a signer from a private key - * - * @param privateKey - The private key to create the signer from - * @returns A WalletSigner object that can sign messages - * @throws Error if signer creation fails - */ -export const createEthersSigner = (privateKey: string): WalletSigner => { - try { - // Create ethers wallet from private key - const wallet = new ethers.Wallet(privateKey); - - return { - address: ethers.getAddress(wallet.address) as Hex, - sign: async (payload: RequestData | ResponsePayload): Promise => { - try { - const message = JSON.stringify(payload); - console.log("Signing message in Sign function:", message); - const digestHex = ethers.id(message); - console.log("Digest Hex:", digestHex); - const messageBytes = ethers.getBytes(digestHex); - - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - - return signature as Hex; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }, - }; - } catch (error) { - console.error("Error creating ethers signer:", error); - throw error; - } -}; - -/** - * Generates a random keypair using ethers v6 - * - * @returns A Promise resolving to a CryptoKeypair object - */ -export const generateKeyPair = async (): Promise => { - try { - // Create random wallet - const wallet = ethers.Wallet.createRandom(); - - // Hash the private key with Keccak256 for additional security - const privateKeyHash = ethers.keccak256(wallet.privateKey as string); - - // Derive public key from hashed private key to create a new wallet - const walletFromHashedKey = new ethers.Wallet(privateKeyHash); - - return { - privateKey: privateKeyHash as Hex, - address: ethers.getAddress(walletFromHashedKey.address) as Hex, - }; - } catch (error) { - console.error("Error generating keypair, using fallback:", error); - // Fallback implementation - const randomHex = ethers.randomBytes(32); - const privateKey = ethers.keccak256(randomHex); - const wallet = new ethers.Wallet(privateKey); - - return { - privateKey: privateKey as Hex, - address: ethers.getAddress(wallet.address) as Hex, - }; - } -}; diff --git a/examples/snake/client/src/main.ts b/examples/snake/client/src/main.ts deleted file mode 100644 index 2425c0f74..000000000 --- a/examples/snake/client/src/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createApp } from 'vue' -import './style.css' -import App from './App.vue' - -createApp(App).mount('#app') diff --git a/examples/snake/client/src/services/ClearNetService.ts b/examples/snake/client/src/services/ClearNetService.ts deleted file mode 100644 index aad2b22d0..000000000 --- a/examples/snake/client/src/services/ClearNetService.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { - ChannelUpdate, - createGetLedgerBalancesMessage, - createPingMessage, - parseAnyRPCResponse, - RPCMethod, -} from "@erc7824/nitrolite"; -import { BROKER_WS_URL, CHAIN_ID } from "../config"; -import { createEthersSigner, generateKeyPair } from "../crypto"; -import type { Account, Transport, Chain, Hex, ParseAccount, WalletClient } from "viem"; -import { authenticate } from "./authentication"; - -class ClearNetService { - walletClient: WalletClient> | null = null; - stateWalletClient: WalletClient> | null = null; - - private isConnected = false; - private currentAddress: string | null = null; - private activeChannel: Hex | null = null; - private wsConnection: WebSocket | null = null; - private readonly wsUrl = BROKER_WS_URL; - private reconnectAttempts = 0; - private maxReconnectAttempts = 5; - private reconnectDelay = 1000; - private reconnectTimeout: number | null = null; - private authenticationInProgress: Promise | null = null; - - async initialize( - walletClient: WalletClient>, - stateWalletClient: WalletClient> - ): Promise { - try { - // Check for required config properties - if (!walletClient) { - throw new Error("walletClient is required in config"); - } - - if (!walletClient.account || !walletClient.account.address) { - throw new Error("walletClient.account.address is required"); - } - - console.log("Initializing with wallet address:", walletClient.account.address); - - // Initialize the Nitrolite client - this.currentAddress = walletClient.account.address; - this.walletClient = walletClient; - this.stateWalletClient = stateWalletClient; - console.log("Current wallet client address:", this.currentAddress); - - // Initialize WebSocket connection to ClearNet - console.log("Initializing WebSocket connection..."); - await this.initializeWebSocket(); - - this.isConnected = true; - console.log("ClearNet client initialized successfully"); - return true; - } catch (error) { - console.error("Failed to initialize ClearNet client:", error); - throw error; // Throw the error instead of returning false - } - } - - private initializeWebSocket(): Promise { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - - return new Promise((resolve, reject) => { - try { - if (this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN) { - return resolve(); - } - - // Check if wallet address is available - if (!this.currentAddress) { - console.error("Cannot initialize WebSocket: No wallet address available"); - return reject(new Error("No wallet address available")); - } - - // Check ethereum provider availability - const { ethereum } = window as any; - if (!ethereum) { - console.error("Cannot initialize WebSocket: No ethereum provider found"); - return reject(new Error("No ethereum provider found")); - } - - console.log("Creating WebSocket connection to:", this.wsUrl); - this.wsConnection = new WebSocket(this.wsUrl); - - let connectTimeout = setTimeout(() => { - console.error("WebSocket connection timeout"); - reject(new Error("WebSocket connection timeout")); - }, 10000); - - this.wsConnection.onopen = async () => { - clearTimeout(connectTimeout); - console.log("WebSocket connection established"); - - try { - // Log wallet client details for debugging - console.log("Wallet client account:", this.walletClient?.account); - console.log("Current address:", this.currentAddress); - - // Authenticate with the broker - await this.authenticateWithBroker(); - this.isConnected = true; - this.reconnectAttempts = 0; - resolve(); - } catch (error) { - console.error("Authentication failed:", error); - this.wsConnection?.close(); - reject(error); - } - }; - - this.wsConnection.onerror = (error) => { - console.error("WebSocket connection error:", error); - reject(error); - }; - - this.wsConnection.onclose = () => { - console.log("WebSocket connection closed"); - this.isConnected = false; - this.handleReconnect(); - }; - - this.wsConnection.onmessage = (event) => { - try { - this.handleWebSocketMessage(event.data.toString()); - } catch (error) { - console.error("Error parsing WebSocket message:", error); - } - }; - } catch (error) { - console.error("Error initializing WebSocket:", error); - reject(error); - } - }); - } - - private handleReconnect(): void { - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.log("Max reconnect attempts reached"); - return; - } - - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - } - - this.reconnectAttempts++; - const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1); - - console.log(`Reconnecting in ${delay}ms...`); - - this.reconnectTimeout = setTimeout(() => { - this.initializeWebSocket().catch(() => { - console.log("Reconnect attempt failed"); - }); - }, delay) as unknown as number; - } - - async getOrCreateKeyPair() { - const KEY_PAIR_KEY = "crypto_keypair"; - const savedKeys = localStorage.getItem(KEY_PAIR_KEY); - - let keyPair = null; - if (savedKeys) { - try { - keyPair = JSON.parse(savedKeys); - } catch (error) { - keyPair = null; - } - } - if (!keyPair) { - keyPair = await generateKeyPair(); - if (typeof window !== "undefined") { - localStorage.setItem(KEY_PAIR_KEY, JSON.stringify(keyPair)); - } - } - - return keyPair; - } - - private async authenticateWithBroker(): Promise { - // If authentication is already in progress, return the existing promise - if (this.authenticationInProgress) { - console.log("Authentication already in progress, reusing existing authentication flow"); - return this.authenticationInProgress; - } - - if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) { - throw new Error("WebSocket not connected"); - } - - // Verify we have wallet client for authentication - const eip712SignerWalletClient = this.walletClient; - if (!eip712SignerWalletClient) { - throw new Error('No main wallet client (e.g., MetaMask) available for EIP-712 authentication'); - } - - const keyPair = await this.getOrCreateKeyPair(); - const signer = createEthersSigner(keyPair.privateKey); - - // Create and store the authentication promise - this.authenticationInProgress = authenticate(this.wsConnection, eip712SignerWalletClient, signer, 15000) - .then(async () => { - console.log("Authentication successful, sending get_balances"); - - // Send get_balances message after successful authentication - // TODO: channel ID should not be stored in local storage - const nitroChannelId = localStorage.getItem("nitro_channel_id"); - if (nitroChannelId && this.wsConnection) { - const getBalancesMsg = await createGetLedgerBalancesMessage( - signer.sign, - nitroChannelId as Hex - ); - this.wsConnection.send(getBalancesMsg); - } - }) - .finally(() => { - this.authenticationInProgress = null; - }); - - return this.authenticationInProgress; - } - - private async handleWebSocketMessage(raw: string): Promise { - console.log("Received WebSocket message:", raw); - - const message = parseAnyRPCResponse(raw); - console.log("Parsed message:", message); - - if (message.method === RPCMethod.GetChannels || message.method === RPCMethod.ChannelsUpdate) { - let channels: ChannelUpdate[]; - if (message.method === RPCMethod.GetChannels) { - channels = message.params; - } else { - channels = [message.params]; - } - - console.log('[ClearNetService] Received channels update:', message); - const channel = channels.find((ch: any) => { - return ch.chain_id === CHAIN_ID && ch.status === "open"; - }); - console.log('[ClearNetService] Received new active channel:', channel); - if (channel) { - this.activeChannel = channel.channelId; - console.log('[ClearNetService] Active channel updated:', this.activeChannel); - } - } - if (message.method === RPCMethod.Ping) { - const keyPair = await this.getOrCreateKeyPair(); - const signer = createEthersSigner(keyPair.privateKey); - const message = await createPingMessage(signer.sign); - this.wsConnection?.send(message); - } - if (message.method === RPCMethod.Error) { - console.error("WebSocket error message:", message.params.error); - } - } - - async signState(stateData: any, stateId: string, channelId: string) { - if (!this.isConnected) { - console.error("ClearNet client not initialized"); - return null; - } - - try { - // We need to properly format the state according to Nitrolite SDK specs - // The state must include the channelId, version, and any allocations - const state = { - channelId, - stateData: JSON.stringify(stateData), - version: BigInt(Math.floor(Date.now() / 1000)), - stateId, - }; - - // Use the state wallet client if available, otherwise fall back to regular wallet client - // This creates a cryptographic signature that proves this state update was authorized - const stateHash = await this.getStateHash(state); - - // Choose which wallet client to use for signing - const signingClient = this.stateWalletClient || this.walletClient; - if (!signingClient) { - throw new Error("No signing client available"); - } - // raw ECDSA signature, where packed state is the `message`, that is hashed and signed - const signature = await signingClient.signMessage({ - message: { raw: stateHash }, - }); - - return { - signature, - stateId, - channelId, - playerId: this.currentAddress, - }; - } catch (error) { - console.error("Failed to sign state:", error); - return null; - } - } - - // Helper method to hash a state with the Nitrolite protocol standard - private async getStateHash(state: any): Promise { - // Format the state as required by the ERC-7824 specification - const stateString = JSON.stringify(state); - - try { - // Add the nitro protocol prefix for state hashing - const prefixedState = `nitro-state:${stateString}`; - - // Convert to Uint8Array for hashing - const encoder = new TextEncoder(); - const data = encoder.encode(prefixedState); - - // Use the browser's crypto API to create the state hash - // This follows the ERC-7824 state hashing specification - const hashBuffer = await window.crypto.subtle.digest("SHA-256", data); - - // Convert hash to hex string - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); - - // Add the 0x prefix for Ethereum compatibility - return "0x" + hashHex as Hex; - } catch (error) { - console.error("Failed to hash state:", error); - // Return a mock hash if there's an error - return "0x" + Array(64).fill("0").join("") as Hex; - } - } - - async getActiveChannel(): Promise { - // Wait until broker pushes the active channel to the client - let attempts = 0; - const timeout = 100; - const maxAttempts = 2000 / timeout; - while (!this.activeChannel) { - await new Promise(resolve => setTimeout(resolve, timeout)); - attempts++; - if (this.isConnected && attempts > maxAttempts) { - throw new Error('No active channel found. Please open a channel at apps.yellow.com'); - } - } - console.log('[ClearNetService] Active channel:', this.activeChannel); - return this.activeChannel; - } -} - -export const clearNetService = new ClearNetService(); -export default clearNetService; diff --git a/examples/snake/client/src/services/GameService.ts b/examples/snake/client/src/services/GameService.ts deleted file mode 100644 index b5a248d26..000000000 --- a/examples/snake/client/src/services/GameService.ts +++ /dev/null @@ -1,630 +0,0 @@ -import { ref } from 'vue'; -import type { Ref } from 'vue'; -import clearNetService from './ClearNetService'; -import { GAMESERVER_WS_URL } from '../config'; -import { createEthersSigner } from '../crypto'; - -export interface GameState { - type: string; - players: Array<{ - id: string; - nickname: string; - segments: Array<{ x: number; y: number }>; - score: number; - isDead: boolean; - }>; - food: { x: number; y: number }; - gridSize: { width: number; height: number }; - isGameOver: boolean; - stateVersion: number; - timestamp: number; -} - -export interface Room { - id: string; - name: string; - players: Array<{ - id: string; - nickname: string; - }>; - maxPlayers: number; - isGameActive: boolean; - createdAt: number; -} - -class GameService { - private ws: WebSocket | null = null; - private isConnected: Ref = ref(false); - private playerId: Ref = ref(''); - private roomId: Ref = ref(''); - private errorMessage: Ref = ref(''); - private gameState: Ref = ref(null); - private availableRooms: Ref = ref([]); - private signatureStatus: Ref = ref(''); // For UI feedback - private isSigningAppSession: Ref = ref(false); - private messageHandlers: Map void> = new Map(); - private connectionPromise: Promise | null = null; - private reconnectAttempts = 0; - private maxReconnectAttempts = 3; - private isConnecting = false; - private reconnectTimeout: number | null = null; - private isCreatingRoom = false; // Add flag to track room creation state - - constructor() { - this.setupMessageHandlers(); - // Initialize connection on service creation - this.connect(); - } - - private setupMessageHandlers() { - this.messageHandlers.set('roomCreated', (data) => { - console.log('[GameService] Room created:', data); - this.roomId.value = data.roomId; - this.playerId.value = data.playerId; - }); - - this.messageHandlers.set('roomJoined', (data) => { - console.log('[GameService] Room joined:', data); - this.roomId.value = data.roomId; - this.playerId.value = data.playerId; - }); - - this.messageHandlers.set('error', (data) => { - console.log('[GameService] Error received:', data); - this.errorMessage.value = data.message; - }); - - this.messageHandlers.set('gameState', (data) => { - this.gameState.value = data; - }); - - this.messageHandlers.set('signState', (data) => { - this.handleStateSignRequest(data.channelId, data.state, data.stateId); - }); - - this.messageHandlers.set('channelFinalized', (data) => { - console.log(`[GameService] Channel ${data.channelId} has been finalized`); - }); - - this.messageHandlers.set('roomsList', (data) => { - console.log('[GameService] Rooms list received:', data); - this.availableRooms.value = data.rooms || []; - }); - - this.messageHandlers.set('roomUpdated', (data) => { - console.log('[GameService] Room updated:', data); - // Update the specific room in the list - const roomIndex = this.availableRooms.value.findIndex(room => room.id === data.room.id); - if (roomIndex !== -1) { - this.availableRooms.value[roomIndex] = data.room; - } else { - // If room doesn't exist, add it - this.availableRooms.value.push(data.room); - } - }); - - this.messageHandlers.set('roomRemoved', (data) => { - console.log('[GameService] Room removed:', data); - this.availableRooms.value = this.availableRooms.value.filter(room => room.id !== data.roomId); - }); - - this.messageHandlers.set('playAgainVoteUpdate', (data) => { - console.log('[GameService] Play again vote update:', data); - // Handle play again vote updates if needed - }); - - this.messageHandlers.set('playerDisconnected', (data) => { - console.log('[GameService] Player disconnected:', data); - // Handle player disconnect notifications if needed - }); - - // App session signature collection handlers - this.messageHandlers.set('appSession:signatureRequest', (data) => { - console.log('[GameService] App session signature request:', data); - this.handleAppSessionSignatureRequest(data); - }); - - this.messageHandlers.set('appSession:startGameRequest', (data) => { - console.log('[GameService] App session start game request:', data); - this.handleAppSessionStartGameRequest(data); - }); - - this.messageHandlers.set('appSession:signatureConfirmed', (data) => { - console.log('[GameService] App session signature confirmed:', data); - this.signatureStatus.value = 'Waiting for host to start game...'; - }); - - this.messageHandlers.set('appSession:gameStarted', (data) => { - this.handleGameStarted(data); - }); - - this.messageHandlers.set('appSession:closeRequest', (data) => { - this.handleCloseSessionSignatureRequest(data); - }); - - this.messageHandlers.set('appSession:closed', () => { - console.log('[GameService] App session closed, clearing room state'); - this.clearRoomState(); - }); - } - - connect() { - console.log('[GameService] connect() called, current state:', { - wsState: this.ws?.readyState, - isConnecting: this.isConnecting, - hasConnectionPromise: !!this.connectionPromise, - reconnectAttempts: this.reconnectAttempts, - roomId: this.roomId.value - }); - - if (this.ws?.readyState === WebSocket.OPEN) { - console.log('[GameService] WebSocket already connected, returning'); - return Promise.resolve(); - } - - if (this.connectionPromise) { - console.log('[GameService] Connection attempt already in progress, returning existing promise'); - return this.connectionPromise; - } - - if (this.isConnecting) { - console.log('[GameService] Already connecting, returning'); - return Promise.resolve(); - } - - this.isConnecting = true; - this.connectionPromise = new Promise((resolve, reject) => { - try { - console.log('[GameService] Creating new WebSocket connection'); - this.ws = new WebSocket(GAMESERVER_WS_URL); - - this.ws.onopen = () => { - this.isConnected.value = true; - this.reconnectAttempts = 0; - this.isConnecting = false; - console.log('[GameService] WebSocket connected successfully'); - resolve(); - }; - - this.ws.onclose = (event) => { - console.log('[GameService] WebSocket closed:', { - wasClean: event.wasClean, - code: event.code, - reason: event.reason, - currentState: { - isConnecting: this.isConnecting, - reconnectAttempts: this.reconnectAttempts, - hasConnectionPromise: !!this.connectionPromise, - roomId: this.roomId.value - } - }); - - this.isConnecting = false; - if (!event.wasClean) { - this.handleDisconnection(); - } else { - this.isConnected.value = false; - this.connectionPromise = null; - // Only schedule reconnect if this wasn't an intentional disconnect - // and we don't have an active room - if (event.reason !== 'Client disconnected' && !this.roomId.value) { - this.scheduleReconnect(); - } - } - }; - - this.ws.onerror = (error) => { - this.isConnecting = false; - console.error('WebSocket error:', error); - }; - - this.ws.onmessage = (event) => { - try { - console.log('[GameService] Received WebSocket message:', event.data); - const data = JSON.parse(event.data); - const handler = this.messageHandlers.get(data.type); - if (handler) { - handler(data); - } else { - console.log('[GameService] No handler found for message type:', data.type); - } - } catch (error) { - console.error('[GameService] Error parsing message:', error); - } - }; - } catch (error) { - this.isConnecting = false; - this.connectionPromise = null; - reject(error); - } - }); - - return this.connectionPromise; - } - - private scheduleReconnect() { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - } - - this.reconnectTimeout = setTimeout(() => { - if (!this.isConnected.value && !this.isConnecting) { - console.log('Attempting to reconnect...'); - this.connect(); - } - }, 1000) as unknown as number; - } - - private handleDisconnection() { - this.isConnected.value = false; - this.connectionPromise = null; - this.isConnecting = false; - - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts++; - console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); - this.scheduleReconnect(); - } else { - this.errorMessage.value = 'Connection lost. Please refresh the page.'; - } - } - - disconnect() { - console.log('[GameService] disconnect() called, current state:', { - wsState: this.ws?.readyState, - isConnecting: this.isConnecting, - hasConnectionPromise: !!this.connectionPromise - }); - - if (this.ws) { - this.ws.close(1000, 'Client disconnected'); - this.ws = null; - this.connectionPromise = null; - this.reconnectAttempts = 0; - this.isConnecting = false; - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - console.log('[GameService] WebSocket disconnected and cleaned up'); - } - } - - private async ensureConnected() { - if (!this.isConnected.value) { - await this.connect(); - } - } - - async createRoom(nickname: string, channelId: string, walletAddress: string) { - try { - // Check if room creation is already in progress - if (this.isCreatingRoom) { - console.log('[GameService] Room creation already in progress, ignoring duplicate request'); - return; - } - - this.isCreatingRoom = true; - await this.ensureConnected(); - - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to server'); - } - - this.ws.send(JSON.stringify({ - type: 'createRoom', - nickname, - channelId, - walletAddress, - })); - } catch (error) { - console.error('Error creating room:', error); - this.errorMessage.value = 'Failed to create room. Please try again.'; - throw error; - } finally { - // Reset the flag after a short delay to prevent race conditions - setTimeout(() => { - this.isCreatingRoom = false; - }, 1000); - } - } - - async joinRoom(roomId: string, nickname: string, channelId: string, walletAddress: string) { - try { - await this.ensureConnected(); - - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to server'); - } - - this.ws.send(JSON.stringify({ - type: 'joinRoom', - roomId, - nickname, - channelId, - walletAddress, - })); - } catch (error) { - console.error('Error joining room:', error); - this.errorMessage.value = 'Failed to join room. Please try again.'; - throw error; - } - } - - changeDirection(direction: 'up' | 'down' | 'left' | 'right') { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - return; - } - - this.ws.send(JSON.stringify({ - type: 'changeDirection', - direction - })); - } - - playAgain() { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - return; - } - - this.ws.send(JSON.stringify({ - type: 'playAgain', - roomId: this.roomId.value - })); - } - - finalizeGame() { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - console.error('[GameService] Cannot finalize game - WebSocket not connected'); - return; - } - - console.log('[GameService] Sending finalizeGame message with data:', { - roomId: this.roomId.value, - playerId: this.playerId.value, - walletAddress: window.ethereum.selectedAddress - }); - - this.ws.send(JSON.stringify({ - type: 'finalizeGame', - roomId: this.roomId.value, - playerId: this.playerId.value, - walletAddress: window.ethereum.selectedAddress - })); - } - - clearRoomState() { - console.log('[GameService] Clearing room state'); - this.roomId.value = ''; - this.playerId.value = ''; - this.gameState.value = null; - this.errorMessage.value = ''; - this.isSigningAppSession.value = false; - this.signatureStatus.value = ''; - } - - async subscribeToRooms() { - try { - await this.ensureConnected(); - - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to server'); - } - - console.log('[GameService] Subscribing to rooms updates'); - this.ws.send(JSON.stringify({ - type: 'subscribeRooms' - })); - } catch (error) { - console.error('Error subscribing to rooms:', error); - } - } - - async unsubscribeFromRooms() { - try { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - return; // Connection is already closed - } - - console.log('[GameService] Unsubscribing from rooms updates'); - this.ws.send(JSON.stringify({ - type: 'unsubscribeRooms' - })); - } catch (error) { - console.error('Error unsubscribing from rooms:', error); - } - } - - async joinRoomById(roomId: string, nickname: string, channelId: string, walletAddress: string) { - try { - await this.ensureConnected(); - - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error('Not connected to server'); - } - - this.ws.send(JSON.stringify({ - type: 'joinRoom', - roomId, - nickname, - channelId, - walletAddress, - })); - } catch (error) { - console.error('Error joining room:', error); - this.errorMessage.value = 'Failed to join room. Please try again.'; - throw error; - } - } - - private async handleStateSignRequest(channelId: string, state: any, stateId: string) { - try { - const signatureData = await clearNetService.signState(state, stateId, channelId); - - if (signatureData && this.ws?.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify({ - type: 'stateSignature', - channelId: signatureData.channelId, - stateId: signatureData.stateId, - signature: signatureData.signature, - playerId: signatureData.playerId - })); - } - } catch (error) { - console.error('Error signing state:', error); - } - } - - private async handleAppSessionSignatureRequest(data: any) { - try { - const { roomId, requestToSign, participantAddress } = data; - - console.log('[GameService] Signing app session request for room:', roomId); - console.log('[GameService] Request to sign:', requestToSign); - - this.isSigningAppSession.value = true; - this.signatureStatus.value = 'Please sign the app session creation request...'; - - const signature = await this.signAppSessionRequest(requestToSign); - this.signatureStatus.value = 'Signature submitted, processing...'; - - if (this.ws?.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify({ - type: 'appSession:signature', - roomId, - signature, - participantAddress - })); - console.log('[GameService] Sent app session signature for room:', roomId); - } - } catch (error) { - console.error('[GameService] Error handling app session signature request:', error); - this.isSigningAppSession.value = false; - this.signatureStatus.value = ''; - this.errorMessage.value = 'Failed to sign app session request. Please try again.'; - } - } - - private async handleAppSessionStartGameRequest(data: any) { - try { - const { roomId, requestToSign, participantAddress } = data; - - console.log('[GameService] Signing start game request for room:', roomId); - console.log('[GameService] Request to sign:', requestToSign); - - this.isSigningAppSession.value = true; - this.signatureStatus.value = 'Please sign to start the game...'; - - const signature = await this.signAppSessionRequest(requestToSign); - this.signatureStatus.value = 'Starting game...'; - - if (this.ws?.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify({ - type: 'appSession:startGame', - roomId, - signature, - participantAddress - })); - console.log('[GameService] Sent start game signature for room:', roomId); - } - } catch (error) { - console.error('[GameService] Error handling start game request:', error); - this.isSigningAppSession.value = false; - this.signatureStatus.value = ''; - this.errorMessage.value = 'Failed to sign start game reqest. Please try again.'; - } - } - - private async signAppSessionRequest(requestToSign: any): Promise { - try { - const messageString = JSON.stringify(requestToSign); - console.log('[GameService] Signing request:', messageString); - const keyPair = await clearNetService.getOrCreateKeyPair(); - const signer = createEthersSigner(keyPair.privateKey); - console.log('[GameService] Successfully signed with session key'); - return signer.sign(requestToSign); - } catch (error) { - console.error('[GameService] Error signing app session request:', error); - throw error; - } - } - - private async handleCloseSessionSignatureRequest(data: any) { - try { - const { roomId, requestToSign, participantAddress } = data; - - console.log('[GameService] Signing app session close request for room:', roomId); - console.log('[GameService] Request to sign:', requestToSign); - - this.isSigningAppSession.value = true; - this.signatureStatus.value = 'Please sign to close the game session...'; - - const signature = await this.signAppSessionRequest(requestToSign); - this.signatureStatus.value = 'Signature submitted, closing session...'; - - if (this.ws?.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify({ - type: 'closeSessionSignature', - roomId, - playerId: this.playerId.value, - signature, - participantAddress - })); - console.log('[GameService] Sent app session close signature for room:', roomId); - } - } catch (error) { - console.error('[GameService] Error handling app session close request:', error); - this.signatureStatus.value = ''; - this.errorMessage.value = 'Failed to sign app session close request. Please try again.'; - } - this.isSigningAppSession.value = false; - } - - private handleGameStarted(data: any) { - console.log('[GameService] App session game started:', data); - this.isSigningAppSession.value = false; - this.signatureStatus.value = ''; - } - - // Getters for reactive state - getIsConnected(): Ref { - return this.isConnected; - } - - getPlayerId(): Ref { - return this.playerId; - } - - getRoomId(): Ref { - return this.roomId; - } - - getErrorMessage(): Ref { - return this.errorMessage; - } - - getGameState(): Ref { - return this.gameState; - } - - getAvailableRooms(): Ref { - return this.availableRooms; - } - - getSignatureStatus(): Ref { - return this.signatureStatus; - } - - getIsSigningAppSession(): Ref { - return this.isSigningAppSession; - } - - // Get the WebSocket instance - getWebSocket(): WebSocket | null { - return this.ws; - } -} - -// Create a singleton instance -const gameService = new GameService(); -export default gameService; diff --git a/examples/snake/client/src/services/authentication.ts b/examples/snake/client/src/services/authentication.ts deleted file mode 100644 index 10bbdce74..000000000 --- a/examples/snake/client/src/services/authentication.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { AuthRequestParams, createAuthRequestMessage, createAuthVerifyMessage, createAuthVerifyMessageWithJWT, createEIP712AuthMessageSigner, parseAnyRPCResponse, RPCMethod } from '@erc7824/nitrolite'; -import type { WalletSigner } from '../crypto'; -import type { Hex } from 'viem'; -import { getAddress } from 'viem'; - -/** - * EIP-712 domain and types for auth_verify challenge - */ -const getAuthDomain = () => { - return { - name: 'Snake Game', - }; -}; - -const expire = String(Math.floor(Date.now() / 1000) + 24 * 60 * 60); - -/** - * Authenticates with the WebSocket server using: - * 1. auth_request: empty signature with wallet address - * 2. auth_verify: EIP-712 signature for challenge verification (with proper challenge extraction) - * - * @param ws - The WebSocket connection - * @param walletClient - The wallet client for signing - * @param signer - The local keys signer (session key) - * @param timeout - Timeout in milliseconds for the entire process - * @returns A Promise that resolves when authenticated - */ -export async function authenticate( - ws: WebSocket, - walletClient: any, - signer: WalletSigner, - timeout: number = 15000 -): Promise { - if (!ws) throw new Error('WebSocket not connected'); - - if (!walletClient) { - throw new Error('No wallet client available for authentication'); - } - - const rawWalletAddress = walletClient.account?.address; - - if (!rawWalletAddress) { - throw new Error('No wallet address available for authentication'); - } - - // Ensure the address is properly checksummed for EIP-55 compliance - const walletAddress = getAddress(rawWalletAddress); - - console.log('Starting authentication with:'); - console.log('- Wallet address:', walletAddress); - console.log('- Session key:', signer.address); - console.log('- Empty signature for auth_request'); - console.log('- EIP-712 signature for auth_verify challenge (UUID only)'); - - const authMessage: AuthRequestParams = { - wallet: walletAddress as Hex, - participant: signer.address as Hex, - app_name: 'Snake Game', - expire: expire, // 24 hours in seconds - scope: 'snake-game', - application: walletAddress as Hex, - allowances: [ - { - asset: 'usdc', - amount: '0', - }, - ], - }; - - // Step 1: Send auth_request with empty signature and wallet address - try { - let authRequest: string; - let usingJWT = false; - const jwtToken = window.localStorage.getItem('jwt_token'); - - if (jwtToken) { - console.log('JWT token found, attempting JWT authentication:', jwtToken); - try { - authRequest = await createAuthVerifyMessageWithJWT(jwtToken); - usingJWT = true; - } catch (jwtError) { - console.warn('JWT auth failed, falling back to signer authentication:', jwtError); - // Remove invalid JWT token - window.localStorage.removeItem('jwt_token'); - authRequest = await createAuthRequestMessage(authMessage); - usingJWT = false; - } - } else { - console.log('No JWT token found, proceeding with challenge-response authentication'); - authRequest = await createAuthRequestMessage(authMessage); - usingJWT = false; - } - - console.log(`Sending auth_request (${usingJWT ? 'JWT' : 'challenge-response'}):`, authRequest); - ws.send(authRequest); - } catch (requestError) { - console.error('Error creating auth_request:', requestError); - throw new Error(`Failed to create auth_request: ${(requestError as Error).message}`); - } - - return new Promise((resolve, reject) => { - if (!ws) return reject(new Error('WebSocket not connected')); - - let authTimeoutId: number | null = null; - - const cleanup = () => { - if (authTimeoutId) { - clearTimeout(authTimeoutId); - authTimeoutId = null; - } - ws.removeEventListener('message', handleAuthResponse); - }; - - const resetTimeout = () => { - if (authTimeoutId) { - clearTimeout(authTimeoutId); - } - authTimeoutId = setTimeout(() => { - cleanup(); - reject(new Error('Authentication timeout')); - }, timeout) as unknown as number; - }; - - const handleAuthResponse = async (event: MessageEvent) => { - let response; - try { - response = parseAnyRPCResponse(event.data); - console.log('Received auth message:', response); - } catch (error) { - console.error('Error parsing auth response:', error); - console.log('Raw auth message:', event.data); - return; - } - - try { - // Check for challenge response: {"res": [id, "auth_challenge", {"challenge": "uuid"}, timestamp]} - if (response.method === RPCMethod.AuthChallenge) { - console.log('Received auth_challenge, preparing EIP-712 auth_verify...'); - resetTimeout(); // Reset timeout while we process and send verify - - try { - // Step 2: Create EIP-712 signing function for challenge verification - if (!walletClient) { - throw new Error('No wallet client available for EIP-712 signing'); - } - - console.log('Creating EIP-712 signing function...'); - const eip712SigningFunction = createEIP712AuthMessageSigner(walletClient, { - scope: authMessage.scope, - application: authMessage.application, - participant: authMessage.participant, - expire: authMessage.expire, - allowances: authMessage.allowances.map(({ asset, amount }: { asset: string, amount: any }) => ({ - asset, - amount: amount.toString(), - })), - }, getAuthDomain()); - - console.log('Calling createAuthVerifyMessage...'); - // Create and send verification message with EIP-712 signature - const authVerify = await createAuthVerifyMessage(eip712SigningFunction, response); - - console.log('Sending auth_verify with EIP-712 signature'); - ws.send(authVerify); - console.log('auth_verify sent successfully'); - } catch (eip712Error) { - console.error('Error creating EIP-712 auth_verify:', eip712Error); - console.error('Error stack:', (eip712Error as Error).stack); - - cleanup(); - reject(new Error(`EIP-712 auth_verify failed: ${(eip712Error as Error).message}`)); - return; - } - } - // Check for success response - else if (response.method === RPCMethod.AuthVerify) { - console.log('Authentication successful'); - - // If response contains a JWT token, store it - if (response.params.jwtToken) { - console.log('JWT token received:', response.params.jwtToken); - window.localStorage.setItem('jwt_token', response.params.jwtToken); - } - - cleanup(); - resolve(); - } - // Check for error response - else if (response.method === RPCMethod.Error) { - const errorMsg = - response.params.error || 'Authentication failed'; - - console.error('Authentication failed:', errorMsg); - - // Check if this is a JWT authentication failure and fallback to signer auth - const errorString = String(errorMsg).toLowerCase(); - if (errorString.includes('jwt') || errorString.includes('token') || errorString.includes('invalid') || errorString.includes('expired')) { - console.warn('JWT authentication failed on server, attempting fallback to signer authentication'); - window.localStorage.removeItem('jwt_token'); - - try { - // Restart authentication with signer - const fallbackAuthRequest = await createAuthRequestMessage(authMessage); - - console.log('Sending fallback auth_request with signer:', fallbackAuthRequest); - ws.send(fallbackAuthRequest); - resetTimeout(); // Reset timeout for the fallback attempt - return; // Continue listening for the fallback response - } catch (fallbackError) { - console.error('Fallback to signer authentication failed:', fallbackError); - cleanup(); - reject(new Error(`Both JWT and signer authentication failed: ${fallbackError}`)); - return; - } - } - - window.localStorage.removeItem('jwt_token'); - cleanup(); - reject(new Error(String(errorMsg))); - } else { - console.log('Received non-auth message during auth, continuing to listen:', response); - // Keep listening if it wasn't a final success/error - } - } catch (error) { - console.error('Error handling auth response:', error); - console.error('Error stack:', (error as Error).stack); - cleanup(); - reject(new Error(`Authentication error: ${error instanceof Error ? error.message : String(error)}`)); - } - }; - - ws.addEventListener('message', handleAuthResponse); - resetTimeout(); // Start the initial timeout - }); -} diff --git a/examples/snake/client/src/style.css b/examples/snake/client/src/style.css deleted file mode 100644 index f4bef1904..000000000 --- a/examples/snake/client/src/style.css +++ /dev/null @@ -1,34 +0,0 @@ -:root { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: #213547; - background-color: #ffffff; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - margin: 0; - display: flex; - min-width: 320px; - min-height: 100vh; -} - -#app { - width: 100%; - margin: 0 auto; - padding: 2rem; -} - -@media (prefers-color-scheme: dark) { - :root { - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - } -} \ No newline at end of file diff --git a/examples/snake/client/src/vite-env.d.ts b/examples/snake/client/src/vite-env.d.ts deleted file mode 100644 index 8356e59b9..000000000 --- a/examples/snake/client/src/vite-env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -interface Window { - __ENV__?: { - [key: string]: string; - }; -} diff --git a/examples/snake/client/tsconfig.json b/examples/snake/client/tsconfig.json deleted file mode 100644 index 975090264..000000000 --- a/examples/snake/client/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - "types": ["vite/client"], - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "preserve", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts", "vite.config.ts"] -} \ No newline at end of file diff --git a/examples/snake/client/vite.config.ts b/examples/snake/client/vite.config.ts deleted file mode 100644 index da819e606..000000000 --- a/examples/snake/client/vite.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [vue()], - server: { - allowedHosts: true, - proxy: { - '/api': { - target: 'http://localhost:3001', - changeOrigin: true, - }, - }, - }, -}) diff --git a/examples/snake/server/config/index.ts b/examples/snake/server/config/index.ts deleted file mode 100644 index 9b571a175..000000000 --- a/examples/snake/server/config/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Hex } from "viem"; - -export const BROKER_WS_URL = process.env.BROKER_WS_URL as string; -export const SERVER_PRIVATE_KEY = process.env.SERVER_PRIVATE_KEY as Hex; -export const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY as Hex; -export const CHAIN_ID = parseInt(process.env.CHAIN_ID || "137"); diff --git a/examples/snake/server/interfaces/index.ts b/examples/snake/server/interfaces/index.ts deleted file mode 100644 index 8359733cc..000000000 --- a/examples/snake/server/interfaces/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Hex } from 'viem'; -import { WebSocket } from 'ws'; - -export interface SnakeWebSocket extends WebSocket { - playerId: string; - roomId?: string; - channelId?: string; -} - -export interface Player { - id: string; - nickname: string; - position: { x: number; y: number }; - direction: 'up' | 'down' | 'left' | 'right'; - segments: Array<{ x: number; y: number }>; - score: number; - isDead?: boolean; -} - -export interface Room { - id: string; - players: Map; - food: { x: number; y: number }; - gameInterval: NodeJS.Timeout | null; - gridSize: { width: number; height: number }; - isGameOver?: boolean; - channelIds: Set; - appId?: Hex; - playerAddresses: Map; // Maps playerId to their ethereum address - currentState: any; - stateVersion: number; - createdAt: number; // Timestamp when the room was created - isClosingAppSession?: boolean; - playAgainVotes?: Set; // Set of player IDs who voted to play again - closeSessionRequest?: any; - closeSessionSignatures: Map; -} - -export interface PendingRequest { - resolve: Function; - reject: Function; - timeout: NodeJS.Timeout; -} - -export interface RPCRequest { - req: [number, string, any[], number]; // [requestId, method, params, timestamp] - sig: string[]; -} - -export interface RPCResponse { - res: [number, string, any[], number]; // [requestId, method, params, timestamp] - sig: string[]; -} - -export interface ChallengeData { - challenge_message: string; -} diff --git a/examples/snake/server/middlewares/authMiddleware.ts b/examples/snake/server/middlewares/authMiddleware.ts deleted file mode 100644 index 43bd5177a..000000000 --- a/examples/snake/server/middlewares/authMiddleware.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ethers } from 'ethers'; -import { verifySignature } from '../services/brokerService.ts'; - -// Simple challenge store - in production, this should use a proper database or Redis -const challenges = new Map(); - -// Generate a challenge for a client -export function generateChallenge(address: string): { challenge: string, timestamp: number } { - // Generate a random challenge - const timestamp = Date.now(); - const randomBytes = ethers.randomBytes(16); - const randomHex = ethers.hexlify(randomBytes); - const challenge = `Sign this message to authenticate with Nitro Snake: ${randomHex} at ${timestamp}`; - - // Store the challenge - challenges.set(address.toLowerCase(), { challenge, timestamp }); - - return { challenge, timestamp }; -} - -// Verify a client's signature against a stored challenge -export function verifyChallengeSignature(address: string, signature: string): boolean { - if (!ethers.isAddress(address)) { - return false; - } - - const storedChallenge = challenges.get(address.toLowerCase()); - if (!storedChallenge) { - return false; - } - - // Check if the challenge has expired (valid for 5 minutes) - const now = Date.now(); - if (now - storedChallenge.timestamp > 5 * 60 * 1000) { - challenges.delete(address.toLowerCase()); - return false; - } - - // Verify the signature - const isValid = verifySignature(storedChallenge.challenge, signature, address); - - // Remove the challenge after verification to prevent reuse - if (isValid) { - challenges.delete(address.toLowerCase()); - } - - return isValid; -} diff --git a/examples/snake/server/middlewares/index.ts b/examples/snake/server/middlewares/index.ts deleted file mode 100644 index 3778bf59c..000000000 --- a/examples/snake/server/middlewares/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './authMiddleware.ts'; diff --git a/examples/snake/server/nixpacks.toml b/examples/snake/server/nixpacks.toml deleted file mode 100644 index a3366a9c6..000000000 --- a/examples/snake/server/nixpacks.toml +++ /dev/null @@ -1,2 +0,0 @@ -[phases.install] -cmds = ['npm install'] diff --git a/examples/snake/server/package-lock.json b/examples/snake/server/package-lock.json deleted file mode 100644 index 4f7c7b65b..000000000 --- a/examples/snake/server/package-lock.json +++ /dev/null @@ -1,1880 +0,0 @@ -{ - "name": "nitro-snake-server", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nitro-snake-server", - "version": "0.0.0", - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "body-parser": "^2.2.0", - "dotenv": "^16.5.0", - "ethers": "6.7.1", - "express": "^5.1.0", - "viem": "2.29.3", - "ws": "^8.16.0" - }, - "devDependencies": { - "@types/body-parser": "^1.19.5", - "@types/express": "^5.0.2", - "@types/node": "^18.19.15", - "@types/ws": "^8.5.10", - "tsx": "^4.7.0", - "typescript": "~5.8.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@erc7824/nitrolite": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@erc7824/nitrolite/-/nitrolite-0.2.20.tgz", - "integrity": "sha512-iYzgHh6Acd3Pq1tjQ+yzEiZQh68xHZ4P+r5UMrweqBMU93Fr39pHEYe/PkdAGppREOHoXvuIDDp6S7ORTxfkug==", - "license": "MIT", - "dependencies": { - "abitype": "^0.10.3", - "viem": "^2.5.0", - "zod": "^3.25.67" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@noble/curves": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", - "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.2" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@scure/base": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", - "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.2.tgz", - "integrity": "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.19.103", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.103.tgz", - "integrity": "sha512-hHTHp+sEz6SxFsp+SA+Tqrua3AbmlAw+Y//aEwdHrdZkYVRWdvWD3y5uPZ0flYOkgskaFWqZ/YGFm3FaFQ0pRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/abitype": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.3.tgz", - "integrity": "sha512-tRN+7XIa7J9xugdbRzFv/95ka5ivR/sRe01eiWvM0HWWjHuigSZEACgKa0sj4wGuekTDtghCx+5Izk/cOi78pQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ethers": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", - "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.9.2", - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.7.1", - "@types/node": "18.15.13", - "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/@adraffy/ens-normalize": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", - "license": "MIT" - }, - "node_modules/ethers/node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/ethers/node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "license": "MIT" - }, - "node_modules/ethers/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.19.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", - "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/viem": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.29.3.tgz", - "integrity": "sha512-9D/nO+4S3Kk0P8vh6yXUA8OJ0mli9nzoY22qyh8iYHf1Q0GIejUnyq0QGjoDbkTcVzCVD5iA9KBkSxw9Tp3vUg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.2", - "@noble/hashes": "1.7.2", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.9", - "ws": "8.18.1" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/examples/snake/server/package.json b/examples/snake/server/package.json deleted file mode 100644 index 20e0e1e6e..000000000 --- a/examples/snake/server/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "nitro-snake-server", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "tsx server.ts", - "build": "tsc", - "start": "node dist/server.js" - }, - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "body-parser": "^2.2.0", - "dotenv": "^16.5.0", - "ethers": "6.7.1", - "express": "^5.1.0", - "viem": "2.29.3", - "ws": "^8.16.0" - }, - "devDependencies": { - "@types/body-parser": "^1.19.5", - "@types/express": "^5.0.2", - "@types/node": "^18.19.15", - "@types/ws": "^8.5.10", - "tsx": "^4.7.0", - "typescript": "~5.8.3" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/examples/snake/server/server.ts b/examples/snake/server/server.ts deleted file mode 100644 index 8f3a91f84..000000000 --- a/examples/snake/server/server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createServer } from 'http'; -import { WebSocketServer } from 'ws'; -import dotenv from 'dotenv'; -import express from 'express'; -import bodyParser from 'body-parser'; - -// Load environment variables from .env file -dotenv.config(); - -// Create Express app -const app = express(); -app.use(bodyParser.json()); - -// Create HTTP server -const server = createServer(app); - -// Create WebSocket server -const wss = new WebSocketServer({ server }); - -// Export server objects -export { server, wss }; - -// Import after exports to avoid circular dependencies -import { connectToBroker } from './services/brokerService.ts'; -import { setupWebSocketHandlers } from './services/websocketService.ts'; -import { gracefulShutdown } from './utils/shutdown.ts'; - -// Setup WebSocket handlers -setupWebSocketHandlers(wss); - -// Initialize broker connection -connectToBroker(); - -// Start server -const PORT = process.env.PORT || 3001; -server.listen(PORT, () => { - console.log(`WebSocket server is running on port ${PORT}`); -}); - -// Handle graceful shutdown -process.on('SIGTERM', () => gracefulShutdown(server, wss, 'SIGTERM')); -process.on('SIGINT', () => gracefulShutdown(server, wss, 'SIGINT')); diff --git a/examples/snake/server/services/appSessionService.ts b/examples/snake/server/services/appSessionService.ts deleted file mode 100644 index 5049c3da9..000000000 --- a/examples/snake/server/services/appSessionService.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { Hex } from 'viem'; -import { ethers } from 'ethers'; -import dotenv from 'dotenv'; -import { createAppSessionMessage, AppDefinition, MessageSigner, CreateAppSessionRequestParams } from '@erc7824/nitrolite'; -import { SERVER_PRIVATE_KEY } from '../config/index.ts'; -import { DEFAULT_PROTOCOL, sendToBroker } from './brokerService.ts'; - -// Load environment variables -dotenv.config(); - -// Types -interface AppSession { - appId: string; - participantA: Hex; - participantB: Hex; - serverAddress: Hex; - tokenAddress: string; - createdAt: number; -} - -interface PendingAppSession { - appSessionData: CreateAppSessionRequestParams[]; - appDefinition: AppDefinition; - participantA: Hex; - participantB: Hex; - serverAddress: Hex; - signatures: Map; - createdAt: number; - nonce: number; - requestToSign: any; - originalSignedMessage: string; -} - -// Maps to store app sessions and pending signatures -const roomAppSessions = new Map(); -const pendingAppSessions = new Map(); - -// Create server wallet for signing -export const serverWallet = new ethers.Wallet(SERVER_PRIVATE_KEY); - -// Create a compatible signer function -export const serverSigner: MessageSigner = async (payload: any): Promise => { - const message = typeof payload === 'string' ? payload : JSON.stringify(payload); - return await serverWallet.signMessage(message) as Hex; -}; - -/** - * Generate app session message for multi-signature collection - */ -export async function generateAppSessionMessage(roomId: string, participantA: Hex, participantB: Hex): Promise<{ - appSessionData: CreateAppSessionRequestParams[]; - appDefinition: AppDefinition; - participants: Hex[]; - requestToSign: any; -}> { - try { - // Format addresses to proper checksum format - const formattedParticipantA = ethers.getAddress(participantA) as Hex; - const formattedParticipantB = ethers.getAddress(participantB) as Hex; - - console.log(`Generating app session message for room ${roomId} with participants A: ${formattedParticipantA}, B: ${formattedParticipantB}`); - - // Check if we already have a pending session - let pendingSession = pendingAppSessions.get(roomId); - - if (pendingSession) { - console.log(`Using existing app session message for room ${roomId} - nonce: ${pendingSession.nonce}, requestToSign: ${JSON.stringify(pendingSession.requestToSign)}`); - return { - appSessionData: pendingSession.appSessionData, - appDefinition: pendingSession.appDefinition, - participants: [pendingSession.participantA, pendingSession.participantB, pendingSession.serverAddress], - requestToSign: pendingSession.requestToSign - }; - } - - // Get the server's address - const serverAddress = serverWallet.address as Hex; - - // Create app definition with fixed nonce - const nonce = Date.now(); - const appDefinition: AppDefinition = { - protocol: DEFAULT_PROTOCOL, - participants: [formattedParticipantA, formattedParticipantB, serverAddress], - weights: [0, 0, 100], - quorum: 100, - challenge: 0, - nonce: nonce, - }; - - const appSessionData: CreateAppSessionRequestParams[] = [{ - definition: appDefinition, - allocations: [ - { - participant: formattedParticipantA, - asset: 'usdc', - amount: '0.00001', - }, - { - participant: formattedParticipantB, - asset: 'usdc', - amount: '0.00001', - }, - { - participant: serverAddress, - asset: 'usdc', - amount: '0', - }, - ] - }]; - - // Generate the complete request structure - const signedMessage = await createAppSessionMessage(serverSigner, appSessionData); - const parsedMessage = JSON.parse(signedMessage); - - // Extract the request structure that clients should sign - const requestToSign = parsedMessage.req; - - console.debug(`Generated request structure for room ${roomId}:`, requestToSign); - - // Store the pending app session data - pendingAppSessions.set(roomId, { - appSessionData, - appDefinition, - participantA: formattedParticipantA, - participantB: formattedParticipantB, - serverAddress, - signatures: new Map(), - createdAt: Date.now(), - nonce: nonce, - requestToSign: requestToSign, - originalSignedMessage: signedMessage - }); - - console.log(`App session message generated for room ${roomId} with nonce ${nonce}`); - return { - appSessionData, - appDefinition, - participants: [formattedParticipantA, formattedParticipantB, serverAddress], - requestToSign: requestToSign - }; - - } catch (error) { - console.error(`Error generating app session message for room ${roomId}:`, error); - throw error; - } -} - -/** - * Add a signature to the pending app session - */ -export async function addAppSessionSignature(roomId: string, participantAddress: Hex, signature: string): Promise { - try { - // Format the participant address - const formattedParticipantAddress = ethers.getAddress(participantAddress) as Hex; - - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - throw new Error(`No pending app session found for room ${roomId}`); - } - - // Verify the participant is part of this session - const isValidParticipant = [pendingSession.participantA, pendingSession.participantB].includes(formattedParticipantAddress); - if (!isValidParticipant) { - throw new Error(`Invalid participant ${formattedParticipantAddress} for room ${roomId}`); - } - - // Store the signature - pendingSession.signatures.set(formattedParticipantAddress, signature); - - console.log(`Added signature for ${formattedParticipantAddress} in room ${roomId} (${pendingSession.signatures.size}/2 collected)`); - console.debug(`Signature details:`, { participantAddress: formattedParticipantAddress, signature: signature.substring(0, 10) + '...', signatureLength: signature.length }); - - // Check if we have all participant signatures - const allParticipantsSigned = pendingSession.signatures.has(pendingSession.participantA) && - pendingSession.signatures.has(pendingSession.participantB); - - return allParticipantsSigned; - - } catch (error) { - console.error(`Error adding signature for room ${roomId}:`, error); - throw error; - } -} - -/** - * Create an app session with collected signatures - */ -export async function createAppSessionWithSignatures(roomId: string): Promise { - try { - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - throw new Error(`No pending app session found for room ${roomId}`); - } - - // Verify all signatures are collected - const allSigned = pendingSession.signatures.has(pendingSession.participantA) && - pendingSession.signatures.has(pendingSession.participantB); - - if (!allSigned) { - throw new Error(`Not all signatures collected for room ${roomId}`); - } - - console.log(`Creating app session with collected signatures for room ${roomId}`); - - // Collect all signatures including server signature - const participantASignature = pendingSession.signatures.get(pendingSession.participantA); - const participantBSignature = pendingSession.signatures.get(pendingSession.participantB); - - console.debug(`Participant signatures for room ${roomId}:`, { - participantA: pendingSession.participantA, - participantB: pendingSession.participantB, - participantASignature, - participantBSignature, - allStoredSignatures: Array.from(pendingSession.signatures.entries()) - }); - - // Validate that we have all participant signatures - if (!participantASignature) { - throw new Error(`Missing signature from participant A: ${pendingSession.participantA}`); - } - if (!participantBSignature) { - throw new Error(`Missing signature from participant B: ${pendingSession.participantB}`); - } - - // Create a properly formatted message with all signatures - const allSignatures = [participantASignature, participantBSignature]; - - // Now let the server sign the same request structure as the clients - const sign = serverSigner; - - console.debug(`Server signing request structure for room ${roomId}:`, pendingSession.requestToSign); - - // Sign the same request structure that clients signed - const serverSignature = await sign(pendingSession.requestToSign); - - console.debug(`Server signature created:`, serverSignature); - - // Add server signature to complete the array - allSignatures.push(serverSignature); - - console.debug(`Combined signatures for room ${roomId}:`, allSignatures); - - // Create the final message with all signatures - const finalMessage = JSON.parse(pendingSession.originalSignedMessage); - finalMessage.sig = allSignatures; - - console.debug(`Final message structure:`, { - req: finalMessage.req, - signatures: finalMessage.sig, - participantsOrder: pendingSession.appSessionData[0].definition.participants, - messageToSend: JSON.stringify(finalMessage) - }); - - console.log("[createAppSessionWithSignatures] Sending request:", finalMessage); - const result = await sendToBroker(finalMessage); - const appId = result.app_session_id || (typeof result[0] === "object" ? result[0].app_session_id : null); - console.log(`[createAppSessionWithSignatures] Created app session ${appId}`); - - // Store the app ID for this room - roomAppSessions.set(roomId, { - appId, - participantA: pendingSession.participantA, - participantB: pendingSession.participantB, - serverAddress: pendingSession.serverAddress, - tokenAddress: process.env.USDC_TOKEN_ADDRESS || '', - createdAt: Date.now() - }); - - // Clean up pending session - pendingAppSessions.delete(roomId); - - console.log(`Created app session with ID ${appId} for room ${roomId}`); - return appId; - - } catch (error) { - console.error(`Error creating app session with signatures for room ${roomId}:`, error); - throw error; - } -} - -/** - * Get the app session for a room - */ -export function getAppSession(roomId: string): AppSession | null { - return roomAppSessions.get(roomId) || null; -} - -/** - * Get existing pending app session message for a room - */ -export function getPendingAppSessionMessage(roomId: string): { - appSessionData: CreateAppSessionRequestParams[]; - appDefinition: AppDefinition; - participants: Hex[]; - requestToSign: any; -} | null { - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - return null; - } - - return { - appSessionData: pendingSession.appSessionData, - appDefinition: pendingSession.appDefinition, - participants: [pendingSession.participantA, pendingSession.participantB, pendingSession.serverAddress], - requestToSign: pendingSession.requestToSign - }; -} - -/** - * Check if a room has an app session - */ -export function hasAppSession(roomId: string): boolean { - return roomAppSessions.has(roomId); -} - -/** - * Get all app sessions - */ -export function getAllAppSessions(): Map { - return roomAppSessions; -} diff --git a/examples/snake/server/services/brokerService.ts b/examples/snake/server/services/brokerService.ts deleted file mode 100644 index f8f08ea58..000000000 --- a/examples/snake/server/services/brokerService.ts +++ /dev/null @@ -1,711 +0,0 @@ -import WebSocket from "ws"; -import {ethers} from "ethers"; -import { - AppDefinition, - AuthRequestParams, - ChannelUpdate, - CloseAppSessionRequestParams, - createAppSessionMessage, - CreateAppSessionRequestParams, - createAuthRequestMessage, - createAuthVerifyMessage, - createAuthVerifyMessageWithJWT, - createCloseAppSessionMessage, - createEIP712AuthMessageSigner, - createGetLedgerBalancesMessage, - MessageSigner, - NitroliteRPC, - parseAnyRPCResponse, - RequestData, - ResponsePayload, - RPCMethod, - RPCResponse, -} from "@erc7824/nitrolite"; -import {BROKER_WS_URL, WALLET_PRIVATE_KEY} from "../config/index.ts"; -import { - addPendingRequest, - clearPendingRequest, - getBrokerWebSocket, - getPendingRequest, - setBrokerWebSocket -} from "./stateService.ts"; -import {createWalletClient, Hex, http} from "viem"; -import {privateKeyToAccount} from "viem/accounts"; -import {polygon} from "viem/chains"; - -import util from 'util'; - -util.inspect.defaultOptions.depth = null; - -export const DEFAULT_PROTOCOL = "app_snake_nitrolite"; -const DEFAULT_WEIGHTS: number[] = [0, 0, 100]; // Alice: 0, Bob: 0, Server: 100 -const DEFAULT_QUORUM: number = 100; // server alone decides the outcome - -// Flag to indicate if we've authenticated with the broker -let isAuthenticated = false; - -// Store JWT token at file level for reuse -let jwtToken: string | null = null; - -async function getChannels(): Promise { - const brokerWs = getBrokerWebSocket(); - if (!brokerWs || brokerWs.readyState !== WebSocket.OPEN) { - throw new Error("WebSocket not connected"); - } - - const signer = createEthersSigner(WALLET_PRIVATE_KEY); - const params = [{participant: signer.address}]; - const request = NitroliteRPC.createRequest(10, RPCMethod.GetChannels, params); - const getChannelMessage = await NitroliteRPC.signRequestMessage(request, signer.sign); - brokerWs.send(JSON.stringify(getChannelMessage)); -} - -// Connects to the Nitrolite broker -export function connectToBroker(): void { - const brokerWs = getBrokerWebSocket(); - if (brokerWs && (brokerWs.readyState === WebSocket.OPEN || brokerWs.readyState === WebSocket.CONNECTING)) { - console.log("WebSocket already connected or connecting. State:", brokerWs.readyState); - return; - } - - console.log(`Connecting to Nitrolite broker at ${BROKER_WS_URL}`); - const ws = new WebSocket(BROKER_WS_URL); - setBrokerWebSocket(ws); - isAuthenticated = false; - - ws.on("open", async () => { - console.log("Connected to Nitrolite broker"); - - // Authenticate with the broker immediately upon connection - try { - await authenticateWithBroker(); - console.log("Successfully authenticated with broker"); - } catch (error) { - console.error("Authentication with broker failed:", error); - } - }); - - ws.on("message", (data) => { - try { - const message = JSON.parse(data.toString()); - console.log("Received message from broker:", { - method: message.res?.[1], - requestId: message.res?.[0], - isAuthenticated - }); - handleBrokerMessage(data.toString()); - } catch (error) { - console.error("Error parsing message from broker:", error); - } - }); - - ws.on("close", (code, reason) => { - console.log("Disconnected from Nitrolite broker:", { - code, - reason: reason.toString(), - isAuthenticated - }); - isAuthenticated = false; - jwtToken = null; // Clear JWT token on disconnect - setTimeout(connectToBroker, 5000); - }); - - ws.on("error", (error) => { - console.error("Error in broker WebSocket connection:", { - error: error.message, - isAuthenticated - }); - }); -} - -// Authenticate with the broker using server's wallet and nitrolite package -async function authenticateWithBroker(): Promise { - const brokerWs = getBrokerWebSocket(); - if (!brokerWs || brokerWs.readyState !== WebSocket.OPEN) { - throw new Error("WebSocket not connected"); - } - - // Create the wallet signer using our factory - const signer = createEthersSigner(WALLET_PRIVATE_KEY); - const serverAddress = signer.address; - if (!serverAddress) { - throw new Error("Server address not found"); - } - - - const expire = String(Math.floor(Date.now() / 1000) + 24 * 60 * 60); - - const authMessage: AuthRequestParams = { - wallet: serverAddress, - participant: serverAddress, - app_name: 'Snake Game', - expire: expire, - scope: 'snake-game', - application: serverAddress, - allowances: [{ - asset: 'usdc', - amount: '0', - }], - }; - - return new Promise(async (resolve, reject) => { - let authTimeout: NodeJS.Timeout; - - // Clean up function to remove listeners and clear timeout - const cleanup = () => { - brokerWs.removeListener("message", authMessageHandler); - clearTimeout(authTimeout); - }; - - // Create a one-time message handler for authentication - const authMessageHandler = async (data: WebSocket.RawData) => { - try { - let message: RPCResponse; - try { - message = parseAnyRPCResponse(data.toString()); - } catch (error) { - console.warn("Error parsing auth message from broker, skipping:", error); - return; - } - console.log("Auth process message received:", message); - - // Check for auth_challenge response (response to our auth_request) - if (message.method === RPCMethod.AuthChallenge) { - console.log("Received auth_challenge, preparing EIP-712 auth_verify..."); - - try { - // Step 2: Create EIP-712 signing function for challenge verification - const account = privateKeyToAccount(WALLET_PRIVATE_KEY as Hex); - const walletClient = createWalletClient({ - account, - chain: polygon, - transport: http() - }); - if (!walletClient) { - throw new Error('No wallet client available for EIP-712 signing'); - } - - console.log('Creating EIP-712 signing function...'); - // @ts-ignore - const eip712SigningFunction = createEIP712AuthMessageSigner(walletClient, { - scope: authMessage.scope, - application: walletClient.account.address, - participant: authMessage.wallet, - expire: authMessage.expire, - allowances: authMessage.allowances.map((allowance) => ({ - asset: allowance.asset, - amount: allowance.amount.toString(), - })), - }, getAuthDomain()); - - // Create and send verification message with EIP-712 signature - console.log('Calling createAuthVerifyMessage...'); - const authVerify = await createAuthVerifyMessage(eip712SigningFunction, message); - - console.log('Sending auth_verify with EIP-712 signature'); - brokerWs.send(authVerify); - console.log('auth_verify sent successfully'); - - // Send additional requests - const getBalances = await createGetLedgerBalancesMessage( - signer.sign, - serverAddress - ); - brokerWs.send(getBalances); - await getChannels(); - } catch (eip712Error) { - console.error('Error creating EIP-712 auth_verify:', eip712Error); - console.error('Error stack:', (eip712Error as Error).stack); - - cleanup(); - reject(new Error(`EIP-712 auth_verify failed: ${(eip712Error as Error).message}`)); - return; - } - } - // Check for auth_verify success response - else if (message.method === RPCMethod.AuthVerify) { - console.log("Authentication successful"); - - // If response contains a JWT token, store it - if (message.params.jwtToken) { - console.log('JWT token received:', message.params.jwtToken); - jwtToken = message.params.jwtToken; - } - - isAuthenticated = true; - cleanup(); - resolve(); - } - // Check for error responses - else if (message.method === RPCMethod.Error) { - const errorMsg = message.params.error || 'Authentication failed'; - console.error('Authentication failed:', message, errorMsg); - - // Check if this is a JWT authentication failure and fallback to signer auth - const errorString = String(errorMsg).toLowerCase(); - if (errorString.includes('jwt') || errorString.includes('token') || errorString.includes('invalid') || errorString.includes('expired')) { - console.warn('JWT authentication failed on server, attempting fallback to signer authentication'); - jwtToken = null; // Clear invalid JWT token - - try { - // Restart authentication with signer - const fallbackAuthRequest = await createAuthRequestMessage(authMessage); - - console.log('Sending fallback auth_request with signer:', fallbackAuthRequest); - brokerWs.send(fallbackAuthRequest); - // Reset timeout for the fallback attempt - clearTimeout(authTimeout); - authTimeout = setTimeout(() => { - cleanup(); - reject(new Error("Authentication timeout")); - }, 15000); - return; // Continue listening for the fallback response - } catch (fallbackError) { - console.error('Fallback to signer authentication failed:', fallbackError); - cleanup(); - reject(new Error(`Both JWT and signer authentication failed: ${fallbackError}`)); - return; - } - } - - jwtToken = null; // Clear JWT token on auth failure - cleanup(); - reject(new Error(String(errorMsg))); - } else { - console.log('Received non-auth message during auth, continuing to listen:', message); - // Keep listening if it wasn't a final success/error - } - } catch (error) { - console.error("Error handling auth response:", error); - console.error("Error stack:", (error as Error).stack); - cleanup(); - reject(new Error(`Authentication error: ${error instanceof Error ? error.message : String(error)}`)); - } - }; - - // Set timeout for auth process - authTimeout = setTimeout(() => { - cleanup(); - reject(new Error("Authentication timeout")); - }, 15000); // 15 second timeout - - // Add temporary listener for authentication messages - brokerWs.on("message", authMessageHandler); - - // Step 1: Send auth_request with JWT token if available - console.log('Starting authentication with:'); - console.log('- Server wallet address:', serverAddress); - console.log('- JWT token if available, otherwise EIP-712 signature for auth_verify challenge'); - - try { - let authRequest: string; - let usingJWT = false; - - if (jwtToken) { - console.log('JWT token found, attempting JWT authentication:', jwtToken); - try { - authRequest = await createAuthVerifyMessageWithJWT(jwtToken); - usingJWT = true; - } catch (jwtError) { - console.warn('JWT auth failed, falling back to signer authentication:', jwtError); - // Clear invalid JWT token - jwtToken = null; - authRequest = await createAuthRequestMessage(authMessage); - usingJWT = false; - } - } else { - console.log('No JWT token found, proceeding with challenge-response authentication'); - authRequest = await createAuthRequestMessage(authMessage); - usingJWT = false; - } - - console.log(`Sending auth_request (${usingJWT ? 'JWT' : 'challenge-response'}):`, authRequest); - brokerWs.send(authRequest); - } catch (requestError) { - console.error('Error creating auth_request:', requestError); - cleanup(); - reject(new Error(`Failed to create auth_request: ${(requestError as Error).message}`)); - } - }); -} - -// Handles messages received from the broker -export function handleBrokerMessage(raw: string): void { - try { - console.log("Received message from broker:", raw); - const message = parseAnyRPCResponse(raw); - console.log("Parsed message:", message); - - // Handle ping messages - if (message.method === RPCMethod.Ping) { - console.log("Received ping from broker, sending pong"); - const brokerWs = getBrokerWebSocket(); - if (brokerWs && brokerWs.readyState === WebSocket.OPEN) { - brokerWs.send(JSON.stringify({type: "pong"})); - } - return; - } else if (message.method === RPCMethod.Error) { - console.log("Received error from broker:", message.params.error); - - // Check if it's a response to a pending request - if (!message.requestId) { - return; - } - const pendingRequest = getPendingRequest(message.requestId.toString()); - if (pendingRequest) { - const {reject, timeout} = pendingRequest; - clearTimeout(timeout); - clearPendingRequest(message.requestId.toString()); - reject(new Error(message.params.error)); - } - return; - } else if (message.method === RPCMethod.GetChannels || message.method === RPCMethod.ChannelsUpdate) { - let channels: ChannelUpdate[]; - if (message.method === RPCMethod.GetChannels) { - channels = message.params; - } else { - channels = [message.params]; - } - - console.log("Received get_channels from broker:", message.params); - if (channels.length === 0) { - throw new Error("No channels found. Please open a channel at apps.yellow.com"); - } - - // Handle successful response to a pending request - if (!message.requestId) { - return; - } - const pendingRequest = getPendingRequest(message.requestId.toString()); - if (pendingRequest) { - const {resolve, timeout} = pendingRequest; - clearTimeout(timeout); - clearPendingRequest(message.requestId.toString()); - resolve(message.params); - return; - } - } - - // Catch-all block for any other message types that might be responses to pending requests - if (message.requestId) { - const pendingRequest = getPendingRequest(message.requestId.toString()); - if (pendingRequest) { - const {resolve, timeout} = pendingRequest; - clearTimeout(timeout); - clearPendingRequest(message.requestId.toString()); - resolve(message.params || []); - return; - } - } - - // Handle other message types like notifications - // (in a real implementation, you might want to emit events for these) - } catch (error) { - console.error("Error handling broker message:", error); - } -} - -// Check authentication status -export function isAuthenticatedWithBroker(): boolean { - return isAuthenticated; -} - -// Re-export the authentication function for external use -export {authenticateWithBroker}; - -// Sends a request to the broker and returns a promise -export async function sendToBroker(request: any): Promise { - // Check authentication first before creating the Promise - if (!isAuthenticated && !(request.req && request.req[1] === "auth_request") && !(request.req && request.req[1] === "auth_verify")) { - try { - console.log("Not authenticated with broker, authenticating first..."); - await authenticateWithBroker(); - } catch (error) { - console.error("Authentication failed:", error); - if (error instanceof Error) { - throw new Error(`Authentication failed: ${error.message}`); - } else { - throw new Error('Authentication failed: Unknown error'); - } - } - } - - return new Promise((resolve, reject) => { - const brokerWs = getBrokerWebSocket(); - if (!brokerWs || brokerWs.readyState !== WebSocket.OPEN) { - console.error("WebSocket not connected or not open. State:", brokerWs?.readyState); - reject(new Error("Not connected to broker")); - return; - } - - console.log("Sending request to broker:", { - method: request.req?.[1], - requestId: request.req?.[0], - isAuthenticated, - wsState: brokerWs.readyState - }); - - // Prepare the request using a Promise chain - const prepareRequest = async (): Promise<{ req: any; requestId: string | number }> => { - let requestId: string | number; - let preparedRequest = request; - - // Check if the request is in the new format - if (request.req && Array.isArray(request.req)) { - requestId = request.req[0] || Date.now(); - preparedRequest.req[0] = requestId; - - // If the signature is empty or missing, add it - if (!preparedRequest.sig || preparedRequest.sig.length === 0 || !preparedRequest.sig[0]) { - const signature = await signRpcRequest(preparedRequest.req); - preparedRequest.sig = [signature]; - } - } else { - // Legacy format - convert to new format - requestId = request.id || `req-${Date.now()}`; - const reqData = [requestId, request.method, request.params ? [request.params] : [], Date.now()]; - - // Sign the request - const signature = await signRpcRequest(reqData); - - preparedRequest = { - req: reqData, - sig: [signature], - }; - } - - return {req: preparedRequest, requestId}; - }; - - // Execute the async preparation outside the Promise executor - prepareRequest() - .then(({req, requestId}) => { - // Convert requestId to string for tracking - const requestIdStr = requestId.toString(); - - const timeout = setTimeout(() => { - console.error("Request timed out:", { - requestId: requestIdStr, - method: req.req[1], - isAuthenticated, - wsState: brokerWs.readyState - }); - clearPendingRequest(requestIdStr); - reject(new Error("Request timeout")); - }, 10000); // 10 second timeout - - addPendingRequest(requestIdStr, resolve, reject, timeout); - brokerWs.send(JSON.stringify(req)); - }) - .catch((error) => { - console.error("Failed to prepare request:", error); - reject(new Error(`Failed to prepare request: ${error.message}`)); - }); - }); -} - -// Creates an application session in the broker -export async function createAppSession(participantA: Hex, participantB: Hex): Promise { - // Ensure we're authenticated before creating an app session - if (!isAuthenticated) { - try { - await authenticateWithBroker(); - } catch (error) { - console.error(`Authentication failed before creating app session:`, error); - if (error instanceof Error) { - throw new Error(`Authentication required to create app session: ${error.message}`); - } else { - throw new Error('Authentication required to create app session: Unknown error'); - } - } - } - - // Get the server's wallet address - const signer = createEthersSigner(WALLET_PRIVATE_KEY); - if (!signer.address) { - throw new Error("Server wallet address not found"); - } - - // Prepare the request object - const participants = [participantA, participantB, signer.address as Hex]; - console.log("[createAppSession] Creating app session with:", { - participants, - signerAddress: signer.address - }); - - const appDefinition: AppDefinition = { - protocol: DEFAULT_PROTOCOL, - participants, - weights: DEFAULT_WEIGHTS, - quorum: DEFAULT_QUORUM, - challenge: 0, - nonce: Date.now(), - }; - const params: CreateAppSessionRequestParams[] = [{ - definition: appDefinition, - allocations: participants.map((participant, index) => ({ - participant, - asset: "usdc", - amount: index < 2 ? "0.00001" : "0", // Players get 0.00001, server gets 0 - })) - }] - - // Create the request with properly formatted parameters - const request = await createAppSessionMessage(signer.sign, params); - console.log("[createAppSession] Sending request:", request); - const result = await sendToBroker(request); - const appId = result.app_session_id || (typeof result[0] === "object" ? result[0].app_session_id : null); - console.log(`[createAppSession] Created app session ${appId}`); - return appId; -} - -// Closes an application session in the broker with server signature only -export async function closeAppSession(appId: Hex, participantA: Hex, participantB: Hex): Promise { - // Ensure we're authenticated before closing an app session - if (!isAuthenticated) { - try { - await authenticateWithBroker(); - } catch (error) { - console.error(`Authentication failed before closing app session:`, error); - if (error instanceof Error) { - throw new Error(`Authentication required to close app session: ${error.message}`); - } else { - throw new Error('Authentication required to close app session: Unknown error'); - } - } - } - - // Get the server's wallet address - const signer = createEthersSigner(WALLET_PRIVATE_KEY); - if (!signer.address) { - throw new Error("Server wallet address not found"); - } - - // Verify the app session exists before trying to close it - try { - const requestId = Date.now(); - const timestamp = Date.now(); - const request: { req: [number, string, { app_session_id: string }[], number] } = { - req: [requestId, "get_app_definition", [{app_session_id: appId}], timestamp] - }; - console.log("[closeAppSession] Verifying app session exists:", appId); - await sendToBroker(request); - console.log("[closeAppSession] App session exists, proceeding with close"); - } catch (error) { - console.error(`[closeAppSession] App session ${appId} not found or already closed:`, error); - if (error instanceof Error) { - throw new Error(`App session ${appId} not found or already closed: ${error.message}`); - } else { - throw new Error(`App session ${appId} not found or already closed: Unknown error`); - } - } - - // Create close message and sign with server - const params: CloseAppSessionRequestParams[] = [{ - app_session_id: appId, - allocations: [participantA, participantB, signer.address].map((participant, index) => ({ - participant, - asset: "usdc", - amount: index < 2 ? "0.00001" : "0", // Players get 0.00001, server gets 0 - })) - }] - const closeRequestData = await createCloseAppSessionMessage(signer.sign, params); - const req = JSON.parse(closeRequestData); - const serverSignature = await signer.sign(req); - - // Create the signed request with server signature only - const signedRequest = { - req: closeRequestData, - sig: [serverSignature] - }; - - console.log("[closeAppSession] Sending close request with server signature:", signedRequest); - await sendToBroker(signedRequest); - console.log(`[closeAppSession] Closed app session ${appId}`); -} - -// Helper function to sign state data with the server's private key -export async function signStateData(stateData: string): Promise<{ signature: string; address: Hex }> { - const signer = createEthersSigner(WALLET_PRIVATE_KEY); - return { - signature: await signer.sign(stateData as unknown as RequestData), - address: signer.address as Hex, - }; -} - -/** - * Interface for a wallet signer that can sign messages - */ -export interface WalletSigner { - /** Public key in hexadecimal format */ - publicKey: string; - /** Optional Ethereum address derived from the public key */ - address?: Hex; - /** Function to sign a message and return a hex signature */ - sign: MessageSigner; -} - -/** - * Creates a signer from a private key using ethers.js - * - * @param privateKey - The private key to create the signer from - * @returns A WalletSigner object that can sign messages - * @throws Error if signer creation fails - */ -export function createEthersSigner(privateKey: string): WalletSigner { - try { - // Create ethers wallet from private key - const wallet = new ethers.Wallet(privateKey); - - return { - publicKey: wallet.address, - address: wallet.address as Hex, - sign: async (data: RequestData | ResponsePayload): Promise => { - try { - const messageStr = JSON.stringify(data); - - const digestHex = ethers.id(messageStr); - const messageBytes = ethers.getBytes(digestHex); - - const {serialized: signature} = wallet.signingKey.sign(messageBytes); - return signature as Hex; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }, - }; - } catch (error) { - console.error("Error creating ethers signer:", error); - throw error; - } -} - -// Helper function to sign RPC request data for the broker -export async function signRpcRequest(requestData: any[]): Promise { - const signer = createEthersSigner(WALLET_PRIVATE_KEY); - return signer.sign(requestData as RequestData); -} - -// Verify a signature against a message and expected signer -export function verifySignature(message: string, signature: string, expectedAddress: string): boolean { - try { - // Use standard Ethereum message verification - const recoveredAddress = ethers.verifyMessage(message, signature); - - // Check if the recovered address matches the expected address - return recoveredAddress.toLowerCase() === expectedAddress.toLowerCase(); - } catch (error) { - console.error("Error verifying signature:", error); - return false; - } -} - -/** - * EIP-712 domain and types for auth_verify challenge - */ -const getAuthDomain = () => { - return { - name: 'Snake Game', - }; -}; diff --git a/examples/snake/server/services/gameService.ts b/examples/snake/server/services/gameService.ts deleted file mode 100644 index 87d28b0a7..000000000 --- a/examples/snake/server/services/gameService.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { randomBytes } from 'crypto'; -import { Player } from '../interfaces/index.ts'; -import { getRoom } from './stateService.ts'; - -// Import websocketService to broadcast game state - imported at bottom of file to avoid circular dependency -let broadcastGameStateToClients: (roomId: string, gameState: any) => void; - -// Generate a random room ID -export function generateRoomId(): string { - return randomBytes(16).toString('hex'); -} - -// Generate a random food position -export function generateFood( - gridSize: { width: number; height: number }, - players: Map -): { x: number; y: number } { - let x: number = 0, y: number = 0; - let validPosition = false; - - while (!validPosition) { - x = Math.floor(Math.random() * gridSize.width); - y = Math.floor(Math.random() * gridSize.height); - - validPosition = true; - - for (const player of players.values()) { - for (const segment of player.segments) { - if (segment.x === x && segment.y === y) { - validPosition = false; - break; - } - } - if (!validPosition) break; - } - } - - return { x, y }; -} - -// Initialize a new player -export function initializePlayer( - id: string, - nickname: string, - gridSize: { width: number; height: number } -): Player { - const x = Math.floor(Math.random() * (gridSize.width - 10)) + 5; - const y = Math.floor(Math.random() * (gridSize.height - 10)) + 5; - - return { - id, - nickname, - position: { x, y }, - direction: ['up', 'down', 'left', 'right'][Math.floor(Math.random() * 4)] as 'up' | 'down' | 'left' | 'right', - segments: [{ x, y }], - score: 0 - }; -} - -// Initialize broadcast function -export function initializeBroadcastFunction( - broadcastFn: (roomId: string, gameState: any) => void -): void { - broadcastGameStateToClients = broadcastFn; -} - -// Process one game tick for a room -export async function gameTick(roomId: string): Promise { - const room = getRoom(roomId); - if (!room) return; - - // If game is over, don't process any more ticks - if (room.isGameOver) { - return; - } - - const { players, food, gridSize } = room; - const playersArray = Array.from(players.values()); - - // Move each player - for (const player of playersArray) { - // Skip if player is dead - if (player.isDead) continue; - - // Get current head position - const head = { ...player.position }; - - // Move head based on direction - switch (player.direction) { - case 'up': - head.y = (head.y - 1 + gridSize.height) % gridSize.height; - break; - case 'down': - head.y = (head.y + 1) % gridSize.height; - break; - case 'left': - head.x = (head.x - 1 + gridSize.width) % gridSize.width; - break; - case 'right': - head.x = (head.x + 1) % gridSize.width; - break; - } - - // Update player position - player.position = head; - - // Check if player ate food - if (head.x === food.x && head.y === food.y) { - player.score += 10; - // Add new segment (will be handled below - we don't remove the tail) - // Generate new food - room.food = generateFood(gridSize, players); - } else { - // Remove tail if didn't eat food - player.segments.pop(); - } - - // Add new head to segments - player.segments.unshift({ ...head }); - - // Check for collisions with other players - let isCollision = false; - for (const otherPlayer of playersArray) { - // Skip first segment of the current player - const segments = otherPlayer === player ? - otherPlayer.segments.slice(1) : - otherPlayer.segments; - - for (const segment of segments) { - if (head.x === segment.x && head.y === segment.y) { - // Collision detected - mark player as dead - console.log(`[gameTick] Collision detected for player ${player.id} at position (${head.x}, ${head.y})`); - console.log(`[gameTick] Collided with segment from player ${otherPlayer.id} at (${segment.x}, ${segment.y})`); - isCollision = true; - player.isDead = true; - break; - } - } - if (isCollision) break; - } - } - - // Check if game is over (only one player left alive or all players dead) - if (playersArray.length > 1) { - const alivePlayers = playersArray.filter(p => !p.isDead); - console.log(`[gameTick] Alive players count: ${alivePlayers.length}`); - console.log(`[gameTick] Player states:`, playersArray.map(p => ({ id: p.id, isDead: p.isDead }))); - - if (alivePlayers.length <= 1) { - console.log(`[gameTick] Game over condition met at ${Date.now()}`); - room.isGameOver = true; - - // If there's an interval, clear it to stop the game - if (room.gameInterval) { - console.log(`[gameTick] Clearing game interval`); - clearInterval(room.gameInterval); - room.gameInterval = null; - } - } - } - - // Broadcast game state to all players in the room - console.log(`[gameTick] Broadcasting game state at ${Date.now()}`); - await broadcastGameState(roomId); -} - -// Broadcast game state to all clients in a room -export async function broadcastGameState(roomId: string): Promise { - const room = getRoom(roomId); - if (!room) { - console.warn(`[broadcastGameState] Room ${roomId} not found`); - return; - } - - // Create game state - const gameState = { - type: 'gameState', - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - nickname: p.nickname, - segments: p.segments, - score: p.score, - isDead: p.isDead || false - })), - food: room.food, - gridSize: room.gridSize, - isGameOver: room.isGameOver || false, - stateVersion: ++room.stateVersion, - timestamp: Date.now() - }; - - // Store the current state in the room - room.currentState = gameState; - - // Broadcast to all players in the room if the broadcast function is initialized - if (broadcastGameStateToClients) { - console.log(`[broadcastGameState] Broadcasting to clients at ${Date.now()}`); - broadcastGameStateToClients(roomId, gameState); - console.log(`[broadcastGameState] Broadcast complete at ${Date.now()}`); - } else { - console.warn(`[broadcastGameState] No broadcast function available`); - } -} - diff --git a/examples/snake/server/services/stateService.ts b/examples/snake/server/services/stateService.ts deleted file mode 100644 index 3f3373137..000000000 --- a/examples/snake/server/services/stateService.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { WebSocket } from 'ws'; -import { Room, PendingRequest } from '../interfaces/index.ts'; - -// Global state -export const rooms = new Map(); -let brokerWs: WebSocket | null = null; -export const pendingRequests = new Map(); - -// Set broker WebSocket connection -export function setBrokerWebSocket(ws: WebSocket) { - brokerWs = ws; -} - -// Get broker WebSocket connection -export function getBrokerWebSocket(): WebSocket | null { - return brokerWs; -} - -// Add a room -export function addRoom(roomId: string, room: Room): void { - console.log(`[stateService] Adding room ${roomId}`); - rooms.set(roomId, room); - console.log(`[stateService] Current rooms:`, Array.from(rooms.keys())); -} - -// Get a room -export function getRoom(roomId: string): Room | undefined { - console.log(`[stateService] Getting room ${roomId}`); - console.log(`[stateService] Available rooms:`, Array.from(rooms.keys())); - return rooms.get(roomId); -} - -// Remove a room -export function removeRoom(roomId: string): boolean { - return rooms.delete(roomId); -} - -// Get all rooms -export function getAllRooms(): Map { - return rooms; -} - -// Add a pending request -export function addPendingRequest( - requestId: string, - resolve: Function, - reject: Function, - timeout: NodeJS.Timeout -): void { - pendingRequests.set(requestId, { resolve, reject, timeout }); -} - -// Get and remove a pending request -export function getPendingRequest(requestId: string): PendingRequest | undefined { - const request = pendingRequests.get(requestId); - pendingRequests.delete(requestId); - return request; -} - -// Clear a pending request -export function clearPendingRequest(requestId: string): void { - if (pendingRequests.has(requestId)) { - const { timeout } = pendingRequests.get(requestId)!; - clearTimeout(timeout); - pendingRequests.delete(requestId); - } -} - -export function createRoom(id: string): Room { - return { - id, - players: new Map(), - food: { x: 0, y: 0 }, - gameInterval: null, - gridSize: { width: 30, height: 30 }, - channelIds: new Set(), - playerAddresses: new Map(), - currentState: null, - stateVersion: 0, - createdAt: Date.now(), - closeSessionSignatures: new Map(), - }; -} diff --git a/examples/snake/server/services/websocketService.ts b/examples/snake/server/services/websocketService.ts deleted file mode 100644 index 72cc5b59e..000000000 --- a/examples/snake/server/services/websocketService.ts +++ /dev/null @@ -1,882 +0,0 @@ -import { WebSocketServer } from 'ws'; -import WebSocket from 'ws'; -import { randomBytes } from 'crypto'; -import { Room, SnakeWebSocket } from '../interfaces/index.ts'; -import { getRoom, addRoom, removeRoom, getAllRooms } from './stateService.ts'; -import { - generateRoomId, - generateFood, - initializePlayer, - gameTick, - initializeBroadcastFunction -} from './gameService.ts'; -import { closeAppSession, sendToBroker } from './brokerService.ts'; -import { - generateAppSessionMessage, - addAppSessionSignature, - serverSigner, - serverWallet -} from './appSessionService.ts'; -import { Hex } from 'viem'; -import { CloseAppSessionRequestParams, createCloseAppSessionMessage } from '@erc7824/nitrolite'; - -// Global reference to the WebSocket server -let webSocketServer: WebSocketServer; - -// Store clients subscribed to room updates -const roomSubscribers = new Set(); - -// Setup WebSocket handlers -export function setupWebSocketHandlers(wss: WebSocketServer): void { - webSocketServer = wss; - - // Initialize the broadcast function in gameService - initializeBroadcastFunction(broadcastGameState); - - wss.on('connection', (ws: WebSocket) => { - console.log('Client connected'); - - const snakeWs = ws as SnakeWebSocket; - snakeWs.playerId = randomBytes(8).toString('hex'); - - snakeWs.on('message', async (message: WebSocket.RawData) => { - try { - const data = JSON.parse(message.toString()); - await handleWebSocketMessage(snakeWs, data); - } catch (error) { - console.error('Error handling message:', error); - } - }); - - snakeWs.on('close', async () => { - // Remove from room subscribers if they were subscribed - roomSubscribers.delete(snakeWs); - await handleDisconnect(snakeWs); - }); - }); -} - -// Get WebSocketServer instance -export function getWebSocketServer(): WebSocketServer { - return webSocketServer; -} - -// Broadcast game state to all clients in a room -export function broadcastGameState(roomId: string, gameState: any): void { - console.log(`[websocketService] Broadcasting game state to room ${roomId} at ${Date.now()}`); - console.log(`[websocketService] Game state version: ${gameState.stateVersion}`); - console.log(`[websocketService] Game over status: ${gameState.isGameOver}`); - - let clientCount = 0; - webSocketServer.clients.forEach(client => { - const snakeClient = client as SnakeWebSocket; - if (snakeClient.roomId === roomId && snakeClient.readyState === WebSocket.OPEN) { - clientCount++; - console.log(`[websocketService] Sending to client ${snakeClient.playerId} at ${Date.now()}`); - snakeClient.send(JSON.stringify(gameState)); - } - }); - console.log(`[websocketService] Broadcast complete. Sent to ${clientCount} clients at ${Date.now()}`); -} - -// Broadcast vote update to all clients in a room -function broadcastVoteUpdate(roomId: string): void { - const room = getRoom(roomId); - if (!room) return; - - const voteUpdate = { - type: 'playAgainVoteUpdate', - playAgainVotes: room.playAgainVotes ? Array.from(room.playAgainVotes) : [], - totalPlayers: room.players.size, - votesNeeded: room.players.size - (room.playAgainVotes?.size || 0) - }; - - console.log(`[broadcastVoteUpdate] Broadcasting vote update to room ${roomId}:`, voteUpdate); - - webSocketServer.clients.forEach(client => { - const snakeClient = client as SnakeWebSocket; - if (snakeClient.roomId === roomId && snakeClient.readyState === WebSocket.OPEN) { - snakeClient.send(JSON.stringify(voteUpdate)); - } - }); -} - -// Broadcast player disconnect notification to all clients in a room -function broadcastPlayerDisconnect(roomId: string, playerNickname: string): void { - const disconnectNotification = { - type: 'playerDisconnected', - playerNickname, - message: `${playerNickname} left the room` - }; - - console.log(`[broadcastPlayerDisconnect] Broadcasting disconnect notification to room ${roomId}:`, disconnectNotification); - - webSocketServer.clients.forEach(client => { - const snakeClient = client as SnakeWebSocket; - if (snakeClient.roomId === roomId && snakeClient.readyState === WebSocket.OPEN) { - snakeClient.send(JSON.stringify(disconnectNotification)); - } - }); -} - -// Handle WebSocket message -async function handleWebSocketMessage(ws: SnakeWebSocket, data: any): Promise { - console.log(`[websocketService] Received message type: ${data.type}`, data); - - switch (data.type) { - case 'createRoom': { - await handleCreateRoom(ws, data); - break; - } - - case 'joinRoom': { - await handleJoinRoom(ws, data); - break; - } - - case 'changeDirection': { - await handleChangeDirection(ws, data); - break; - } - - case 'playAgain': { - await handlePlayAgain(data); - break; - } - - case 'finalizeGame': { - console.log('[websocketService] Handling finalizeGame message:', data); - await handleFinalizeGame(data); - break; - } - - case 'subscribeRooms': { - handleSubscribeRooms(ws); - break; - } - - case 'unsubscribeRooms': { - handleUnsubscribeRooms(ws); - break; - } - - case 'appSession:signature': { - await handleAppSessionSignature(ws, data); - break; - } - - case 'closeSessionSignature': { - await handleCloseSessionSignature(data); - break; - } - - default: { - console.log(`[websocketService] Unknown message type: ${data.type}`); - break; - } - } -} - -// Handle create room message -async function handleCreateRoom(ws: SnakeWebSocket, data: any): Promise { - console.log('[websocketService] Creating room with data:', data); - - // Check if player is already in a room - if (ws.roomId) { - console.log(`[websocketService] Player ${ws.playerId} already in room ${ws.roomId}, ignoring create room request`); - ws.send(JSON.stringify({ - type: 'error', - message: 'You are already in a room' - })); - return; - } - - const roomId = generateRoomId(); - const { nickname, channelId, walletAddress } = data; - const gridSize = { width: 40, height: 30 }; - - // Create player - const player = initializePlayer(ws.playerId, nickname, gridSize); - - // Create room with channel support - const room: Room = { - id: roomId, - players: new Map([[player.id, player]]), - food: generateFood(gridSize, new Map([[player.id, player]])), - gameInterval: null, - gridSize, - channelIds: new Set(), - playerAddresses: new Map([[player.id, walletAddress]]), - currentState: null, - stateVersion: 0, - createdAt: Date.now(), - closeSessionSignatures: new Map() - }; - - // Add channelId if provided - if (channelId) { - room.channelIds.add(channelId); - ws.channelId = channelId; - } - - // Store the room - addRoom(roomId, room); - - ws.roomId = roomId; - - // Respond with room info - ws.send(JSON.stringify({ - type: 'roomCreated', - roomId, - playerId: player.id - })); - - console.log(`[websocketService] Room created: ${roomId}, Player: ${player.id}, Address: ${walletAddress}`); - - // Notify subscribers about new room - broadcastRoomUpdate(roomId); -} - -// Handle join room message -async function handleJoinRoom(ws: SnakeWebSocket, data: any): Promise { - console.log('[websocketService] Joining room with data:', data); - - // Check if player is already in a room - if (ws.roomId) { - console.log(`[websocketService] Player ${ws.playerId} already in room ${ws.roomId}, ignoring join room request`); - ws.send(JSON.stringify({ - type: 'error', - message: 'You are already in a room' - })); - return; - } - - const { roomId, nickname, channelId, walletAddress } = data; - const room = getRoom(roomId); - console.log('[websocketService] Room lookup result:', room ? 'found' : 'not found'); - - if (!room) { - ws.send(JSON.stringify({ - type: 'error', - message: 'Room not found' - })); - return; - } - - if (room.players.size >= 2) { - ws.send(JSON.stringify({ - type: 'error', - message: 'Room is full' - })); - return; - } - - // Create player - const player = initializePlayer(ws.playerId, nickname, room.gridSize); - - // Add player to room - room.players.set(player.id, player); - room.playerAddresses.set(player.id, walletAddress); - - ws.roomId = roomId; - - // Add channelId if provided - if (channelId) { - room.channelIds.add(channelId); - ws.channelId = channelId; - } - - // Respond with room info - ws.send(JSON.stringify({ - type: 'roomJoined', - roomId, - playerId: player.id - })); - - console.log(`Player joined room: ${roomId}, Player: ${player.id}, Address: ${walletAddress}`); - console.log('Room data:', room); - - // Notify subscribers about room update - broadcastRoomUpdate(roomId); - - // If we have 2 players and a channel, initiate app session creation with signatures - if (room.players.size === 2 && room.channelIds.size > 0) { - try { - const players = Array.from(room.players.values()); - const participantA = room.playerAddresses.get(players[0].id) as Hex; - const participantB = room.playerAddresses.get(players[1].id) as Hex; - - console.log(`[websocketService] Initiating app session creation for room ${roomId} with players:`, { - player1: { id: players[0].id, address: participantA }, - player2: { id: players[1].id, address: participantB } - }); - - // Generate the message structure for signing - const { requestToSign } = await generateAppSessionMessage(roomId, participantA, participantB); - - // Start signature collection with guest player (participant B) - const guestPlayerId = players[1].id; // Second player to join - const guestClient = Array.from(webSocketServer.clients).find(client => { - const snakeClient = client as SnakeWebSocket; - return snakeClient.playerId === guestPlayerId && snakeClient.roomId === roomId; - }) as SnakeWebSocket; - - if (guestClient && guestClient.readyState === WebSocket.OPEN) { - guestClient.send(JSON.stringify({ - type: 'appSession:signatureRequest', - roomId, - requestToSign, - participantAddress: participantB - })); - console.log(`[websocketService] Sent signature request to guest player ${guestPlayerId}`); - } else { - throw new Error("Guest player not found or not connected"); - } - - } catch (error: unknown) { - console.error(`[websocketService] Error initiating app session for room ${roomId}:`, error); - ws.send(JSON.stringify({ - type: 'error', - message: 'Failed to initiate app session: ' + (error instanceof Error ? error.message : 'Unknown error') - })); - } - } -} - -// Handle change direction message -async function handleChangeDirection(ws: SnakeWebSocket, data: any): Promise { - const roomId = ws.roomId; - const { direction } = data; - - if (!roomId) return; - - const room = getRoom(roomId); - if (!room) return; - - // Don't process direction changes if game is over - if (room.isGameOver) return; - - const player = room.players.get(ws.playerId); - if (!player) return; - - // Don't allow dead players to change direction - if (player.isDead) return; - - // Prevent 180 degree turns - if (player.direction === 'up' && direction === 'down') return; - if (player.direction === 'down' && direction === 'up') return; - if (player.direction === 'left' && direction === 'right') return; - if (player.direction === 'right' && direction === 'left') return; - - player.direction = direction; -} - -// Handle play again message -async function handlePlayAgain(data: any): Promise { - const { roomId, playerId } = data; - if (!roomId || !playerId) return; - - const room = getRoom(roomId); - if (!room) return; - - // Initialize votes set if it doesn't exist - if (!room.playAgainVotes) { - room.playAgainVotes = new Set(); - } - - // Add this player's vote - room.playAgainVotes.add(playerId); - - console.log(`[handlePlayAgain] Player ${playerId} voted to play again. Current votes: ${room.playAgainVotes.size}/${room.players.size}`); - console.log(`[handlePlayAgain] Current players in room:`, Array.from(room.players.keys())); - console.log(`[handlePlayAgain] Current voters:`, Array.from(room.playAgainVotes)); - - // Broadcast vote update to all players in the room - broadcastVoteUpdate(roomId); - - // Check if all players have voted to play again (and we have at least 2 players) - if (room.playAgainVotes.size === room.players.size && room.players.size >= 2) { - console.log(`[handlePlayAgain] All players voted to play again. Restarting game.`); - - // Clear votes for next time - room.playAgainVotes.clear(); - - // Reset game state - room.isGameOver = false; - - // Reset players - for (const player of room.players.values()) { - const { width, height } = room.gridSize; - const x = Math.floor(Math.random() * (width - 10)) + 5; - const y = Math.floor(Math.random() * (height - 10)) + 5; - - player.position = { x, y }; - player.direction = ['up', 'down', 'left', 'right'][Math.floor(Math.random() * 4)] as 'up' | 'down' | 'left' | 'right'; - player.segments = [{ x, y }]; - player.score = 0; - player.isDead = false; - } - - // Create new food - room.food = generateFood(room.gridSize, room.players); - - // Reset state version - room.stateVersion = 0; - - // Restart game interval if needed - if (!room.gameInterval) { - room.gameInterval = setInterval(async () => { - await gameTick(roomId); - }, 150); - } - - // Create and broadcast initial game state - const gameState = { - type: 'gameState', - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - nickname: p.nickname, - segments: p.segments, - score: p.score, - isDead: p.isDead || false - })), - food: room.food, - gridSize: room.gridSize, - isGameOver: room.isGameOver || false, - stateVersion: ++room.stateVersion, - timestamp: Date.now() - }; - - // Store the current state in the room - room.currentState = gameState; - - // Broadcast to all players in the room - broadcastGameState(roomId, gameState); - } -} - -// Handle finalize game message -async function handleFinalizeGame(data: any): Promise { - const { roomId, playerId, walletAddress } = data; - if (!roomId || !playerId || !walletAddress) return; - - const room = getRoom(roomId); - if (!room || !room.appId) { - console.log(`[websocketService] Cannot finalize game - room ${roomId} not found or no app session`); - return; - } - - console.log(`[websocketService] Finalizing game for room ${roomId} with app session ${room.appId}`); - - // Mark game as over - room.isGameOver = true; - - // Get player addresses - const playerAddresses = Array.from(room.playerAddresses.values()); - const server = serverWallet.address as Hex; - - try { - // Create close message and sign with server - const params: CloseAppSessionRequestParams[] = [{ - app_session_id: room.appId, - allocations: [ - { - participant: playerAddresses[0] as Hex, - asset: "usdc", - amount: "0.00001" - }, - { - participant: playerAddresses[1] as Hex, - asset: "usdc", - amount: "0.00001" - }, - { - participant: server, - asset: "usdc", - amount: "0" - } - ] - }]; - - // Create the message to be signed by all participants - const closeRequestData = await createCloseAppSessionMessage(serverSigner, params); - - // Store the request in the room for later use - const requestToSign = JSON.parse(closeRequestData); - console.log('[websocketService] Close session request to sign:', requestToSign); - room.closeSessionRequest = requestToSign; - room.closeSessionSignatures = new Map(); - - // Send signature request to all players - console.log('[websocketService] Sending close request to all players'); - webSocketServer.clients.forEach(client => { - const snakeClient = client as SnakeWebSocket; - const closeRequest = { - type: 'appSession:closeRequest', - roomId, - requestToSign, - participantAddress: room.playerAddresses.get(playerId) - }; - if (snakeClient.roomId === roomId && snakeClient.readyState === WebSocket.OPEN) { - snakeClient.send(JSON.stringify(closeRequest)); - } - }); - - // Create and broadcast final game state - const gameState = { - type: 'gameState', - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - nickname: p.nickname, - segments: p.segments, - score: p.score, - isDead: p.isDead || false - })), - food: room.food, - gridSize: room.gridSize, - isGameOver: true, - stateVersion: ++room.stateVersion, - timestamp: Date.now() - }; - - // Store the current state in the room - room.currentState = gameState; - - // Broadcast to all players in the room - broadcastGameState(roomId, gameState); - } catch (error) { - console.error(`[websocketService] Error preparing close session request for room ${roomId}:`, error); - } -} - -// Handle close session signature -async function handleCloseSessionSignature(data: any): Promise { - const { roomId, playerId, signature, participantAddress } = data; - if (!roomId || !playerId || !signature || !participantAddress) { - console.log(`[websocketService] Missing required fields for close session signature`); - return; - } - - const room = getRoom(roomId); - if (!room || !room.appId || !room.closeSessionRequest) { - console.log(`[websocketService] Cannot process signature - room ${roomId} not found or no close session request`); - return; - } - - // Store the signature - room.closeSessionSignatures.set(playerId, signature); - console.log(`[websocketService] Received signature from player ${playerId} for room ${roomId}`); - - // Check if we have all required signatures - if (room.closeSessionSignatures.size === room.players.size) { - try { - room.isClosingAppSession = true; - console.log(`[websocketService] All player signatures received, closing app session ${room.appId}`); - - // Get server signature - const serverSignature = await serverSigner(room.closeSessionRequest); - console.log(`[websocketService] Server signature created for room ${roomId}`); - - // Combine all signatures - const allSignatures = [ - ...Array.from(room.closeSessionSignatures.values()), - serverSignature - ]; - - // Create final message with all signatures - const finalMessage = { - req: room.closeSessionRequest.req, - sig: allSignatures - }; - - console.log(`[websocketService] Sending final close request to broker for room ${roomId}`); - await sendToBroker(finalMessage); - console.log(`[websocketService] App session ${room.appId} closed successfully`); - - // Notify all players that the session is closed - webSocketServer.clients.forEach(client => { - const snakeClient = client as SnakeWebSocket; - if (snakeClient.roomId === roomId && snakeClient.readyState === WebSocket.OPEN) { - snakeClient.send(JSON.stringify({ - type: 'appSession:closed' - })); - } - }); - - // Clean up room state - room.appId = undefined; - room.closeSessionRequest = undefined; - room.closeSessionSignatures.clear(); - } catch (error) { - console.error(`[websocketService] Error closing app session ${room.appId}:`, error); - } finally { - room.isClosingAppSession = false; - } - } else { - console.log(`[websocketService] Waiting for more signatures for room ${roomId} (${room.closeSessionSignatures.size}/${room.players.size})`); - } -} - -// Handle client disconnect -async function handleDisconnect(ws: SnakeWebSocket): Promise { - console.log(`[websocketService] Client disconnected: ${ws.playerId}`); - - // Find the room this player was in - const roomId = ws.roomId; - if (!roomId) { - console.log(`[websocketService] No room found for disconnected player ${ws.playerId}`); - return; - } - - const room = getRoom(roomId); - if (!room) { - console.log(`[websocketService] Room ${roomId} not found for disconnected player ${ws.playerId}`); - return; - } - - // Get player info before removing them - const disconnectedPlayer = room.players.get(ws.playerId); - const playerNickname = disconnectedPlayer?.nickname || 'Unknown player'; - - // Remove the player from the room - room.players.delete(ws.playerId); - console.log(`[websocketService] Removed player ${ws.playerId} from room ${roomId}`); - - // Remove player's vote if they had one - if (room.playAgainVotes) { - room.playAgainVotes.delete(ws.playerId); - } - - // Notify remaining players about the disconnect - if (room.players.size > 0) { - broadcastPlayerDisconnect(roomId, playerNickname); - broadcastRoomUpdate(roomId); - } - - // If room is empty and this wasn't an intentional disconnect, clean up - if (room.players.size === 0 && ws.readyState === WebSocket.CLOSED) { - console.log(`[websocketService] Room ${roomId} is empty, cleaning up`); - - // Stop the game interval if it's running - if (room.gameInterval) { - clearInterval(room.gameInterval); - room.gameInterval = null; - } - - // Mark game as over - room.isGameOver = true; - - // Close the app session if not already being closed - if (room.appId && !room.isClosingAppSession) { - try { - room.isClosingAppSession = true; - console.log(`[websocketService] Closing app session ${room.appId} for room ${roomId}`); - const players = Array.from(room.players.values()); - await closeAppSession( - room.appId, - room.playerAddresses.get(players[0].id) as Hex, - room.playerAddresses.get(players[1].id) as Hex); - console.log(`[websocketService] App session ${room.appId} closed successfully`); - room.appId = undefined; // Clear the app ID after successful closure - } catch (error) { - console.error(`[websocketService] Error closing app session ${room.appId}:`, error); - // Don't clear the app ID on error - it might still be valid - } finally { - room.isClosingAppSession = false; - } - } - - // Remove the room - removeRoom(roomId); - console.log(`[websocketService] Room ${roomId} removed`); - - // Notify subscribers about room removal - broadcastRoomRemoved(roomId); - } -} - -// Handle room subscription -function handleSubscribeRooms(ws: SnakeWebSocket): void { - console.log(`[websocketService] Client ${ws.playerId} subscribing to room updates`); - roomSubscribers.add(ws); - - // Send current rooms list - const availableRooms = getAvailableRoomsList(); - ws.send(JSON.stringify({ - type: 'roomsList', - rooms: availableRooms - })); -} - -// Handle room unsubscription -function handleUnsubscribeRooms(ws: SnakeWebSocket): void { - console.log(`[websocketService] Client ${ws.playerId} unsubscribing from room updates`); - roomSubscribers.delete(ws); -} - -// Get list of available rooms (not full and not in active game) -function getAvailableRoomsList(): Array { - const allRooms = getAllRooms(); - const availableRooms: Array = []; - - allRooms.forEach((room, roomId) => { - // Include rooms that are not full (less than 2 players) and not in active game - if (room.players.size < 2 && !room.isGameOver) { - availableRooms.push({ - id: roomId, - name: `Room ${roomId.slice(0, 8)}`, - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - nickname: p.nickname - })), - maxPlayers: 2, - isGameActive: !!room.gameInterval && !room.isGameOver, - createdAt: room.createdAt - }); - } - }); - - return availableRooms; -} - -// Broadcast room updates to all subscribers -export function broadcastRoomUpdate(roomId: string): void { - const room = getRoom(roomId); - if (!room) return; - - const roomData = { - id: roomId, - name: `Room ${roomId.slice(0, 8)}`, - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - nickname: p.nickname - })), - maxPlayers: 2, - isGameActive: !!room.gameInterval && !room.isGameOver, - createdAt: room.createdAt - }; - - const updateMessage = JSON.stringify({ - type: 'roomUpdated', - room: roomData - }); - - // Send to all subscribers - roomSubscribers.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(updateMessage); - } - }); -} - -// Broadcast room removal to all subscribers -export function broadcastRoomRemoved(roomId: string): void { - const removeMessage = JSON.stringify({ - type: 'roomRemoved', - roomId - }); - - // Send to all subscribers - roomSubscribers.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(removeMessage); - } - }); -} - -// Handle app session signature submission from guest player -async function handleAppSessionSignature(ws: SnakeWebSocket, data: any): Promise { - const { roomId, signature, participantAddress } = data; - - if (!roomId || !signature || !participantAddress) { - ws.send(JSON.stringify({ - type: 'error', - message: 'Missing required fields for app session signature' - })); - return; - } - - const room = getRoom(roomId); - if (!room) { - ws.send(JSON.stringify({ - type: 'error', - message: 'Room not found' - })); - return; - } - - try { - // Add the signature - const allSigned = addAppSessionSignature(roomId, participantAddress as Hex, signature); - if (!allSigned) { - // Send confirmation to the player who just signed - ws.send(JSON.stringify({ - type: 'appSession:signatureConfirmed', - roomId - })); - console.log(`[handleAppSessionSignature] Player ${ws.playerId} submitted signature for room ${roomId}, waiting for other player`); - return; - } - - console.log(`[handleAppSessionSignature] All signatures collected for room ${roomId}, creating app session`); - - // Create the app session with all signatures - // const appId = await createAppSessionWithSignatures(roomId); - const appId = '0x66ffb53d1b788f396dca76d79ef95a6203aba487d4d2bec1a3a7cf3c7a0a40cb'; - room.appId = appId as Hex; - - console.log(`[handleAppSessionSignature] Created app session ${appId} for room ${roomId}`); - - // Start the game - room.gameInterval = setInterval(async () => { - await gameTick(roomId); - }, 150); - - // Initial game state broadcast - const gameState = { - type: 'gameState', - players: Array.from(room.players.values()).map(p => ({ - id: p.id, - nickname: p.nickname, - segments: p.segments, - score: p.score, - isDead: p.isDead || false - })), - food: room.food, - gridSize: room.gridSize, - isGameOver: room.isGameOver || false, - stateVersion: ++room.stateVersion, - timestamp: Date.now() - }; - - // Store the current state in the room - room.currentState = gameState; - - // Broadcast to all players in the room - broadcastGameState(roomId, gameState); - - // Notify all players that the game has started - const players = Array.from(room.players.values()); - for (const player of players) { - const client = Array.from(webSocketServer.clients).find(client => { - const snakeClient = client as SnakeWebSocket; - return snakeClient.playerId === player.id && snakeClient.roomId === roomId; - }) as SnakeWebSocket; - - if (client && client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify({ - type: 'appSession:gameStarted', - roomId, - appId - })); - } - } - - } catch (error) { - console.error(`[handleAppSessionSignature] Error handling signature for room ${roomId}:`, error); - ws.send(JSON.stringify({ - type: 'error', - message: 'Failed to process signature: ' + (error instanceof Error ? error.message : 'Unknown error') - })); - } -} diff --git a/examples/snake/server/tsconfig.json b/examples/snake/server/tsconfig.json deleted file mode 100644 index 8ef8abc37..000000000 --- a/examples/snake/server/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "dist", - "skipLibCheck": true, - "strict": true, - "allowImportingTsExtensions": true, - "rewriteRelativeImportExtensions": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "resolveJsonModule": true - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] -} \ No newline at end of file diff --git a/examples/snake/server/utils/shutdown.ts b/examples/snake/server/utils/shutdown.ts deleted file mode 100644 index beb6cdb0c..000000000 --- a/examples/snake/server/utils/shutdown.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Server } from 'http'; -import { WebSocketServer } from 'ws'; -import { getAllRooms, getBrokerWebSocket } from '../services/stateService.ts'; -import { closeAppSession } from '../services/brokerService.ts'; -import { Hex } from 'viem'; - -// Handle graceful shutdown of the server -export async function gracefulShutdown(server: Server, wss: WebSocketServer, signal: string = 'SIGTERM'): Promise { - console.log(`\n${signal} received. Shutting down gracefully...`); - - // Close all WebSocket connections - console.log('Closing WebSocket connections...'); - wss.clients.forEach(client => { - client.close(1000, 'Server shutting down'); - }); - - // Close app sessions for all active rooms - console.log('Closing app sessions...'); - const closePromises: Promise[] = []; - const rooms = getAllRooms(); - - for (const room of rooms.values()) { - // Skip if no app session or not game over - if (!room.appId || room.channelIds.size === 0) continue; - // Mark all rooms as game over - room.isGameOver = true; - const players = Array.from(room.players.values()); - closePromises.push(closeAppSession( - room.appId, - room.playerAddresses.get(players[0].id) as Hex, - room.playerAddresses.get(players[1].id) as Hex)); - } - - try { - // Wait for all app sessions to be closed - if (closePromises.length > 0) { - console.log(`Closing ${closePromises.length} app sessions...`); - await Promise.all(closePromises); - console.log('All app sessions closed successfully'); - } else { - console.log('No active app sessions to close'); - } - } catch (error) { - console.error('Error closing app sessions:', error); - } - - // Close broker connection - const brokerWs = getBrokerWebSocket(); - if (brokerWs) { - console.log('Closing broker connection...'); - brokerWs.close(); - } - - // Close the HTTP server - console.log('Closing HTTP server...'); - server.close(() => { - console.log('HTTP server closed'); - process.exit(0); - }); - - // If server doesn't close in 5 seconds, force exit - setTimeout(() => { - console.error('Could not close connections in time, forcefully shutting down'); - process.exit(1); - }, 5000); -} diff --git a/examples/snake/tsconfig.json b/examples/snake/tsconfig.json deleted file mode 100644 index 5991839a4..000000000 --- a/examples/snake/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" }, - { "path": "./tsconfig.server.json" } - ] -} diff --git a/examples/tictactoe/Readme.md b/examples/tictactoe/Readme.md deleted file mode 100644 index 01f68baf4..000000000 --- a/examples/tictactoe/Readme.md +++ /dev/null @@ -1,54 +0,0 @@ -# Tic Tac Toe with Nitrolite - -This example demonstrates a multiplayer Tic Tac Toe game using Nitrolite for state channels. - -## Structure - -- `/client` - React frontend application -- `/server` - WebSocket server for game coordination - -## Prerequisites - -- Node.js >= 20.0.0 -- npm or yarn - -## Quick Start - -1. Install dependencies and start the server: - -```bash -# Start the server -cd server -npm install -npm run dev -``` - -2. In a new terminal, start the client: - -```bash -# Start the client -cd client -npm install -npm run dev -``` - -3. Open your browser to the URL shown in the client terminal (usually http://localhost:5173) - -## Development - -- Client development server: `npm run dev` in the `/client` directory -- Server development with auto-restart: `npm run dev` in the `/server` directory - -## Building for Production - -```bash -# Build the client -cd client -npm run build - -# Start the server in production -cd ../server -npm start -``` - -See individual README files in the `/client` and `/server` directories for more details. \ No newline at end of file diff --git a/examples/tictactoe/client/.gitignore b/examples/tictactoe/client/.gitignore deleted file mode 100644 index a547bf36d..000000000 --- a/examples/tictactoe/client/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/tictactoe/client/README.md b/examples/tictactoe/client/README.md deleted file mode 100644 index 4cbfd65f5..000000000 --- a/examples/tictactoe/client/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Tic Tac Toe Client - -React-based frontend for the Tic Tac Toe game with Nitrolite integration. - -## Setup - -```bash -# Install dependencies -npm install -``` - -## Development - -```bash -# Start development server -npm run dev -``` - -The application will be available at http://localhost:5173 (or another port if 5173 is in use). - -## Build - -```bash -# Build for production -npm run build - -# Preview production build -npm run preview -``` - -The production build will be generated in the `dist` directory. - -## Environment Setup - -Create a `.env` file in the client directory with the following variables if needed: - -``` -VITE_WS_URL=ws://localhost:3000 -``` - -Adjust the WebSocket URL according to your server configuration. \ No newline at end of file diff --git a/examples/tictactoe/client/eslint.config.js b/examples/tictactoe/client/eslint.config.js deleted file mode 100644 index dd5edae27..000000000 --- a/examples/tictactoe/client/eslint.config.js +++ /dev/null @@ -1,29 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - '@typescript-eslint/ban-ts-comment': 'off', - }, - }, -) diff --git a/examples/tictactoe/client/index.html b/examples/tictactoe/client/index.html deleted file mode 100644 index 253da86d6..000000000 --- a/examples/tictactoe/client/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - Nitro Aura | Light speed, neon bleed - - - - - - -
- - - diff --git a/examples/tictactoe/client/package-lock.json b/examples/tictactoe/client/package-lock.json deleted file mode 100644 index cfc697d96..000000000 --- a/examples/tictactoe/client/package-lock.json +++ /dev/null @@ -1,11197 +0,0 @@ -{ - "name": "nitro-aura", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nitro-aura", - "version": "0.0.0", - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "@privy-io/cross-app-connect": "0.2.1", - "@radix-ui/react-dialog": "1.1.13", - "@radix-ui/react-label": "2.1.6", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-tabs": "1.1.11", - "@rainbow-me/rainbowkit": "2.2.6", - "@tailwindcss/vite": "4.1.6", - "@tanstack/react-query": "5.80.6", - "@types/uuid": "10.0.0", - "ethers": "6.7.1", - "lucide-react": "0.510.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "uuid": "11.1.0", - "viem": "^2.31.0", - "wagmi": "2.15.6" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@types/ws": "8.18.1", - "@vitejs/plugin-react-swc": "^3.9.0", - "autoprefixer": "10.4.21", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "postcss": "8.5.3", - "tailwind-merge": "3.3.0", - "tailwindcss": "3.4.17", - "tailwindcss-animate": "1.0.7", - "typescript": "~5.8.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@coinbase/wallet-sdk": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.3.tgz", - "integrity": "sha512-h8gMLQNvP5TIJVXFOyQZaxbi1Mg5alFR4Z2/PEIngdyXZEoQGcVhzyQGuDa3t9zpllxvqfAaKfzDhsfCo+nhSQ==", - "license": "Apache-2.0", - "dependencies": { - "@noble/hashes": "^1.4.0", - "clsx": "^1.2.1", - "eventemitter3": "^5.0.1", - "preact": "^10.24.2" - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@ecies/ciphers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.3.tgz", - "integrity": "sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA==", - "license": "MIT", - "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" - }, - "peerDependencies": { - "@noble/ciphers": "^1.0.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@erc7824/nitrolite": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@erc7824/nitrolite/-/nitrolite-0.2.20.tgz", - "integrity": "sha512-iYzgHh6Acd3Pq1tjQ+yzEiZQh68xHZ4P+r5UMrweqBMU93Fr39pHEYe/PkdAGppREOHoXvuIDDp6S7ORTxfkug==", - "license": "MIT", - "dependencies": { - "abitype": "^0.10.3", - "viem": "^2.5.0", - "zod": "^3.25.67" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@ethereumjs/common": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", - "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", - "license": "MIT", - "dependencies": { - "@ethereumjs/util": "^8.1.0", - "crc-32": "^1.2.0" - } - }, - "node_modules/@ethereumjs/rlp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", - "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/tx": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", - "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/common": "^3.2.0", - "@ethereumjs/rlp": "^4.0.1", - "@ethereumjs/util": "^8.1.0", - "ethereum-cryptography": "^2.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/util": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", - "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "ethereum-cryptography": "^2.0.0", - "micro-ftch": "^0.3.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", - "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz", - "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.2.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz", - "integrity": "sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==", - "dependencies": { - "@metamask/json-rpc-engine": "^7.0.0", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz", - "integrity": "sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==", - "license": "ISC", - "dependencies": { - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine/node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", - "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.1.2", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "semver": "^7.3.8", - "superstruct": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/json-rpc-engine": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz", - "integrity": "sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==", - "license": "ISC", - "dependencies": { - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-middleware-stream": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.2.tgz", - "integrity": "sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==", - "license": "ISC", - "dependencies": { - "@metamask/json-rpc-engine": "^8.0.2", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0", - "readable-stream": "^3.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/object-multiplex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-2.1.0.tgz", - "integrity": "sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==", - "license": "ISC", - "dependencies": { - "once": "^1.4.0", - "readable-stream": "^3.6.2" - }, - "engines": { - "node": "^16.20 || ^18.16 || >=20" - } - }, - "node_modules/@metamask/onboarding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@metamask/onboarding/-/onboarding-1.0.1.tgz", - "integrity": "sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==", - "license": "MIT", - "dependencies": { - "bowser": "^2.9.0" - } - }, - "node_modules/@metamask/providers": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@metamask/providers/-/providers-16.1.0.tgz", - "integrity": "sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==", - "license": "MIT", - "dependencies": { - "@metamask/json-rpc-engine": "^8.0.1", - "@metamask/json-rpc-middleware-stream": "^7.0.1", - "@metamask/object-multiplex": "^2.0.0", - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.1.1", - "@metamask/utils": "^8.3.0", - "detect-browser": "^5.2.0", - "extension-port-stream": "^3.0.0", - "fast-deep-equal": "^3.1.3", - "is-stream": "^2.0.0", - "readable-stream": "^3.6.2", - "webextension-polyfill": "^0.10.0" - }, - "engines": { - "node": "^18.18 || >=20" - } - }, - "node_modules/@metamask/rpc-errors": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", - "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", - "license": "MIT", - "dependencies": { - "@metamask/utils": "^9.0.0", - "fast-safe-stringify": "^2.0.6" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.1.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/rpc-errors/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/rpc-errors/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/safe-event-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.2.tgz", - "integrity": "sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==", - "license": "ISC", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@metamask/sdk": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.32.0.tgz", - "integrity": "sha512-WmGAlP1oBuD9hk4CsdlG1WJFuPtYJY+dnTHJMeCyohTWD2GgkcLMUUuvu9lO1/NVzuOoSi1OrnjbuY1O/1NZ1g==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@metamask/onboarding": "^1.0.1", - "@metamask/providers": "16.1.0", - "@metamask/sdk-communication-layer": "0.32.0", - "@metamask/sdk-install-modal-web": "0.32.0", - "@paulmillr/qr": "^0.2.1", - "bowser": "^2.9.0", - "cross-fetch": "^4.0.0", - "debug": "^4.3.4", - "eciesjs": "^0.4.11", - "eth-rpc-errors": "^4.0.3", - "eventemitter2": "^6.4.9", - "obj-multiplex": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^3.6.2", - "socket.io-client": "^4.5.1", - "tslib": "^2.6.0", - "util": "^0.12.4", - "uuid": "^8.3.2" - } - }, - "node_modules/@metamask/sdk-communication-layer": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.32.0.tgz", - "integrity": "sha512-dmj/KFjMi1fsdZGIOtbhxdg3amxhKL/A5BqSU4uh/SyDKPub/OT+x5pX8bGjpTL1WPWY/Q0OIlvFyX3VWnT06Q==", - "dependencies": { - "bufferutil": "^4.0.8", - "date-fns": "^2.29.3", - "debug": "^4.3.4", - "utf-8-validate": "^5.0.2", - "uuid": "^8.3.2" - }, - "peerDependencies": { - "cross-fetch": "^4.0.0", - "eciesjs": "*", - "eventemitter2": "^6.4.9", - "readable-stream": "^3.6.2", - "socket.io-client": "^4.5.1" - } - }, - "node_modules/@metamask/sdk-communication-layer/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/sdk-install-modal-web": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.32.0.tgz", - "integrity": "sha512-TFoktj0JgfWnQaL3yFkApqNwcaqJ+dw4xcnrJueMP3aXkSNev2Ido+WVNOg4IIMxnmOrfAC9t0UJ0u/dC9MjOQ==", - "dependencies": { - "@paulmillr/qr": "^0.2.1" - } - }, - "node_modules/@metamask/sdk/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/superstruct": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.2.1.tgz", - "integrity": "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/utils/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/utils/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz", - "integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@paulmillr/qr": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@paulmillr/qr/-/qr-0.2.1.tgz", - "integrity": "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==", - "deprecated": "The package is now available as \"qr\": npm install qr", - "license": "(MIT OR Apache-2.0)", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@privy-io/cross-app-connect": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@privy-io/cross-app-connect/-/cross-app-connect-0.2.1.tgz", - "integrity": "sha512-/2stSJrvu33ieluj1JrW5WgQZxbwvrk+vJwn7pHY8jbuSBOx+78TkiyVWErABYSOfZW5dGi2LPkJUkOaBc4HkA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^1.5.0", - "@noble/hashes": "1.3.2", - "@scure/base": "~1.1.2", - "fflate": "0.8.2" - }, - "peerDependencies": { - "@rainbow-me/rainbowkit": "^2.2.3", - "@wagmi/core": "^2.16.4", - "viem": "^2.22.23" - }, - "peerDependenciesMeta": { - "@rainbow-me/rainbowkit": { - "optional": true - }, - "@wagmi/core": { - "optional": true - } - } - }, - "node_modules/@privy-io/cross-app-connect/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@privy-io/cross-app-connect/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", - "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz", - "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.9", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.6", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.8", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", - "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", - "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz", - "integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", - "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", - "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", - "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.6", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.11.tgz", - "integrity": "sha512-4FiKSVoXqPP/KfzlB7lwwqoFV6EPwkrrqGp9cUYXjwDYHhvpnqq79P+EPHKcdoTE7Rl8w/+6s9rTlsfXHES9GA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-roving-focus": "1.1.9", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rainbow-me/rainbowkit": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.6.tgz", - "integrity": "sha512-mJhKC+5BCyC+/O2rL3q8iJDrhb58p7o4kp26yWcmY/Aeur3M5OooETcHFe/LhWR2EGbBKRq2pzM75BIlrf4/PA==", - "license": "MIT", - "dependencies": { - "@vanilla-extract/css": "1.15.5", - "@vanilla-extract/dynamic": "2.1.2", - "@vanilla-extract/sprinkles": "1.6.3", - "clsx": "2.1.1", - "qrcode": "1.5.4", - "react-remove-scroll": "2.6.2", - "ua-parser-js": "^1.0.37" - }, - "engines": { - "node": ">=12.4" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.0.0", - "react": ">=18", - "react-dom": ">=18", - "viem": "2.x", - "wagmi": "^2.9.0" - } - }, - "node_modules/@rainbow-me/rainbowkit/node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@reown/appkit": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.8.tgz", - "integrity": "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-pay": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@reown/appkit-scaffold-ui": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/types": "2.21.0", - "@walletconnect/universal-provider": "2.21.0", - "bs58": "6.0.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-common": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.7.8.tgz", - "integrity": "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==", - "license": "Apache-2.0", - "dependencies": { - "big.js": "6.2.2", - "dayjs": "1.11.13", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-controllers": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.7.8.tgz", - "integrity": "sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/universal-provider": "2.21.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-pay": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz", - "integrity": "sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "lit": "3.3.0", - "valtio": "1.13.2" - } - }, - "node_modules/@reown/appkit-polyfills": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.7.8.tgz", - "integrity": "sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==", - "license": "Apache-2.0", - "dependencies": { - "buffer": "6.0.3" - } - }, - "node_modules/@reown/appkit-scaffold-ui": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.7.8.tgz", - "integrity": "sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "lit": "3.3.0" - } - }, - "node_modules/@reown/appkit-ui": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.7.8.tgz", - "integrity": "sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "lit": "3.3.0", - "qrcode": "1.5.3" - } - }, - "node_modules/@reown/appkit-ui/node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@reown/appkit-utils": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz", - "integrity": "sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/universal-provider": "2.21.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - }, - "peerDependencies": { - "valtio": "1.13.2" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit-utils/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-wallet": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz", - "integrity": "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@walletconnect/logger": "2.1.2", - "zod": "3.22.4" - } - }, - "node_modules/@reown/appkit-wallet/node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@reown/appkit/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@reown/appkit/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@safe-global/safe-apps-provider": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz", - "integrity": "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==", - "license": "MIT", - "dependencies": { - "@safe-global/safe-apps-sdk": "^9.1.0", - "events": "^3.3.0" - } - }, - "node_modules/@safe-global/safe-apps-sdk": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz", - "integrity": "sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==", - "license": "MIT", - "dependencies": { - "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", - "viem": "^2.1.1" - } - }, - "node_modules/@safe-global/safe-gateway-typescript-sdk": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.23.1.tgz", - "integrity": "sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@swc/core": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", - "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.21" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.24", - "@swc/core-darwin-x64": "1.11.24", - "@swc/core-linux-arm-gnueabihf": "1.11.24", - "@swc/core-linux-arm64-gnu": "1.11.24", - "@swc/core-linux-arm64-musl": "1.11.24", - "@swc/core-linux-x64-gnu": "1.11.24", - "@swc/core-linux-x64-musl": "1.11.24", - "@swc/core-win32-arm64-msvc": "1.11.24", - "@swc/core-win32-ia32-msvc": "1.11.24", - "@swc/core-win32-x64-msvc": "1.11.24" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", - "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", - "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", - "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", - "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", - "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", - "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", - "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", - "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", - "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.24", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", - "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", - "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", - "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.29.2", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.6" - } - }, - "node_modules/@tailwindcss/node/node_modules/tailwindcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", - "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", - "license": "MIT" - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", - "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-x64": "4.1.6", - "@tailwindcss/oxide-freebsd-x64": "4.1.6", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-x64-musl": "4.1.6", - "@tailwindcss/oxide-wasm32-wasi": "4.1.6", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", - "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", - "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", - "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", - "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", - "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", - "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", - "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", - "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", - "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", - "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", - "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", - "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz", - "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.6", - "@tailwindcss/oxide": "4.1.6", - "tailwindcss": "4.1.6" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6" - } - }, - "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", - "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", - "license": "MIT" - }, - "node_modules/@tanstack/query-core": { - "version": "5.80.6", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.6.tgz", - "integrity": "sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.80.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.6.tgz", - "integrity": "sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.80.6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.4.tgz", - "integrity": "sha512-WxYAszDYgsMV31OVyoG4jbAgJI1Gw0Xq9V19zwhy6+hUUJlJIdZ3r/cbdmTqFv++SktQkZ/X+46yGFxp5XJBEg==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vanilla-extract/css": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.5.tgz", - "integrity": "sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@vanilla-extract/private": "^1.0.6", - "css-what": "^6.1.0", - "cssesc": "^3.0.0", - "csstype": "^3.0.7", - "dedent": "^1.5.3", - "deep-object-diff": "^1.1.9", - "deepmerge": "^4.2.2", - "lru-cache": "^10.4.3", - "media-query-parser": "^2.0.2", - "modern-ahocorasick": "^1.0.0", - "picocolors": "^1.0.0" - } - }, - "node_modules/@vanilla-extract/dynamic": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.2.tgz", - "integrity": "sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A==", - "license": "MIT", - "dependencies": { - "@vanilla-extract/private": "^1.0.6" - } - }, - "node_modules/@vanilla-extract/private": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.8.tgz", - "integrity": "sha512-oRAbUlq1SyTWCo7dQnTVm+xgJMqNl8K1dEempQHXzQvUuyEfBabMt0wNGf+VCHzvKbx/Bzr9p/2wy8WA9+2z2g==", - "license": "MIT" - }, - "node_modules/@vanilla-extract/sprinkles": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.3.tgz", - "integrity": "sha512-oCHlQeYOBIJIA2yWy2GnY5wE2A7hGHDyJplJo4lb+KEIBcJWRnDJDg8ywDwQS5VfWJrBBO3drzYZPFpWQjAMiQ==", - "license": "MIT", - "peerDependencies": { - "@vanilla-extract/css": "^1.0.0" - } - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", - "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@swc/core": "^1.11.21" - }, - "peerDependencies": { - "vite": "^4 || ^5 || ^6" - } - }, - "node_modules/@wagmi/connectors": { - "version": "5.8.5", - "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-5.8.5.tgz", - "integrity": "sha512-CHh4uYP6MziCMlSVXmuAv7wMoYWdxXliuzwCRAxHNNkgXE7z37ez5XzJu0Sm39NUau3Fl8WSjwKo4a4w9BOYNA==", - "license": "MIT", - "dependencies": { - "@coinbase/wallet-sdk": "4.3.3", - "@metamask/sdk": "0.32.0", - "@safe-global/safe-apps-provider": "0.18.6", - "@safe-global/safe-apps-sdk": "9.1.0", - "@walletconnect/ethereum-provider": "2.21.1", - "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@wagmi/core": "2.17.3", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wagmi/core": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.17.3.tgz", - "integrity": "sha512-fgZR9fAiCFtGaosTspkTx5lidccq9Z5xRWOk1HG0VfB6euQGw2//Db7upiP4uQ7DPst2YS9yQN2A1m9+iJLYCw==", - "license": "MIT", - "dependencies": { - "eventemitter3": "5.0.1", - "mipd": "0.0.7", - "zustand": "5.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/query-core": ">=5.0.0", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "@tanstack/query-core": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/core": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.1.tgz", - "integrity": "sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@walletconnect/environment": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", - "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/environment/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/ethereum-provider": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/ethereum-provider/-/ethereum-provider-2.21.1.tgz", - "integrity": "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit": "1.7.8", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/sign-client": "2.21.1", - "@walletconnect/types": "2.21.1", - "@walletconnect/universal-provider": "2.21.1", - "@walletconnect/utils": "2.21.1", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", - "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", - "license": "MIT", - "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/events/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/heartbeat": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", - "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", - "license": "MIT", - "dependencies": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-http-connection": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", - "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.1", - "cross-fetch": "^3.1.4", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-http-connection/node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/@walletconnect/jsonrpc-provider": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", - "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.8", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-types": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", - "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0", - "keyvaluestorage-interface": "^1.0.0" - } - }, - "node_modules/@walletconnect/jsonrpc-utils": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", - "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", - "license": "MIT", - "dependencies": { - "@walletconnect/environment": "^1.0.1", - "@walletconnect/jsonrpc-types": "^1.0.3", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-ws-connection": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", - "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0", - "ws": "^7.5.1" - } - }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@walletconnect/logger": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.2.tgz", - "integrity": "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "7.11.0" - } - }, - "node_modules/@walletconnect/relay-api": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", - "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-types": "^1.0.2" - } - }, - "node_modules/@walletconnect/relay-auth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", - "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.0", - "@noble/hashes": "1.7.0", - "@walletconnect/safe-json": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "uint8arrays": "^3.0.0" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", - "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/safe-json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", - "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/safe-json/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/sign-client": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.1.tgz", - "integrity": "sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.1", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", - "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/time/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/types": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.1.tgz", - "integrity": "sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/universal-provider": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.1.tgz", - "integrity": "sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.1", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/utils": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.1.tgz", - "integrity": "sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@walletconnect/utils/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@walletconnect/utils/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@walletconnect/utils/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/window-getters": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", - "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-getters/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/window-metadata": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", - "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", - "license": "MIT", - "dependencies": { - "@walletconnect/window-getters": "^1.0.1", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-metadata/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/abitype": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.3.tgz", - "integrity": "sha512-tRN+7XIa7J9xugdbRzFv/95ka5ivR/sRe01eiWvM0HWWjHuigSZEACgKa0sj4wGuekTDtghCx+5Izk/cOi78pQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/async-mutex": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz", - "integrity": "sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/cbw-sdk": { - "name": "@coinbase/wallet-sdk", - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-3.9.3.tgz", - "integrity": "sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==", - "license": "Apache-2.0", - "dependencies": { - "bn.js": "^5.2.1", - "buffer": "^6.0.3", - "clsx": "^1.2.1", - "eth-block-tracker": "^7.1.0", - "eth-json-rpc-filters": "^6.0.0", - "eventemitter3": "^5.0.1", - "keccak": "^3.0.3", - "preact": "^10.16.0", - "sha.js": "^2.4.11" - } - }, - "node_modules/cbw-sdk/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-es": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", - "license": "MIT" - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/cross-fetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", - "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-object-diff": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/derive-valtio": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz", - "integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==", - "license": "MIT", - "peerDependencies": { - "valtio": "*" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "license": "MIT" - }, - "node_modules/detect-browser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", - "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eciesjs": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.15.tgz", - "integrity": "sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==", - "license": "MIT", - "dependencies": { - "@ecies/ciphers": "^0.2.3", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.1", - "@noble/hashes": "^1.8.0" - }, - "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" - } - }, - "node_modules/eciesjs/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.152", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz", - "integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-toolkit": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.33.0.tgz", - "integrity": "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", - "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.13.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.26.0", - "@eslint/plugin-kit": "^0.2.8", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@modelcontextprotocol/sdk": "^1.8.0", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "zod": "^3.24.2" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eth-block-tracker": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-7.1.0.tgz", - "integrity": "sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==", - "license": "MIT", - "dependencies": { - "@metamask/eth-json-rpc-provider": "^1.0.0", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^5.0.1", - "json-rpc-random-id": "^1.0.1", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-block-tracker/node_modules/@metamask/utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", - "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.1.2", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "semver": "^7.3.8", - "superstruct": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-block-tracker/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-json-rpc-filters": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-6.0.1.tgz", - "integrity": "sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==", - "license": "ISC", - "dependencies": { - "@metamask/safe-event-emitter": "^3.0.0", - "async-mutex": "^0.2.6", - "eth-query": "^2.1.2", - "json-rpc-engine": "^6.1.0", - "pify": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-json-rpc-filters/node_modules/pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eth-query": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz", - "integrity": "sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==", - "license": "ISC", - "dependencies": { - "json-rpc-random-id": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "node_modules/eth-rpc-errors": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz", - "integrity": "sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==", - "license": "MIT", - "dependencies": { - "fast-safe-stringify": "^2.0.6" - } - }, - "node_modules/ethereum-cryptography": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", - "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethers": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", - "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.9.2", - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.7.1", - "@types/node": "18.15.13", - "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, - "node_modules/eventemitter2": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", - "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/extension-port-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-3.0.0.tgz", - "integrity": "sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==", - "license": "ISC", - "dependencies": { - "readable-stream": "^3.6.2 || ^4.4.2", - "webextension-polyfill": ">=0.10.0 <1.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/h3": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.3.tgz", - "integrity": "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==", - "license": "MIT", - "dependencies": { - "cookie-es": "^1.2.2", - "crossws": "^0.3.4", - "defu": "^6.1.4", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.0", - "radix3": "^1.1.2", - "ufo": "^1.6.1", - "uncrypto": "^0.1.3" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", - "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isows": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", - "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-rpc-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz", - "integrity": "sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==", - "license": "ISC", - "dependencies": { - "@metamask/safe-event-emitter": "^2.0.0", - "eth-rpc-errors": "^4.0.2" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/json-rpc-engine/node_modules/@metamask/safe-event-emitter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", - "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==", - "license": "ISC" - }, - "node_modules/json-rpc-random-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", - "integrity": "sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==", - "license": "ISC" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keccak": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", - "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/keyvaluestorage-interface": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", - "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz", - "integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.2.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz", - "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/lucide-react": { - "version": "0.510.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.510.0.tgz", - "integrity": "sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-query-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", - "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micro-ftch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", - "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mipd": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", - "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "license": "MIT", - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/modern-ahocorasick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", - "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", - "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-mock-http": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", - "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/obj-multiplex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/obj-multiplex/-/obj-multiplex-1.0.0.tgz", - "integrity": "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==", - "license": "ISC", - "dependencies": { - "end-of-stream": "^1.4.0", - "once": "^1.4.0", - "readable-stream": "^2.3.3" - } - }, - "node_modules/obj-multiplex/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/obj-multiplex/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/obj-multiplex/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ofetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", - "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", - "license": "MIT", - "dependencies": { - "destr": "^2.0.3", - "node-fetch-native": "^1.6.4", - "ufo": "^1.5.4" - } - }, - "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ox": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.7.1.tgz", - "integrity": "sha512-+k9fY9PRNuAMHRFIUbiK9Nt5seYHHzSQs9Bj+iMETcGtlpS7SmBzcGSVUQO3+nqGLEiNK4598pHNFlVRaZbRsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", - "license": "MIT" - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/pony-cause": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", - "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", - "license": "0BSD", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/preact": { - "version": "10.26.8", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.8.tgz", - "integrity": "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-compare": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", - "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/superstruct": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", - "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", - "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tailwindcss/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", - "license": "MIT", - "dependencies": { - "real-require": "^0.1.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" - }, - "node_modules/uint8arrays": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", - "integrity": "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.4.2" - } - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", - "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unstorage": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.16.0.tgz", - "integrity": "sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^4.0.3", - "destr": "^2.0.5", - "h3": "^1.15.2", - "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.6", - "ofetch": "^1.4.1", - "ufo": "^1.6.1" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6.0.3 || ^7.0.0", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/kv": "^1.0.1", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, - "node_modules/unstorage/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/unstorage/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/valtio": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", - "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", - "license": "MIT", - "dependencies": { - "derive-valtio": "0.1.0", - "proxy-compare": "2.6.0", - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/valtio/node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/viem": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.31.0.tgz", - "integrity": "sha512-U7OMQ6yqK+bRbEIarf2vqxL7unSEQvNxvML/1zG7suAmKuJmipqdVTVJGKBCJiYsm/EremyO2FS4dHIPpGv+eA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.0.8", - "isows": "1.0.7", - "ox": "0.7.1", - "ws": "8.18.2" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/wagmi": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.15.6.tgz", - "integrity": "sha512-tR4tm+7eE0UloQe1oi4hUIjIDyjv5ImQlzq/QcvvfJYWF/EquTfGrmht6+nTYGCIeSzeEvbK90KgWyNqa+HD7Q==", - "license": "MIT", - "dependencies": { - "@wagmi/connectors": "5.8.5", - "@wagmi/core": "2.17.3", - "use-sync-external-store": "1.4.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.0.0", - "react": ">=18", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/webextension-polyfill": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", - "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", - "license": "MPL-2.0" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "devOptional": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, - "node_modules/zustand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", - "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - } - } -} diff --git a/examples/tictactoe/client/package.json b/examples/tictactoe/client/package.json deleted file mode 100644 index b5c1072d9..000000000 --- a/examples/tictactoe/client/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "nitro-aura", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "@privy-io/cross-app-connect": "0.2.1", - "@radix-ui/react-dialog": "1.1.13", - "@radix-ui/react-label": "2.1.6", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-tabs": "1.1.11", - "@rainbow-me/rainbowkit": "2.2.6", - "@tailwindcss/vite": "4.1.6", - "@tanstack/react-query": "5.80.6", - "@types/uuid": "10.0.0", - "ethers": "6.7.1", - "lucide-react": "0.510.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "uuid": "11.1.0", - "viem": "^2.31.0", - "wagmi": "2.15.6" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@types/ws": "8.18.1", - "@vitejs/plugin-react-swc": "^3.9.0", - "autoprefixer": "10.4.21", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "postcss": "8.5.3", - "tailwind-merge": "3.3.0", - "tailwindcss": "3.4.17", - "tailwindcss-animate": "1.0.7", - "typescript": "~5.8.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/examples/tictactoe/client/postcss.config.js b/examples/tictactoe/client/postcss.config.js deleted file mode 100644 index e99ebc2c0..000000000 --- a/examples/tictactoe/client/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/examples/tictactoe/client/public/android-chrome-192x192.png b/examples/tictactoe/client/public/android-chrome-192x192.png deleted file mode 100644 index 64c9057b6dac6dc992e3ee97de1fa47696ab4862..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28617 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

W@PD6#$n{YfuS>7LH6rKszp1LK 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

?`XX{HmApSZ^%vQ{ja4gMT09yY+hzPiXa0v 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�OU#yJF=2%0WgAX3+%TJ_2F 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
(*ZFy>galVcqg=etfTi)=z5Pw!CJc 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 diff --git a/examples/tictactoe/client/public/banner_v2.webp b/examples/tictactoe/client/public/banner_v2.webp deleted file mode 100644 index 146278a7c9503503921b5054e2a9f108c42b96c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37730 zcmZ6yV{~OrxGoyIW81cqj@?1Wwr$%<$F^;oE4J0KZ97?~zkTmMKhB?b))=#D%;NiC z%u@O#A)%E70-`Q1qM)Y0P5k>`*`W|D2b2~B0t7-pkvc)5sIZ`bB=Ob50S?yOZZiWo zU6vASHIVPt4EO!e|AF=wkOutrGx9o}%K_0{d%eR7W%7&T{aXF7efHn|a`qoQp6&Ku zcpy0yeA_t$FnzoF&j91TWj@@8{m*tf0C_-R!Ajs0u;=~4Z`KFwYZ{pL!FJ2_UiOmr zMYPAi3q1bb^d9(r_)dCG`z9J=dWgH%fAq!?90D!@f!91=bf5k}fTjKipk?RbTN6+R z9Q+!4CH#s3F24bRz*BG_pd1>A$nO95MoCcpTkuox$ozV?{#%+4_!0LCda*undf{GRZ?o#fGNRFfBUbt@4Jt{>yNL|4f#yYy>Gx5;GM`o-^kDR+xSEM&HjC7rJJ2d^u7VZfr6upSAZ+Pxqs~E;0M^Z;g|K-$@9&F zV2dD;{+WL)P!QPl<^FF}KoWo`5E3Z)N%hY34Y=?JeuD#PzKgyuKHA>=zX0a|An@s< z5x91Z^*jqS?`XRy`<|Ws%8^T1Y;;m{8jqD{Sd;*%fv-75ASirg>{0SXh5*0v4 zI>mdtx%%O7Nl+A^a8=XN+Pbp7AyR@MlO$^f*H8MrR1g@XcX5sM$`&J9?-sS&-HW1O z{X^s(m5!UwP;aR?%qpgEvlqT*^6c*`wcEe8+Saz#0T}zujBQM{5Yf;rtt|l9qSlp8 zP%tY6fE|nex&Hb)9i%}uMv$yy+49j*I^*JdGa~?N0rD5ZGM@Cm!HM7M+Ka2dXH-97 zY?2n(3mJ0WZ>tK-%tp@jpwpsV6q1yub%wmVepN{2?zAEHuQLT*IK8`Jv@qQY7Ie~9 z0+qpN&&lQgeSBb3BqS83Lj1wbA%A8|B;Y{!#pLqeoH23FiA&=iSH_5~V^HfoH~zT( zH)5QE!$^<9?WKZBER&-Z6Zo2`xyzA{N+nSaiG>)kw%yDx@W{I7t(%MSC;{->N{r34KbaRyTpT0Qgi3=Cxf0 zL<#t;eeUAGLr`u5zQIwCvS65vtUyN<%O?*zU3(#eYvyH9sd)B2s9}_lYG|(nw*KVa zTI=I9b@30?A2bdzhK9cKw)|0>A#y9%Z2LqjH6UCCT1-x(*jt7*3kb%Yu z{JY9KbQ(Fxv%9p^q&pjeQUw}6(@_A+xmZLq9F5d4+c5$aT5< zD@JGtY#IUf09c(RsCA5fh{}ZmGJG+$z*)5~J#OK6**w?K#M*5hLmYX|K=qDS>%^@( zugzws@%kzw*ZuP`S+JC}OcUf>R~T>O4aa#NeAz4-St|Ec4GT$jn$X!al0!;f`3}I= zyK~E8o;UP@>U~o>lwT1>rCy0lA@PxgtP>;fjjuc4h38wfH}K^eRBsPC@Ani<0D50v zZ=^ZXDA0yv=G0Dtbt>Znr`nF8L_^YvUhJZ6va;y1PKMU9erTF0)gEzq4cyR`R(svsj;$en!U zp5wV#{W``)%4W2N#!_zTyUaZkegjK`_9*9Sxl!mAudnUg0PA*lu3XK*Efy3kD8%S& zF?remXV3QBBJb?|FS>tiVPwKlrsjduRjPiMA)j~i=~dhDx%TQ)X7=hBf~Q2S@r#)# zR~Atw#I2`i<{G1hg;&k=gqtfWUqaIYo~Y?ST3J`O2f(b5?Y145pdZJX!4=>0F;G)` zvfkjQc2rti>3bbB9#rL|;v#pcgNY530t-#^rJr+bvA@Jm9Q{m*mP*;fJVrtzZ#-Nz zb zwM)w=f$HItT_04>7UF52wn4Cjos$+GSw@#_-A-9itn~aKiJ-?*uovZa8Fe)?ru#8_ zG!S5;0{~Ud!SGdRl3KI@DTT%@0d|)B&=8LI?Me7m6tB!v{=#kS5bGu)g-mM&~c)nNQN?O$Hc^0cSTQx8AC(^3W4%~>?K z8EgBjB$aAjv5O2O*3lt;L4kIYIp>m!C5aK4R?#p98NJ4MH=UOstF9QVRDTp=Go?(C z`Cn_7_ofLEYxPVthJHgoVidTxay$?tB4$$$0!2bn!z4u%>%zRDzr273$H#(bg<|MD z;EU_pxXBn&HRBD32Mjxb=8_otEeoYJE4PLo=pEF9+xt5IQn?Q2IeRO;vqhxN9z2kr zM?0DzYiY=aXTpk6)8EW>d*|gW&P@6qc|Z5Z!sURyNbYc-cJ);IgR@tQ_-Z5edYz|cS| ze^|=n9FJnWSC{Ku`EWL4p}0ANc9yi7vHSUYmxM30qHP64GoAe(72SzcDf5WB{^_lB zF>Qk~$OMx*tz)ylkwe%&!sn9o$2fh!FkesZj7})a<)Z#!-zwrxIF`OYFIHOTM~I5d zmWc09xb?KppH?`!t>2G>wfVc7P8zpYfJ~=V=VdV);w+|YyL8t3o9BC>k85l07MA>D=S$}WGa;P8B3tZ9{ zIUzbD>uHlIu8)w`z1p|9t#62_9b#Euc@zXjz<~Jtb%6xj*Wct`8+!4n!+cnu^na2` zlJ1jZrWZZjt1m|StLD2Nt557IMMT zXRY*)*k}cOv&*&!F=>sbg4E z7JSx~-1fUK3l;hu<%~YyE1&&Irlaa#g`cZEQMOEN?bb_I{=Vh5imwQ6*gZk&i~>b; z|5Tn4hJ)Sv#cIWs&2R_&pvFZuk)yBS(B)rn3khH1`P0F1!i}+?3=drKiohtD=uoGf zU|-C5x`=z4XI%nx0@4Mqh{Mq5lkTzU?$Q2HEQ$6iF1nO|i7lCh`r0qhLcfiZ759V} z^1T6P%wIoqVArmVnJNQc7?o?3V2l({0(^3J`3$lzBmg7JYE2Q10D}( z{DD2Y$3a2~xcyHDq@mw%1kJPxHLLGg&t*Ro6##BZaLOsd5k{+KDjmE##SRZL?Bo=h51h8Z-6OF|}BY4o>n>8qN5+|YhR z=^v@ARp*@!f$A7DYx(`GSgx2mOY?-Y2$@#rF}P{IDQWrhB8WWAO~h%X&K6g2^{^+_ zgd*bO#m(k^cNgDsJk(7kQX1YuU_rKY!aS97Z&gSVMDQQarSg5a)1w>sFR>QX0DTT9 zoc1B1R;zLz@x4_XoM;jlE*~KysOqnVPG3DW&V9^c#I}YKrk<4y6Uxyz|90ON%=1|z z4;g@e-p|H;D0dCx8R}HM$HQvU^X;2`q`ZtIhWS)rxJr5w6in+sc_182)yt4TZ#0Cr zk5!UPN=n>=W-eU{bFXPSML2x!YPY2i*e>avC&xpOwy%ukY4=_`Me1+rnZt<^X=l)R z&tg3{Op0QB!Bs9Kl}7Dv_;k6#K3B*YvucIx^Xs1|55+2Re9{c5I;EoessehlGnCvf`>~Az2NF1!ZBZot#zw@ z@#?G(N$TeFxWU>c^o~g*EE2JH@_-4@a=(}+zknbbE>nhZfql0aLx|{xwDyVqM9((+ zY;_j8&_SEUR*Rpm%1Q2BDS2b>Re$Z((CZYCJd2u4jJ;K@Xf-<%h<72xooSy3rvG!fEaS*|TeQ#=xduU=3ISn+U`A?5kE@@uNVn#WpTN%mU9mFaX z_l-uf!7Pc3Z1&~^JhxOEYuc((gLT!Z-&WIDL+#G!jYoFJYp~{2W_>~{i>Q9!pnUh3 z-g>^C0H0T>)Xu2Cq;YUGi@M^1PIl3ch;I>F)xdW|t=pTI7h1>!c?%4fc5xaWZ1&tc z2Z~=V4>uP&BXF1(8S{Q=n!#D4A(=#mV?O+J{7I=y!H9L^wf^=;Id5g1bV#L!gS*^C z1HZ+ZQFDYtzSrEvvpS%}ES@4WPbh8YFYT+A2i-zs^+2gi+V zKm80m_2Luw2iTi<+8?cb{<+fV%4o@iEiuIU8G7a~xa^J9vnHl0N=zq8sS}-_Jy8T_ zBDf|zXkI1GhI=g7+QGPaDNA#!B$sZ}{D{#4$yW0A8{r05x;3wi>!1ISfLER9KMe)G zjzPoHQB@?Jk1jYlhW4n}vzq;erKsB(3W~Sjsq!Ar-x<-{wX78S?;N$bYu|zIxc-wZ zcKW{oEw}7;a$v*V?W!Br+P5~oJxOqN57&^`G<&t6!4Y!w9PnCaLBK2fy%Y8}`ufuS@+;&3&0}U*e^v)>%>k;)!W$7aPXa49X18`vH|AvhZrfkaXe#ETJ%7}kbnTh@@ zUDZFe?_&>w)u>ql5;Kh0Yq`4}&$Nv-hiu6lHfaQpu39&n6pR~<-{`Vel1eY*%3U2i`Lk`e+QMPvl zO&lx;E!L-pv_q+BSbbi}9?Nz*QS)nxfPFP4?gXjKqm!CuHA5-x8OR{c9ouM2H*EH_ zGtw-h5UJVTx28_1KtmQVhj@Dq${$j5hzLRje+9ZbhFx9z3w|iHaS?8hnU$~i5QzV`ZXsi13K#iEhk6D5IsL%$Bu3{0(=^`dvEafu zuS$=4z~q$wK*lFc-av*d1F#I||63pE+~7O=OA~KvU62A2Fe@dh!xtRp^uZcdemi~0 zm7~XtkN?@6{~06li9rnsDNfbh7%po*R%~X5e>XV)zESJ9A+}m1e_xB)Ou=FmlqB8Y z(eiJXcHSJc_*&f|2Vp>X=5`|!{OTHm2WhhTF-WJ)Sd>jCLo(`c_a=l^o5knBI}ME< zG=C^8&=xxG4@_)%t@+CE^)B@hIE=~p?iSBiH~I0S}2UeKH0n1p=3 z>Ts`o3Q3SY$2IZPj(F1j(_)N{gd8$Dcw{GTaU2%EHSOXPJn+*~Y6@m8Ue%6`%bxLjG-(zKY3EF_yF% zjK`EWK`+rLR%Od{0@EfLH{W2-sFlqh#2X{=n4ML9jpEw(9!UTN2+7}o63>vP5#_I(? zKCSRxCOe_UxN3z)H=@R;h+J((>Z`5(@w~_eBC!>oHjjmEn;g&T--5*wzj6BG(nKfXqGJsI6r6^{d0)H8&!<`LsmGc`P3V9 zvwDoztV(s1MWF17%0Q(JhCN()tm0G(l7L|%C}(Xt`gU)l1!7v8jT>IOk_t$F4`y?MHAswMHr{QE4 zqRE!HNzm^AVHL(1uU)zcp6}MQ?2>dS!VvGCCY1&qD$w6K6|)3Cb6QsYG*vSHg4hcf zUy5R?!22v0c*(1jfP)tUYduf+1HYg&~Havck7E=`+KX^Zd#$$XO7qD?M^j7=vp1RCe=AZ8_P)ySFl2% zULzc2XHKP6+4v~!j~^)bXq(>q7k?pwe;z-pDi#1hb*QH8XY2f-BMmka(6~{p44Rb^ z7+1HAudYoDT{XK*R;G}2m!lY&0rM-~Is`U+IO!^C7Waq@WDcg8H*uMO1QMCbL_16D zHi^?CNt5>r2TFZdB?E!u!Gg*b7~EIis!by z@~60WQf9;vXm>(69&__R(Nn=c&RKX72?%jL&hVF^eY(7*SJUhG4xy*5te#NWGy2n4 zUvbZTnI^8#9Igu$YNUU?5#&ecG5+w~uM~};n?Y-ZgI?ZcmH+tY3p?^DC?@h%zokRRQ8}mhju%vm{WgYH|YAocC^u56wWU z;>J~EreulNdCV)PN=kPR{7O@yF&y_FVf;Urg+G9c-YL835nC74JG`>AA}*U>i2bn# zLC=4EbMR6{LlAjq;NTB}OCj;^tRO5YXy z!%eD#+f$ffXFfcxfp5wfor)!O>)dFM<1iD5)ZM5-LTSV9#Y+tpv&GiWEFy>%$yQ;c zsN>Mu5;1pQ@D!`0YNpNZ#ORfFySLF5P?D=cQHx4C90!uf^sGZvbIzqh1D=l*Vzj$U z49HCLTcquHfApJbOGBu~fSEdqmA|j}j9_dZTItYYt~gNEQ&+d3@IOtA%A1JvX()Mj z9LBxq7nktAW~+@E+-Hf}7nUiEK#CCa`bBoJ4rJVPnoGOC$iVE?$fC*rd|9p=#I%VW zug-Gi-?y=UKEcUVA^Q?;sXtRT$3)PmgnibODn~?91i!fRQ}X5~$^mCFRs6eK4F=4{ z)XFl?IR|7t*nKH_!HEn*L;)(FUF}`YI(-qmD*A|(gvB?%JU`_(GlZ(|4rfxls&9i@sm=UbD#31-%MndATl(G@J=;%9&mdPtKG<9-hOZy@AIQ%Tl!^%T& z?-5`CN^bI{|EjO6M^|ZhaHNHy06lX3(UClA3pqh$YGY^9<@t449$ZKu)*2x}?oQAo z6*q|O7KTbXc9}Et3+y#e=GDACFG4+b(E2XAFi;D`q^hedwQh_!>YF(PedR|zI^vaS3BP(H;mS<{-JY*wrtS@aQVP=ZADR4BfI?^vMIdS zt9ZdQ;Uh8&`r?4kUu%ZtSLp$vwB|>^x>t!{w5fpZZ!$dYyi(#6)B}(>MDfoEoTZo# z(i|qFdisUidSH`bjY)Q5Yc^_LZq+`%wsPU;z8)J)MQqHekHI7~_npQcyt`gksyY-K zSz^~4Jf6Zdu(ahwoEvXQ-Yr19jj^rIb;c6RF?s{~#|uU*s_$0#8s?xzF52V|)kdo! zv)Uj*eBS70F^(|Epi$n3KbH&c45DibGAPrXbImKTNjULsB;o2w2Q}T0ADt-|I3D|-fqjViGVQ@-E@Wt4C;%lTgymJVHH3j5xHcyawY#O*=(zz= z=9fs0o0LYPyb42eev9Mi8jhJRiAnu%bw<~}rKpW2AjhvUhlHGoqn^Xqd&#{(QmSAT zo5x3OvU0uHa^W;q_p9jxdvRc`Rtki!+6;$96SfyW{}mVS(ff#R60YeKD$*q&43LMh zn$MLq-4LErgFa^Mp@|sPR4AfpwmOM#Q8}7wqbm!#hPvZguYTE@bjKa<5Id&jTJzm+ z=VR1C`Xb)tNP|?Hq)L`~0Xteq!?NY)oD;LCi+f$55Df%NsltpJPw;?W0<&zX>m6wq z5F*S(j9wOG6FYU*OePQUX1qWtkBwD)fZ!K_wSWOdIlG{eRp{AFqE;J@xho!cN~PuD z=8&S%83gFb`-FoeSeUmm5ad5xi(fz}c^BCOf4YYm^Xi5$bsY}F8a8r?X95PWhkTmp zs0gAV*YIw#$Ii`-}C9s^)DEMi`+O+k{> z$mQGthEIa9hk;4OCxV*=b&t{ra_Me9kZW<9ME;8sJre~;<;#c0pwprOs==t+yUU)x z6y-3N)INLPxf5PRYSg|_5~;wC*?>EV|lf(>is~Xgq21E zx%4xg_{4l>6S)n?v7fu%hyIK~;S0cOgE}VDjSDXY9xut6*cIsZ-DVC08cJ@8$o6;0 zAgj>m-LTC;r0!5>%cCAi>=4-<=eZt+T1LNb7ejM_xq3I~q}+K>qWsqmBAx%##ApPN zMH5@gpuapA)|X&ZG8xjmTr#RKE+#oFO|(cC3xmCzj1ZLPtosHk2g4BJdZ9vg@_x1= zNNH`?O$#ZxNCDIw)UOvhH!&C0a4uCc17E6rD!o4)z=YFT2|3+5Z^_Y2u~F-MzJN0%kbx48cG=9cT~b&wI|uj# zVh_Cp!cR%!1s;cykj$u3*v3;mF1kmzW7x>r=uc}$l3xNXjxh~_I+dp?X{F2H_tRY;(MBw`&P zjLPrUHqw%t+I`+Qq*Zitn*V6oXT>Z&Q9o@kYi(ht1^+pFfIgwm!>(%7umK-hjmn4L zrDlo!qCY&be!0dt+GBQ68E)=T^wXD01u=t;@_aE`Lb>%MRlwNN=$7!Fft=H`ZeGX` z8nH#G^dEl z)t`)njtWFfzvJkVKF^h5nvtS^8Hx4CndswxTU^tw1oO-N%iMBNPjou)=Lf&Sz2yr4y z)x8Z@jbO6zjC}Mw4ZackrM}fz`S)Ut`-(7r-45O8JukDutt4k8SwuMtELmc<`5IO5 zOt>70KCiRu*UgE-GCHfS+EMPB9(!~erc45`g0==&M@w2SoY4f=1j4{VCL zch)rUN9%ERp_wx{VzVkPjZah4wlLhuB;t?0z3;R#ORF<oiWSs zfz-ESi}_P5_#1X(#E+uS*f%evKLT{N-m-5l5Y`hfo2Zu6wea{9Z8Q$lf1x+}FqL&< z1`{p3v1_DyJRXYse{={~@_;qRe9znTqWxjs7xe=^Wef$45Er#fb&+lVTE-}hfM)Tt zau+D6T%~bS(P$bY8;n_*LiN?Dd1aQ%&CE?7Q!*WNyFe{?zsbctm#?m>dW(N_pwjCE}S6Nr}zURQ2-Sox%^&qTSyirE!2uTcd!<-wC z26I!haGfG$LFj)2jTzLLZx4j=MivksB7!*7S9a7NcS3@F7?Y)pvxEALg>`dfNa^QH zajWd4JiTcC0SXzvo%!8H1kJT+jsf;0X+CY_JmTNtY`0%TORftoPP8Ho_jZP z0lHb872}$nuw@;LJ|IJ5;}L-^P}!Hd(ZXuWsrZ8TW%{WT+}#Vf&z}hAiqD0ns&0*b zuz6g(K)U9iy-6fWeD?D z4%5b%UA04YOT8DPU*l*BGA~exUPf;H{V-BuRYtx^!@;-gbmt~MQx{uHjE9Pk{MD2+ zS%`9XWpR@v@Fu!A=p^c?!A-!uNKl4d6uYRDFt3Q?3qn;C>NZ&9<29#au3FAE{{%e8 zaUfL@9pBne+#mJivAQXqf^hWDljUZ+UW$qb*6A@;S##mgmkc!tTKmvbkeN-8o2SUJ z_iZ`gw*UoR?~S5W3|ha!_e!Qn{%grBGB?_C)jegWq%nzzF~YuOmjZmTqp~UaEK6vN z4m5NH+pPF(-21Yk~Pq0DkDQ^49z6}VZ#SjWY39E2d zH}moGMf?yT$W;{!j1|9wJfq{A=jXvZDbTShwZ_y6=Qs%3q|>w+eDJTdBX>aRtOfb< zGauL}H#jQ@0iv3!a4|3=k<;P>k|N$uuBD+DP!kotNGVAfAuY?8=6Jp-hu$*p623A; zasy`O`Kj1VzJSV@ghxXz&R7>uV+|&v~v^*F~XjJ*eux0_-SSRUy~N zYUM$Av1Ec9cw=gOrdn^O$o74Vsdy@5BBcTNPuXHu^}sUN7D&Pnel200$R6A-9oK?b zU0Q@mW^W6P^{0^N2ylzldgu-wb?mUFgGMQT4YAvmP>IJ3j;WqmLq?e``p@(^4Vi~!9ISL>u&yVnVsUaLqb>Ql%gXex0gVI_ z;w<`i-1RP?ukzCzx<8i~DqgAt(3g(a=Zb{_WI8zZh>X`IIHL2Q)v8X)z_Ct%4|l*z zz}F_^w-M6w%c&STkfpnQ#sIYvIsPWe%-z$N#MK6_46L4yPmRsgb6ssp?u;0P{!Cd8 z2R`*e)m`9Ng{4Rtw|r}OE9>pAyAyp?{gkdZeM0zpbgVZd-WDo3VznQ<68zd;@fLz* z)GIk5PJFd zsvTL>4Z$7mhZC|!(d^A0Aq-j_7;Iq|3eePtn4O=`Dq|d}V0lb1dc(G8&d6il*eUhM z;gKNET5g==B^nDfU@ko>*0W1zi3ABb915$$K?8Ny6dI^@lSrRnW#A75)Dry-d3>&} zxn+ebINydwDBi>yQN(~G@5bbify7r*+8%hcXl2?pCn2iwrX3qjmRKTlpUCO=T1LNL z(5L4u92I)Qv@iJCxr8d`gB}Ghw7)L}3O6BoJsRH!I_KMMQIy7z_Q@VoFRJpJ3vz-wj}eX8@}be}Kkr?|%P!shce_8T`)XMgNM^Jhbh~e56l+ zNY0{yQQ?M|?j~pL>EzR0AB#J{Yv{Qmx4&={IccmP+tJ^#lTNYn6r~K?63n;r_>pKd zZ*@dQIJqF*J5PJWRdX){WwerqNQ1s#6xpR*+OH?~Gbq85OJY>9S!_7+^lKDQ6#6q1 z&1UUQ(koztEm4y$t*ilKyGLs+IzLm_5NnFXkpspmnO~I_lL-Ns zqmX^A9gD5?WJrHx&^%_vhKTyOgW{lP; z_h6w^$@BGIessJA#VJOQI?8f3#ikR(imkD77}-YE5ZokNRMLh=oSRjo7ZBq+77JUO z=>ExqQhdT_cJDW3SSPtoa4zNWd8^k@3ozf4L@G~O8N5%BgX$;QNfL5RHk4cgyjGG& zb-6)0)kA>X%5=M^WbJq(=^_Fbc{|RM2PzqrCi`!yLPo9=S$ITv8C|VOU%1x$XU3 zr6F5NR3FTT$e{1Vc9FKMCUBLT&w-~{Y`t;1N|bqP2CVc8K*1)?^iV6Hn=7lQm7qtR z^*G@lOk#4*H4h0;^-M?m?5Vh_A=(WFFYFv8Hj|KZUN6XB5&av|Reo5Tqy-a$?Mo~( z_j1spDzSRC6BEwL{d)j(u(*4IEzkU$iEF)=;)~qrZ-!fI z2(PWGw+(4^PKpqz2|G0L*&RVqiLql4;=Sk|EbC7gV6{akNycP|$Ay*PH|H&PgU3si zBu!}!b=q=Uw9y1DG-TAq0W`ZZ)uzemPv7gGDX%%Q!U!R7NE^ ze6kudxp7zK))nUdEY}7xaS|!*U()I4u0xsh#@~@RicAX?{J|%AdS*uZ5)T<*xw^sx z;q7!aWtm8dEkM8aXh*O%HqVG((tmGEheax^IaxuTWt+`D3n3F)=kg>yLZ^{q$+U@y z)R_YGsIu|&pur0J*N9M7L@HLY&~QmB6kU7z+pbj*U8NU1q4tN7a7I5>TS-d1(Uhh9 z{^-`=*gLi-lDs+Dq^VEW$Nss(`&Fnk>7G{O_+$K+Iz!M-pyJYHiP^wYIBA;ZK#yT|CFCZ{luYExGdlkai_iAAMM0bvz4q%7NBGqm zbG>}GBi!F?P-ysjE-ik}^(|OaUb@s5d?cPcDn|(wmD^p>&`{?yc8N9VYTuoo^IKQh zModcG&uGySHC#QXQSqmNMGN2W{R}K<;XFfa+7}OS4x~9XSQ*Y-ll$WVKRdIRBb;t# zU+b2`;ZhbVTFGw|l;tGctI8C3tr{z>-9}DMQ^Bn(``_?h661!jt7##x{ocCy^-tS` z1s~^08$8=trgU61)K5S>x0Rd1aTBI9G_~}$08TK51G->$;^|H7*exc!Ju)F9^|e!F z!opUv_rqI-*!^3iGe*xnw40RRA_46*KEIK%O#1j$;xS&Xt4Gr5b-I{SkTbLt`GrB1 z&RioIe?`slJyq~R{DCQM)=}8~tH+)}F9?MzUx9(L#jYJ=th?@HN75Rp=nD`qK9QIT4>X!u<_37f8gFtK!@W;;cYT?vmJ^qII0w8day{j1xbzyd@uL4pz* zK07k#aQ?hQ=GKCN3|JK#E#hA|50PpB>PtG0s+el8de$Iyq(M}Bj%G!Ht}h5jv`&Vu z7Fg1_%D7qbv6YJ)VuptrnqaBs`q0-SkEBp;xzt5 zymQ64{H__{SO>{YjZe>uoS|{E*h{$9>3v9+K?#IOp}MII_s-pl73SpF$BN*J$U}rY zlhYjI$~?9Qd{~{lJQ4xAmheprM?zYZ?0fOJ-T{m9m@^5a6(da;=b4V(meJy5nsqa> z{4{GF=U1QgpugZcH-4VLLDw%BxJ0mvJWX3SOlku)-9qr2fXVxc1#)xBj^~DZLQI>& zo;i2Bp%>;%e{;4@ukLp(zeh&JPJU!fdW3=F zNZIBA0yzgk*hYMfCV_W1AZ!N1$O0_~s(A_mXQgORV_duxh&_W~oEAs(#S5Tck=iS5 zFz19J1)SnP%KXyY8Nyf)8EMTww843ujdcx7F^J5G4g}1hosmO+KO_p9llIHTNn99;svc`(x`dM)^ycbK~!Z_?l1tkH@k-OwHN(!ed zCA?SK$93X-BwR*&{Z|u(ekt;(`)WT&Y#k0g#ogBY9eXmx%802w-VCv|Bnm54F86`6RtVq}O3f-)a?D%xto4B9o;*J+mbITWxkul~4TL*-3g#=b9*#lD&7azv>IP zZ)Q_iL;PdZ7xAC#18zHE$m)U+T^T$NM3+KRx27m-VrJ?b4tSbDAQu`&PZ?aCcCo}R zG4Bi4VlXRW^=Ug5$~G|l(?Mdgg`O&JU(SPKlk(|)mg5B*p+xwd(8qTr;3o0PTDgJh#AVGmB0Kzqg z)2f7jL~|WL!1!Xys9k_dKWoZRdL3FJO467QqrcWtjXO_aV!us(vn`oBBD8di#x?Un zig1T~;9LV87KyJM$!ZWBE{GMIB7`Kn4hq_4Diby+*rA}bnP^m;mW#Idp#eRP2im-3bc=HPwSz?EMZnAo(nrQ!S;Mw!|>|w$1b*=p8YlRh!PwlX=z!%+8`t} z`yfk{-ue;hZnA@dpU%0Is5lYImdg;KfiEAbbg>{AFeWT@6F5m+O{N5J$UkAW`WWB5 zy@6ZfDA%3R103tiF%9(MrKzr6$@6=WAV!726iEinipf&!*{wATz8cH$feU+b!e9J? zH0lB+eUOZIcMt$vjJLR$gSOH=?|z3U)#qx{UWmr};1_YNaRnke*+;0yEUU~6sdm3! z*(y9zaWxRZl)tS|Jiy3lcr<_@U8r~^T)d7WL4p|cLETP~s;f^ugM7c@dcpmRCMUUIIQlu}2L&_Tt(u5KKH<(mw zV~x3ZG&h*^q@rP0ebmdg?0kIF{c2r4lSqw2Vot2JBOa12%yUk1whfGd6DOEUhd8mi z0mHRxs|_pKrsh#X53lHI%kqPxUV?D=Kc)qf3XMGef(V`1Oka7 zN0u~2zV|~u+a==zCZ4If5NMx|`*P|GaCcG~AKV8CXfVLu3xL1EXeJ0O!K;eHx$nZY*IgA{o|#S`Cs<|dy-U9nOt(0>$G?b4z7 zy8#v?t__Y**{cxilPj2?+zcva@$RUNu}~2!+2bb3YljrjG^HG5e)Ece8HpetsyqMv zSG-7_#?Iu{UJz^|P1g)NnRaq2&CGKDQpTAXj2=gICnkvt`Y9q;d9+$WNxyr+Bu2~& zFgoD&r&(hTxE|(AFDhW%ICw<_g0maStsT^CF}ewI&8s~l#jgJ|U3xd&;ooIbA+sYL zxEmId6?yeK5du{qFWyN}nCN@OapGG)DmoXdr4B;q{{tXE-@j7xn+34`lRjF2E)z|C zs9p-)g7A(*5Vd+&Nh#5{B}{0iE9MW%G{5toc-BihqA^lPR_AwlC$=uO_RZL!)* zpkJOZ9nKK%K(w)z95tDFv)WO$TDaKPh7QSzVs4OS!InDUWCK2;m~zz*OsOpPJ?#9^ zyFxsMcblVXRwV<&E#JI@{r_+H?t8)^?Pk?uMi8s2e)b4+ddN?{D0@=3-p4FB3Xfxp zS|C)4dO+|^biJhJA~>U)i%t+2s_O|aQsQQ_>Lcezr3eAW zWAXO8IsJeqt-?#tu@H&B0f2L>I*VSMZS$#NfO8BgP_jhCQjz^sP;9q5j6l&zEFVj( z(IhQ-tR{7ku!OvVsJE(z(%Gv{7i zSUy9;i^qRLP;Y-@+Gn#S<33~wKB8WE zC5tPD?u=GFHGI`<+7k^u5f6#;?S<5`2v>!%ZH^`Fp&|o^Gz$iAPOvU+)vEOsg+sO{ z9U%P{MEZoTE13`-sL71I=rs`EPh+^0DyybL2hJiQdv9K%mI&8Lw74^;4Q;HZ8%e!UcOQ($Y%MeTB03^IuCMc*5R%vm z8~dAtpr{xHiE2B!=F?F|fcMhd+H}E)CGyqgzd|q7T7KepC!8fT(72tL_NE0->o2`_ zTov``HasL|K90zR^C5!0`36=}kF>>~0ONI{?mEbf6mhgTshA3VbVo~bO75NcK)jzM z9Y#Tl)HmF)kW}}HFOq0IMmI7TdIpiPe+Zi|Die=y(UU;COnd!!+n)jR;n zpR+75QV$tO>##}Q=dwdzPSOf;^6D7C} zF%h29-hbRdiIT}epM2aqjFOkEAr8_HV=;8Q)S|(XGq%f8?!{zJx(=wADR`N!bG%(CA{rDLsj$%JtqMG;0gx0~`g(r-03%SZr zfr!{ZnQx8o4tWbdgVrpYr#oY*4%E>x?p!FcG%ul#t(HaF^jCt6qEc!U9gE zG0j)hRl%URl49g{Q`lU2pB4a34A_}9knC}!`z~d&K-{AWu+c6L+5MG1!m+3k$?pyh zCZVpT;dIk9DvsWx97*)p^ctIDoLmnw&MgCyZ+08%2Dx!LA+htwlPj+0d-9lQDFy4M z=&qtP()a4gprkB&Qf(MQf)lo>W)W6)G0J;WJJaV;g|U3(MtX$jJ7QD^L~%XO?*W%< z*vq*g^Ipk!i|;nzT^_fIQgyCZSstVW-%q7aj_B_qj7jc0FF4m>n7zSvSp{Xa`z3DoRFTXYlsTS{`MmrBINvU41{W%9lFWF4 zaDr!@&qYHLlaUW3YGB?SLV2JW`nCUHNCNbM)fdfrk~;$iuhosfq_vq0W?y#6xFGod zEVsg!Y4LsSNd5j@MsxbWKrpM}Im8){6rIFok~42=rRN&gPt@XMoybgk1QzUgKGu~# zJ7WV)%Rrm$-vq3YR8wCdfecfHmC%U~`3P|8a|?P*y(qz9U81u~9@JxIv9u#k%#S&@ zp9on_*h80L&quJVacr0ykmgvDU+Z>m-qHIeO^V zacR5mr#5R9cJgk#Q1n#V%S3Vobem^nYOFe;tD-+V<*RpV7E3@~V90lwF=XD@X%6#u z@;9lv|8S%)wZ2i(UcoSLNiRQVSIL|EXR@3sv;A{KwBxg_<8NYkgb{D~DmD;fiY36pwc%9d?*@PIY}x3@MoAjd6U0q)I2mE@76BiKWg zD>acfdO-qRuCbjT4Rplv0prlYf%I^kTIo7^BnqYqH;rQ&J9N68bC-IuNy8rUV_ol} zM?bC9t^M~WwnI97V##|@pmLQrz|&l!)kCWfGK zuV2*NyBBmNXafM>jL*mS72q42)p8(ggsKqLdLl{Rc3~>oT-G3SX^jz- z&uGlov4SIOYtWxKys&VTIZq|GtSHBz3v7mg>kOG`hdojqeOOd@9d^1>_p}*3CG$#s zSXi28=l0AI46r8S_p<}<-W*3*P%nJT!*|=RD}Q6?`8x^8L!?OrQ-BT1Vw~GaP%qRo zv>H|L57!bwwoy|>zZar+Ir7RZC%lOi7}yI3Fw&zt%APyi9?vU3VNL!2PY5A%xk4$L z8VKm#7I0XyWe$r&OzT7_>|*d6fAJ!G+Ti~i&=J`AvZSGMgt`*y zbkCLUWRostNksSI-)%e2s-M-C`%Q!Rm9%vE_;;1}buYdN$+s9ld=kDBHY^4j)0!eN zY4op(DX{qYg7~SIc@8ac4vDOe*R^Qv59gPn(94Mz0~hEk#5@vjqHbc$AAj+{|GcPr zbWueh0ozqeNSGh-tLT9TG&=U$!YF#o^ca}|5hN8UhA!l4{b{mhr z^!?8>R;rLmgP~+Ir0aRPH|5{)M@TMELHwnHwv^8FCrVIW)`Z-D`FIz3E>Y2s}- zl2GSQFS6qZ+*JakIvOT$i8g^!^;4y$KyO5ziTPrl$v7B0Gdtn86Nt(M(FuxBA}Pt1 zsg-W)i9ueX`|tU#o+g7XXI^%T(4u#Xewp&4VPRc zcRC4`uR*Fk?tDNHAGj@o`HR!WaAe6E>_|gK7wGpIEwN+7)(Dg;r+hc>7I-l(Nypl9 zxQ%h9dV)4qO}3R?l7a`%Se-2CjPN<^UqQZ7Ebc2bwyj%ZLcM_1eNjJ!^G|NZn)|<6 zLqkh7D)QSWKa&JEs>Okf&(t@&D^+cSGi3i^t=G4eZ+svqZY5}`JoYo4gc2`BknLR0KFsvlBobz@KjaGu9)Cb;EI^PQzFFZ$owE~T|?>Tu5~f+{=Nh3 zOA6rcv3@`(gtkN3Zb{I)d*q}=EQAW7Y?Jebu8<_ns^RPHba0zTMEH*@<;7@1Fq=?; zJt`c{615mRgmk-pQ#K>XQ}yP1fEqe%AUl!X_xPr)37wlofQDbkmZ_Yn~u@#g{Y2d3EuCItqqcvI2q;i6W%wO8W{t%MUuFl zh9{@21uJsP8ESsX(5niU3CTjBI{5mj{to<^C_@jbJ}xU?C6Rp<_9Laue;E21n;Kd* z$wKVi&&SkNc$`BWU%UL%J&iEnnOl7z3oW{VAe}8*%JOCIX>#RA65~76?tViGppMhN z10xiPt4v6gCzdh&szp+AHhJLRsOM=tJsUn?8csN&Ckw<^In<*AyfWMm%$= zp}gDTeXNX|%%PLUBd0BP9P9a-MD%elKVZ}ycUTjxNfa+~`LC8dHa(gCp?Hvp`7QY> zB*$J7Y3A%F54>r-M~mT|15-MRnF|lm(}^PSo(l&5^ETZ~3GibWPuY?(JcQr{>v0 z{0ZMEaU%}W^c-Q|gTn*kTI|P%b#I~H>-#6_nGiEywIezh{?uv*2trGX-QgZVgD^2C?Bw(#T~kxAT*lb z@yo?$)=0LGYDCb(QG+HolGHQAcC(sAPj$H88ceD97<1czgP`Y-I^)2L3of)TUuCb% zfuZzKqalT*>56d1hv{O4e)Xkpo$EKrr;%F7h!<5Q?#|Y`atO$RXla>V@esm^$lPt> zUFT4`Qd&!~F67ALO$T!{mX4HXijKnD)jng_^EEFTEltd9+q*HJGZin%QGS5uwoVDe z_7Yg4P0n~YorQ-&@-ME^_?@m+d&i>;KE6N%?vOf}%)#=I!p!TQ z<+12sz$6Wr=rRYkZROBye(TqDrf}&R5+MK2wmh$&DqO=LUCO%?HH_y!urmlW;Frqw z`0x}qtf8`?zKOmS=Oq4mOmxpvjE;24fpCymX#mp*4}S+fGX?wOmt6X-ds02lD&W~; zSmfZk=ow#QcGrdp^`;!aH~iLj(w8wig6RT^>M>>^ZAE1de&gS#E0IGciOtU?RWX5|2FhlACuhLjvJeq zB%bM(QHeaYOsxCUt_~!z!wIQMOpQpc5#U*^N$p8zmWtu>ms%Z!qqCck0?O0Ge~*xy z;A-e|nV)E?0(xhKZyP0dQA!rn#H~Tc26bQTU^uv3(FcbIP@ma-1^0j>+Zg!T9}OJ2 z>uvg;r*}bJxqYe2x)v1{sAM@qd1QNSe8g*^;0(-}sW*x|L0>E^QV>E}j67r}X~a1s zP*0Z4#|NQK+%#{WAOp@_X77a0TtJds0H@05UW-p~Kcb!|Y`QLE83XM>Ip$^F! z@4qfHpLwLyNsq0x)0LuADcwK^hE0M~bVtYAh-DD142s{i>ZZvHK{p|z4CCB2lXvoi zEKD*2#!Lt>y0e z^qEYp2lh(n=*rQ`#<~}cbGF7d1eb^$&L5WsYi=%te9I>!70kvJf?!sQe-%n#N{&)6 z88Gg$d;$y%DJ#h8!nPpcppv0Y$>~=72ve~wEg5&=ufs6yaq0LOw=WPXkWyY2_`~Zs z0yXQ+FrPmE%H&H2mv8D$zZ!A-`84)?g%8~2r0BlQyrm<|l%D!eKi*;DF_$|?^6q*g zxlBPI1(69+`1JNki|l~n>?#3UZskr3_Ar>nKc_}$F|GX|>6p)HH;#;p;x0p)Zymj- z0j@6L(+y$&hVYNwH1;%5!<3D=b6~6U_`m>CR52)9Nyth?G5p{BZpsSCvJkYPDfzCd zjhymFJ04mQX(XlooAfd2s!!^rapEe%pzk%lnCsaO|E=Z@(oAP!AK~Jc0aLXW*!J_r z(eu*P#?^s3yitX$CBU2}80^*=#^9XQ1XHO(n_*n^XY>>@4H*^|sFf_f$7^zN*yrDo z@CPGL{dOhxITbFTOM3CPbA5xDe2Tsk$tdD*MHn$MvTEOL%8W^farGg{ZX7 zT5*hEM>a(G=>?U#lIczBf0FlP8QkIQM1j@L9JYBDBw3#Xxrza=Fe|xbLpQQNvYHsZmJ_> z$zcY7-|OX$!l&Ob3wqX-g1xjC{cN{Emd*g#-5-TMIK7UtNMSjMa-Cs5@I0{Kb4l*& zPn&ZEd>#Ak|693q1@<Di(jeZS^#KrYimR z;TriYRgK{;rrXu!Xm}>8To58@PKo9BYlMBLstEiEGLdD)AS_g4dr&hTb3~eJN z-ntv=LgW8Z$6#?f2JU3Gb!DvKZVhCUPYIg-cBAE7fT0 zV9-2%B&3!>APk1GfIb@#aRs24Cqv|MaWT|~Nc=?rWSnBCmfU{X83 zCft%56h@9kWiMG7L7h|*pXvW8p*TeA&B%c(o@#Wi%sm4}ACWQ+sRm*T9k}hhp0f~G zs;cO{tHaK7y2Fw)jRLjP8eZs0y|3*b$qu>r5yiZ zg7Z;tWeaJQI0SdSQzNsU5PtKwlDil*YwEC`CFZV=cqc?(O9+Kxu)DrF(9Cu~VsdzD z3j)cp_lb6Yv5Uo4PIdCzU=@n>lJ=36ke3LdCkAaw#>a4+YHtfmY?}|44pm~6B?~ju z*6BkBWoByLXa$rHcX;$y&+nHo4?)S2kzQ;d8t&NLb^YHwFcbDg0kR6fP^>bbV7)}I zlMn{Z&wWRz*;VSdLoPV_uaX%pF_V?oZ3PfAyY6^sqP40jbAwnrRSTB~_VrE!pF(m` z^tb7g9lPgW|89$qHvmsxhGBpMWL9h}M8Ggbg%oh89Z{@Fgl-e%TYaGv_9^S!6az++ zsv6|1jrU+qS54Y5?#L!}n<^D2n>2(z7#HnN*5cVztwp~GTqxW2G8#?`WJAw8HdpnI z$q^8RN|B%0XyB~J-!q+Hn*kc+>26wWLsYbPhvwroj4bTg(eJ9kHoqFFa!evkjQ9!& zLe3|X*!|n58kFpaH9%FJk^v$wdn}j|+pG0$freOA?aqo$pRd_#DEGwP$K=pl)(lcE>Yc*>#0UEYK zul*yD_ti8P#K1RfnyF~egw`QI&?_+84BlK1g=Q3Fdw;et zxt~J@X5InS&XB%~94h2pD)hi<`afz-uC88R^AFj*>@W(KswYuR6L`e~r|5b0=|vQA zqfoq#+SM@(xg3tKNV)k`vM+>9r;R6K^v3H} z6>a>;|3viaZC}7y0C+vK_$PJiLQI)j%b?lQs#yuERzI*Yw@}bTgHzaTaC2kJ)$I@H z7sBySp*OLl-G`4xqgR76bE3o6@MO!hAxBGYC)>_`G3AQ00W)I#`95Y;91n%K6W6rTsBQ*ss?g ze0G(io+ne|o6b%(O3#&^g*@i`u+-}Q;84~M>hG2D)8Ijbs$Zp3v&pRt#*P!+?rf?L zT5Z55n+HDf8@ZCa^(q)IKLBoKa^;Y#>E_I@O0P@?r+<}PntrQR$z zb{-&JuQDWHRqV5YfLCMFiyWSR-2D^~3AYSwSo-vG;`c8zl|M%_!UCTAG*2Yh)TUQz z^|~TrrKB{gp{lI8EvI+hHc}DsyQ}z`EcZv7tqb*5Zd7v)#ud?VA$&sGh?QD6X=7d~ zrsSdcLD`z>&(t`E3Jet<^D03@oUoXFz!a0ilpHBvj@cdT1Y9R2yisgmID6?s5FkXJ z@>b`u&i?yB?8l=)28Gd-U#w}{1zKSmn?guc6REHdWoi7&A}d{P^0l>4OYUlR_O992Hr@`b@Dj8zCpX}QC!W6w@N%sM zp!xYBb~cfnW_%C=kae-K9soNC!ec!ThUr&)TG$G)BX7=0y8^5PbhFl_@9ZGl&gL7{ z^w_t$EURRP#W>tHFdX;QM{{f7BPXIan&ziU8E9k& z8h}nLO|>H35Nn*kp*@pFMD*V%qQMbujerAgI!>WCT$s~vO86mc~C=pChOXt0Q_Wn;!Up6b2Ki0Jb^-9C*UP4 z0F{t(-Zy~>vgy#nWlaQEib2ZV^GGe9t?6&$bnVn|u9zh0*1|<6x_Z4&1O;7bW*MvO z4Uoc#6G$gkLvopzoE1J-P_R4^-&JXDeH}`QDuhho&Fc`DwQ*+oqy2;W8%iHgC>n@E z%W0&#tn7cr)5U?Kcw4Bbal*JPBxGB5IsAJ2g(OjBxD|0h z@!fw5--m_-Ae&yJvh^VKU4{SCh|@62S?B6lr`LoBLuuo{@6*SKJ^6#YokOqak|7ps zQ6ov_*=`4Wxo}^5^WMTI_>pZRGKYgPu^aERA{#`1?VFzcCr^;%jAfqu8Sh~8U^O=Z zTF>#x?med2K6Z?q_+A+yCGp>wC4{=)+KltMLt~3oo{K)mC7qO4;5^mrfS1f9v=h;j zpoZ)~&;6~4eJClb-bT((z|dc*<`cp>u!Z_?{il#siFZb7m5htnB{a-zxpABAsaOpA z{AafVS98m$&ia07p9~HR5DPLiWQK(S(c`yZ%KaD7HhiRl-gHmWz|bGFnWK%eFaI5@ zU(0Z|#_r{HNMa9%=j^}Rm^^7qpu6?9O4xo|ig&6IpY$A+bXOZ2qL=?hh35=u)%Y!l zd1Sne)6VPTm^;_Rx*jxv2dIhALi}OBD%ebK@R~qJW~&wuUF?^lJiGmKAIAaUt7Ab{ zi{E0LOOoQKz#YPXgr4Slvrd^>9}C31kQ*@Tjd_{$g5pi4jb;CoCv z4do7lwW%cu6(nJGxVOI(ORyrCq7ec~?Mfw5lxb=x`V^CZx&>l@w5-r204wh_wIC zAY|G8w6sGq`VDS7?+o5td^lh${R+T_rp8Qc-V6s%sfS&pI;n;85KULRxu+%S8eSxY zUgg<#c}8A#z9@r4`x%-*0Vs^@pT!n_#el|SKb;rk>*UZnrvaC?H;W;Z5Tk}~DgXP^ zwhv~a@q$?M^MI=-^5c6&|0C7Ci&WMcL59B7?i(Eo3~N`dIPfM-R%!bXsp#t|09?c_Szo4rXCHFV4S<{4r$%tYsSwh_{r0p^J>q;V(3*Cd)O?QsgU#@=~gTI3;288F4^(MR%V0@nxe9 zW01*xfQBMr&mZ~Zj=I@4XvnRr03gCD4v!Z3R&jJ1OaqHsVilwOEf8_mrApQxUg_#K z=XY8hUtKups^mC4)smH=@q34`ZE}%5v`VM>61fZP9z-?WLQaVku2)X}Xh@=sZ+Byz zgYrcIk&dBY)yY;6(Uf$GS$R*+Psy-Q{fnvoB}L#~K}3OTwy&dH&ONX2C^z(7P66Hy zELVaDCLVkJ#Zr^1OvpVg)RTN@@;{Vp1& zg-Hs^qKU%~`@|{T-1g!noUh!xyj<<8&tU<&9wjah?!!~85%^Mgjg@>tRv|7Q%+UrO|64Rd?fdkKAUtmQ$ne6l)qLW% zAi;O5k$FM7aTj|MEeJM?2#x_3uN<`Rw)MjrVoZIO6M}5U=M%n%k67?qlH?}NX0NsU z;VAxiFX1il-ET6o)veL0ji1P*7r?X!3Pxn04x;=>x)84(p(U8-h6M|F_f0@w@_c4b#kT2hi81_?lVMhw;$?ZtENm zziPfhD$8!ZU8zKQ*OzE{u)A`2Ax1>qTlc~4$^#g0Zv9dlh5o7GW-cDtPoZCz-%W&% z#QA?{XQwoU00QI99|LZ>YIXo@MTqGzkFx1O;U}~QKX}}d+64E}8jn>zTKuR}ylEff zp9ANb;~DDmad$b8$|zF2q-HCfEEj;r(RB}otQ|++>R>g6Cvdm-ukO!Ajkj)pg3%Y2 zCaauWRz8Qd5mjXvD6_A;`891hKfvt~M@nJA?uVfTd^wqc@w!@57_ydJyiA^iSpgd! zKC2AIh~DfKo=pLsZ59bD&}>r%iK?)G>kj$#j>vYN8Zvm!?o`hfeo`FPWdq&qhGDAL z;d|=9!fyjspz(8YePRHW!7&PDQi0uUt&2GCIYo@g#8}{!a}9OYRw^Dx&r+E9;K%{F zT=1+#J|yt}0s^n&{+NK!TmNSM(&+ZP?5w;erY}|ER76=Im!`~r$&Ac-dEpl;w+7Y| zaDLCzO~n=X-M`eH3*m%L+(EFFYoB4B<2ytb|@dWGSbW^$)^95oO7Rm@$@^7&Nk{J7HWZgkut3| zq>jsHx|z)9F705gAaNvlHHr60EEmF~ z)67|RCeIjul?=N1fVU$Nz1kKau|*c4op>lOlI2PVcw|t1(t_nQS1=0j1v(I&9|1iC zK2MXJN@=65Ie>Bc$^g#>id>1xXY7^X-C9&t?ZgO9dGEl1$WV>{x&`5P_t$(Ph$wU` z>|ysA^|j(i>KYWyL@fi1aP#?Hcip=423}ELR1#nHDPQ-!eHG`5u@ZYX60=(s%{s1NTiW)!G}Nl1LdRJk`@fMbA%{mDia~@0>Ji zj=&ua>--Y)+^N)YbPnd0?3{~N9 z(swtnFdU@MIEvh#TV&ANjz{dh8^iua^eO=(`DfU7$E3Yo1>e*ylX?%FMydUF8Wocr zD2hf^m?)T68aGI{?%v#1XN&-Epb0af9n!wMiK^8zcFD-hmLL-Mgm@zPo-)hZs@pcWC7Xm||B%$NRukrfPzspPQHwe&w3aHux!QdWUI z79@7sR^>a+M?xNQf(B%WEeJx7m0hpiQqB?GD(_Byfi;ugfsbJ!gv6qq< zz(FNvt)?t9%-blR?d};~{Q3V})3KZ@0hJJ;h@L27;z>=_sUB^qIRiNL1VFtFKt2qRYW;$t0r-LX z(cl%q$T8j?hElp-scHjTh~K{1&i?SheM{F7s0k>dgiXj#%VCVD3qp(PqD#HjrAw3} zj4&T7T=>stt(O0bdR4S!TOh+KPQ~!vzW%ttQ|Q0d2iIt!S1 zbAU#zX5E(n-p3m>kGJ>ZDCz{ZvwVR}!?j zU5C~_##_iCXG5-|`Kqhk{v7Al5ZV(F)k#GQEtMJ{NIp)**t$4mICqpx(}F=-SP2!- zzp(H4W|rv|VK6<<8DpY1hT~ael2fDfd^Ihhws7rF9cxw&^E}|SE0gLYnP?o66f3i* z?;(~W^lwAaHevG?XL1*Am~NGH+BG&;Az$OC(JhJ){(=Td;nbjn+ zx)3`$An`5G!A@-Raun~L_vKu?zu9}a5{v`RPFwpXI86h&a~Xxey2`PYJxKl9rDsx;Drxs zg{+~5;&8JenWBVPWoa5b5-wQoz**mv0@QwDi z5qtE+hByOST~rKZ+5pB1h`Cr-Ld=k)S^?V_dU6rB=Jn;h{l%Fu5KV6FYP#49hjss2 zsg0_SMeWXml-25w(=8RIEa@&@Iq3c?U&)PzUfA`bJ2X*O4gDf&Qo%C_<)M0mPb>(1 z3&>x>a|#RrhZbrJHZrq{4H*GiN;Ncq3)ichHU(0-3H!NxhVd#@VE@LjWYshz4}|Ci ztN&O0ZSF+8<`>)4 z0-JBB>5J~F2sQB>#>8by33LrcTb|J;jg}F^U}$Wa?Z2H+6B+&stLtkurl&3bq=Qy! zeao3#Tn`O$D}jH(ubuAgh=d-`n+ zwvpNmro*343dUZq&ws=rI9#G0-Xf z0vx6=J7AExxD0<)=gM)P1svg2kZUItgxIpS)P{|P3!20u>yN@Mp#gI15nhsZ3q#ff zDs3mKzvRF-6~@{@LHQT25hc;^IFMyX?{`d8+~o(42CxeDz? zol@`8p<#zJA%Y{<{gA$UW=hPk+7or9Stpx28l$4@C~)O}h!+_N>W%@a=+BZsx;H-c ziR@_k)Mg>7#oWkvWUcS4u~l9X2$OwYcM?@41w3TP96Tw6Av5_0N>V0`Zd?uLlCgo>-$QM;+vT@Ff}q>)QW0P<*jwF zj$3G)-@DCxjLW@dBFlh!%A2wWqCxqvg>b7+MkZDm&m1sm@FIvjqZ()-2hX4iTJhGG z?Ae0O?G50r$VlmUzFB- zq-$&` z$!p}B*HXy>r3mLbr{JYgRg&jH7LpDrkoJ~kT|kxbP@gz;voG;s-hB1%wym>BHXuX4 zPIJXC>&Y2d7ww14oljvu;c#-hG&+n2csA84ECbPS6>eF;-Mqy_U{Tl~f`7qH1_8zh zT%S4jzwE+FB)|CvEKcQm0D~Nd5BZEZr#qX=7}^lDvV?3aPA9NK$0lhU-MmfZt0nZA ziB^@xW_cw){dU@5b>vb!$+oULCq=T-*`M(<2C+cLveZN`>Mu$POGKnDt*247%O~Bb z**R<~5Si}TJtq5!#h_1L0|l6b2w11~(n1qb3TBgM5$*EnKh)Qj+$vD#@bDmd4;257%{Z0 zau~)bvA7#h|Hg-!wcVjYXE)uhaDEoN%O+hi45ha3&4D)Zu0_f4n-rM3J?RT zXr@Sck*jo{EbBx$KE>N;<7^3alY>j|&0%?R2^4+i{kqJZ-!n zcr+emRfT%XmR^cW9*%InBU{@F=7?av9JPo{Qlr*jqk-#$hm48^9W$F^M$R7MvA$#( zJX`<=Vnb)gJJ#ObHGS#1TCKM*o-0GP={8leYoKJq z0*O+gLDX)uxXzd{$v$^b)%2@JN&sADlX*A-t6~Y{(=}tQTCV-)# z;k~zhETJSuB3wqW@fD`AJF-B;&n_|^%(h#X-@EKCT&IfR)or<+{{5RQOh3&kF?>A> zJ8`o>;9|t1blwo!nf=EO0F}2fD*73b-n;W>-^^*c>r{4H7M=H?U>+M}qIRe_pM<41 zN)G}2oU<`#60^$*&(A94Qv+%vi){7if?(Hu`b1JDrl<3U9=1)F$OvtM3OM@vu)>TS zsDsM?9>8YJ=y&zWMcn$aAd&dfp`5X;vSGMkCZPP&)Ss^juyxwNOxeZtpas3HaM*Ji z@R0O2)hyzz_BVKS&Q3wt#QKQ&c)t z@@*KX4S7v4&!K7qlacp0>-mJVO)!p4r@+cr`&W>Vpf@AX8 z`b@#th;y)XUoQR3X0Wpyjhb8&#P-P?OyO#;wnvWP`D!G&hPrB7n0!w@+j~AhY=&d8 zTcu(fITFSLkpmA+tM{~s)lpgqy!kC#)C1Gd4kv9W;N(KBPs%|6(s~$>ML6k%nE@za z!%`!*e99ulkSk=%EZX{LTj?!XP?n5Ga^TyuRSJ&pd)Yu4j&(W3Rb5G^x5r%{ucJWp zxU^${#l=VhUaM@3D>_ayZ_SbLsg7DA02G0cq+Abx)h`5^eoFa{Fz9vKR-vU&Q!w?o zVX4gjTl(18UXm>f<2^Fg)bHnVlHkw6`OXg;V=G3(d|>;fs3;%Vt`6kmoh?f`LYkR2 zUdCISFR7B`KFF-1>3869sSO|Cr;B3JUUisZ%_DkxvY1IyQ&i3Ld4MqT(#c(6jq4X?vu+ zQhs`}V-R{6B^Gx0B}!qgUWZzSl}2@blS=ptEYZtOx8pLzHiG5keCiO6YN!=QRZqZ6 zB;AF8NQ4m+ga0Z^b&*11#g~t)-9M5p^Y5a^usCyRc#QiNP|)ufj4^q;nL%FVU?g{_ z)%vHm-+1&g;!*R=Tl@S{_5oZoK1}5ZhRg$7-fGp7M*eJ_;AOB=%lxb<<%7ERKOPijnSsn zG-V_)M9`IJl*}+>&#j>L$J{mpRV^{U(B{YzJ>419>JvC~M`GEfb^Jx)uSd!%I~Swg zVAY%euWB~6*qNIH+{Xx7S&|%@mp!NkLEe&Nf&*N8#PFw07vuc&^&VF^Cf5g!E@>am z{)U30RtXr0kJ zuhnnQS6#7-moSCZQ_Tn96Ei`8e;-R?ah7Cy?v5 zO-hEkZXnj=n;60^_(bjWC*dOkaj|Rlqln9{Sp_a(GbebEq$@azT(QTnsUoPT?0jc~ z3d!tWf|2!EQar0qg>q;`Bv!FeN=)EYV(<8Bo4O{%?Rtd#Hn`z;a|XH=#JcBU?^Inh z3Qh{kQISsh_f6W_NT6ST*w2Ek&5HWpb3KBL12d3ZZgDT?CztyJxt=Xe2DKDUA2FoD8Fc%y zK?E4Kf(bXDvsrk^SZu#VOio53P^vT0zPkw=IXRq&X9RZoc0owmIk03%5mnM4xsDfD zsMa94MfP=W~BR}&NZwwcO12)N_Ptk|wawWmu8+xXVq z1MeO?KSzu40E5#$9hfkSl#%M_*S9*v@7361A0zWd*nHTKC7~04U`@&+g+eQUU@KC| z4sI_FTEaMvMt(w)oF)Bk zyux|!!chm$ORoT_D6SN26-#sZrw zxI8J`rfGa8f@*jA@0|l^cNOc$V1EwZ4|AKnIiRhNj=wEdK!^CYX^(&=(>uU*L@kE-h|$eO zkMr@!J*_*IX6p8shZxVAvL{8@yNNi++1fcH^C6_QnsJswkH+svK4KE60Bn=s_(Pj< zL#65L0Mj;3Df%_x3Et(*yb)p1jAAU<17HEti|n0?Bc$s{+cua!R|Q z4lwhLJ99`YMyvibX%Xa_tOOfBByP8D(nHCmR}3D?`G9MgJ6o^t9&C@KeUURdpt7?S z_U&-5`~TKD`kv*mJY+|C|M^TfZASYfud~t`H2qZ+4h{`oiIclcLZc}^@{x}xOWh-) zj$gh8>zfJKPboy?8c}dYP@|K25OJZzCt(tY*NlW5+g~|a?uP-LBwKI=5GUY>h#sA% zD}Un7AWWbyvqZs$ZxKVUx8DI35(ua!i^Bl=%Hk?R)t9zE4;tO%a%fE68GY0L1VT$p zd?s0uW&yJCfX2XWlzYqLOUoGX7!l)*u*dv1sW|7s$s@l2R`XxAiZE*$-siz}{5mi` zA_X1$KLYPHrZrv8>WB`-t!I@QVRUp2;j>KZdH8WjFXa0?O_o>YDAfR#0I{^d7uY&{m?I()7Y% z<)0+{5-PmLa-3pxp^>_ITV{K;9cE849izp3BZn`7WC{nGgP!8wD*HHYiAM}1a8NxpU{kfge_Bd7uxLGUC8UOk82iG3;Ecg7J$bSt?yxeB` z-~YqYgk=|m<$T!Nd4?y+|INRDjQ{?k_vikIs@lEUqZF8x2383nY<3M%d;MA;W->-C z}Frj z7SyfNpRB(<=9QzqG9hR+8%%sdFv#gC&}~W^H=rs`#74n{Rh6)GtLgq2)iDF~YN6Qy z6W7I}-+JFKu zM7j!2*8Ic(#DrgLquz>KCle<`AZq6Tr$V-)dP4#&5x_;H4jYw<8=1{K8V<*xfFd(L zrZ95g-LoyEMT|J{4EVqW2uzs<9Rd?8l(|*IX^B=#p@D$eanz1~TPn(og>t@8r`1r0 zu@;wEs!DyM&t3t362vaCE5Mnf8`yFP7#3AFN~fX-UwYVVTO7u9q$0?BF^B;Rek;`j zf0s#SJ!uPRKf(;uh7>z^o6x-0?YB^VeB(P&rsOoi(>2QT9}5AAHwtB#LW>mk0N@xz z;Aylh`}t2}wdG^wDm*`Ks4)X;A&tPL&m@Vp)NkJis#|CTbGKP>+?XitoR4?&c2&qX(`@Um=?^9X zQF3-PH>jMlaJ=T;QtLxbHB>?#VeTh)q9vcn$rkCf&ea+3gpu|I^S8d5Y--yXo?FlJ zI|DlBEZvhxX3D;3UH|yI2R5}T)Cm=Ts;#Nz!UL@(KK%AIc^IhHx8hO2T0(a=fmV)^ z^cJE$82WyNnMhqiw}iMs{Gm-%`t;k=`O^iBigRH{ABkv}#qg3%7aCowr>DLgWVAH^ zR`v2CtIIf!y~{WA*W92s4`Z;oLq9CIuBsKM$1oO_AAB>KqL>3O`y! zW>>necCJZWv6aA&s7=W9|AYzKDBK4^9d8hsEfvsjv^p_${lrkOPA!;D!!eU%9KHu0 z(tlg@m=gE&aXC_6&i{am-6I1YK8AkE5Fi6)P&XvlqNm){70A1MfVjV+G@Ou!K9NGG z2~mcj&erNeFG7?7V+I?ocb~T6P?z-^=}WjdB6Xe*dJOu5gro;bHk`qL zM@(CE=|L-SCo6XgV?S_@LvEmhaETI5gq)pEJ$*~-T8H4kAb2*l8@_0fd#0X3;~^|3 zbyZraZ}8Q9yK%DOHpd)&s`?x+y*n-XS``a&;K{N8ftCoeIOsvU+G}ONPg}g-WqZ>G zQ-&KEFR#SC>tP8reWtqUG*TJxSUtQLw{g?Z##yqk$g$um*xqo(qbdMg3$z#L6`lD6 znFPMmDar?O+!4q8SRlC4uOa-p(4*AdIV7& zs)hYj;0NFC`R{1RDJ5-dc)I>OhcGR`w#He;h<1kcqwRz1e*?Iu(Uk|=(4Z=~IQY=( zLRMU)qFUybuhM$B7N>{RuBDYdCTAPv&Np?BL^QLSgW}< zE7ohwWNRG1j_@Ht6xxhpn-Ju{MM2a%gGl=}VhJQHF(11tod)V+XflFaLOhn-=)Zvm zEt2v3ctC=$Zd3xbHK$F_Cv4$VuTS7~uZx?zBcX#JE+dL?G zKFYSbg0sK7*7-HBK;54KhwLm>ayZ0M8%>SJ-gR@7VO&0{IKtd@F0e?nW5s=JODfQu zs5N$uw0Z6uB#`Z<8;vbTvaI5d#EAccNz3z#Zf~ z+z{M5m`VS;#>h=ll@7I1r+_q?nD!+JMkCDb&5X=wQSBYOvkg5W-}IDp^p?@9x~!w= z*E@~Vp#N1J4iPTb1qG~Fl(E&_rTP?gAj)9;lFiW$dEb&7{cx;ro!DJX)->2WUB$2E zJigI9H}ba+xUnlFCth6RS3rkXjIh`yxV;4RN;q2V$DYawDk$5RhQ-?69?1y@kXWTy zKBCRPl|TLRlyD*;O0!SJ6bF{1fZz~C(UoEwBeh#L2 z?74Dx1?|2z5vj9yt*>SM_8OB026Jpnw6~Ngj~b}LUdl=cwS9jrs>v6M@{f$6_|V5_ zmr47M(6nCeJAre_p1E-(5^wiD#+Ee};TP?IltbvsLe+-OdnKolT&W36(}}gdtG9XS zn$QX7LnBkFeb9A^O)`adOewcxcn-;h0i>Z{M6SA1GBV5E2!zkRi*X9-g>%Oa&cI+aKhk|rpQ$F@v>bV9$I?vtym?$L}ND337(jdM=| z(1!iy1MSF(uOSt6Hg^y=pRkW8yU=@=&~hJGdtd&liZNK;&h(Fk=az1BHA?vSrS4HL z0$^A)y;(48`(fN@A$Rk0jQbxOU*t0durCW8xOYRQylwe6x_n<--|kU>sAm6xyO<|x zQWw4|^y-=tJwdoj%LVj4Z@;y|8u-??Z4uxNK_nalq;>Ly<_b%En6Tb8$9D}x*qED{ zQPh@a#7{WXV>ERoA`1Jp;$=Afi@&10Q{fDvjBCZCe%Xh7WR3sf*^#smz`DETp=A*S z1hYydW%^sY1a(er$EJE{>h=OK;ujGy%lsh&KI$VM=Gvs?EISmjSc684wC?W+;;xpI zXx_V@(!F)C&j%Qma7s0?QfMG-zJ99}#1kiZ^!HTm9V2bQ2b_%gqRT*yORWy}hPr>< zm){%A$F-74$gU*6S9izfJV2>A77f$|4%n|oH#`>`Au~^*q9Ru#^>hnkh z#LTB4ah^s=7Q?tn5(5cRL>6YJI46`Epd2?yz7QZO#h+Wu$dE2`r zqO&dx##w8;3=Y+$I&veyX)+ohqD}w_{Ll~9J3>8m&2 zx^Y6xHoSParEga|b;=pJ_q=s!%n_H)ZPiGk7=pGJSQRssM4UI5Z+Lx79i?JN;u{R4x462pXcc{_0vxa!YK|J_D!S0 z1`bbVxKE`{TRjgyY6>N@z4OCd%1TEzrV0$lZQhP+?G>6mj^_UzO22h$XVIW-!Z3)&CbP-L^^2pTW(ysY$x_S^y9B#znZzr!7KHEpINOic-q zByN|x5da;5GFquW)|it4FJ*JY>fxSb-iODIU&7C#=UhH-iG`3qOl9p7ff`iUgd zabinNwXESelZ=iyUCgmTiu^dGt~L1PLJI3&fCoa9Q=xk3M(4B?bA&$_f(JLep|BQP zN3=3|GlWq2&b~<6I)6uKw4JpFIiVY{!&uAjJUcS*)NFh|HN}oWVm)X0q@CPpJ zg};8Q-$X^%2XtDP z_RAeg`~$fUY%|dX6bGo{%NCCJ#%_yE;gt;i;YHZx*0}tR2I!0+0AczziU@=}NrH$Z z6>1Pc!rjRhIV9$gS-qL2R#E`$iif(FQS~AKfQIR3YVgN#U3hs}(>&KaAw^P&NPG{@lvN5=dk3RxPLM-s>fTz-AYXc)NZFx%APfvL}S#+2h} z@tA5tRcQYQD1vkNlb!$+>@u z48i)oq!LPKrQ%wvYD%jFp)RyUJeV-~W3ppNiHFa4+>)yZ{^z0Pv@nA#YLm;$gEys_XL~ z%d})%4^;cCAopmfNb4z%9VBCWmOaR?&*NucM{*h%cO-;3yutlwQT1gzrsUb=rLH@x zy2qb%{+SznF*Ck`|I|z2EAiCS>q|>L6eY6LJiydxNPEUrC}?x0p`Yq0dIia(QWS4r XjSF?@uk*gK2M`1|CPx%YDq*vR5(wKlTS#~VHn3h?{CgIo272QS%;A5uuZIsPd+Eov-t+wpOk7 zJp_XRZ8`L}N~6Jq&9t2#tBaU~k#=(w$ict z7DIE_skNM?JM5v;T#qL{douy{GM}l%?MS};45~_=cYzAWDR#}L8T4PnW9#8^ zu!Bx>9iHSPZ**xrK(CWuVoY{gd z#ul5?pWEnmq^738t^-DCIB-u!{gc8QnWR`VQz}Z>sTApenP|{go?#>%kaOPc&}OE< zW+U(A1cxfx)ua`veWoL)*&0e>j|fU40&5rwBN9tIg+&Sm63_Uq z1TL)vzTD36!CxIzXPwk!4M-J)QRq$TmX%V!X&{8c(^zo!{(n~9%;Id6lXYD;1N{SS W@&+I_zCsNE0000Px*=}AOER9HvtmwRwj)g8w_=ia;7-DEe92_!s11!RiZ1gf3RAa6!nOF)A5L4w8b zm;%Kj7(2!Uq@Y-VVp0Yu6(6IBEfXR_fFd$Lph&5;j?hjbLTLxuLb9bKu$%1eeVxwT zg&2W6X8e~sdw0&>-#O>^neX}ie)qS64wsZt2q9$0XSW+W^SYbUo&AQNue?!rj=Hno z0q9bJfdlVNZ_%v@SYn{6sXkRzHAM(Tf&aSYh7`a$5j@ zcK$C+!&u^MqNb;zjvN8$X$Ya>B!FTIT&BQl2;7DwB_{Aj!5fiyA`(}`CKhhyct zw@UmWi7y1nVT+V7d`-`09QU*C@>*(I-vc466t_F?^2NV?-Vx#!1=6wu<+@?6@_zkp z)E#dTePkx)xqn`l^oF!Imfd_LUr6sb8JskT8vhpNT>A;9j0RMfYgxFd{>3f<{8_nM zOw%gyS1iV`CDAqOQFHns2IOF$K8xLaYdrP#=P)37Fv75CX!(?g-PZ2or^&PFZ}(zJ zbS;XeZEX!UlynJ@nw_`HFpR1GjjPe?juYFl2k+9SkqhU+A3sGeo{4Sf9rh3`8TBS( zlo6!02-aU%PW83wc6)(m9MdVFBwB&0YBix?eNo41d&?1|X6Mx!hEeF>T#51iA)>qX z;i)J^d3X{sGYk3VHlk}*w>wBp%H*wY9p#nN<&0Oyl4d8f@PALX+b6omFjGusdIDg3 zC|FfY_e>c%ChVKmfUWKS|49#9F+Vwz>mxClB^3;)%ZH zJbrQv<2@5tnf5#*FO9^IM%+HpJ(^j{B#I+TQB}1%6l|E@9e|_26#r|#Lf`p&V*3u_ zDRTgFiO!mjzicU4qo;Cd{tswtJkK-ty@Ar&%9=BaNLSPOL%-w94VLgx?2`n5#{npy zxb3-~0c7+YRvXjx1i+5H#P%P;{Y)t$5Fk2x9zs(Y`rI4zkEQdM4GUN^@&mS=s^DmF zZ`|SAeYSBb@-dZHt8N8wsnwyzZWL$-c(p14@WJ5(Kwg08thw<`-M$Q_&UqK}@7#1K} zGOs)n&%Vqlt6FsAttrSqyw#+$#?blbNVlX#P+XAUCG*N5}xPqfE`GZ^j}PL9$C zVOa>NQ*WK)+pZDhi-9a|t3q?R-U$U89`Cs5cCX_K-7sI$a`O?(*JCZ5j(xf5OOAT_ zBTLETkar^sVyif&pF~S?{X87}Y-N`KsX2imMw=aPNL=45LH+)G#E}DdE;izcTJhVb zOOkc)M4emW)xm8Dyt=?^3X*NeA(N@vST-5etc||oG(~oDl56;t#)kSX0UV=0JMfZe zm=(@g%g(_yd=y@H1}VCR&rnIzReUiep7*{8cw-Vz)W)UTsHTCiby{RIhpj*JcjI62 z?oiXz4dMTuUUaj1opr^+-}|0&(`G5s?$DI>RQwL}1TjUSYSM&nEUT%hv3d%S2)2Ru zrkici2ZY5?TaYOwZbje{LTK?u94Qg;FPZ4ig=I@y3Mpbzv9Ai`S)XbjY-&0e>nf{rkXkYQDZCHdZ8-RYZ!0Es&1y?2ItPMzwmuIlgm z|N85%>c8sx`90wGYd@vZk8+FO=>PKbOY-ycvsk2iyUNdR4VA^j)IPWR`F+Uy`Pr!s zHK7(#Ic5Lne|A1#s7>R)&8qR=NBMp`7jVGN`R}LePpxYIk0`xi<@_HdK^|xKOhtfvJ$6Z5A8V`#Pl1;Z6+7s_LEZr>UY zr^LIq)hIVj!XHh4#B5^;?3#dM02;_ZvYwbfU4@}N6exT|A>zK5Gq$GOHWa-L@ff5_ zM}htkYCr3?_Uv7DF7U*HVhx5c{Mz>OQ*v;y!vehKD94x1U1-mT%k7WW$7k0Lp4yjc zME->%6v#{B@zUTf*wA*2$Yq;#iI~q}_T#_bvYvvZ{O<)nw5YmM;g0`X2tF;aza ztPU?0+m}iF8?9-Xhb91CPK~7@v#I^<{m1Ml*&2vNvz1shONAvfG+0t# z!QzJ#u_&h-79~e5{TdB?ns@kCMn|jwTQO5e{Zt0i6V1C9zDU}lQPxpWKHPBKG#8{L{yAYUb*C?iu zAAE+!=MwS<%N@L8HI0|ml&^9Kij__wa0S&{LE~sC%?Z!j)d34=J}8Rf0tQ5AwL(jA zss00Yo-j47t}Sx7sz2mSf zI}Ix`vazB=f2>G+6wAHivDEqmp0hlOg{GN!%20%<`U&VHco3y(Md?BE&4G)XgYPs2 zMcUP^ZZiDYj|Ab|n<{LWLimFXcsf5EbF-Z&%S=L9S}MwtyI@XiPt0-kN11gP=32(k zd^Q>7hUr*kSdJ6sZ!t?hL&T3#KUnS5sa^Nt;M4|BW7xSV3Y^{%g!a}tpDpX8xp^`d ziUT2)$P>~LseBOR%V*pR-z5ZXVDDd38jRVarTI3^6Nv0K(lvXQ8KuY2So>{uk!NU) zai-oFqtC;`x_qS4x?qZ7JeC<(iv8de6ffQrUmLWIVQgEe!suQS=CV0T?Ajdc!z<_z zgIm*QphJk>S2i*v3%5L#_{RDvD#Aa*`LOGlu73i<488EOahq7r$p^k#e|~O?i%U?v z#_(T%L5-0z{!=RK+9s`e*t0mj64xFtsxD2=5Z~KL7l8}0A7f4UEb%^tx1cKQ2D11B z3?=+mOs~uMo9=~AI+F0$t1+r~kl1e7(<W_O(fs0e%VEkI_ajvGh zTv{6xnueny>}%N6{)FEXuhhwZSA53Jr45qsH*gr0UyDCajXiIwMJ^1RrNYFvxH)VU z3T#<;BYHO42s)7O5ud#lwhkN2tHtYKg#U8LYvMJn4_>*O{I&RE|Nr@A0i*gzeZG|R zzx$1b_z5~S{xy6Be$0Lam(#Z5Vw?AHG3I0Z({l(noTo9-FrXHn`rjn=KjFW8FMPL7 zDdBGtFuI>oe8ZFvzQwUPKCe4c%{pA~yAG4>U3_>4oSU#b zY`X8hKE6Z3zvCLOe*ZK4#|G8nmrLW1;jb?jaA~g_=}wJ^vA6R~ywyfpLnrZi+>EY3 ze|>7x_`E3pz3@f*|1;r_A6kpg?C*6Irah`e^=Sd)dP~?$-!t6Z@k9C>h~+hy6q1X} z?qhIiq;bQZ57+m?CZo^(nf~vH&uqXq{>c6&j*x6_**q1#KCZ#F@6}jCT-bZ%l&sU@ zM)x-{IWh-ZYzy#q)bD-ISe+4szuB-=EF=GSSNpHU-^$wkX`_Gwl9ydCpRY#EbrlZm zRKcxnI3KY-`LKH_s(b82O^1V+VHwr{{}Dqj;ct@h?>hb&KCNfgG0f-Z33#qn)>LmG z26a=QbwopKR*$7lp=LbdNWt*Pc8H;G8M&P0b4`Ub7cZ3Xg@E-8<8D8if2Xo`S#8uf zdxpMm6BVM}voa>DoXgkjUQUh6GIcI|7xe{tYTJl)T-w0tcf)4S;!M16{V{Qb2ItOj zcx$r?8<%OY;du=_!$PkIMrqwO4fwSEUpFOAVvN^IWiQ$15H!v7ajt$1laY__XUOYyHS&s@R85W9J z1Fa~@i$zKIwwTqqGm6`G!%SKe{WdBKNrE&#vv<+F_H=WwSsl>&)2&rKN$cIKv=)P$ z*n66jSbe!3#Yuj23PHv6yQAN`6R#MHMd>jpdwR9XiLpk_^aQg|oEWMrNetDNwhq&l zCbC4=i6J~Cp@gOQ5KU>Ei7SrQDGS_sWiOA}Op&PjzyA*!xR-C1WnlGwZj#^ipPsWg z3596=hEcj7Cm|)=|J~QXeGN2A1C9N4wRDA=>`Y2EK2!LEYDFRQI7+QJzCk_M%u=j#cHK zWz__9cI$%^=p9z8rj^mF)dmptyM9ZwR5Yj4?>A09D9gtAFQvVNk7)h%9j$F|(psvf zaia2?yCu1PqqT8$l-mCS?N=S5y`mRvd|+NO>8hnXo2t*^ixmHO)~@zHU(3t4E|)p{ z!a2$Bf6>M%Ui`T-#2XC$E^Y8Co2Kqt!bRWNDfGRQ?hHhhI~ZM^YIJvKkmJ-M*BOF* zM;Q9pBhfd?j=qs@^a+bbA4?qa%<;%IC8C!;5jomKbmQaEg^NQ57mLok7b#o>eVWWUd z{A>?gE*pZ&Gk%Lpk59*?$DYEaVb9}Y-g;c@`X^lM^bRhy{Q#HZKf@*W-*D0P4K75U z!@2Ow_{#E6d=PpFON@(=Da4C0MJ9t){oqxb5I8#Dk2Zu`_@j8Q+Q9|9E_2U{3&i1# zLAbIj7=NUF$3ZDU$Z`dti$jSnb~Q4s9K}BcWJKtZ5pG1LB?OtFp~wiaAk!F*OhY6x z^;Tr+?8MPY`=cIY^3lkoKIqS7V!7ctoHAd+UejT86OzTaY$U~}ZCt>+R)d0TA$>oO zL1U*qJZAHVHu&R}@?c!p6NHgn>f*JsY`;`KP{^Rz_F4SF_%dSDf+z!x=SyyVprfVy z-A1k(?K3c6f=t@m{KuYP6wyBMFU6I;hkXk+e&1yMNE{12OEKEk5`U`WG;lpy%0Jr3 zO_tw3Hcy3b_68%}BgHA$c%DEptQqvZoETOYyI}QaW+dX-?rF%jT5jXUxV9HUQ64@P ztHMjM!cvAM<}%DP%|!|AZzNG{u_JHCr{+qGH#{n~XYcgrR88)`-c6=Bvrr`SFM5o_ z(YJ%qp3Vy}4%DBh_*jXv$AZv1wPBy^7?q1##Z$4mYY))gZUY$-c^$Udm!R5x5sv^F_h4AY*W4T8DC=tng7WwgnxOzz%kp~m~I?_$@>17 zq92a45m)e``H<*C8UOc*f2qDi^h2z#ox0${mh$%+)OGwPaQOUPCEEG;ll~8>aP~_j z@;cSU6j|SR1qIF*OuhNTV{_3nBFe{qQg{x2h~JN|T|1GhOY)WXrI_f2@Ecffd=Xv^ zhqf|*gO7g=>A%JNJqGnm;v?yw@!zRLd%I-oZaU{s@wpn`f2Bfx=Z5-cGOX{Bd!u^X z0xW5lM`P3Ymi15k zl?5%;KdrZX{7WVYII>68fdAi)5dXic(dVIt`e*zHyIr_8dOp6)e;UbJ9k$1n;%3{w zU|G~8(s5m!HiqNyY2;q~J^VBVXr)-Wr2kJ)YAllbsKxx5tls+eKUs?-AE@j2v+?`6 zhWKlmxnm%TBl}S-TP^P8u>Bm?FTM3`a60M)z6k#uiJVE~ zE7|`iXnm;Q?ui2jDL*ee?pYghz&y=6l@-LSCPwC^t|Ii2r`fGD>?N^i5FX)hj15PA%b| zVA1*b&wfIWBL|4TvsVAwK>Y0>^FM6--fsWx%sghNrr}J#1-Ov29tE@yl&Ui$RU1lY zn=D8Z!e|{Di-V5MxK4XwiJZ~LpV|LT%YKv?>+HXle~a`VSD*hB(*HpY?Q8jq{^x)S z-&ZPc;~%3}VEf2!*fBg0`*Ww^MxTwC7d?=Cjx_hNzG11W-i4~@Pw$7F@`{_8tuWYHG}cys2r5EPDjn4_1MvV zCK3eRr(?!*nq>fLqAO7zI=(?UopEP#>U)k=_|bcuzGdPY@wbrv znf=d@%IoQ09-sHh**IbAABM#elYOc@V(Z~6i{}OW7;_YdY}@I4T&S;{VM+RD_EW2W(xGhs_u?%-XZ+*M0-L`i z{!?{0euP5@cRl`xB>sJ9{AR`}aO^z+*H4(SY)S|_@8{z*%9@ND*<0|J{u(HHSJ5uauThCOD+zt(>0^xrJsFUwZTU$p;OkLywKg@9Bae_DUi`r*PE6+U>2$Bpk*_-elz zz1!cu7K-7O*qmI5n#}hx&pr}ug%F%_zlU?K1L&u3-#}I-1LOYz@t;lSi`YH$KOSlR zY8n3!VFtAq`4<)FP+6uErg}apw-ibGqC3r;z{INBm3mzx!GI6GHV*%HJDuwDW#oad2EXTR7z^Oa)<)A#V?V2mFegcu`Rk4bf9&h>LP zo+l-w(wW==cqVZO%Dh8pK4$*(_BI(>7fyv%;xT zEhY$Ge`&zVg|vQbG>0| zs~72U`GN*BC#XR&Tv6Y$&gN#sG2nk=L;9$+^ALfe&8SQZ8oi^J8r?Q*j0Ec zVw`whUG6iAKV9+gis8hm+21bp`1`T=tw*a^DcOWH*4WrhqBHkt?f`U(p*Sw3hiVhM z&LAI^?o}Yu6O0VE3h6F7+wb6zZr38irb9-g5$O>oq=%W25o*ChAz?^0Sm;cJj-B)O z>79*RiodR_-4Ccwt3}r=e(u&P7gH>;isYziaE3y(bt#Tk-=<}G>zBzotzRbBlb_Y* zHRP}UN&f>_?sPhT(;~8Ws2mv?`QH=+>+0l!meTp?eNIilHx!$??BW8hQ@TN8>=sMT zdbxZpKGV>1jlY0C$;i^cNb9q9bzVz!^) zQX9QQ(2Fcqy+Z7XBs7z zQ|i__F21wd=8X&0J3K;gIE_`^&)u{5$->=lcwYneHSoW#0ru_1(oZEKxBqwMGPj?@ NQ+~(&{6AX*{{x&)yea?y diff --git a/examples/tictactoe/client/public/logo.png b/examples/tictactoe/client/public/logo.png deleted file mode 100644 index 4680304bebb18e08a42983750d7954b4737f24cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272311 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 ZUfbnjsNPv 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 Q-mopZe9`0rLDwnfFUmwSDF6Tf diff --git a/examples/tictactoe/client/public/logo.webp b/examples/tictactoe/client/public/logo.webp deleted file mode 100644 index 35a4062b6fb0fab7bbf9ff8a820da08b34acf4bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28244 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 - ); -} - -export default App; diff --git a/examples/tictactoe/client/src/assets/react.svg b/examples/tictactoe/client/src/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/examples/tictactoe/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/BackgroundAnimation.tsx b/examples/tictactoe/client/src/components/BackgroundAnimation.tsx deleted file mode 100644 index 89c82086c..000000000 --- a/examples/tictactoe/client/src/components/BackgroundAnimation.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { useEffect, useRef } from 'react'; - -const COLORS = { - CYAN: 'rgba(0, 229, 255, 0.05)', - MAGENTA: 'rgba(255, 73, 225, 0.05)', - CYAN_BRIGHT: 'rgba(0, 229, 255, 0.15)', - MAGENTA_BRIGHT: 'rgba(255, 73, 225, 0.15)' -}; - -export function BackgroundAnimation() { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set canvas dimensions to match the window - const resizeCanvas = () => { - if (!canvas) return; - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - }; - - window.addEventListener('resize', resizeCanvas); - resizeCanvas(); - - // Create particles - const particlesArray: Particle[] = []; - const numberOfParticles = Math.min(Math.max(window.innerWidth / 15, 20), 120); // Increased particle count - - // Track mouse position for interactive effects - let mouseX = 0; - let mouseY = 0; - - const updateMousePosition = (e: MouseEvent) => { - mouseX = e.clientX; - mouseY = e.clientY; - }; - - window.addEventListener('mousemove', updateMousePosition); - - class Particle { - x = 0; - y = 0; - size = 0; - speedX = 0; - speedY = 0; - color = ''; - moveAmplitude = 0; - moveFrequency = 0; - movePhase = 0; - age = 0; - lifespan = 0; - - constructor() { - if (!canvas) return; - this.x = Math.random() * canvas.width; - this.y = Math.random() * canvas.height; - this.size = Math.random() * 6 + 1; - this.speedX = (Math.random() - 0.5) * 0.2; - this.speedY = (Math.random() - 0.5) * 0.2; - this.color = Math.random() > 0.5 ? COLORS.CYAN : COLORS.MAGENTA; - this.moveAmplitude = Math.random() * 2; - this.moveFrequency = Math.random() * 0.01; - this.movePhase = Math.random() * Math.PI * 2; - this.age = 0; - this.lifespan = 500 + Math.random() * 1000; - } - - update() { - if (!canvas || !ctx) return; - this.age++; - - // Calculate distance to mouse for interactive effects - const dx = this.x - mouseX; - const dy = this.y - mouseY; - const distance = Math.sqrt(dx * dx + dy * dy); - const mouseInfluenceRadius = 150; - - // Apply mouse influence if within radius - if (distance < mouseInfluenceRadius) { - // Particles move away from mouse cursor - const repelFactor = (1 - distance / mouseInfluenceRadius) * 0.3; - this.x += (dx / distance) * repelFactor; - this.y += (dy / distance) * repelFactor; - - // Particles near mouse are brighter - this.color = this.color === COLORS.CYAN ? COLORS.CYAN_BRIGHT : COLORS.MAGENTA_BRIGHT; - } else { - // Restore original color when away from mouse - this.color = this.color === COLORS.CYAN_BRIGHT ? COLORS.CYAN : - this.color === COLORS.MAGENTA_BRIGHT ? COLORS.MAGENTA : this.color; - } - - // Oscillating movement - this.x += this.speedX + Math.sin(this.age * this.moveFrequency + this.movePhase) * this.moveAmplitude; - this.y += this.speedY + Math.cos(this.age * this.moveFrequency + this.movePhase) * this.moveAmplitude; - - // Fade in and out based on age - let opacity = 1; - const fadeInDuration = 50; - const fadeOutStart = this.lifespan - 200; - - if (this.age < fadeInDuration) { - opacity = this.age / fadeInDuration; - } else if (this.age > fadeOutStart) { - opacity = (this.lifespan - this.age) / (this.lifespan - fadeOutStart); - } - - // Wrap particles at screen edges - if (this.x < 0) this.x = canvas.width; - if (this.x > canvas.width) this.x = 0; - if (this.y < 0) this.y = canvas.height; - if (this.y > canvas.height) this.y = 0; - - // Reset particle after lifespan ends - if (this.age >= this.lifespan) { - this.reset(); - } - - // Draw particle - ctx.beginPath(); - ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - - // Adjust color opacity - const color = this.color.slice(0, -4) + opacity + ')'; - - ctx.fillStyle = color; - ctx.fill(); - - // Draw glow - ctx.shadowColor = this.color; - ctx.shadowBlur = 10; - ctx.fillStyle = color; - ctx.fill(); - - // Reset shadow - ctx.shadowBlur = 0; - } - - reset() { - if (!canvas) return; - this.x = Math.random() * canvas.width; - this.y = Math.random() * canvas.height; - this.size = Math.random() * 6 + 1; - this.age = 0; - this.lifespan = 500 + Math.random() * 1000; - } - } - - // Create particles - const init = () => { - for (let i = 0; i < numberOfParticles; i++) { - particlesArray.push(new Particle()); - } - }; - - init(); - - // Animation loop - const animate = () => { - if (!canvas || !ctx) return; - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw each particle - particlesArray.forEach(particle => particle.update()); - - // Optional: connect particles with lines - connectParticles(); - - requestAnimationFrame(animate); - }; - - // Connect particles with faint lines if close enough - const connectParticles = () => { - if (!ctx) return; - const maxDistance = 120; - - for (let a = 0; a < particlesArray.length; a++) { - for (let b = a; b < particlesArray.length; b++) { - const particleA = particlesArray[a]; - const particleB = particlesArray[b]; - - const dx = particleA.x - particleB.x; - const dy = particleA.y - particleB.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < maxDistance) { - // Calculate opacity based on distance - const opacity = 1 - (distance / maxDistance); - - // Determine line color as a mix of the two particles - const colorA = particleA.color === COLORS.CYAN ? 'rgba(0, 229, 255, ' : 'rgba(255, 73, 225, '; - const colorB = particleB.color === COLORS.CYAN ? 'rgba(0, 229, 255, ' : 'rgba(255, 73, 225, '; - - // Create a gradient for the line - const gradient = ctx.createLinearGradient(particleA.x, particleA.y, particleB.x, particleB.y); - gradient.addColorStop(0, colorA + (opacity * 0.05) + ')'); - gradient.addColorStop(1, colorB + (opacity * 0.05) + ')'); - - ctx.strokeStyle = gradient; - ctx.lineWidth = 0.3; - ctx.beginPath(); - ctx.moveTo(particleA.x, particleA.y); - ctx.lineTo(particleB.x, particleB.y); - ctx.stroke(); - } - } - } - }; - - // Start animation - animate(); - - // Add a method to create a burst of particles - const createParticleBurst = (x: number, y: number, color: string, count = 10) => { - for (let i = 0; i < count; i++) { - const particle = new Particle(); - particle.x = x; - particle.y = y; - particle.color = color; - particle.size = Math.random() * 4 + 1; - particle.speedX = (Math.random() - 0.5) * 3; - particle.speedY = (Math.random() - 0.5) * 3; - particle.lifespan = 150 + Math.random() * 100; - particlesArray.push(particle); - } - }; - - // Listen for custom events from cell marks - const handleCellMarked = (e: CustomEvent) => { - const { x, y, isX } = e.detail; - createParticleBurst( - x, - y, - isX ? COLORS.CYAN_BRIGHT : COLORS.MAGENTA_BRIGHT, - 15 - ); - }; - - window.addEventListener('cellMarked', handleCellMarked as EventListener); - - // Cleanup on component unmount - return () => { - window.removeEventListener('resize', resizeCanvas); - window.removeEventListener('mousemove', updateMousePosition); - window.removeEventListener('cellMarked', handleCellMarked as EventListener); - }; - }, []); - - return ( - - ); -} diff --git a/examples/tictactoe/client/src/components/Board.tsx b/examples/tictactoe/client/src/components/Board.tsx deleted file mode 100644 index fe8cb5e48..000000000 --- a/examples/tictactoe/client/src/components/Board.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Cell } from './Cell'; -import type { GameState, PlayerSymbol } from '../types'; -import { cn } from '../lib/utils'; - -interface BoardProps { - gameState: GameState; - playerSymbol: PlayerSymbol | null; - isPlayerTurn: boolean; - gameOver: boolean; - onCellClick: (position: number) => void; -} - -export function Board({ - gameState, - playerSymbol, - isPlayerTurn, - gameOver, - onCellClick -}: BoardProps) { - return ( -
- {/* Glow effect based on player */} -
- - {/* Board container with enhanced styling */} -
- {/* Background patterns and effects */} -
-
-
-
- - {/* Game cells */} - {gameState.board.map((cell, index) => ( - - ))} -
-
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/Cell.tsx b/examples/tictactoe/client/src/components/Cell.tsx deleted file mode 100644 index 44e1ed36a..000000000 --- a/examples/tictactoe/client/src/components/Cell.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useState, useEffect } from 'react'; -import type { PlayerSymbol } from '../types'; -import { cn } from '../lib/utils'; -import { CellAuraEffect } from './CellAuraEffect'; -import { CellMarkEffect } from './CellMarkEffect'; -import { useSoundEffects } from '../hooks/useSoundEffects'; - -interface CellProps { - value: PlayerSymbol | null; - position: number; - isPlayerTurn: boolean; - onClick: (position: number) => void; - gameOver: boolean; -} - -export function Cell({ value, position, isPlayerTurn, onClick, gameOver }: CellProps) { - // Determine if cell is clickable - const isClickable = !value && isPlayerTurn && !gameOver; - - // Track when a symbol is placed for animation purposes - const [activated, setActivated] = useState(false); - const [prevValue, setPrevValue] = useState(null); - - // Sound effects - const { playSound } = useSoundEffects(); - - // Detect when a new symbol is placed - useEffect(() => { - if (value && value !== prevValue) { - setActivated(true); - setPrevValue(value); - - // Play sound effect based on the symbol - playSound(value === 'X' ? 'mark-x' : 'mark-o', 0.4); - - // Dispatch custom event for background particles to react - const rect = document.getElementById(`cell-${position}`)?.getBoundingClientRect(); - if (rect) { - const cellMarkedEvent = new CustomEvent('cellMarked', { - detail: { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2, - isX: value === 'X' - } - }); - window.dispatchEvent(cellMarkedEvent); - } - } - }, [value, prevValue, position]); - - return ( -
isClickable ? onClick(position) : undefined} - role="button" - aria-label={value ? `Cell ${position + 1}, ${value}` : `Cell ${position + 1}, empty`} - aria-disabled={!isClickable} - tabIndex={isClickable ? 0 : -1} - > - {/* Enhanced aura effects - only appears when cell has a value */} - {value && ( - <> - {/* Inner glow */} -
- - {/* Outer glow */} -
- - {/* Static particle effect (background) */} -
-
-
- - {/* Animated pulsing effect */} -
- - {/* Dynamic canvas-based particle effect */} - - - {/* Dramatic mark effect when cell is activated */} - - - )} - - {/* Hover effect for empty cells */} - {!value && isPlayerTurn && !gameOver && ( -
- )} - - {/* The X or O with text shadow */} - - {value} - -
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/CellAuraEffect.tsx b/examples/tictactoe/client/src/components/CellAuraEffect.tsx deleted file mode 100644 index 04677c555..000000000 --- a/examples/tictactoe/client/src/components/CellAuraEffect.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useEffect, useRef } from 'react'; -import type { PlayerSymbol } from '../types'; - -interface CellAuraEffectProps { - value: PlayerSymbol; - activated: boolean; -} - -export function CellAuraEffect({ value, activated }: CellAuraEffectProps) { - const canvasRef = useRef(null); - - // Colors based on player symbol - const color = value === 'X' - ? { primary: '#00e5ff', glow: 'rgba(0, 229, 255, ' } - : { primary: '#ff49e1', glow: 'rgba(255, 73, 225, ' }; - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set canvas dimensions to match the parent element (cell) - canvas.width = canvas.offsetWidth || 100; - canvas.height = canvas.offsetHeight || 100; - - // Create particles - const particlesArray: Particle[] = []; - const numberOfParticles = 20; - - class Particle { - x = 0; - y = 0; - size = 0; - speedX = 0; - speedY = 0; - opacity = 0; - age = 0; - lifespan = 0; - - constructor(exploding: boolean = false) { - // Place particles centrally - this.x = (canvas?.width || 100) / 2 + (Math.random() - 0.5) * 20; - this.y = (canvas?.height || 100) / 2 + (Math.random() - 0.5) * 20; - - this.size = Math.random() * 3 + 0.5; - - if (exploding) { - // Exploding particles move outward in all directions - const angle = Math.random() * Math.PI * 2; - const speed = Math.random() * 3 + 1; - this.speedX = Math.cos(angle) * speed; - this.speedY = Math.sin(angle) * speed; - this.opacity = 1; - } else { - // Normal particles drift randomly - this.speedX = (Math.random() - 0.5) * 0.5; - this.speedY = (Math.random() - 0.5) * 0.5; - this.opacity = Math.random() * 0.7; - } - - this.age = 0; - this.lifespan = 60 + Math.random() * 60; // 1-2 seconds at 60fps - } - - update() { - this.age++; - - // Move particle - this.x += this.speedX; - this.y += this.speedY; - - // Gradually slow down exploding particles - this.speedX *= 0.98; - this.speedY *= 0.98; - - // Fade out toward end of life - let alpha = this.opacity; - if (this.age > this.lifespan * 0.7) { - alpha = this.opacity * (1 - (this.age - this.lifespan * 0.7) / (this.lifespan * 0.3)); - } - - // Draw particle - if (ctx) { - ctx.beginPath(); - ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - ctx.fillStyle = color.glow + alpha + ')'; - - // Add glow effect - ctx.shadowColor = color.primary; - ctx.shadowBlur = 5; - ctx.fill(); - ctx.shadowBlur = 0; - } - - // Return true if particle is still alive - return this.age < this.lifespan; - } - } - - // Initialize particles - const init = () => { - particlesArray.length = 0; // Clear any existing particles - for (let i = 0; i < numberOfParticles; i++) { - particlesArray.push(new Particle()); - } - }; - - // Add explosion particles when the cell is activated - const addExplosionParticles = () => { - for (let i = 0; i < 30; i++) { - particlesArray.push(new Particle(true)); - } - }; - - let active = false; - - // Animation loop - const animate = () => { - if (!canvas || !ctx) return; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Update and filter particles (remove dead ones) - for (let i = 0; i < particlesArray.length; i++) { - if (!particlesArray[i].update()) { - particlesArray.splice(i, 1); - i--; - } - } - - // Add new particles to maintain a minimum count - while (particlesArray.length < numberOfParticles / 2) { - particlesArray.push(new Particle()); - } - - // Continue animation - animationFrameId = requestAnimationFrame(animate); - }; - - // Initialize and start animation - init(); - let animationFrameId = requestAnimationFrame(animate); - - // Add explosion effect when activated changes to true - if (activated && !active) { - addExplosionParticles(); - active = true; - } - - // Handle window resize - const handleResize = () => { - if (!canvas) return; - canvas.width = canvas.offsetWidth || 100; - canvas.height = canvas.offsetHeight || 100; - }; - - window.addEventListener('resize', handleResize); - - // Cleanup - return () => { - window.removeEventListener('resize', handleResize); - cancelAnimationFrame(animationFrameId); - }; - }, [color.glow, color.primary, activated]); - - return ( - - ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/CellMarkEffect.tsx b/examples/tictactoe/client/src/components/CellMarkEffect.tsx deleted file mode 100644 index ae1c1ee59..000000000 --- a/examples/tictactoe/client/src/components/CellMarkEffect.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState, useEffect } from 'react'; -import type { PlayerSymbol } from '../types'; -import { cn } from '../lib/utils'; - -interface CellMarkEffectProps { - value: PlayerSymbol; - activated: boolean; -} - -export function CellMarkEffect({ value, activated }: CellMarkEffectProps) { - const [showEffect, setShowEffect] = useState(false); - - // Reset and show the effect when activated changes - useEffect(() => { - if (activated) { - setShowEffect(true); - - // Remove elements after animation completes - const timer = setTimeout(() => { - setShowEffect(false); - }, 1500); // Match duration with animation - - return () => clearTimeout(timer); - } - }, [activated]); - - if (!showEffect) return null; - - const isX = value === 'X'; - // const baseColor = isX ? 'cyan' : 'magenta'; - - return ( -
- {/* Center burst */} -
- - {/* Orbiting particles */} - {Array.from({ length: 6 }).map((_, i) => ( -
- ))} - - {/* Random floating particles */} - {Array.from({ length: 12 }).map((_, i) => { - // Calculate random positions and delays - const angle = Math.random() * Math.PI * 2; - const distance = 20 + Math.random() * 40; - const size = 1 + Math.random() * 2; - const delay = Math.random() * 0.5; - const duration = 0.5 + Math.random() * 1; - - const x = Math.cos(angle) * distance; - const y = Math.sin(angle) * distance; - - return ( -
- ); - })} - - {/* Lines radiating outward */} - {Array.from({ length: 8 }).map((_, i) => { - const angle = (i * Math.PI) / 4; // 8 directions - const delay = i * 0.05; - - return ( -
- ); - })} - - {/* Add custom keyframes for line growth with a style tag */} - -
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ChannelDeposit.tsx b/examples/tictactoe/client/src/components/ChannelDeposit.tsx deleted file mode 100644 index 562cd6052..000000000 --- a/examples/tictactoe/client/src/components/ChannelDeposit.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useState } from "react"; -import { useChannel } from "../hooks/useChannel"; -import { WalletStore } from "../store"; -import { useStore } from "../store/storeUtils"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card"; -import { Button } from "./ui/button"; -import { cn } from "../lib/utils"; - -// Default USDC token address on Polygon -const USDC_ADDRESS = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; - -/** - * Component for depositing funds and creating a channel - */ -export function ChannelDeposit() { - const amount = "0.0001"; // Fixed amount - const [isProcessing, setIsProcessing] = useState(false); - const [error, setError] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); - - const { createChannel, depositToChannel, isChannelOpen } = useChannel(); - const wallet = useStore(WalletStore.state); - - // Fixed amount, no change handler needed - - const handleCreateChannel = async () => { - if (!amount || parseFloat(amount) <= 0) { - setError("Please enter a valid amount"); - return; - } - - setIsProcessing(true); - setError(null); - setSuccessMessage(null); - - try { - // Then deposit to it - await depositToChannel(USDC_ADDRESS, amount); - - // First create a channel - await createChannel(USDC_ADDRESS, amount); - - setSuccessMessage(`Successfully created channel and deposited ${amount} USDC`); - } catch (err) { - console.error("Channel creation/deposit error:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setIsProcessing(false); - } - }; - - if (isChannelOpen) { - return ( - - - Channel Active - - You have an active channel with {wallet.channelAmount ? Number(wallet.channelAmount) / 1000000 : "0"} USDC deposited. - - - - - - - ); - } - - return ( - - - Create Channel - Create a new channel by depositing USDC. This is required to play games. - - - {error &&
{error}
} - - {successMessage && ( -
{successMessage}
- )} - -
- -
-
- 0.0001 -
- - USDC - -
-
-
-

Fixed deposit amount: 0.0001 USDC

-
-
- - - -
- ); -} diff --git a/examples/tictactoe/client/src/components/ChannelRequiredModal.tsx b/examples/tictactoe/client/src/components/ChannelRequiredModal.tsx deleted file mode 100644 index 17641a374..000000000 --- a/examples/tictactoe/client/src/components/ChannelRequiredModal.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { useState, useEffect } from "react"; -import { Button } from "./ui/button"; -import { Loader2, AlertCircle, Coins, ArrowRight } from "lucide-react"; -import { cn } from "../lib/utils"; -// Since we're importing from viem which isn't configured yet, let's declare the types here -// type Address = `0x${string}`; -type Hex = `0x${string}`; -import { useChannel } from "../hooks/useChannel"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "./ui/dialog"; - -// USDC token address on the testnet -const USDC_ADDRESS: Hex = "0x07865c6E87B9F70255377e024ace6630C1Eaa37F"; - -interface ChannelRequiredModalProps { - isOpen: boolean; - onClose: () => void; - onSuccess: (action: "join" | "create", roomId?: string) => void; - mode: "join" | "create"; - roomId?: string; -} - -export function ChannelRequiredModal({ isOpen, onClose, onSuccess, mode, roomId }: ChannelRequiredModalProps) { - const amount = "0.0001"; // Fixed amount - const [step, setStep] = useState<"info" | "create" | "success">("info"); - const { createChannel, depositToChannel, isLoading, error, isChannelOpen } = useChannel(); - - // Check if the modal should stay open when there's no channel - useEffect(() => { - // If modal is open, check if channel ID exists - if (isOpen) { - const channelId = localStorage.getItem("nitrolite_channel_id"); - - // If channel is open in state and channel ID exists, proceed to success - if (isChannelOpen && channelId) { - console.log("Channel is already open and ID exists, closing modal without calling onSuccess"); - // Just close the modal without calling onSuccess to prevent double join message - onClose(); - } - } - }, [isChannelOpen, isOpen, onClose]); - - const handleCreateChannel = async () => { - // Fixed amount, no validation needed - - // No validation needed for fixed amount - - try { - console.log("Starting channel creation process with fixed amount:", amount); - // Immediately show the creating screen - setStep("create"); - - try { - // Step 1: Deposit to channel - console.log("Starting deposit with amount:", amount); - await depositToChannel(USDC_ADDRESS, amount); - console.log("Deposit successful, creating channel..."); - - // Step 2: Create channel - const result = await createChannel(USDC_ADDRESS, amount); - console.log("Channel creation successful:", result); - - // Only proceed to success if we have a valid result - if (result && result.channelId) { - // Make sure the channel ID exists in localStorage - localStorage.setItem("nitrolite_channel_id", result.channelId); - console.log("Saved channel ID to localStorage:", result.channelId); - - // Show success screen - setStep("success"); - - // Auto proceed after 1.5 seconds to allow the user to see the success message - setTimeout(() => { - // Log the room action before proceeding - console.log(`Proceeding with ${mode} action after channel creation, roomId:`, roomId); - onSuccess(mode, roomId); - onClose(); - }, 1500); - } else { - throw new Error("Channel creation successful but no channel ID returned"); - } - } catch (innerErr) { - console.error("Process failed during channel creation:", innerErr); - throw innerErr; // Re-throw to be caught by the outer catch - } - } catch (err) { - console.error("Channel creation failed:", err); - // Error state is handled by the useChannel hook - // Stay in the "create" step for 1.5 seconds so user sees the error before going back to info - setTimeout(() => { - setStep("info"); - }, 1500); - } - }; - - // No handleAmountChange needed for fixed amount - - return ( - { - if (!open) { - onClose(); - } - }}> - - {/* Background gradient */} -
- - {/* Particle effects */} -
-
-
- - {/* Content */} -
- - - {step === "info" && "Channel Required"} - {step === "create" && "Creating Channel..."} - {step === "success" && "Channel Created!"} - - - {step === "info" && "To play Nitro Aura, you need to create a payment channel by depositing USDC."} - {step === "create" && "Please confirm the transaction in your wallet."} - {step === "success" && "Your payment channel has been created successfully!"} - - - -
- {step === "info" && ( -
-
-

- - Why deposit USDC? -

-

- Nitro Aura uses payment channels to enable instant, secure gameplay without gas fees for each move. -

-

Your funds remain fully under your control and can be withdrawn at any time.

-
- -
- -
- $ - 0.0001 -
-

Fixed deposit amount: 0.0001 USDC

-
- - {error && ( -
- -
-

Error

-

{error}

-
-
- )} -
- )} - - {step === "create" && ( -
- -

Creating your channel...

-

Check your wallet for transaction confirmation

-
- )} - - {step === "success" && ( -
-
- - - -
-

Deposit Successful!

-

- Your payment channel is now active. -
- You'll be redirected to the game momentarily. -

-
- )} -
- - - {step === "info" && ( - <> - - - - )} - - {step === "create" && ( - - )} - - {step === "success" && ( - - )} - -
-
-
- ); -} diff --git a/examples/tictactoe/client/src/components/ErrorModal.tsx b/examples/tictactoe/client/src/components/ErrorModal.tsx deleted file mode 100644 index 578543172..000000000 --- a/examples/tictactoe/client/src/components/ErrorModal.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { AlertTriangle } from 'lucide-react'; -import { Button } from './ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from './ui/dialog'; - -interface ErrorModalProps { - message: string; - onClose: () => void; -} - -export function ErrorModal({ message, onClose }: ErrorModalProps) { - // Use a simple implementation without local state to avoid possible state conflicts - return ( - - - {/* Error glow effect */} -
- -
- -
- -
- - Connection Error - -
- -
-

{message}

-
- - - - -
-
-
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/GameLobbyIntegrated.tsx b/examples/tictactoe/client/src/components/GameLobbyIntegrated.tsx deleted file mode 100644 index e288e9168..000000000 --- a/examples/tictactoe/client/src/components/GameLobbyIntegrated.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useState, useEffect } from "react"; -import { Lobby } from "./Lobby"; -// import { ChannelDeposit } from './ChannelDeposit'; -import { useChannel } from "../hooks/useChannel"; -import { useWebSocketContext } from "../context/WebSocketContext"; -import { useNitroliteIntegration } from "../hooks/useNitroliteIntegration"; -import { useMetaMask } from "../hooks/useMetaMask"; -import type { JoinRoomPayload, AvailableRoom } from "../types"; -import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card"; -import { Loader2, AlertCircle } from "lucide-react"; - -/** - * Integrated game lobby that handles channel state - */ -interface GameLobbyIntegratedProps { - onJoinRoom: (payload: JoinRoomPayload) => void; - availableRooms: AvailableRoom[]; - onGetAvailableRooms: () => void; - onlineUsers?: number; -} - -export function GameLobbyIntegrated({ onJoinRoom, availableRooms = [], onGetAvailableRooms, onlineUsers = 1 }: GameLobbyIntegratedProps) { - const [isLoading, setIsLoading] = useState(true); - const { isConnected, status } = useWebSocketContext(); - const { clearStoredChannel } = useChannel(); - const { isConnected: isMetaMaskConnected } = useMetaMask(); - useNitroliteIntegration(); // Ensure proper integration - - // Handle channel initialization on component mount - useEffect(() => { - // Check if nitrolite_channel_id exists in localStorage - const channelId = localStorage.getItem('nitrolite_channel_id'); - if (!channelId) { - console.log("GameLobbyIntegrated: No nitrolite_channel_id found in localStorage"); - // If we need to force a channel creation, we can clear any stale state - clearStoredChannel(); - } else { - console.log("GameLobbyIntegrated: Found nitrolite_channel_id in localStorage:", channelId); - } - }, [clearStoredChannel]); - - // Simulate loading state - useEffect(() => { - const timer = setTimeout(() => { - setIsLoading(false); - }, 1000); - - return () => clearTimeout(timer); - }, []); - - // Show loading state - if (isLoading) { - return ( - <> -
- -
- - ); - } - - // Show Lobby component which has built-in MetaMask connection UI - if (!isMetaMaskConnected) { - return ( - <> - - - ); - } - - // Show WebSocket connection status if MetaMask is connected but WebSocket is not - if (!isConnected) { - return ( - <> - - - - WebSocket Status: {status} - - - Connecting to the game server... - {status === "reconnect_failed" && ( - Connection failed. Please refresh the page and try again. - )} - - - - - ); - } - - // The channel creation is now handled on-demand when joining/creating a game - // We'll keep this code commented for reference - /* - if (!isChannelOpen) { - return ( - <> - -
- - - Channel Required - - You need to create a channel and deposit funds to play games. - - - - - -
- - ); - } - */ - - // Show the game lobby when connected and channel is open - return ( - <> - - - ); -} diff --git a/examples/tictactoe/client/src/components/GameOver.tsx b/examples/tictactoe/client/src/components/GameOver.tsx deleted file mode 100644 index 456de876c..000000000 --- a/examples/tictactoe/client/src/components/GameOver.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useEffect } from 'react'; -import type { GameOver as GameOverType, PlayerSymbol } from '../types'; -import { Button } from './ui/button'; -import { cn } from '../lib/utils'; -import { Trophy, Medal, CircleSlash } from 'lucide-react'; -import { useSoundEffects } from '../hooks/useSoundEffects'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from './ui/dialog'; - -interface GameOverProps { - gameOver: GameOverType; - playerSymbol: PlayerSymbol | null; - onPlayAgain: () => void; -} - -export function GameOver({ gameOver, playerSymbol, onPlayAgain }: GameOverProps) { - const { winner } = gameOver; - const { playSound } = useSoundEffects(); - - // Play appropriate sound effect when component mounts - useEffect(() => { - if (winner === playerSymbol) { - playSound('win', 0.5); - } else if (winner) { - playSound('game-over', 0.5); - } else { - playSound('draw', 0.5); - } - }, [winner, playerSymbol, playSound]); - - // Determine message and styling based on game outcome - const getMessage = () => { - if (!winner) { - return "It's a Draw!"; - } - - return winner === playerSymbol ? "You Won!" : "You Lost!"; - }; - - // Get appropriate icon for result - const ResultIcon = !winner ? CircleSlash : (winner === playerSymbol ? Trophy : Medal); - - // Styles based on winner - const iconColor = winner === 'X' ? 'text-cyan-400' : winner === 'O' ? 'text-fuchsia-400' : 'text-gray-400'; - const bgGradient = winner === 'X' - ? 'from-cyan-900/30 to-gray-900/90' - : winner === 'O' - ? 'from-fuchsia-900/30 to-gray-900/90' - : 'from-gray-800/30 to-gray-900/90'; - - return ( - - - {/* Background gradient */} -
- - {/* Particle effects */} - {winner === playerSymbol && ( -
-
-
- )} - - {/* Content */} -
- -
- -
- - {getMessage()} - -
- -
-

- {winner ? `${winner} has won the game!` : "No more moves available."} -

-
- - - - -
-
-
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/GameScreen.tsx b/examples/tictactoe/client/src/components/GameScreen.tsx deleted file mode 100644 index a9aa523a6..000000000 --- a/examples/tictactoe/client/src/components/GameScreen.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { Board } from "./Board"; -import { GameStatus } from "./GameStatus"; -import { GameOver } from "./GameOver"; -import { RoomInfo } from "./RoomInfo"; -import type { GameState, GameOver as GameOverType, PlayerSymbol } from "../types"; -import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; -import { Button } from "./ui/button"; -import { useEffect, useState } from "react"; - -interface GameScreenProps { - gameState: GameState; - playerSymbol: PlayerSymbol | null; - isPlayerTurn: boolean; - isRoomReady: boolean; - isGameStarted: boolean; - isHost: boolean; - gameOver: GameOverType | null; - playerAddress: string; - opponentAddress: string; - roomId: string; - formatShortAddress: (address: string) => string; - onCellClick: (position: number) => void; - onPlayAgain: () => void; - onStartGame: () => void; - awaitingHostStart?: boolean; - isSigningInProgress?: boolean; -} - -export function GameScreen({ - gameState, - playerSymbol, - isPlayerTurn, - isRoomReady, - isGameStarted, - isHost, - gameOver, - playerAddress, - opponentAddress, - roomId, - formatShortAddress, - onCellClick, - onPlayAgain, - onStartGame, - awaitingHostStart = false, - isSigningInProgress = false, -}: GameScreenProps) { - const [hasChannelId, setHasChannelId] = useState(false); - - useEffect(() => { - const channelId = localStorage.getItem("nitrolite_channel_id"); - setHasChannelId(!!channelId); - }, []); - - // Debug information - console.log("GameScreen state:", { - roomId, - isRoomReady, - isGameStarted, - isHost, - playerSymbol, - opponentAddress, - hasChannelId, - }); - - return ( -
- - {/* Subtle background glow effect for the card */} -
- - - - - - - {/* Waiting for players or game start */} - {(!isRoomReady || !isGameStarted) && ( -
- {!isRoomReady ? ( - - - -
- {hasChannelId ? "Waiting for another player to join..." : "Preparing your game..."} -
-
- - {roomId && hasChannelId && ( -
-

Share this room ID:

-
- {roomId} -
-
- )} - {hasChannelId ? ( -

Players need this ID to join your game

- ) : ( -
-
-

Creating your channel...

-

Please wait while we set up your game

-
- )} -
-
- ) : isHost ? ( - - - - - Game ready! You are the host - - - - - - - ) : ( - - - - - Game ready! - - - -

Waiting for host to start the game...

-
-
- Please wait -
-
-
- )} -
- )} - - {/* Game Status - only show when game is started */} - {isGameStarted && ( -
- -
- )} - - {/* Game Board - only show when game is started */} - {isGameStarted && ( -
- -
- )} -
-
- - {/* Game Over Modal */} - {gameOver && } -
- ); -} diff --git a/examples/tictactoe/client/src/components/GameStatus.tsx b/examples/tictactoe/client/src/components/GameStatus.tsx deleted file mode 100644 index 749dec4fb..000000000 --- a/examples/tictactoe/client/src/components/GameStatus.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import type { PlayerSymbol } from '../types'; -import { cn } from '../lib/utils'; -import { Badge } from './ui/badge'; -import { UserCheck, User, Clock } from 'lucide-react'; - -interface GameStatusProps { - isPlayerTurn: boolean; - playerSymbol: PlayerSymbol | null; - isRoomReady: boolean; - isGameStarted: boolean; - playerAddress: string; - opponentAddress: string; - formatShortAddress: (address: string) => string; -} - -export function GameStatus({ - isPlayerTurn, - playerSymbol, - playerAddress, - opponentAddress, - formatShortAddress -}: GameStatusProps) { - // This component is only shown when the game has started, so we don't need - // to handle the waiting states here anymore - they're handled in GameScreen - - // Status message and styling - const statusMessage = isPlayerTurn - ? "Your Move" - : "Opponent's Turn"; - - const playerBadgeVariant = playerSymbol === 'X' ? 'cyan' : 'magenta'; - const opponentBadgeVariant = playerSymbol === 'X' ? 'magenta' : 'cyan'; - - // Get player colors - const playerColor = playerSymbol === 'X' ? 'text-cyan-400' : 'text-fuchsia-400'; - const opponentColor = playerSymbol === 'X' ? 'text-fuchsia-400' : 'text-cyan-400'; - - // Glow classes - const playerGlow = playerSymbol === 'X' ? 'glow-cyan' : 'glow-magenta'; - const opponentGlow = playerSymbol === 'X' ? 'glow-magenta' : 'glow-cyan'; - - return ( -
- {/* Turn indicator */} - - {/* Animated background for active player */} - {isPlayerTurn && ( -
-
-
- )} - -
- {isPlayerTurn ? ( - - ) : ( - - )} - {statusMessage} -
-
- - {/* Player info */} -
- {/* Player (You) */} -
- {/* Active player indicator */} - {isPlayerTurn && ( -
- )} - - - - {playerSymbol} - - - {formatShortAddress(playerAddress)} - - (you) -
- - {/* VS */} -
- VS - - {isPlayerTurn ? "your turn" : "waiting"} - -
- - {/* Opponent */} -
- {/* Active player indicator */} - {!isPlayerTurn && ( -
- )} - - - - {playerSymbol === 'X' ? 'O' : 'X'} - - - {formatShortAddress(opponentAddress)} - -
-
-
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/Lobby.tsx b/examples/tictactoe/client/src/components/Lobby.tsx deleted file mode 100644 index 23f8682ca..000000000 --- a/examples/tictactoe/client/src/components/Lobby.tsx +++ /dev/null @@ -1,459 +0,0 @@ -import { useState, useEffect } from "react"; -import type { JoinRoomPayload, AvailableRoom } from "../types"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; -import { Input } from "./ui/input"; -import { Button } from "./ui/button"; -import { cn } from "../lib/utils"; -import { Wallet, Users, Loader2, KeyRound, GamepadIcon, RefreshCw, Clock, AlertCircle } from "lucide-react"; -import { useAccount } from "wagmi"; -import { StyledWalletButton } from "./ui/styled-wallet-button"; -import { ChannelRequiredModal } from "./ChannelRequiredModal"; -import { OnlinePlayersCounter } from "./OnlinePlayersCounter"; - -interface LobbyProps { - onJoinRoom: (payload: JoinRoomPayload) => void; - isConnected: boolean; - error: string | null; - availableRooms?: AvailableRoom[]; - onGetAvailableRooms: () => void; - onlineUsers?: number; -} - -export function Lobby({ onJoinRoom, isConnected, error, availableRooms = [], onGetAvailableRooms, onlineUsers = 1 }: LobbyProps) { - const [roomId, setRoomId] = useState(""); - const [roomIdError, setRoomIdError] = useState(""); - const [mode, setMode] = useState<"create" | "join">("create"); - const [loadingRooms, setLoadingRooms] = useState(false); - const [showChannelModal, setShowChannelModal] = useState(false); - const [pendingRoomAction, setPendingRoomAction] = useState<{ mode: "create" | "join"; roomId?: string } | null>(null); - - // Use channel hook to check if channel exists - const { isChannelOpen } = { isChannelOpen: true }; - - // Use wagmi hook for wallet connection - const { address, isConnected: isWalletConnected } = useAccount(); - - // Check if MetaMask is installed - const isMetaMaskInstalled = typeof window !== 'undefined' && typeof window.ethereum !== 'undefined'; - - // Fetch available rooms when tab changes to 'join' - useEffect(() => { - if (mode === "join" && isConnected && isWalletConnected) { - setLoadingRooms(true); - onGetAvailableRooms(); - // Set a timeout to hide the loading indicator after 2 seconds - // (in case the server doesn't respond or takes too long) - const timeoutId = setTimeout(() => { - setLoadingRooms(false); - }, 2000); - - return () => clearTimeout(timeoutId); - } - }, [mode, isConnected, isWalletConnected, onGetAvailableRooms]); - - // When availableRooms changes, stop the loading indicator - useEffect(() => { - setLoadingRooms(false); - }, [availableRooms]); - - // Validate Room ID format for joining - const validateRoomId = (id: string): boolean => { - // For joining, room ID is required - if (mode === "join") { - if (!id.trim()) { - setRoomIdError("Room ID is required when joining a game"); - return false; - } - - // Should be a valid UUID format (8-4-4-4-12 hex chars) - const isValid = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/.test(id); - setRoomIdError(isValid ? "" : "Please enter a valid room ID (UUID format)"); - return isValid; - } - - // For creating, room ID is ignored - return true; - }; - - // Handle tab change - const handleTabChange = (value: string) => { - setMode(value as "create" | "join"); - setRoomIdError(""); - - // Fetch available rooms when switching to join tab - if (value === "join" && isConnected && isWalletConnected) { - onGetAvailableRooms(); - } - }; - - // Helper function to format time since creation - const formatTimeAgo = (timestamp: number): string => { - const now = Date.now(); - const secondsAgo = Math.floor((now - timestamp) / 1000); - - if (secondsAgo < 60) { - return `${secondsAgo} second${secondsAgo !== 1 ? "s" : ""} ago`; - } - - const minutesAgo = Math.floor(secondsAgo / 60); - if (minutesAgo < 60) { - return `${minutesAgo} minute${minutesAgo !== 1 ? "s" : ""} ago`; - } - - const hoursAgo = Math.floor(minutesAgo / 60); - return `${hoursAgo} hour${hoursAgo !== 1 ? "s" : ""} ago`; - }; - - // Handle joining a specific available room - const handleJoinAvailableRoom = (selectedRoomId: string) => { - if (!isWalletConnected || !address) { - return; - } - - // Check if channel exists first - if (!isChannelOpen) { - setPendingRoomAction({ mode: "join", roomId: selectedRoomId }); - setShowChannelModal(true); - return; - } - - // Use MetaMask wallet address for app session participants - console.log("Joining available room with MetaMask address:", address, "and roomId:", selectedRoomId); - onJoinRoom({ eoa: address, roomId: selectedRoomId }); - }; - - // Handle manual refresh of available rooms - const handleRefreshRooms = () => { - if (isConnected && isWalletConnected) { - setLoadingRooms(true); - onGetAvailableRooms(); - } - }; - - // Handle form submission - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - - if (!isWalletConnected || !address) { - return; - } - - const isRoomIdValid = validateRoomId(roomId); - - if (!isRoomIdValid && mode === "join") { - return; - } - - // Check if channel exists first - if (!isChannelOpen) { - if (mode === "create") { - setPendingRoomAction({ mode: "create" }); - } else { - setPendingRoomAction({ mode: "join", roomId: roomId.trim() }); - } - setShowChannelModal(true); - return; - } - - // Use MetaMask wallet address for app session participants - if (mode === "create") { - // When creating a room, always pass undefined for roomId - console.log("Creating a room with MetaMask address:", address); - onJoinRoom({ eoa: address, roomId: undefined }); - } else { - // When joining, use the entered roomId - console.log("Joining a room with MetaMask address:", address, "and roomId:", roomId.trim()); - onJoinRoom({ eoa: address, roomId: roomId.trim() }); - } - }; - - // Handle successful channel creation - const handleChannelSuccess = (action: "join" | "create", roomIdParam?: string) => { - if (!address) return; - - // Use MetaMask wallet address for app session participants - - // Add a debounce mechanism to prevent duplicate calls - const now = Date.now(); - const lastCallTime = window.localStorage.getItem("last_join_call_time"); - const throttleTime = 2000; // 2 seconds - - if (lastCallTime && now - parseInt(lastCallTime) < throttleTime) { - console.log("Throttling joinRoom call - too soon after previous call"); - return; - } - - // Store this call time - window.localStorage.setItem("last_join_call_time", now.toString()); - - if (action === "create") { - console.log("Creating a room with MetaMask address after channel creation:", address); - onJoinRoom({ eoa: address, roomId: undefined }); - } else { - console.log("Joining a room with MetaMask address after channel creation:", address, "and roomId:", roomIdParam); - onJoinRoom({ eoa: address, roomId: roomIdParam }); - } - }; - - - return ( -
- {/* Channel creation modal */} - setShowChannelModal(false)} - onSuccess={handleChannelSuccess} - mode={pendingRoomAction?.mode || "create"} - roomId={pendingRoomAction?.roomId} - /> - {/* Animated glow behind card */} -
- - - {/* Header with particle effect */} - -
-
-
- - - Nitro - Aura - - Every move leaves an aura. -
- - - {!isMetaMaskInstalled ? ( -
- ) : !isWalletConnected ? ( -
-
- -
-

- Connect Your Wallet -

-

Connect your MetaMask wallet to create or join games.

- -
-
-
- ) : ( - - - - - Create Game - - - - Join Game - - - -
- {/* Wallet address display */} -
- -
-
{address}
-
-
- - Connected -
-
-
-
- - {/* Join tab content */} - - {/* Betting amount notice */} -
-

Game Stakes

-

- Each player bets $0.01. Winner takes all! -

-
- - {/* Available games list */} -
-
-

- - Available Games -

- -
- - {loadingRooms ? ( -
-
- -

Loading available games...

-
-
- ) : availableRooms.length > 0 ? ( -
- {availableRooms.map((room) => ( -
-
-
-
- - {room.roomId} - -
-
- - Created {formatTimeAgo(room.createdAt)} -
-
- -
-
- ))} -
- ) : ( -
- -

No games available

-

Create a new game or try again later

-
- )} -
- - {/* Manual room ID entry */} -
-
- -

Or enter room ID manually

-
- setRoomId(e.target.value)} - placeholder="Enter the room ID to join" - icon={} - variant="magenta" - className={cn(roomIdError && "border-red-500 focus-visible:ring-red-500")} - /> - {roomIdError && ( -

- - {roomIdError} -

- )} -

Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

-
-
- - {/* Create tab content */} - - {/* Betting amount notice */} -
-

Game Stakes

-

- Each player bets $0.01. Winner takes all! -

-
- -
-

- - Host a New Game -

-

You'll create a room and get a Room ID to share with your opponent.

-
-
- - {/* Error message */} - {error && ( -
-

Error

-

{error}

-
- )} - - {/* Submit button */} - -
-
- )} - - - {/* Card footer with tagline and online counter */} - -

Light speed, neon bleed.

-
- -
-
- -
- ); -} diff --git a/examples/tictactoe/client/src/components/OnlinePlayersCounter.tsx b/examples/tictactoe/client/src/components/OnlinePlayersCounter.tsx deleted file mode 100644 index 93132edf9..000000000 --- a/examples/tictactoe/client/src/components/OnlinePlayersCounter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Users } from "lucide-react"; -import { Badge } from "./ui/badge"; -import { cn } from "../lib/utils"; -import { useEffect } from "react"; - -interface OnlinePlayersCounterProps { - className?: string; - count?: number; -} - -export function OnlinePlayersCounter({ className, count = 1 }: OnlinePlayersCounterProps) { - // For debugging - log count when it changes - useEffect(() => { - console.log("OnlinePlayersCounter rendering with count:", count); - }, [count]); - - return ( -
- - - {count} online - -
- ); -} diff --git a/examples/tictactoe/client/src/components/README_AURA_EFFECTS.md b/examples/tictactoe/client/src/components/README_AURA_EFFECTS.md deleted file mode 100644 index 51f72848c..000000000 --- a/examples/tictactoe/client/src/components/README_AURA_EFFECTS.md +++ /dev/null @@ -1,82 +0,0 @@ -# Nitro Aura Visual Effects Guide - -This document provides an overview of the particle and aura visual effects implemented in the Nitro Aura game. - -## Core Components - -### 1. CellAuraEffect - -Located in `CellAuraEffect.tsx`, this component creates dynamic canvas-based particle effects that appear around cells when a player marks X or O. The effects are customized based on the player symbol (cyan for X, magenta for O). - -Key features: -- Canvas-based rendering for performance -- Dynamic particle generation and movement -- Particle fading and lifecycle management -- Explosion effect when a cell is first marked - -### 2. CellMarkEffect - -Located in `CellMarkEffect.tsx`, this component adds dramatic visual effects when a player marks a cell, including: -- Center burst animation -- Orbiting particles -- Radiating lines -- Random floating particles - -The component uses CSS animations and transforms for these effects. - -### 3. BackgroundAnimation - -Located in `BackgroundAnimation.tsx`, this component renders the global background particle effect. Features include: -- Floating particles across the entire game background -- Interactive effects that respond to mouse movement -- Connection lines between nearby particles -- Burst effects triggered by cell marks via custom events - -## CSS and Animation - -### Custom Animations - -The following custom animations are defined in `tailwind.config.js`: -- `sparkle`: Particles moving upward with rotation -- `float`: Gentle vertical floating motion -- `pulse-cyan` and `pulse-magenta`: Glowing pulsation effects -- `explode-cyan` and `explode-magenta`: Explosion animations -- `orbit`: Circular movement for particles - -Additional CSS animations in `App.css`: -- Particle movement -- Glow effects -- Line growth animations - -## Event Communication - -Components communicate through custom events: -- `cellMarked`: Dispatched when a cell is marked with X or O, includes position and player data -- This event triggers background particle bursts and sound effects - -## Sound Effects - -Sound feedback is provided through the `useSoundEffects` hook, which: -- Generates programmatic audio for different game events -- Plays appropriate sounds for marking cells and game outcomes -- Adjusts volume and timing for optimal user experience - -## Integration Points - -The effects are integrated at these key points: -1. In `Cell.tsx` when a player marks a cell -2. In `GameOver.tsx` for win/lose/draw animations -3. Throughout the game interface with consistent cyan/magenta theming - -## Extending the Effects - -To add new visual effects: -1. Use the existing color scheme (cyan for X, magenta for O) -2. Follow the pattern of component-based effects -3. Utilize custom events for communication between components -4. Add new animations to tailwind.config.js -5. Consider performance implications for mobile devices - ---- - -*"Every move leaves an aura" - Nitro Aura* \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/RoomInfo.tsx b/examples/tictactoe/client/src/components/RoomInfo.tsx deleted file mode 100644 index 98df3dd6f..000000000 --- a/examples/tictactoe/client/src/components/RoomInfo.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Copy, Check } from 'lucide-react'; -import { Badge } from './ui/badge'; -import { Button } from './ui/button'; -import { useState, useEffect } from 'react'; -import { cn } from '../lib/utils'; - -interface RoomInfoProps { - roomId: string; -} - -export function RoomInfo({ roomId }: RoomInfoProps) { - const [copied, setCopied] = useState(false); - - // Copy room ID to clipboard - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(roomId); - setCopied(true); - } catch (err) { - console.error('Failed to copy room ID', err); - } - }; - - // Reset copied state after a delay - useEffect(() => { - if (copied) { - const timeout = setTimeout(() => setCopied(false), 2000); - return () => clearTimeout(timeout); - } - }, [copied]); - - // Don't show anything if roomId is not set yet - if (!roomId) { - return null; - } - - return ( -
- - Room: - - {roomId.length > 12 ? `${roomId.substring(0, 8)}...` : roomId} - - - -
- ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/badge.tsx b/examples/tictactoe/client/src/components/ui/badge.tsx deleted file mode 100644 index 337d5cf29..000000000 --- a/examples/tictactoe/client/src/components/ui/badge.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "../../lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-gray-800 text-gray-200 shadow hover:bg-gray-700/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - cyan: "border-transparent bg-cyan-500/20 text-cyan-400 border-cyan-500/30", - magenta: "border-transparent bg-fuchsia-500/20 text-fuchsia-400 border-fuchsia-500/30", - success: "border-transparent bg-green-500/20 text-green-400 border-green-500/30", - warning: "border-transparent bg-amber-500/20 text-amber-400 border-amber-500/30", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ); -} - -export { Badge, badgeVariants }; \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/button.tsx b/examples/tictactoe/client/src/components/ui/button.tsx deleted file mode 100644 index 9d0c78aa9..000000000 --- a/examples/tictactoe/client/src/components/ui/button.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "../../lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 transform hover:scale-[1.02] active:scale-[0.98]", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90 hover:shadow-primary/20 hover:shadow-lg", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow-lg", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 hover:shadow-secondary/20 hover:shadow-lg", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - cyan: "bg-gradient-to-r from-cyan-600 to-cyan-700 text-white shadow-md hover:shadow-cyan-500/30 hover:shadow-lg hover:from-cyan-500 hover:to-cyan-600 border border-cyan-700/30", - magenta: "bg-gradient-to-r from-fuchsia-600 to-fuchsia-700 text-white shadow-md hover:shadow-fuchsia-500/30 hover:shadow-lg hover:from-fuchsia-500 hover:to-fuchsia-600 border border-fuchsia-700/30", - glass: "bg-gray-900/30 backdrop-blur-md border border-gray-800/40 text-white shadow-sm hover:bg-gray-900/40 hover:border-cyan-800/30", - glowCyan: "bg-gray-900/60 text-cyan-400 border border-cyan-900/50 shadow-[0_0_10px_rgba(0,229,255,0.1)] hover:shadow-[0_0_15px_rgba(0,229,255,0.2)] hover:bg-gray-900/80 hover:border-cyan-800/60", - glowMagenta: "bg-gray-900/60 text-fuchsia-400 border border-fuchsia-900/50 shadow-[0_0_10px_rgba(255,73,225,0.1)] hover:shadow-[0_0_15px_rgba(255,73,225,0.2)] hover:bg-gray-900/80 hover:border-fuchsia-800/60", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - xl: "h-12 rounded-md px-8 text-base font-medium", - xxl: "h-14 rounded-md px-10 text-lg font-medium", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, leftIcon, rightIcon, children, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - {leftIcon && {leftIcon}} - {children} - {rightIcon && {rightIcon}} - - ); - } -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/card.tsx b/examples/tictactoe/client/src/components/ui/card.tsx deleted file mode 100644 index 570805480..000000000 --- a/examples/tictactoe/client/src/components/ui/card.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from "react"; - -import { cn } from "../../lib/utils"; - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -CardFooter.displayName = "CardFooter"; - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/dialog.tsx b/examples/tictactoe/client/src/components/ui/dialog.tsx deleted file mode 100644 index c7f89b209..000000000 --- a/examples/tictactoe/client/src/components/ui/dialog.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" - -import { cn } from "../../lib/utils" - -// Modified to include proper modal support -const Dialog = ({ modal = false, ...props }: React.ComponentPropsWithoutRef & { modal?: boolean }) => ( - -) -Dialog.displayName = "Dialog" - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, style, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/input.tsx b/examples/tictactoe/client/src/components/ui/input.tsx deleted file mode 100644 index 12928e265..000000000 --- a/examples/tictactoe/client/src/components/ui/input.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from "react"; -import { cva } from "class-variance-authority"; -import { cn } from "../../lib/utils"; - -const inputVariants = cva( - "flex h-10 w-full rounded-md px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200", - { - variants: { - variant: { - default: - "border border-gray-700/50 bg-gray-800/70 text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-cyan-500 focus-visible:border-cyan-500/50", - cyan: - "border border-cyan-800/40 bg-gray-800/70 text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-cyan-500 focus-visible:border-cyan-500/50", - magenta: - "border border-fuchsia-800/40 bg-gray-800/70 text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-fuchsia-500 focus-visible:border-fuchsia-500/50", - glass: - "border border-gray-700/30 bg-gray-900/40 backdrop-blur-md text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-cyan-500/50", - }, - size: { - default: "h-10", - sm: "h-8 text-xs px-2.5", - lg: "h-12 text-base", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface InputProps - extends Omit, 'size' | 'prefix'> { - icon?: React.ReactNode; - prefix?: string; - variant?: 'default' | 'cyan' | 'magenta' | 'glass'; - size?: 'default' | 'sm' | 'lg'; -} - -const Input = React.forwardRef( - ({ className, type, variant, size, icon, prefix, ...props }, ref) => { - return ( -
- {icon && ( -
- {icon} -
- )} - {prefix && ( -
- {prefix} -
- )} - -
- ); - } -); -Input.displayName = "Input"; - -export { Input, inputVariants }; \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/styled-wallet-button.tsx b/examples/tictactoe/client/src/components/ui/styled-wallet-button.tsx deleted file mode 100644 index 7de8c8758..000000000 --- a/examples/tictactoe/client/src/components/ui/styled-wallet-button.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { WalletButton } from "@rainbow-me/rainbowkit"; -import { Button } from "./button"; -import { Wallet, Loader2 } from "lucide-react"; - -export function StyledWalletButton() { - return ( - - {({ ready, connect, loading }) => { - return ( - - ); - }} - - ); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/components/ui/tabs.tsx b/examples/tictactoe/client/src/components/ui/tabs.tsx deleted file mode 100644 index 4c373c832..000000000 --- a/examples/tictactoe/client/src/components/ui/tabs.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" - -import { cn } from "../../lib/utils" - -const Tabs = TabsPrimitive.Root - -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsList.displayName = TabsPrimitive.List.displayName - -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName - -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsContent.displayName = TabsPrimitive.Content.displayName - -export { Tabs, TabsList, TabsTrigger, TabsContent } \ No newline at end of file diff --git a/examples/tictactoe/client/src/context/NitroliteClientWrapper.tsx b/examples/tictactoe/client/src/context/NitroliteClientWrapper.tsx deleted file mode 100644 index bda229648..000000000 --- a/examples/tictactoe/client/src/context/NitroliteClientWrapper.tsx +++ /dev/null @@ -1,237 +0,0 @@ -"use client"; - -import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; -import { createPublicClient, createWalletClient, custom, http, type Hex } from "viem"; -import { NitroliteStore, WalletStore } from "../store"; -import { NitroliteClient, type ContractAddresses } from "@erc7824/nitrolite"; - -import { ethers } from "ethers"; -import { generateKeyPair } from "./createSigner"; -import { polygon } from "viem/chains"; -import APP_CONFIG from "./app"; -import { useMetaMask } from "../hooks/useMetaMask"; - -const CRYPTO_KEYPAIR_KEY = "crypto_keypair"; - -export const CHAINS = polygon; - -export const USDC_ADDRESS = APP_CONFIG.TOKENS[polygon.id] as Hex; - -// Create context for the Nitrolite client -interface NitroliteContextType { - client: NitroliteClient | null; - loading: boolean; - error: string | null; -} - -const NitroliteContext = createContext({ - client: null, - loading: true, - error: null, -}); - -// Hook for components to use the Nitrolite client -export const useNitrolite = () => useContext(NitroliteContext); - -interface NitroliteClientWrapperProps { - children?: React.ReactNode; -} - -export function NitroliteClientWrapper({ children }: NitroliteClientWrapperProps) { - const [clientState, setClientState] = useState({ - client: null, - loading: true, - error: null, - }); - - // Use MetaMask hook for wallet connection - const { address, isConnected } = useMetaMask(); - - const initializeKeys = useCallback(async (): Promise<{ keyPair: unknown; stateWalletClient: unknown }> => { - try { - let keyPair = null; - const savedKeys = localStorage.getItem(CRYPTO_KEYPAIR_KEY); - - if (savedKeys) { - try { - keyPair = JSON.parse(savedKeys); - } catch (error) { - keyPair = null; - console.error("Failed to parse saved keys:", error); - } - } - - if (!keyPair) { - keyPair = await generateKeyPair(); - if (typeof window !== "undefined") { - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(keyPair)); - } - } - - const wallet = new ethers.Wallet(keyPair.privateKey); - - const stateWalletClient = { - ...wallet, - account: { - address: wallet.address, - }, - signMessage: async ({ message: { raw } }: { message: { raw: string } }) => { - const { serialized: signature } = wallet.signingKey.sign(raw as ethers.BytesLike); - - return signature as Hex; - }, - }; - - return { keyPair, stateWalletClient }; - } catch (error) { - console.error("Failed to initialize keys:", error); - return { keyPair: null, stateWalletClient: null }; - } - }, []); - - useEffect(() => { - const initializeNitrolite = async () => { - try { - setClientState((prev) => ({ ...prev, loading: true, error: null })); - - // Only proceed if MetaMask is connected - if (!isConnected || !address) { - setClientState((prev) => ({ - ...prev, - loading: false, - error: "MetaMask not connected. Please connect your wallet.", - })); - return; - } - - // Check if window.ethereum is available - if (!(window as any).ethereum) { - setClientState((prev) => ({ - ...prev, - loading: false, - error: "MetaMask provider not found. Please refresh the page or reinstall MetaMask.", - })); - return; - } - - const keyInitResult = await initializeKeys(); - - if (!keyInitResult || !keyInitResult.stateWalletClient) { - throw new Error("Failed to initialize state wallet client keys."); - } - const { stateWalletClient } = keyInitResult; - - const publicClient = createPublicClient({ - transport: http(), - chain: polygon, - }); - - // Use MetaMask provider for the walletClient - console.log("Creating wallet client with ethereum provider..."); - const ethereum = (window as any).ethereum; - console.log("Ethereum provider:", ethereum ? "available" : "not available"); - - if (!ethereum) { - throw new Error("Ethereum provider not found in window object"); - } - - // Create the wallet client using the ethereum provider - const walletClient = createWalletClient({ - transport: custom(ethereum), - chain: polygon, - account: address as Hex, - }); - - WalletStore.setWalletClient(walletClient); - - console.log("Wallet client created successfully:", walletClient.account); - - const addresses: ContractAddresses = { - custody: APP_CONFIG.CUSTODIES[polygon.id], - adjudicator: APP_CONFIG.ADJUDICATORS[polygon.id], - guestAddress: APP_CONFIG.CHANNEL.DEFAULT_GUEST as Hex, - }; - - const challengeDuration = APP_CONFIG.CHANNEL.CHALLENGE_PERIOD; - - console.log("Creating Nitrolite client with params:", { - publicClientAvailable: !!publicClient, - walletClientAvailable: !!walletClient, - stateWalletClientAvailable: !!stateWalletClient, - account: walletClient.account, - chainId: polygon.id, - challengeDuration: challengeDuration.toString(), - addresses: { - custody: addresses.custody, - adjudicator: addresses.adjudicator, - guestAddress: addresses.guestAddress, - }, - }); - - // Create the Nitrolite client - const client = new NitroliteClient({ - // @ts-ignore - publicClient, - // @ts-ignore - walletClient, - // @ts-ignore - stateWalletClient: stateWalletClient, - account: walletClient.account, - chainId: polygon.id, - challengeDuration: challengeDuration, - addresses, - }); - - // Check if client was created successfully - if (!client) { - throw new Error("Nitrolite client creation failed - client is null"); - } - - console.log("Nitrolite client initialized successfully!"); - - // Store the client in the global store for access elsewhere - NitroliteStore.setClient(client); - - setClientState({ - client, - loading: false, - error: null, - }); - } catch (error: unknown) { - console.error("Failed to initialize Nitrolite client:", error); - - // Provide more specific error messages based on the error - let errorMessage = "Failed to initialize Nitrolite client"; - - if (error instanceof Error) { - if (error.message.includes("provider")) { - errorMessage = "MetaMask provider error. Please refresh the page and try again."; - } else if (error.message.includes("wallet")) { - errorMessage = "Wallet client creation failed. Please ensure MetaMask is connected properly."; - } else { - // Include the actual error message for debugging - errorMessage = `Nitrolite client error: ${error.message}`; - } - - // Log additional details for debugging - console.debug("Error details:", { - message: error.message, - stack: error.stack, - address: address || "not available", - }); - } - - setClientState({ - client: null, - loading: false, - error: errorMessage, - }); - } - }; - - initializeNitrolite(); - }, [initializeKeys, address, isConnected]); - - // Provide the client through context - return {children}; -} diff --git a/examples/tictactoe/client/src/context/WebSocketContext.tsx b/examples/tictactoe/client/src/context/WebSocketContext.tsx deleted file mode 100644 index 739995167..000000000 --- a/examples/tictactoe/client/src/context/WebSocketContext.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode, useRef } from "react"; -import { WebSocketClient, createWebSocketClient, type WSStatus, type WalletSigner, getAddressFromPublicKey } from "../websocket"; -import type { Channel } from "@erc7824/nitrolite"; -import APP_CONFIG from "./app"; -import { generateKeyPair, createEthersSigner } from "./createSigner"; -import { WalletStore } from "../store"; - -// Interface for key pairs -export interface CryptoKeypair { - privateKey: string; - publicKey?: string; - address?: string; -} - -const CRYPTO_KEYPAIR_KEY = "crypto_keypair"; -const WS_URL = APP_CONFIG.WEBSOCKET.URL; - -interface WebSocketContextProps { - client: WebSocketClient | null; - status: WSStatus; - keyPair: CryptoKeypair | null; - wsChannel: Channel | null; - currentNitroliteChannel: Channel | null; - isConnected: boolean; - hasKeys: boolean; - generateKeys: () => Promise; - connect: () => Promise; - disconnect: () => void; - setNitroliteChannel: (channel: Channel) => void; - clearKeys: () => void; - sendPing: () => Promise; - sendRequest: (payload: string) => Promise; -} - -const WebSocketContext = createContext(undefined); - -export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [status, setStatus] = useState("disconnected"); - const [keyPair, setKeyPair] = useState(null); - const [currentSigner, setCurrentSigner] = useState(null); - const [wsChannel, setWsChannel] = useState(null); - const [currentNitroliteChannel, setCurrentNitroliteChannel] = useState(null); - const clientRef = useRef(null); - - useEffect(() => { - if (typeof window !== "undefined") { - const savedKeys = localStorage.getItem(CRYPTO_KEYPAIR_KEY); - - if (savedKeys) { - try { - const parsed = JSON.parse(savedKeys) as CryptoKeypair; - - if (parsed.publicKey && !parsed.address) { - parsed.address = getAddressFromPublicKey(parsed.publicKey); - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(parsed)); - } - setKeyPair(parsed); - console.log("Loaded existing keys from storage"); - // @ts-ignore - } catch (e) { - console.error("Failed to parse saved keys - will generate new ones"); - localStorage.removeItem(CRYPTO_KEYPAIR_KEY); - generateNewKeysAndStore(); - } - } else { - console.log("No saved keys found - generating new ones"); - generateNewKeysAndStore(); - } - } - }, []); - - const generateNewKeysAndStore = async () => { - try { - const newKeyPair = await generateKeyPair(); - setKeyPair(newKeyPair); - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(newKeyPair)); - console.log("Generated and stored new crypto keys"); - return newKeyPair; - } catch (error) { - console.error("Error generating and storing keys:", error); - return null; - } - }; - - const generateKeys = useCallback(async () => { - try { - if (typeof window !== "undefined") { - const savedKeys = localStorage.getItem(CRYPTO_KEYPAIR_KEY); - - if (savedKeys) { - try { - const parsed = JSON.parse(savedKeys) as CryptoKeypair; - - if (parsed && typeof parsed.privateKey === "string" && typeof parsed.publicKey === "string") { - if (parsed.publicKey && !parsed.address) { - parsed.address = getAddressFromPublicKey(parsed.publicKey); - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(parsed)); - } - setKeyPair(parsed); - const signer = createEthersSigner(parsed.privateKey); - - setCurrentSigner(signer); - return parsed; - } - } catch (e) { - // Could not parse, fall through and generate keys. - console.error("Failed to parse saved keys during generateKeys:", e); - localStorage.removeItem(CRYPTO_KEYPAIR_KEY); - } - } - } - // If no valid keys in storage, generate new ones. - const newKeyPair = await generateKeyPair(); - - setKeyPair(newKeyPair); - if (typeof window !== "undefined") { - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(newKeyPair)); - } - const newSigner = createEthersSigner(newKeyPair.privateKey); - - setCurrentSigner(newSigner); - console.log("Generated new cryptographic keys"); - return newKeyPair; - } catch (error) { - const errorMsg = `Error generating keys: ${error instanceof Error ? error.message : String(error)}`; - console.error(errorMsg); - return null; - } - }, []); - - const clearKeys = useCallback(() => { - if (typeof window !== "undefined") { - localStorage.removeItem(CRYPTO_KEYPAIR_KEY); - } - setKeyPair(null); - setCurrentSigner(null); - if (clientRef.current?.isConnected) { - clientRef.current.close(); - } - console.log("Cleared cryptographic keys"); - }, []); - - useEffect(() => { - if (keyPair?.privateKey && !currentSigner) { - try { - const signer = createEthersSigner(keyPair.privateKey); - setCurrentSigner(signer); - console.log("Initialized signer from keys"); - } catch (e) { - console.error(`Failed to create signer: ${e instanceof Error ? e.message : String(e)}`); - } - } - }, [keyPair, currentSigner]); - - useEffect(() => { - if (currentSigner && !clientRef.current) { - const newClient = createWebSocketClient(WS_URL, currentSigner, { - autoReconnect: true, - reconnectDelay: 1000, - maxReconnectAttempts: 5, - requestTimeout: 10000, - }); - - clientRef.current = newClient; - - newClient.onStatusChange((newStatus) => { - setStatus(newStatus); - if (newStatus === "connected") { - setWsChannel(newClient.currentSubscribedChannel); - setCurrentNitroliteChannel(newClient.currentNitroliteChannel); - } else if (newStatus === "disconnected" || newStatus === "reconnect_failed") { - setWsChannel(null); - setCurrentNitroliteChannel(null); - } - }); - - newClient.onError((error) => { - console.error(`WebSocket error: ${error.message}`); - }); - - newClient.onMessage((message) => { - const hasType = (msg: unknown): msg is { type: unknown } => typeof msg === "object" && msg !== null && "type" in msg; - const messageType = hasType(message) ? (typeof message.type === "string" ? message.type : String(message.type)) : "unknown"; - - console.log(`Received message (type: ${messageType})`); - }); - - // Don't automatically connect - wait for MetaMask to be connected first - console.log("WebSocket client initialized, waiting for MetaMask connection..."); - } - - return () => { - if (clientRef.current) { - clientRef.current.close(); - clientRef.current = null; - setStatus("disconnected"); - setWsChannel(null); - setCurrentNitroliteChannel(null); - } - }; - }, [currentSigner]); - - const connect = useCallback(async () => { - if (!clientRef.current) { - console.error("Cannot connect: WebSocket client not initialized (no signer?)"); - return false; - } - if (clientRef.current.isConnected) { - console.log("Already connected"); - return true; - } - - // Check if MetaMask wallet is connected first - const walletClient = WalletStore.getWalletClient(); - if (!walletClient?.account?.address) { - console.log("Cannot connect to WebSocket: MetaMask wallet not connected. Please connect MetaMask first."); - return false; - } - - try { - console.log("MetaMask connected, connecting to WebSocket server..."); - await clientRef.current.connect(); - console.log("WebSocket connection established"); - return true; - } catch (error) { - const errorMsg = `Connection error: ${error instanceof Error ? error.message : String(error)}`; - console.error(errorMsg); - return false; - } - }, []); - - // Effect to automatically connect WebSocket when MetaMask becomes available - useEffect(() => { - const checkAndConnect = async () => { - const walletClient = WalletStore.getWalletClient(); - if (walletClient?.account?.address && clientRef.current && !clientRef.current.isConnected && status === "disconnected") { - console.log("MetaMask connected, attempting WebSocket connection..."); - await connect(); - } - }; - - // Check immediately - checkAndConnect(); - - // Set up an interval to check periodically (in case we miss the wallet connection) - const interval = setInterval(checkAndConnect, 1000); - - return () => clearInterval(interval); - }, [connect, status]); - - const disconnect = useCallback(() => { - if (clientRef.current) { - clientRef.current.close(); - console.log("WebSocket connection closed"); - } else { - console.error("Cannot disconnect: WebSocket client not initialized"); - } - }, []); - - const setNitroliteChannel = useCallback((nitroliteChannel: Channel) => { - if (!clientRef.current) { - console.error("Cannot set Nitrolite channel: Client not initialized"); - return; - } - setCurrentNitroliteChannel(nitroliteChannel); - clientRef.current.setNitroliteChannel(nitroliteChannel); - console.log(`Set Nitrolite channel: ${JSON.stringify(nitroliteChannel).slice(0, 50)}...`); - }, []); - - const sendPing = useCallback(async () => { - if (!clientRef.current?.isConnected) { - console.error("Cannot ping: Not connected"); - return; - } - try { - console.log("Sending ping to server..."); - await clientRef.current.ping(); - console.log("Ping successful"); - } catch (error) { - console.error(`Ping error: ${error instanceof Error ? error.message : String(error)}`); - } - }, []); - - // @ts-ignore - const sendRequest = useCallback(async (signedRequest) => { - if (!clientRef.current?.isConnected) { - const errorMsg = `Cannot send request: Not connected`; - console.error(errorMsg); - throw new Error("WebSocket not connected"); - } - - try { - console.log("Sending request to server..."); - const response = await clientRef.current.sendRequest(signedRequest); - console.log("Request successful"); - return response; - } catch (error) { - const errorMsg = `Request error: ${error instanceof Error ? error.message : String(error)}`; - console.error(errorMsg); - throw error; - } - }, []); - - const value = useMemo( - () => ({ - client: clientRef.current, - status, - keyPair, - wsChannel, - currentNitroliteChannel, - isConnected: status === "connected", - hasKeys: !!keyPair, - generateKeys, - connect, - disconnect, - setNitroliteChannel, - clearKeys, - sendPing, - sendRequest, - }), - [ - status, - keyPair, - wsChannel, - currentNitroliteChannel, - generateKeys, - connect, - disconnect, - setNitroliteChannel, - clearKeys, - sendPing, - sendRequest, - ] - ); - - return {children}; -}; - -export const useWebSocketContext = (): WebSocketContextProps => { - const context = useContext(WebSocketContext); - - if (context === undefined) { - throw new Error("useWebSocketContext must be used within a WebSocketProvider"); - } - return context; -}; diff --git a/examples/tictactoe/client/src/context/app.ts b/examples/tictactoe/client/src/context/app.ts deleted file mode 100644 index 1b0041e9a..000000000 --- a/examples/tictactoe/client/src/context/app.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type Address } from "viem"; - -/** - * Application configuration - * - * This file contains configuration settings for the application, - * including network endpoints and default values. - */ -export const APP_CONFIG = { - // WebSocket configuration for real-time communication - WEBSOCKET: { - URL: "wss://clearnet.yellow.com/ws", - }, - - CHANNEL: { - DEFAULT_GUEST: "0x3c93C321634a80FB3657CFAC707718A11cA57cBf", - CHALLENGE_PERIOD: BigInt(3600), - }, - - TOKENS: { - 137: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" as Address, - }, - - CUSTODIES: { - 137: "0x4C8Bd8877C3b403BA9f9ECfaAD910AF0d8CA2c4D" as Address, - }, - - DEFAULT_ADJUDICATOR: "dummy", - - ADJUDICATORS: { - 137: "0x5F4A4B1D293A973a1Bc0daD3BB3692Bd51058FCF" as Address, - }, -}; - -export default APP_CONFIG; diff --git a/examples/tictactoe/client/src/context/createSigner.ts b/examples/tictactoe/client/src/context/createSigner.ts deleted file mode 100644 index 6fc31ef2b..000000000 --- a/examples/tictactoe/client/src/context/createSigner.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type Hex } from "viem"; -import { ethers } from "ethers"; -import { type MessageSigner, type RequestData, type ResponsePayload } from "@erc7824/nitrolite"; - -/** - * Interface for a cryptographic keypair - */ -export interface CryptoKeypair { - /** Private key in hexadecimal format */ - privateKey: string; - /** Optional Ethereum address derived from the public key */ - address?: string; -} - -/** - * Interface for a wallet signer that can sign messages - */ -export interface WalletSigner { - /** Optional Ethereum address derived from the public key */ - address: Hex; - /** Function to sign a message and return a hex signature */ - sign: MessageSigner; -} - -/** - * Creates a signer from a private key using ethers.js v6 - * - * @param privateKey - The private key to create the signer from - * @returns A WalletSigner object that can sign messages - * @throws Error if signer creation fails - */ -export const createEthersSigner = (privateKey: string): WalletSigner => { - try { - // Create ethers wallet from private key - const wallet = new ethers.Wallet(privateKey); - - return { - address: ethers.getAddress(wallet.address) as Hex, - sign: async (payload: RequestData | ResponsePayload): Promise => { - try { - const message = JSON.stringify(payload); - console.log("Signing message in Sign function:", message); - const digestHex = ethers.id(message); - console.log("Digest Hex:", digestHex); - const messageBytes = ethers.getBytes(digestHex); - - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - - return signature as Hex; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }, - }; - } catch (error) { - console.error("Error creating ethers signer:", error); - throw error; - } -}; - -/** - * Generates a random keypair using ethers v6 - * - * @returns A Promise resolving to a CryptoKeypair object - */ -export const generateKeyPair = async (): Promise => { - try { - // Create random wallet - const wallet = ethers.Wallet.createRandom(); - - // Hash the private key with Keccak256 for additional security - const privateKeyHash = ethers.keccak256(wallet.privateKey as string); - - // Derive public key from hashed private key to create a new wallet - const walletFromHashedKey = new ethers.Wallet(privateKeyHash); - - return { - privateKey: privateKeyHash, - address: ethers.getAddress(walletFromHashedKey.address), - }; - } catch (error) { - console.error("Error generating keypair, using fallback:", error); - // Fallback implementation - const randomHex = ethers.randomBytes(32); - const privateKey = ethers.keccak256(randomHex); - const wallet = new ethers.Wallet(privateKey); - - return { - privateKey: privateKey, - address: ethers.getAddress(wallet.address), - }; - } -}; diff --git a/examples/tictactoe/client/src/hooks/useAppSessionSignature.ts b/examples/tictactoe/client/src/hooks/useAppSessionSignature.ts deleted file mode 100644 index 1e378e421..000000000 --- a/examples/tictactoe/client/src/hooks/useAppSessionSignature.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useState, useCallback } from "react"; -import { useWebSocketContext } from "../context/WebSocketContext"; -import { createEthersSigner } from "../context/createSigner"; -import type { AppSessionSignatureRequestMessage, AppSessionStartGameRequestMessage } from "../types"; - -/** - * Hook for handling app session signature requests - */ -export function useAppSessionSignature( - sendSignature?: (roomId: string, signature: string) => void, - sendStartGame?: (roomId: string, signature: string) => void -) { - const [isSigningInProgress, setIsSigningInProgress] = useState(false); - const [signatureError, setSignatureError] = useState(null); - const { keyPair } = useWebSocketContext(); - - /** - * Signs an app session message and sends it to the server - */ - const signAppSessionMessage = useCallback( - async (roomId: string, requestToSign: unknown[], messageType: "appSession:signature" | "appSession:startGame") => { - if (!keyPair?.privateKey) { - throw new Error("No private key available for signing"); - } - - setIsSigningInProgress(true); - setSignatureError(null); - - try { - // Create a signer from the local private key - const signer = createEthersSigner(keyPair.privateKey); - console.log("Client signer created with address:", signer.address); - console.log("Client signing request structure:", JSON.stringify(requestToSign, null, 2)); - - // Sign the exact same request structure that the server generated - // This ensures all participants sign the identical payload - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const signature = await signer.sign(requestToSign as any); - console.log("Client signature created:", signature); - - // Send the signature to the server using the provided callback - if (messageType === "appSession:signature" && sendSignature) { - sendSignature(roomId, signature); - } else if (messageType === "appSession:startGame" && sendStartGame) { - sendStartGame(roomId, signature); - } else { - throw new Error("No send function available for message type: " + messageType); - } - - setIsSigningInProgress(false); - return signature; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown signing error"; - console.error("App session signing error:", errorMessage); - setSignatureError(errorMessage); - setIsSigningInProgress(false); - throw error; - } - }, - [keyPair, sendSignature, sendStartGame] - ); - - /** - * Handles participant B signature request (when joining) - */ - const handleParticipantBSignature = useCallback( - async (message: AppSessionSignatureRequestMessage) => { - try { - await signAppSessionMessage(message.roomId, message.requestToSign, "appSession:signature"); - } catch (error) { - console.error("Failed to sign as participant B:", error); - throw error; - } - }, - [signAppSessionMessage] - ); - - /** - * Handles participant A signature request (when starting game) - */ - const handleParticipantASignature = useCallback( - async (message: AppSessionStartGameRequestMessage) => { - try { - await signAppSessionMessage(message.roomId, message.requestToSign, "appSession:startGame"); - } catch (error) { - console.error("Failed to sign as participant A:", error); - throw error; - } - }, - [signAppSessionMessage] - ); - - return { - isSigningInProgress, - signatureError, - handleParticipantBSignature, - handleParticipantASignature, - signAppSessionMessage, - }; -} diff --git a/examples/tictactoe/client/src/hooks/useChannel.ts b/examples/tictactoe/client/src/hooks/useChannel.ts deleted file mode 100644 index 1ac0beaa0..000000000 --- a/examples/tictactoe/client/src/hooks/useChannel.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { useCallback, useState } from "react"; -import type { Address, Hex } from "viem"; -import { NitroliteStore, WalletStore } from "../store"; -import { useStore } from "../store/storeUtils"; -import { parseTokenUnits } from "./utils/tokenDecimals"; -import { useWebSocketContext } from "../context/WebSocketContext"; -import type { State } from "@erc7824/nitrolite"; -import { USDC_ADDRESS } from '../context/NitroliteClientWrapper'; - -// Define localStorage keys -const STORAGE_KEYS = { - CHANNEL: "nitrolite_channel", - CHANNEL_STATE: "nitrolite_channel_state", - CHANNEL_ID: "nitrolite_channel_id", -}; - -const EMPTY_STATE_DATA = "0x"; - -/** - * Custom hook for managing Nitrolite channels - */ -export function useChannel() { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const { currentNitroliteChannel, setNitroliteChannel } = useWebSocketContext(); - const walletState = useStore(WalletStore.state); - - /** - * Check for existing channels - */ - const checkForExistingChannel = useCallback(async () => { - const savedChannelId = localStorage.getItem(STORAGE_KEYS.CHANNEL_ID); - - if (savedChannelId) { - console.log("Found existing channel ID in localStorage:", savedChannelId); - return { exists: true, source: "localStorage" }; - } - - if (walletState.channelOpen) { - console.log("Channel is open according to walletStore"); - return { exists: true, source: "walletStore" }; - } - - console.log("No existing channel found"); - return { exists: false }; - }, [walletState.channelOpen]); - - /** - * Save channel state to localStorage - */ - const saveChannelToStorage = useCallback((state: State, channelId: string) => { - try { - const stateData = JSON.stringify(state, (_, value) => (typeof value === "bigint" ? value.toString() + "n" : value)); - - localStorage.setItem(STORAGE_KEYS.CHANNEL_STATE, stateData); - localStorage.setItem(STORAGE_KEYS.CHANNEL_ID, channelId); - - console.log("Saved channel data to localStorage"); - } catch (error) { - console.error("Failed to save channel to localStorage:", error); - } - }, []); - - /** - * Create a new Nitrolite channel - */ - const createChannel = useCallback( - async (tokenAddress: Hex, amount: string) => { - setIsLoading(true); - setError(null); - - try { - const existingChannel = await checkForExistingChannel(); - - if (existingChannel.exists) { - const source = existingChannel.source; - let message = "Cannot create a new channel because one already exists."; - - if (source === "accountChannels") { - message += " You have active channel(s). Please close existing channels before creating a new one."; - } else { - message += " Please close the existing channel before creating a new one."; - } - - setError(message); - throw new Error(message); - } - - const nitroliteState = NitroliteStore.state.getState(); - console.log("NitroliteStore state:", { - isInitialized: nitroliteState.isInitialized, - hasClient: !!nitroliteState.client, - }); - - const client = nitroliteState.client; - if (!client) { - console.error("Nitrolite client not initialized - client is null in store"); - throw new Error("Nitrolite client not initialized"); - } - - // Log client methods - console.log("Available client methods:", Object.keys(client)); - - const amountBigInt = parseTokenUnits(tokenAddress, amount); - const result = await client.createChannel(USDC_ADDRESS, { - initialAllocationAmounts: [amountBigInt, BigInt(0)], - stateData: EMPTY_STATE_DATA, - }); - - saveChannelToStorage(result.initialState, result.channelId); - WalletStore.setChannelOpen(true); - - if (setNitroliteChannel && result) { - setNitroliteChannel(result as any); - } - - return result; - } catch (error) { - console.error("Error creating channel:", error); - setError(error instanceof Error ? error.message : String(error)); - WalletStore.setChannelOpen(false); - throw error; - } finally { - setIsLoading(false); - } - }, - [checkForExistingChannel, saveChannelToStorage, setNitroliteChannel] - ); - - /** - * Deposit to a channel - */ - const depositToChannel = useCallback(async (tokenAddress: Address, amount: string) => { - setIsLoading(true); - setError(null); - - try { - const nitroliteState = NitroliteStore.state.getState(); - console.log("[depositToChannel] NitroliteStore state:", { - isInitialized: nitroliteState.isInitialized, - hasClient: !!nitroliteState.client, - }); - - const client = nitroliteState.client; - if (!client) { - console.error("[depositToChannel] Nitrolite client not initialized - client is null in store"); - throw new Error("Nitrolite client not initialized"); - } - - // Log client methods for deposit - console.log("[depositToChannel] Available client methods:", Object.keys(client)); - - const amountBigInt = typeof amount === "string" && !amount.startsWith("0x") ? parseTokenUnits(tokenAddress, amount) : BigInt(amount); - - await client.deposit(USDC_ADDRESS, amountBigInt); - WalletStore.openChannel(tokenAddress, amountBigInt.toString()); - - return true; - } catch (depositError) { - let errorMessage = "Deposit failed"; - - if (String(depositError).includes("approve") && String(depositError).includes("not been authorized")) { - errorMessage = "Token approval was rejected. Please approve the USDC spend in your wallet to proceed."; - } else if (String(depositError).includes("user rejected transaction")) { - errorMessage = "Transaction was rejected. Please confirm the transaction in your wallet."; - } else { - errorMessage = `Deposit error: ${depositError}`; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }, []); - - /** - * Clear stored channel data - */ - const clearStoredChannel = useCallback(() => { - try { - localStorage.removeItem(STORAGE_KEYS.CHANNEL); - localStorage.removeItem(STORAGE_KEYS.CHANNEL_STATE); - localStorage.removeItem(STORAGE_KEYS.CHANNEL_ID); - WalletStore.closeChannel(); - - console.log("Cleared channel data from localStorage"); - } catch (error) { - console.error("Failed to clear channel data from localStorage:", error); - } - }, []); - - return { - createChannel, - depositToChannel, - clearStoredChannel, - currentChannel: currentNitroliteChannel, - isChannelOpen: walletState.channelOpen, - isLoading, - error, - }; -} diff --git a/examples/tictactoe/client/src/hooks/useGameState.ts b/examples/tictactoe/client/src/hooks/useGameState.ts deleted file mode 100644 index b3e8ea14b..000000000 --- a/examples/tictactoe/client/src/hooks/useGameState.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import type { - GameState, - GameOver, - WebSocketMessages, - AppSessionSignatureRequestMessage, - AppSessionStartGameRequestMessage -} from '../types'; -import { useAppSessionSignature } from './useAppSessionSignature'; - -// Initial empty game state -const EMPTY_BOARD = Array(9).fill(null); -const INITIAL_GAME_STATE: GameState = { - roomId: '', - board: EMPTY_BOARD, - nextTurn: 'X', - players: { X: '', O: '' } -}; - -// Game state hook that processes WebSocket messages -export function useGameState( - lastMessage: WebSocketMessages | null, - eoaAddress: string, - sendAppSessionSignature?: (roomId: string, signature: string) => void, - sendAppSessionStartGame?: (roomId: string, signature: string) => void -) { - // Game state - const [gameState, setGameState] = useState(INITIAL_GAME_STATE); - const [gameOver, setGameOver] = useState(null); - const [roomId, setRoomId] = useState(''); - const [errorMessage, setErrorMessage] = useState(null); - const [isRoomReady, setIsRoomReady] = useState(false); - const [isGameStarted, setIsGameStarted] = useState(false); - const [isHost, setIsHost] = useState(false); - const [pendingSignatureRequest, setPendingSignatureRequest] = useState(null); - const [awaitingHostStart, setAwaitingHostStart] = useState(false); - - // App session signature handling - const { - isSigningInProgress, - signatureError, - handleParticipantBSignature, - handleParticipantASignature - } = useAppSessionSignature(sendAppSessionSignature, sendAppSessionStartGame); - - // Determine player's role (X or O) - const playerSymbol = gameState.players.X === eoaAddress ? 'X' : - gameState.players.O === eoaAddress ? 'O' : null; - - // Is it the player's turn? - const isPlayerTurn = playerSymbol === gameState.nextTurn; - - // We don't need to generate room IDs client-side anymore - // The server handles room creation - - // Process WebSocket messages to update game state - useEffect(() => { - if (!lastMessage) return; - - console.log("Received WebSocket message:", lastMessage.type, lastMessage); - - switch (lastMessage.type) { - case 'room:created': - console.log("Room created:", lastMessage.roomId, "role:", lastMessage.role); - setRoomId(lastMessage.roomId); - - // Set host status based on role - if (lastMessage.role === 'host') { - console.log("Player is host"); - setIsHost(true); - } else { - console.log("Player is guest"); - setIsHost(false); - } - - setErrorMessage(null); - break; - - case 'room:state': - console.log("Received room:state", lastMessage, "eoaAddress:", eoaAddress); - - setGameState({ - roomId: lastMessage.roomId, - board: lastMessage.board, - nextTurn: lastMessage.nextTurn, - players: lastMessage.players - }); - - // Set host status based on player role (X is always host) - if (lastMessage.players.X === eoaAddress) { - console.log("Player is host (X)"); - setIsHost(true); - } else { - console.log("Player is guest (O)"); - setIsHost(false); - } - - // Always update room ID when we get a room:state message - if (lastMessage.roomId) { - setRoomId(lastMessage.roomId); - } - - setErrorMessage(null); - break; - - case 'room:ready': - setRoomId(lastMessage.roomId); - setIsRoomReady(true); - setErrorMessage(null); - break; - - case 'game:started': - setIsGameStarted(true); - setErrorMessage(null); - break; - - case 'game:over': - setGameOver({ - winner: lastMessage.winner, - board: lastMessage.board - }); - setErrorMessage(null); - break; - - case 'appSession:signatureRequest': - console.log("Received signature request for participant B:", lastMessage); - setPendingSignatureRequest(lastMessage as AppSessionSignatureRequestMessage); - - // Automatically sign for participant B (guest) - if (!isHost) { - try { - handleParticipantBSignature(lastMessage as AppSessionSignatureRequestMessage); - } catch (error) { - console.error('Failed to handle participant B signature:', error); - setErrorMessage('Failed to sign app session message'); - } - } - break; - - case 'appSession:startGameRequest': - console.log("Received start game request for participant A (host):", lastMessage); - setPendingSignatureRequest(lastMessage as AppSessionStartGameRequestMessage); - setAwaitingHostStart(true); - break; - - case 'appSession:signatureConfirmed': - console.log("App session signature confirmed:", lastMessage); - setPendingSignatureRequest(null); - setErrorMessage(null); - break; - - case 'error': - setErrorMessage(lastMessage.msg); - break; - - default: - // Ignore unknown message types - break; - } - }, [lastMessage, eoaAddress, handleParticipantBSignature, isHost]); - - // Helper to format short address display - const formatShortAddress = (address: string): string => { - if (!address) return ''; - return `${address.slice(0, 6)}...${address.slice(-4)}`; - }; - - // Get opponent's address - const getOpponentAddress = (): string => { - if (!playerSymbol) return ''; - return playerSymbol === 'X' ? gameState.players.O : gameState.players.X; - }; - - // Handle host signing and starting game - const signAndStartGame = useCallback(async () => { - if (!pendingSignatureRequest || pendingSignatureRequest.type !== 'appSession:startGameRequest') { - console.error('No pending start game request'); - return; - } - - try { - await handleParticipantASignature(pendingSignatureRequest as AppSessionStartGameRequestMessage); - setPendingSignatureRequest(null); - setAwaitingHostStart(false); - } catch (error) { - console.error('Failed to sign and start game:', error); - setErrorMessage('Failed to sign and start game'); - } - }, [pendingSignatureRequest, handleParticipantASignature]); - - // Reset game state - const resetGame = useCallback(() => { - setGameState(INITIAL_GAME_STATE); - setGameOver(null); - setIsRoomReady(false); - setIsGameStarted(false); - setIsHost(false); - setRoomId(''); - setErrorMessage(null); - setPendingSignatureRequest(null); - setAwaitingHostStart(false); - }, []); - - // TODO: Add integration with @erc7824/nitrolite for persisting game state - - return { - gameState, - gameOver, - roomId, - errorMessage, - isRoomReady, - isGameStarted, - isHost, - playerSymbol, - isPlayerTurn, - formatShortAddress, - getOpponentAddress, - resetGame, - pendingSignatureRequest, - awaitingHostStart, - signAndStartGame, - isSigningInProgress, - signatureError - }; -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/hooks/useMetaMask.ts b/examples/tictactoe/client/src/hooks/useMetaMask.ts deleted file mode 100644 index 1efd42655..000000000 --- a/examples/tictactoe/client/src/hooks/useMetaMask.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { useState, useEffect } from 'react'; -import { type Address } from 'viem'; -import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit'; -import { useAccount, useConnect, useSwitchChain } from 'wagmi'; -import { SettingsStore, WalletStore } from '../store'; -import { useStore } from '../store/storeUtils'; - -// Storage key for wallet connection -const WALLET_CONNECTION_KEY = 'wallet_connection'; - -export function useMetaMask() { - const [isMetaMaskInstalled, setIsMetaMaskInstalled] = - useState(false); - const walletState = useStore(WalletStore.state); - const settingsState = useStore(SettingsStore.state); - - const { openConnectModal } = useConnectModal(); - const { openAccountModal } = useAccountModal(); - - const { address, isConnected, isConnecting, chain } = useAccount(); - const { connect, connectors } = useConnect(); - const { switchChain } = useSwitchChain(); - - useEffect(() => { - if (typeof window !== 'undefined') { - setIsMetaMaskInstalled(!!window.ethereum?.isMetaMask); - } - }, []); - - useEffect(() => { - if (isConnected && address) { - if ( - !walletState.isConnected || - walletState.walletAddress !== address - ) { - WalletStore.connect(address as Address); - localStorage.setItem(WALLET_CONNECTION_KEY, 'true'); - } - - if (chain && walletState.chainId !== chain.id) { - WalletStore.setChainId(chain.id); - } - } else if (!isConnected && walletState.isConnected) { - localStorage.removeItem(WALLET_CONNECTION_KEY); - } - }, [ - isConnected, - address, - chain, - walletState.isConnected, - walletState.walletAddress, - walletState.chainId, - ]); - - // Connect using RainbowKit - const connectWallet = async () => { - try { - // Find MetaMask connector - const metamaskConnector = connectors.find((connector) => - connector.name.toLowerCase().includes('metamask') - ); - - if (metamaskConnector) { - connect({ connector: metamaskConnector }); - } else { - openConnectModal?.(); - } - } catch (error) { - console.error('Error connecting wallet:', error); - WalletStore.setError('Failed to connect wallet'); - } - }; - - const switchNetwork = async (chainId: number) => { - try { - await switchChain({ chainId }); - } catch (error) { - console.error('Error switching network:', error); - WalletStore.setError('Failed to switch network'); - } - }; - - useEffect(() => { - if ( - settingsState.activeChain && - chain && - settingsState.activeChain !== chain.id && - isConnected - ) { - WalletStore.setChainId(chain.id); - } - }, [settingsState.activeChain, chain, isConnected]); - - return { - isMetaMaskInstalled, - isConnected: walletState.isConnected, - account: walletState.walletAddress, - address: walletState.walletAddress, - chainId: walletState.chainId, - error: walletState.error, - connect: connectWallet, - connectWallet, - openConnectModal, - openAccountModal, - switchNetwork, - isConnecting, - }; -} diff --git a/examples/tictactoe/client/src/hooks/useNitroliteIntegration.ts b/examples/tictactoe/client/src/hooks/useNitroliteIntegration.ts deleted file mode 100644 index db16077e4..000000000 --- a/examples/tictactoe/client/src/hooks/useNitroliteIntegration.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { NitroliteStore, WalletStore } from '../store'; -import { useStore } from '../store/storeUtils'; -import { useWebSocketContext } from '../context/WebSocketContext'; -import { useChannel } from './useChannel'; - -/** - * This hook integrates the WebSocket connections with Nitrolite channels - * It should be used at app level to handle WebSocket and Channel connections - */ -export function useNitroliteIntegration() { - const { status, isConnected } = useWebSocketContext(); - const walletState = useStore(WalletStore.state); - const nitroliteState = useStore(NitroliteStore.state); - const { currentChannel, clearStoredChannel } = useChannel(); - - /** - * Handle recovered or existing channels - */ - useEffect(() => { - // If we're connected to WebSocket and have a client but no channel, try to recover it - if (isConnected && nitroliteState.client && !currentChannel) { - const channelId = localStorage.getItem('nitrolite_channel_id'); - const channelState = localStorage.getItem('nitrolite_channel_state'); - - if (channelId && channelState) { - try { - // This is a simplified version - in a real app you would parse the state and reconnect - console.log('Found saved channel, should reconnect:', channelId); - - // Mark as having an open channel - WalletStore.setChannelOpen(true); - } catch (error) { - console.error('Failed to recover channel:', error); - clearStoredChannel(); - } - } else { - console.log('No existing channel data found in localStorage'); - // Ensure channels are marked as closed if no data exists - WalletStore.setChannelOpen(false); - } - } - }, [isConnected, nitroliteState.client, currentChannel, clearStoredChannel]); - - /** - * Initialize the Nitrolite client when necessary - */ - const initializeNitroliteClient = useCallback(async (clientInstance: any) => { - NitroliteStore.setClient(clientInstance); - }, []); - - /** - * Handle WebSocket disconnection - */ - useEffect(() => { - if (status === 'disconnected' && walletState.channelOpen) { - // Websocket was disconnected but we have an open channel - // This is just logging for now, but you could implement reconnection logic - console.log('WebSocket disconnected while channel is open - should attempt to reconnect'); - } - }, [status, walletState.channelOpen]); - - return { - wsStatus: status, - isWsConnected: isConnected, - hasOpenChannel: walletState.channelOpen, - currentChannelId: currentChannel ? JSON.stringify(currentChannel).substring(0, 20) : null, - initializeNitroliteClient - }; -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/hooks/useSoundEffects.ts b/examples/tictactoe/client/src/hooks/useSoundEffects.ts deleted file mode 100644 index 432ede10a..000000000 --- a/examples/tictactoe/client/src/hooks/useSoundEffects.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -// Define sound types for type safety -type SoundType = 'mark-x' | 'mark-o' | 'game-over' | 'win' | 'draw'; - -export function useSoundEffects() { - const [loaded, setLoaded] = useState(false); - const audioContext = useRef(null); - const buffers = useRef>(new Map()); - - // Initialize audio context and load sounds - useEffect(() => { - // Create audio context - const ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); - audioContext.current = ctx; - - // Define the sounds to preload - // const soundsToLoad: Array<{ type: SoundType; url: string }> = [ - // // These URLs would point to your actual sound files - // { type: 'mark-x', url: '/sounds/mark-x.mp3' }, - // { type: 'mark-o', url: '/sounds/mark-o.mp3' }, - // { type: 'game-over', url: '/sounds/game-over.mp3' }, - // { type: 'win', url: '/sounds/win.mp3' }, - // { type: 'draw', url: '/sounds/draw.mp3' }, - // ]; - - // Instead of loading real files, we'll generate audio buffers programmatically - // This is a fallback for development when sound files might not exist - const generateToneBuffer = (frequency: number, duration: number, fadeOut = true): AudioBuffer => { - const sampleRate = ctx.sampleRate; - const buffer = ctx.createBuffer(2, sampleRate * duration, sampleRate); - - // Generate sound data for both channels - for (let channel = 0; channel < 2; channel++) { - const channelData = buffer.getChannelData(channel); - - for (let i = 0; i < channelData.length; i++) { - // Basic sine wave - const t = i / sampleRate; - let value = Math.sin(2 * Math.PI * frequency * t); - - // Add a bit of randomness for more texture - const noise = Math.random() * 0.05; - value = value * 0.5 + value * noise; - - // Apply fade out if needed - if (fadeOut && t > duration * 0.7) { - const fadePosition = (t - duration * 0.7) / (duration * 0.3); - value *= 1 - fadePosition; - } - - channelData[i] = value; - } - } - - return buffer; - }; - - // Generate different sounds for different actions - const xMarkBuffer = generateToneBuffer(880, 0.3); // Higher pitch for X - const oMarkBuffer = generateToneBuffer(440, 0.3); // Lower pitch for O - const gameOverBuffer = generateToneBuffer(220, 1, true); // Low tone for game over - const winBuffer = generateToneBuffer(880, 0.8, true); // Celebratory tone - const drawBuffer = generateToneBuffer(440, 0.5, true); // Neutral tone - - // Store the buffers - buffers.current.set('mark-x', xMarkBuffer); - buffers.current.set('mark-o', oMarkBuffer); - buffers.current.set('game-over', gameOverBuffer); - buffers.current.set('win', winBuffer); - buffers.current.set('draw', drawBuffer); - - setLoaded(true); - - // Clean up - return () => { - if (audioContext.current && audioContext.current.state !== 'closed') { - audioContext.current.close(); - } - }; - }, []); - - // Function to play a sound - const playSound = (type: SoundType, volume = 0.5) => { - if (!loaded || !audioContext.current) return; - - const buffer = buffers.current.get(type); - if (!buffer) return; - - // Create source and gain nodes - const source = audioContext.current.createBufferSource(); - const gainNode = audioContext.current.createGain(); - - // Connect nodes - source.buffer = buffer; - source.connect(gainNode); - gainNode.connect(audioContext.current.destination); - - // Set volume - gainNode.gain.value = volume; - - // Play the sound - source.start(); - }; - - return { playSound, loaded }; -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/hooks/useWebSocket.ts b/examples/tictactoe/client/src/hooks/useWebSocket.ts deleted file mode 100644 index b78130ac2..000000000 --- a/examples/tictactoe/client/src/hooks/useWebSocket.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from "react"; -import type { WebSocketMessages, JoinRoomPayload, MovePayload } from "../types"; - -// WebSocket hook for connecting to the game server -export function useWebSocket() { - const [isConnected, setIsConnected] = useState(false); - const [error, setError] = useState(null); - const webSocketRef = useRef(null); - const [lastMessage, setLastMessage] = useState(null); - - // WebSocket server URL (use environment variable if available) - const wsUrl = import.meta.env.VITE_WS_URL || "ws://localhost:8080"; - - // Initialize WebSocket connection - useEffect(() => { - const webSocket = new WebSocket(wsUrl); - - webSocket.onopen = () => { - setIsConnected(true); - setError(null); - }; - - webSocket.onclose = () => { - setIsConnected(false); - }; - - webSocket.onerror = () => { - setError("Failed to connect to game server"); - setIsConnected(false); - }; - - webSocket.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - - setLastMessage(message as WebSocketMessages); - } catch (err) { - console.error("Error parsing WebSocket message", err); - } - }; - - webSocketRef.current = webSocket; - - // Cleanup on unmount - return () => { - webSocket.close(); - }; - }, [wsUrl]); - - // Send a message to the server - const sendMessage = useCallback( - (message: object) => { - if (webSocketRef.current && isConnected) { - webSocketRef.current.send(JSON.stringify(message)); - } else { - setError("Not connected to server"); - } - }, - [isConnected] - ); - - // Join a room - const joinRoom = useCallback( - (payload: JoinRoomPayload) => { - sendMessage({ - type: "joinRoom", - payload, - }); - }, - [sendMessage] - ); - - // Make a move - const makeMove = useCallback( - (payload: MovePayload) => { - sendMessage({ - type: "move", - payload, - }); - }, - [sendMessage] - ); - - // Start the game (host only) - const startGame = useCallback( - (roomId: string) => { - sendMessage({ - type: "startGame", - payload: { roomId }, - }); - }, - [sendMessage] - ); - - // Get available rooms - const getAvailableRooms = useCallback(() => { - sendMessage({ - type: "getAvailableRooms", - }); - }, [sendMessage]); - - // Send app session signature - const sendAppSessionSignature = useCallback( - (roomId: string, signature: string) => { - sendMessage({ - type: "appSession:signature", - payload: { roomId, signature }, - }); - }, - [sendMessage] - ); - - // Send app session start game with signature - const sendAppSessionStartGame = useCallback( - (roomId: string, signature: string) => { - sendMessage({ - type: "appSession:startGame", - payload: { roomId, signature }, - }); - }, - [sendMessage] - ); - - return { - isConnected, - error, - lastMessage, - joinRoom, - makeMove, - startGame, - getAvailableRooms, - sendAppSessionSignature, - sendAppSessionStartGame, - }; -} diff --git a/examples/tictactoe/client/src/hooks/useWebSocketNitrolite.ts b/examples/tictactoe/client/src/hooks/useWebSocketNitrolite.ts deleted file mode 100644 index 21c6331dd..000000000 --- a/examples/tictactoe/client/src/hooks/useWebSocketNitrolite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useWebSocketContext } from "../context/WebSocketContext"; - -/** - * Custom hook to manage WebSocket connection and operations using WebSocketContext. - * This hook provides a simple interface to the WebSocket functionality - * without exposing the implementation details. - */ -export function useWebSocketNitrolite() { - const context = useWebSocketContext(); - - return { - // Connection status - status: context.status, - isConnected: context.isConnected, - - // Key management - keyPair: context.keyPair, - hasKeys: context.hasKeys, - generateKeys: context.generateKeys, - clearKeys: context.clearKeys, - - // Channel management - wsChannel: context.wsChannel, - currentNitroliteChannel: context.currentNitroliteChannel, - setNitroliteChannel: context.setNitroliteChannel, - - // Connection management - connect: context.connect, - disconnect: context.disconnect, - - // Message handling - sendPing: context.sendPing, - sendRequest: context.sendRequest, - }; -} diff --git a/examples/tictactoe/client/src/hooks/utils/tokenDecimals.ts b/examples/tictactoe/client/src/hooks/utils/tokenDecimals.ts deleted file mode 100644 index 34675e1d6..000000000 --- a/examples/tictactoe/client/src/hooks/utils/tokenDecimals.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { Address } from 'viem'; - -// Common token decimals -const TOKEN_DECIMALS: Record = { - // USDC on Polygon - '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359': 6, - // USDC on Ethereum - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 6, - // USDC on most testnets (including Goerli) - '0x07865c6e87b9f70255377e024ace6630c1eaa37f': 6, - // Add more tokens as needed -}; - -// Default decimal places if token not found -const DEFAULT_DECIMALS = 18; - -/** - * Get the number of decimal places for a token - * @param tokenAddress The token address - * @returns The number of decimal places - */ -export function getTokenDecimals(tokenAddress: Address): number { - if (!tokenAddress) { - console.warn('Token address is empty, using default decimals'); - return DEFAULT_DECIMALS; - } - - const normalizedAddress = tokenAddress.toLowerCase(); - const decimals = TOKEN_DECIMALS[normalizedAddress]; - - if (decimals !== undefined) { - return decimals; - } - - // If the token appears to be USDC based on its address - if (normalizedAddress.includes('usdc')) { - console.log(`Token address ${tokenAddress} appears to be USDC, using 6 decimals`); - return 6; - } - - console.log(`Token decimals not found for ${tokenAddress}, using default ${DEFAULT_DECIMALS}`); - return DEFAULT_DECIMALS; -} - -/** - * Parse a token amount from human-readable to contract units - * @param tokenAddress The token address - * @param amount The amount as a string - * @returns The amount in BigInt - */ -export function parseTokenUnits(tokenAddress: Address, amount: string): bigint { - try { - const decimals = getTokenDecimals(tokenAddress); - - // Handle empty or invalid amounts - if (!amount || amount === '' || isNaN(Number(amount))) { - console.warn(`Invalid amount provided: "${amount}", defaulting to 0`); - return BigInt(0); - } - - // Handle numbers with decimal points - const parts = amount.split('.'); - const wholePart = parts[0] || '0'; - let fractionalPart = parts[1] || ''; - - // Pad or truncate fractional part to match decimals - if (fractionalPart.length > decimals) { - fractionalPart = fractionalPart.substring(0, decimals); - } else { - while (fractionalPart.length < decimals) { - fractionalPart += '0'; - } - } - - // Remove leading zeros from whole part - const wholePartWithoutLeadingZeros = wholePart.replace(/^0+/, '') || '0'; - - // Combine parts without decimal point - const combinedString = wholePartWithoutLeadingZeros + fractionalPart; - - // Convert to BigInt - return BigInt(combinedString); - } catch (error) { - console.error(`Error parsing token units:`, error); - // Return 0 as a fallback to prevent crashes - return BigInt(0); - } -} - -/** - * Format a token amount from contract units to human-readable - * @param tokenAddress The token address - * @param amountBigInt The amount as a BigInt - * @returns The formatted amount as a string - */ -export function formatTokenUnits(tokenAddress: Address, amountBigInt: bigint): string { - try { - const decimals = getTokenDecimals(tokenAddress); - - // Handle zero amount - if (amountBigInt === BigInt(0)) { - return '0'; - } - - // Convert to string and pad with leading zeros if needed - let amountStr = amountBigInt.toString(); - while (amountStr.length <= decimals) { - amountStr = '0' + amountStr; - } - - // Split into whole and fractional parts - const wholePart = amountStr.slice(0, -decimals) || '0'; - const fractionalPart = amountStr.slice(-decimals); - - // Format with decimal point and trim trailing zeros - let result = `${wholePart}.${fractionalPart}`; - - // Remove trailing zeros after decimal point - result = result.replace(/\.?0+$/, ''); - if (result.endsWith('.')) { - result = result.slice(0, -1); - } - - return result || '0'; - } catch (error) { - console.error(`Error formatting token units:`, error); - // Return '0' as a fallback - return '0'; - } -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/index.css b/examples/tictactoe/client/src/index.css deleted file mode 100644 index b5d1022c1..000000000 --- a/examples/tictactoe/client/src/index.css +++ /dev/null @@ -1,217 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - /* Shadcn Variables */ - --background: 225 71% 3%; - --foreground: 210 20% 98%; - --card: 224 71% 4%; - --card-foreground: 210 20% 98%; - --popover: 224 71% 4%; - --popover-foreground: 210 20% 98%; - --primary: 187 100% 50%; - --primary-foreground: 210 20% 98%; - --secondary: 300 100% 64%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 216 34% 17%; - --accent-foreground: 210 20% 98%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 20% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 187 100% 50%; - --radius: 0.5rem; - - /* Brand Color Palette */ - --color-cyan: #00e5ff; - --color-cyan-dark: #0099cc; - --color-magenta: #ff49e1; - --color-magenta-dark: #cc00aa; - --color-navy: #070720; - --color-navy-light: #141432; - } -} - -/* Base styles */ -@layer base { - html, body { - min-height: 100vh; - overflow-x: hidden; - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - color-scheme: dark; - color: rgba(255, 255, 255, 0.95); - background-color: var(--color-navy); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - body { - margin: 0; - display: flex; - min-width: 320px; - /* Enhanced radial gradient with brand colors */ - background: radial-gradient(circle at center, var(--color-navy-light) 0%, var(--color-navy) 100%); - position: relative; - } - - /* Neon grid pattern overlay with brand cyan */ - body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: - linear-gradient(rgba(0, 229, 255, 0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(0, 229, 255, 0.03) 1px, transparent 1px); - background-size: 30px 30px; - z-index: -1; - } -} - -/* Nitro Aura brand components */ -@layer components { - /* Text styles with glow effects */ - .text-glow-cyan { - @apply text-[#00e5ff]; - text-shadow: 0 0 8px rgba(0, 229, 255, 0.8); - } - - .text-glow-magenta { - @apply text-[#ff49e1]; - text-shadow: 0 0 8px rgba(255, 73, 225, 0.8); - } - - /* Glowing X and O elements */ - .cell-x { - text-shadow: 0 0 10px #00e5ff, 0 0 20px #00e5ff50, 0 0 30px #00e5ff30; - position: relative; - } - - .cell-o { - text-shadow: 0 0 10px #ff49e1, 0 0 20px #ff49e150, 0 0 30px #ff49e130; - position: relative; - } - - /* Grid pattern background */ - .bg-grid-pattern { - background-image: - linear-gradient(to right, var(--color-cyan) 1px, transparent 1px), - linear-gradient(to bottom, var(--color-cyan) 1px, transparent 1px); - background-size: 20px 20px; - opacity: 0.05; - } - - /* Glass panel effect */ - .glass-panel { - @apply bg-gray-900/30 backdrop-blur-md border border-gray-800/40 rounded-xl; - } - - /* Glow effects */ - .glow-cyan { - box-shadow: 0 0 15px 5px rgba(0, 229, 255, 0.15); - } - - .glow-magenta { - box-shadow: 0 0 15px 5px rgba(255, 73, 225, 0.15); - } - - /* Aura effects for cells */ - .aura-cyan { - position: relative; - } - - .aura-cyan::before { - content: ""; - position: absolute; - inset: -5px; - border-radius: 8px; - background: radial-gradient(circle at center, rgba(0, 229, 255, 0.2) 0%, transparent 70%); - opacity: 0.5; - z-index: -1; - } - - .aura-magenta { - position: relative; - } - - .aura-magenta::before { - content: ""; - position: absolute; - inset: -5px; - border-radius: 8px; - background: radial-gradient(circle at center, rgba(255, 73, 225, 0.2) 0%, transparent 70%); - opacity: 0.5; - z-index: -1; - } - - /* Text shadow utilities for symbols */ - .text-shadow-cyan { - text-shadow: 0 0 10px #00e5ff, 0 0 20px #00e5ff50, 0 0 30px #00e5ff30; - } - - .text-shadow-magenta { - text-shadow: 0 0 10px #ff49e1, 0 0 20px #ff49e150, 0 0 30px #ff49e130; - } -} - -/* Game-specific particles effect */ -.particles { - position: absolute; - width: 100%; - height: 100%; - background-image: radial-gradient(circle, #ffffff 1px, transparent 1px); - background-size: 16px 16px; - opacity: 0.05; - mix-blend-mode: screen; - animation: float 8s infinite linear; - pointer-events: none; -} - -/* Improved pulse animation for cells */ -@keyframes pulse-cyan { - 0% { box-shadow: 0 0 10px rgba(0, 229, 255, 0.5); } - 50% { box-shadow: 0 0 20px rgba(0, 229, 255, 0.7), 0 0 30px rgba(0, 229, 255, 0.3); } - 100% { box-shadow: 0 0 10px rgba(0, 229, 255, 0.5); } -} - -@keyframes pulse-magenta { - 0% { box-shadow: 0 0 10px rgba(255, 73, 225, 0.5); } - 50% { box-shadow: 0 0 20px rgba(255, 73, 225, 0.7), 0 0 30px rgba(255, 73, 225, 0.3); } - 100% { box-shadow: 0 0 10px rgba(255, 73, 225, 0.5); } -} - -@keyframes float { - 0% { background-position: 0 0; } - 100% { background-position: 100px 100px; } -} - -@keyframes sparkle { - 0% { transform: translateY(0) rotate(0deg); } - 100% { transform: translateY(-100px) rotate(20deg); } -} - -@keyframes pulse { - 0% { opacity: 0.8; } - 50% { opacity: 1; } - 100% { opacity: 0.8; } -} - -@keyframes shimmer { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } -} - -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/lib/utils.ts b/examples/tictactoe/client/src/lib/utils.ts deleted file mode 100644 index d1f96aa04..000000000 --- a/examples/tictactoe/client/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)); -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/main.tsx b/examples/tictactoe/client/src/main.tsx deleted file mode 100644 index 495f17866..000000000 --- a/examples/tictactoe/client/src/main.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import '@rainbow-me/rainbowkit/styles.css'; -import App from './App.tsx'; -import { NitroliteClientWrapper } from './context/NitroliteClientWrapper.tsx'; -import { WebSocketProvider } from './context/WebSocketContext.tsx'; -import { RainbowKitConnectProvider } from './providers/RainbowkitConnectProvider.tsx'; - -createRoot(document.getElementById('root')!).render( - - - - - - - - - -); diff --git a/examples/tictactoe/client/src/providers/RainbowkitConnectProvider.tsx b/examples/tictactoe/client/src/providers/RainbowkitConnectProvider.tsx deleted file mode 100644 index 1707670b9..000000000 --- a/examples/tictactoe/client/src/providers/RainbowkitConnectProvider.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import { lightTheme, RainbowKitProvider, connectorsForWallets } from '@rainbow-me/rainbowkit'; -import { metaMaskWallet } from '@rainbow-me/rainbowkit/wallets'; -import { WagmiProvider } from 'wagmi'; -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { createConfig, http } from 'wagmi'; -import { polygon } from 'viem/chains'; - -const connectors = connectorsForWallets( - [ - { - groupName: 'Recommended', - wallets: [metaMaskWallet], - }, - ], - { - appName: 'TicTacToe', - projectId: import.meta.env?.NEXT_PUBLIC_PROJECT_ID ?? '014c1e90b23a969ce37a8444f1977fad', - } -); - -export const rainbowkitConfig = createConfig({ - connectors, - chains: [polygon], - transports: { - [polygon.id]: http(), - }, - ssr: true, -}); - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, - }, - }, -}); - -interface IRainbowKitConnectProvider { - children: React.ReactNode; -} - -export const RainbowKitConnectProvider: React.FC< - IRainbowKitConnectProvider -> = ({ children }: IRainbowKitConnectProvider) => { - return ( - - - - {children} - - - - ); -}; diff --git a/examples/tictactoe/client/src/store/index.ts b/examples/tictactoe/client/src/store/index.ts deleted file mode 100644 index edf9eab06..000000000 --- a/examples/tictactoe/client/src/store/index.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { NitroliteClient } from '@erc7824/nitrolite'; -import { Store } from './storeUtils'; -import type { Address, WalletClient } from 'viem'; - -// Nitrolite Store -interface NitroliteStoreState { - client: NitroliteClient | null; // This would be NitroliteClient from the package - isInitialized: boolean; -} - -export const NitroliteStore = { - state: new Store({ - client: null, - isInitialized: false, - }), - - setClient(client: any): void { - this.state.set('client', client); - this.state.set('isInitialized', true); - }, -}; - -// Wallet Store -interface WalletStoreState { - walletAddress: string | null; - chainId: number | null; - isConnected: boolean; - channelOpen: boolean; - walletClient: WalletClient | null; - channelToken: Address | null; - channelAmount: string | null; - error: string | null; -} - -export const WalletStore = { - state: new Store({ - walletAddress: null, - chainId: null, - isConnected: false, - channelOpen: false, - walletClient: null, - channelToken: null, - channelAmount: null, - error: null, - }), - - connect(address: Address): void { - this.state.setState({ - walletAddress: address, - isConnected: true, - error: null, - }); - }, - - setError(error: string | null): void { - this.state.set('error', error); - }, - - setWalletAddress(address: string | null): void { - this.state.set('walletAddress', address); - this.state.set('isConnected', !!address); - }, - - setChainId(chainId: number | null): void { - this.state.set('chainId', chainId); - }, - - setChannelOpen(isOpen: boolean): void { - this.state.set('channelOpen', isOpen); - }, - - openChannel(token: Address, amount: string): void { - this.state.setState({ - channelOpen: true, - channelToken: token, - channelAmount: amount, - }); - }, - - /** - * Set wallet client - * @param walletClient Wallet client instance - */ - setWalletClient(walletClient: WalletClient | null): void { - this.state.set('walletClient', walletClient); - - if (walletClient?.account?.address) { - this.state.set('walletAddress', walletClient.account.address); - } - }, - - /** - * Get Wallet Client - */ - getWalletClient(): WalletClient | null { - return this.state.getState().walletClient; - }, - - closeChannel(): void { - this.state.setState({ - channelOpen: false, - channelToken: null, - channelAmount: null, - }); - }, -}; - -// Settings Store -interface SettingsStoreState { - activeChain: number; -} - -export const SettingsStore = { - state: new Store({ - activeChain: 137, // Default to Polygon - }), - - setActiveChain(chainId: number): void { - this.state.set('activeChain', chainId); - }, -}; diff --git a/examples/tictactoe/client/src/store/storeUtils.ts b/examples/tictactoe/client/src/store/storeUtils.ts deleted file mode 100644 index a28ef137f..000000000 --- a/examples/tictactoe/client/src/store/storeUtils.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useState, useEffect } from 'react'; - -/** - * Simple store implementation for state management - */ -export class Store { - private state: T; - private listeners: (() => void)[] = []; - - constructor(initialState: T) { - this.state = initialState; - } - - /** - * Get the current state - */ - getState(): T { - return this.state; - } - - /** - * Update the state - */ - setState(newState: Partial): void { - this.state = { ...this.state, ...newState }; - this.notifyListeners(); - } - - /** - * Set a specific key in the state - */ - set(key: K, value: T[K]): void { - this.state = { ...this.state, [key]: value }; - this.notifyListeners(); - } - - /** - * Subscribe to changes - */ - subscribe(listener: () => void): () => void { - this.listeners.push(listener); - return () => { - this.listeners = this.listeners.filter(l => l !== listener); - }; - } - - /** - * Notify all listeners of state changes - */ - private notifyListeners(): void { - this.listeners.forEach(listener => listener()); - } -} - -/** - * Custom hook to use a store - */ -export function useStore(store: Store): T { - const [state, setState] = useState(store.getState()); - - useEffect(() => { - const unsubscribe = store.subscribe(() => { - setState(store.getState()); - }); - return unsubscribe; - }, [store]); - - return state; -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/types/index.ts b/examples/tictactoe/client/src/types/index.ts deleted file mode 100644 index 41b87592d..000000000 --- a/examples/tictactoe/client/src/types/index.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Game and WebSocket types for Nitro Aura - */ - -import type { CreateAppSessionRequest } from '@erc7824/nitrolite'; - -// Player piece: X or O -export type PlayerSymbol = 'X' | 'O'; - -// Game board (3x3 grid) -export type Board = Array; - -// Players in the game -export interface Players { - X: string; // EOA address of X player (host) - O: string; // EOA address of O player (guest) -} - -// Game state from server -export interface GameState { - roomId: string; - board: Board; - nextTurn: PlayerSymbol; - players: Players; -} - -// Game over state -export interface GameOver { - winner: PlayerSymbol | null; // null for draw - board: Board; -} - -// Room join payload -export interface JoinRoomPayload { - roomId?: string | undefined; // Explicitly marked as optional - eoa: string; -} - -// Move payload -export interface MovePayload { - roomId: string; - pos: number; // 0-8 -} - -// WebSocket message types -export type WebSocketMessageType = - | 'joinRoom' - | 'startGame' - | 'move' - | 'getAvailableRooms' - | 'room:state' - | 'room:ready' - | 'room:created' - | 'room:available' - | 'game:started' - | 'game:over' - | 'onlineUsers' - | 'players:count' - | 'error' - | 'appSession:signatureRequest' - | 'appSession:startGameRequest' - | 'appSession:signatureConfirmed' - | 'appSession:signature' - | 'appSession:startGame'; - -// Base WebSocket message -export interface WebSocketMessage { - type: WebSocketMessageType; -} - -// Client -> Server messages - -export interface JoinRoomMessage extends WebSocketMessage { - type: 'joinRoom'; - payload: JoinRoomPayload; -} - -export interface StartGamePayload { - roomId: string; -} - -export interface StartGameMessage extends WebSocketMessage { - type: 'startGame'; - payload: StartGamePayload; -} - -export interface MoveMessage extends WebSocketMessage { - type: 'move'; - payload: MovePayload; -} - -// Server -> Client messages - -export interface RoomStateMessage extends WebSocketMessage, GameState { - type: 'room:state'; -} - -export interface RoomReadyMessage extends WebSocketMessage { - type: 'room:ready'; - roomId: string; -} - -export interface RoomCreatedMessage extends WebSocketMessage { - type: 'room:created'; - roomId: string; - role: 'host' | 'guest'; -} - -export interface GameStartedMessage extends WebSocketMessage { - type: 'game:started'; - roomId: string; - firstTurn: PlayerSymbol; -} - -export interface GameOverMessage extends WebSocketMessage, GameOver { - type: 'game:over'; -} - -export interface ErrorMessage extends WebSocketMessage { - type: 'error'; - code: string; - msg: string; -} - -// Available Room type -export interface AvailableRoom { - roomId: string; - hostAddress: string; - createdAt: number; -} - -export interface AvailableRoomsMessage extends WebSocketMessage { - type: 'room:available'; - rooms: AvailableRoom[]; -} - -export interface GetAvailableRoomsMessage extends WebSocketMessage { - type: 'getAvailableRooms'; -} - -export interface OnlineUsersMessage extends WebSocketMessage { - type: 'onlineUsers' | 'players:count'; - count: number; -} - -// App Session related messages - -export interface AppSessionSignatureRequestMessage extends WebSocketMessage { - type: 'appSession:signatureRequest'; - roomId: string; - appSessionData: CreateAppSessionRequest[]; - appDefinition: unknown; - participants: string[]; - requestToSign: unknown[]; -} - -export interface AppSessionStartGameRequestMessage extends WebSocketMessage { - type: 'appSession:startGameRequest'; - roomId: string; - appSessionData: CreateAppSessionRequest[]; - appDefinition: unknown; - participants: string[]; - requestToSign: unknown[]; -} - -export interface AppSessionSignatureConfirmedMessage extends WebSocketMessage { - type: 'appSession:signatureConfirmed'; - roomId: string; -} - -export interface AppSessionSignatureMessage extends WebSocketMessage { - type: 'appSession:signature'; - payload: { - roomId: string; - signature: string; - }; -} - -export interface AppSessionStartGameMessage extends WebSocketMessage { - type: 'appSession:startGame'; - payload: { - roomId: string; - signature: string; - }; -} - -// Union type for all WebSocket messages -export type WebSocketMessages = - | JoinRoomMessage - | StartGameMessage - | MoveMessage - | RoomStateMessage - | RoomReadyMessage - | RoomCreatedMessage - | GameStartedMessage - | GameOverMessage - | AvailableRoomsMessage - | GetAvailableRoomsMessage - | OnlineUsersMessage - | ErrorMessage - | AppSessionSignatureRequestMessage - | AppSessionStartGameRequestMessage - | AppSessionSignatureConfirmedMessage - | AppSessionSignatureMessage - | AppSessionStartGameMessage; - -// MetaMask Ethereum Provider -export interface MetaMaskEthereumProvider { - isMetaMask?: boolean; - request: (request: { method: string; params?: Array }) => Promise; - on: (event: string, listener: (...args: any[]) => void) => void; - removeListener: (event: string, listener: (...args: any[]) => void) => void; - selectedAddress?: string; - isConnected?: () => boolean; -} - -// Add type definition for window.ethereum -declare global { - interface Window { - ethereum?: MetaMaskEthereumProvider; - } -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/utils/metamask.ts b/examples/tictactoe/client/src/utils/metamask.ts deleted file mode 100644 index b57e8fba0..000000000 --- a/examples/tictactoe/client/src/utils/metamask.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * MetaMask utility functions - */ - -/** - * Check if the MetaMask extension is installed - * @returns boolean indicating whether MetaMask is installed - */ -export function isMetaMaskInstalled(): boolean { - return typeof window !== 'undefined' && - typeof window.ethereum !== 'undefined' && - window.ethereum.isMetaMask === true; -} - -/** - * Format an Ethereum address for display - * @param address Full Ethereum address - * @returns Shortened address with ellipsis (e.g., 0x1234...5678) - */ -export function formatEthAddress(address: string): string { - if (!address) return ''; - if (address.length < 10) return address; - - return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; -} \ No newline at end of file diff --git a/examples/tictactoe/client/src/vite-env.d.ts b/examples/tictactoe/client/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a..000000000 --- a/examples/tictactoe/client/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/tictactoe/client/src/websocket/index.ts b/examples/tictactoe/client/src/websocket/index.ts deleted file mode 100644 index d00f8374f..000000000 --- a/examples/tictactoe/client/src/websocket/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Export everything from our consolidated websocket implementation -export * from './websocket'; \ No newline at end of file diff --git a/examples/tictactoe/client/src/websocket/websocket.ts b/examples/tictactoe/client/src/websocket/websocket.ts deleted file mode 100644 index cee0591ba..000000000 --- a/examples/tictactoe/client/src/websocket/websocket.ts +++ /dev/null @@ -1,648 +0,0 @@ -import { type Hex } from "viem"; -import { ethers } from "ethers"; -import { - createAuthRequestMessage, - NitroliteRPC, - createAuthVerifyMessage, - createPingMessage, - createAuthVerifyMessageWithJWT, - createEIP712AuthMessageSigner, - parseAnyRPCResponse, - RPCMethod, -} from "@erc7824/nitrolite"; -import type { Channel } from "@erc7824/nitrolite"; -import { WalletStore } from "../store"; - -// ===== Types ===== - -/** - * WebSocket ready states - */ -export const WebSocketReadyState = { - CONNECTING: 0, - OPEN: 1, - CLOSING: 2, - CLOSED: 3, -} as const; - -export type WebSocketReadyState = (typeof WebSocketReadyState)[keyof typeof WebSocketReadyState]; - -/** - * WebSocket connection status - */ -export type WSStatus = "connected" | "connecting" | "disconnected" | "reconnecting" | "reconnect_failed" | "auth_failed" | "authenticating"; - -/** - * WebSocket client configuration options - */ -export interface WebSocketClientOptions { - autoReconnect: boolean; - reconnectDelay: number; - maxReconnectAttempts: number; - requestTimeout: number; -} - -/** - * Wallet signer interface - */ -export interface WalletSigner { - address: Hex; - sign: (payload: any) => Promise; -} - -/** - * Gets address from a public key - */ -export const getAddressFromPublicKey = (publicKey: string): string => { - const formattedKey = publicKey.startsWith("0x") ? publicKey : `0x${publicKey}`; - const hash = ethers.keccak256(formattedKey); - const address = `0x${hash.slice(-40)}`; - return ethers.getAddress(address); -}; - -/** - * EIP-712 domain for auth_verify challenge - */ -const getAuthDomain = () => { - return { - name: "Nitro Aura", - }; -}; - -const expire = String(Math.floor(Date.now() / 1000) + 24 * 60 * 60); - - -// ===== Connection ===== - -/** - * Core WebSocket client for browser applications - */ -export class WebSocketClient { - private ws: WebSocket | null = null; - private pendingRequests = new Map void; reject: (reason: Error) => void }>(); - // private requestCounter = 0; - private reconnectAttempts = 0; - private reconnectTimeout: any = null; - private statusHandlers: ((status: WSStatus) => void)[] = []; - private messageHandlers: ((message: unknown) => void)[] = []; - private errorHandlers: ((error: Error) => void)[] = []; - private currentChannel: any = null; - private nitroliteChannel: Channel | null = null; - private pingInterval: any = null; - - /** - * Creates a new WebSocket client - */ - private url: string; - private signer: WalletSigner; - private options: WebSocketClientOptions; - - constructor( - url: string, - signer: WalletSigner, - options: WebSocketClientOptions = { - autoReconnect: true, - reconnectDelay: 1000, - maxReconnectAttempts: 5, - requestTimeout: 10000, - } - ) { - this.url = url; - this.signer = signer; - this.options = options; - } - - /** - * Registers a status change callback - */ - onStatusChange(callback: (status: WSStatus) => void): void { - this.statusHandlers.push(callback); - } - - /** - * Registers a message handler - */ - onMessage(callback: (message: unknown) => void): void { - this.messageHandlers.push(callback); - } - - /** - * Registers an error handler - */ - onError(callback: (error: Error) => void): void { - this.errorHandlers.push(callback); - } - - /** - * Gets whether the client is connected - */ - get isConnected(): boolean { - return this.ws !== null && this.ws.readyState === WebSocketReadyState.OPEN; - } - - /** - * Gets the current WebSocket ready state - */ - get readyState(): WebSocketReadyState { - return this.ws ? (this.ws.readyState as WebSocketReadyState) : WebSocketReadyState.CLOSED; - } - - /** - * Gets the current channel - */ - get currentSubscribedChannel(): any { - return this.currentChannel; - } - - /** - * Gets the current Nitrolite channel - */ - get currentNitroliteChannel(): Channel | null { - return this.nitroliteChannel; - } - - /** - * Sets the Nitrolite channel - */ - setNitroliteChannel(channel: Channel): void { - this.nitroliteChannel = channel; - } - - /** - * Connects to the WebSocket server - */ - async connect(): Promise { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - - if (this.isConnected) return; - - return new Promise((resolve, reject) => { - try { - this.ws = new WebSocket(this.url); - this.emitStatus("connecting"); - - this.ws.onopen = async () => { - try { - this.emitStatus("authenticating"); - await this.authenticate(); - this.emitStatus("connected"); - this.reconnectAttempts = 0; - this.startPingInterval(); - resolve(); - } catch (error) { - this.emitStatus("auth_failed"); - this.emitError(error instanceof Error ? error : new Error(String(error))); - reject(error); - this.close(); - this.handleReconnect(); - } - }; - - this.ws.onmessage = this.handleMessage.bind(this); - - this.ws.onerror = () => { - this.emitError(new Error("WebSocket connection error")); - reject(new Error("WebSocket connection error")); - }; - - this.ws.onclose = () => { - this.emitStatus("disconnected"); - this.ws = null; - this.currentChannel = null; - this.stopPingInterval(); - - this.pendingRequests.forEach(({ reject }) => reject(new Error("WebSocket connection closed"))); - this.pendingRequests.clear(); - - this.handleReconnect(); - }; - } catch (error) { - reject(error); - this.handleReconnect(); - } - }); - } - - /** - * Waits for wallet client to be available - */ - private async waitForWalletClient(timeout: number = 10000): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const walletClient = WalletStore.getWalletClient(); - if (walletClient?.account?.address) { - return walletClient; - } - - // Wait 100ms before checking again - await new Promise((resolve) => setTimeout(resolve, 100)); - } - - throw new Error("Timeout waiting for wallet client to be available"); - } - - /** - * Authenticates with the WebSocket server - */ - private async authenticate(): Promise { - // Wait for wallet client to be available - const walletClient = await this.waitForWalletClient(); - console.log("Authenticating with wallet client:", walletClient); - if (!this.ws) throw new Error("WebSocket not connected"); - - if (!walletClient?.account?.address) throw new Error("Wallet client not initialized or address not available"); - - const privyWalletAddress = walletClient.account.address; - - console.log("Starting authentication with:"); - console.log("- Privy wallet address:", privyWalletAddress); - - // Check for JWT token first - const jwtToken = typeof window !== "undefined" ? window.localStorage?.getItem("jwtToken") : null; - - let authRequest: string; - - if (jwtToken) { - console.log("JWT token found, sending auth request with token"); - authRequest = await createAuthVerifyMessageWithJWT(jwtToken); - } else { - console.log("No JWT token found, proceeding with challenge-response authentication"); - authRequest = await createAuthRequestMessage({ - wallet: ethers.getAddress(privyWalletAddress) as `0x${string}`, // wallet - participant: this.signer.address, //session key - app_name: "Nitro Aura", - expire: expire, - scope: "app.nitro.aura", - application: ethers.getAddress(privyWalletAddress) as `0x${string}`, - allowances: [], - }); - } - - this.ws.send(authRequest); - - return new Promise((resolve, reject) => { - const authTimeout = setTimeout(() => { - this.ws?.removeEventListener("message", handleAuthResponse); - reject(new Error("Authentication timeout")); - }, this.options.requestTimeout); - - const handleAuthResponse = async (event: MessageEvent) => { - const data = event.data || event; - - try { - const response = parseAnyRPCResponse(data); - - // Check for challenge response: {"res": [id, "auth_challenge", {"challenge": "uuid"}, timestamp]} - if (response.method === RPCMethod.AuthChallenge) { - console.log("Received auth_challenge, preparing EIP-712 auth_verify..."); - - try { - console.log("Creating EIP-712 signing function..."); - const eip712SigningFunction = createEIP712AuthMessageSigner( - walletClient, - { - scope: "app.nitro.aura", - application: privyWalletAddress, - participant: this.signer.address, - expire: expire, - allowances: [], - }, - getAuthDomain() - ); - - console.log("Calling createAuthVerifyMessage..."); - const authVerify = await createAuthVerifyMessage(eip712SigningFunction, response); - - console.log("Sending auth_verify with EIP-712 signature"); - this.ws?.send(authVerify); - console.log("auth_verify sent successfully"); - } catch (eip712Error) { - console.error("Error creating EIP-712 auth_verify:", eip712Error); - console.error("Error stack:", (eip712Error as Error)?.stack); - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - reject( - new Error(`EIP-712 auth_verify failed: ${eip712Error instanceof Error ? eip712Error.message : String(eip712Error)}`) - ); - return; - } - } - // Check for success response - else if (response.method === RPCMethod.AuthVerify) { - if (!response.params.success) { - return; - } - console.log("Authentication successful"); - - // If response contains a JWT token, store it - if (response.params.jwtToken) { - const jwtToken = response.params.jwtToken; - console.log("JWT token received:", jwtToken); - if (typeof window !== "undefined") { - window.localStorage?.setItem("jwtToken", jwtToken); - } - } - - // Authentication successful - const paramsForChannels = [{ participant: ethers.getAddress(privyWalletAddress) as `0x${string}` }]; - const getChannelsMessage = NitroliteRPC.createRequest(10, RPCMethod.GetChannels, paramsForChannels); - const getChannelMessage = await NitroliteRPC.signRequestMessage(getChannelsMessage, this.signer.sign); - console.log("getChannelMessage", getChannelMessage); - this.ws?.send(JSON.stringify(getChannelMessage)); - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - resolve(); - } - // Check for error response - else if (response.method === RPCMethod.Error) { - const errorMsg = response.params.error || "Authentication failed"; - console.error("Authentication failed:", errorMsg); - if (typeof window !== "undefined") { - window.localStorage?.removeItem("jwtToken"); - } - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - reject(new Error(String(errorMsg))); - } else { - console.log("Received non-auth message during auth, continuing to listen:", response); - // Keep listening if it wasn't a final success/error - } - } catch (error) { - // Ignore non-auth methods during authentication - if (error instanceof Error && error.message && error.message.includes("Unknown method:")) { - console.log("Ignoring non-auth message during authentication:", error.message); - return; - } - - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - reject(new Error(`Authentication error: ${error instanceof Error ? error.message : String(error)}`)); - } - }; - - this.ws?.addEventListener("message", handleAuthResponse); - }); - } - - /** - * Handles reconnection logic - */ - private handleReconnect(): void { - if (!this.options.autoReconnect || this.reconnectAttempts >= this.options.maxReconnectAttempts) { - if (this.reconnectAttempts >= this.options.maxReconnectAttempts) { - this.emitStatus("reconnect_failed"); - } - return; - } - - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - } - - this.reconnectAttempts++; - const delay = this.options.reconnectDelay * this.reconnectAttempts; - - this.emitStatus("reconnecting"); - - this.reconnectTimeout = setTimeout(() => { - this.connect().catch(() => { - // Silent catch to prevent unhandled rejections - }); - }, delay); - } - - /** - * Starts ping interval to keep connection alive - */ - private startPingInterval(): void { - this.stopPingInterval(); - this.pingInterval = setInterval(async () => { - if (this.isConnected) { - try { - await this.ping(); - } catch (error) { - console.error("Error sending ping:", error); - } - } - }, 20000); - } - - /** - * Stops the ping interval - */ - private stopPingInterval(): void { - if (this.pingInterval) { - clearInterval(this.pingInterval); - this.pingInterval = null; - } - } - - /** - * Closes the WebSocket connection - */ - close(): void { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - - this.stopPingInterval(); - - if (this.ws && (this.ws.readyState === WebSocketReadyState.OPEN || this.ws.readyState === WebSocketReadyState.CONNECTING)) { - try { - this.ws.close(1000, "Normal closure"); - } catch (err) { - console.error("Error while closing WebSocket:", err); - } - } - - this.ws = null; - this.currentChannel = null; - - this.pendingRequests.forEach(({ reject }) => reject(new Error("WebSocket connection closed by client"))); - this.pendingRequests.clear(); - this.emitStatus("disconnected"); - } - - /** - * Emits a status change to all registered handlers - */ - private emitStatus(status: WSStatus): void { - this.statusHandlers.forEach((handler) => handler(status)); - } - - /** - * Emits a message to all registered handlers - */ - private emitMessage(message: unknown): void { - this.messageHandlers.forEach((handler) => handler(message)); - } - - /** - * Emits an error to all registered handlers - */ - private emitError(error: Error): void { - this.errorHandlers.forEach((handler) => handler(error)); - } - - /** - * Handles incoming WebSocket messages - */ - private handleMessage(event: MessageEvent): void { - let message; - - try { - message = JSON.parse(event.data); - } catch (error) { - this.emitError(new Error(`Failed to parse message: ${event.data}`)); - return; - } - - try { - // Notify message handlers - this.emitMessage(message); - - if (typeof message !== "object" || message === null) { - return; - } - - // Type guard to check for property existence - const hasProperty = (obj: T, prop: K): obj is T & Record => { - return prop in obj; - }; - - // Handle standard RPC responses (success) - if (hasProperty(message, "res") && Array.isArray(message.res) && message.res.length >= 3) { - const requestId = typeof message.res[0] === "number" ? message.res[0] : -1; - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.get(requestId)!.resolve(message.res[2]); - this.pendingRequests.delete(requestId); - } - return; - } - - // Handle error responses - if (hasProperty(message, "err") && Array.isArray(message.err) && message.err.length >= 3) { - const requestId = typeof message.err[0] === "number" ? message.err[0] : -1; - const errorMessage = `Error ${message.err[1]}: ${message.err[2]}`; - - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.get(requestId)!.reject(new Error(errorMessage)); - this.pendingRequests.delete(requestId); - } - return; - } - - // Handle typed messages - if (hasProperty(message, "type") && typeof message.type === "string") { - // Handle channel subscription - if ( - message.type === "subscribe_success" && - hasProperty(message, "data") && - typeof message.data === "object" && - message.data && - hasProperty(message.data, "channel") - ) { - this.currentChannel = message.data.channel; - } - - // Handle request responses with requestId - if (hasProperty(message, "requestId") && typeof message.requestId === "number") { - const requestId = message.requestId; - if (this.pendingRequests.has(requestId)) { - const result = hasProperty(message, "data") ? message.data : message; - this.pendingRequests.get(requestId)!.resolve(result); - this.pendingRequests.delete(requestId); - } - } - } - } catch (error) { - this.emitError(new Error(`Error processing message: ${error instanceof Error ? error.message : String(error)}`)); - } - } - - /** - * Sends a request to the server - */ - async sendRequest(signedRequest: string): Promise { - if (!this.isConnected || !this.ws) { - throw new Error("WebSocket not connected"); - } - - let requestId: number; - - try { - const parsedRequest = JSON.parse(signedRequest); - - if ( - !parsedRequest || - !parsedRequest.req || - !Array.isArray(parsedRequest.req) || - parsedRequest.req.length < 2 || - typeof parsedRequest.req[0] !== "number" || - typeof parsedRequest.req[1] !== "string" - ) { - throw new Error("Invalid request format"); - } - - requestId = parsedRequest.req[0]; - } catch (parseError) { - throw new Error(`Failed to parse request: ${parseError instanceof Error ? parseError.message : String(parseError)}`); - } - - return new Promise((resolve, reject) => { - const requestTimeout = setTimeout(() => { - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.delete(requestId); - reject(new Error(`Request timeout`)); - } - }, this.options.requestTimeout); - - this.pendingRequests.set(requestId, { - resolve: (result: unknown) => { - clearTimeout(requestTimeout); - resolve(result); - }, - reject: (error: Error) => { - clearTimeout(requestTimeout); - reject(error); - }, - }); - - try { - if (!this.ws) { - throw new Error("WebSocket is not initialized"); - } - this.ws.send(signedRequest); - } catch (error) { - clearTimeout(requestTimeout); - this.pendingRequests.delete(requestId); - reject(new Error(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`)); - } - }); - } - - /** - * Sends a ping to the server - */ - async ping(): Promise { - return this.sendRequest(await createPingMessage(this.signer.sign)); - } -} - -/** - * Creates a new WebSocket client - */ -export function createWebSocketClient(url: string, signer: WalletSigner, options?: Partial): WebSocketClient { - return new WebSocketClient(url, signer, { - autoReconnect: true, - reconnectDelay: 1000, - maxReconnectAttempts: 5, - requestTimeout: 10000, - ...options, - }); -} diff --git a/examples/tictactoe/client/tailwind.config.js b/examples/tictactoe/client/tailwind.config.js deleted file mode 100644 index cfe4e10a8..000000000 --- a/examples/tictactoe/client/tailwind.config.js +++ /dev/null @@ -1,190 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ["class"], - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - prefix: "", - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - // Brand colors - brand: { - cyan: { - DEFAULT: "#00e5ff", - dark: "#0099cc", - light: "#80f2ff", - }, - magenta: { - DEFAULT: "#ff49e1", - dark: "#cc00aa", - light: "#ffb2f0", - }, - navy: { - DEFAULT: "#070720", - light: "#141432", - lighter: "#1e1e46", - }, - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - boxShadow: { - glow: { - cyan: "0 0 20px 5px rgba(0, 229, 255, 0.3)", - magenta: "0 0 20px 5px rgba(255, 73, 225, 0.3)", - }, - }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - pulse: { - "0%, 100%": { opacity: 0.8 }, - "50%": { opacity: 1 }, - }, - sparkle: { - "0%": { transform: "translateY(0) rotate(0deg)" }, - "100%": { transform: "translateY(-100px) rotate(20deg)" }, - }, - float: { - "0%": { transform: "translateY(0px)" }, - "50%": { transform: "translateY(-10px)" }, - "100%": { transform: "translateY(0px)" }, - }, - "pulse-cyan": { - "0%": { boxShadow: "0 0 10px rgba(0, 229, 255, 0.5)" }, - "50%": { - boxShadow: "0 0 20px rgba(0, 229, 255, 0.7), 0 0 30px rgba(0, 229, 255, 0.3)" - }, - "100%": { boxShadow: "0 0 10px rgba(0, 229, 255, 0.5)" }, - }, - "pulse-magenta": { - "0%": { boxShadow: "0 0 10px rgba(255, 73, 225, 0.5)" }, - "50%": { - boxShadow: "0 0 20px rgba(255, 73, 225, 0.7), 0 0 30px rgba(255, 73, 225, 0.3)" - }, - "100%": { boxShadow: "0 0 10px rgba(255, 73, 225, 0.5)" }, - }, - "explode-cyan": { - "0%": { - transform: "scale(0.3)", - opacity: 1, - filter: "blur(2px)" - }, - "80%": { - opacity: 0.7 - }, - "100%": { - transform: "scale(3)", - opacity: 0, - filter: "blur(1px)" - }, - }, - "explode-magenta": { - "0%": { - transform: "scale(0.3)", - opacity: 1, - filter: "blur(2px)" - }, - "80%": { - opacity: 0.7 - }, - "100%": { - transform: "scale(3)", - opacity: 0, - filter: "blur(1px)" - }, - }, - "orbit": { - "0%": { transform: "rotate(0deg) translateX(10px) rotate(0deg)" }, - "100%": { transform: "rotate(360deg) translateX(10px) rotate(-360deg)" }, - }, - fadeIn: { - "0%": { opacity: 0 }, - "100%": { opacity: 1 }, - }, - fadeInUp: { - "0%": { - opacity: 0, - transform: "translateY(10px)", - }, - "100%": { - opacity: 1, - transform: "translateY(0)", - }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "pulse": "pulse 3s infinite ease-in-out", - "pulse-fast": "pulse 1.5s infinite ease-in-out", - "sparkle": "sparkle 8s infinite linear", - "float": "float 6s infinite ease-in-out", - "pulse-cyan": "pulse-cyan 2s infinite ease-in-out", - "pulse-magenta": "pulse-magenta 2s infinite ease-in-out", - "explode-cyan": "explode-cyan 1.5s ease-out forwards", - "explode-magenta": "explode-magenta 1.5s ease-out forwards", - "orbit": "orbit 3s infinite linear", - "orbit-reverse": "orbit 3s infinite linear reverse", - "fadeIn": "fadeIn 0.3s ease-in-out", - "fadeInUp": "fadeInUp 0.5s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} \ No newline at end of file diff --git a/examples/tictactoe/client/tsconfig.app.json b/examples/tictactoe/client/tsconfig.app.json deleted file mode 100644 index bae4bc9e1..000000000 --- a/examples/tictactoe/client/tsconfig.app.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "skipDefaultLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": false, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": false - }, - "include": ["src"] -} diff --git a/examples/tictactoe/client/tsconfig.json b/examples/tictactoe/client/tsconfig.json deleted file mode 100644 index 1ffef600d..000000000 --- a/examples/tictactoe/client/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/examples/tictactoe/client/tsconfig.node.json b/examples/tictactoe/client/tsconfig.node.json deleted file mode 100644 index 9728af2d8..000000000 --- a/examples/tictactoe/client/tsconfig.node.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/examples/tictactoe/client/vite.config.ts b/examples/tictactoe/client/vite.config.ts deleted file mode 100644 index b17502bfe..000000000 --- a/examples/tictactoe/client/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}); diff --git a/examples/tictactoe/server/.env b/examples/tictactoe/server/.env deleted file mode 100644 index af135695a..000000000 --- a/examples/tictactoe/server/.env +++ /dev/null @@ -1,18 +0,0 @@ -# Nitro Aura Server Environment Variables - -# WebSocket URL for the Nitrolite service -WS_URL=wss://clearnet.yellow.com/ws - -# Server private key (replace with your actual private key) -SERVER_PRIVATE_KEY=0x - -# Nitrolite configuration -CHAIN_ID=137 -USDC_TOKEN_ADDRESS=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 -CUSTODY_ADDRESS=0x4C8Bd8877C3b403BA9f9ECfaAD910AF0d8CA2c4D -ADJUDICATOR_ADDRESS=0x2627644ae08aa21Da0Fb458D8879729743D1bB51 -DEFAULT_GUEST_ADDRESS=0x3c93C321634a80FB3657CFAC707718A11cA57cBf -POLYGON_RPC_URL=https://polygon-rpc.com - -# Server port -PORT=8080 \ No newline at end of file diff --git a/examples/tictactoe/server/.gitignore b/examples/tictactoe/server/.gitignore deleted file mode 100644 index a547bf36d..000000000 --- a/examples/tictactoe/server/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/tictactoe/server/README.md b/examples/tictactoe/server/README.md deleted file mode 100644 index 5c5cd565e..000000000 --- a/examples/tictactoe/server/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Nitro Aura Server - -A WebSocket server for the Nitro Aura 1-vs-1 Tic Tac Toe game. This server handles game rooms, player connections, and game state management. - -## Quick Start - -```bash -# Install dependencies -npm install - -# Start the server (production) -npm start - -# Start the server with hot reload (development) -npm run dev - -# Run linting -npm run lint -``` - -Server runs on port 8080 by default. - -## Protocol - -### Client → Server Messages - -```js -// Join a room -{ - "type": "joinRoom", - "payload": { - "roomId": "uuid-string", // Optional - system will create if not provided - "eoa": "0x..." // Ethereum address - } -} - -// Make a move -{ - "type": "move", - "payload": { - "roomId": "uuid-string", // Required - "pos": 0 // Position on board (0-8) - } -} -``` - -### Server → Client Messages - -```js -// Room state update (after join or move) -{ - "type": "room:state", - "roomId": "uuid-string", - "board": [null,null,null,null,"X",null,null,null,null], - "nextTurn": "O", - "players": { - "X": "0x...", // Host EOA - "O": "0x..." // Guest EOA - } -} - -// Room ready notification (2 players joined) -{ - "type": "room:ready", - "roomId": "uuid-string" -} - -// Game over notification -{ - "type": "game:over", - "winner": "X", // "X", "O", or null (draw) - "board": ["X","O","X","X","O","O","X",null,null] -} - -// Error message -{ - "type": "error", - "code": "ERROR_CODE", - "msg": "Error description" -} -``` - -## Sequence Diagram - -``` -┌──────┐ ┌─────────┐ ┌──────┐ -│Client│ │ Server │ │Client│ -└──┬───┘ └────┬────┘ └──┬───┘ - │ │ │ - │ {"type":"joinRoom",...} │ │ - ├────────────────────────────► │ - │ │ │ - │ {"type":"room:state",...} │ │ - ◄├───────────────────────────┤ │ - │ │ │ - │ │ {"type":"joinRoom",...} │ - │ ◄───────────────────────────┤ - │ │ │ - │ {"type":"room:state",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ {"type":"room:ready",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ {"type":"move",...} │ │ - ├────────────────────────────► │ - │ │ │ - │ {"type":"room:state",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ │ {"type":"move",...} │ - │ ◄───────────────────────────┤ - │ │ │ - │ {"type":"room:state",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ (moves continue...) │ │ - ├────────────────────────────► │ - │ │ │ - │ {"type":"game:over",...} │ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ -``` - -## Technical Notes - -- Uses in-memory storage for rooms and game state (will be replaced with @erc7824/nitrolite in future) -- First player is assigned as host (X), second as guest (O) -- Rooms are automatically created if a non-existent room ID is provided -- Rooms are automatically cleaned up after games complete -- Game rules enforce alternating turns and valid move placement -- Validation for Ethereum addresses and game actions \ No newline at end of file diff --git a/examples/tictactoe/server/package-lock.json b/examples/tictactoe/server/package-lock.json deleted file mode 100644 index 60c3cf6be..000000000 --- a/examples/tictactoe/server/package-lock.json +++ /dev/null @@ -1,1734 +0,0 @@ -{ - "name": "nitro-aura-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nitro-aura-server", - "version": "1.0.0", - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "chalk": "5.4.1", - "dotenv": "16.5.0", - "ethers": "^6.11.1", - "uuid": "^9.0.1", - "viem": "^2.7.15", - "ws": "^8.16.0" - }, - "devDependencies": { - "eslint": "^8.56.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT" - }, - "node_modules/@erc7824/nitrolite": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@erc7824/nitrolite/-/nitrolite-0.2.20.tgz", - "integrity": "sha512-iYzgHh6Acd3Pq1tjQ+yzEiZQh68xHZ4P+r5UMrweqBMU93Fr39pHEYe/PkdAGppREOHoXvuIDDp6S7ORTxfkug==", - "license": "MIT", - "dependencies": { - "abitype": "^0.10.3", - "viem": "^2.5.0", - "zod": "^3.25.67" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@scure/base": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", - "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", - "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.2" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/abitype": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.3.tgz", - "integrity": "sha512-tRN+7XIa7J9xugdbRzFv/95ka5ivR/sRe01eiWvM0HWWjHuigSZEACgKa0sj4wGuekTDtghCx+5Izk/cOi78pQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ethers": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.0.tgz", - "integrity": "sha512-KgHwltNSMdbrGWEyKkM0Rt2s+u1nDH/5BVDQakLinzGEJi4bWindBzZSCC4gKsbZjwDTI6ex/8suR9Ihbmz4IQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/viem": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.29.2.tgz", - "integrity": "sha512-cukRxab90jvQ+TDD84sU3qB3UmejYqgCw4cX8SfWzvh7JPfZXI3kAMUaT5OSR2As1Mgvx1EJawccwPjGqkSSwA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.2", - "@noble/hashes": "1.7.2", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.9", - "ws": "8.18.1" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/@noble/curves": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", - "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.2" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/examples/tictactoe/server/package.json b/examples/tictactoe/server/package.json deleted file mode 100644 index 2d1bb5197..000000000 --- a/examples/tictactoe/server/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "nitro-aura-server", - "version": "1.0.0", - "type": "module", - "description": "WebSocket server for Nitro Aura Tic Tac Toe game", - "main": "src/server.js", - "scripts": { - "start": "node src/server.js", - "dev": "node --watch src/server.js", - "lint": "eslint src/**/*.js" - }, - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "chalk": "5.4.1", - "dotenv": "16.5.0", - "ethers": "^6.11.1", - "uuid": "^9.0.1", - "viem": "^2.7.15", - "ws": "^8.16.0" - }, - "devDependencies": { - "eslint": "^8.56.0" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/examples/tictactoe/server/src/config/websocket.js b/examples/tictactoe/server/src/config/websocket.js deleted file mode 100644 index 3eaa2efe3..000000000 --- a/examples/tictactoe/server/src/config/websocket.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * WebSocket server configuration - */ -import { WebSocketServer } from 'ws'; -import dotenv from 'dotenv'; -import logger from '../utils/logger.js'; - -// Load environment variables -dotenv.config(); - -/** - * Creates a new WebSocket server - * @returns {WebSocketServer} The WebSocket server instance - */ -export function createWebSocketServer() { - const port = process.env.PORT || 8080; - logger.system(`Creating WebSocket server on port ${port}`); - - return new WebSocketServer({ - host: '0.0.0.0', - port - }); -} - -/** - * Utility function to send an error message - * @param {WebSocket} ws - WebSocket connection - * @param {string} code - Error code - * @param {string} msg - Error message - */ -export function sendError(ws, code, msg) { - ws.send(JSON.stringify({ - type: 'error', - code, - msg - })); -} - -/** - * Starts a ping interval to keep connections alive - * @param {WebSocketServer} wss - The WebSocket server instance - * @param {number} interval - Ping interval in milliseconds (default: 30000) - * @returns {NodeJS.Timeout} The interval timer - */ -export function startPingInterval(wss, interval = 30000) { - return setInterval(() => { - wss.clients.forEach((client) => { - if (client.readyState === 1) { // WebSocket.OPEN - client.send(JSON.stringify({ type: 'ping' })); - } - }); - }, interval); -} \ No newline at end of file diff --git a/examples/tictactoe/server/src/routes/gameRoutes.js b/examples/tictactoe/server/src/routes/gameRoutes.js deleted file mode 100644 index edd7bb977..000000000 --- a/examples/tictactoe/server/src/routes/gameRoutes.js +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Game-related WebSocket message handlers - */ - -import { validateMovePayload } from '../utils/validators.js'; -import { - formatGameState, - formatGameOverMessage, - createGame, - createAppSession, - closeAppSession, - hasAppSession, - generateAppSessionMessage, - addAppSessionSignature, - createAppSessionWithSignatures -} from '../services/index.js'; -import logger from '../utils/logger.js'; - -/** - * Handles a start game request - * @param {WebSocket} ws - WebSocket connection - * @param {Object} payload - Request payload - * @param {Object} context - Application context containing roomManager and connections - */ -export async function handleStartGame(ws, payload, { roomManager, connections, sendError }) { - if (!payload || typeof payload !== 'object') { - return sendError(ws, 'INVALID_PAYLOAD', 'Invalid payload format'); - } - - const { roomId } = payload; - - if (!roomId) { - return sendError(ws, 'INVALID_PAYLOAD', 'Room ID is required'); - } - - // Find the player trying to start the game - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - // Get the room - const room = roomManager.rooms.get(roomId); - if (!room) { - return sendError(ws, 'ROOM_NOT_FOUND', 'Room not found'); - } - - // Only the host can start the game - if (room.players.host !== playerEoa) { - return sendError(ws, 'NOT_AUTHORIZED', 'Only the host can start the game'); - } - - // Need both players - if (!room.players.host || !room.players.guest) { - return sendError(ws, 'ROOM_NOT_FULL', 'Room must have two players to start the game'); - } - - // Initialize game state if not already done - if (!room.gameState) { - room.gameState = createGame(room.players.host, room.players.guest); - } - - // Create an app session for this game if not already created - if (!hasAppSession(roomId)) { - try { - logger.nitro(`Creating app session for room ${roomId}`); - const appId = await createAppSession(roomId, room.players.host, room.players.guest); - logger.nitro(`App session created with ID ${appId}`); - - // Store the app ID in the room object - room.appId = appId; - } catch (error) { - logger.error(`Failed to create app session for room ${roomId}:`, error); - // Continue with the game even if app session creation fails - // This allows the game to work in a fallback mode - } - } - - // Broadcast game started - roomManager.broadcastToRoom( - roomId, - 'game:started', - { roomId, firstTurn: 'X' } - ); - - // Send the initial game state - roomManager.broadcastToRoom( - roomId, - 'room:state', - formatGameState(room.gameState, roomId) - ); -} - -/** - * Handles a move request - * @param {WebSocket} ws - WebSocket connection - * @param {Object} payload - Request payload - * @param {Object} context - Application context containing roomManager and connections - */ -export async function handleMove(ws, payload, { roomManager, connections, sendError }) { - // Validate payload - const validation = validateMovePayload(payload); - if (!validation.success) { - return sendError(ws, 'INVALID_PAYLOAD', validation.error); - } - - const { roomId, pos } = payload; - - // Find the player making the move - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - // Process the move - const result = roomManager.processMove(roomId, pos, playerEoa); - if (!result.success) { - return sendError(ws, 'MOVE_FAILED', result.error); - } - - // Broadcast updated game state - roomManager.broadcastToRoom( - roomId, - 'room:state', - formatGameState(result.gameState, roomId) - ); - - // Handle game over condition - if (result.isGameOver) { - roomManager.broadcastToRoom( - roomId, - 'game:over', - formatGameOverMessage(result.gameState) - ); - - // Close the app session if one was created - try { - const room = roomManager.rooms.get(roomId); - - // First check if the room has an appId directly - if (room && room.appId) { - logger.nitro(`Closing app session with ID ${room.appId} for room ${roomId}`); - - // Determine winner based on game result - let winnerId = null; - if (result.gameState.winner === 'X') { - winnerId = 'A'; // X is player A (host) - } else if (result.gameState.winner === 'O') { - winnerId = 'B'; // O is player B (guest) - } - // null winner means tie - - // Calculate allocations based on winner - let finalAllocations; - if (winnerId === 'A') { - // Player A wins - gets all the funds - finalAllocations = ['0.02', '0', '0']; // A gets both initial allocations - } else if (winnerId === 'B') { - // Player B wins - gets all the funds - finalAllocations = ['0', '0.02', '0']; // B gets both initial allocations - } else { - // Tie or no winner - split evenly - finalAllocations = ['0.01', '0.01', '0']; - } - - await closeAppSession(roomId, finalAllocations); - logger.nitro(`App session closed for room ${roomId}`); - } - // Otherwise check the app sessions storage - else if (hasAppSession(roomId)) { - logger.nitro(`Closing app session from storage for room ${roomId}`); - - // Determine winner based on game result - let winnerId = null; - if (result.gameState.winner === 'X') { - winnerId = 'A'; // X is player A (host) - } else if (result.gameState.winner === 'O') { - winnerId = 'B'; // O is player B (guest) - } - // null winner means tie - - // Calculate allocations based on winner - let finalAllocations; - if (winnerId === 'A') { - // Player A wins - gets all the funds - finalAllocations = ['0.02', '0', '0']; // A gets both initial allocations - } else if (winnerId === 'B') { - // Player B wins - gets all the funds - finalAllocations = ['0', '0.02', '0']; // B gets both initial allocations - } else { - // Tie or no winner - split evenly - finalAllocations = ['0.01', '0.01', '0']; - } - - await closeAppSession(roomId, finalAllocations); - logger.nitro(`App session closed for room ${roomId}`); - } - } catch (error) { - logger.error(`Failed to close app session for room ${roomId}:`, error); - // Continue with room cleanup even if app session closure fails - } - - // Clean up the room after a short delay - setTimeout(() => { - roomManager.closeRoom(roomId); - }, 5000); - } -} \ No newline at end of file diff --git a/examples/tictactoe/server/src/routes/roomRoutes.js b/examples/tictactoe/server/src/routes/roomRoutes.js deleted file mode 100644 index 880c2a1dd..000000000 --- a/examples/tictactoe/server/src/routes/roomRoutes.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Room-related WebSocket message handlers - */ - -import { validateJoinRoomPayload } from '../utils/validators.js'; -import { formatGameState, generateAppSessionMessage } from '../services/index.js'; -import logger from '../utils/logger.js'; - -/** - * Handles a request to join a room - * @param {WebSocket} ws - WebSocket connection - * @param {Object} payload - Request payload - * @param {Object} context - Application context containing roomManager and connections - */ -export async function handleJoinRoom(ws, payload, { roomManager, connections, sendError }) { - // Validate payload - const validation = validateJoinRoomPayload(payload); - if (!validation.success) { - return sendError(ws, 'INVALID_PAYLOAD', validation.error); - } - - const { roomId, eoa } = payload; - console.log(`Processing ${validation.isCreating ? 'CREATE' : 'JOIN'} request for EOA: ${eoa}, roomId: ${roomId || 'NEW'}`); - - // Check if address is already connected - if (connections.has(eoa)) { - return sendError(ws, 'ALREADY_CONNECTED', 'Address already connected'); - } - - let result; - if (validation.isCreating) { - // Creating a new room - const newRoomId = roomManager.createRoom(); - console.log(`Created new room with ID: ${newRoomId}`); - - // Join the newly created room as host - result = roomManager.joinRoom(newRoomId, eoa, ws); - - if (result.success) { - console.log(`New room created: ${newRoomId} for player (host): ${eoa}`); - - // Send room ID to client immediately so they can share it - ws.send(JSON.stringify({ - type: 'room:created', - roomId: newRoomId, - role: 'host' - })); - } - } else { - // Joining an existing room - result = roomManager.joinRoom(roomId, eoa, ws); - - if (result.success) { - console.log(`Player ${eoa} joined room: ${roomId} as ${result.role}`); - } - } - - if (!result.success) { - return sendError(ws, 'JOIN_FAILED', result.error); - } - - // Store connection - connections.set(eoa, { ws, roomId: result.roomId }); - - // Get room - const room = roomManager.rooms.get(result.roomId); - - // Send room state to all players - if (room.gameState) { - roomManager.broadcastToRoom( - result.roomId, - 'room:state', - formatGameState(room.gameState, result.roomId) - ); - } - - // Notify all players that room is ready if applicable - if (result.isRoomReady) { - roomManager.broadcastToRoom(result.roomId, 'room:ready', { roomId: result.roomId }); - - logger.nitro(`Room ${result.roomId} is ready - starting signature collection flow`); - logger.data(`Room players:`, { host: room.players.host, guest: room.players.guest }); - - // Generate app session message for signature collection when room becomes ready (both players joined) - try { - const appSessionMessage = await generateAppSessionMessage( - result.roomId, - room.players.host, - room.players.guest - ); - - logger.nitro(`Generated app session message for room ${result.roomId}`); - - // Send the message to participant B (guest) for signature - const guestConnection = room.connections.get(room.players.guest); - if (guestConnection && guestConnection.ws.readyState === 1) { - guestConnection.ws.send(JSON.stringify({ - type: 'appSession:signatureRequest', - roomId: result.roomId, - appSessionData: appSessionMessage.appSessionData, - appDefinition: appSessionMessage.appDefinition, - participants: appSessionMessage.participants, - requestToSign: appSessionMessage.requestToSign - })); - } - - } catch (error) { - logger.error(`Failed to generate app session message for room ${result.roomId}:`, error); - } - } -} - -/** - * Handles a request to get available rooms - * @param {WebSocket} ws - WebSocket connection - * @param {Object} context - Application context containing roomManager - */ -export async function handleGetAvailableRooms(ws, { roomManager }) { - // Filter rooms that are not full - const availableRooms = []; - - // Get current timestamp - const now = Date.now(); - - // Iterate through all rooms and find available ones - for (const [roomId, room] of roomManager.rooms.entries()) { - // Room is available if it has a host but no guest, and game is not started - if (room.players.host && !room.players.guest && !room.gameState) { - availableRooms.push({ - roomId, - hostAddress: room.players.host, - createdAt: room.createdAt || now // Use tracked creation time or fall back to now - }); - } - } - - // Send available rooms to client - ws.send(JSON.stringify({ - type: 'room:available', - rooms: availableRooms - })); -} \ No newline at end of file diff --git a/examples/tictactoe/server/src/server.js b/examples/tictactoe/server/src/server.js deleted file mode 100644 index a63727f3a..000000000 --- a/examples/tictactoe/server/src/server.js +++ /dev/null @@ -1,317 +0,0 @@ -/** - * WebSocket server for Nitro Aura Tic Tac Toe game - */ - -import { createWebSocketServer, sendError, startPingInterval } from './config/websocket.js'; -import { initializeRPCClient, createRoomManager } from './services/index.js'; -import { handleJoinRoom, handleGetAvailableRooms } from './routes/roomRoutes.js'; -import { handleStartGame, handleMove } from './routes/gameRoutes.js'; -import { addAppSessionSignature, createAppSessionWithSignatures, getPendingAppSessionMessage } from './services/index.js'; -import logger from './utils/logger.js'; - -// Create WebSocket server -const wss = createWebSocketServer(); -const roomManager = createRoomManager(); - -// Track active connections -// TODO: Use @erc7824/nitrolite for connection tracking when available -const connections = new Map(); - -// Track online users count -let onlineUsersCount = 0; - -/** - * Handles app session signature submission - */ -async function handleAppSessionSignature(ws, payload, { roomManager, connections, sendError }) { - if (!payload || typeof payload !== 'object') { - return sendError(ws, 'INVALID_PAYLOAD', 'Invalid payload format'); - } - - const { roomId, signature } = payload; - - if (!roomId || !signature) { - return sendError(ws, 'INVALID_PAYLOAD', 'Room ID and signature are required'); - } - - // Find the player submitting the signature - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - try { - const allSignaturesCollected = await addAppSessionSignature(roomId, playerEoa, signature); - - logger.nitro(`Signature added for ${playerEoa} in room ${roomId}`); - - // Send confirmation to the signing player - ws.send(JSON.stringify({ - type: 'appSession:signatureConfirmed', - roomId - })); - - // Check if this was participant B (guest) signing, and if so, request signature from participant A (host) - const room = roomManager.rooms.get(roomId); - if (room && playerEoa === room.players.guest && !allSignaturesCollected) { - logger.nitro(`Participant B signed, now requesting signature from participant A (host)`); - - // Send signature request to participant A (host) - const hostConnection = room.connections.get(room.players.host); - if (hostConnection && hostConnection.ws.readyState === 1) { - // Get the existing pending app session message (don't generate a new one!) - const appSessionMessage = getPendingAppSessionMessage(roomId); - - if (!appSessionMessage) { - logger.error(`No pending app session found for room ${roomId}`); - return; - } - - hostConnection.ws.send(JSON.stringify({ - type: 'appSession:startGameRequest', - roomId, - appSessionData: appSessionMessage.appSessionData, - appDefinition: appSessionMessage.appDefinition, - participants: appSessionMessage.participants, - requestToSign: appSessionMessage.requestToSign - })); - - logger.nitro(`Sent start game request to host ${room.players.host}`); - } else { - logger.error(`Host connection not found or not ready for room ${roomId}`); - } - } - - // If all signatures are collected, create the app session (this happens after participant A signs) - if (allSignaturesCollected) { - logger.nitro(`All signatures collected for room ${roomId}, creating app session`); - // The app session creation will be handled by the handleAppSessionStartGame function - } - - } catch (error) { - logger.error(`Error handling app session signature for room ${roomId}:`, error); - return sendError(ws, 'SIGNATURE_ERROR', error.message); - } -} - -/** - * Handles app session start game request (with host signature) - */ -async function handleAppSessionStartGame(ws, payload, { roomManager, connections, sendError }) { - if (!payload || typeof payload !== 'object') { - return sendError(ws, 'INVALID_PAYLOAD', 'Invalid payload format'); - } - - const { roomId, signature } = payload; - - if (!roomId || !signature) { - return sendError(ws, 'INVALID_PAYLOAD', 'Room ID and signature are required'); - } - - // Find the player submitting the signature (should be host) - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - // Get the room - const room = roomManager.rooms.get(roomId); - if (!room) { - return sendError(ws, 'ROOM_NOT_FOUND', 'Room not found'); - } - - // Only the host can start the game - if (room.players.host !== playerEoa) { - return sendError(ws, 'NOT_AUTHORIZED', 'Only the host can start the game'); - } - - try { - // Add the host's signature - const allSignaturesCollected = await addAppSessionSignature(roomId, playerEoa, signature); - - if (!allSignaturesCollected) { - return sendError(ws, 'SIGNATURES_INCOMPLETE', 'Not all signatures collected'); - } - - logger.nitro(`Host signature added for room ${roomId}, creating app session`); - - // Create the app session with all collected signatures - const appId = await createAppSessionWithSignatures(roomId); - - // Store the app ID in the room object - room.appId = appId; - - // Initialize game state - if (!room.gameState) { - const { createGame } = await import('./services/index.js'); - room.gameState = createGame(room.players.host, room.players.guest); - } - - // Broadcast game started - roomManager.broadcastToRoom( - roomId, - 'game:started', - { roomId, firstTurn: 'X', appId } - ); - - // Send the initial game state - const { formatGameState } = await import('./services/index.js'); - roomManager.broadcastToRoom( - roomId, - 'room:state', - formatGameState(room.gameState, roomId) - ); - - } catch (error) { - logger.error(`Error handling app session start game for room ${roomId}:`, error); - return sendError(ws, 'START_GAME_ERROR', error.message); - } -} - -// Function to broadcast online users count to all clients -const broadcastOnlineUsersCount = () => { - const message = JSON.stringify({ - type: 'onlineUsers', - count: onlineUsersCount - }); - - wss.clients.forEach((client) => { - if (client.readyState === 1) { // WebSocket.OPEN - client.send(message); - } - }); - - logger.ws(`Broadcasting online users count: ${onlineUsersCount}`); -}; - -// Create context object to share between route handlers -const context = { - roomManager, - connections, - sendError: (ws, code, msg) => sendError(ws, code, msg) -}; - -wss.on('connection', (ws) => { - logger.ws('Client connected'); - - // Increment online users count and broadcast to all clients - onlineUsersCount++; - broadcastOnlineUsersCount(); - - // Handle client messages - ws.on('message', async (message) => { - let data; - try { - data = JSON.parse(message); - } catch (e) { - return sendError(ws, 'INVALID_JSON', 'Invalid JSON format'); - } - - // Process message based on type - try { - switch (data.type) { - case 'joinRoom': - await handleJoinRoom(ws, data.payload, context); - break; - case 'startGame': - await handleStartGame(ws, data.payload, context); - break; - case 'move': - await handleMove(ws, data.payload, context); - break; - case 'getAvailableRooms': - await handleGetAvailableRooms(ws, context); - break; - case 'appSession:signature': - await handleAppSessionSignature(ws, data.payload, context); - break; - case 'appSession:startGame': - await handleAppSessionStartGame(ws, data.payload, context); - break; - default: - sendError(ws, 'INVALID_MESSAGE_TYPE', 'Invalid message type'); - } - } catch (error) { - logger.error(`Error handling message type ${data.type}:`, error); - sendError(ws, 'INTERNAL_ERROR', 'An internal error occurred'); - } - }); - - // Handle disconnection - ws.on('close', () => { - // Find and remove the player from any room - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - const result = roomManager.leaveRoom(eoa); - if (result.success && result.roomId) { - roomManager.broadcastToRoom(result.roomId, 'room:state', { - roomId: result.roomId, - // Send updated room state here - }); - } - connections.delete(eoa); - break; - } - } - - // Decrement online users count and broadcast to all clients - onlineUsersCount = Math.max(0, onlineUsersCount - 1); - broadcastOnlineUsersCount(); - - logger.ws('Client disconnected'); - }); -}); - -// Initialize Nitrolite client and channel when server starts -async function initializeNitroliteServices() { - try { - logger.nitro('Initializing Nitrolite services...'); - const rpcClient = await initializeRPCClient(); - logger.nitro('Nitrolite RPC client initialized successfully'); - - // Check if we have an existing channel - if (rpcClient.channel) { - logger.nitro('Connected to existing channel'); - logger.data('Channel info', rpcClient.channel); - } else { - logger.warn('No channel established after initialization'); - logger.nitro('Channels will be created as needed via getChannelInfo'); - } - } catch (error) { - logger.error('Failed to initialize Nitrolite services:', error); - logger.system('Continuing in mock mode without Nitrolite channel'); - } -} - -// Start server -const port = process.env.PORT || 8080; -logger.system(`WebSocket server starting on port ${port}`); - -// Initialize Nitrolite client and channel -initializeNitroliteServices().then(() => { - logger.system('Server initialization complete'); -}).catch(error => { - logger.error('Server initialization failed:', error); -}); - -// Start keepalive mechanism -startPingInterval(wss); - -// Broadcast online users count periodically to ensure all clients have the latest count -setInterval(() => { - broadcastOnlineUsersCount(); -}, 30000); \ No newline at end of file diff --git a/examples/tictactoe/server/src/services/appSessions.js b/examples/tictactoe/server/src/services/appSessions.js deleted file mode 100644 index 148834b67..000000000 --- a/examples/tictactoe/server/src/services/appSessions.js +++ /dev/null @@ -1,691 +0,0 @@ -/** - * Nitrolite app sessions for game rooms - * This file handles creating and closing app sessions for games - */ -import { createAppSessionMessage, createCloseAppSessionMessage, parseAnyRPCResponse, RPCMethod } from '@erc7824/nitrolite'; -import { ethers } from 'ethers'; -import dotenv from 'dotenv'; -import logger from '../utils/logger.js'; -import { getRPCClient } from './nitroliteRPC.js'; - -// Load environment variables -dotenv.config(); - -// Map to store app sessions by room ID -const roomAppSessions = new Map(); - -// Map to store pending app session signatures by room ID -const pendingAppSessions = new Map(); - -/** - * Generate app session message for multi-signature collection - * @param {string} roomId - Room ID - * @param {string} participantA - First player's address - * @param {string} participantB - Second player's address - * @returns {Promise} The unsigned app session message and app definition - */ -export async function generateAppSessionMessage(roomId, participantA, participantB) { - try { - // Format addresses to proper checksum format - const formattedParticipantA = ethers.getAddress(participantA); - const formattedParticipantB = ethers.getAddress(participantB); - - logger.nitro(`Generating app session message for room ${roomId} with participants A: ${formattedParticipantA}, B: ${formattedParticipantB}`); - - // Check if we already have a pending session (to ensure consistency) - let pendingSession = pendingAppSessions.get(roomId); - - if (pendingSession) { - logger.nitro(`Using existing app session message for room ${roomId} - nonce: ${pendingSession.nonce}, requestToSign: ${JSON.stringify(pendingSession.requestToSign)}`); - return { - appSessionData: pendingSession.appSessionData, - appDefinition: pendingSession.appDefinition, - participants: [pendingSession.participantA, pendingSession.participantB, pendingSession.serverAddress], - requestToSign: pendingSession.requestToSign - }; - } - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Get the server's address and format it - const serverAddress = ethers.getAddress(rpcClient.address); - - // Create app definition with fixed nonce to ensure all participants sign the same message - const nonce = Date.now(); - const appDefinition = { - protocol: "app_aura_nitrolite_v0", - participants: [formattedParticipantA, formattedParticipantB, serverAddress], - weights: [0, 0, 100], - quorum: 100, - challenge: 0, - nonce: nonce, - }; - - const appSessionData = [{ - definition: appDefinition, - allocations: [ - { - participant: formattedParticipantA, - asset: 'usdc', - amount: '0.01', - }, - { - participant: formattedParticipantB, - asset: 'usdc', - amount: '0.01', - }, - { - participant: serverAddress, - asset: 'usdc', - amount: '0', - }, - ] - }]; - - // Generate the complete request structure that everyone will sign - const sign = rpcClient.signMessage.bind(rpcClient); - const signedMessage = await createAppSessionMessage(sign, appSessionData); - const parsedMessage = JSON.parse(signedMessage); - - // Extract the request structure that clients should sign (same as what server signs) - const requestToSign = parsedMessage.req; - - logger.data(`Generated request structure for room ${roomId}:`, requestToSign); - - // Store the pending app session data including the request structure - pendingAppSessions.set(roomId, { - appSessionData, - appDefinition, - participantA: formattedParticipantA, - participantB: formattedParticipantB, - serverAddress, - signatures: new Map(), - createdAt: Date.now(), - nonce: nonce, - requestToSign: requestToSign, - originalSignedMessage: signedMessage - }); - - logger.nitro(`App session message generated for room ${roomId} with nonce ${nonce}`); - return { - appSessionData, - appDefinition, - participants: [formattedParticipantA, formattedParticipantB, serverAddress], - requestToSign: requestToSign - }; - - } catch (error) { - logger.error(`Error generating app session message for room ${roomId}:`, error); - throw error; - } -} - -/** - * Add a signature to the pending app session - * @param {string} roomId - Room ID - * @param {string} participantAddress - Address of the signing participant - * @param {string} signature - The participant's signature - * @returns {Promise} Whether all signatures are collected - */ -export async function addAppSessionSignature(roomId, participantAddress, signature) { - try { - // Format the participant address to proper checksum format - const formattedParticipantAddress = ethers.getAddress(participantAddress); - - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - throw new Error(`No pending app session found for room ${roomId}`); - } - - // Verify the participant is part of this session - const isValidParticipant = [pendingSession.participantA, pendingSession.participantB].includes(formattedParticipantAddress); - if (!isValidParticipant) { - throw new Error(`Invalid participant ${formattedParticipantAddress} for room ${roomId}`); - } - - // Store the signature - pendingSession.signatures.set(formattedParticipantAddress, signature); - - logger.nitro(`Added signature for ${formattedParticipantAddress} in room ${roomId} (${pendingSession.signatures.size}/2 collected)`); - logger.data(`Signature details:`, { participantAddress: formattedParticipantAddress, signature: signature.substring(0, 10) + '...', signatureLength: signature.length }); - - // Check if we have all participant signatures (not including server) - const allParticipantsSigned = pendingSession.signatures.has(pendingSession.participantA) && - pendingSession.signatures.has(pendingSession.participantB); - - return allParticipantsSigned; - - } catch (error) { - logger.error(`Error adding signature for room ${roomId}:`, error); - throw error; - } -} - -/** - * Create an app session with collected signatures - * @param {string} roomId - Room ID - * @returns {Promise} The app session ID - */ -export async function createAppSessionWithSignatures(roomId) { - try { - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - throw new Error(`No pending app session found for room ${roomId}`); - } - - // Verify all signatures are collected - const allSigned = pendingSession.signatures.has(pendingSession.participantA) && - pendingSession.signatures.has(pendingSession.participantB); - - if (!allSigned) { - throw new Error(`Not all signatures collected for room ${roomId}`); - } - - logger.nitro(`Creating app session with collected signatures for room ${roomId}`); - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Collect all signatures including server signature - const participantASignature = pendingSession.signatures.get(pendingSession.participantA); - const participantBSignature = pendingSession.signatures.get(pendingSession.participantB); - - logger.data(`Participant signatures for room ${roomId}:`, { - participantA: pendingSession.participantA, - participantB: pendingSession.participantB, - participantASignature, - participantBSignature, - allStoredSignatures: Array.from(pendingSession.signatures.entries()) - }); - - // Validate that we have all participant signatures - if (!participantASignature) { - throw new Error(`Missing signature from participant A: ${pendingSession.participantA}`); - } - if (!participantBSignature) { - throw new Error(`Missing signature from participant B: ${pendingSession.participantB}`); - } - - // Don't create a new server signature - use the existing signed message structure - // but replace the single server signature with all collected signatures - - // Create a properly formatted message with all signatures - // The signatures should be in the same order as participants: [participantA, participantB, server] - const allSignatures = [participantASignature, participantBSignature]; - - // Now let the server sign the same request structure as the clients - const sign = rpcClient.signMessage.bind(rpcClient); - - logger.data(`Server signing request structure for room ${roomId}:`, pendingSession.requestToSign); - - // Sign the same request structure that clients signed - const serverSignature = await sign(pendingSession.requestToSign); - - logger.data(`Server signature created:`, serverSignature); - - // Add server signature to complete the array - allSignatures.push(serverSignature); - - logger.data(`Combined signatures for room ${roomId}:`, allSignatures); - - // Send the message directly using ws.send - logger.nitro(`Sending app session creation message for room ${roomId}`); - - if (!rpcClient.ws || rpcClient.ws.readyState !== 1) { - throw new Error('WebSocket not connected or not in OPEN state'); - } - - // Create the final message with all signatures - const finalMessage = JSON.parse(pendingSession.originalSignedMessage); - finalMessage.sig = allSignatures; - - logger.data(`Final message structure:`, { - req: finalMessage.req, - signatures: finalMessage.sig, - participantsOrder: pendingSession.appSessionData[0].definition.participants, - messageToSend: JSON.stringify(finalMessage) - }); - - // Set up a promise to handle the response from the WebSocket - const appSessionResponsePromise = new Promise((resolve, reject) => { - const handleAppSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - - logger.data(`Received app session creation response:`, message); - - if (message.method === RPCMethod.CreateAppSession) { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - resolve(message.params); - } - - if (message.method === RPCMethod.Error) { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error(`Error: ${message.params.error}`)); - } - } catch (error) { - logger.error('Error handling app session response:', error); - } - }; - - rpcClient.ws.on('message', handleAppSessionResponse); - - setTimeout(() => { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error('App session creation timeout')); - }, 10000); - }); - - // Send the final message (convert to string) - rpcClient.ws.send(JSON.stringify(finalMessage)); - - // Wait for the response - const response = await appSessionResponsePromise; - - logger.data(`App session creation response for room ${roomId}:`, response); - - const appId = response?.app_session_id || response?.[0]?.app_session_id; - - if (!appId) { - throw new Error('Failed to get app ID from response'); - } - - // Store the app ID for this room - roomAppSessions.set(roomId, { - appId, - participantA: pendingSession.participantA, - participantB: pendingSession.participantB, - serverAddress: pendingSession.serverAddress, - tokenAddress: process.env.USDC_TOKEN_ADDRESS, - createdAt: Date.now() - }); - - // Clean up pending session - pendingAppSessions.delete(roomId); - - logger.nitro(`Created app session with ID ${appId} for room ${roomId}`); - return appId; - - } catch (error) { - logger.error(`Error creating app session with signatures for room ${roomId}:`, error); - throw error; - } -} - -/** - * Create an app session for a game room (original function for backward compatibility) - * @param {string} roomId - Room ID - * @param {string} participantA - First player's address - * @param {string} participantB - Second player's address - * @returns {Promise} The app session ID - */ -export async function createAppSession(roomId, participantA, participantB) { - try { - logger.nitro(`Creating app session for room ${roomId}`); - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Get the server's address - const serverAddress = rpcClient.address; - - // Check if token address is available - const tokenAddress = process.env.USDC_TOKEN_ADDRESS; - if (!tokenAddress) { - throw new Error('Token address not set in environment variables'); - } - - // Create app definition - const appDefinition = { - protocol: "app_aura_nitrolite_v0", - participants: [participantA, participantB, serverAddress], - weights: [0, 0, 100], - quorum: 100, - challenge: 0, - nonce: Date.now(), - }; - - // Use the RPC client's signMessage method for consistent signing - const sign = rpcClient.signMessage.bind(rpcClient); - - // Create the signed message - const signedMessage = await createAppSessionMessage( - sign, - [ - { - definition: appDefinition, - allocations: [ - { - participant: participantA, - asset: 'usdc', - amount: '0.01', - }, - { - participant: participantB, - asset: 'usdc', - amount: '0.01', - }, - { - participant: serverAddress, - asset: 'usdc', - amount: '0', - }, - ] - }, - ] - ); - logger.data(`Signed app session message for room ${roomId}:`, signedMessage); - // Send the message directly using ws.send, similar to authentication - logger.nitro(`Sending app session creation message for room ${roomId}`); - - if (!rpcClient.ws || rpcClient.ws.readyState !== 1) { // WebSocket.OPEN - throw new Error('WebSocket not connected or not in OPEN state'); - } - - // Set up a promise to handle the response from the WebSocket - const appSessionResponsePromise = new Promise((resolve, reject) => { - // Create a one-time message handler for the app session response - const handleAppSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - - logger.data(`Received app session creation response:`, message); - - // Check if this is an app session response - if (message.method === RPCMethod.CreateAppSession) { - // Remove the listener once we get the response - rpcClient.ws.removeListener('message', handleAppSessionResponse); - resolve(message.params); // The app session data should be in the 3rd position - } - - // Also check for error responses - if (message.method === RPCMethod.Error) { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error(`Error: ${message.params.error}`)); - } - } catch (error) { - logger.error('Error handling app session response:', error); - } - }; - - // Add the message handler - rpcClient.ws.on('message', handleAppSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error('App session creation timeout')); - }, 10000); - }); - - // Send the signed message directly - rpcClient.ws.send(signedMessage); - - // Wait for the response - const response = await appSessionResponsePromise; - - // Log the response - logger.data(`App session creation response for room ${roomId}:`, response); - - // The response structure might vary, adapt this based on actual response - const appId = response?.app_session_id || response?.[0]?.app_session_id; - - if (!appId) { - throw new Error('Failed to get app ID from response'); - } - - // Store the app ID for this room - roomAppSessions.set(roomId, { - appId, - participantA, - participantB, - serverAddress, - tokenAddress, - createdAt: Date.now() - }); - - logger.nitro(`Created app session with ID ${appId} for room ${roomId}`); - return appId; - - } catch (error) { - logger.error(`Error creating app session for room ${roomId}:`, error); - throw error; - } -} - -/** - * Close an app session with winner taking the allocation - * @param {string} roomId - Room ID - * @param {string} winnerId - Winner's participant ID ('A' or 'B'), null for tie - * @returns {Promise} Success status - */ -export async function closeAppSessionWithWinner(roomId, winnerId = null) { - try { - // Get the app session for this room - const appSession = roomAppSessions.get(roomId); - if (!appSession) { - logger.warn(`No app session found for room ${roomId}`); - return false; - } - - const { participantA, participantB } = appSession; - - // Calculate allocations based on winner - let allocations; - if (winnerId === 'A') { - // Player A wins - gets all the funds - allocations = ['0.02', '0', '0']; // A gets both initial allocations - logger.nitro(`Player A (${participantA}) wins room ${roomId} - taking full allocation`); - } else if (winnerId === 'B') { - // Player B wins - gets all the funds - allocations = ['0', '0.02', '0']; // B gets both initial allocations - logger.nitro(`Player B (${participantB}) wins room ${roomId} - taking full allocation`); - } else { - // Tie or no winner - split evenly - allocations = ['0.01', '0.01', '0']; - logger.nitro(`Tie in room ${roomId} - splitting allocation evenly`); - } - - // Use the existing closeAppSession function with calculated allocations - return await closeAppSession(roomId, allocations); - - } catch (error) { - logger.error(`Error closing app session with winner for room ${roomId}:`, error); - return false; - } -} - -/** - * Close an app session for a game room - * @param {string} roomId - Room ID - * @param {Array} [allocations=[0,0,0]] - Final allocations - * @returns {Promise} Success status - */ -export async function closeAppSession(roomId, allocations) { - try { - // Get the app session for this room - const appSession = roomAppSessions.get(roomId); - if (!appSession) { - logger.warn(`No app session found for room ${roomId}`); - return false; - } - - // Make sure appId exists and is properly extracted - const appId = appSession.appId; - if (!appId) { - logger.error(`No appId found in app session for room ${roomId}`); - return false; - } - - logger.nitro(`Closing app session ${appId} for room ${roomId}`); - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Extract participant addresses from the stored app session - const { participantA, participantB, serverAddress } = appSession; - - // Check if we have all the required participants - if (!participantA || !participantB || !serverAddress) { - throw new Error('Missing participant information in app session'); - } - - const finalAllocations = [ - { - participant: participantA, - asset: 'usdc', - amount: allocations[0].toString(), - }, - { - participant: participantB, - asset: 'usdc', - amount: allocations[1].toString(), - }, - { - participant: serverAddress, - asset: 'usdc', - amount: allocations[2].toString(), - }, - ]; - - // Final allocations and close request - const closeRequest = { - app_session_id: appId, - allocations: finalAllocations, - }; - - // Use the RPC client's signMessage method for consistent signing - const sign = rpcClient.signMessage.bind(rpcClient); - - // Create the signed message - const signedMessage = await createCloseAppSessionMessage( - sign, - [closeRequest], - ); - - logger.data(`Signed app session close message for room ${roomId}:`, signedMessage); - - // Send the message directly using ws.send, similar to authentication - logger.nitro(`Sending app session close message for room ${roomId}`); - - if (!rpcClient.ws || rpcClient.ws.readyState !== 1) { // WebSocket.OPEN - throw new Error('WebSocket not connected or not in OPEN state'); - } - - // Set up a promise to handle the response from the WebSocket - const closeSessionResponsePromise = new Promise((resolve, reject) => { - // Create a one-time message handler for the close session response - const handleCloseSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - - logger.data(`Received close session response:`, message); - - // Check if this is a close session response - if (message.method === RPCMethod.CloseAppSession) { - // Remove the listener once we get the response - rpcClient.ws.removeListener('message', handleCloseSessionResponse); - resolve(message.params); - } - - // Also check for error responses - if (message.method === RPCMethod.Error) { - rpcClient.ws.removeListener('message', handleCloseSessionResponse); - reject(new Error(`Error: ${message.params.error}`)); - } - } catch (error) { - logger.error('Error handling close session response:', error); - } - }; - - // Add the message handler - rpcClient.ws.on('message', handleCloseSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - rpcClient.ws.removeListener('message', handleCloseSessionResponse); - reject(new Error('Close session timeout')); - }, 10000); - }); - - // Send the signed message directly - rpcClient.ws.send(signedMessage); - - // Wait for the response - const response = await closeSessionResponsePromise; - - // Log the response - logger.data(`App session close response for room ${roomId}:`, response); - - // Remove the app session - roomAppSessions.delete(roomId); - - logger.nitro(`Closed app session ${appId} for room ${roomId}`); - return true; - - } catch (error) { - logger.error(`Error closing app session for room ${roomId}:`, error); - return false; - } -} - -/** - * Get the app session for a room - * @param {string} roomId - Room ID - * @returns {Object|null} The app session or null if not found - */ -export function getAppSession(roomId) { - return roomAppSessions.get(roomId) || null; -} - -/** - * Get existing pending app session message for a room - * @param {string} roomId - Room ID - * @returns {Object|null} The existing app session message or null if not found - */ -export function getPendingAppSessionMessage(roomId) { - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - return null; - } - - return { - appSessionData: pendingSession.appSessionData, - appDefinition: pendingSession.appDefinition, - participants: [pendingSession.participantA, pendingSession.participantB, pendingSession.serverAddress], - requestToSign: pendingSession.requestToSign - }; -} - -/** - * Check if a room has an app session - * @param {string} roomId - Room ID - * @returns {boolean} Whether the room has an app session - */ -export function hasAppSession(roomId) { - return roomAppSessions.has(roomId); -} - -/** - * Get all app sessions - * @returns {Map} Map of all app sessions - */ -export function getAllAppSessions() { - return roomAppSessions; -} diff --git a/examples/tictactoe/server/src/services/index.js b/examples/tictactoe/server/src/services/index.js deleted file mode 100644 index 2bf0a3356..000000000 --- a/examples/tictactoe/server/src/services/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Services index - exports all service modules - */ - -// Nitrolite RPC (WebSocket) client -export { - initializeRPCClient, - getRPCClient, - NitroliteRPCClient, - WSStatus -} from './nitroliteRPC.js'; - - -// App sessions for game rooms -export { - createAppSession, - closeAppSession, - getAppSession, - hasAppSession, - getAllAppSessions, - generateAppSessionMessage, - getPendingAppSessionMessage, - addAppSessionSignature, - createAppSessionWithSignatures -} from './appSessions.js'; - -// Room management -export { createRoomManager } from './roomManager.js'; - -// Tic Tac Toe game logic -export { createGame, makeMove, checkWinner, formatGameState, formatGameOverMessage } from './ticTacToe.js'; \ No newline at end of file diff --git a/examples/tictactoe/server/src/services/nitroliteOnChain.js b/examples/tictactoe/server/src/services/nitroliteOnChain.js deleted file mode 100644 index 8b5261788..000000000 --- a/examples/tictactoe/server/src/services/nitroliteOnChain.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Nitrolite on-chain operations (separate from WebSocket RPC) - * This file handles all interactions with the blockchain - */ -import { createPublicClient, createWalletClient, http } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { polygon } from "viem/chains"; -import dotenv from "dotenv"; -import logger from "../utils/logger.js"; - -// Load environment variables -dotenv.config(); - -// Singleton instances -let walletClient = null; -let publicClient = null; - -/** - * Initialize wallet and public clients - * @param {string} privateKey - Private key for the server wallet - * @returns {Object} The initialized clients - */ -export async function initializeClients(privateKey) { - try { - if (walletClient && publicClient) { - logger.nitro("Clients already initialized"); - return { walletClient, publicClient }; - } - - logger.nitro("Initializing wallet and public clients..."); - - // Create wallet from private key - const wallet = privateKeyToAccount(privateKey); - const address = wallet.address; - - logger.system(`Server wallet initialized with address: ${address}`); - - // Create public client - publicClient = createPublicClient({ - transport: http(process.env.POLYGON_RPC_URL), - chain: polygon, - }); - - // Create wallet client - walletClient = createWalletClient({ - transport: http(process.env.POLYGON_RPC_URL), - chain: polygon, - account: wallet, - }); - - logger.nitro("Wallet and public clients initialized successfully"); - return { walletClient, publicClient }; - } catch (error) { - logger.error("Error initializing clients:", error); - throw error; - } -} - -/** - * Get the existing wallet client or initialize a new one - * @param {string} privateKey - Private key for the server wallet - * @returns {Object} The wallet client instance - */ -export async function getWalletClient(privateKey) { - if (!walletClient && privateKey) { - await initializeClients(privateKey); - } - return walletClient; -} - -/** - * Get the existing public client or initialize a new one - * @param {string} privateKey - Private key for the server wallet - * @returns {Object} The public client instance - */ -export async function getPublicClient(privateKey) { - if (!publicClient && privateKey) { - await initializeClients(privateKey); - } - return publicClient; -} diff --git a/examples/tictactoe/server/src/services/nitroliteRPC.js b/examples/tictactoe/server/src/services/nitroliteRPC.js deleted file mode 100644 index ab3321480..000000000 --- a/examples/tictactoe/server/src/services/nitroliteRPC.js +++ /dev/null @@ -1,549 +0,0 @@ -/** - * Nitrolite RPC (WebSocket) client - * This file handles all WebSocket communication with Nitrolite server - */ -import { - createAuthRequestMessage, - createAuthVerifyMessage, - createEIP712AuthMessageSigner, - createPingMessage, - NitroliteRPC, - parseAnyRPCResponse, - RPCMethod -} from "@erc7824/nitrolite"; -import dotenv from "dotenv"; -import { ethers } from "ethers"; -import WebSocket from "ws"; - -import logger from "../utils/logger.js"; - -import { getWalletClient } from "./nitroliteOnChain.js"; - -/** - * EIP-712 domain for auth_verify challenge - */ -const getAuthDomain = () => { - return { - name: "Nitro Aura", - }; -}; - -const expire = String(Math.floor(Date.now() / 1000) + 24 * 60 * 60); - -// Load environment variables -dotenv.config(); - -// Connection status -export const WSStatus = { - CONNECTED: "connected", - CONNECTING: "connecting", - DISCONNECTED: "disconnected", - RECONNECTING: "reconnecting", - RECONNECT_FAILED: "reconnect_failed", - AUTH_FAILED: "auth_failed", - AUTHENTICATING: "authenticating", -}; - -// Server-side WebSocket client with authentication -export class NitroliteRPCClient { - constructor(url, privateKey) { - this.url = url; - this.privateKey = privateKey; - this.ws = null; - this.status = WSStatus.DISCONNECTED; - this.channel = null; - this.wallet = new ethers.Wallet(privateKey); - this.address = this.wallet.address; - this.pendingRequests = new Map(); - this.nextRequestId = 1; - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; - this.reconnectDelay = 1000; - this.reconnectTimeout = null; - this.onMessageCallbacks = []; - this.onStatusChangeCallbacks = []; - this.walletClient = null; - - logger.system(`RPC client initialized with address: ${this.address}`); - } - - // Register message callback - onMessage(callback) { - this.onMessageCallbacks.push(callback); - } - - // Register status change callback - onStatusChange(callback) { - this.onStatusChangeCallbacks.push(callback); - } - - // Connect to WebSocket server - async connect() { - if (this.status === WSStatus.CONNECTED || this.status === WSStatus.CONNECTING) { - logger.ws("Already connected or connecting..."); - return; - } - - try { - logger.ws(`Connecting to ${this.url}...`); - this.setStatus(WSStatus.CONNECTING); - - this.ws = new WebSocket(this.url); - - this.ws.on("open", async () => { - logger.ws("WebSocket connection established"); - this.setStatus(WSStatus.AUTHENTICATING); - try { - await this.authenticate(); - logger.auth("Successfully authenticated with the WebSocket server"); - this.reconnectAttempts = 0; - this.startPingInterval(); - } catch (error) { - logger.error("Authentication failed:", error); - this.setStatus(WSStatus.AUTH_FAILED); - this.ws.close(); - } - }); - - this.ws.on("message", (data) => { - this.handleMessage(data); - }); - - this.ws.on("error", (error) => { - logger.error("WebSocket error:", error); - }); - - this.ws.on("close", () => { - logger.ws("WebSocket connection closed"); - this.setStatus(WSStatus.DISCONNECTED); - clearInterval(this.pingInterval); - this.handleReconnect(); - }); - } catch (error) { - logger.error("Failed to connect:", error); - this.setStatus(WSStatus.DISCONNECTED); - this.handleReconnect(); - } - } - - // Update status and notify listeners - setStatus(status) { - const prevStatus = this.status; - this.status = status; - logger.ws(`Status changed: ${prevStatus} -> ${status}`); - this.onStatusChangeCallbacks.forEach((callback) => callback(status)); - } - - // Sign message function for non-auth requests - async signMessage(data) { - const messageStr = typeof data === "string" ? data : JSON.stringify(data); - const digestHex = ethers.id(messageStr); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = this.wallet.signingKey.sign(messageBytes); - return signature; - } - - /** - * Authenticates with the WebSocket server using: - * 1. auth_request: empty signature with wallet address - * 2. auth_verify: EIP-712 signature for challenge verification - * - * @param timeout - Timeout in milliseconds for the entire process - * @returns A Promise that resolves when authenticated - */ - async authenticate(timeout = 10000) { - if (!this.ws) { - throw new Error("WebSocket not connected"); - } - - logger.auth("Starting authentication with SDK 0.2.11 flow..."); - logger.auth("- Wallet address:", this.address); - logger.auth("- EIP-712 signature for auth_verify challenge"); - - const authMessage = { - wallet: this.address, - participant: this.address, - app_name: "Nitro Aura", - expire: expire, // 24 hours in seconds - scope: "console", - application: this.address, - allowances: [], - }; - - return new Promise((resolve, reject) => { - let authTimeoutId = null; - - const cleanup = () => { - if (authTimeoutId) { - clearTimeout(authTimeoutId); - authTimeoutId = null; - } - this.ws.removeEventListener("message", handleAuthResponse); - }; - - const resetTimeout = () => { - if (authTimeoutId) { - clearTimeout(authTimeoutId); - } - authTimeoutId = setTimeout(() => { - cleanup(); - reject(new Error("Authentication timeout")); - }, timeout); - }; - - const handleAuthResponse = async (event) => { - const data = event.data || event; - - try { - const response = parseAnyRPCResponse(data); - - if (response.method === RPCMethod.AuthChallenge) { - logger.auth("Received auth_challenge, preparing EIP-712 auth_verify..."); - resetTimeout(); // Reset timeout while we process and send verify - - try { - logger.auth("Creating EIP-712 signing function..."); - - // Ensure we have a wallet client for EIP-712 signing - if (!this.walletClient) { - logger.auth("Initializing wallet client for EIP-712 signing..."); - this.walletClient = await getWalletClient(this.privateKey); - } - - const eip712SigningFunction = createEIP712AuthMessageSigner( - this.walletClient, - { - scope: authMessage.scope, - application: authMessage.application, - participant: authMessage.participant, - expire: authMessage.expire, - allowances: authMessage.allowances.map((allowance) => ({ - asset: allowance.symbol || allowance.asset, - amount: allowance.amount.toString(), - })), - }, - getAuthDomain(), - ); - - logger.auth("Calling createAuthVerifyMessage..."); - const authVerify = await createAuthVerifyMessage(eip712SigningFunction, response); - - logger.auth("Sending auth_verify with EIP-712 signature"); - this.ws.send(authVerify); - logger.auth("auth_verify sent successfully"); - } catch (eip712Error) { - logger.error("Error creating EIP-712 auth_verify:", eip712Error); - logger.error("Error stack:", eip712Error.stack); - - cleanup(); - reject(new Error(`EIP-712 auth_verify failed: ${eip712Error.message}`)); - return; - } - } - // Check for success response - else if (response.method === RPCMethod.AuthVerify) { - if (!response.params.success) { - return; - } - logger.auth("Authentication successful"); - - cleanup(); - - // Set status to connected - this.setStatus(WSStatus.CONNECTED); - - try { - // Request channel information for our address and check if we - // need to create one - const channels = await this.getChannelInfo(); - // Check if we have valid channels - const hasValidChannel = channels && Array.isArray(channels) && channels.length > 0 && channels[0] !== null; - - if (!hasValidChannel) { - logger.nitro("No valid channels found after authentication, will create one"); - } - } catch (error) { - logger.error("Failed to get channel info, continuing anyway:", error); - } - - resolve(); - } - // Check for error response - else if (response.method === RPCMethod.Error) { - const errorMsg = response.params.error || "Authentication failed"; - - logger.error("Authentication failed:", errorMsg); - cleanup(); - reject(new Error(String(errorMsg))); - } else { - logger.auth("Received non-auth message during auth, continuing to listen:", response); - // Keep listening if it wasn't a final success/error - } - } catch (error) { - // Ignore non-auth methods during authentication - if (error.message && error.message.includes("Unknown method:")) { - logger.auth("Ignoring non-auth message during authentication:", error.message); - return; - } - - logger.error("Error handling auth response:", error); - logger.error("Error stack:", error.stack); - cleanup(); - reject(new Error(`Authentication error: ${error instanceof Error ? error.message : String(error)}`)); - } - }; - - // Step 1: Send auth_request - const sendAuthRequest = async () => { - try { - logger.auth("Sending auth_request..."); - const authRequest = await createAuthRequestMessage(authMessage); - this.ws.send(authRequest); - logger.auth("auth_request sent successfully"); - } catch (requestError) { - logger.error("Error creating auth_request:", requestError); - cleanup(); - reject(new Error(`Failed to create auth_request: ${requestError.message}`)); - } - }; - - this.ws.addEventListener("message", handleAuthResponse); - resetTimeout(); // Start the initial timeout - - // Start authentication process - sendAuthRequest(); - }); - } - - // Handle incoming WebSocket messages - handleMessage(data) { - try { - // Ensure data is properly handled as string - const rawData = typeof data === "string" ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - logger.data("Received message", message); - - // Notify callbacks first to allow for authentication handling - this.onMessageCallbacks.forEach((callback) => callback(message)); - const requestId = message.requestId; - - // Handle response to pending requests - if (message.method === RPCMethod.GetChannels || message.method === RPCMethod.ChannelsUpdate) { - let channels = []; - if (message.method === RPCMethod.GetChannels) { - channels = message.params; - } else { - channels = [message.params]; - } - if (this.pendingRequests.has(requestId)) { - const { resolve } = this.pendingRequests.get(requestId); - resolve(channels); - this.pendingRequests.delete(requestId); - } - } - - // Handle errors - if (message.method === RPCMethod.Error) { - if (this.pendingRequests.has(requestId)) { - const { reject } = this.pendingRequests.get(requestId); - reject(new Error(`Error: ${message.params.error}`)); - this.pendingRequests.delete(requestId); - } - } - } catch (error) { - logger.error("Error handling message:", error); - } - } - - // Send a request to the WebSocket server - async sendRequest(method, params = {}) { - if (!this.ws) { - throw new Error("WebSocket instance not initialized"); - } - - if (this.ws.readyState !== WebSocket.OPEN) { - logger.error(`WebSocket not in OPEN state. Current state: ${this.ws.readyState}, Status: ${this.status}`); - throw new Error(`WebSocket not in OPEN state. Current readyState: ${this.ws.readyState}`); - } - - if (this.status !== WSStatus.CONNECTED) { - logger.warn(`WebSocket status is ${this.status}, should be ${WSStatus.CONNECTED}. Proceeding anyway.`); - if (this.status === WSStatus.AUTHENTICATING) { - logger.system("Fixing status to CONNECTED for authenticated connection"); - this.setStatus(WSStatus.CONNECTED); - } - } - - const requestId = this.nextRequestId++; - const sign = this.signMessage.bind(this); - - return new Promise(async (resolve, reject) => { - try { - const request = NitroliteRPC.createRequest(requestId, method, params); - const signedRequest = await NitroliteRPC.signRequestMessage(request, sign); - - logger.ws(`Sending request: ${JSON.stringify(signedRequest).slice(0, 100)}...`); - - this.pendingRequests.set(requestId, { resolve, reject }); - - setTimeout(() => { - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.delete(requestId); - reject(new Error("Request timeout")); - } - }, 10000); - - this.ws.send(typeof signedRequest === "string" ? signedRequest : JSON.stringify(signedRequest)); - } catch (error) { - logger.error("Error sending request:", error); - this.pendingRequests.delete(requestId); - reject(error); - } - }); - } - - // Start ping interval to keep connection alive - startPingInterval() { - clearInterval(this.pingInterval); - this.pingInterval = setInterval(async () => { - if (this.status === WSStatus.CONNECTED) { - try { - const sign = this.signMessage.bind(this); - const pingMessage = await createPingMessage(sign); - this.ws.send(pingMessage); - } catch (error) { - logger.error("Error sending ping:", error); - } - } - }, 30000); - } - - // Handle reconnection - handleReconnect() { - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - logger.ws("Maximum reconnect attempts reached"); - this.setStatus(WSStatus.RECONNECT_FAILED); - return; - } - - this.reconnectAttempts++; - const delay = this.reconnectDelay * this.reconnectAttempts; - - logger.ws(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); - this.setStatus(WSStatus.RECONNECTING); - - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = setTimeout(() => { - this.connect(); - }, delay); - } - - // Close connection - close() { - clearInterval(this.pingInterval); - clearTimeout(this.reconnectTimeout); - - if (this.ws) { - this.ws.close(); - this.ws = null; - } - - logger.ws("WebSocket connection closed manually"); - this.setStatus(WSStatus.DISCONNECTED); - } - - // Get channel information - async getChannelInfo() { - try { - logger.nitro("Requesting channel information..."); - const response = await this.sendRequest("get_channels", [{ participant: this.address }]); - logger.data("Channel info received", response); - - logger.system("Debug - Raw channel response:"); - logger.system(`- response type: ${typeof response}`); - logger.system(`- is array: ${Array.isArray(response)}`); - logger.system(`- stringified: ${JSON.stringify(response)}`); - - let channels = response; - - if (Array.isArray(response) && response.length === 1 && response[0] === null) { - logger.system("Debug - Got array with single null item"); - } - - if (channels && Array.isArray(channels) && channels.length > 0 && channels[0] !== null) { - logger.nitro(`Found ${channels.length} valid existing channels`); - this.channel = channels[0]; - return channels; - } - - logger.nitro("No valid channels found"); - - if (!this.walletClient) { - logger.nitro("Getting wallet client..."); - this.walletClient = await getWalletClient(this.privateKey); - } - - return []; - } catch (error) { - logger.error("Error getting channel info:", error); - throw error; - } - } -} - -// Initialize and export the client instance -let rpcClient = null; - -export async function initializeRPCClient() { - logger.system("Initializing Nitrolite RPC client..."); - if (rpcClient) { - logger.system("Nitrolite RPC client already initialized, returning existing instance..."); - return rpcClient; - } - - try { - logger.system("Initializing new Nitrolite RPC client..."); - - if (!process.env.SERVER_PRIVATE_KEY) { - throw new Error("SERVER_PRIVATE_KEY environment variable is not set"); - } - - if (!process.env.WS_URL) { - throw new Error("WS_URL environment variable is not set"); - } - - rpcClient = new NitroliteRPCClient(process.env.WS_URL, process.env.SERVER_PRIVATE_KEY); - - rpcClient.onMessage((message) => { - logger.ws("RPC Message:", JSON.stringify(message, null, 2)); - }); - - rpcClient.onStatusChange((status) => { - logger.ws("RPC Status changed:", status); - }); - - await rpcClient.connect(); - rpcClient.walletClient = await getWalletClient(process.env.SERVER_PRIVATE_KEY); - - logger.system("Checking for existing channels..."); - const channels = await rpcClient.getChannelInfo(); - - const hasValidChannel = channels && Array.isArray(channels) && channels.length > 0 && channels[0] !== null; - - if (hasValidChannel) { - logger.nitro(`Found ${channels.length} existing valid channels`); - logger.data("Channel data", channels[0]); - rpcClient.channel = channels[0]; - } else { - logger.nitro("No valid channels found in initializeRPCClient"); - } - } catch (error) { - logger.error("Error during RPC client initialization:", error); - } - - return rpcClient; -} - -export function getRPCClient() { - return rpcClient; -} diff --git a/examples/tictactoe/server/src/services/roomManager.js b/examples/tictactoe/server/src/services/roomManager.js deleted file mode 100644 index a61e3d950..000000000 --- a/examples/tictactoe/server/src/services/roomManager.js +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Room manager for the tic-tac-toe game - */ - -import { v4 as uuidv4 } from 'uuid'; -import { ethers } from 'ethers'; -import { makeMove } from './index.js'; - -/** - * @typedef {Object} Room - * @property {string} id - Unique room identifier - * @property {Object} players - Map of player roles - * @property {string|null} players.host - Host's Ethereum address (X player) - * @property {string|null} players.guest - Guest's Ethereum address (O player) - * @property {Map} connections - Map of player connections by EOA - * @property {Object|null} gameState - Current game state - * @property {boolean} isReady - Whether the room is ready to start - */ - -/** - * @typedef {Object} RoomManager - * @property {Map} rooms - Map of active rooms by ID - * @property {Map} addressToRoom - Map of addresses to room IDs - */ - -/** - * Creates a new room manager - * @returns {RoomManager} Room manager instance - */ -export function createRoomManager() { - // In-memory storage for rooms and address-to-room mapping - const rooms = new Map(); - const addressToRoom = new Map(); - - /** - * Creates a new room - * @returns {string} Room ID - */ - function createRoom() { - const roomId = uuidv4(); - rooms.set(roomId, { - id: roomId, - players: { - host: null, - guest: null - }, - connections: new Map(), - gameState: null, - isReady: false, - createdAt: Date.now() - }); - return roomId; - } - - /** - * Adds a player to a room - * @param {string} roomId - Room ID - * @param {string} eoa - Player's Ethereum address - * @param {Object} ws - WebSocket connection - * @returns {Object} Result with success flag and additional info - */ - function joinRoom(roomId, eoa, ws) { - // Format address to proper checksum format - const formattedEoa = ethers.getAddress(eoa); - - // Check if player is already in another room - if (addressToRoom.has(formattedEoa)) { - return { - success: false, - error: 'Player already in another room' - }; - } - - // Get the room - if roomId doesn't exist, return error - if (!rooms.has(roomId)) { - return { - success: false, - error: 'Room not found' - }; - } - - let room = rooms.get(roomId); - - // Check if room is full - if (room.players.host && room.players.guest) { - return { - success: false, - error: 'Room is full' - }; - } - - // Assign player to available role - let role; - if (!room.players.host) { - room.players.host = formattedEoa; - role = 'host'; - } else if (!room.players.guest) { - room.players.guest = formattedEoa; - role = 'guest'; - } - - // Store connection and map address to room - room.connections.set(formattedEoa, { ws, role }); - addressToRoom.set(formattedEoa, roomId); - - // Check if room has both players (ready to potentially start) - const isRoomFull = room.players.host && room.players.guest; - - // Mark as ready when both players are present, but don't auto-start the game - if (isRoomFull && !room.isReady) { - room.isReady = true; - console.log(`Room ${roomId} is ready with players: ${room.players.host} (host) and ${room.players.guest} (guest)`); - // The game will be created when the host clicks "Start Game" - } - - return { - success: true, - roomId, - role, - isRoomReady: isRoomFull - }; - } - - /** - * Process a move in a game - * @param {string} roomId - Room ID - * @param {number} position - Position on the board (0-8) - * @param {string} eoa - Player's Ethereum address - * @returns {Object} Result with success flag and additional info - */ - function processMove(roomId, position, eoa) { - // Format address to proper checksum format - const formattedEoa = ethers.getAddress(eoa); - - if (!rooms.has(roomId)) { - return { - success: false, - error: 'Room not found' - }; - } - - const room = rooms.get(roomId); - - // Check if player is in this room - if (!room.connections.has(formattedEoa)) { - return { - success: false, - error: 'Player not in this room' - }; - } - - // Check if the game has started - if (!room.gameState) { - return { - success: false, - error: 'Game has not started' - }; - } - - // Make the move - const result = makeMove(room.gameState, position, formattedEoa); - if (!result.success) { - return result; - } - - // Update game state - room.gameState = result.gameState; - - return { - success: true, - gameState: room.gameState, - isGameOver: room.gameState.isGameOver - }; - } - - /** - * Removes a player from a room - * @param {string} eoa - Player's Ethereum address - * @returns {Object} Result with success flag and removed room info - */ - function leaveRoom(eoa) { - // Format address to proper checksum format - const formattedEoa = ethers.getAddress(eoa); - - if (!addressToRoom.has(formattedEoa)) { - return { - success: false, - error: 'Player not in any room' - }; - } - - const roomId = addressToRoom.get(formattedEoa); - const room = rooms.get(roomId); - - // Clean up player connections - if (room) { - room.connections.delete(formattedEoa); - - // Update player list - if (room.players.host === formattedEoa) { - room.players.host = null; - } else if (room.players.guest === formattedEoa) { - room.players.guest = null; - } - - // Clean up room if empty - if (!room.players.host && !room.players.guest) { - rooms.delete(roomId); - } - } - - addressToRoom.delete(formattedEoa); - - return { - success: true, - roomId - }; - } - - /** - * Broadcasts a message to all players in a room - * @param {string} roomId - Room ID - * @param {string} type - Message type - * @param {Object} data - Message data - */ - function broadcastToRoom(roomId, type, data) { - if (!rooms.has(roomId)) return; - - const room = rooms.get(roomId); - const message = JSON.stringify({ type, ...data }); - - for (const connection of room.connections.values()) { - if (connection.ws.readyState === 1) { // WebSocket.OPEN - connection.ws.send(message); - } - } - } - - /** - * Closes a room and notifies all players - * @param {string} roomId - Room ID - */ - function closeRoom(roomId) { - if (!rooms.has(roomId)) return; - - const room = rooms.get(roomId); - - // Remove all players from the room - for (const eoa of room.connections.keys()) { - addressToRoom.delete(eoa); - } - - // Delete the room - rooms.delete(roomId); - } - - // Return public API - return { - rooms, - addressToRoom, - createRoom, - joinRoom, - processMove, - leaveRoom, - broadcastToRoom, - closeRoom - }; -} \ No newline at end of file diff --git a/examples/tictactoe/server/src/services/ticTacToe.js b/examples/tictactoe/server/src/services/ticTacToe.js deleted file mode 100644 index e6ae0c53b..000000000 --- a/examples/tictactoe/server/src/services/ticTacToe.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Tic Tac Toe game engine - */ -import { ethers } from 'ethers'; - -/** - * @typedef {Object} GameState - * @property {Array} board - 9 elements representing the board (null or player symbol) - * @property {string} nextTurn - The player whose turn is next ('X' or 'O') - * @property {string|null} winner - The winner of the game ('X', 'O', or null if no winner yet) - * @property {boolean} isGameOver - Whether the game is over - * @property {Object} players - Object with player information - * @property {string} players.X - EOA address of player X - * @property {string} players.O - EOA address of player O - */ - -/** - * Creates a new game state - * @param {string} hostEoa - Host's Ethereum address (X player) - * @param {string} guestEoa - Guest's Ethereum address (O player) - * @returns {GameState} Initial game state - */ -export function createGame(hostEoa, guestEoa) { - // Format addresses to proper checksum format - const formattedHostEoa = ethers.getAddress(hostEoa); - const formattedGuestEoa = ethers.getAddress(guestEoa); - - return { - board: Array(9).fill(null), - nextTurn: 'X', // X goes first - winner: null, - isGameOver: false, - players: { - X: formattedHostEoa, - O: formattedGuestEoa - } - }; -} - -/** - * Makes a move on the board - * @param {GameState} gameState - Current game state - * @param {number} position - Position to place the piece (0-8) - * @param {string} playerEoa - Player's Ethereum address - * @returns {Object} Result with updated game state or error - */ -export function makeMove(gameState, position, playerEoa) { - // Format player address to proper checksum format - const formattedPlayerEoa = ethers.getAddress(playerEoa); - - // Check if the game is already over - if (gameState.isGameOver) { - return { success: false, error: 'Game is already over' }; - } - - // Check if it's the player's turn - const playerSymbol = gameState.players.X === formattedPlayerEoa ? 'X' : 'O'; - if (playerSymbol !== gameState.nextTurn) { - return { success: false, error: 'Not your turn' }; - } - - // Check if the position is valid and empty - if (position < 0 || position > 8 || gameState.board[position] !== null) { - return { success: false, error: 'Invalid move: position is occupied or out of bounds' }; - } - - // Create a new board with the move applied - const newBoard = [...gameState.board]; - newBoard[position] = playerSymbol; - - // Check for win or draw - const winner = checkWinner(newBoard); - const isDraw = !winner && newBoard.every(cell => cell !== null); - - // Create updated game state - const updatedGameState = { - ...gameState, - board: newBoard, - nextTurn: playerSymbol === 'X' ? 'O' : 'X', - winner: winner, - isGameOver: !!winner || isDraw - }; - - return { - success: true, - gameState: updatedGameState - }; -} - -/** - * Checks if there's a winner on the board - * @param {Array} board - The current board state - * @returns {string|null} The winner ('X' or 'O') or null if no winner - */ -export function checkWinner(board) { - // Winning patterns: rows, columns, and diagonals - const lines = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns - [0, 4, 8], [2, 4, 6] // Diagonals - ]; - - for (const [a, b, c] of lines) { - if (board[a] && board[a] === board[b] && board[a] === board[c]) { - return board[a]; // Return the winner symbol ('X' or 'O') - } - } - - return null; // No winner -} - -/** - * Formats game state for client consumption - * @param {GameState} gameState - Current game state - * @param {string} roomId - Room ID - * @returns {Object} Formatted game state for client - */ -export function formatGameState(gameState, roomId) { - return { - roomId, - board: gameState.board, - nextTurn: gameState.nextTurn, - players: gameState.players - }; -} - -/** - * Formats game over message - * @param {GameState} gameState - Current game state - * @returns {Object} Game over message - */ -export function formatGameOverMessage(gameState) { - return { - winner: gameState.winner, - board: gameState.board - }; -} \ No newline at end of file diff --git a/examples/tictactoe/server/src/utils/logger.js b/examples/tictactoe/server/src/utils/logger.js deleted file mode 100644 index 3836876f5..000000000 --- a/examples/tictactoe/server/src/utils/logger.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Logger utility with color-coded console outputs - */ -import chalk from 'chalk'; - -// Log level colors -const colors = { - info: chalk.blue, - success: chalk.green, - warn: chalk.yellow, - error: chalk.red, - debug: chalk.magenta, - system: chalk.cyan, - auth: chalk.hex('#FF8800'), - ws: chalk.hex('#00AAFF'), - nitro: chalk.hex('#9900FF'), - game: chalk.hex('#00FF99') -}; - -// Timestamp generator -const timestamp = () => { - const now = new Date(); - return chalk.gray(`[${now.toISOString().split('T')[1].slice(0, -1)}]`); -}; - -// Logger implementation -export const logger = { - info: (message, ...args) => console.log(timestamp(), colors.info('INFO'), message, ...args), - success: (message, ...args) => console.log(timestamp(), colors.success('SUCCESS'), message, ...args), - warn: (message, ...args) => console.warn(timestamp(), colors.warn('WARNING'), message, ...args), - error: (message, ...args) => console.error(timestamp(), colors.error('ERROR'), message, ...args), - debug: (message, ...args) => console.debug(timestamp(), colors.debug('DEBUG'), message, ...args), - system: (message, ...args) => console.log(timestamp(), colors.system('SYSTEM'), message, ...args), - auth: (message, ...args) => console.log(timestamp(), colors.auth('AUTH'), message, ...args), - ws: (message, ...args) => console.log(timestamp(), colors.ws('WEBSOCKET'), message, ...args), - nitro: (message, ...args) => console.log(timestamp(), colors.nitro('NITROLITE'), message, ...args), - game: (message, ...args) => console.log(timestamp(), colors.game('GAME'), message, ...args), - - // Special format for objects/data - data: (label, data) => { - console.log( - timestamp(), - chalk.hex('#888888')('DATA'), - chalk.cyan(label + ':'), - typeof data === 'object' ? '\n' + JSON.stringify(data, null, 2) : data - ); - } -}; - -export default logger; \ No newline at end of file diff --git a/examples/tictactoe/server/src/utils/validators.js b/examples/tictactoe/server/src/utils/validators.js deleted file mode 100644 index 792ce8e60..000000000 --- a/examples/tictactoe/server/src/utils/validators.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Validators for game inputs - */ - -/** - * Validates Ethereum address format - * @param {string} address - Ethereum address to validate - * @returns {boolean} True if the address is valid - */ -export function isValidEthereumAddress(address) { - // Check if it's a string and matches Ethereum address pattern (0x followed by 40 hex chars) - return typeof address === 'string' - && /^0x[a-fA-F0-9]{40}$/.test(address); -} - -/** - * Validates room ID format - * @param {string} roomId - Room ID to validate - * @returns {boolean} True if the room ID is valid - */ -export function isValidRoomId(roomId) { - // Basic UUID v4 format check - return typeof roomId === 'string' - && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(roomId); -} - -/** - * Validates move position format - * @param {number} pos - Position on the board (0-8) - * @returns {boolean} True if the position is valid - */ -export function isValidPosition(pos) { - return Number.isInteger(pos) && pos >= 0 && pos <= 8; -} - -/** - * Validates join room payload - * @param {object} payload - The payload to validate - * @param {string} payload.roomId - Room ID - * @param {string} payload.eoa - Ethereum address - * @returns {object} Validation result with success flag and optional error message - */ -export function validateJoinRoomPayload(payload) { - if (!payload || typeof payload !== 'object') { - return { success: false, error: 'Invalid payload format' }; - } - - // EOA validation - if (!payload.eoa) { - return { success: false, error: 'Ethereum address is required' }; - } - - if (!isValidEthereumAddress(payload.eoa)) { - return { success: false, error: 'Invalid Ethereum address format' }; - } - - // Room ID validation - // If roomId is undefined, we're creating a new room - // If roomId is provided, we're joining an existing room - if (payload.roomId === undefined) { - // Creating a new room - no further validation needed - console.log("Creating new room"); - return { success: true, isCreating: true }; - } else { - // Joining a room - validate room ID - if (!isValidRoomId(payload.roomId)) { - return { success: false, error: 'Invalid room ID format' }; - } - console.log("Joining existing room:", payload.roomId); - return { success: true, isJoining: true }; - } -} - -/** - * Validates move payload - * @param {object} payload - The payload to validate - * @param {string} payload.roomId - Room ID - * @param {number} payload.pos - Position on the board (0-8) - * @returns {object} Validation result with success flag and optional error message - */ -export function validateMovePayload(payload) { - if (!payload || typeof payload !== 'object') { - return { success: false, error: 'Invalid payload format' }; - } - - if (!payload.roomId) { - return { success: false, error: 'Room ID is required' }; - } - - if (!isValidRoomId(payload.roomId)) { - return { success: false, error: 'Invalid room ID format' }; - } - - if (payload.pos === undefined) { - return { success: false, error: 'Position is required' }; - } - - if (!isValidPosition(payload.pos)) { - return { success: false, error: 'Invalid position format (must be 0-8)' }; - } - - return { success: true }; -} \ No newline at end of file diff --git a/examples/viper-duel/.gitignore b/examples/viper-duel/.gitignore deleted file mode 100644 index 66d62f85b..000000000 --- a/examples/viper-duel/.gitignore +++ /dev/null @@ -1 +0,0 @@ -**/.claude/settings.local.json diff --git a/examples/viper-duel/GAME_DEVELOPMENT_PROMPTS.md b/examples/viper-duel/GAME_DEVELOPMENT_PROMPTS.md deleted file mode 100644 index 5f43d95cb..000000000 --- a/examples/viper-duel/GAME_DEVELOPMENT_PROMPTS.md +++ /dev/null @@ -1,596 +0,0 @@ -# Game Development Prompts & Requirements Template - -This document contains comprehensive prompts and patterns for converting tic-tac-toe games to other multiplayer games with betting systems. Originally built for Viper Duel Snake game, these templates work for any real-time multiplayer game. - -## Initial Setup & Architecture Analysis - -### 1. Codebase Analysis -``` -init is analyzing your codebase… -Please analyze this codebase and create a CLAUDE.md file, which will be given to future instances of Claude Code to operate in this repository. -``` - -**Purpose**: Understand existing codebase structure and create documentation for future development. - -## Game Conversion Framework - -### Converting from Tic-Tac-Toe to Any Multiplayer Game - -Use this systematic approach to convert a tic-tac-toe base to any real-time multiplayer game: - -#### Step 1: Core Game Logic Replacement -``` -I want to convert this tic-tac-toe game to [YOUR GAME TYPE]. Please: -1. Replace the game logic in services/[game].js -2. Update the game state interfaces -3. Implement [GAME-SPECIFIC] mechanics -4. Keep the existing room management and WebSocket infrastructure -``` - -#### Step 2: Visual Component Updates -``` -Replace the tic-tac-toe board with [YOUR GAME] visuals: -1. Create new game rendering component (Board.tsx → [Game]Canvas.tsx) -2. Update game state display components -3. Implement [GAME-SPECIFIC] visual effects -4. Maintain responsive design and theming -``` - -#### Step 3: Input/Control System -``` -Update the input system for [YOUR GAME]: -1. Replace click-based moves with [INPUT TYPE] (keyboard/mouse/touch) -2. Implement real-time input handling -3. Add input validation and anti-cheat measures -4. Support multiple input methods if needed -``` - -## Betting System Implementation - -### Complete Betting System Setup - -Use this comprehensive prompt to add betting functionality to any game: - -``` -Add a flexible betting system to this game with the following requirements: - -BETTING AMOUNTS: -- Support bet amounts: free, 0.01, 0.1, 1, 2 (in USDC) -- Winner-takes-all pot system (betAmount × 2) -- Display currency as $ symbol (not USDC) - -CLIENT IMPLEMENTATION: -1. Add bet amount types (BetAmount, BetOption) to types/index.ts -2. Update interfaces: GameState, JoinRoomPayload, AvailableRoom -3. Create bet selection UI in lobby with visual feedback -4. Add bet amount badges to available rooms with proper alignment -5. Update all WebSocket message payloads to include betAmount -6. Fix TypeScript errors in game state management - -SERVER IMPLEMENTATION: -1. Update room management to support bet amounts in roomManager.js -2. Add bet amount validation in room creation and joining -3. Implement bet amount matching (players can only join matching stakes) -4. Update formatGameState to include bet amounts -5. Add comprehensive bet amount validation in validators.js -6. Update all WebSocket message handlers - -UI REQUIREMENTS: -- 3x2 grid layout for bet selection with visual feedback -- Bet amount badges on available rooms (Free, $0.01, $0.1, $1, $2) -- Winner-takes-all pot calculation display -- Proper alignment and responsive design -- Remove any hardcoded betting messages - -VALIDATION REQUIREMENTS: -- Client and server-side bet amount validation -- Room bet amount matching enforcement -- Error handling for bet mismatches -- Type safety throughout the system -``` - -### Betting UI Components Pattern - -``` -Create a professional betting interface with: - -BET SELECTION: -- Grid layout with buttons for each bet amount -- Visual feedback (glow effect for selected amount) -- Winner pot calculation display -- Clean typography and spacing - -ROOM DISPLAY: -- Bet amount badges on available rooms -- Proper alignment and truncation handling -- Color coding (gray for free, amber for paid) -- Responsive layout for different stake amounts - -CURRENCY DISPLAY: -- Use $ symbol consistently (not USDC) -- Proper decimal formatting ($0.01, not $0.010) -- Clear pot calculations (Winner takes all! Total pot: $2.00) -``` - -## Visual Enhancement Patterns - -### Canvas-Based Game Rendering - -For smooth, professional game visuals, replace basic CSS grids with canvas rendering: - -``` -Replace the blocky [GAME ELEMENTS] with smooth, glowing, brand-consistent visuals: - -Visual spec: -- [MAIN ELEMENTS] – [SPECIFIC DESIGN REQUIREMENTS] -- [SECONDARY ELEMENTS] – [DESIGN DETAILS] -- Neon fill/glow with brand colors -- Enhanced animations and effects - -Create radial gradients per [GAME ELEMENT]: -- [COLOR SCHEME 1]: #[HEX] → #[HEX] → rgba([RGB],0) -- [COLOR SCHEME 2]: #[HEX] → #[HEX] → rgba([RGB],0) - -Effects: -- ctx.shadowBlur = 12; ctx.shadowColor = color; -- Highlight pass with CRT shine effect -- Smooth transitions and animations -``` - -### Brand Identity Implementation - -``` -Complete rebrand from [OLD NAME] to [NEW NAME] with: - -BRAND ELEMENTS: -- Update all references in codebase -- Package names and descriptions -- Documentation and README files -- Authentication domains and app identifiers - -VISUAL DESIGN: -- Logo references and taglines -- Color palette and themes -- Visual effects and particles -- Typography and spacing - -TECHNICAL UPDATES: -- EIP-712 domain names -- WebSocket app names -- Error messages and labels -- File and component names -``` - -## Architecture Patterns - -### Real-Time Game Loop Implementation - -``` -Convert from turn-based to real-time gameplay: - -MOVEMENT SYSTEM: -- Implement automatic [GAME MECHANICS] (every Xms) -- Player input only changes [CONTROL VARIABLES] -- Continuous [GAME STATE] updates -- Remove turn-based validation - -GAME LOOP: -- Server-side game state management -- Broadcast updates to all players -- Handle disconnections gracefully -- Implement game over detection - -TIMING: -- Configurable game speed (200ms-1000ms intervals) -- Input buffering and validation -- Smooth client-side prediction -- Server authoritative state -``` - -### WebSocket Message Architecture - -``` -Implement comprehensive real-time messaging: - -CLIENT → SERVER: -- joinRoom (with bet amount) -- [GAME_ACTION] (player input) -- getAvailableRooms -- appSession signatures - -SERVER → CLIENT: -- room:state (game state updates) -- room:ready (room full notification) -- game:started (game begin) -- game:update (real-time updates) -- game:over (end state) -- error (validation failures) - -VALIDATION: -- Server-side input validation -- Rate limiting and anti-cheat -- State synchronization -- Error handling and recovery -``` - -## Core Game Mechanics Issues - -### Snake Game Specific Examples - -These examples show the progression from tic-tac-toe to Snake game mechanics: - -### 2. Movement Controls Fix -``` -ok now i receive error Cannot reverse direction - -movements doesnt work we need support wasd and arrows movements -``` - -**Issues to Fix**: -- Remove "Cannot reverse direction" restrictions -- Add support for both WASD and arrow key controls -- Fix keyboard event detection (case sensitivity issues) - -**Solution Applied**: -- Fixed arrow key detection (`ArrowUp` vs `arrowup`) -- Removed reverse direction validation -- Added dual control support (WASD + arrows) - -### 3. Automatic Movement Implementation -``` -and also when game start in defult game snakes is move imidiatly -``` - -**Requirement**: Implement automatic Snake movement like classic arcade games -- Snakes should move automatically every second -- Players only change direction, don't trigger movement -- Continuous movement in current direction - -### 4. Movement Logic Clarification -``` -no it start move imidiatly movements and user controle it -``` - -**Clarification**: User wants classic Snake behavior: -- Automatic movement every second in current direction -- User input only changes direction -- Not manual movement per key press - -### 5. UI Movement Issues -``` -0 movements on ui, maybe issue on client -``` - -**Problem**: No visual movement on the client side -**Root Cause**: Game loop wasn't starting in app session flow -**Solution**: Added missing `startGameLoop()` call to app session initialization - -### 6. Game Ending & Scoring Issues -``` -Final Scores -Player 1: 1 -Player 2: 0 -Game Time: 17 seconds -Player 1 wins! - -now it works can - -but i just got 1 score, and game end only when snake touch each other, it can have n amoutn of score and make more fast change of direction -``` - -**Issues Identified**: -- Limited scoring (only 1 point possible) -- Game only ends on snake-to-snake collision -- Need faster direction changes -- Want unlimited scoring potential - -**Fixes Applied**: -- Multiple food items (3+ always available) -- Faster game loop (500ms instead of 1000ms) -- Fixed collision detection for walls and self-collision -- Unlimited food respawning - -### 7. Real-Time Movement Request -``` -lets do movements update in real time without delay, and i just collect first item to eat and game was end -``` - -**Final Requirements**: -- **Real-time movement**: No delays, instant response to key presses -- **Bug fix**: Game ending after first food collection - -**Implementation**: -- Removed automatic timer-based movement -- Movement happens instantly on key press -- Added comprehensive debugging for collision detection -- Fixed premature game ending bugs - -## Technical Implementation Patterns - -### Movement System Evolution - -**Phase 1: Automatic Timer-Based** -```javascript -setInterval(() => { - // Move all snakes automatically every X ms - updateGameState(); -}, intervalTime); -``` - -**Phase 2: Real-Time Input-Based** -```javascript -// On direction change: -changeDirection(direction) { - updateDirection(direction); - moveSnake(playerId); // Immediate movement - broadcastUpdate(); -} -``` - -### Collision Detection Requirements -- Wall collisions should end game -- Self-collisions should end game -- Snake-to-snake collisions should end game -- Food collisions should increase score and grow snake - -### Food & Scoring System -- Multiple food items on board (3+ minimum) -- Unlimited scoring potential -- Automatic food respawning -- Score increases with each food eaten - -### Network Architecture -- Real-time WebSocket communication -- Client-server state synchronization -- Immediate broadcast of movement updates -- Game over detection and cleanup - -## Common Game Development Issues & Solutions - -### 1. Keyboard Input Handling -**Problem**: Arrow keys not working -**Solution**: Check case sensitivity (`ArrowUp` vs `arrowup`) - -### 2. Game Loop Management -**Problem**: No visual updates -**Solution**: Ensure game loop starts in all code paths (regular + app session) - -### 3. Collision Detection -**Problem**: Game ending unexpectedly -**Solution**: Add comprehensive logging for all collision types - -### 4. Real-Time vs Turn-Based -**Problem**: Delayed responses -**Solution**: Move from timer-based to event-driven movement - -### 5. Multiplayer Synchronization -**Problem**: Players seeing different states -**Solution**: Immediate broadcast after each player action - -## Debug Logging Patterns - -### Movement Debugging -```javascript -console.log(`Key pressed: ${event.key}, Direction: ${direction}`); -console.log(`🐍 GAME UPDATE:`, { gameTime, player1Pos, player2Pos }); -``` - -### Collision Debugging -```javascript -console.log(`💥 ${playerId} hit wall at (${x}, ${y})`); -console.log(`🔄 ${playerId} collided with self`); -console.log(`🐍 ${playerId} collided with ${otherPlayer}`); -``` - -### Game State Debugging -```javascript -console.log(`🍎 ${playerId} ate food! New score: ${score}`); -console.log(`🔍 Game state: player1 alive=${alive1}, player2 alive=${alive2}`); -``` - -## Performance Optimization Patterns - -### Network Optimization -- Only broadcast state changes, not full state every frame -- Use efficient message types (`room:state`, `game:update`, `game:over`) -- Implement game over detection loops separate from movement - -### Client-Side Optimization -- Prevent default browser behavior for game keys -- Use React state updates efficiently -- Implement proper cleanup for event listeners - -## Future Game Development Checklist - -### Initial Setup -- [ ] Analyze existing codebase structure -- [ ] Create comprehensive CLAUDE.md documentation -- [ ] Set up client-server architecture - -### Movement System -- [ ] Implement keyboard input handling (WASD + arrows) -- [ ] Choose movement model (real-time vs timer-based) -- [ ] Add collision detection (walls, self, others) -- [ ] Test movement responsiveness - -### Game Mechanics -- [ ] Implement scoring system -- [ ] Add multiple objectives (food items) -- [ ] Set up win/lose conditions -- [ ] Add game over detection - -### Multiplayer Features -- [ ] WebSocket real-time communication -- [ ] State synchronization between players -- [ ] Room management system -- [ ] Player authentication - -### Debug & Testing -- [ ] Add comprehensive logging -- [ ] Test all collision scenarios -- [ ] Verify multiplayer synchronization -- [ ] Performance testing - -### Polish & UX -- [ ] Visual feedback for actions -- [ ] Sound effects (optional) -- [ ] Responsive controls -- [ ] Clear game state indicators - -## Template Commands for Claude - -### Quick Fixes -``` -"Fix [specific issue] - [brief description]" -"Add support for [feature] - [requirements]" -"Debug [problem] - showing [symptoms]" -``` - -### Feature Requests -``` -"Implement [feature] with [specific requirements]" -"Make [system] work like [reference/example]" -"Add [functionality] that [specific behavior]" -``` - -### Technical Issues -``` -"No [expected behavior] on [platform/component]" -"[Component] not working - [specific symptoms]" -"[Feature] only works [partial condition], need [full requirement]" -``` - -This template captures the iterative development process and can be used as a reference for building similar real-time multiplayer games. - -## Complete Development Workflow - -### Recommended Development Sequence - -Use this step-by-step workflow for converting any tic-tac-toe base to a new multiplayer game: - -#### Phase 1: Foundation Setup (1-2 hours) -``` -1. Analyze existing codebase and create CLAUDE.md -2. Update brand identity (name, colors, taglines) -3. Replace core game logic (tic-tac-toe → your game) -4. Update game state interfaces and types -``` - -#### Phase 2: Core Mechanics (2-4 hours) -``` -1. Implement game-specific input system -2. Add real-time game loop and state updates -3. Replace visual components (Board → GameCanvas) -4. Add game-specific validation and rules -``` - -#### Phase 3: Betting System (1-2 hours) -``` -1. Add betting types and interfaces -2. Update UI with bet selection components -3. Implement server-side bet validation -4. Test room matching and error handling -``` - -#### Phase 4: Visual Polish (2-3 hours) -``` -1. Replace basic visuals with canvas rendering -2. Add neon effects, gradients, and animations -3. Implement smooth visual transitions -4. Add responsive design and accessibility -``` - -#### Phase 5: Testing & Deployment (1 hour) -``` -1. Test all betting scenarios and game mechanics -2. Verify WebSocket message handling -3. Test multiplayer synchronization -4. Deploy and monitor performance -``` - -### Quick Start Templates - -#### Basic Game Conversion -``` -Convert this tic-tac-toe game to [GAME NAME]: - -1. Replace game logic in services/snake.js with [GAME] mechanics -2. Update GameState interface for [GAME] requirements -3. Create [GAME]Canvas.tsx for visual rendering -4. Implement [INPUT_TYPE] controls (keyboard/mouse/touch) -5. Add real-time game loop with [TIMING] updates -6. Keep existing room management and WebSocket infrastructure -``` - -#### Betting System Integration -``` -Add flexible betting (free, $0.01, $0.1, $1, $2) to this game: - -CLIENT SIDE: -- Add BetAmount type and betting interfaces -- Create bet selection UI with visual feedback -- Update all message payloads to include betAmount -- Add bet badges to available rooms - -SERVER SIDE: -- Update room creation/joining with bet validation -- Implement bet matching enforcement -- Add comprehensive error handling -- Update game state formatting - -UI POLISH: -- Use $ symbol for currency display -- Add winner-takes-all pot calculations -- Ensure proper alignment and responsive design -``` - -#### Visual Enhancement Upgrade -``` -Replace basic [GAME] elements with smooth, glowing visuals: - -CANVAS RENDERING: -- Create smooth [GAME ELEMENTS] with neon effects -- Implement radial gradients and glow effects -- Add directional animations and transitions -- Use brand-consistent color schemes - -EFFECTS: -- ctx.shadowBlur = 12 for glow effects -- Highlight passes with CRT shine -- Smooth animations between game states -- Particle effects for game events -``` - -### Best Practices Checklist - -#### Code Quality -- [ ] TypeScript strict mode enabled -- [ ] Comprehensive input validation (client + server) -- [ ] Error handling for all edge cases -- [ ] Consistent code formatting and naming - -#### Game Design -- [ ] Balanced game mechanics and timing -- [ ] Clear win/lose conditions -- [ ] Smooth player experience and feedback -- [ ] Mobile-responsive design - -#### Betting System -- [ ] Secure bet amount validation -- [ ] Room stake matching enforcement -- [ ] Clear UI for bet selection and display -- [ ] Proper currency formatting - -#### Visual Polish -- [ ] Brand-consistent color schemes -- [ ] Smooth animations and transitions -- [ ] Professional typography and spacing -- [ ] Accessibility considerations - -#### Testing -- [ ] Multiplayer synchronization testing -- [ ] Bet amount validation scenarios -- [ ] Edge case handling (disconnections, etc.) -- [ ] Performance testing with multiple rooms - -This framework enables rapid development of professional multiplayer games with betting systems from any tic-tac-toe foundation. \ No newline at end of file diff --git a/examples/viper-duel/Readme.md b/examples/viper-duel/Readme.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/viper-duel/client/.gitignore b/examples/viper-duel/client/.gitignore deleted file mode 100644 index a547bf36d..000000000 --- a/examples/viper-duel/client/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/viper-duel/client/README.md b/examples/viper-duel/client/README.md deleted file mode 100644 index da9844432..000000000 --- a/examples/viper-duel/client/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default tseslint.config({ - extends: [ - // Remove ...tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - ], - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config({ - plugins: { - // Add the react-x and react-dom plugins - 'react-x': reactX, - 'react-dom': reactDom, - }, - rules: { - // other rules... - // Enable its recommended typescript rules - ...reactX.configs['recommended-typescript'].rules, - ...reactDom.configs.recommended.rules, - }, -}) -``` diff --git a/examples/viper-duel/client/eslint.config.js b/examples/viper-duel/client/eslint.config.js deleted file mode 100644 index dd5edae27..000000000 --- a/examples/viper-duel/client/eslint.config.js +++ /dev/null @@ -1,29 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - '@typescript-eslint/ban-ts-comment': 'off', - }, - }, -) diff --git a/examples/viper-duel/client/index.html b/examples/viper-duel/client/index.html deleted file mode 100644 index 88e81751a..000000000 --- a/examples/viper-duel/client/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - Viper Duel | Out-slither your rival - - - - - - -
- - - diff --git a/examples/viper-duel/client/nixpacks.toml b/examples/viper-duel/client/nixpacks.toml deleted file mode 100644 index df4da7df1..000000000 --- a/examples/viper-duel/client/nixpacks.toml +++ /dev/null @@ -1,2 +0,0 @@ -[phases.install] - cmds = ['npm install'] \ No newline at end of file diff --git a/examples/viper-duel/client/package-lock.json b/examples/viper-duel/client/package-lock.json deleted file mode 100644 index 23eb82ad9..000000000 --- a/examples/viper-duel/client/package-lock.json +++ /dev/null @@ -1,10526 +0,0 @@ -{ - "name": "viper-duel", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "viper-duel", - "version": "0.0.0", - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "@privy-io/cross-app-connect": "0.2.1", - "@radix-ui/react-dialog": "1.1.13", - "@radix-ui/react-label": "2.1.6", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-tabs": "1.1.11", - "@rainbow-me/rainbowkit": "2.2.6", - "@tailwindcss/vite": "4.1.6", - "@tanstack/react-query": "5.80.6", - "@types/uuid": "10.0.0", - "ethers": "6.7.1", - "lucide-react": "0.510.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "uuid": "11.1.0", - "viem": "^2.31.0", - "wagmi": "2.15.6" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react-swc": "^3.9.0", - "autoprefixer": "10.4.21", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "postcss": "8.5.3", - "tailwind-merge": "3.3.0", - "tailwindcss": "3.4.17", - "tailwindcss-animate": "1.0.7", - "typescript": "~5.8.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@coinbase/wallet-sdk": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.3.tgz", - "integrity": "sha512-h8gMLQNvP5TIJVXFOyQZaxbi1Mg5alFR4Z2/PEIngdyXZEoQGcVhzyQGuDa3t9zpllxvqfAaKfzDhsfCo+nhSQ==", - "license": "Apache-2.0", - "dependencies": { - "@noble/hashes": "^1.4.0", - "clsx": "^1.2.1", - "eventemitter3": "^5.0.1", - "preact": "^10.24.2" - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@ecies/ciphers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.3.tgz", - "integrity": "sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA==", - "license": "MIT", - "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" - }, - "peerDependencies": { - "@noble/ciphers": "^1.0.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@erc7824/nitrolite": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@erc7824/nitrolite/-/nitrolite-0.2.20.tgz", - "integrity": "sha512-iYzgHh6Acd3Pq1tjQ+yzEiZQh68xHZ4P+r5UMrweqBMU93Fr39pHEYe/PkdAGppREOHoXvuIDDp6S7ORTxfkug==", - "license": "MIT", - "dependencies": { - "abitype": "^0.10.3", - "viem": "^2.5.0", - "zod": "^3.25.67" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@erc7824/nitrolite/node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.14.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@ethereumjs/common": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", - "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", - "license": "MIT", - "dependencies": { - "@ethereumjs/util": "^8.1.0", - "crc-32": "^1.2.0" - } - }, - "node_modules/@ethereumjs/rlp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", - "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/tx": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", - "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/common": "^3.2.0", - "@ethereumjs/rlp": "^4.0.1", - "@ethereumjs/util": "^8.1.0", - "ethereum-cryptography": "^2.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/util": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", - "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "ethereum-cryptography": "^2.0.0", - "micro-ftch": "^0.3.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", - "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz", - "integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.2.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz", - "integrity": "sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==", - "dependencies": { - "@metamask/json-rpc-engine": "^7.0.0", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz", - "integrity": "sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==", - "license": "ISC", - "dependencies": { - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine/node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", - "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.1.2", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "semver": "^7.3.8", - "superstruct": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/json-rpc-engine": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz", - "integrity": "sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==", - "license": "ISC", - "dependencies": { - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-middleware-stream": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.2.tgz", - "integrity": "sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==", - "license": "ISC", - "dependencies": { - "@metamask/json-rpc-engine": "^8.0.2", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0", - "readable-stream": "^3.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/object-multiplex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-2.1.0.tgz", - "integrity": "sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==", - "license": "ISC", - "dependencies": { - "once": "^1.4.0", - "readable-stream": "^3.6.2" - }, - "engines": { - "node": "^16.20 || ^18.16 || >=20" - } - }, - "node_modules/@metamask/onboarding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@metamask/onboarding/-/onboarding-1.0.1.tgz", - "integrity": "sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==", - "license": "MIT", - "dependencies": { - "bowser": "^2.9.0" - } - }, - "node_modules/@metamask/providers": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@metamask/providers/-/providers-16.1.0.tgz", - "integrity": "sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==", - "license": "MIT", - "dependencies": { - "@metamask/json-rpc-engine": "^8.0.1", - "@metamask/json-rpc-middleware-stream": "^7.0.1", - "@metamask/object-multiplex": "^2.0.0", - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.1.1", - "@metamask/utils": "^8.3.0", - "detect-browser": "^5.2.0", - "extension-port-stream": "^3.0.0", - "fast-deep-equal": "^3.1.3", - "is-stream": "^2.0.0", - "readable-stream": "^3.6.2", - "webextension-polyfill": "^0.10.0" - }, - "engines": { - "node": "^18.18 || >=20" - } - }, - "node_modules/@metamask/rpc-errors": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", - "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", - "license": "MIT", - "dependencies": { - "@metamask/utils": "^9.0.0", - "fast-safe-stringify": "^2.0.6" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.1.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/rpc-errors/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/rpc-errors/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/safe-event-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.2.tgz", - "integrity": "sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA==", - "license": "ISC", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@metamask/sdk": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.32.0.tgz", - "integrity": "sha512-WmGAlP1oBuD9hk4CsdlG1WJFuPtYJY+dnTHJMeCyohTWD2GgkcLMUUuvu9lO1/NVzuOoSi1OrnjbuY1O/1NZ1g==", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@metamask/onboarding": "^1.0.1", - "@metamask/providers": "16.1.0", - "@metamask/sdk-communication-layer": "0.32.0", - "@metamask/sdk-install-modal-web": "0.32.0", - "@paulmillr/qr": "^0.2.1", - "bowser": "^2.9.0", - "cross-fetch": "^4.0.0", - "debug": "^4.3.4", - "eciesjs": "^0.4.11", - "eth-rpc-errors": "^4.0.3", - "eventemitter2": "^6.4.9", - "obj-multiplex": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^3.6.2", - "socket.io-client": "^4.5.1", - "tslib": "^2.6.0", - "util": "^0.12.4", - "uuid": "^8.3.2" - } - }, - "node_modules/@metamask/sdk-communication-layer": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.32.0.tgz", - "integrity": "sha512-dmj/KFjMi1fsdZGIOtbhxdg3amxhKL/A5BqSU4uh/SyDKPub/OT+x5pX8bGjpTL1WPWY/Q0OIlvFyX3VWnT06Q==", - "dependencies": { - "bufferutil": "^4.0.8", - "date-fns": "^2.29.3", - "debug": "^4.3.4", - "utf-8-validate": "^5.0.2", - "uuid": "^8.3.2" - }, - "peerDependencies": { - "cross-fetch": "^4.0.0", - "eciesjs": "*", - "eventemitter2": "^6.4.9", - "readable-stream": "^3.6.2", - "socket.io-client": "^4.5.1" - } - }, - "node_modules/@metamask/sdk-communication-layer/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/sdk-install-modal-web": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.32.0.tgz", - "integrity": "sha512-TFoktj0JgfWnQaL3yFkApqNwcaqJ+dw4xcnrJueMP3aXkSNev2Ido+WVNOg4IIMxnmOrfAC9t0UJ0u/dC9MjOQ==", - "dependencies": { - "@paulmillr/qr": "^0.2.1" - } - }, - "node_modules/@metamask/sdk/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/superstruct": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.2.1.tgz", - "integrity": "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/utils/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/utils/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@paulmillr/qr": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@paulmillr/qr/-/qr-0.2.1.tgz", - "integrity": "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==", - "deprecated": "The package is now available as \"qr\": npm install qr", - "license": "(MIT OR Apache-2.0)", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@privy-io/cross-app-connect": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@privy-io/cross-app-connect/-/cross-app-connect-0.2.1.tgz", - "integrity": "sha512-/2stSJrvu33ieluj1JrW5WgQZxbwvrk+vJwn7pHY8jbuSBOx+78TkiyVWErABYSOfZW5dGi2LPkJUkOaBc4HkA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^1.5.0", - "@noble/hashes": "1.3.2", - "@scure/base": "~1.1.2", - "fflate": "0.8.2" - }, - "peerDependencies": { - "@rainbow-me/rainbowkit": "^2.2.3", - "@wagmi/core": "^2.16.4", - "viem": "^2.22.23" - }, - "peerDependenciesMeta": { - "@rainbow-me/rainbowkit": { - "optional": true - }, - "@wagmi/core": { - "optional": true - } - } - }, - "node_modules/@privy-io/cross-app-connect/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@privy-io/cross-app-connect/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", - "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz", - "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.9", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.6", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.8", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", - "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", - "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.6.tgz", - "integrity": "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", - "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", - "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz", - "integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.6", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.11.tgz", - "integrity": "sha512-4FiKSVoXqPP/KfzlB7lwwqoFV6EPwkrrqGp9cUYXjwDYHhvpnqq79P+EPHKcdoTE7Rl8w/+6s9rTlsfXHES9GA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-roving-focus": "1.1.9", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@rainbow-me/rainbowkit": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.6.tgz", - "integrity": "sha512-mJhKC+5BCyC+/O2rL3q8iJDrhb58p7o4kp26yWcmY/Aeur3M5OooETcHFe/LhWR2EGbBKRq2pzM75BIlrf4/PA==", - "license": "MIT", - "dependencies": { - "@vanilla-extract/css": "1.15.5", - "@vanilla-extract/dynamic": "2.1.2", - "@vanilla-extract/sprinkles": "1.6.3", - "clsx": "2.1.1", - "qrcode": "1.5.4", - "react-remove-scroll": "2.6.2", - "ua-parser-js": "^1.0.37" - }, - "engines": { - "node": ">=12.4" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.0.0", - "react": ">=18", - "react-dom": ">=18", - "viem": "2.x", - "wagmi": "^2.9.0" - } - }, - "node_modules/@rainbow-me/rainbowkit/node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@reown/appkit": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.8.tgz", - "integrity": "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-pay": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@reown/appkit-scaffold-ui": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/types": "2.21.0", - "@walletconnect/universal-provider": "2.21.0", - "bs58": "6.0.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-common": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.7.8.tgz", - "integrity": "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==", - "license": "Apache-2.0", - "dependencies": { - "big.js": "6.2.2", - "dayjs": "1.11.13", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-controllers": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.7.8.tgz", - "integrity": "sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/universal-provider": "2.21.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-pay": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz", - "integrity": "sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "lit": "3.3.0", - "valtio": "1.13.2" - } - }, - "node_modules/@reown/appkit-polyfills": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.7.8.tgz", - "integrity": "sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==", - "license": "Apache-2.0", - "dependencies": { - "buffer": "6.0.3" - } - }, - "node_modules/@reown/appkit-scaffold-ui": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.7.8.tgz", - "integrity": "sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "lit": "3.3.0" - } - }, - "node_modules/@reown/appkit-ui": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.7.8.tgz", - "integrity": "sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "lit": "3.3.0", - "qrcode": "1.5.3" - } - }, - "node_modules/@reown/appkit-ui/node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@reown/appkit-utils": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz", - "integrity": "sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/universal-provider": "2.21.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - }, - "peerDependencies": { - "valtio": "1.13.2" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit-utils/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-wallet": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz", - "integrity": "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@walletconnect/logger": "2.1.2", - "zod": "3.22.4" - } - }, - "node_modules/@reown/appkit/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@reown/appkit/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.9", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", - "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.2.tgz", - "integrity": "sha512-YvIQXGGDzbOpkLuFcjGs+aiAi38D8FCyJanIdlcV2m9DWMJpHTSY8L9piO93VHBLRoe8O9C/FiycjnJ8+aP1tg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.2.tgz", - "integrity": "sha512-8AVWhLnN9FteevGP+9pj/Y79vqE9TdziZTe5XkN5Z9+9QY7TEBbr4iz2te8/vXbLSLEdmaQx+o2GWXrLXDKGPg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.2.tgz", - "integrity": "sha512-w/idsvFiipvVGDcXKu1pdiIEnh9V63NPjpo5E48BbbrhUzEQthVXylKjYKj9lgX+89Ao36BxArkKPxxs7aXn3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.2.tgz", - "integrity": "sha512-oNUD4Kywoild1lsuUu1jydg3+IdQbyg4JNZb1+3Voh/revkYxMqMl4a/sZw5z+BSgFbXK6rR0RGutnHO8QS53g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.2.tgz", - "integrity": "sha512-yKXgW9UF2f78/PaSAFgDJbrLB/aETxH98jJFIpqESLyhyRmEqZl1dqAoy0IigJJZdwP+ZJdLw2isKiwb1wAqcA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.2.tgz", - "integrity": "sha512-ye4rQ80/fAIcUyIil7S/IzGpHvlPUoZvUQSq0OmU+LXb/I9E5Brh7PKJ/K0plsLCQx9Hx01526Uf4Iy8RqmLJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.2.tgz", - "integrity": "sha512-q2aqiDKNKNj9jG/6JIHakx1hmj6A8bxc3U6rpZjL8znx3O4OGpPJcyEiJSxmFd8olpjfRezh7PONU+3RTQmj0A==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.2.tgz", - "integrity": "sha512-qDtvGRNcj2SCY+XFV24s6/vOV46fktFa3/AtedKLVJZdO/WGyOtDDq2zF7nNGhzJa0Tx7bvJ92z9PjjF7nKbBg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.2.tgz", - "integrity": "sha512-Cg1ywrBHMMCj4Il0QdcVamt5UK9HQ1ntNUXzhAw4yZ2rTJGFkjnkYyEPEpunb1L9orm+g0kvtZUydShTtLUNkA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.2.tgz", - "integrity": "sha512-FxPrm21SE2/p+L/Qy2P6zPPWtbKRunZ71ChHXuZu6b//zOeDYCF7kbXiGdE7ZJgUvboLx8lQtov4jsz04zdmiw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.2.tgz", - "integrity": "sha512-Bp2BYb+QWgefn2e8DblGUzejLEyhtG0DnIcSMzAzFtBK33ITvzLM9B8maTHPoy7Cx7XdxEb7l0iNKQAXBZ1NOw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.2.tgz", - "integrity": "sha512-1DmJwH0PPRPjVwUGHINP1YiH7fO25mhpf2B5N8ubBQdOjEVTRI0x69m8eJGoUSLISoW0kgPX2zTAiHc4zllQ/g==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.2.tgz", - "integrity": "sha512-/9TBJIyoervs137gteTbzOSsIc/Io6xnXvjXYTEEqDRi3a9OokdBwfllN8AmPFGcU+WzoAiTc6n9SJiPohJ3CQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.2.tgz", - "integrity": "sha512-qqwpXqu6mV6Vm+EsU2NQk/xLiQfCv+NYmYNCYF8FFIf7kzlHvDoqiNaqqOw0hAr8O5nRBQO+H6X4qEw4Y/0b8w==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.2.tgz", - "integrity": "sha512-j5oHqcj58pDqN3+CeJdTB3NfHipxIxVIH24VkRlVVhs9jJXks1ovtO4Tf6YxlBzf44ZTx9D2uhO9DZttC/wgUA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.2.tgz", - "integrity": "sha512-F3fxs7ajUNny4z1TsprdWB9gg8QRwSNSSILVfTmG8rXdeUFWuHEf3Uf5ltrdii8CkzZS3kD/VFPdNVdH3BOpIA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.2.tgz", - "integrity": "sha512-2S74RTJ1lJeiX+HSCxlLD/6Z6ndu9/+Huf8kYrI2XTCztFosOWsjPt+aD4cxBG1JTWjoiRYtutc8HABruppFQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.2.tgz", - "integrity": "sha512-LOXSg8GprvL36erslsrNEUirlxy28JcuyTH5PYSBj8wwa0gDQlR8sZricFRbZGCzLhFixvmW2ozj7Mi+j023sg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.2.tgz", - "integrity": "sha512-PFhMLaWu0lFziS+buQIJ+YGaifkABS2TGtmeuPRrPO8JKcG17sDSNj358otAP45gIzRWd4o9QM8R+DurDWJgTA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.2.tgz", - "integrity": "sha512-AQ5GvrrLYw6dfkfT/bgYg1NU54exCKDrO9JKOE87zy2mdf2CyI6Vayy41r8Vo+kNDpqacaVQppwvYl30YGJu/Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@safe-global/safe-apps-provider": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz", - "integrity": "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==", - "license": "MIT", - "dependencies": { - "@safe-global/safe-apps-sdk": "^9.1.0", - "events": "^3.3.0" - } - }, - "node_modules/@safe-global/safe-apps-sdk": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz", - "integrity": "sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==", - "license": "MIT", - "dependencies": { - "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", - "viem": "^2.1.1" - } - }, - "node_modules/@safe-global/safe-gateway-typescript-sdk": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.23.1.tgz", - "integrity": "sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@swc/core": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.31.tgz", - "integrity": "sha512-mAby9aUnKRjMEA7v8cVZS9Ah4duoRBnX7X6r5qrhTxErx+68MoY1TPrVwj/66/SWN3Bl+jijqAqoB8Qx0QE34A==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.21" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.11.31", - "@swc/core-darwin-x64": "1.11.31", - "@swc/core-linux-arm-gnueabihf": "1.11.31", - "@swc/core-linux-arm64-gnu": "1.11.31", - "@swc/core-linux-arm64-musl": "1.11.31", - "@swc/core-linux-x64-gnu": "1.11.31", - "@swc/core-linux-x64-musl": "1.11.31", - "@swc/core-win32-arm64-msvc": "1.11.31", - "@swc/core-win32-ia32-msvc": "1.11.31", - "@swc/core-win32-x64-msvc": "1.11.31" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.31.tgz", - "integrity": "sha512-NTEaYOts0OGSbJZc0O74xsji+64JrF1stmBii6D5EevWEtrY4wlZhm8SiP/qPrOB+HqtAihxWIukWkP2aSdGSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.31.tgz", - "integrity": "sha512-THSGaSwT96JwXDwuXQ6yFBbn+xDMdyw7OmBpnweAWsh5DhZmQkALEm1DgdQO3+rrE99MkmzwAfclc0UmYro/OA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.31.tgz", - "integrity": "sha512-laKtQFnW7KHgE57Hx32os2SNAogcuIDxYE+3DYIOmDMqD7/1DCfJe6Rln2N9WcOw6HuDbDpyQavIwZNfSAa8vQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.31.tgz", - "integrity": "sha512-T+vGw9aPE1YVyRxRr1n7NAdkbgzBzrXCCJ95xAZc/0+WUwmL77Z+js0J5v1KKTRxw4FvrslNCOXzMWrSLdwPSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.31.tgz", - "integrity": "sha512-Mztp5NZkyd5MrOAG+kl+QSn0lL4Uawd4CK4J7wm97Hs44N9DHGIG5nOz7Qve1KZo407Y25lTxi/PqzPKHo61zQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.31.tgz", - "integrity": "sha512-DDVE0LZcXOWwOqFU1Xi7gdtiUg3FHA0vbGb3trjWCuI1ZtDZHEQYL4M3/2FjqKZtIwASrDvO96w91okZbXhvMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.31.tgz", - "integrity": "sha512-mJA1MzPPRIfaBUHZi0xJQ4vwL09MNWDeFtxXb0r4Yzpf0v5Lue9ymumcBPmw/h6TKWms+Non4+TDquAsweuKSw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.31.tgz", - "integrity": "sha512-RdtakUkNVAb/FFIMw3LnfNdlH1/ep6KgiPDRlmyUfd0WdIQ3OACmeBegEFNFTzi7gEuzy2Yxg4LWf4IUVk8/bg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.31.tgz", - "integrity": "sha512-hErXdCGsg7swWdG1fossuL8542I59xV+all751mYlBoZ8kOghLSKObGQTkBbuNvc0sUKWfWg1X0iBuIhAYar+w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.31", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.31.tgz", - "integrity": "sha512-5t7SGjUBMMhF9b5j17ml/f/498kiBJNf4vZFNM421UGUEETdtjPN9jZIuQrowBkoFGJTCVL/ECM4YRtTH30u/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.22.tgz", - "integrity": "sha512-D13mY/ZA4PPEFSy6acki9eBT/3WgjMoRqNcdpIvjaYLQ44Xk5BdaL7UkDxAh6Z9UOe7tCCp67BVmZCojYp9owg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", - "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.29.2", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.6" - } - }, - "node_modules/@tailwindcss/node/node_modules/tailwindcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", - "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", - "license": "MIT" - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", - "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-arm64": "4.1.6", - "@tailwindcss/oxide-darwin-x64": "4.1.6", - "@tailwindcss/oxide-freebsd-x64": "4.1.6", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", - "@tailwindcss/oxide-linux-x64-musl": "4.1.6", - "@tailwindcss/oxide-wasm32-wasi": "4.1.6", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", - "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", - "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", - "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", - "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", - "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", - "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", - "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", - "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", - "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", - "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", - "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", - "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz", - "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.6", - "@tailwindcss/oxide": "4.1.6", - "tailwindcss": "4.1.6" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6" - } - }, - "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", - "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", - "license": "MIT" - }, - "node_modules/@tanstack/query-core": { - "version": "5.80.6", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.6.tgz", - "integrity": "sha512-nl7YxT/TAU+VTf+e2zTkObGTyY8YZBMnbgeA1ee66lIVqzKlYursAII6z5t0e6rXgwUMJSV4dshBTNacNpZHbQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.80.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.6.tgz", - "integrity": "sha512-izX+5CnkpON3NQGcEm3/d7LfFQNo9ZpFtX2QsINgCYK9LT2VCIdi8D3bMaMSNhrAJCznRoAkFic76uvLroALBw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.80.6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", - "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", - "integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.33.1", - "@typescript-eslint/type-utils": "8.33.1", - "@typescript-eslint/utils": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.33.1", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", - "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.33.1", - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/typescript-estree": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", - "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.33.1", - "@typescript-eslint/types": "^8.33.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", - "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", - "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz", - "integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.33.1", - "@typescript-eslint/utils": "8.33.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", - "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", - "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.33.1", - "@typescript-eslint/tsconfig-utils": "8.33.1", - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", - "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.33.1", - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/typescript-estree": "8.33.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", - "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.33.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vanilla-extract/css": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.5.tgz", - "integrity": "sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@vanilla-extract/private": "^1.0.6", - "css-what": "^6.1.0", - "cssesc": "^3.0.0", - "csstype": "^3.0.7", - "dedent": "^1.5.3", - "deep-object-diff": "^1.1.9", - "deepmerge": "^4.2.2", - "lru-cache": "^10.4.3", - "media-query-parser": "^2.0.2", - "modern-ahocorasick": "^1.0.0", - "picocolors": "^1.0.0" - } - }, - "node_modules/@vanilla-extract/dynamic": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.2.tgz", - "integrity": "sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A==", - "license": "MIT", - "dependencies": { - "@vanilla-extract/private": "^1.0.6" - } - }, - "node_modules/@vanilla-extract/private": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.8.tgz", - "integrity": "sha512-oRAbUlq1SyTWCo7dQnTVm+xgJMqNl8K1dEempQHXzQvUuyEfBabMt0wNGf+VCHzvKbx/Bzr9p/2wy8WA9+2z2g==", - "license": "MIT" - }, - "node_modules/@vanilla-extract/sprinkles": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.3.tgz", - "integrity": "sha512-oCHlQeYOBIJIA2yWy2GnY5wE2A7hGHDyJplJo4lb+KEIBcJWRnDJDg8ywDwQS5VfWJrBBO3drzYZPFpWQjAMiQ==", - "license": "MIT", - "peerDependencies": { - "@vanilla-extract/css": "^1.0.0" - } - }, - "node_modules/@vitejs/plugin-react-swc": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.1.tgz", - "integrity": "sha512-FmQvN3yZGyD9XW6IyxE86Kaa/DnxSsrDQX1xCR1qojNpBLaUop+nLYFvhCkJsq8zOupNjCRA9jyhPGOJsSkutA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.9", - "@swc/core": "^1.11.22" - }, - "peerDependencies": { - "vite": "^4 || ^5 || ^6" - } - }, - "node_modules/@wagmi/connectors": { - "version": "5.8.5", - "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-5.8.5.tgz", - "integrity": "sha512-CHh4uYP6MziCMlSVXmuAv7wMoYWdxXliuzwCRAxHNNkgXE7z37ez5XzJu0Sm39NUau3Fl8WSjwKo4a4w9BOYNA==", - "license": "MIT", - "dependencies": { - "@coinbase/wallet-sdk": "4.3.3", - "@metamask/sdk": "0.32.0", - "@safe-global/safe-apps-provider": "0.18.6", - "@safe-global/safe-apps-sdk": "9.1.0", - "@walletconnect/ethereum-provider": "2.21.1", - "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@wagmi/core": "2.17.3", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wagmi/core": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.17.3.tgz", - "integrity": "sha512-fgZR9fAiCFtGaosTspkTx5lidccq9Z5xRWOk1HG0VfB6euQGw2//Db7upiP4uQ7DPst2YS9yQN2A1m9+iJLYCw==", - "license": "MIT", - "dependencies": { - "eventemitter3": "5.0.1", - "mipd": "0.0.7", - "zustand": "5.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/query-core": ">=5.0.0", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "@tanstack/query-core": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/core": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.1.tgz", - "integrity": "sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@walletconnect/environment": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", - "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/environment/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/ethereum-provider": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/ethereum-provider/-/ethereum-provider-2.21.1.tgz", - "integrity": "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit": "1.7.8", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/sign-client": "2.21.1", - "@walletconnect/types": "2.21.1", - "@walletconnect/universal-provider": "2.21.1", - "@walletconnect/utils": "2.21.1", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", - "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", - "license": "MIT", - "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/events/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/heartbeat": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", - "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", - "license": "MIT", - "dependencies": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-http-connection": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", - "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.1", - "cross-fetch": "^3.1.4", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-http-connection/node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/@walletconnect/jsonrpc-provider": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", - "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.8", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-types": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", - "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0", - "keyvaluestorage-interface": "^1.0.0" - } - }, - "node_modules/@walletconnect/jsonrpc-utils": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", - "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", - "license": "MIT", - "dependencies": { - "@walletconnect/environment": "^1.0.1", - "@walletconnect/jsonrpc-types": "^1.0.3", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-ws-connection": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", - "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0", - "ws": "^7.5.1" - } - }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@walletconnect/logger": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.2.tgz", - "integrity": "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "7.11.0" - } - }, - "node_modules/@walletconnect/relay-api": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", - "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-types": "^1.0.2" - } - }, - "node_modules/@walletconnect/relay-auth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", - "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.0", - "@noble/hashes": "1.7.0", - "@walletconnect/safe-json": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "uint8arrays": "^3.0.0" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", - "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/safe-json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", - "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/safe-json/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/sign-client": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.1.tgz", - "integrity": "sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.1", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", - "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/time/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/types": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.1.tgz", - "integrity": "sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/universal-provider": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.1.tgz", - "integrity": "sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.1", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/utils": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.1.tgz", - "integrity": "sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@walletconnect/utils/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/@walletconnect/utils/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@walletconnect/utils/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/window-getters": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", - "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-getters/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/window-metadata": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", - "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", - "license": "MIT", - "dependencies": { - "@walletconnect/window-getters": "^1.0.1", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-metadata/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/abitype": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.3.tgz", - "integrity": "sha512-tRN+7XIa7J9xugdbRzFv/95ka5ivR/sRe01eiWvM0HWWjHuigSZEACgKa0sj4wGuekTDtghCx+5Izk/cOi78pQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/async-mutex": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz", - "integrity": "sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", - "license": "MIT" - }, - "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001721", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", - "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/cbw-sdk": { - "name": "@coinbase/wallet-sdk", - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-3.9.3.tgz", - "integrity": "sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==", - "license": "Apache-2.0", - "dependencies": { - "bn.js": "^5.2.1", - "buffer": "^6.0.3", - "clsx": "^1.2.1", - "eth-block-tracker": "^7.1.0", - "eth-json-rpc-filters": "^6.0.0", - "eventemitter3": "^5.0.1", - "keccak": "^3.0.3", - "preact": "^10.16.0", - "sha.js": "^2.4.11" - } - }, - "node_modules/cbw-sdk/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie-es": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/cross-fetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", - "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-object-diff": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "license": "MIT" - }, - "node_modules/derive-valtio": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz", - "integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==", - "license": "MIT", - "peerDependencies": { - "valtio": "*" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "license": "MIT" - }, - "node_modules/detect-browser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", - "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eciesjs": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.15.tgz", - "integrity": "sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==", - "license": "MIT", - "dependencies": { - "@ecies/ciphers": "^0.2.3", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.1", - "@noble/hashes": "^1.8.0" - }, - "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" - } - }, - "node_modules/eciesjs/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.165", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", - "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-toolkit": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.33.0.tgz", - "integrity": "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", - "@eslint/core": "^0.14.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", - "@eslint/plugin-kit": "^0.3.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eth-block-tracker": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-7.1.0.tgz", - "integrity": "sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==", - "license": "MIT", - "dependencies": { - "@metamask/eth-json-rpc-provider": "^1.0.0", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^5.0.1", - "json-rpc-random-id": "^1.0.1", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-block-tracker/node_modules/@metamask/utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", - "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.1.2", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "semver": "^7.3.8", - "superstruct": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-block-tracker/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-json-rpc-filters": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-6.0.1.tgz", - "integrity": "sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==", - "license": "ISC", - "dependencies": { - "@metamask/safe-event-emitter": "^3.0.0", - "async-mutex": "^0.2.6", - "eth-query": "^2.1.2", - "json-rpc-engine": "^6.1.0", - "pify": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-json-rpc-filters/node_modules/pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eth-query": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz", - "integrity": "sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==", - "license": "ISC", - "dependencies": { - "json-rpc-random-id": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "node_modules/eth-rpc-errors": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz", - "integrity": "sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==", - "license": "MIT", - "dependencies": { - "fast-safe-stringify": "^2.0.6" - } - }, - "node_modules/ethereum-cryptography": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", - "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethers": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", - "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.9.2", - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.7.1", - "@types/node": "18.15.13", - "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, - "node_modules/eventemitter2": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", - "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/extension-port-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-3.0.0.tgz", - "integrity": "sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==", - "license": "ISC", - "dependencies": { - "readable-stream": "^3.6.2 || ^4.4.2", - "webextension-polyfill": ">=0.10.0 <1.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", - "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/h3": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.3.tgz", - "integrity": "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==", - "license": "MIT", - "dependencies": { - "cookie-es": "^1.2.2", - "crossws": "^0.3.4", - "defu": "^6.1.4", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.0", - "radix3": "^1.1.2", - "ufo": "^1.6.1", - "uncrypto": "^0.1.3" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/idb-keyval": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", - "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", - "license": "Apache-2.0" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", - "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isows": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", - "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-rpc-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz", - "integrity": "sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==", - "license": "ISC", - "dependencies": { - "@metamask/safe-event-emitter": "^2.0.0", - "eth-rpc-errors": "^4.0.2" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/json-rpc-engine/node_modules/@metamask/safe-event-emitter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", - "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==", - "license": "ISC" - }, - "node_modules/json-rpc-random-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", - "integrity": "sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==", - "license": "ISC" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keccak": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", - "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/keyvaluestorage-interface": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", - "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz", - "integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.2.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz", - "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/lucide-react": { - "version": "0.510.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.510.0.tgz", - "integrity": "sha512-p8SQRAMVh7NhsAIETokSqDrc5CHnDLbV29mMnzaXx+Vc/hnqQzwI2r0FMWCcoTXnbw2KEjy48xwpGdEL+ck06Q==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-query-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", - "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micro-ftch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", - "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mipd": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", - "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "license": "MIT", - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/modern-ahocorasick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", - "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", - "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-mock-http": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.0.tgz", - "integrity": "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/obj-multiplex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/obj-multiplex/-/obj-multiplex-1.0.0.tgz", - "integrity": "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==", - "license": "ISC", - "dependencies": { - "end-of-stream": "^1.4.0", - "once": "^1.4.0", - "readable-stream": "^2.3.3" - } - }, - "node_modules/obj-multiplex/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/obj-multiplex/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/obj-multiplex/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/ofetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", - "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", - "license": "MIT", - "dependencies": { - "destr": "^2.0.3", - "node-fetch-native": "^1.6.4", - "ufo": "^1.5.4" - } - }, - "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ox": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.7.1.tgz", - "integrity": "sha512-+k9fY9PRNuAMHRFIUbiK9Nt5seYHHzSQs9Bj+iMETcGtlpS7SmBzcGSVUQO3+nqGLEiNK4598pHNFlVRaZbRsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/@adraffy/ens-normalize": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", - "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", - "license": "MIT" - }, - "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", - "license": "MIT" - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/pony-cause": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", - "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", - "license": "0BSD", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/preact": { - "version": "10.26.8", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.8.tgz", - "integrity": "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "license": "MIT" - }, - "node_modules/proxy-compare": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", - "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", - "license": "MIT" - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.2.tgz", - "integrity": "sha512-6VwIRjxJPRGfwtHnBWrApOrFposYNARbBUnBzWSBYutdukosTadkxSB1prmEd95jhiJ1uWtZOD3sDx7qfVwu4A==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.2", - "@rollup/rollup-android-arm64": "4.41.2", - "@rollup/rollup-darwin-arm64": "4.41.2", - "@rollup/rollup-darwin-x64": "4.41.2", - "@rollup/rollup-freebsd-arm64": "4.41.2", - "@rollup/rollup-freebsd-x64": "4.41.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.2", - "@rollup/rollup-linux-arm-musleabihf": "4.41.2", - "@rollup/rollup-linux-arm64-gnu": "4.41.2", - "@rollup/rollup-linux-arm64-musl": "4.41.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.2", - "@rollup/rollup-linux-riscv64-gnu": "4.41.2", - "@rollup/rollup-linux-riscv64-musl": "4.41.2", - "@rollup/rollup-linux-s390x-gnu": "4.41.2", - "@rollup/rollup-linux-x64-gnu": "4.41.2", - "@rollup/rollup-linux-x64-musl": "4.41.2", - "@rollup/rollup-win32-arm64-msvc": "4.41.2", - "@rollup/rollup-win32-ia32-msvc": "4.41.2", - "@rollup/rollup-win32-x64-msvc": "4.41.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/superstruct": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", - "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", - "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animate": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", - "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/tailwindcss/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", - "license": "MIT", - "dependencies": { - "real-require": "^0.1.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz", - "integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.33.1", - "@typescript-eslint/parser": "8.33.1", - "@typescript-eslint/utils": "8.33.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" - }, - "node_modules/uint8arrays": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", - "integrity": "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.4.2" - } - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", - "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", - "license": "MIT" - }, - "node_modules/unstorage": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.16.0.tgz", - "integrity": "sha512-WQ37/H5A7LcRPWfYOrDa1Ys02xAbpPJq6q5GkO88FBXVSQzHd7+BjEwfRqyaSWCv9MbsJy058GWjjPjcJ16GGA==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^4.0.3", - "destr": "^2.0.5", - "h3": "^1.15.2", - "lru-cache": "^10.4.3", - "node-fetch-native": "^1.6.6", - "ofetch": "^1.4.1", - "ufo": "^1.6.1" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6.0.3 || ^7.0.0", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/kv": "^1.0.1", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, - "node_modules/unstorage/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/unstorage/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/valtio": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", - "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", - "license": "MIT", - "dependencies": { - "derive-valtio": "0.1.0", - "proxy-compare": "2.6.0", - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/valtio/node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/viem": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.31.0.tgz", - "integrity": "sha512-U7OMQ6yqK+bRbEIarf2vqxL7unSEQvNxvML/1zG7suAmKuJmipqdVTVJGKBCJiYsm/EremyO2FS4dHIPpGv+eA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.0.8", - "isows": "1.0.7", - "ox": "0.7.1", - "ws": "8.18.2" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/wagmi": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.15.6.tgz", - "integrity": "sha512-tR4tm+7eE0UloQe1oi4hUIjIDyjv5ImQlzq/QcvvfJYWF/EquTfGrmht6+nTYGCIeSzeEvbK90KgWyNqa+HD7Q==", - "license": "MIT", - "dependencies": { - "@wagmi/connectors": "5.8.5", - "@wagmi/core": "2.17.3", - "use-sync-external-store": "1.4.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.0.0", - "react": ">=18", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/webextension-polyfill": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", - "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", - "license": "MPL-2.0" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "devOptional": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zustand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", - "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - } - } -} diff --git a/examples/viper-duel/client/package.json b/examples/viper-duel/client/package.json deleted file mode 100644 index 4c697316f..000000000 --- a/examples/viper-duel/client/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "viper-duel", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "@privy-io/cross-app-connect": "0.2.1", - "@radix-ui/react-dialog": "1.1.13", - "@radix-ui/react-label": "2.1.6", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-tabs": "1.1.11", - "@rainbow-me/rainbowkit": "2.2.6", - "@tailwindcss/vite": "4.1.6", - "@tanstack/react-query": "5.80.6", - "@types/uuid": "10.0.0", - "ethers": "6.7.1", - "lucide-react": "0.510.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "uuid": "11.1.0", - "viem": "^2.31.0", - "wagmi": "2.15.6" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react-swc": "^3.9.0", - "autoprefixer": "10.4.21", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "postcss": "8.5.3", - "tailwind-merge": "3.3.0", - "tailwindcss": "3.4.17", - "tailwindcss-animate": "1.0.7", - "typescript": "~5.8.3", - "typescript-eslint": "^8.30.1", - "vite": "^6.3.5" - } -} diff --git a/examples/viper-duel/client/postcss.config.js b/examples/viper-duel/client/postcss.config.js deleted file mode 100644 index e99ebc2c0..000000000 --- a/examples/viper-duel/client/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/examples/viper-duel/client/public/android-chrome-192x192.png b/examples/viper-duel/client/public/android-chrome-192x192.png deleted file mode 100644 index 5bedb40d346f987f4774ed4333127893486ea478..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16670 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 x6!{N?`(F$`uLa^YjgS$g;3l^Nepn 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 bM8RRqK4rd{-FW~y#vNBJ?96Mgc*Ok+yM>f6 diff --git a/examples/viper-duel/client/public/apple-touch-icon.png b/examples/viper-duel/client/public/apple-touch-icon.png deleted file mode 100644 index c9c048b8009eb5a26b7e1d304038b3c34c5c7aca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15027 zcmeHORZ|?m(#739xGnA)AlTxr!8N!O+;wqxm&FMh+zAACcXxN!oBI#G*RSe6%=AN7 zP0e(ln(EWlCqh|K3KfYE2?7EFRYqD|^`C9{??Qn4w+oKLar`ro&Z<&Ci0TRAV+aUx z2pMrvbq|AcUql~$iTh7JPUlN+PwlK?h1CYD-$X89L0WoR#ruOAeyVhoBV@1=Zkkhh z;wsm)!jQ`_rYhh9W_1Y>mHd=q+jTlK`eZ0Kxm7N!%suAEiLqzzgRbOABZC>51&>Sk z%#IH4%Q2rr?@7VcY|-#If#771!-hkMLj=J5dBdrWx~NHo`Rv_noqf`*!^7Yq0(G0}%ytua zh&*R@8p~f^h{+#y{?zaSE5#W;dW}QUM*DoXR>5TF`oVz7XWt$y49(9X@auDZb3F}n z9bh+NUibN(uY9lH;ONADorC7izUT4M`T8J5K#_@&xm8n3oHjjz5;8{^gI&qqbcC{P z){<73c-+oFhqE5H61}cxc^^9w{ZfIh544eGUWorX^Wt@pB=pPh>kp(N$M%oh;T<^p z8EZzEC|io*APPt9|7oK;8%=<0vq;{<(kC9BeVHGEaP4JYum_M z8Up-CWi;!y-LiFN&}uGLz-b>?+M&CT&|0$y2!#-`St54mqRGcipfrYib+fNSMAWQU z5<7R&nC@XTwKe$xng~_`iCp3fR_vl0&4e82>gRc0ZLDx)*ln!J8^|k`zTEC@Q1{59 zOyFVjrjS?kq34>G>U9_xTIjD<@Awb|#F(z*zqotl%dZBjD<+Jftu1(Y;w=f``XhYr zn@ux1ARy-zf66E0j}JAOxnKk~E|{-yl%l*i9tUlNQqs6MP<5oOb>A1S9Va5jARGnG z>pIE~!-XTfb71Iw@Or+U`&c|e>)mk}p$3$(Y@DIK+~ki{cUiP$MmC`gqtbr_xD&s4 zkRlMv*wX1Z!daA#^b7!MUqnH8XR1p~aV&X(J{W3wT7~lZA;LUr>w&13FltA5H<{{fG$P3og^_SW zl-?kSH&zU`aEzr;(>TNm;{73iXc!x-9K_yTMs&r>GX%_-c1kX3 zO6$R40D2FjygJRMw+pV}3lCk{@`;7vla2jK)okyCbNZCSaqO=h90))peZv-jT5CHP zA*d27J-JLI{D%N1pO)TV)hq;_16vQ^IG4Y+L+?!u71z)36B3gp!TIOO1e?ta;!h5n zR$kFh&9yAV^Mp3!~DxccsShwnaqt(Yc`!gaRP&YW4e?w{E|xkK!y3`V~7tfbv^$KS;EFnq=eosNP%1I_ZvjY!3gWBH|ciilT}WV_7p#pt@N= zM4v`0EJWl=as&OOQb{Ew9dt%y2UQ((k#x`yj1`k&5sZ4zbc9Yzf1M4HqngoQ>H)?C>egOB4%S!Jh(brkKtL3fSu(#!z?gUHO12g`GfdI=F2>QS;^*`+a()khyiY0+ zp`j~Y05^u07pW2hvVu75&*IqIV>S zLWVrMoFwccrJx%q_S!z{^&M!~&;kCj0%DI6GaIPACSw0ij-4DalAsO=#cY95`&U>t z;K_g{s};d={P}SHCw7gMIbzk%>RKpeNLZ4dy%F?R zR%eW>&a3d~wr)OVLTRQKf^bZSKv@{_Om2h}m}HWGkpK)t5nY(R?h20hRy`fWl-?f@ zRwG;Crl@NJvlb{^gvhjWK2Ib-VSlR^N}`d=q+*b_Gl8|;Fb@gOZfYC87r&Gs%^mBC zpI6J>o>nOl+c6U+k|>Y?DR2|7055(5VH#~z0Dy{74b%7Cry7pYG}TXXC)>Hb`4`>M6!OY`Sx~`jlC;^QCeBMcdgr^aH4a#OdWU zABN598Ul&!eg2kOcQS28*2Jf*H~<4DD$9Wf+Mzd5NVqrU_-4rDMO`3na|T&QFlm7* zX|vK~67w<8G=Jb)CkZtamw;(Vfkk?4d|!{z0PW5gRU-xJG93r}rHNT{50Rj2 zj*jyV`&~gpsb{d$miLVy6>8u_YBN5o`&M&lqY^53B~BeVhbjjG0vZW6=;{pwI}{j# zLnyrqK_`PYXOx9|0dY zRT72(W?5)7Nn-V}2@y0u8%%b#%F74s(whwmI!eL?a&9^~CF15CQZIrGD~pQ6bCpq6 ztMosMtaz=aY`HL1zSvwYIDOC3r+v=Z@ql`N1#qZl$P<;34C z`!_KBs=iF7ZBy%WdgNEO4O5v+#pQo$<-M&KNyqNlLv@8dhVyUr0 z$Yv4fX@pyG3Q?O^Nu*5+zouwZu~a4>>eO$u5gANaOOZNr`4>vc^7K7XOG{IVU3%%I*d?c!_q8L&c{Rs&*;pR*a9d zCKA-Q&Pq*<&AA?Dtf$_#`1j#c--fhxyT1-l-)}>G4=c4~{QU?iAD)a*Njb{mQ4uiD z0v*Ush*B)pXs<#GM!bUfs*5_3ik3iuqitHJL;pq@L=LfyTo(wu5>z(A`~w|;kw{)P z^f(__J1IdL&bgGX4#E?#eE3pKd+&84r&Khs;W*w@>Rd1C{rGhgQ;Z0t28W;T4Vv2tKa78238 zha7u_A8|5j3Fl_h=W6*gFaaLsj}(nCg@;v;$$b8`ovTbw($++B{E1>g}|Up6I6Ag zDF?H9lEXW7*+(s)yW7Ll!mJc*JTaAs4!~4V2J8>o;WnjK>RHts@RlT}V6#nzPxr2@OY4oW23eKnEgJ-uwzg0W`^Z5KXP`Vt&G7-=%hnZMY7GQ4Sg&8R7Ki zDoPr`pkB+<%E5yqiXz1zFzpXs&qie9Qe227qle$H`%^%TX-(v}W9xbY-Htrst^YYX zK8eD>%-F8>dO{Vz&_9PcWUo1Kx4mD4keKGsf`C=8%mEb+$796*^|S0`WwX<;TVtH6 z+r@FP9IDefa8H>-nF`>+a)c?7kzL-O`;zlfr@sh&;ESfhdV{P_RWY_~ebFY(A$|%4 ztCfXZiua3$sr$Ft$d%05{ubOB9QKJ|fz#cmL1Ds?;@WtBo=l1YbB`2h$_C5I1?ZE75Lg4B(eEOj!$Sh zBZqFw=XGM#I9US|$^OlASb1FVukc5-ASw>$L)YD}QSpRDqvi7n7gu&To*BMgdg3T8 z4N761j;*{~x@8GTX}<}3S$n0pTp?R6^TjT1F0Sy&u{Rw-Oao@1XEr0@AN~tP+L~Lk zmfB+h?EW8Voe0mwD2LtAdS=rM=M#O^9l|l*gNBayb~yN38_R6js&rt@^)-%Gx6Mei zGcU?IqUb43uwdDptkB7p_e< z^pXE1-4DFYmC?B_$lS|~E7t$tGmX^85J$)*)kTvht?rax7&5wTX_w)5# z_nb4FjmxC&Yrk~n&T`M0t83g*yHv9zeI*StwKCCG+Cy->_dkFeZcelHJtk@gah^8We*0xS#CHsYpxev#=1<+Qit6Be+Fut>wI8(y z*$q%$KQVpAL4@90$jDnNS|I>I-l5SU#OMo@IIs`dUvR%S|Mafu0Df=ce$}0_ZfmG4 z5^1Mw1KS0+S?^fYb}Cci&pZnLoTxk1p+V2m`aK8JksQebDzaF>Kl;0YeeQlp+OBYv zgrBSDwAX`um~=_f^8&z5hu|+#5IXyznmP}6z|wsRLlDuY&1{>=Xt0(#S%wVCF~WE> zzVO;J!wacVgqF=9-XljS0>K2Jh=97Nzg)>q4&82hju6vG=`jFu2h0gEY*p+$dinNg zg;q((H!jygU2hp4NjU7qG~y>8{k7rmZqI0364Sra(bT^VYXmzNLleNN%C-0oSt4lJ zaU)2qWIM}Cj4QR~d>^g1P}497gY(I=Ah+v^+-^ISBID$5rhVxsy}UkiWloYh)9e}} zOe^aa&Ci{#q2{WdI(@4y9hX!cXMNqN=|IS zzVab7faWra`0_!Yym4MxQA=<3$LD!?ZS_%VfFe9+<_o|(S3mt52b!-y5Z2#8030hs zZ7vK~?=Mzg15)qz-1H^GqO@%D$T|Hr_lfxr*S0SjM`#VP9hK=E8T#`n?tX-bCZ>q` zdTQ`Yf!Ye!{JpcWL|?BK(FO0k3YzLJ+@)pRk3~uXtk*p_&4FzkwVpPcg*2pf|2k8WEV4aaJgA!0H_3ewzend@>e*WUF~$o(~i=CYzfjot;#F)*x0i4t%?XoK|Dw53w5ccu3_e{NmH+-nr zikJU671Du`=J(@IKyVAI(zj`!%9==jS|AKyDIj%;~)di59$U zSE}G;{(fDoUy}R5I52BM(q=qA3@C<{g{$;$?>x>tzRmH!e|eBgO}L@SvTZruxv2d6-|Iq>^6k&d&0-tn)5O1vB-xqA&gQ_fgRC`62JAsMXAEY#Mkz=hYXu6 z+WAY}-)q*S^~;u(87lrh#t{no>)uNKn<-&0}sISezRMB11q}8FDDAH;k#dafQ zcP3c|BwziP0RH%6M~Zp&V}h@8yHRROS?zpWe_ov7@jG|+*kXb8w9z>vw#PaW9am|U zQl&)<#$KMdEyZ0R2i&Bmr^-k=Pv7gESceQ{U%4+L`_q5VZFq8AmNKXwPl`>WYV31c zCP9(=l}=hFyem^Tb(#_tgdv4<3Kid&GwQM~y2F+HGrj7gJ;44VJCVDDNGDa7Aw zG`R6EoO-u8buu}khfrGaActKC&qO(4^#X_!2{r*wI@5OwLCDi(KOXa}35F|tW~XzR z*C~(BVg=(+(&weT39jn>6BBRv@$9dzJGCPJ0l^w$Cs7W%u-wu1E<>t7;nY%0(We-~ zI?3^)-yu(tzbhNy*gZh&T-}WE!b9lo6rDrcV7hlEBYWKfsN_+mkD%{0(GG;hHaLdg z-x@?EIP6X$bnw_J2c+FmLtmcU)gLYwwWfBm;8DMuOEFfW0!!^(urg6aPf2I-lI8Xcn=O-2QT`u`uzl*C%@t12$jT7`X5Sgm(;!zbR z&q{61FcBOWhv}&|d@VAElJvDiALkn5Q~RJ8g|Xy}=h)wQx;!|B_#LlBKGZ_Kvx3rvu!~g+wJW z>sHw;ULE(VW!Rghmtlv0|1NWfV0(>|1r;Op@8z66xzR8Dt?rqC^?M6-!qiTK??rTH zxwN+kbS{qXmH*oM(bua{`RzJ*JV{Ei%viXAip%t#{jbvcyU)?Zm<;&vLWQJlO7ipA zQ{2PKZNq|{!omE4JVI|`=6Te0*U&`-Naa+LJE!A}F?hdu+wx*WK9$7PU-7hs@!dxd zj7u6po@4X*ggcy+cNQD?(z#Vm5A1!{*iw!aVzD3XtzSknwX!7v=ZF%N6WZcP8aw>L z63XUwWJ{(GsVph0Y<)lu4%MqW^}rXJ_G(4N+Ek65x+}epiAXY^90;reC6W20-LJ;K2&qY6sI$VkR z1gPPulV84s`rlc!?IwK~)INPC6zO`NbA;{HY_^T}gZKdr*gj_k&i4E;M(@kg1Zncq zrPekH&M<&*qtPtpS3mcg7G8IfXOpOvF0)Ga)i5>?{ez@f0os|Gc76kW3>GO< zpmVVjND@sj7PkZEKynC##PiEDt=2-WMnXpM+Vjo%AFumHTOySlT5!-Zq~v ztg~zb_LNB(>?dYgx#hPDaV&=pyr!|0TpM&$FG}%Z zaRS+TZIZ>hfp4)G`>&c+0R zM}A`r)1^Y<Hb1eiAyUWS2MK2(IsPB7_y-48X4dY*(h-@$WuW2yPocV*tyW~9_ zKM;pS*R1$ySxx0(@V`xFM3^WtM^ByYm%WPgBkkg~uAnZY|6}iuGT(&tuGr%@g7L`v z{q`T&ybn&!nk+KLD&5lKPa!46OX8uKh{V zK9A**+9hcPRl`HqI8x%_Un;u_|@O@;N?Sk$7Y$?tS*T6YYOOBpmCP zx3am=kuD*S7&Ej$ycJYJlCKY9sxiO9qAB%~rp?ekzp4zVKY<5E9Jb>!%b zN`0C|o`#%EfhrD;i=~GQ(4dZlz%mt{EVn9&jo@^y19hhK zslS8jl|!^oHMBGA;Pzn)_m{&J(wB6W)Huu9#8bEea9$;k9j}{Lob zyX#(QG}rpbo{iX{dd%m1sTY}ac2fvI(I70mjQJe^RZP<0(ZKHgSV@3o7VHlz#pye- zIin5%L1_HnSpalWR6^|fSRrVEwt+CmeZg&24ZbH|$c2fFyWw8IUFNG(J)QDx>kA@& zCi@pb&#^Bd$N#FFFy8>N%>*>m_zb?62EWwN>3f*7k1vG=-7MGOm*11taWriJD9|ML z@dGl?L2tBVVbMbo&w|)ro1S8bkfD2#ZvYvrW9ZRQ2X!bK21@g{;Kg55asJ%~KbJbi z{NJ3I&v{#18lhw!ZEXXyB{j?nQfTG3Ce~2VGzk61sC~;SVM^zdTlz0Mld zq%N4s>TR<8-*#h1qwlNZvqMUcejjk#)tmeOH1b}w#O%5nbWsS&A7k0_l+r3NB~NWyC;CI$7TLHWOKE@WuEpH0K(djj~ktV~36b5x4 zP|!OyUNQcV6%?&Vj${^n7yUa|1mGzJqq%~~<)OOtV*2Ap)umyq2!Qku1J{pt`O2$I+x=G(ta_z&y3&g<9(%R#4m%1v5>ZWE(Bxc_tK9p*FS_llJ`(c1^2$G6 zpjAT!2cf=`f+C(Tu)RtI6+T?4^Jp?dus2#_fE(;^HCa_&s1>mMEzB^t5;yn7=gptx z&#Opb+Sfbuk0$LQO%w=o=zLn<(%*vQ%_?k1boCPnt(@sQczsAuH9fdoWlY<@Q0e+K z!QAd=jrS%CU7JS!pIfnVGTxi}B8|zlj=QN3+7>zAr5n9^&4kg@Ah)PB=?0Ew(fAqr zVVIA!eBHfLc|Lra5fOyftT?TD50RsASl$e7L2U`Rj9fMJ4Q^DH0w!d40-12oe>sH(wfQ?n}7?CBzE8#sb9&FpW*Lw{#8 zc98OMov$|CaedEz>#sh0?EdY)pEt9C7M*88Y;MrisssF;XLr{&NQpypqq!$Sq)&D{ zX(7}U1m(DP6}R(zyLtP}5`KMugm3-)ATgvp_l>iIQtIiA(M6`cOUw%yji&?#7xT>u z>Hf>p^Tiz#_S3DWFA-8hsOu5b_gLXirQ1GZlN&5TOTk(+61gMN+b?LF9)_L5mQ%7Z zC7>9>&B)TtA#Q!6MjT^BJji2^M4XMuGcnd-6-+kujd=PE9d`-cvvX`*JRAGB9KoYI`e?h<`=9r7LuBY@-^TzMt(NjuHsEdW!&^=uRfDslb>?l4GZO6`qw_3n zh_-*Oa{xmF&k-s3R5GRJUvW_D7F>K5P0tSYR}^&-Vz@{&>io)gF=4 zCKy6h#9&72W|*>Kq>GAc)QKZBc|?KXwu>4^@z58JdYcjeKb9Vctv9`qWJPMT${UtH z=@f#4V7$HJhLDZv- z+73NH{41qii6AzS)_7`{o~3=%s2^jan+TkgA(dfRVMT@ms@hnRaDbP z_(dQltyKV%pbn*V&NfqJosf-iiAs=+l_p&zN?O2-)IZ-XL;Us~?hxd7?ZUjJ()2|T zg%7#NHN_ww7f3D-5$ew z+KCUZ#VAwlFu#{nqc3%7!E%RCx%}^Iy{ zWZEB4@ERV7sR3F*{N6>DNj}TLda@b%_CHuCJ3%2FbGJnG$62m)YGo8wC?!loMIzOk zP?6GJsu*d!bFRf~fP}<6V+9c7IWA^ShpXHynGGE3i;blM;L~@MN6f)~;^&K*!TJz( zCgO$9)R~BArRDOmAu7~CEP~; zGL)c^`%Y0+;KF}TL<&EZ-~1uwF>u>YvH}o*uGG&fJm5LUdM=EV_5`-45I!44N~V9U z1i@)~R)g2>Q|bn2kwqfY7S6KgkZk}juO(&p;r9+8!}=<*ulsdr$G}L zD-Uz9qCov5)(bnjsZEC!<6L;LemwCRLT#7@epPH`sPLpT7%*1G3sUAbengxjPM=uo z92Euh+!?pX2D#Dx$wS8_pCOcg+7FJ7b4)4tsG_2GVPzDFH&SwtxFu;zjoTWcz9%m~ z{9}b-gPtkyo+@pWqJP)U;Shkto1UwADF5BhE&; zR30L0Npm-4JkQdzS%+q4JRq8eW3G(HRqlONCZvksfmr z)qYdvNy2aV>rd#=Dm~ClcDip`Vkm1IQDm3bUht^vdNvK8pVs-(wiP4Bo!dc!0U_7c zVKT?#pkH6^-~DzM>s7*&^b$mR`N&$!98*l5$6K+3vRmSg$Kq@hXOq$KrAIam*1rCH zIyxz1fzOBm!gHd($yPey@E_{u@`CNI$9cAWW0|QWTFM*s4dYQ`EVxovMUr)bRBM$b zsa5rM7mDSfz6h6d{AJjtzB96Hcx9f=K}5Z~VLT_O!<}s7Xg|@by7KAdw^)}u+$Z3i z3!`Z4=;02Sx$12YH9@l!(Om7!H@UE5#;5euZ0|*e%CogbQvCKf6B*RRu6t*q!S|Ic z6S-xDFU(N2Wuyl*VRkL_AXXnI4&PcHh9f-!YiOUXY&gjPZ&8)0FcZr81BlPH9+JcL zb5MLu&ALwzEN&+ufl_2LE+mx$8mE}z4|WS_bN?W!<2L z-dbC$3WLmInXN4R>m`JZ(uK-W(Bx%N?%(WZgRlQUcg{!;#(DF8s}57oYgM0>$~oGr z&aT_5T~?u}#;B8=x=dhI135#0IG>gpsE;4&f{qcg#x4FEfk#S9%*bzt_4LA$1EWJq zt<(t=d5Z`=%{YBz0`=l8XsW2}-*k}fPkg>NVq%Z4&h91Wxw<;tmeDP-yyar~rDtyX zY+$l>{`VkqzQ8;>j)#T|46kK{E)Y5?%OG4&k z4lo|Q&bcTGwcF!dE~eqlvEgpnP8%^DC&p_7!bW0{NwoMX925!lRVj@QWsR$)@7r6! z?I^mJ`~|JTR{WEMpM&z2<=@F^9}FortABAGsYN-YP)7c zTP84aPnGUr^y*6=SWDwA+}L~b98baDdc~X!ErwtW^s(B67aUy@SY!o*Q4ArCmSs_*y#z ziSs<^ZcCbS8aOkb-mDv3ejdK#$dbBUU6{566~rMBi#g5@y4;gt35BU3X>1Q3*y*fQ z-J=N?om8MDypBm2fr4wk6#)i7A0-Rfe9v9iy)D(*O8oW$X@HHDzd6Hk??PS@NbpSy zc+R%55Z+8s9KE*m?cg{4EOPbzG+$GDjf3E#*3VAl%6zTL1|j1IK#0syi13)66=rwu zx~VGXPl4kzQc=xm))l-SyZwQCOOMj=AMg@#^Xh;H6ssga3C}&gn+-iBAs_FyPqx06 z9I&n^>ks16VjBHcoJGv*s;tbnJNmfmCK1B|z9mBlSyo{zh&Sf&zh1%(eh?p)#6HZM zoJSPqe{-sK9qf}duBxk`qX;&=jJCx;JUN&f`57cSj{(tg-g9 zcX3h-BVEDHbip$;XB9voNU*%kskve!8!*?M!n~igm9XYtI}9 zyyg++Do?-jT%O!D>(&W_X`<0JZc5O~*}bOqQBGkubS|}K226E#fWg5T8;#i6UfcC} zUznNqN=@%c1YYF{!uS8EE7rDaqChLCB(-2ZgC20o4^f~R)YFt^ocUIuptWwWVh^vm zAB}`|C%8 zL$d9~h40JFo9Cg!io;RuNs3N8PM;*ocyt_3avxS3FOBisONgtMkWo2o8O`W~xliFw zs)L!*r%|VVuG0g)VJN@+5?tME z#_`O~r~h#`%x9IXyw0r1H@-6`Y*pLM-!T_wM;Q6eY{XnBl!a1PO4Ze)MUw&@H9EUFT8Frd z&>OP6+OhW5uQr<_>I)QE7yrb{B=nrkr!qn8Ty5#PM3t=m*fr($*t#7k0OHVOm3Zsw zcJjI`panvlRQ`6w$pl;0epOB7Q|+ZO_z-U9)};mu@Xne>oXZ4kVo#ia{cSk#VsK%8jomz9ozwmkv8TD-SR2B_e-#gq}hu-147=zz_<{9$KRF&20Bw z*8`#UU$5DZV+>)9i6ii!uO7A=!l-j#6nQNn)9&W>iwR+2lZ%v9MIMb0d2$-!4(6B)&nNdsMegIxmC@g)MbRs7Ypq+} zi&&TGC^${ck-957X6=p25)|`VsA5y;Ix|{`19*Hgc33k#4ssg2)>@h_;52pwwdRe3)A z+D#Z0U=gFJaTF5fU<8=P2~>iu-wo!6eJozk!MuXJq}V5qc;7ptqP}D;Puu!$EF_HO zmk=g#1~W%+%&FUm31BdLZ8h<@b?KG{Ir_u^xJo5aR%k&pzkGy9y^HIYM-2#ne|`_4 zxpC)S=JkC)hr|W!M-4}^6FMrqq?Krh>%4Y=*_-=xIHJ5~ld72LwiYi6e~T z^%x?ASM?u^wokt%4kbvUG2PHe+vBS|yk9A8R4po!GsbeHG7Oo~tS~L#Wykq9AAIC0 zs9qY*J5Y0paqoFPuN=Qc zuG6ion0InL#2-H1xp4iPUt_-tL4pGOj~&XoYxXtFE*I zmzRr+cp${}aiq`?vn^?k;ky?!!nXx2ricYIDV-1_{a!~amkHI;$xdCHow@}*m+ax~ zhuONml!-;4_X~1OG~eF!Ms{ZftAVDvrm71#et0seSQaP!$M>Oxlwc7Rw*>Doeli&f zt%CMW#6rF`2}SnQ=KP`5zuL&J4y`2aRx;Z7l7cK zgg(G;zVsp2o;T|5@D^Y0jk(csE6F$zlgTC|kx*1|oVOYXA|paI6+>>4(YbcQXkZb# z_+>1JbpMDGm20rbiumqsB)-#{B_$-iF<$YocdP<@$n7wByiD2bxHVjhy0~wLPg?JU z+o?B!dBF-ypYn}xThpka`X}yNq=+Rtl7soD#{sPT)a{HFkv}pB`)Z%hiuq5VmS#C< zchCP%5$H{_(-PPvPEO-uSU}P|e~Qotx^NBOh5O%O|Nmn4Nd|1f)K=wWRQabug^-a@ K6t4yv1^oxI`}ybq diff --git a/examples/viper-duel/client/public/favicon-16x16.png b/examples/viper-duel/client/public/favicon-16x16.png deleted file mode 100644 index 3c30e21b5a6ae17254a4c7b2ac4856b974f14e2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 474 zcmV<00VV#4P)Px$lu1NER5(v#WS|f*QmCI{2pRD1?tgyHKYzm*{xE<4`;Xxgk1pdSkoUn}nDTpu zGYbo+U6C+(HkVSEs&6u(5E<{QCS??B91rtGhdYAF}S}*abGA^GAap z6T2AC%fJ6WOEZWH7qWycsQZ2<{>@LW{G2OFHT-{BSlF4FS^ocHIL`2k!C2IVam$&s z|6C@s7{7rHnDjQ6FHd;#r=mZr<9_{P=8|XSUcBQy+ivEsf=qRHH5x@fFn?iXU=(Ni z#e9~Lg+W_Bh;a+dg($HQ@PD84{g1yzRC&22Pv8C9Ysf8@pL|^Ip2TNn1`c-ivkZTJ z%d-4olF&?I!ES)V|NSiY-tbIi`uxxH$2%r*TWMyQfQ`zgGT%8kSpG3^F)=Y-V`5~I zGB3bk0LWSL&(D{sRxf}AL;tU0 zKKD{xm|dLt!!<^oUkwfane-UGh;T!7urZu@=h)!*2d4p~ChZ}VlyP|t0GgMI5jl_a Qs{jB107*qoM6N<$f*s%I`Tzg` diff --git a/examples/viper-duel/client/public/favicon-32x32.png b/examples/viper-duel/client/public/favicon-32x32.png deleted file mode 100644 index 7046e13e0e2352bd6bff60407b9e4de584400da2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1205 zcmV;m1WNmfP)Px(Z%IT!R9Hvtm1|5~MHq(Xa`x<5SYTnf7PhDbWU&Qe5QJJ<4919JjaW#mNrA-H zL@w6arlz(MR{5cArGO0-4VFluU`%RlwZ@2X8$#2f#BwPup^n?lY>^W!7m=$TD zV$5O)fB1e(GBfYYJb7onZy;FXfNOj{SO-{lVBLXLabUb_T&?D`ZvYt}L=hnbob!x< zW6P!dssPx!gm{ecOVS)_#xn2%95_T}xm%*-l206aZ869{0M3dHX@J2=P=*jtI6PqN zX)H-Ak=!X*wk{e+C6KI+12x?(K%unReEdSWNJfit`p(bQ@gg*(mV5r?$bEoQLe(CY zVKbc+^xKKpWQBsCKhi|`1Fy$&@hgSy0L)wy;;4+1{gQF@OdBnh35f+1lj(k%jreK17|o$vMYA>!C09K`O%@_~PIKM2pyWK%}Y! zPogqAIsbwrqnOVCoQNTS$R(1gFO>0j0cr5F$yQD{e#yr4-asLXM=`K8G*%)7}Inm(~8A#*%!?N>gkJT~p zyheBQFv{HJ8vjIU&u_5WY6r{+9`bqAxUEZjL)oi5oUjjycfS$0L&P8k06;0Yt;>WX zgBOWG5FM2fFMExXE;$LG6Zk=<+QotnZU&S@I z8%Qa1qRPnp6#%44#=WzGq?$C7Nqqpf+k?XIe;X9a&C@K%cqp)?DKIkU!g1(U)*+ooP&J3-D&1_F-XMcP}5L zRd|BZ9VAb z8(6rKXnRN9=&+1rtNa3|02=!gfDvJy6UmW$*cq50HdM}jtPm~7vftLtwVvd^WdIsM z2zUbmu53NPe$jqHEG6>)0En_&&#=re2hBrrB5(o~CM?j$F_j&``eGMk{YlF)Psr3} z?Vqn~55~_QI!h(H_-T5X+T>OiEQoi|R_2dM-=C}#D4aTAsD}zmKHem;OSk2Bm!|Q9 zDIJm(WgNwAEW$Dd03cUG{@#Je>A^s?dvggCRs`T$6f@qc7Km>+>*`=;swZ&sy~Ued zF1(`@w85Jg0FYwQgy@0+!eB;;c6PbLmsG-k6Rh_5s(i=pK(7O=JFxD+nmh0(_es$( T&#s*v00000NkvXXu0mjfa`rqf diff --git a/examples/viper-duel/client/public/favicon.ico b/examples/viper-duel/client/public/favicon.ico deleted file mode 100644 index 7e2de5287498a1d53f5e9c02080d85da33ec4be1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHN2UJwY8eU_bScuUWTWnY&1RIvv5*wUYh%I@dNvtt;#l$F^iVD&}qzOt} zihzjpWg9H8OOd56MMVUcwy?W*@7}$0=iR%wKvbfjo_BKI&N)9bcjhnOeCOxh*_oly z`q28)h76&RwE^wG`!w2o8jWUP(Ee;Rlt%L-`%FxpjZdY~GKSM=Mx+jDLRz%~uM7|=2BPh@~8G1kwon=&oG&Ul(gVm#{kIfyOe^Gf_)ayQ4;j~H9d=W3F6JO= zb8kOtle8bbkV4k&g3wdKAzh?dv%GH1u6jAGQ_b@pjARNqdJ18R9#NP$kSM92>#Wsj zy&8B{;TSnu`}};ZvQ4*^|CTcH`To~^7AlPVbIJw>WC`?}vI;&1S!n|ZQTdFg0=VyA zl#e#oper?383U*`dX*W6UFCRTPP`Dzfwzn5Q#=C@7HS;Ls~&bT ztH~%KTehH%ovlv@^Cl1_4I9=`{@Q#)?V-Paf2xhTaNMX`?!=#oxe5+YZMCbQ_wn(i z`EvT5eRc=^(;X<(miHas+n*rEnSo0A2TR_Te?lZ<#v$_#-Shq(K?u2}#~C_QSn^UF!`v^*(?j@gdjZk&xN2hq3|8 zMjOcPj4%6L|p0TW! zSs=4nlm9TI;QzY4`hh?$;kgdbz;+EAZT@#TVU#p7voPxd|vy(qXR5 z7z&UDP=rK+`wDBUpmjk{ef36PB9iEX3giSRM^3^r^f)Y5ywmyi2BXeK$AFFj9RvSs z28>7mbzQ`#qr`Pn*GX`P3-ZRfMN2px6NA5-d-cOxw~3%E`Cj?B$DmKHS*sRR=571^y$m& z-)3K4)2Ck*G)$_vUo-B>{mQW_QTZ59B%Y`iJ^suwqh^t2&siIf+ym_^vtev%I~SgV zoQsJ@Z0>}U`|+eChNHGgq@*_ z2iij~FU?+X*RhwiFPp%Mbt|G+D|WGG9}Y}~-FG639+fkYNdpD3zXs4lBp_qi5N zi|5Vda?jun{HRm9mCA{A8gt2-^-PYa9Qm5NFO&^-F=IC7nC%8##NECNw7z>ceA(jGbm0dCH)9Y{P-Fzk8a8@qeYg0At&sbLU0^)p zA9dqeDc!kSwd1Ipa4qg8jL<$~E+l?8;akTKA+1k_`irYvY=M93)3M*A)qb*@i!2Ju z04q;q;%j-#N&_N2SD)bXhZ5q(Efr;rJ2dAEvfwCFMM#t_q{H_pjB2keE2U>kvoJxg<(x~&sEO>Yr2mKLAc`%Mh7kxkoiw(ge=_!wz znqvdx_wJb~;^I5;2NzhWg7P}>eSU{vy4k0p1MiZ6whYFuG+l^PIrJ1RcQ*Ao< zXB1n3@DfWHT``Ul{C4#&B0}7OAG=vR2ZUF-y-oZK_@6Nm;7r&J<>sZ=TISCRRqWVs z7upo0zzZGcMLgX0>vZ{MRpR3j1hZloQCj=G>S>F|MkSkVred;e=+E*8zg_-49#eS> zkFLC?k(jDm>f2Q|?JSV*pHT4}5dElE_^B9&+uVkoXmdWJ^+(k~_yzM2NcVA5t()d4 zTRuHp{o5aRfJ4VwIi-`0IE>50P8K9KP9~Bce?;WfE(4Mlk2ocdXR2cH&TILHcb*KS z&JAH#Nv`~m@SX9k33M{N1?&U;ApOXtw$+9n zn8n;kU>_0(J(jdbZ2y}983S^Gw`a0!F-U3{L1ffU0`+a4YDB^&+7~cuPJ#3~t9E%x)=#a}X-`i=X5&UJ z7gz#`KULBz{B-h8$6?vJX~&e?mz_a&FQTJ+r&`PQPNQQ#nEXNJjddO?)eE%dpHX*ppTs6DlGC7Y6 z;9i}B>TIqZYRQdP9%P?|7Tw=@aU=hCeQ>{>ez@O{fxyS(CgFSg7Vy3q1bl)*fo}){ z`bS5SXTMDN_jh`GG8a6jWj&EC?O6Xn=97zO#$SG4bQg<3<^p+MJv9^tenv^K%HN2A1dO4>_U~wSc0KLKMT+&XP!XXy(S$u`mw~fgI^j z$cDqAP-aTbDT5FxM0i0P8x=?=zpMCf>Q`Ob_5~

- {/* Background particles */} - - - {/* Background grid pattern */} -
- - {/* Decorative glow effects */} -
-
- - {/* Only show the app header in game view */} - {gameView === "game" && ( -
-
-

-
- VIPER -
- DUEL -
-

-

→ Out-slither your rival ←

-
-
- )} - - {/* Main Content */} -
- {gameView === "lobby" ? ( - - ) : ( - - )} - - {showError && } -
-
- ); -} - -export default App; diff --git a/examples/viper-duel/client/src/assets/react.svg b/examples/viper-duel/client/src/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/examples/viper-duel/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/BackgroundAnimation.tsx b/examples/viper-duel/client/src/components/BackgroundAnimation.tsx deleted file mode 100644 index 4e477d7a9..000000000 --- a/examples/viper-duel/client/src/components/BackgroundAnimation.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import { useEffect, useRef } from 'react'; - -const COLORS = { - GREEN: 'rgba(42, 255, 107, 0.25)', - PURPLE: 'rgba(180, 37, 255, 0.25)', - YELLOW: 'rgba(255, 237, 0, 0.3)', - GREEN_BRIGHT: 'rgba(42, 255, 107, 0.5)', - PURPLE_BRIGHT: 'rgba(180, 37, 255, 0.5)', - YELLOW_BRIGHT: 'rgba(255, 237, 0, 0.6)', -}; - -export function BackgroundAnimation() { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set canvas dimensions to match the window - const resizeCanvas = () => { - if (!canvas) return; - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - }; - - window.addEventListener('resize', resizeCanvas); - resizeCanvas(); - - // Snake-like segments array - const snakeSegments: SnakeSegment[] = []; - const foodParticles: FoodParticle[] = []; - const numberOfSnakes = Math.min(Math.max(Math.floor(window.innerWidth / 300), 4), 12); - const numberOfFood = Math.min(Math.max(Math.floor(window.innerWidth / 150), 8), 20); - - // Track mouse position for interactive effects - let mouseX = 0; - let mouseY = 0; - - const updateMousePosition = (e: MouseEvent) => { - mouseX = e.clientX; - mouseY = e.clientY; - }; - - window.addEventListener('mousemove', updateMousePosition); - - // Snake segment class for flowing snake-like movement - class SnakeSegment { - x = 0; - y = 0; - targetX = 0; - targetY = 0; - size = 0; - color = ''; - direction = 0; - speed = 0; - segments: { x: number; y: number; size: number }[] = []; - segmentLength = 15; - age = 0; - changeDirectionCooldown = 0; - glowing = false; - - constructor() { - if (!canvas) return; - this.x = Math.random() * canvas.width; - this.y = Math.random() * canvas.height; - this.targetX = this.x; - this.targetY = this.y; - this.size = Math.random() * 4 + 3; - this.color = Math.random() > 0.5 ? COLORS.GREEN : COLORS.PURPLE; - this.direction = Math.random() * Math.PI * 2; - this.speed = Math.random() * 0.5 + 0.3; - - // Initialize snake segments - for (let i = 0; i < this.segmentLength; i++) { - this.segments.push({ - x: this.x - i * (this.size * 2), - y: this.y, - size: this.size * (1 - i * 0.05) - }); - } - } - - update() { - if (!canvas || !ctx) return; - this.age++; - this.changeDirectionCooldown--; - - // Calculate distance to mouse for interactive effects - const dx = this.x - mouseX; - const dy = this.y - mouseY; - const distance = Math.sqrt(dx * dx + dy * dy); - const mouseInfluenceRadius = 200; - - // Snake avoids mouse cursor (like it's afraid) - if (distance < mouseInfluenceRadius && distance > 50) { - const avoidFactor = (1 - distance / mouseInfluenceRadius) * 0.02; - this.direction += (Math.atan2(dy, dx) - this.direction) * avoidFactor; - this.glowing = true; - this.color = this.color === COLORS.GREEN ? COLORS.GREEN_BRIGHT : COLORS.PURPLE_BRIGHT; - } else { - this.glowing = false; - this.color = this.color === COLORS.GREEN_BRIGHT ? COLORS.GREEN : - this.color === COLORS.PURPLE_BRIGHT ? COLORS.PURPLE : this.color; - } - - // Random direction changes (snake-like behavior) - if (this.changeDirectionCooldown <= 0 && Math.random() < 0.005) { - this.direction += (Math.random() - 0.5) * 0.5; - this.changeDirectionCooldown = 60 + Math.random() * 120; - } - - // Subtle direction wobble for organic movement - this.direction += Math.sin(this.age * 0.01) * 0.002; - - // Move snake head - this.x += Math.cos(this.direction) * this.speed; - this.y += Math.sin(this.direction) * this.speed; - - // Boundary wrapping with smooth transition - if (this.x < -50) this.x = canvas.width + 50; - if (this.x > canvas.width + 50) this.x = -50; - if (this.y < -50) this.y = canvas.height + 50; - if (this.y > canvas.height + 50) this.y = -50; - - // Update snake segments (follow the head) - this.segments[0] = { x: this.x, y: this.y, size: this.size }; - - for (let i = 1; i < this.segments.length; i++) { - const prev = this.segments[i - 1]; - const current = this.segments[i]; - - const segmentDx = prev.x - current.x; - const segmentDy = prev.y - current.y; - const segmentDistance = Math.sqrt(segmentDx * segmentDx + segmentDy * segmentDy); - - if (segmentDistance > this.size * 1.5) { - const moveX = (segmentDx / segmentDistance) * 0.3; - const moveY = (segmentDy / segmentDistance) * 0.3; - current.x += moveX; - current.y += moveY; - } - - current.size = this.size * (1 - i * 0.03); - } - - this.draw(); - } - - draw() { - if (!ctx) return; - - // Draw snake segments from tail to head - for (let i = this.segments.length - 1; i >= 0; i--) { - const segment = this.segments[i]; - const alpha = (this.segments.length - i) / this.segments.length; - - ctx.beginPath(); - ctx.arc(segment.x, segment.y, segment.size, 0, Math.PI * 2); - - // Color with fading alpha for tail effect - const baseColor = this.color.slice(0, -4); - const segmentOpacity = alpha * (this.glowing ? 0.8 : 0.4); - ctx.fillStyle = baseColor + segmentOpacity + ')'; - - // Add glow effect for snake segments - if (this.glowing || i < 5) { - ctx.shadowColor = this.color; - ctx.shadowBlur = this.glowing ? 25 : 15; - } - - ctx.fill(); - ctx.shadowBlur = 0; - } - } - } - - // Food particle class for floating food items - class FoodParticle { - x = 0; - y = 0; - size = 0; - age = 0; - pulsePhase = 0; - floatPhase = 0; - glowing = false; - - constructor() { - if (!canvas) return; - this.x = Math.random() * canvas.width; - this.y = Math.random() * canvas.height; - this.size = Math.random() * 3 + 2.5; - this.pulsePhase = Math.random() * Math.PI * 2; - this.floatPhase = Math.random() * Math.PI * 2; - } - - update() { - if (!canvas || !ctx) return; - this.age++; - - // Calculate distance to mouse - const dx = this.x - mouseX; - const dy = this.y - mouseY; - const distance = Math.sqrt(dx * dx + dy * dy); - - // Food glows when mouse is near - this.glowing = distance < 150; - - // Gentle floating motion - this.x += Math.sin(this.age * 0.008 + this.floatPhase) * 0.1; - this.y += Math.cos(this.age * 0.006 + this.floatPhase) * 0.08; - - // Wrap at boundaries - if (this.x < 0) this.x = canvas.width; - if (this.x > canvas.width) this.x = 0; - if (this.y < 0) this.y = canvas.height; - if (this.y > canvas.height) this.y = 0; - - this.draw(); - } - - draw() { - if (!ctx) return; - - // Pulsing size - const pulseSize = this.size * (1 + Math.sin(this.age * 0.05 + this.pulsePhase) * 0.3); - - ctx.beginPath(); - ctx.arc(this.x, this.y, pulseSize, 0, Math.PI * 2); - - const color = this.glowing ? COLORS.YELLOW_BRIGHT : COLORS.YELLOW; - ctx.fillStyle = color; - - if (this.glowing) { - ctx.shadowColor = COLORS.YELLOW; - ctx.shadowBlur = 20; - } else { - ctx.shadowColor = COLORS.YELLOW; - ctx.shadowBlur = 8; - } - - ctx.fill(); - ctx.shadowBlur = 0; - - // Draw small sparkle effect - if (this.glowing && Math.random() < 0.1) { - const sparkleX = this.x + (Math.random() - 0.5) * pulseSize * 2; - const sparkleY = this.y + (Math.random() - 0.5) * pulseSize * 2; - - ctx.beginPath(); - ctx.arc(sparkleX, sparkleY, 0.5, 0, Math.PI * 2); - ctx.fillStyle = COLORS.YELLOW_BRIGHT; - ctx.fill(); - } - } - } - - // Create snake segments and food - const init = () => { - for (let i = 0; i < numberOfSnakes; i++) { - snakeSegments.push(new SnakeSegment()); - } - for (let i = 0; i < numberOfFood; i++) { - foodParticles.push(new FoodParticle()); - } - }; - - init(); - - // Animation loop - const animate = () => { - if (!canvas || !ctx) return; - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw food particles first (behind snakes) - foodParticles.forEach(food => food.update()); - - // Draw snake segments - snakeSegments.forEach(snake => snake.update()); - - // Optional: connect nearby snakes with faint lines - connectSnakes(); - - requestAnimationFrame(animate); - }; - - // Connect nearby snake heads with faint energy lines - const connectSnakes = () => { - if (!ctx) return; - const maxDistance = 150; - - for (let a = 0; a < snakeSegments.length; a++) { - for (let b = a + 1; b < snakeSegments.length; b++) { - const snakeA = snakeSegments[a]; - const snakeB = snakeSegments[b]; - - const dx = snakeA.x - snakeB.x; - const dy = snakeA.y - snakeB.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < maxDistance) { - const opacity = (1 - distance / maxDistance) * 0.08; - - // Different colored connection based on snake types - const colorA = snakeA.color === COLORS.GREEN || snakeA.color === COLORS.GREEN_BRIGHT ? 'rgba(42, 255, 107, ' : 'rgba(180, 37, 255, '; - const colorB = snakeB.color === COLORS.GREEN || snakeB.color === COLORS.GREEN_BRIGHT ? 'rgba(42, 255, 107, ' : 'rgba(180, 37, 255, '; - - if (snakeA.color !== snakeB.color) { - // Different colored snakes create special connections - const gradient = ctx.createLinearGradient(snakeA.x, snakeA.y, snakeB.x, snakeB.y); - gradient.addColorStop(0, colorA + opacity + ')'); - gradient.addColorStop(0.5, 'rgba(255, 237, 0, ' + (opacity * 0.5) + ')'); // Yellow middle - gradient.addColorStop(1, colorB + opacity + ')'); - - ctx.strokeStyle = gradient; - ctx.lineWidth = 1.2; - ctx.beginPath(); - ctx.moveTo(snakeA.x, snakeA.y); - ctx.lineTo(snakeB.x, snakeB.y); - ctx.stroke(); - } - } - } - } - }; - - // Start animation - animate(); - - // Create special effects for game events - const createSnakeExplosion = (x: number, y: number, isPlayer1: boolean, count = 8) => { - const color = isPlayer1 ? COLORS.GREEN_BRIGHT : COLORS.PURPLE_BRIGHT; - - // Create temporary explosion snake segments - for (let i = 0; i < count; i++) { - const angle = (Math.PI * 2 * i) / count; - const snake = new SnakeSegment(); - snake.x = x; - snake.y = y; - snake.direction = angle; - snake.speed = Math.random() * 2 + 1; - snake.color = color; - snake.segmentLength = 8; - snake.glowing = true; - - // Remove after short time - setTimeout(() => { - const index = snakeSegments.indexOf(snake); - if (index > -1) snakeSegments.splice(index, 1); - }, 2000); - - snakeSegments.push(snake); - } - }; - - // Listen for game events - const handleGameEvent = (e: CustomEvent) => { - const { x, y, type, isPlayer1 } = e.detail; - - if (type === 'snakeMove') { - createSnakeExplosion(x, y, isPlayer1, 5); - } else if (type === 'foodEaten') { - // Create food explosion - for (let i = 0; i < 6; i++) { - const food = new FoodParticle(); - food.x = x; - food.y = y; - food.glowing = true; - foodParticles.push(food); - - setTimeout(() => { - const index = foodParticles.indexOf(food); - if (index > -1) foodParticles.splice(index, 1); - }, 1500); - } - } - }; - - window.addEventListener('gameEvent', handleGameEvent as EventListener); - - // Cleanup on component unmount - return () => { - window.removeEventListener('resize', resizeCanvas); - window.removeEventListener('mousemove', updateMousePosition); - window.removeEventListener('gameEvent', handleGameEvent as EventListener); - }; - }, []); - - return ( - - ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/Board.tsx b/examples/viper-duel/client/src/components/Board.tsx deleted file mode 100644 index b9ce5b4d5..000000000 --- a/examples/viper-duel/client/src/components/Board.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import type { GameState, Snake } from '../types'; -import { cn } from '../lib/utils'; - -interface BoardProps { - gameState: GameState; - playerId: string | null; // 'player1' or 'player2' -} - -const GRID_SIZE = 20; -const CELL_SIZE = 'w-4 h-4'; // Tailwind classes for cell size - -export function Board({ - gameState, - playerId -}: BoardProps) { - // Helper function to determine what's at a position - const getCellContent = (x: number, y: number): 'empty' | 'player1' | 'player2' | 'food' => { - // Check for food - if (gameState.food.some(food => food.x === x && food.y === y)) { - return 'food'; - } - - // Check for player1 snake - if (gameState.snakes.player1.body.some(segment => segment.x === x && segment.y === y)) { - return 'player1'; - } - - // Check for player2 snake - if (gameState.snakes.player2.body.some(segment => segment.x === x && segment.y === y)) { - return 'player2'; - } - - return 'empty'; - }; - - // Helper function to check if position is snake head - const isSnakeHead = (x: number, y: number, snake: Snake): boolean => { - return snake.body.length > 0 && snake.body[0].x === x && snake.body[0].y === y; - }; - - return ( -
- {/* Glow effect based on player */} -
- - {/* Board container */} -
- {/* Background patterns and effects */} -
- - {/* Game cells */} - {Array.from({ length: GRID_SIZE * GRID_SIZE }, (_, index) => { - const x = index % GRID_SIZE; - const y = Math.floor(index / GRID_SIZE); - const content = getCellContent(x, y); - - return ( -
- {/* Add subtle glow for snake heads */} - {((content === 'player1' && isSnakeHead(x, y, gameState.snakes.player1)) || - (content === 'player2' && isSnakeHead(x, y, gameState.snakes.player2))) && ( -
- )} -
- ); - })} -
-
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/Cell.tsx b/examples/viper-duel/client/src/components/Cell.tsx deleted file mode 100644 index cf9edce5e..000000000 --- a/examples/viper-duel/client/src/components/Cell.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useState, useEffect } from 'react'; -// Cell component for individual grid cells (legacy from tic-tac-toe) -import { cn } from '../lib/utils'; -import { CellAuraEffect } from './CellAuraEffect'; -import { CellMarkEffect } from './CellMarkEffect'; -import { useSoundEffects } from '../hooks/useSoundEffects'; - -interface CellProps { - value: string | null; - position: number; - isPlayerTurn: boolean; - onClick: (position: number) => void; - gameOver: boolean; -} - -export function Cell({ value, position, isPlayerTurn, onClick, gameOver }: CellProps) { - // Determine if cell is clickable - const isClickable = !value && isPlayerTurn && !gameOver; - - // Track when a symbol is placed for animation purposes - const [activated, setActivated] = useState(false); - const [prevValue, setPrevValue] = useState(null); - - // Sound effects - const { playSound } = useSoundEffects(); - - // Detect when a new symbol is placed - useEffect(() => { - if (value && value !== prevValue) { - setActivated(true); - setPrevValue(value); - - // Play sound effect based on the symbol - playSound(value === 'X' ? 'mark-x' : 'mark-o', 0.4); - - // Dispatch custom event for background particles to react - const rect = document.getElementById(`cell-${position}`)?.getBoundingClientRect(); - if (rect) { - const cellMarkedEvent = new CustomEvent('cellMarked', { - detail: { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2, - isX: value === 'X' - } - }); - window.dispatchEvent(cellMarkedEvent); - } - } - }, [value, prevValue, position]); - - return ( -
isClickable ? onClick(position) : undefined} - role="button" - aria-label={value ? `Cell ${position + 1}, ${value}` : `Cell ${position + 1}, empty`} - aria-disabled={!isClickable} - tabIndex={isClickable ? 0 : -1} - > - {/* Enhanced aura effects - only appears when cell has a value */} - {value && ( - <> - {/* Inner glow */} -
- - {/* Outer glow */} -
- - {/* Static particle effect (background) */} -
-
-
- - {/* Animated pulsing effect */} -
- - {/* Dynamic canvas-based particle effect */} - - - {/* Dramatic mark effect when cell is activated */} - - - )} - - {/* Hover effect for empty cells */} - {!value && isPlayerTurn && !gameOver && ( -
- )} - - {/* The X or O with text shadow */} - - {value} - -
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/CellAuraEffect.tsx b/examples/viper-duel/client/src/components/CellAuraEffect.tsx deleted file mode 100644 index 8e2dc7468..000000000 --- a/examples/viper-duel/client/src/components/CellAuraEffect.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useEffect, useRef } from 'react'; -// Aura effect component (legacy from tic-tac-toe) - -interface CellAuraEffectProps { - value: string; - activated: boolean; -} - -export function CellAuraEffect({ value, activated }: CellAuraEffectProps) { - const canvasRef = useRef(null); - - // Colors based on player symbol - const color = value === 'X' - ? { primary: '#00e5ff', glow: 'rgba(0, 229, 255, ' } - : { primary: '#ff49e1', glow: 'rgba(255, 73, 225, ' }; - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set canvas dimensions to match the parent element (cell) - canvas.width = canvas.offsetWidth || 100; - canvas.height = canvas.offsetHeight || 100; - - // Create particles - const particlesArray: Particle[] = []; - const numberOfParticles = 20; - - class Particle { - x = 0; - y = 0; - size = 0; - speedX = 0; - speedY = 0; - opacity = 0; - age = 0; - lifespan = 0; - - constructor(exploding: boolean = false) { - // Place particles centrally - this.x = (canvas?.width || 100) / 2 + (Math.random() - 0.5) * 20; - this.y = (canvas?.height || 100) / 2 + (Math.random() - 0.5) * 20; - - this.size = Math.random() * 3 + 0.5; - - if (exploding) { - // Exploding particles move outward in all directions - const angle = Math.random() * Math.PI * 2; - const speed = Math.random() * 3 + 1; - this.speedX = Math.cos(angle) * speed; - this.speedY = Math.sin(angle) * speed; - this.opacity = 1; - } else { - // Normal particles drift randomly - this.speedX = (Math.random() - 0.5) * 0.5; - this.speedY = (Math.random() - 0.5) * 0.5; - this.opacity = Math.random() * 0.7; - } - - this.age = 0; - this.lifespan = 60 + Math.random() * 60; // 1-2 seconds at 60fps - } - - update() { - this.age++; - - // Move particle - this.x += this.speedX; - this.y += this.speedY; - - // Gradually slow down exploding particles - this.speedX *= 0.98; - this.speedY *= 0.98; - - // Fade out toward end of life - let alpha = this.opacity; - if (this.age > this.lifespan * 0.7) { - alpha = this.opacity * (1 - (this.age - this.lifespan * 0.7) / (this.lifespan * 0.3)); - } - - // Draw particle - if (ctx) { - ctx.beginPath(); - ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); - ctx.fillStyle = color.glow + alpha + ')'; - - // Add glow effect - ctx.shadowColor = color.primary; - ctx.shadowBlur = 5; - ctx.fill(); - ctx.shadowBlur = 0; - } - - // Return true if particle is still alive - return this.age < this.lifespan; - } - } - - // Initialize particles - const init = () => { - particlesArray.length = 0; // Clear any existing particles - for (let i = 0; i < numberOfParticles; i++) { - particlesArray.push(new Particle()); - } - }; - - // Add explosion particles when the cell is activated - const addExplosionParticles = () => { - for (let i = 0; i < 30; i++) { - particlesArray.push(new Particle(true)); - } - }; - - let active = false; - - // Animation loop - const animate = () => { - if (!canvas || !ctx) return; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Update and filter particles (remove dead ones) - for (let i = 0; i < particlesArray.length; i++) { - if (!particlesArray[i].update()) { - particlesArray.splice(i, 1); - i--; - } - } - - // Add new particles to maintain a minimum count - while (particlesArray.length < numberOfParticles / 2) { - particlesArray.push(new Particle()); - } - - // Continue animation - animationFrameId = requestAnimationFrame(animate); - }; - - // Initialize and start animation - init(); - let animationFrameId = requestAnimationFrame(animate); - - // Add explosion effect when activated changes to true - if (activated && !active) { - addExplosionParticles(); - active = true; - } - - // Handle window resize - const handleResize = () => { - if (!canvas) return; - canvas.width = canvas.offsetWidth || 100; - canvas.height = canvas.offsetHeight || 100; - }; - - window.addEventListener('resize', handleResize); - - // Cleanup - return () => { - window.removeEventListener('resize', handleResize); - cancelAnimationFrame(animationFrameId); - }; - }, [color.glow, color.primary, activated]); - - return ( - - ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/CellMarkEffect.tsx b/examples/viper-duel/client/src/components/CellMarkEffect.tsx deleted file mode 100644 index 7fe317a62..000000000 --- a/examples/viper-duel/client/src/components/CellMarkEffect.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState, useEffect } from 'react'; -// Mark effect component (legacy from tic-tac-toe) -import { cn } from '../lib/utils'; - -interface CellMarkEffectProps { - value: string; - activated: boolean; -} - -export function CellMarkEffect({ value, activated }: CellMarkEffectProps) { - const [showEffect, setShowEffect] = useState(false); - - // Reset and show the effect when activated changes - useEffect(() => { - if (activated) { - setShowEffect(true); - - // Remove elements after animation completes - const timer = setTimeout(() => { - setShowEffect(false); - }, 1500); // Match duration with animation - - return () => clearTimeout(timer); - } - }, [activated]); - - if (!showEffect) return null; - - const isX = value === 'X'; - // const baseColor = isX ? 'cyan' : 'magenta'; - - return ( -
- {/* Center burst */} -
- - {/* Orbiting particles */} - {Array.from({ length: 6 }).map((_, i) => ( -
- ))} - - {/* Random floating particles */} - {Array.from({ length: 12 }).map((_, i) => { - // Calculate random positions and delays - const angle = Math.random() * Math.PI * 2; - const distance = 20 + Math.random() * 40; - const size = 1 + Math.random() * 2; - const delay = Math.random() * 0.5; - const duration = 0.5 + Math.random() * 1; - - const x = Math.cos(angle) * distance; - const y = Math.sin(angle) * distance; - - return ( -
- ); - })} - - {/* Lines radiating outward */} - {Array.from({ length: 8 }).map((_, i) => { - const angle = (i * Math.PI) / 4; // 8 directions - const delay = i * 0.05; - - return ( -
- ); - })} - - {/* Add custom keyframes for line growth with a style tag */} - -
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ChannelDeposit.tsx b/examples/viper-duel/client/src/components/ChannelDeposit.tsx deleted file mode 100644 index 562cd6052..000000000 --- a/examples/viper-duel/client/src/components/ChannelDeposit.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useState } from "react"; -import { useChannel } from "../hooks/useChannel"; -import { WalletStore } from "../store"; -import { useStore } from "../store/storeUtils"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card"; -import { Button } from "./ui/button"; -import { cn } from "../lib/utils"; - -// Default USDC token address on Polygon -const USDC_ADDRESS = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; - -/** - * Component for depositing funds and creating a channel - */ -export function ChannelDeposit() { - const amount = "0.0001"; // Fixed amount - const [isProcessing, setIsProcessing] = useState(false); - const [error, setError] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); - - const { createChannel, depositToChannel, isChannelOpen } = useChannel(); - const wallet = useStore(WalletStore.state); - - // Fixed amount, no change handler needed - - const handleCreateChannel = async () => { - if (!amount || parseFloat(amount) <= 0) { - setError("Please enter a valid amount"); - return; - } - - setIsProcessing(true); - setError(null); - setSuccessMessage(null); - - try { - // Then deposit to it - await depositToChannel(USDC_ADDRESS, amount); - - // First create a channel - await createChannel(USDC_ADDRESS, amount); - - setSuccessMessage(`Successfully created channel and deposited ${amount} USDC`); - } catch (err) { - console.error("Channel creation/deposit error:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setIsProcessing(false); - } - }; - - if (isChannelOpen) { - return ( - - - Channel Active - - You have an active channel with {wallet.channelAmount ? Number(wallet.channelAmount) / 1000000 : "0"} USDC deposited. - - - - - - - ); - } - - return ( - - - Create Channel - Create a new channel by depositing USDC. This is required to play games. - - - {error &&
{error}
} - - {successMessage && ( -
{successMessage}
- )} - -
- -
-
- 0.0001 -
- - USDC - -
-
-
-

Fixed deposit amount: 0.0001 USDC

-
-
- - - -
- ); -} diff --git a/examples/viper-duel/client/src/components/ChannelRequiredModal.tsx b/examples/viper-duel/client/src/components/ChannelRequiredModal.tsx deleted file mode 100644 index 16b2af725..000000000 --- a/examples/viper-duel/client/src/components/ChannelRequiredModal.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { useState, useEffect } from "react"; -import { Button } from "./ui/button"; -import { Loader2, AlertCircle, Coins, ArrowRight } from "lucide-react"; -import { cn } from "../lib/utils"; -// Since we're importing from viem which isn't configured yet, let's declare the types here -// type Address = `0x${string}`; -type Hex = `0x${string}`; -import { useChannel } from "../hooks/useChannel"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "./ui/dialog"; - -// USDC token address on the testnet -const USDC_ADDRESS: Hex = "0x07865c6E87B9F70255377e024ace6630C1Eaa37F"; - -interface ChannelRequiredModalProps { - isOpen: boolean; - onClose: () => void; - onSuccess: (action: "join" | "create", roomId?: string) => void; - mode: "join" | "create"; - roomId?: string; -} - -export function ChannelRequiredModal({ isOpen, onClose, onSuccess, mode, roomId }: ChannelRequiredModalProps) { - const amount = "0.0001"; // Fixed amount - const [step, setStep] = useState<"info" | "create" | "success">("info"); - const { createChannel, depositToChannel, isLoading, error, isChannelOpen } = useChannel(); - - // Check if the modal should stay open when there's no channel - useEffect(() => { - // If modal is open, check if channel ID exists - if (isOpen) { - const channelId = localStorage.getItem("nitrolite_channel_id"); - - // If channel is open in state and channel ID exists, proceed to success - if (isChannelOpen && channelId) { - console.log("Channel is already open and ID exists, closing modal without calling onSuccess"); - // Just close the modal without calling onSuccess to prevent double join message - onClose(); - } - } - }, [isChannelOpen, isOpen, onClose]); - - const handleCreateChannel = async () => { - // Fixed amount, no validation needed - - // No validation needed for fixed amount - - try { - console.log("Starting channel creation process with fixed amount:", amount); - // Immediately show the creating screen - setStep("create"); - - try { - // Step 1: Deposit to channel - console.log("Starting deposit with amount:", amount); - await depositToChannel(USDC_ADDRESS, amount); - console.log("Deposit successful, creating channel..."); - - // Step 2: Create channel - const result = await createChannel(USDC_ADDRESS, amount); - console.log("Channel creation successful:", result); - - // Only proceed to success if we have a valid result - if (result && result.channelId) { - // Make sure the channel ID exists in localStorage - localStorage.setItem("nitrolite_channel_id", result.channelId); - console.log("Saved channel ID to localStorage:", result.channelId); - - // Show success screen - setStep("success"); - - // Auto proceed after 1.5 seconds to allow the user to see the success message - setTimeout(() => { - // Log the room action before proceeding - console.log(`Proceeding with ${mode} action after channel creation, roomId:`, roomId); - onSuccess(mode, roomId); - onClose(); - }, 1500); - } else { - throw new Error("Channel creation successful but no channel ID returned"); - } - } catch (innerErr) { - console.error("Process failed during channel creation:", innerErr); - throw innerErr; // Re-throw to be caught by the outer catch - } - } catch (err) { - console.error("Channel creation failed:", err); - // Error state is handled by the useChannel hook - // Stay in the "create" step for 1.5 seconds so user sees the error before going back to info - setTimeout(() => { - setStep("info"); - }, 1500); - } - }; - - // No handleAmountChange needed for fixed amount - - return ( - { - if (!open) { - onClose(); - } - }}> - - {/* Background gradient */} -
- - {/* Particle effects */} -
-
-
- - {/* Content */} -
- - - {step === "info" && "Channel Required"} - {step === "create" && "Creating Channel..."} - {step === "success" && "Channel Created!"} - - - {step === "info" && "To play Viper Duel, you need to create a payment channel by depositing USDC."} - {step === "create" && "Please confirm the transaction in your wallet."} - {step === "success" && "Your payment channel has been created successfully!"} - - - -
- {step === "info" && ( -
-
-

- - Why deposit USDC? -

-

- Viper Duel uses payment channels to enable instant, secure gameplay without gas fees for each move. -

-

Your funds remain fully under your control and can be withdrawn at any time.

-
- -
- -
- $ - 0.0001 -
-

Fixed deposit amount: 0.0001 USDC

-
- - {error && ( -
- -
-

Error

-

{error}

-
-
- )} -
- )} - - {step === "create" && ( -
- -

Creating your channel...

-

Check your wallet for transaction confirmation

-
- )} - - {step === "success" && ( -
-
- - - -
-

Deposit Successful!

-

- Your payment channel is now active. -
- You'll be redirected to the game momentarily. -

-
- )} -
- - - {step === "info" && ( - <> - - - - )} - - {step === "create" && ( - - )} - - {step === "success" && ( - - )} - -
-
-
- ); -} diff --git a/examples/viper-duel/client/src/components/ErrorModal.tsx b/examples/viper-duel/client/src/components/ErrorModal.tsx deleted file mode 100644 index ce8d82256..000000000 --- a/examples/viper-duel/client/src/components/ErrorModal.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { AlertTriangle } from 'lucide-react'; -import { Button } from './ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from './ui/dialog'; - -interface ErrorModalProps { - message: string; - onClose: () => void; -} - -export function ErrorModal({ message, onClose }: ErrorModalProps) { - // Use a simple implementation without local state to avoid possible state conflicts - return ( - - - {/* Error glow effect */} -
- -
- -
- -
- - Connection Error - -
- -
-

{message}

-
- - - - -
-
-
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/GameLobbyIntegrated.tsx b/examples/viper-duel/client/src/components/GameLobbyIntegrated.tsx deleted file mode 100644 index d16a3637e..000000000 --- a/examples/viper-duel/client/src/components/GameLobbyIntegrated.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useState, useEffect } from "react"; -import { Lobby } from "./Lobby"; -// import { ChannelDeposit } from './ChannelDeposit'; -import { useChannel } from "../hooks/useChannel"; -import { useWebSocketContext } from "../context/WebSocketContext"; -import { useNitroliteIntegration } from "../hooks/useNitroliteIntegration"; -import { useAccount } from 'wagmi'; -import type { JoinRoomPayload, AvailableRoom } from "../types"; -import { Card, CardDescription, CardHeader, CardTitle } from "./ui/card"; -import { Loader2, AlertCircle } from "lucide-react"; - -/** - * Integrated game lobby that handles channel state - */ -interface GameLobbyIntegratedProps { - onJoinRoom: (payload: JoinRoomPayload) => void; - availableRooms: AvailableRoom[]; - onGetAvailableRooms: () => void; - onlineUsers?: number; -} - -export function GameLobbyIntegrated({ onJoinRoom, availableRooms = [], onGetAvailableRooms, onlineUsers = 1 }: GameLobbyIntegratedProps) { - const [isLoading, setIsLoading] = useState(true); - const { isConnected, status } = useWebSocketContext(); - const { clearStoredChannel } = useChannel(); - const { isConnected: isMetaMaskConnected } = useAccount(); - useNitroliteIntegration(); // Ensure proper integration - - // Handle channel initialization on component mount - useEffect(() => { - // Check if nitrolite_channel_id exists in localStorage - const channelId = localStorage.getItem('nitrolite_channel_id'); - if (!channelId) { - console.log("GameLobbyIntegrated: No nitrolite_channel_id found in localStorage"); - // If we need to force a channel creation, we can clear any stale state - clearStoredChannel(); - } else { - console.log("GameLobbyIntegrated: Found nitrolite_channel_id in localStorage:", channelId); - } - }, [clearStoredChannel]); - - // Simulate loading state - useEffect(() => { - const timer = setTimeout(() => { - setIsLoading(false); - }, 1000); - - return () => clearTimeout(timer); - }, []); - - // Show loading state - if (isLoading) { - return ( - <> -
- -
- - ); - } - - // Show Lobby component which has built-in MetaMask connection UI - if (!isMetaMaskConnected) { - return ( - <> - - - ); - } - - // Show WebSocket connection status if MetaMask is connected but WebSocket is not - if (!isConnected) { - return ( - <> - - - - WebSocket Status: {status} - - - Connecting to the game server... - {status === "reconnect_failed" && ( - Connection failed. Please refresh the page and try again. - )} - - - - - ); - } - - // The channel creation is now handled on-demand when joining/creating a game - // We'll keep this code commented for reference - /* - if (!isChannelOpen) { - return ( - <> - -
- - - Channel Required - - You need to create a channel and deposit funds to play games. - - - - - -
- - ); - } - */ - - // Show the game lobby when connected and channel is open - return ( - <> - - - ); -} diff --git a/examples/viper-duel/client/src/components/GameOver.tsx b/examples/viper-duel/client/src/components/GameOver.tsx deleted file mode 100644 index 19450c3b3..000000000 --- a/examples/viper-duel/client/src/components/GameOver.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useEffect } from 'react'; -import type { GameOver as GameOverType } from '../types'; -import { Button } from './ui/button'; -import { cn } from '../lib/utils'; -import { Trophy, Medal, CircleSlash } from 'lucide-react'; -import { useSoundEffects } from '../hooks/useSoundEffects'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from './ui/dialog'; - -interface GameOverProps { - gameOver: GameOverType; - playerId: string | null; - onPlayAgain: () => void; -} - -export function GameOver({ gameOver, playerId, onPlayAgain }: GameOverProps) { - const { winner, finalScores, gameTime } = gameOver; - const { playSound } = useSoundEffects(); - - // Play appropriate sound effect when component mounts - useEffect(() => { - if (winner === playerId) { - playSound('win', 0.5); - } else if (winner) { - playSound('game-over', 0.5); - } else { - playSound('draw', 0.5); - } - }, [winner, playerId, playSound]); - - // Determine message and styling based on game outcome - const getMessage = () => { - if (!winner) { - return "It's a Tie!"; - } - - return winner === playerId ? "You Won!" : "You Lost!"; - }; - - // Get appropriate icon for result - const ResultIcon = !winner ? CircleSlash : (winner === playerId ? Trophy : Medal); - - // Styles based on winner - const iconColor = winner === 'player1' ? 'text-viper-green' : winner === 'player2' ? 'text-viper-purple' : 'text-viper-grey'; - const bgGradient = winner === 'player1' - ? 'from-viper-green/30 to-viper-charcoal/90' - : winner === 'player2' - ? 'from-viper-purple/30 to-viper-charcoal/90' - : 'from-viper-charcoal-light/30 to-viper-charcoal/90'; - - return ( - - - {/* Background gradient */} -
- - {/* Particle effects */} - {winner === playerId && ( -
-
-
- )} - - {/* Content */} -
- -
- -
- - {getMessage()} - -
- -
-
-

Final Scores

-
- Player 1: - {finalScores.player1} -
-
- Player 2: - {finalScores.player2} -
-
- Game Time: {gameTime} seconds -
-
-

- {winner ? `${winner === 'player1' ? 'Player 1' : 'Player 2'} wins!` : "Both snakes died - it's a tie!"} -

-
- - - - -
-
-
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/GameScreen.tsx b/examples/viper-duel/client/src/components/GameScreen.tsx deleted file mode 100644 index 0375932bd..000000000 --- a/examples/viper-duel/client/src/components/GameScreen.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { SnakeCanvas } from "./SnakeCanvas"; -import { GameOver } from "./GameOver"; -import { RoomInfo } from "./RoomInfo"; -import type { GameState, GameOver as GameOverType, Direction } from "../types"; -import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; -import { Button } from "./ui/button"; -import { useEffect, useState } from "react"; - -interface GameScreenProps { - gameState: GameState; - playerId: string | null; - isRoomReady: boolean; - isGameStarted: boolean; - isHost: boolean; - gameOver: GameOverType | null; - opponentAddress: string; - roomId: string; - onDirectionChange: (direction: Direction) => void; - onPlayAgain: () => void; - onStartGame: () => void; - awaitingHostStart?: boolean; - isSigningInProgress?: boolean; -} - -export function GameScreen({ - gameState, - playerId, - isRoomReady, - isGameStarted, - isHost, - gameOver, - opponentAddress, - roomId, - onDirectionChange, - onPlayAgain, - onStartGame, - awaitingHostStart = false, - isSigningInProgress = false, -}: GameScreenProps) { - const [hasChannelId, setHasChannelId] = useState(false); - - useEffect(() => { - const channelId = localStorage.getItem("nitrolite_channel_id"); - setHasChannelId(!!channelId); - }, []); - - // Debug information - console.log("GameScreen state:", { - roomId, - isRoomReady, - isGameStarted, - isHost, - playerId, - opponentAddress, - hasChannelId, - }); - - // Handle keyboard input for snake direction - useEffect(() => { - if (!isGameStarted || gameOver) return; - - const handleKeyPress = (event: KeyboardEvent) => { - // Handle both WASD and Arrow keys - let direction: Direction | null = null; - - // Check for WASD keys (case insensitive) - const key = event.key.toLowerCase(); - if (key === 'w') direction = 'UP'; - else if (key === 's') direction = 'DOWN'; - else if (key === 'a') direction = 'LEFT'; - else if (key === 'd') direction = 'RIGHT'; - - // Check for Arrow keys (case sensitive) - else if (event.key === 'ArrowUp') direction = 'UP'; - else if (event.key === 'ArrowDown') direction = 'DOWN'; - else if (event.key === 'ArrowLeft') direction = 'LEFT'; - else if (event.key === 'ArrowRight') direction = 'RIGHT'; - - if (direction) { - event.preventDefault(); - console.log(`Key pressed: ${event.key}, Direction: ${direction}`); // Debug log - onDirectionChange(direction); - } - }; - - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [isGameStarted, gameOver, onDirectionChange]); - - return ( -
- - {/* Subtle background glow effect for the card */} -
- - - - - - - {/* Waiting for players or game start */} - {(!isRoomReady || !isGameStarted) && ( -
- {!isRoomReady ? ( - - - -
- {hasChannelId ? "Waiting for another player to join..." : "Preparing your game..."} -
-
- - {roomId && hasChannelId && ( -
-

Share this room ID:

-
- {roomId} -
-
- )} - {hasChannelId ? ( -

Players need this ID to join your game

- ) : ( -
-
-

Creating your channel...

-

Please wait while we set up your game

-
- )} -
-
- ) : isHost ? ( - - - - - Game ready! You are the host - - - - - - - ) : ( - - - - - Game ready! - - - -

Waiting for host to start the game...

-
-
- Please wait -
-
-
- )} -
- )} - - {/* Game Status - only show when game is started */} - {isGameStarted && ( -
-
-
- You ({playerId}) - - Score: {playerId === 'player1' ? gameState.snakes.player1.score : gameState.snakes.player2.score} - - - {playerId === 'player1' ? (gameState.snakes.player1.alive ? 'Alive' : 'Dead') : (gameState.snakes.player2.alive ? 'Alive' : 'Dead')} - -
-
- Time -
{gameState.gameTime}s
-
Use WASD or arrows
-
-
- Opponent - - Score: {playerId === 'player1' ? gameState.snakes.player2.score : gameState.snakes.player1.score} - - - {playerId === 'player1' ? (gameState.snakes.player2.alive ? 'Alive' : 'Dead') : (gameState.snakes.player1.alive ? 'Alive' : 'Dead')} - -
-
-
- )} - - {/* Game Board - only show when game is started */} - {isGameStarted && ( -
- -
- )} -
-
- - {/* Game Over Modal */} - {gameOver && } -
- ); -} diff --git a/examples/viper-duel/client/src/components/GameStatus.tsx b/examples/viper-duel/client/src/components/GameStatus.tsx deleted file mode 100644 index f14c562dd..000000000 --- a/examples/viper-duel/client/src/components/GameStatus.tsx +++ /dev/null @@ -1,136 +0,0 @@ -// Game status component (legacy from tic-tac-toe) -import { cn } from '../lib/utils'; -import { Badge } from './ui/badge'; -import { UserCheck, User, Clock } from 'lucide-react'; - -interface GameStatusProps { - isPlayerTurn: boolean; - playerSymbol: string | null; - isRoomReady: boolean; - isGameStarted: boolean; - playerAddress: string; - opponentAddress: string; - formatShortAddress: (address: string) => string; -} - -export function GameStatus({ - isPlayerTurn, - playerSymbol, - playerAddress, - opponentAddress, - formatShortAddress -}: GameStatusProps) { - // This component is only shown when the game has started, so we don't need - // to handle the waiting states here anymore - they're handled in GameScreen - - // Status message and styling - const statusMessage = isPlayerTurn - ? "Your Move" - : "Opponent's Turn"; - - const playerBadgeVariant = playerSymbol === 'X' ? 'cyan' : 'magenta'; - const opponentBadgeVariant = playerSymbol === 'X' ? 'magenta' : 'cyan'; - - // Get player colors - const playerColor = playerSymbol === 'X' ? 'text-cyan-400' : 'text-fuchsia-400'; - const opponentColor = playerSymbol === 'X' ? 'text-fuchsia-400' : 'text-cyan-400'; - - // Glow classes - const playerGlow = playerSymbol === 'X' ? 'glow-cyan' : 'glow-magenta'; - const opponentGlow = playerSymbol === 'X' ? 'glow-magenta' : 'glow-cyan'; - - return ( -
- {/* Turn indicator */} - - {/* Animated background for active player */} - {isPlayerTurn && ( -
-
-
- )} - -
- {isPlayerTurn ? ( - - ) : ( - - )} - {statusMessage} -
-
- - {/* Player info */} -
- {/* Player (You) */} -
- {/* Active player indicator */} - {isPlayerTurn && ( -
- )} - - - - {playerSymbol} - - - {formatShortAddress(playerAddress)} - - (you) -
- - {/* VS */} -
- VS - - {isPlayerTurn ? "your turn" : "waiting"} - -
- - {/* Opponent */} -
- {/* Active player indicator */} - {!isPlayerTurn && ( -
- )} - - - - {playerSymbol === 'X' ? 'O' : 'X'} - - - {formatShortAddress(opponentAddress)} - -
-
-
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/Lobby.tsx b/examples/viper-duel/client/src/components/Lobby.tsx deleted file mode 100644 index 8e2b37253..000000000 --- a/examples/viper-duel/client/src/components/Lobby.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import { useState, useEffect } from "react"; -import type { JoinRoomPayload, AvailableRoom, BetAmount, BetOption } from "../types"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; -import { Input } from "./ui/input"; -import { Button } from "./ui/button"; -import { cn } from "../lib/utils"; -import { Wallet, Users, Loader2, KeyRound, GamepadIcon, RefreshCw, Clock, AlertCircle } from "lucide-react"; -import { useAccount } from 'wagmi'; -import { StyledWalletButton } from './ui/styled-wallet-button'; -import { ChannelRequiredModal } from "./ChannelRequiredModal"; -import { OnlinePlayersCounter } from "./OnlinePlayersCounter"; - -interface LobbyProps { - onJoinRoom: (payload: JoinRoomPayload) => void; - isConnected: boolean; - error: string | null; - availableRooms?: AvailableRoom[]; - onGetAvailableRooms: () => void; - onlineUsers?: number; -} - -// Bet options for game creation -const BET_OPTIONS: BetOption[] = [ - { value: 0, label: "Free" }, - { value: 0.01, label: "$0.01" }, - { value: 0.1, label: "$0.1" }, - { value: 1, label: "$1" }, - { value: 2, label: "$2" } -]; - -export function Lobby({ onJoinRoom, isConnected, error, availableRooms = [], onGetAvailableRooms, onlineUsers = 1 }: LobbyProps) { - const [roomId, setRoomId] = useState(""); - const [roomIdError, setRoomIdError] = useState(""); - const [mode, setMode] = useState<"create" | "join">("create"); - const [loadingRooms, setLoadingRooms] = useState(false); - const [showChannelModal, setShowChannelModal] = useState(false); - const [pendingRoomAction, setPendingRoomAction] = useState<{ mode: "create" | "join"; roomId?: string } | null>(null); - const [selectedBetAmount, setSelectedBetAmount] = useState(0); - - // Use channel hook to check if channel exists - const { isChannelOpen } = { isChannelOpen: true }; - - // Use wagmi hook for wallet connection - const { address, isConnected: isWalletConnected } = useAccount(); - const metamaskError = null; // No error from wagmi by default - const isMetaMaskInstalled = true; // RainbowKit handles wallet detection - - // Fetch available rooms when tab changes to 'join' - useEffect(() => { - if (mode === "join" && isConnected && isWalletConnected) { - setLoadingRooms(true); - onGetAvailableRooms(); - // Set a timeout to hide the loading indicator after 2 seconds - // (in case the server doesn't respond or takes too long) - const timeoutId = setTimeout(() => { - setLoadingRooms(false); - }, 2000); - - return () => clearTimeout(timeoutId); - } - }, [mode, isConnected, isWalletConnected, onGetAvailableRooms]); - - // When availableRooms changes, stop the loading indicator - useEffect(() => { - setLoadingRooms(false); - }, [availableRooms]); - - // Validate Room ID format for joining - const validateRoomId = (id: string): boolean => { - // For joining, room ID is required - if (mode === "join") { - if (!id.trim()) { - setRoomIdError("Room ID is required when joining a game"); - return false; - } - - // Should be a valid UUID format (8-4-4-4-12 hex chars) - const isValid = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/.test(id); - setRoomIdError(isValid ? "" : "Please enter a valid room ID (UUID format)"); - return isValid; - } - - // For creating, room ID is ignored - return true; - }; - - // Handle tab change - const handleTabChange = (value: string) => { - setMode(value as "create" | "join"); - setRoomIdError(""); - - // Fetch available rooms when switching to join tab - if (value === "join" && isConnected && isWalletConnected) { - onGetAvailableRooms(); - } - }; - - // Helper function to format time since creation - const formatTimeAgo = (timestamp: number): string => { - const now = Date.now(); - const secondsAgo = Math.floor((now - timestamp) / 1000); - - if (secondsAgo < 60) { - return `${secondsAgo} second${secondsAgo !== 1 ? "s" : ""} ago`; - } - - const minutesAgo = Math.floor(secondsAgo / 60); - if (minutesAgo < 60) { - return `${minutesAgo} minute${minutesAgo !== 1 ? "s" : ""} ago`; - } - - const hoursAgo = Math.floor(minutesAgo / 60); - return `${hoursAgo} hour${hoursAgo !== 1 ? "s" : ""} ago`; - }; - - // Handle joining a specific available room - const handleJoinAvailableRoom = (selectedRoomId: string) => { - if (!isWalletConnected || !address) { - return; - } - - // Check if channel exists first - if (!isChannelOpen) { - setPendingRoomAction({ mode: "join", roomId: selectedRoomId }); - setShowChannelModal(true); - return; - } - - // Use MetaMask wallet address for app session participants - // Find the room to get its bet amount - const selectedRoom = availableRooms.find(room => room.roomId === selectedRoomId); - const roomBetAmount = selectedRoom?.betAmount || 0; - console.log("Joining available room with MetaMask address:", address, "and roomId:", selectedRoomId, "bet amount:", roomBetAmount); - onJoinRoom({ eoa: address, roomId: selectedRoomId, betAmount: roomBetAmount }); - }; - - // Handle manual refresh of available rooms - const handleRefreshRooms = () => { - if (isConnected && isWalletConnected) { - setLoadingRooms(true); - onGetAvailableRooms(); - } - }; - - // Handle form submission - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - - if (!isWalletConnected || !address) { - return; - } - - const isRoomIdValid = validateRoomId(roomId); - - if (!isRoomIdValid && mode === "join") { - return; - } - - // Check if channel exists first - if (!isChannelOpen) { - if (mode === "create") { - setPendingRoomAction({ mode: "create" }); - } else { - setPendingRoomAction({ mode: "join", roomId: roomId.trim() }); - } - setShowChannelModal(true); - return; - } - - // Use MetaMask wallet address for app session participants - if (mode === "create") { - // When creating a room, always pass undefined for roomId - console.log("Creating a room with MetaMask address:", address, "bet amount:", selectedBetAmount); - onJoinRoom({ eoa: address, roomId: undefined, betAmount: selectedBetAmount }); - } else { - // When joining, use the entered roomId - console.log("Joining a room with MetaMask address:", address, "and roomId:", roomId.trim(), "bet amount:", selectedBetAmount); - onJoinRoom({ eoa: address, roomId: roomId.trim(), betAmount: selectedBetAmount }); - } - }; - - // Handle successful channel creation - const handleChannelSuccess = (action: "join" | "create", roomIdParam?: string) => { - if (!address) return; - - // Use MetaMask wallet address for app session participants - - // Add a debounce mechanism to prevent duplicate calls - const now = Date.now(); - const lastCallTime = window.localStorage.getItem("last_join_call_time"); - const throttleTime = 2000; // 2 seconds - - if (lastCallTime && now - parseInt(lastCallTime) < throttleTime) { - console.log("Throttling joinRoom call - too soon after previous call"); - return; - } - - // Store this call time - window.localStorage.setItem("last_join_call_time", now.toString()); - - if (action === "create") { - console.log("Creating a room with MetaMask address after channel creation:", address, "bet amount:", selectedBetAmount); - onJoinRoom({ eoa: address, roomId: undefined, betAmount: selectedBetAmount }); - } else { - console.log("Joining a room with MetaMask address after channel creation:", address, "and roomId:", roomIdParam, "bet amount:", selectedBetAmount); - onJoinRoom({ eoa: address, roomId: roomIdParam, betAmount: selectedBetAmount }); - } - }; - - - return ( -
- {/* Channel creation modal */} - setShowChannelModal(false)} - onSuccess={handleChannelSuccess} - mode={pendingRoomAction?.mode || "create"} - roomId={pendingRoomAction?.roomId} - /> - {/* Animated glow behind card */} -
- - - {/* Header with particle effect */} - -
-
-
- - -
- VIPER -
- DUEL -
-
- - Out-slither your rival - -
- - - {!isMetaMaskInstalled ? ( -
-
- -
-

MetaMask Not Detected

-

- To play Viper Duel, you need to install the MetaMask browser extension. -

- - Install MetaMask - -
-
-
- ) : !isWalletConnected ? ( -
-
- -
-

- Connect Your Wallet -

-

Connect your wallet to create or join games.

-
- -
- - {metamaskError &&

{metamaskError}

} -
-
-
- ) : ( - - - - - Create Game - - - - Join Game - - - -
- {/* Wallet address display */} -
- -
-
{address}
-
-
- - Connected -
-
-
-
- - {/* Join tab content */} - - {/* Available games list */} -
-
-

- - Available Games -

- -
- - {loadingRooms ? ( -
-
- -

Loading available games...

-
-
- ) : availableRooms.length > 0 ? ( -
- {availableRooms.map((room) => ( -
-
-
-
- - {room.roomId} - - - {room.betAmount === 0 ? "Free" : `$${room.betAmount}`} - -
-
- - Created {formatTimeAgo(room.createdAt)} -
-
- -
-
- ))} -
- ) : ( -
- -

No games available

-

Create a new game or try again later

-
- )} -
- - {/* Manual room ID entry */} -
-
- -

Or enter room ID manually

-
- setRoomId(e.target.value)} - placeholder="Enter the room ID to join" - icon={} - variant="magenta" - className={cn(roomIdError && "border-red-500 focus-visible:ring-red-500")} - /> - {roomIdError && ( -

- - {roomIdError} -

- )} -

Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

-
-
- - {/* Create tab content */} - - {/* Bet selection */} -
- -
- {BET_OPTIONS.map((option) => ( - - ))} -
- {selectedBetAmount > 0 && ( -

- Winner takes all! Total pot: ${(selectedBetAmount * 2).toFixed(2)} -

- )} -
- -
-

- - Host a New Game -

-

You'll create a room and get a Room ID to share with your opponent.

-
-
- - {/* Error message */} - {error && ( -
-

Error

-

{error}

-
- )} - - {/* Submit button */} - -
-
- )} -
- - {/* Card footer with tagline and online counter */} - -

Light speed, neon bleed.

-
- -
-
-
-
- ); -} diff --git a/examples/viper-duel/client/src/components/OnlinePlayersCounter.tsx b/examples/viper-duel/client/src/components/OnlinePlayersCounter.tsx deleted file mode 100644 index 7cd9b0baa..000000000 --- a/examples/viper-duel/client/src/components/OnlinePlayersCounter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Users } from "lucide-react"; -import { Badge } from "./ui/badge"; -import { cn } from "../lib/utils"; -import { useEffect } from "react"; - -interface OnlinePlayersCounterProps { - className?: string; - count?: number; -} - -export function OnlinePlayersCounter({ className, count = 1 }: OnlinePlayersCounterProps) { - // For debugging - log count when it changes - useEffect(() => { - console.log("OnlinePlayersCounter rendering with count:", count); - }, [count]); - - return ( -
- - - {count} online - -
- ); -} diff --git a/examples/viper-duel/client/src/components/README_AURA_EFFECTS.md b/examples/viper-duel/client/src/components/README_AURA_EFFECTS.md deleted file mode 100644 index 109d5c5ed..000000000 --- a/examples/viper-duel/client/src/components/README_AURA_EFFECTS.md +++ /dev/null @@ -1,82 +0,0 @@ -# Viper Duel Visual Effects Guide - -This document provides an overview of the particle and aura visual effects implemented in the Viper Duel game. - -## Core Components - -### 1. CellAuraEffect - -Located in `CellAuraEffect.tsx`, this component creates dynamic canvas-based particle effects that appear around cells when a player marks X or O. The effects are customized based on the player symbol (cyan for X, magenta for O). - -Key features: -- Canvas-based rendering for performance -- Dynamic particle generation and movement -- Particle fading and lifecycle management -- Explosion effect when a cell is first marked - -### 2. CellMarkEffect - -Located in `CellMarkEffect.tsx`, this component adds dramatic visual effects when a player marks a cell, including: -- Center burst animation -- Orbiting particles -- Radiating lines -- Random floating particles - -The component uses CSS animations and transforms for these effects. - -### 3. BackgroundAnimation - -Located in `BackgroundAnimation.tsx`, this component renders the global background particle effect. Features include: -- Floating particles across the entire game background -- Interactive effects that respond to mouse movement -- Connection lines between nearby particles -- Burst effects triggered by cell marks via custom events - -## CSS and Animation - -### Custom Animations - -The following custom animations are defined in `tailwind.config.js`: -- `sparkle`: Particles moving upward with rotation -- `float`: Gentle vertical floating motion -- `pulse-cyan` and `pulse-magenta`: Glowing pulsation effects -- `explode-cyan` and `explode-magenta`: Explosion animations -- `orbit`: Circular movement for particles - -Additional CSS animations in `App.css`: -- Particle movement -- Glow effects -- Line growth animations - -## Event Communication - -Components communicate through custom events: -- `cellMarked`: Dispatched when a cell is marked with X or O, includes position and player data -- This event triggers background particle bursts and sound effects - -## Sound Effects - -Sound feedback is provided through the `useSoundEffects` hook, which: -- Generates programmatic audio for different game events -- Plays appropriate sounds for marking cells and game outcomes -- Adjusts volume and timing for optimal user experience - -## Integration Points - -The effects are integrated at these key points: -1. In `Cell.tsx` when a player marks a cell -2. In `GameOver.tsx` for win/lose/draw animations -3. Throughout the game interface with consistent cyan/magenta theming - -## Extending the Effects - -To add new visual effects: -1. Use the existing color scheme (cyan for X, magenta for O) -2. Follow the pattern of component-based effects -3. Utilize custom events for communication between components -4. Add new animations to tailwind.config.js -5. Consider performance implications for mobile devices - ---- - -*"Every move leaves an aura" - Viper Duel* \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/RoomInfo.tsx b/examples/viper-duel/client/src/components/RoomInfo.tsx deleted file mode 100644 index 7abf46f22..000000000 --- a/examples/viper-duel/client/src/components/RoomInfo.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Copy, Check } from 'lucide-react'; -import { Badge } from './ui/badge'; -import { Button } from './ui/button'; -import { useState, useEffect } from 'react'; -import { cn } from '../lib/utils'; - -interface RoomInfoProps { - roomId: string; -} - -export function RoomInfo({ roomId }: RoomInfoProps) { - const [copied, setCopied] = useState(false); - - // Copy room ID to clipboard - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(roomId); - setCopied(true); - } catch (err) { - console.error('Failed to copy room ID', err); - } - }; - - // Reset copied state after a delay - useEffect(() => { - if (copied) { - const timeout = setTimeout(() => setCopied(false), 2000); - return () => clearTimeout(timeout); - } - }, [copied]); - - // Don't show anything if roomId is not set yet - if (!roomId) { - return null; - } - - return ( -
- - Room: - - {roomId.length > 12 ? `${roomId.substring(0, 8)}...` : roomId} - - - -
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/SnakeCanvas.tsx b/examples/viper-duel/client/src/components/SnakeCanvas.tsx deleted file mode 100644 index 8a7503f98..000000000 --- a/examples/viper-duel/client/src/components/SnakeCanvas.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import { useEffect, useRef } from 'react'; -import type { GameState, Snake } from '../types'; - -interface SnakeCanvasProps { - gameState: GameState; - playerId: string | null; -} - -const GRID_SIZE = 20; -const CANVAS_SIZE = 400; // 400px canvas -const CELL_SIZE = CANVAS_SIZE / GRID_SIZE; // 20px per cell - -export function SnakeCanvas({ gameState, playerId }: SnakeCanvasProps) { - const canvasRef = useRef(null); - const gradientsRef = useRef<{ [key: string]: CanvasGradient }>({}); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set canvas size and scale for high DPI - const dpr = window.devicePixelRatio || 1; - canvas.width = CANVAS_SIZE * dpr; - canvas.height = CANVAS_SIZE * dpr; - canvas.style.width = `${CANVAS_SIZE}px`; - canvas.style.height = `${CANVAS_SIZE}px`; - ctx.scale(dpr, dpr); - - // Clear canvas - ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); - - // Create cached gradients if not exists - if (!gradientsRef.current.green) { - const greenGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, CELL_SIZE * 0.6); - greenGradient.addColorStop(0, '#2AFF6B'); // Bright green center - greenGradient.addColorStop(0.7, '#1EC64E'); // Medium green - greenGradient.addColorStop(1, 'rgba(42,255,107,0)'); // Transparent edge - gradientsRef.current.green = greenGradient; - } - - if (!gradientsRef.current.purple) { - const purpleGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, CELL_SIZE * 0.6); - purpleGradient.addColorStop(0, '#B425FF'); // Bright purple center - purpleGradient.addColorStop(0.7, '#782BF5'); // Medium purple - purpleGradient.addColorStop(1, 'rgba(180,37,255,0)'); // Transparent edge - gradientsRef.current.purple = purpleGradient; - } - - // Draw background grid (subtle) - ctx.globalAlpha = 0.1; - ctx.strokeStyle = '#4B5563'; - ctx.lineWidth = 0.5; - for (let i = 0; i <= GRID_SIZE; i++) { - const pos = (i * CELL_SIZE); - ctx.beginPath(); - ctx.moveTo(pos, 0); - ctx.lineTo(pos, CANVAS_SIZE); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(0, pos); - ctx.lineTo(CANVAS_SIZE, pos); - ctx.stroke(); - } - ctx.globalAlpha = 1; - - // Draw food - gameState.food.forEach(food => { - const x = food.x * CELL_SIZE + CELL_SIZE / 2; - const y = food.y * CELL_SIZE + CELL_SIZE / 2; - - // Food glow - ctx.shadowBlur = 8; - ctx.shadowColor = '#FACC15'; - ctx.fillStyle = '#FACC15'; - ctx.beginPath(); - ctx.arc(x, y, CELL_SIZE * 0.3, 0, Math.PI * 2); - ctx.fill(); - - // Food highlight - ctx.shadowBlur = 0; - ctx.globalAlpha = 0.6; - ctx.fillStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.arc(x - CELL_SIZE * 0.1, y - CELL_SIZE * 0.1, CELL_SIZE * 0.15, 0, Math.PI * 2); - ctx.fill(); - ctx.globalAlpha = 1; - }); - - // Helper function to check if two segments are connected (not a teleport wrap) - const isConnected = (seg1: { x: number; y: number }, seg2: { x: number; y: number }): boolean => { - const dx = Math.abs(seg1.x - seg2.x); - const dy = Math.abs(seg1.y - seg2.y); - // Connected if distance is 1 in either direction (not wrapped around edges) - return (dx <= 1 && dy <= 1) && !(dx > 1 || dy > 1); - }; - - // Helper function to draw a smooth snake - const drawSnake = (snake: Snake, isPlayer1: boolean) => { - if (snake.body.length === 0) return; - - const color = isPlayer1 ? '#2AFF6B' : '#B425FF'; - const bodyThickness = CELL_SIZE * 0.6; - - // Draw body segments individually to avoid wraparound lines - for (let i = 0; i < snake.body.length - 1; i++) { - const current = snake.body[i]; - const next = snake.body[i + 1]; - - // Only draw line if segments are actually connected (not wrapped around) - if (isConnected(current, next)) { - const x1 = current.x * CELL_SIZE + CELL_SIZE / 2; - const y1 = current.y * CELL_SIZE + CELL_SIZE / 2; - const x2 = next.x * CELL_SIZE + CELL_SIZE / 2; - const y2 = next.y * CELL_SIZE + CELL_SIZE / 2; - - // Draw segment with glow - ctx.shadowBlur = 12; - ctx.shadowColor = color; - ctx.lineWidth = bodyThickness; - ctx.lineCap = 'round'; - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - - // Draw highlight pass - ctx.shadowBlur = 0; - ctx.globalAlpha = 0.3; - ctx.lineWidth = bodyThickness * 0.3; - ctx.strokeStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.stroke(); - ctx.globalAlpha = 1; - } - } - - // Draw each body segment as a circle for better continuity - snake.body.forEach((segment, index) => { - const x = segment.x * CELL_SIZE + CELL_SIZE / 2; - const y = segment.y * CELL_SIZE + CELL_SIZE / 2; - const radius = bodyThickness / 2; - - // Skip head (we'll draw it separately) - if (index === 0) return; - - // Body segment glow - ctx.shadowBlur = 8; - ctx.shadowColor = color; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - - // Body segment highlight - ctx.shadowBlur = 0; - ctx.globalAlpha = 0.3; - ctx.fillStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.arc(x - radius * 0.3, y - radius * 0.3, radius * 0.4, 0, Math.PI * 2); - ctx.fill(); - ctx.globalAlpha = 1; - }); - - // Draw head as enhanced design - if (snake.body.length > 0) { - const head = snake.body[0]; - const headX = head.x * CELL_SIZE + CELL_SIZE / 2; - const headY = head.y * CELL_SIZE + CELL_SIZE / 2; - - // Determine head direction for rotation - let direction = { x: 0, y: -1 }; // Default up - if (snake.body.length > 1) { - const neck = snake.body[1]; - direction.x = head.x - neck.x; - direction.y = head.y - neck.y; - - // Handle wraparound cases - if (Math.abs(direction.x) > 1) direction.x = direction.x > 0 ? -1 : 1; - if (Math.abs(direction.y) > 1) direction.y = direction.y > 0 ? -1 : 1; - } - - // Enhanced head design - larger and more distinctive - const headRadius = CELL_SIZE * 0.5; - - ctx.save(); - ctx.translate(headX, headY); - - // Rotate based on direction - let angle = 0; - if (direction.x === 1) angle = Math.PI / 2; // Right - else if (direction.x === -1) angle = -Math.PI / 2; // Left - else if (direction.y === 1) angle = Math.PI; // Down - ctx.rotate(angle); - - // Head outer glow - ctx.shadowBlur = 20; - ctx.shadowColor = color; - - // Main head circle - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(0, 0, headRadius, 0, Math.PI * 2); - ctx.fill(); - - // Inner gradient for depth - ctx.shadowBlur = 0; - const headGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, headRadius); - headGradient.addColorStop(0, color); - headGradient.addColorStop(0.6, color); - headGradient.addColorStop(1, isPlayer1 ? '#1EC64E' : '#782BF5'); - ctx.fillStyle = headGradient; - ctx.beginPath(); - ctx.arc(0, 0, headRadius, 0, Math.PI * 2); - ctx.fill(); - - // Eye-like highlights for direction - ctx.globalAlpha = 0.8; - ctx.fillStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.ellipse(headRadius * 0.2, -headRadius * 0.3, headRadius * 0.15, headRadius * 0.25, 0, 0, Math.PI * 2); - ctx.fill(); - - ctx.beginPath(); - ctx.ellipse(-headRadius * 0.2, -headRadius * 0.3, headRadius * 0.15, headRadius * 0.25, 0, 0, Math.PI * 2); - ctx.fill(); - - // Shine effect - ctx.globalAlpha = 0.4; - ctx.fillStyle = '#FFFFFF'; - ctx.beginPath(); - ctx.arc(-headRadius * 0.3, -headRadius * 0.3, headRadius * 0.3, 0, Math.PI * 2); - ctx.fill(); - - ctx.globalAlpha = 1; - ctx.restore(); - } - }; - - // Draw snakes - if (gameState.snakes.player1.alive) { - drawSnake(gameState.snakes.player1, true); - } - if (gameState.snakes.player2.alive) { - drawSnake(gameState.snakes.player2, false); - } - - }, [gameState, playerId]); - - return ( -
- {/* Glow effect based on player */} -
- - {/* Canvas container */} -
- {/* Background patterns and effects */} -
- - -
-
- ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/badge.tsx b/examples/viper-duel/client/src/components/ui/badge.tsx deleted file mode 100644 index 337d5cf29..000000000 --- a/examples/viper-duel/client/src/components/ui/badge.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "../../lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-gray-800 text-gray-200 shadow hover:bg-gray-700/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - cyan: "border-transparent bg-cyan-500/20 text-cyan-400 border-cyan-500/30", - magenta: "border-transparent bg-fuchsia-500/20 text-fuchsia-400 border-fuchsia-500/30", - success: "border-transparent bg-green-500/20 text-green-400 border-green-500/30", - warning: "border-transparent bg-amber-500/20 text-amber-400 border-amber-500/30", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ); -} - -export { Badge, badgeVariants }; \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/button.tsx b/examples/viper-duel/client/src/components/ui/button.tsx deleted file mode 100644 index 6fc3f1643..000000000 --- a/examples/viper-duel/client/src/components/ui/button.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "../../lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 transform hover:scale-[1.02] active:scale-[0.98] cursor-pointer select-none", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90 hover:shadow-primary/20 hover:shadow-lg", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow-lg", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 hover:shadow-secondary/20 hover:shadow-lg", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - viperGreen: "bg-gradient-to-br from-viper-green via-viper-green to-viper-green-dark text-viper-charcoal shadow-lg shadow-viper-green/25 hover:shadow-viper-green/40 hover:shadow-xl hover:from-viper-green-light hover:to-viper-green border-2 border-viper-green/40 hover:border-viper-green/60 font-bold tracking-wide relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent before:translate-x-[-100%] hover:before:translate-x-[100%] before:transition-transform before:duration-700", - viperPurple: "bg-gradient-to-br from-viper-purple via-viper-purple to-viper-purple-dark text-white shadow-lg shadow-viper-purple/25 hover:shadow-viper-purple/40 hover:shadow-xl hover:from-viper-purple-light hover:to-viper-purple border-2 border-viper-purple/40 hover:border-viper-purple/60 font-bold tracking-wide relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent before:translate-x-[-100%] hover:before:translate-x-[100%] before:transition-transform before:duration-700", - viperYellow: "bg-gradient-to-br from-viper-yellow via-viper-yellow to-viper-yellow-dark text-viper-charcoal shadow-lg shadow-viper-yellow/25 hover:shadow-viper-yellow/40 hover:shadow-xl hover:from-viper-yellow-light hover:to-viper-yellow border-2 border-viper-yellow/40 hover:border-viper-yellow/60 font-bold tracking-wide relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent before:translate-x-[-100%] hover:before:translate-x-[100%] before:transition-transform before:duration-700", - glass: "bg-viper-charcoal/30 backdrop-blur-md border border-viper-charcoal-light/40 text-white shadow-sm hover:bg-viper-charcoal/40 hover:border-viper-green/30", - glowGreen: "bg-viper-charcoal/60 text-viper-green border border-viper-green/50 shadow-[0_0_10px_rgba(42,255,107,0.1)] hover:shadow-[0_0_15px_rgba(42,255,107,0.2)] hover:bg-viper-charcoal/80 hover:border-viper-green/60", - glowPurple: "bg-viper-charcoal/60 text-viper-purple border border-viper-purple/50 shadow-[0_0_10px_rgba(180,37,255,0.1)] hover:shadow-[0_0_15px_rgba(180,37,255,0.2)] hover:bg-viper-charcoal/80 hover:border-viper-purple/60", - }, - size: { - default: "h-10 px-6 py-2 text-sm", - sm: "h-8 px-4 text-xs", - lg: "h-12 px-8 text-base", - xl: "h-14 px-10 text-lg font-medium", - xxl: "h-16 px-12 text-xl font-medium", - icon: "h-10 w-10", - wide: "h-12 px-16 text-base", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, leftIcon, rightIcon, children, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - {leftIcon && {leftIcon}} - {children} - {rightIcon && {rightIcon}} - - ); - } -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/card.tsx b/examples/viper-duel/client/src/components/ui/card.tsx deleted file mode 100644 index 570805480..000000000 --- a/examples/viper-duel/client/src/components/ui/card.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from "react"; - -import { cn } from "../../lib/utils"; - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -CardFooter.displayName = "CardFooter"; - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/dialog.tsx b/examples/viper-duel/client/src/components/ui/dialog.tsx deleted file mode 100644 index c7f89b209..000000000 --- a/examples/viper-duel/client/src/components/ui/dialog.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" - -import { cn } from "../../lib/utils" - -// Modified to include proper modal support -const Dialog = ({ modal = false, ...props }: React.ComponentPropsWithoutRef & { modal?: boolean }) => ( - -) -Dialog.displayName = "Dialog" - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, style, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/input.tsx b/examples/viper-duel/client/src/components/ui/input.tsx deleted file mode 100644 index 12928e265..000000000 --- a/examples/viper-duel/client/src/components/ui/input.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from "react"; -import { cva } from "class-variance-authority"; -import { cn } from "../../lib/utils"; - -const inputVariants = cva( - "flex h-10 w-full rounded-md px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200", - { - variants: { - variant: { - default: - "border border-gray-700/50 bg-gray-800/70 text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-cyan-500 focus-visible:border-cyan-500/50", - cyan: - "border border-cyan-800/40 bg-gray-800/70 text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-cyan-500 focus-visible:border-cyan-500/50", - magenta: - "border border-fuchsia-800/40 bg-gray-800/70 text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-fuchsia-500 focus-visible:border-fuchsia-500/50", - glass: - "border border-gray-700/30 bg-gray-900/40 backdrop-blur-md text-gray-200 shadow-inner focus-visible:ring-1 focus-visible:ring-cyan-500/50", - }, - size: { - default: "h-10", - sm: "h-8 text-xs px-2.5", - lg: "h-12 text-base", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface InputProps - extends Omit, 'size' | 'prefix'> { - icon?: React.ReactNode; - prefix?: string; - variant?: 'default' | 'cyan' | 'magenta' | 'glass'; - size?: 'default' | 'sm' | 'lg'; -} - -const Input = React.forwardRef( - ({ className, type, variant, size, icon, prefix, ...props }, ref) => { - return ( -
- {icon && ( -
- {icon} -
- )} - {prefix && ( -
- {prefix} -
- )} - -
- ); - } -); -Input.displayName = "Input"; - -export { Input, inputVariants }; \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/styled-wallet-button.tsx b/examples/viper-duel/client/src/components/ui/styled-wallet-button.tsx deleted file mode 100644 index c581f0da2..000000000 --- a/examples/viper-duel/client/src/components/ui/styled-wallet-button.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { WalletButton } from '@rainbow-me/rainbowkit'; -import { Wallet, Loader2 } from 'lucide-react'; -import { cn } from '../../lib/utils'; - -interface StyledWalletButtonProps { - className?: string; - variant?: 'viperGreen' | 'viperPurple'; - size?: 'default' | 'sm' | 'lg' | 'xl' | 'xxl'; - disabled?: boolean; -} - -export function StyledWalletButton({ - className, - variant = 'viperGreen', - size = 'lg', - disabled = false -}: StyledWalletButtonProps) { - const baseClasses = cn( - // Base styling - "inline-flex items-center justify-center whitespace-nowrap rounded-lg font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 transform hover:scale-[1.02] active:scale-[0.98] cursor-pointer select-none border-2", - - // Size variants - { - 'h-8 px-4 text-xs': size === 'sm', - 'h-10 px-6 py-2 text-sm': size === 'default', - 'h-12 px-8 text-base': size === 'lg', - 'h-14 px-10 text-lg font-medium': size === 'xl', - 'h-16 px-12 text-xl font-medium': size === 'xxl', - }, - - // Variant styling - { - 'bg-gradient-to-br from-viper-green via-viper-green to-viper-green-dark text-viper-charcoal shadow-lg shadow-viper-green/25 hover:shadow-viper-green/40 hover:shadow-xl hover:from-viper-green-light hover:to-viper-green border-viper-green/40 hover:border-viper-green/60 font-bold tracking-wide relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent before:translate-x-[-100%] hover:before:translate-x-[100%] before:transition-transform before:duration-700': variant === 'viperGreen', - - 'bg-gradient-to-br from-viper-purple via-viper-purple to-viper-purple-dark text-white shadow-lg shadow-viper-purple/25 hover:shadow-viper-purple/40 hover:shadow-xl hover:from-viper-purple-light hover:to-viper-purple border-viper-purple/40 hover:border-viper-purple/60 font-bold tracking-wide relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent before:translate-x-[-100%] hover:before:translate-x-[100%] before:transition-transform before:duration-700': variant === 'viperPurple', - }, - - className - ); - - return ( - - {({ ready, connect, loading }) => { - const buttonDisabled = !ready || disabled; - - return ( - - ); - }} - - ); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/components/ui/tabs.tsx b/examples/viper-duel/client/src/components/ui/tabs.tsx deleted file mode 100644 index 4c373c832..000000000 --- a/examples/viper-duel/client/src/components/ui/tabs.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" - -import { cn } from "../../lib/utils" - -const Tabs = TabsPrimitive.Root - -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsList.displayName = TabsPrimitive.List.displayName - -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName - -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsContent.displayName = TabsPrimitive.Content.displayName - -export { Tabs, TabsList, TabsTrigger, TabsContent } \ No newline at end of file diff --git a/examples/viper-duel/client/src/context/NitroliteClientWrapper.tsx b/examples/viper-duel/client/src/context/NitroliteClientWrapper.tsx deleted file mode 100644 index 0a5b55761..000000000 --- a/examples/viper-duel/client/src/context/NitroliteClientWrapper.tsx +++ /dev/null @@ -1,213 +0,0 @@ -"use client"; - -import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; -import { createPublicClient, http, type Hex } from "viem"; -import { NitroliteStore, WalletStore } from "../store"; -import { NitroliteClient, type ContractAddresses } from "@erc7824/nitrolite"; - -import { ethers } from "ethers"; -import { generateKeyPair } from "./createSigner"; -import { polygon } from "viem/chains"; -import APP_CONFIG from "./app"; -import { useAccount, useWalletClient } from 'wagmi'; - -const CRYPTO_KEYPAIR_KEY = "crypto_keypair"; - -export const CHAINS = polygon; - -export const USDC_ADDRESS = APP_CONFIG.TOKENS[polygon.id] as Hex; - -// Create context for the Nitrolite client -interface NitroliteContextType { - client: NitroliteClient | null; - loading: boolean; - error: string | null; -} - -const NitroliteContext = createContext({ - client: null, - loading: true, - error: null, -}); - -// Hook for components to use the Nitrolite client -export const useNitrolite = () => useContext(NitroliteContext); - -interface NitroliteClientWrapperProps { - children?: React.ReactNode; -} - -export function NitroliteClientWrapper({ children }: NitroliteClientWrapperProps) { - const [clientState, setClientState] = useState({ - client: null, - loading: true, - error: null, - }); - - // Use wagmi hooks for wallet connection - const { address, isConnected } = useAccount(); - const { data: walletClient } = useWalletClient(); - - const initializeKeys = useCallback(async (): Promise<{ keyPair: unknown; stateWalletClient: unknown }> => { - try { - let keyPair = null; - const savedKeys = localStorage.getItem(CRYPTO_KEYPAIR_KEY); - - if (savedKeys) { - try { - keyPair = JSON.parse(savedKeys); - } catch (error) { - keyPair = null; - console.error("Failed to parse saved keys:", error); - } - } - - if (!keyPair) { - keyPair = await generateKeyPair(); - if (typeof window !== "undefined") { - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(keyPair)); - } - } - - const wallet = new ethers.Wallet(keyPair.privateKey); - - const stateWalletClient = { - ...wallet, - account: { - address: wallet.address, - }, - signMessage: async ({ message: { raw } }: { message: { raw: string } }) => { - const { serialized: signature } = wallet.signingKey.sign(raw as ethers.BytesLike); - - return signature as Hex; - }, - }; - - return { keyPair, stateWalletClient }; - } catch (error) { - console.error("Failed to initialize keys:", error); - return { keyPair: null, stateWalletClient: null }; - } - }, []); - - useEffect(() => { - const initializeNitrolite = async () => { - try { - setClientState((prev) => ({ ...prev, loading: true, error: null })); - - // Only proceed if wallet is connected - if (!isConnected || !address || !walletClient) { - setClientState((prev) => ({ - ...prev, - loading: false, - error: "Wallet not connected. Please connect your wallet.", - })); - return; - } - - const keyInitResult = await initializeKeys(); - - if (!keyInitResult || !keyInitResult.stateWalletClient) { - throw new Error("Failed to initialize state wallet client keys."); - } - const { stateWalletClient } = keyInitResult; - - const publicClient = createPublicClient({ - transport: http(), - chain: polygon, - }); - - // Use wagmi wallet client - console.log("Using wagmi wallet client..."); - WalletStore.setWalletClient(walletClient); - console.log("Wallet client set successfully:", walletClient.account); - - const addresses: ContractAddresses = { - custody: APP_CONFIG.CUSTODIES[polygon.id], - adjudicator: APP_CONFIG.ADJUDICATORS[polygon.id], - guestAddress: APP_CONFIG.CHANNEL.DEFAULT_GUEST as Hex, - }; - - const challengeDuration = APP_CONFIG.CHANNEL.CHALLENGE_PERIOD; - - console.log("Creating Nitrolite client with params:", { - publicClientAvailable: !!publicClient, - walletClientAvailable: !!walletClient, - stateWalletClientAvailable: !!stateWalletClient, - account: walletClient.account, - chainId: polygon.id, - challengeDuration: challengeDuration.toString(), - addresses: { - custody: addresses.custody, - adjudicator: addresses.adjudicator, - guestAddress: addresses.guestAddress, - }, - }); - - // Create the Nitrolite client - const client = new NitroliteClient({ - // @ts-ignore - publicClient, - // @ts-ignore - walletClient, - // @ts-ignore - stateWalletClient: stateWalletClient, - account: walletClient.account, - chainId: polygon.id, - challengeDuration: challengeDuration, - addresses, - }); - - // Check if client was created successfully - if (!client) { - throw new Error("Nitrolite client creation failed - client is null"); - } - - console.log("Nitrolite client initialized successfully!"); - - // Store the client in the global store for access elsewhere - NitroliteStore.setClient(client); - - setClientState({ - client, - loading: false, - error: null, - }); - } catch (error: unknown) { - console.error("Failed to initialize Nitrolite client:", error); - - // Provide more specific error messages based on the error - let errorMessage = "Failed to initialize Nitrolite client"; - - if (error instanceof Error) { - if (error.message.includes("provider")) { - errorMessage = "MetaMask provider error. Please refresh the page and try again."; - } else if (error.message.includes("wallet")) { - errorMessage = "Wallet client creation failed. Please ensure MetaMask is connected properly."; - } else { - // Include the actual error message for debugging - errorMessage = `Nitrolite client error: ${error.message}`; - } - - // Log additional details for debugging - console.debug("Error details:", { - message: error.message, - stack: error.stack, - address: address || "not available", - }); - } - - setClientState({ - client: null, - loading: false, - error: errorMessage, - }); - } - }; - - initializeNitrolite(); - }, [initializeKeys, address, isConnected, walletClient]); - - // Provide the client through context - return {children}; -} diff --git a/examples/viper-duel/client/src/context/WebSocketContext.tsx b/examples/viper-duel/client/src/context/WebSocketContext.tsx deleted file mode 100644 index 739995167..000000000 --- a/examples/viper-duel/client/src/context/WebSocketContext.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode, useRef } from "react"; -import { WebSocketClient, createWebSocketClient, type WSStatus, type WalletSigner, getAddressFromPublicKey } from "../websocket"; -import type { Channel } from "@erc7824/nitrolite"; -import APP_CONFIG from "./app"; -import { generateKeyPair, createEthersSigner } from "./createSigner"; -import { WalletStore } from "../store"; - -// Interface for key pairs -export interface CryptoKeypair { - privateKey: string; - publicKey?: string; - address?: string; -} - -const CRYPTO_KEYPAIR_KEY = "crypto_keypair"; -const WS_URL = APP_CONFIG.WEBSOCKET.URL; - -interface WebSocketContextProps { - client: WebSocketClient | null; - status: WSStatus; - keyPair: CryptoKeypair | null; - wsChannel: Channel | null; - currentNitroliteChannel: Channel | null; - isConnected: boolean; - hasKeys: boolean; - generateKeys: () => Promise; - connect: () => Promise; - disconnect: () => void; - setNitroliteChannel: (channel: Channel) => void; - clearKeys: () => void; - sendPing: () => Promise; - sendRequest: (payload: string) => Promise; -} - -const WebSocketContext = createContext(undefined); - -export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [status, setStatus] = useState("disconnected"); - const [keyPair, setKeyPair] = useState(null); - const [currentSigner, setCurrentSigner] = useState(null); - const [wsChannel, setWsChannel] = useState(null); - const [currentNitroliteChannel, setCurrentNitroliteChannel] = useState(null); - const clientRef = useRef(null); - - useEffect(() => { - if (typeof window !== "undefined") { - const savedKeys = localStorage.getItem(CRYPTO_KEYPAIR_KEY); - - if (savedKeys) { - try { - const parsed = JSON.parse(savedKeys) as CryptoKeypair; - - if (parsed.publicKey && !parsed.address) { - parsed.address = getAddressFromPublicKey(parsed.publicKey); - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(parsed)); - } - setKeyPair(parsed); - console.log("Loaded existing keys from storage"); - // @ts-ignore - } catch (e) { - console.error("Failed to parse saved keys - will generate new ones"); - localStorage.removeItem(CRYPTO_KEYPAIR_KEY); - generateNewKeysAndStore(); - } - } else { - console.log("No saved keys found - generating new ones"); - generateNewKeysAndStore(); - } - } - }, []); - - const generateNewKeysAndStore = async () => { - try { - const newKeyPair = await generateKeyPair(); - setKeyPair(newKeyPair); - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(newKeyPair)); - console.log("Generated and stored new crypto keys"); - return newKeyPair; - } catch (error) { - console.error("Error generating and storing keys:", error); - return null; - } - }; - - const generateKeys = useCallback(async () => { - try { - if (typeof window !== "undefined") { - const savedKeys = localStorage.getItem(CRYPTO_KEYPAIR_KEY); - - if (savedKeys) { - try { - const parsed = JSON.parse(savedKeys) as CryptoKeypair; - - if (parsed && typeof parsed.privateKey === "string" && typeof parsed.publicKey === "string") { - if (parsed.publicKey && !parsed.address) { - parsed.address = getAddressFromPublicKey(parsed.publicKey); - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(parsed)); - } - setKeyPair(parsed); - const signer = createEthersSigner(parsed.privateKey); - - setCurrentSigner(signer); - return parsed; - } - } catch (e) { - // Could not parse, fall through and generate keys. - console.error("Failed to parse saved keys during generateKeys:", e); - localStorage.removeItem(CRYPTO_KEYPAIR_KEY); - } - } - } - // If no valid keys in storage, generate new ones. - const newKeyPair = await generateKeyPair(); - - setKeyPair(newKeyPair); - if (typeof window !== "undefined") { - localStorage.setItem(CRYPTO_KEYPAIR_KEY, JSON.stringify(newKeyPair)); - } - const newSigner = createEthersSigner(newKeyPair.privateKey); - - setCurrentSigner(newSigner); - console.log("Generated new cryptographic keys"); - return newKeyPair; - } catch (error) { - const errorMsg = `Error generating keys: ${error instanceof Error ? error.message : String(error)}`; - console.error(errorMsg); - return null; - } - }, []); - - const clearKeys = useCallback(() => { - if (typeof window !== "undefined") { - localStorage.removeItem(CRYPTO_KEYPAIR_KEY); - } - setKeyPair(null); - setCurrentSigner(null); - if (clientRef.current?.isConnected) { - clientRef.current.close(); - } - console.log("Cleared cryptographic keys"); - }, []); - - useEffect(() => { - if (keyPair?.privateKey && !currentSigner) { - try { - const signer = createEthersSigner(keyPair.privateKey); - setCurrentSigner(signer); - console.log("Initialized signer from keys"); - } catch (e) { - console.error(`Failed to create signer: ${e instanceof Error ? e.message : String(e)}`); - } - } - }, [keyPair, currentSigner]); - - useEffect(() => { - if (currentSigner && !clientRef.current) { - const newClient = createWebSocketClient(WS_URL, currentSigner, { - autoReconnect: true, - reconnectDelay: 1000, - maxReconnectAttempts: 5, - requestTimeout: 10000, - }); - - clientRef.current = newClient; - - newClient.onStatusChange((newStatus) => { - setStatus(newStatus); - if (newStatus === "connected") { - setWsChannel(newClient.currentSubscribedChannel); - setCurrentNitroliteChannel(newClient.currentNitroliteChannel); - } else if (newStatus === "disconnected" || newStatus === "reconnect_failed") { - setWsChannel(null); - setCurrentNitroliteChannel(null); - } - }); - - newClient.onError((error) => { - console.error(`WebSocket error: ${error.message}`); - }); - - newClient.onMessage((message) => { - const hasType = (msg: unknown): msg is { type: unknown } => typeof msg === "object" && msg !== null && "type" in msg; - const messageType = hasType(message) ? (typeof message.type === "string" ? message.type : String(message.type)) : "unknown"; - - console.log(`Received message (type: ${messageType})`); - }); - - // Don't automatically connect - wait for MetaMask to be connected first - console.log("WebSocket client initialized, waiting for MetaMask connection..."); - } - - return () => { - if (clientRef.current) { - clientRef.current.close(); - clientRef.current = null; - setStatus("disconnected"); - setWsChannel(null); - setCurrentNitroliteChannel(null); - } - }; - }, [currentSigner]); - - const connect = useCallback(async () => { - if (!clientRef.current) { - console.error("Cannot connect: WebSocket client not initialized (no signer?)"); - return false; - } - if (clientRef.current.isConnected) { - console.log("Already connected"); - return true; - } - - // Check if MetaMask wallet is connected first - const walletClient = WalletStore.getWalletClient(); - if (!walletClient?.account?.address) { - console.log("Cannot connect to WebSocket: MetaMask wallet not connected. Please connect MetaMask first."); - return false; - } - - try { - console.log("MetaMask connected, connecting to WebSocket server..."); - await clientRef.current.connect(); - console.log("WebSocket connection established"); - return true; - } catch (error) { - const errorMsg = `Connection error: ${error instanceof Error ? error.message : String(error)}`; - console.error(errorMsg); - return false; - } - }, []); - - // Effect to automatically connect WebSocket when MetaMask becomes available - useEffect(() => { - const checkAndConnect = async () => { - const walletClient = WalletStore.getWalletClient(); - if (walletClient?.account?.address && clientRef.current && !clientRef.current.isConnected && status === "disconnected") { - console.log("MetaMask connected, attempting WebSocket connection..."); - await connect(); - } - }; - - // Check immediately - checkAndConnect(); - - // Set up an interval to check periodically (in case we miss the wallet connection) - const interval = setInterval(checkAndConnect, 1000); - - return () => clearInterval(interval); - }, [connect, status]); - - const disconnect = useCallback(() => { - if (clientRef.current) { - clientRef.current.close(); - console.log("WebSocket connection closed"); - } else { - console.error("Cannot disconnect: WebSocket client not initialized"); - } - }, []); - - const setNitroliteChannel = useCallback((nitroliteChannel: Channel) => { - if (!clientRef.current) { - console.error("Cannot set Nitrolite channel: Client not initialized"); - return; - } - setCurrentNitroliteChannel(nitroliteChannel); - clientRef.current.setNitroliteChannel(nitroliteChannel); - console.log(`Set Nitrolite channel: ${JSON.stringify(nitroliteChannel).slice(0, 50)}...`); - }, []); - - const sendPing = useCallback(async () => { - if (!clientRef.current?.isConnected) { - console.error("Cannot ping: Not connected"); - return; - } - try { - console.log("Sending ping to server..."); - await clientRef.current.ping(); - console.log("Ping successful"); - } catch (error) { - console.error(`Ping error: ${error instanceof Error ? error.message : String(error)}`); - } - }, []); - - // @ts-ignore - const sendRequest = useCallback(async (signedRequest) => { - if (!clientRef.current?.isConnected) { - const errorMsg = `Cannot send request: Not connected`; - console.error(errorMsg); - throw new Error("WebSocket not connected"); - } - - try { - console.log("Sending request to server..."); - const response = await clientRef.current.sendRequest(signedRequest); - console.log("Request successful"); - return response; - } catch (error) { - const errorMsg = `Request error: ${error instanceof Error ? error.message : String(error)}`; - console.error(errorMsg); - throw error; - } - }, []); - - const value = useMemo( - () => ({ - client: clientRef.current, - status, - keyPair, - wsChannel, - currentNitroliteChannel, - isConnected: status === "connected", - hasKeys: !!keyPair, - generateKeys, - connect, - disconnect, - setNitroliteChannel, - clearKeys, - sendPing, - sendRequest, - }), - [ - status, - keyPair, - wsChannel, - currentNitroliteChannel, - generateKeys, - connect, - disconnect, - setNitroliteChannel, - clearKeys, - sendPing, - sendRequest, - ] - ); - - return {children}; -}; - -export const useWebSocketContext = (): WebSocketContextProps => { - const context = useContext(WebSocketContext); - - if (context === undefined) { - throw new Error("useWebSocketContext must be used within a WebSocketProvider"); - } - return context; -}; diff --git a/examples/viper-duel/client/src/context/app.ts b/examples/viper-duel/client/src/context/app.ts deleted file mode 100644 index 1b0041e9a..000000000 --- a/examples/viper-duel/client/src/context/app.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type Address } from "viem"; - -/** - * Application configuration - * - * This file contains configuration settings for the application, - * including network endpoints and default values. - */ -export const APP_CONFIG = { - // WebSocket configuration for real-time communication - WEBSOCKET: { - URL: "wss://clearnet.yellow.com/ws", - }, - - CHANNEL: { - DEFAULT_GUEST: "0x3c93C321634a80FB3657CFAC707718A11cA57cBf", - CHALLENGE_PERIOD: BigInt(3600), - }, - - TOKENS: { - 137: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" as Address, - }, - - CUSTODIES: { - 137: "0x4C8Bd8877C3b403BA9f9ECfaAD910AF0d8CA2c4D" as Address, - }, - - DEFAULT_ADJUDICATOR: "dummy", - - ADJUDICATORS: { - 137: "0x5F4A4B1D293A973a1Bc0daD3BB3692Bd51058FCF" as Address, - }, -}; - -export default APP_CONFIG; diff --git a/examples/viper-duel/client/src/context/createSigner.ts b/examples/viper-duel/client/src/context/createSigner.ts deleted file mode 100644 index 6fc31ef2b..000000000 --- a/examples/viper-duel/client/src/context/createSigner.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type Hex } from "viem"; -import { ethers } from "ethers"; -import { type MessageSigner, type RequestData, type ResponsePayload } from "@erc7824/nitrolite"; - -/** - * Interface for a cryptographic keypair - */ -export interface CryptoKeypair { - /** Private key in hexadecimal format */ - privateKey: string; - /** Optional Ethereum address derived from the public key */ - address?: string; -} - -/** - * Interface for a wallet signer that can sign messages - */ -export interface WalletSigner { - /** Optional Ethereum address derived from the public key */ - address: Hex; - /** Function to sign a message and return a hex signature */ - sign: MessageSigner; -} - -/** - * Creates a signer from a private key using ethers.js v6 - * - * @param privateKey - The private key to create the signer from - * @returns A WalletSigner object that can sign messages - * @throws Error if signer creation fails - */ -export const createEthersSigner = (privateKey: string): WalletSigner => { - try { - // Create ethers wallet from private key - const wallet = new ethers.Wallet(privateKey); - - return { - address: ethers.getAddress(wallet.address) as Hex, - sign: async (payload: RequestData | ResponsePayload): Promise => { - try { - const message = JSON.stringify(payload); - console.log("Signing message in Sign function:", message); - const digestHex = ethers.id(message); - console.log("Digest Hex:", digestHex); - const messageBytes = ethers.getBytes(digestHex); - - const { serialized: signature } = wallet.signingKey.sign(messageBytes); - - return signature as Hex; - } catch (error) { - console.error("Error signing message:", error); - throw error; - } - }, - }; - } catch (error) { - console.error("Error creating ethers signer:", error); - throw error; - } -}; - -/** - * Generates a random keypair using ethers v6 - * - * @returns A Promise resolving to a CryptoKeypair object - */ -export const generateKeyPair = async (): Promise => { - try { - // Create random wallet - const wallet = ethers.Wallet.createRandom(); - - // Hash the private key with Keccak256 for additional security - const privateKeyHash = ethers.keccak256(wallet.privateKey as string); - - // Derive public key from hashed private key to create a new wallet - const walletFromHashedKey = new ethers.Wallet(privateKeyHash); - - return { - privateKey: privateKeyHash, - address: ethers.getAddress(walletFromHashedKey.address), - }; - } catch (error) { - console.error("Error generating keypair, using fallback:", error); - // Fallback implementation - const randomHex = ethers.randomBytes(32); - const privateKey = ethers.keccak256(randomHex); - const wallet = new ethers.Wallet(privateKey); - - return { - privateKey: privateKey, - address: ethers.getAddress(wallet.address), - }; - } -}; diff --git a/examples/viper-duel/client/src/hooks/useAppSessionSignature.ts b/examples/viper-duel/client/src/hooks/useAppSessionSignature.ts deleted file mode 100644 index 1e378e421..000000000 --- a/examples/viper-duel/client/src/hooks/useAppSessionSignature.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useState, useCallback } from "react"; -import { useWebSocketContext } from "../context/WebSocketContext"; -import { createEthersSigner } from "../context/createSigner"; -import type { AppSessionSignatureRequestMessage, AppSessionStartGameRequestMessage } from "../types"; - -/** - * Hook for handling app session signature requests - */ -export function useAppSessionSignature( - sendSignature?: (roomId: string, signature: string) => void, - sendStartGame?: (roomId: string, signature: string) => void -) { - const [isSigningInProgress, setIsSigningInProgress] = useState(false); - const [signatureError, setSignatureError] = useState(null); - const { keyPair } = useWebSocketContext(); - - /** - * Signs an app session message and sends it to the server - */ - const signAppSessionMessage = useCallback( - async (roomId: string, requestToSign: unknown[], messageType: "appSession:signature" | "appSession:startGame") => { - if (!keyPair?.privateKey) { - throw new Error("No private key available for signing"); - } - - setIsSigningInProgress(true); - setSignatureError(null); - - try { - // Create a signer from the local private key - const signer = createEthersSigner(keyPair.privateKey); - console.log("Client signer created with address:", signer.address); - console.log("Client signing request structure:", JSON.stringify(requestToSign, null, 2)); - - // Sign the exact same request structure that the server generated - // This ensures all participants sign the identical payload - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const signature = await signer.sign(requestToSign as any); - console.log("Client signature created:", signature); - - // Send the signature to the server using the provided callback - if (messageType === "appSession:signature" && sendSignature) { - sendSignature(roomId, signature); - } else if (messageType === "appSession:startGame" && sendStartGame) { - sendStartGame(roomId, signature); - } else { - throw new Error("No send function available for message type: " + messageType); - } - - setIsSigningInProgress(false); - return signature; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown signing error"; - console.error("App session signing error:", errorMessage); - setSignatureError(errorMessage); - setIsSigningInProgress(false); - throw error; - } - }, - [keyPair, sendSignature, sendStartGame] - ); - - /** - * Handles participant B signature request (when joining) - */ - const handleParticipantBSignature = useCallback( - async (message: AppSessionSignatureRequestMessage) => { - try { - await signAppSessionMessage(message.roomId, message.requestToSign, "appSession:signature"); - } catch (error) { - console.error("Failed to sign as participant B:", error); - throw error; - } - }, - [signAppSessionMessage] - ); - - /** - * Handles participant A signature request (when starting game) - */ - const handleParticipantASignature = useCallback( - async (message: AppSessionStartGameRequestMessage) => { - try { - await signAppSessionMessage(message.roomId, message.requestToSign, "appSession:startGame"); - } catch (error) { - console.error("Failed to sign as participant A:", error); - throw error; - } - }, - [signAppSessionMessage] - ); - - return { - isSigningInProgress, - signatureError, - handleParticipantBSignature, - handleParticipantASignature, - signAppSessionMessage, - }; -} diff --git a/examples/viper-duel/client/src/hooks/useChannel.ts b/examples/viper-duel/client/src/hooks/useChannel.ts deleted file mode 100644 index 1ac0beaa0..000000000 --- a/examples/viper-duel/client/src/hooks/useChannel.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { useCallback, useState } from "react"; -import type { Address, Hex } from "viem"; -import { NitroliteStore, WalletStore } from "../store"; -import { useStore } from "../store/storeUtils"; -import { parseTokenUnits } from "./utils/tokenDecimals"; -import { useWebSocketContext } from "../context/WebSocketContext"; -import type { State } from "@erc7824/nitrolite"; -import { USDC_ADDRESS } from '../context/NitroliteClientWrapper'; - -// Define localStorage keys -const STORAGE_KEYS = { - CHANNEL: "nitrolite_channel", - CHANNEL_STATE: "nitrolite_channel_state", - CHANNEL_ID: "nitrolite_channel_id", -}; - -const EMPTY_STATE_DATA = "0x"; - -/** - * Custom hook for managing Nitrolite channels - */ -export function useChannel() { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const { currentNitroliteChannel, setNitroliteChannel } = useWebSocketContext(); - const walletState = useStore(WalletStore.state); - - /** - * Check for existing channels - */ - const checkForExistingChannel = useCallback(async () => { - const savedChannelId = localStorage.getItem(STORAGE_KEYS.CHANNEL_ID); - - if (savedChannelId) { - console.log("Found existing channel ID in localStorage:", savedChannelId); - return { exists: true, source: "localStorage" }; - } - - if (walletState.channelOpen) { - console.log("Channel is open according to walletStore"); - return { exists: true, source: "walletStore" }; - } - - console.log("No existing channel found"); - return { exists: false }; - }, [walletState.channelOpen]); - - /** - * Save channel state to localStorage - */ - const saveChannelToStorage = useCallback((state: State, channelId: string) => { - try { - const stateData = JSON.stringify(state, (_, value) => (typeof value === "bigint" ? value.toString() + "n" : value)); - - localStorage.setItem(STORAGE_KEYS.CHANNEL_STATE, stateData); - localStorage.setItem(STORAGE_KEYS.CHANNEL_ID, channelId); - - console.log("Saved channel data to localStorage"); - } catch (error) { - console.error("Failed to save channel to localStorage:", error); - } - }, []); - - /** - * Create a new Nitrolite channel - */ - const createChannel = useCallback( - async (tokenAddress: Hex, amount: string) => { - setIsLoading(true); - setError(null); - - try { - const existingChannel = await checkForExistingChannel(); - - if (existingChannel.exists) { - const source = existingChannel.source; - let message = "Cannot create a new channel because one already exists."; - - if (source === "accountChannels") { - message += " You have active channel(s). Please close existing channels before creating a new one."; - } else { - message += " Please close the existing channel before creating a new one."; - } - - setError(message); - throw new Error(message); - } - - const nitroliteState = NitroliteStore.state.getState(); - console.log("NitroliteStore state:", { - isInitialized: nitroliteState.isInitialized, - hasClient: !!nitroliteState.client, - }); - - const client = nitroliteState.client; - if (!client) { - console.error("Nitrolite client not initialized - client is null in store"); - throw new Error("Nitrolite client not initialized"); - } - - // Log client methods - console.log("Available client methods:", Object.keys(client)); - - const amountBigInt = parseTokenUnits(tokenAddress, amount); - const result = await client.createChannel(USDC_ADDRESS, { - initialAllocationAmounts: [amountBigInt, BigInt(0)], - stateData: EMPTY_STATE_DATA, - }); - - saveChannelToStorage(result.initialState, result.channelId); - WalletStore.setChannelOpen(true); - - if (setNitroliteChannel && result) { - setNitroliteChannel(result as any); - } - - return result; - } catch (error) { - console.error("Error creating channel:", error); - setError(error instanceof Error ? error.message : String(error)); - WalletStore.setChannelOpen(false); - throw error; - } finally { - setIsLoading(false); - } - }, - [checkForExistingChannel, saveChannelToStorage, setNitroliteChannel] - ); - - /** - * Deposit to a channel - */ - const depositToChannel = useCallback(async (tokenAddress: Address, amount: string) => { - setIsLoading(true); - setError(null); - - try { - const nitroliteState = NitroliteStore.state.getState(); - console.log("[depositToChannel] NitroliteStore state:", { - isInitialized: nitroliteState.isInitialized, - hasClient: !!nitroliteState.client, - }); - - const client = nitroliteState.client; - if (!client) { - console.error("[depositToChannel] Nitrolite client not initialized - client is null in store"); - throw new Error("Nitrolite client not initialized"); - } - - // Log client methods for deposit - console.log("[depositToChannel] Available client methods:", Object.keys(client)); - - const amountBigInt = typeof amount === "string" && !amount.startsWith("0x") ? parseTokenUnits(tokenAddress, amount) : BigInt(amount); - - await client.deposit(USDC_ADDRESS, amountBigInt); - WalletStore.openChannel(tokenAddress, amountBigInt.toString()); - - return true; - } catch (depositError) { - let errorMessage = "Deposit failed"; - - if (String(depositError).includes("approve") && String(depositError).includes("not been authorized")) { - errorMessage = "Token approval was rejected. Please approve the USDC spend in your wallet to proceed."; - } else if (String(depositError).includes("user rejected transaction")) { - errorMessage = "Transaction was rejected. Please confirm the transaction in your wallet."; - } else { - errorMessage = `Deposit error: ${depositError}`; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }, []); - - /** - * Clear stored channel data - */ - const clearStoredChannel = useCallback(() => { - try { - localStorage.removeItem(STORAGE_KEYS.CHANNEL); - localStorage.removeItem(STORAGE_KEYS.CHANNEL_STATE); - localStorage.removeItem(STORAGE_KEYS.CHANNEL_ID); - WalletStore.closeChannel(); - - console.log("Cleared channel data from localStorage"); - } catch (error) { - console.error("Failed to clear channel data from localStorage:", error); - } - }, []); - - return { - createChannel, - depositToChannel, - clearStoredChannel, - currentChannel: currentNitroliteChannel, - isChannelOpen: walletState.channelOpen, - isLoading, - error, - }; -} diff --git a/examples/viper-duel/client/src/hooks/useGameState.ts b/examples/viper-duel/client/src/hooks/useGameState.ts deleted file mode 100644 index 81e48ec9c..000000000 --- a/examples/viper-duel/client/src/hooks/useGameState.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import type { - GameState, - GameOver, - WebSocketMessages, - AppSessionSignatureRequestMessage, - AppSessionStartGameRequestMessage -} from '../types'; -import { useAppSessionSignature } from './useAppSessionSignature'; - -// Initial empty game state -const INITIAL_GAME_STATE: GameState = { - roomId: '', - snakes: { - player1: { - body: [], - direction: 'RIGHT', - alive: false, - score: 0 - }, - player2: { - body: [], - direction: 'LEFT', - alive: false, - score: 0 - } - }, - food: [], - players: { player1: '', player2: '' }, - gameTime: 0, - betAmount: 0 -}; - -// Game state hook that processes WebSocket messages -export function useGameState( - lastMessage: WebSocketMessages | null, - eoaAddress: string, - sendAppSessionSignature?: (roomId: string, signature: string) => void, - sendAppSessionStartGame?: (roomId: string, signature: string) => void -) { - // Game state - const [gameState, setGameState] = useState(INITIAL_GAME_STATE); - const [gameOver, setGameOver] = useState(null); - const [roomId, setRoomId] = useState(''); - const [errorMessage, setErrorMessage] = useState(null); - const [isRoomReady, setIsRoomReady] = useState(false); - const [isGameStarted, setIsGameStarted] = useState(false); - const [isHost, setIsHost] = useState(false); - const [pendingSignatureRequest, setPendingSignatureRequest] = useState(null); - const [awaitingHostStart, setAwaitingHostStart] = useState(false); - - // App session signature handling - const { - isSigningInProgress, - signatureError, - handleParticipantBSignature, - handleParticipantASignature - } = useAppSessionSignature(sendAppSessionSignature, sendAppSessionStartGame); - - // Determine player's role (player1 or player2) - const playerId = gameState.players.player1 === eoaAddress ? 'player1' : - gameState.players.player2 === eoaAddress ? 'player2' : null; - - // We don't need to generate room IDs client-side anymore - // The server handles room creation - - // Process WebSocket messages to update game state - useEffect(() => { - if (!lastMessage) return; - - console.log("Received WebSocket message:", lastMessage.type, lastMessage); - - switch (lastMessage.type) { - case 'room:created': - console.log("Room created:", lastMessage.roomId, "role:", lastMessage.role); - setRoomId(lastMessage.roomId); - - // Set host status based on role - if (lastMessage.role === 'host') { - console.log("Player is host"); - setIsHost(true); - } else { - console.log("Player is guest"); - setIsHost(false); - } - - setErrorMessage(null); - break; - - case 'room:state': - console.log("Received room:state", lastMessage, "eoaAddress:", eoaAddress); - - setGameState({ - roomId: lastMessage.roomId, - snakes: lastMessage.snakes, - food: lastMessage.food, - players: lastMessage.players, - gameTime: lastMessage.gameTime, - betAmount: lastMessage.betAmount || 0 - }); - - // Set host status based on player role (player1 is always host) - if (lastMessage.players.player1 === eoaAddress) { - console.log("Player is host (player1)"); - setIsHost(true); - } else { - console.log("Player is guest (player2)"); - setIsHost(false); - } - - // Always update room ID when we get a room:state message - if (lastMessage.roomId) { - setRoomId(lastMessage.roomId); - } - - setErrorMessage(null); - break; - - case 'room:ready': - setRoomId(lastMessage.roomId); - setIsRoomReady(true); - setErrorMessage(null); - break; - - case 'game:started': - setIsGameStarted(true); - setErrorMessage(null); - break; - - case 'game:update': - console.log("🐍 GAME UPDATE:", { - gameTime: lastMessage.gameTime, - player1Pos: lastMessage.snakes?.player1?.body?.[0], - player2Pos: lastMessage.snakes?.player2?.body?.[0], - player1Alive: lastMessage.snakes?.player1?.alive, - player2Alive: lastMessage.snakes?.player2?.alive - }); - setGameState({ - roomId: lastMessage.roomId, - snakes: lastMessage.snakes, - food: lastMessage.food, - players: lastMessage.players, - gameTime: lastMessage.gameTime, - betAmount: lastMessage.betAmount || 0 - }); - setErrorMessage(null); - break; - - case 'game:over': - setGameOver({ - winner: lastMessage.winner, - finalScores: lastMessage.finalScores, - gameTime: lastMessage.gameTime - }); - setErrorMessage(null); - break; - - case 'appSession:signatureRequest': - console.log("Received signature request for participant B:", lastMessage); - setPendingSignatureRequest(lastMessage as AppSessionSignatureRequestMessage); - - // Automatically sign for participant B (guest) - if (!isHost) { - try { - handleParticipantBSignature(lastMessage as AppSessionSignatureRequestMessage); - } catch (error) { - console.error('Failed to handle participant B signature:', error); - setErrorMessage('Failed to sign app session message'); - } - } - break; - - case 'appSession:startGameRequest': - console.log("Received start game request for participant A (host):", lastMessage); - setPendingSignatureRequest(lastMessage as AppSessionStartGameRequestMessage); - setAwaitingHostStart(true); - break; - - case 'appSession:signatureConfirmed': - console.log("App session signature confirmed:", lastMessage); - setPendingSignatureRequest(null); - setErrorMessage(null); - break; - - case 'error': - setErrorMessage(lastMessage.msg); - break; - - default: - // Ignore unknown message types - break; - } - }, [lastMessage, eoaAddress, handleParticipantBSignature, isHost]); - - // Helper to format short address display - const formatShortAddress = (address: string): string => { - if (!address) return ''; - return `${address.slice(0, 6)}...${address.slice(-4)}`; - }; - - // Get opponent's address - const getOpponentAddress = (): string => { - if (!playerId) return ''; - return playerId === 'player1' ? gameState.players.player2 : gameState.players.player1; - }; - - // Handle host signing and starting game - const signAndStartGame = useCallback(async () => { - if (!pendingSignatureRequest || pendingSignatureRequest.type !== 'appSession:startGameRequest') { - console.error('No pending start game request'); - return; - } - - try { - await handleParticipantASignature(pendingSignatureRequest as AppSessionStartGameRequestMessage); - setPendingSignatureRequest(null); - setAwaitingHostStart(false); - } catch (error) { - console.error('Failed to sign and start game:', error); - setErrorMessage('Failed to sign and start game'); - } - }, [pendingSignatureRequest, handleParticipantASignature]); - - // Reset game state - const resetGame = useCallback(() => { - setGameState(INITIAL_GAME_STATE); - setGameOver(null); - setIsRoomReady(false); - setIsGameStarted(false); - setIsHost(false); - setRoomId(''); - setErrorMessage(null); - setPendingSignatureRequest(null); - setAwaitingHostStart(false); - }, []); - - // TODO: Add integration with @erc7824/nitrolite for persisting game state - - return { - gameState, - gameOver, - roomId, - errorMessage, - isRoomReady, - isGameStarted, - isHost, - playerId, - formatShortAddress, - getOpponentAddress, - resetGame, - pendingSignatureRequest, - awaitingHostStart, - signAndStartGame, - isSigningInProgress, - signatureError - }; -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/hooks/useMetaMask.ts b/examples/viper-duel/client/src/hooks/useMetaMask.ts deleted file mode 100644 index 531943290..000000000 --- a/examples/viper-duel/client/src/hooks/useMetaMask.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { useState, useEffect } from 'react'; -import { type Address } from 'viem'; -import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit'; -import { useAccount, useConnect, useSwitchChain } from 'wagmi'; -import { SettingsStore, WalletStore } from '../store'; -import { useStore } from '../store/storeUtils'; - -// Storage key for wallet connection -const WALLET_CONNECTION_KEY = 'wallet_connection'; - -export function useMetaMask() { - const [isMetaMaskInstalled, setIsMetaMaskInstalled] = - useState(false); - const walletState = useStore(WalletStore.state); - const settingsState = useStore(SettingsStore.state); - - const { openConnectModal } = useConnectModal(); - const { openAccountModal } = useAccountModal(); - - const { address, isConnected, isConnecting, chain } = useAccount(); - const { connect, connectors } = useConnect(); - const { switchChain } = useSwitchChain(); - - useEffect(() => { - if (typeof window !== 'undefined') { - setIsMetaMaskInstalled(!!window.ethereum?.isMetaMask); - } - }, []); - - useEffect(() => { - if (isConnected && address) { - if ( - !walletState.isConnected || - walletState.walletAddress !== address - ) { - WalletStore.connect(address as Address); - localStorage.setItem(WALLET_CONNECTION_KEY, 'true'); - } - - if (chain && walletState.chainId !== chain.id) { - WalletStore.setChainId(chain.id); - } - } else if (!isConnected && walletState.isConnected) { - localStorage.removeItem(WALLET_CONNECTION_KEY); - } - }, [ - isConnected, - address, - chain, - walletState.isConnected, - walletState.walletAddress, - walletState.chainId, - ]); - - // Connect using RainbowKit - const connectWallet = async () => { - try { - // Find MetaMask connector - const metamaskConnector = connectors.find((connector) => - connector.name.toLowerCase().includes('metamask') - ); - - if (metamaskConnector) { - connect({ connector: metamaskConnector }); - } else { - openConnectModal?.(); - } - } catch (error) { - console.error('Error connecting wallet:', error); - WalletStore.setError('Failed to connect wallet'); - } - }; - - const switchNetwork = async (chainId: number) => { - try { - await switchChain({ chainId }); - } catch (error) { - console.error('Error switching network:', error); - WalletStore.setError('Failed to switch network'); - } - }; - - useEffect(() => { - if ( - settingsState.activeChain && - chain && - settingsState.activeChain !== chain.id && - isConnected - ) { - WalletStore.setChainId(chain.id); - } - }, [settingsState.activeChain, chain, isConnected]); - - return { - isMetaMaskInstalled, - isConnected: walletState.isConnected, - account: walletState.walletAddress, - address: walletState.walletAddress, - chainId: walletState.chainId, - error: walletState.error, - connect: connectWallet, - connectWallet, - openConnectModal, - openAccountModal, - switchNetwork, - isConnecting, - }; -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/hooks/useNitroliteIntegration.ts b/examples/viper-duel/client/src/hooks/useNitroliteIntegration.ts deleted file mode 100644 index db16077e4..000000000 --- a/examples/viper-duel/client/src/hooks/useNitroliteIntegration.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { NitroliteStore, WalletStore } from '../store'; -import { useStore } from '../store/storeUtils'; -import { useWebSocketContext } from '../context/WebSocketContext'; -import { useChannel } from './useChannel'; - -/** - * This hook integrates the WebSocket connections with Nitrolite channels - * It should be used at app level to handle WebSocket and Channel connections - */ -export function useNitroliteIntegration() { - const { status, isConnected } = useWebSocketContext(); - const walletState = useStore(WalletStore.state); - const nitroliteState = useStore(NitroliteStore.state); - const { currentChannel, clearStoredChannel } = useChannel(); - - /** - * Handle recovered or existing channels - */ - useEffect(() => { - // If we're connected to WebSocket and have a client but no channel, try to recover it - if (isConnected && nitroliteState.client && !currentChannel) { - const channelId = localStorage.getItem('nitrolite_channel_id'); - const channelState = localStorage.getItem('nitrolite_channel_state'); - - if (channelId && channelState) { - try { - // This is a simplified version - in a real app you would parse the state and reconnect - console.log('Found saved channel, should reconnect:', channelId); - - // Mark as having an open channel - WalletStore.setChannelOpen(true); - } catch (error) { - console.error('Failed to recover channel:', error); - clearStoredChannel(); - } - } else { - console.log('No existing channel data found in localStorage'); - // Ensure channels are marked as closed if no data exists - WalletStore.setChannelOpen(false); - } - } - }, [isConnected, nitroliteState.client, currentChannel, clearStoredChannel]); - - /** - * Initialize the Nitrolite client when necessary - */ - const initializeNitroliteClient = useCallback(async (clientInstance: any) => { - NitroliteStore.setClient(clientInstance); - }, []); - - /** - * Handle WebSocket disconnection - */ - useEffect(() => { - if (status === 'disconnected' && walletState.channelOpen) { - // Websocket was disconnected but we have an open channel - // This is just logging for now, but you could implement reconnection logic - console.log('WebSocket disconnected while channel is open - should attempt to reconnect'); - } - }, [status, walletState.channelOpen]); - - return { - wsStatus: status, - isWsConnected: isConnected, - hasOpenChannel: walletState.channelOpen, - currentChannelId: currentChannel ? JSON.stringify(currentChannel).substring(0, 20) : null, - initializeNitroliteClient - }; -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/hooks/useSoundEffects.ts b/examples/viper-duel/client/src/hooks/useSoundEffects.ts deleted file mode 100644 index 432ede10a..000000000 --- a/examples/viper-duel/client/src/hooks/useSoundEffects.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -// Define sound types for type safety -type SoundType = 'mark-x' | 'mark-o' | 'game-over' | 'win' | 'draw'; - -export function useSoundEffects() { - const [loaded, setLoaded] = useState(false); - const audioContext = useRef(null); - const buffers = useRef>(new Map()); - - // Initialize audio context and load sounds - useEffect(() => { - // Create audio context - const ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); - audioContext.current = ctx; - - // Define the sounds to preload - // const soundsToLoad: Array<{ type: SoundType; url: string }> = [ - // // These URLs would point to your actual sound files - // { type: 'mark-x', url: '/sounds/mark-x.mp3' }, - // { type: 'mark-o', url: '/sounds/mark-o.mp3' }, - // { type: 'game-over', url: '/sounds/game-over.mp3' }, - // { type: 'win', url: '/sounds/win.mp3' }, - // { type: 'draw', url: '/sounds/draw.mp3' }, - // ]; - - // Instead of loading real files, we'll generate audio buffers programmatically - // This is a fallback for development when sound files might not exist - const generateToneBuffer = (frequency: number, duration: number, fadeOut = true): AudioBuffer => { - const sampleRate = ctx.sampleRate; - const buffer = ctx.createBuffer(2, sampleRate * duration, sampleRate); - - // Generate sound data for both channels - for (let channel = 0; channel < 2; channel++) { - const channelData = buffer.getChannelData(channel); - - for (let i = 0; i < channelData.length; i++) { - // Basic sine wave - const t = i / sampleRate; - let value = Math.sin(2 * Math.PI * frequency * t); - - // Add a bit of randomness for more texture - const noise = Math.random() * 0.05; - value = value * 0.5 + value * noise; - - // Apply fade out if needed - if (fadeOut && t > duration * 0.7) { - const fadePosition = (t - duration * 0.7) / (duration * 0.3); - value *= 1 - fadePosition; - } - - channelData[i] = value; - } - } - - return buffer; - }; - - // Generate different sounds for different actions - const xMarkBuffer = generateToneBuffer(880, 0.3); // Higher pitch for X - const oMarkBuffer = generateToneBuffer(440, 0.3); // Lower pitch for O - const gameOverBuffer = generateToneBuffer(220, 1, true); // Low tone for game over - const winBuffer = generateToneBuffer(880, 0.8, true); // Celebratory tone - const drawBuffer = generateToneBuffer(440, 0.5, true); // Neutral tone - - // Store the buffers - buffers.current.set('mark-x', xMarkBuffer); - buffers.current.set('mark-o', oMarkBuffer); - buffers.current.set('game-over', gameOverBuffer); - buffers.current.set('win', winBuffer); - buffers.current.set('draw', drawBuffer); - - setLoaded(true); - - // Clean up - return () => { - if (audioContext.current && audioContext.current.state !== 'closed') { - audioContext.current.close(); - } - }; - }, []); - - // Function to play a sound - const playSound = (type: SoundType, volume = 0.5) => { - if (!loaded || !audioContext.current) return; - - const buffer = buffers.current.get(type); - if (!buffer) return; - - // Create source and gain nodes - const source = audioContext.current.createBufferSource(); - const gainNode = audioContext.current.createGain(); - - // Connect nodes - source.buffer = buffer; - source.connect(gainNode); - gainNode.connect(audioContext.current.destination); - - // Set volume - gainNode.gain.value = volume; - - // Play the sound - source.start(); - }; - - return { playSound, loaded }; -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/hooks/useWebSocket.ts b/examples/viper-duel/client/src/hooks/useWebSocket.ts deleted file mode 100644 index 7da9330c6..000000000 --- a/examples/viper-duel/client/src/hooks/useWebSocket.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from "react"; -import type { WebSocketMessages, JoinRoomPayload, DirectionPayload } from "../types"; - -// WebSocket hook for connecting to the game server -export function useWebSocket() { - const [isConnected, setIsConnected] = useState(false); - const [error, setError] = useState(null); - const webSocketRef = useRef(null); - const [lastMessage, setLastMessage] = useState(null); - - // WebSocket server URL (use environment variable if available) - const wsUrl = import.meta.env.VITE_WS_URL || "ws://localhost:8080"; - - // Initialize WebSocket connection - useEffect(() => { - const webSocket = new WebSocket(wsUrl); - - webSocket.onopen = () => { - setIsConnected(true); - setError(null); - }; - - webSocket.onclose = () => { - setIsConnected(false); - }; - - webSocket.onerror = () => { - setError("Failed to connect to game server"); - setIsConnected(false); - }; - - webSocket.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - - setLastMessage(message as WebSocketMessages); - } catch (err) { - console.error("Error parsing WebSocket message", err); - } - }; - - webSocketRef.current = webSocket; - - // Cleanup on unmount - return () => { - webSocket.close(); - }; - }, [wsUrl]); - - // Send a message to the server - const sendMessage = useCallback( - (message: object) => { - if (webSocketRef.current && isConnected) { - webSocketRef.current.send(JSON.stringify(message)); - } else { - setError("Not connected to server"); - } - }, - [isConnected] - ); - - // Join a room - const joinRoom = useCallback( - (payload: JoinRoomPayload) => { - sendMessage({ - type: "joinRoom", - payload, - }); - }, - [sendMessage] - ); - - // Change snake direction - const changeDirection = useCallback( - (payload: DirectionPayload) => { - sendMessage({ - type: "changeDirection", - payload, - }); - }, - [sendMessage] - ); - - // Start the game (host only) - const startGame = useCallback( - (roomId: string) => { - sendMessage({ - type: "startGame", - payload: { roomId }, - }); - }, - [sendMessage] - ); - - // Get available rooms - const getAvailableRooms = useCallback(() => { - sendMessage({ - type: "getAvailableRooms", - }); - }, [sendMessage]); - - // Send app session signature - const sendAppSessionSignature = useCallback( - (roomId: string, signature: string) => { - sendMessage({ - type: "appSession:signature", - payload: { roomId, signature }, - }); - }, - [sendMessage] - ); - - // Send app session start game with signature - const sendAppSessionStartGame = useCallback( - (roomId: string, signature: string) => { - sendMessage({ - type: "appSession:startGame", - payload: { roomId, signature }, - }); - }, - [sendMessage] - ); - - return { - isConnected, - error, - lastMessage, - joinRoom, - changeDirection, - startGame, - getAvailableRooms, - sendAppSessionSignature, - sendAppSessionStartGame, - }; -} diff --git a/examples/viper-duel/client/src/hooks/useWebSocketNitrolite.ts b/examples/viper-duel/client/src/hooks/useWebSocketNitrolite.ts deleted file mode 100644 index 21c6331dd..000000000 --- a/examples/viper-duel/client/src/hooks/useWebSocketNitrolite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useWebSocketContext } from "../context/WebSocketContext"; - -/** - * Custom hook to manage WebSocket connection and operations using WebSocketContext. - * This hook provides a simple interface to the WebSocket functionality - * without exposing the implementation details. - */ -export function useWebSocketNitrolite() { - const context = useWebSocketContext(); - - return { - // Connection status - status: context.status, - isConnected: context.isConnected, - - // Key management - keyPair: context.keyPair, - hasKeys: context.hasKeys, - generateKeys: context.generateKeys, - clearKeys: context.clearKeys, - - // Channel management - wsChannel: context.wsChannel, - currentNitroliteChannel: context.currentNitroliteChannel, - setNitroliteChannel: context.setNitroliteChannel, - - // Connection management - connect: context.connect, - disconnect: context.disconnect, - - // Message handling - sendPing: context.sendPing, - sendRequest: context.sendRequest, - }; -} diff --git a/examples/viper-duel/client/src/hooks/utils/tokenDecimals.ts b/examples/viper-duel/client/src/hooks/utils/tokenDecimals.ts deleted file mode 100644 index 34675e1d6..000000000 --- a/examples/viper-duel/client/src/hooks/utils/tokenDecimals.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { Address } from 'viem'; - -// Common token decimals -const TOKEN_DECIMALS: Record = { - // USDC on Polygon - '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359': 6, - // USDC on Ethereum - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 6, - // USDC on most testnets (including Goerli) - '0x07865c6e87b9f70255377e024ace6630c1eaa37f': 6, - // Add more tokens as needed -}; - -// Default decimal places if token not found -const DEFAULT_DECIMALS = 18; - -/** - * Get the number of decimal places for a token - * @param tokenAddress The token address - * @returns The number of decimal places - */ -export function getTokenDecimals(tokenAddress: Address): number { - if (!tokenAddress) { - console.warn('Token address is empty, using default decimals'); - return DEFAULT_DECIMALS; - } - - const normalizedAddress = tokenAddress.toLowerCase(); - const decimals = TOKEN_DECIMALS[normalizedAddress]; - - if (decimals !== undefined) { - return decimals; - } - - // If the token appears to be USDC based on its address - if (normalizedAddress.includes('usdc')) { - console.log(`Token address ${tokenAddress} appears to be USDC, using 6 decimals`); - return 6; - } - - console.log(`Token decimals not found for ${tokenAddress}, using default ${DEFAULT_DECIMALS}`); - return DEFAULT_DECIMALS; -} - -/** - * Parse a token amount from human-readable to contract units - * @param tokenAddress The token address - * @param amount The amount as a string - * @returns The amount in BigInt - */ -export function parseTokenUnits(tokenAddress: Address, amount: string): bigint { - try { - const decimals = getTokenDecimals(tokenAddress); - - // Handle empty or invalid amounts - if (!amount || amount === '' || isNaN(Number(amount))) { - console.warn(`Invalid amount provided: "${amount}", defaulting to 0`); - return BigInt(0); - } - - // Handle numbers with decimal points - const parts = amount.split('.'); - const wholePart = parts[0] || '0'; - let fractionalPart = parts[1] || ''; - - // Pad or truncate fractional part to match decimals - if (fractionalPart.length > decimals) { - fractionalPart = fractionalPart.substring(0, decimals); - } else { - while (fractionalPart.length < decimals) { - fractionalPart += '0'; - } - } - - // Remove leading zeros from whole part - const wholePartWithoutLeadingZeros = wholePart.replace(/^0+/, '') || '0'; - - // Combine parts without decimal point - const combinedString = wholePartWithoutLeadingZeros + fractionalPart; - - // Convert to BigInt - return BigInt(combinedString); - } catch (error) { - console.error(`Error parsing token units:`, error); - // Return 0 as a fallback to prevent crashes - return BigInt(0); - } -} - -/** - * Format a token amount from contract units to human-readable - * @param tokenAddress The token address - * @param amountBigInt The amount as a BigInt - * @returns The formatted amount as a string - */ -export function formatTokenUnits(tokenAddress: Address, amountBigInt: bigint): string { - try { - const decimals = getTokenDecimals(tokenAddress); - - // Handle zero amount - if (amountBigInt === BigInt(0)) { - return '0'; - } - - // Convert to string and pad with leading zeros if needed - let amountStr = amountBigInt.toString(); - while (amountStr.length <= decimals) { - amountStr = '0' + amountStr; - } - - // Split into whole and fractional parts - const wholePart = amountStr.slice(0, -decimals) || '0'; - const fractionalPart = amountStr.slice(-decimals); - - // Format with decimal point and trim trailing zeros - let result = `${wholePart}.${fractionalPart}`; - - // Remove trailing zeros after decimal point - result = result.replace(/\.?0+$/, ''); - if (result.endsWith('.')) { - result = result.slice(0, -1); - } - - return result || '0'; - } catch (error) { - console.error(`Error formatting token units:`, error); - // Return '0' as a fallback - return '0'; - } -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/index.css b/examples/viper-duel/client/src/index.css deleted file mode 100644 index c4c6b3ebe..000000000 --- a/examples/viper-duel/client/src/index.css +++ /dev/null @@ -1,267 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - /* Shadcn Variables */ - --background: 225 71% 3%; - --foreground: 210 20% 98%; - --card: 224 71% 4%; - --card-foreground: 210 20% 98%; - --popover: 224 71% 4%; - --popover-foreground: 210 20% 98%; - --primary: 187 100% 50%; - --primary-foreground: 210 20% 98%; - --secondary: 300 100% 64%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 216 34% 17%; - --accent-foreground: 210 20% 98%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 20% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 187 100% 50%; - --radius: 0.5rem; - - /* Viper Duel Brand Color Palette */ - --color-green: #2AFF6B; - --color-green-dark: #1ACC53; - --color-purple: #B425FF; - --color-purple-dark: #8B1ACC; - --color-charcoal: #0E0F11; - --color-charcoal-light: #1A1C1F; - --color-yellow: #FFED00; - --color-grey: #9DA0A5; - } -} - -/* Base styles */ -@layer base { - html, body { - min-height: 100vh; - overflow-x: hidden; - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - color-scheme: dark; - color: rgba(255, 255, 255, 0.95); - background-color: var(--color-charcoal); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - body { - margin: 0; - display: flex; - min-width: 320px; - /* Enhanced radial gradient with brand colors */ - background: radial-gradient(circle at center, var(--color-charcoal-light) 0%, var(--color-charcoal) 100%); - position: relative; - } - - /* Neon grid pattern overlay with brand green */ - body::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: - linear-gradient(rgba(42, 255, 107, 0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(42, 255, 107, 0.03) 1px, transparent 1px); - background-size: 30px 30px; - z-index: -1; - } -} - -/* Viper Duel brand components */ -@layer components { - /* Text styles with glow effects */ - .text-glow-green { - @apply text-[#2AFF6B]; - text-shadow: 0 0 8px rgba(42, 255, 107, 0.8); - } - - .text-glow-purple { - @apply text-[#B425FF]; - text-shadow: 0 0 8px rgba(180, 37, 255, 0.8); - } - - .text-glow-yellow { - @apply text-[#FFED00]; - text-shadow: 0 0 8px rgba(255, 237, 0, 0.8); - } - - /* Glowing Snake elements */ - .snake-player1 { - text-shadow: 0 0 10px #2AFF6B, 0 0 20px #2AFF6B50, 0 0 30px #2AFF6B30; - position: relative; - } - - .snake-player2 { - text-shadow: 0 0 10px #B425FF, 0 0 20px #B425FF50, 0 0 30px #B425FF30; - position: relative; - } - - /* Grid pattern background */ - .bg-grid-pattern { - background-image: - linear-gradient(to right, var(--color-green) 1px, transparent 1px), - linear-gradient(to bottom, var(--color-green) 1px, transparent 1px); - background-size: 20px 20px; - opacity: 0.05; - } - - /* Glass panel effect */ - .glass-panel { - @apply bg-gray-900/30 backdrop-blur-md border border-gray-800/40 rounded-xl; - } - - /* Glow effects */ - .glow-green { - box-shadow: 0 0 15px 5px rgba(42, 255, 107, 0.15); - } - - .glow-purple { - box-shadow: 0 0 15px 5px rgba(180, 37, 255, 0.15); - } - - .glow-yellow { - box-shadow: 0 0 15px 5px rgba(255, 237, 0, 0.15); - } - - /* Aura effects for snake segments */ - .aura-green { - position: relative; - } - - .aura-green::before { - content: ""; - position: absolute; - inset: -5px; - border-radius: 8px; - background: radial-gradient(circle at center, rgba(42, 255, 107, 0.2) 0%, transparent 70%); - opacity: 0.5; - z-index: -1; - } - - .aura-purple { - position: relative; - } - - .aura-purple::before { - content: ""; - position: absolute; - inset: -5px; - border-radius: 8px; - background: radial-gradient(circle at center, rgba(180, 37, 255, 0.2) 0%, transparent 70%); - opacity: 0.5; - z-index: -1; - } - - /* Text shadow utilities for elements */ - .text-shadow-green { - text-shadow: 0 0 10px #2AFF6B, 0 0 20px #2AFF6B50, 0 0 30px #2AFF6B30; - } - - .text-shadow-purple { - text-shadow: 0 0 10px #B425FF, 0 0 20px #B425FF50, 0 0 30px #B425FF30; - } - - .text-shadow-yellow { - text-shadow: 0 0 10px #FFED00, 0 0 20px #FFED0050, 0 0 30px #FFED0030; - } - - /* Enhanced button styling */ - .button-viper { - position: relative; - overflow: hidden; - border-radius: 0.75rem; - transition: all 0.3s ease; - } - - .button-viper::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.7s ease; - } - - .button-viper:hover::before { - left: 100%; - } - - /* Improved logo styling */ - .logo-enhanced { - filter: drop-shadow(0 0 20px rgba(42, 255, 107, 0.3)) - drop-shadow(0 0 40px rgba(180, 37, 255, 0.2)); - } -} - -/* Game-specific particles effect */ -.particles { - position: absolute; - width: 100%; - height: 100%; - background-image: radial-gradient(circle, #ffffff 1px, transparent 1px); - background-size: 16px 16px; - opacity: 0.05; - mix-blend-mode: screen; - animation: float 8s infinite linear; - pointer-events: none; -} - -/* Improved pulse animation for snake segments */ -@keyframes pulse-green { - 0% { box-shadow: 0 0 10px rgba(42, 255, 107, 0.5); } - 50% { box-shadow: 0 0 20px rgba(42, 255, 107, 0.7), 0 0 30px rgba(42, 255, 107, 0.3); } - 100% { box-shadow: 0 0 10px rgba(42, 255, 107, 0.5); } -} - -@keyframes pulse-purple { - 0% { box-shadow: 0 0 10px rgba(180, 37, 255, 0.5); } - 50% { box-shadow: 0 0 20px rgba(180, 37, 255, 0.7), 0 0 30px rgba(180, 37, 255, 0.3); } - 100% { box-shadow: 0 0 10px rgba(180, 37, 255, 0.5); } -} - -@keyframes pulse-yellow { - 0% { box-shadow: 0 0 10px rgba(255, 237, 0, 0.5); } - 50% { box-shadow: 0 0 20px rgba(255, 237, 0, 0.7), 0 0 30px rgba(255, 237, 0, 0.3); } - 100% { box-shadow: 0 0 10px rgba(255, 237, 0, 0.5); } -} - -@keyframes float { - 0% { background-position: 0 0; } - 100% { background-position: 100px 100px; } -} - -@keyframes sparkle { - 0% { transform: translateY(0) rotate(0deg); } - 100% { transform: translateY(-100px) rotate(20deg); } -} - -@keyframes pulse { - 0% { opacity: 0.8; } - 50% { opacity: 1; } - 100% { opacity: 0.8; } -} - -@keyframes shimmer { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } -} - -@keyframes fadeIn { - 0% { opacity: 0; } - 100% { opacity: 1; } -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/lib/utils.ts b/examples/viper-duel/client/src/lib/utils.ts deleted file mode 100644 index d1f96aa04..000000000 --- a/examples/viper-duel/client/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)); -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/main.tsx b/examples/viper-duel/client/src/main.tsx deleted file mode 100644 index 495f17866..000000000 --- a/examples/viper-duel/client/src/main.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import '@rainbow-me/rainbowkit/styles.css'; -import App from './App.tsx'; -import { NitroliteClientWrapper } from './context/NitroliteClientWrapper.tsx'; -import { WebSocketProvider } from './context/WebSocketContext.tsx'; -import { RainbowKitConnectProvider } from './providers/RainbowkitConnectProvider.tsx'; - -createRoot(document.getElementById('root')!).render( - - - - - - - - - -); diff --git a/examples/viper-duel/client/src/providers/RainbowkitConnectProvider.tsx b/examples/viper-duel/client/src/providers/RainbowkitConnectProvider.tsx deleted file mode 100644 index c8fb4b5a4..000000000 --- a/examples/viper-duel/client/src/providers/RainbowkitConnectProvider.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { lightTheme, RainbowKitProvider, type AvatarComponent } from "@rainbow-me/rainbowkit"; -import { WagmiProvider } from "wagmi"; -import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; -import { connectorsForWallets } from "@rainbow-me/rainbowkit"; -import { createConfig, http } from "wagmi"; -import { polygon } from "viem/chains"; -import { metaMaskWallet } from "@rainbow-me/rainbowkit/wallets"; - -const connectors = connectorsForWallets( - [ - { - groupName: "Recommended", - wallets: [metaMaskWallet], - }, - ], - { - appName: "Viper Duel", - projectId: import.meta.env?.NEXT_PUBLIC_PROJECT_ID ?? "7e477e0ec531e636b5ab844fff3db798", - } -); - -export const rainbowkitConfig = createConfig({ - chains: [polygon], - transports: { - [polygon.id]: http(), - }, - connectors, - ssr: true, -}); - -const CustomAvatar: AvatarComponent = () => { - return null; -}; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, - }, - }, -}); - -interface IRainbowKitConnectProvider { - children: React.ReactNode; -} - -export const RainbowKitConnectProvider: React.FC = ({ children }: IRainbowKitConnectProvider) => { - return ( - - - - {children} - - - - ); -}; diff --git a/examples/viper-duel/client/src/store/index.ts b/examples/viper-duel/client/src/store/index.ts deleted file mode 100644 index a8990adfa..000000000 --- a/examples/viper-duel/client/src/store/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Store } from './storeUtils'; -import type { Address, WalletClient } from 'viem'; - -// Nitrolite Store -interface NitroliteStoreState { - client: any; // This would be NitroliteClient from the package - isInitialized: boolean; -} - -export const NitroliteStore = { - state: new Store({ - client: null, - isInitialized: false, - }), - - setClient(client: any): void { - this.state.set('client', client); - this.state.set('isInitialized', true); - }, -}; - -// Wallet Store -interface WalletStoreState { - walletAddress: string | null; - chainId: number | null; - isConnected: boolean; - channelOpen: boolean; - walletClient: WalletClient | null; - channelToken: Address | null; - channelAmount: string | null; - error: string | null; -} - -export const WalletStore = { - state: new Store({ - walletAddress: null, - chainId: null, - isConnected: false, - channelOpen: false, - walletClient: null, - channelToken: null, - channelAmount: null, - error: null, - }), - - connect(address: Address): void { - this.state.setState({ - walletAddress: address, - isConnected: true, - error: null, - }); - }, - - setError(error: string | null): void { - this.state.set('error', error); - }, - - setWalletAddress(address: string | null): void { - this.state.set('walletAddress', address); - this.state.set('isConnected', !!address); - }, - - setChainId(chainId: number | null): void { - this.state.set('chainId', chainId); - }, - - setChannelOpen(isOpen: boolean): void { - this.state.set('channelOpen', isOpen); - }, - - openChannel(token: Address, amount: string): void { - this.state.setState({ - channelOpen: true, - channelToken: token, - channelAmount: amount, - }); - }, - - /** - * Set wallet client - * @param walletClient Wallet client instance - */ - setWalletClient(walletClient: WalletClient | null): void { - this.state.set('walletClient', walletClient); - - if (walletClient?.account?.address) { - this.state.set('walletAddress', walletClient.account.address); - } - }, - - /** - * Get Wallet Client - */ - getWalletClient(): WalletClient | null { - return this.state.getState().walletClient; - }, - - closeChannel(): void { - this.state.setState({ - channelOpen: false, - channelToken: null, - channelAmount: null, - }); - }, -}; - -// Settings Store -interface SettingsStoreState { - activeChain: number; -} - -export const SettingsStore = { - state: new Store({ - activeChain: 137, // Default to Polygon - }), - - setActiveChain(chainId: number): void { - this.state.set('activeChain', chainId); - }, -}; diff --git a/examples/viper-duel/client/src/store/storeUtils.ts b/examples/viper-duel/client/src/store/storeUtils.ts deleted file mode 100644 index a28ef137f..000000000 --- a/examples/viper-duel/client/src/store/storeUtils.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useState, useEffect } from 'react'; - -/** - * Simple store implementation for state management - */ -export class Store { - private state: T; - private listeners: (() => void)[] = []; - - constructor(initialState: T) { - this.state = initialState; - } - - /** - * Get the current state - */ - getState(): T { - return this.state; - } - - /** - * Update the state - */ - setState(newState: Partial): void { - this.state = { ...this.state, ...newState }; - this.notifyListeners(); - } - - /** - * Set a specific key in the state - */ - set(key: K, value: T[K]): void { - this.state = { ...this.state, [key]: value }; - this.notifyListeners(); - } - - /** - * Subscribe to changes - */ - subscribe(listener: () => void): () => void { - this.listeners.push(listener); - return () => { - this.listeners = this.listeners.filter(l => l !== listener); - }; - } - - /** - * Notify all listeners of state changes - */ - private notifyListeners(): void { - this.listeners.forEach(listener => listener()); - } -} - -/** - * Custom hook to use a store - */ -export function useStore(store: Store): T { - const [state, setState] = useState(store.getState()); - - useEffect(() => { - const unsubscribe = store.subscribe(() => { - setState(store.getState()); - }); - return unsubscribe; - }, [store]); - - return state; -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/types/index.ts b/examples/viper-duel/client/src/types/index.ts deleted file mode 100644 index 92c9dd2b0..000000000 --- a/examples/viper-duel/client/src/types/index.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * Game and WebSocket types for Viper Duel - */ - -import type { CreateAppSessionRequest } from '@erc7824/nitrolite'; - -// Snake direction -export type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'; - -// Bet amounts (in USDC) -export type BetAmount = 0 | 0.01 | 0.1 | 1 | 2; - -// Bet option for UI display -export interface BetOption { - value: BetAmount; - label: string; - disabled?: boolean; -} - -// Position on the grid -export interface Position { - x: number; - y: number; -} - -// Snake data -export interface Snake { - body: Position[]; - direction: Direction; - alive: boolean; - score: number; -} - -// Players in the game -export interface Players { - player1: string; // EOA address of player 1 (host) - player2: string; // EOA address of player 2 (guest) -} - -// Game state from server -export interface GameState { - roomId: string; - snakes: { - player1: Snake; - player2: Snake; - }; - food: Position[]; - players: Players; - gameTime: number; - betAmount: BetAmount; -} - -// Game over state -export interface GameOver { - winner: string | null; // 'player1', 'player2', or null for tie - finalScores: { - player1: number; - player2: number; - }; - gameTime: number; -} - -// Room join payload -export interface JoinRoomPayload { - roomId?: string | undefined; // Explicitly marked as optional - eoa: string; - betAmount: BetAmount; -} - -// Direction change payload -export interface DirectionPayload { - roomId: string; - direction: Direction; -} - -// WebSocket message types -export type WebSocketMessageType = - | 'joinRoom' - | 'startGame' - | 'changeDirection' - | 'getAvailableRooms' - | 'room:state' - | 'room:ready' - | 'room:created' - | 'room:available' - | 'game:started' - | 'game:over' - | 'game:update' - | 'onlineUsers' - | 'players:count' - | 'error' - | 'appSession:signatureRequest' - | 'appSession:startGameRequest' - | 'appSession:signatureConfirmed' - | 'appSession:signature' - | 'appSession:startGame'; - -// Base WebSocket message -export interface WebSocketMessage { - type: WebSocketMessageType; -} - -// Client -> Server messages - -export interface JoinRoomMessage extends WebSocketMessage { - type: 'joinRoom'; - payload: JoinRoomPayload; -} - -export interface StartGamePayload { - roomId: string; -} - -export interface StartGameMessage extends WebSocketMessage { - type: 'startGame'; - payload: StartGamePayload; -} - -export interface DirectionChangeMessage extends WebSocketMessage { - type: 'changeDirection'; - payload: DirectionPayload; -} - -// Server -> Client messages - -export interface RoomStateMessage extends WebSocketMessage, GameState { - type: 'room:state'; -} - -export interface RoomReadyMessage extends WebSocketMessage { - type: 'room:ready'; - roomId: string; -} - -export interface RoomCreatedMessage extends WebSocketMessage { - type: 'room:created'; - roomId: string; - role: 'host' | 'guest'; -} - -export interface GameStartedMessage extends WebSocketMessage { - type: 'game:started'; - roomId: string; -} - -export interface GameUpdateMessage extends WebSocketMessage, GameState { - type: 'game:update'; -} - -export interface GameOverMessage extends WebSocketMessage, GameOver { - type: 'game:over'; -} - -export interface ErrorMessage extends WebSocketMessage { - type: 'error'; - code: string; - msg: string; -} - -// Available Room type -export interface AvailableRoom { - roomId: string; - hostAddress: string; - createdAt: number; - betAmount: BetAmount; -} - -export interface AvailableRoomsMessage extends WebSocketMessage { - type: 'room:available'; - rooms: AvailableRoom[]; -} - -export interface GetAvailableRoomsMessage extends WebSocketMessage { - type: 'getAvailableRooms'; -} - -export interface OnlineUsersMessage extends WebSocketMessage { - type: 'onlineUsers' | 'players:count'; - count: number; -} - -// App Session related messages - -export interface AppSessionSignatureRequestMessage extends WebSocketMessage { - type: 'appSession:signatureRequest'; - roomId: string; - appSessionData: CreateAppSessionRequest[]; - appDefinition: unknown; - participants: string[]; - requestToSign: unknown[]; -} - -export interface AppSessionStartGameRequestMessage extends WebSocketMessage { - type: 'appSession:startGameRequest'; - roomId: string; - appSessionData: CreateAppSessionRequest[]; - appDefinition: unknown; - participants: string[]; - requestToSign: unknown[]; -} - -export interface AppSessionSignatureConfirmedMessage extends WebSocketMessage { - type: 'appSession:signatureConfirmed'; - roomId: string; -} - -export interface AppSessionSignatureMessage extends WebSocketMessage { - type: 'appSession:signature'; - payload: { - roomId: string; - signature: string; - }; -} - -export interface AppSessionStartGameMessage extends WebSocketMessage { - type: 'appSession:startGame'; - payload: { - roomId: string; - signature: string; - }; -} - -// Union type for all WebSocket messages -export type WebSocketMessages = - | JoinRoomMessage - | StartGameMessage - | DirectionChangeMessage - | RoomStateMessage - | RoomReadyMessage - | RoomCreatedMessage - | GameStartedMessage - | GameUpdateMessage - | GameOverMessage - | AvailableRoomsMessage - | GetAvailableRoomsMessage - | OnlineUsersMessage - | ErrorMessage - | AppSessionSignatureRequestMessage - | AppSessionStartGameRequestMessage - | AppSessionSignatureConfirmedMessage - | AppSessionSignatureMessage - | AppSessionStartGameMessage; - -// MetaMask Ethereum Provider -export interface MetaMaskEthereumProvider { - isMetaMask?: boolean; - request: (request: { method: string; params?: Array }) => Promise; - on: (event: string, listener: (...args: any[]) => void) => void; - removeListener: (event: string, listener: (...args: any[]) => void) => void; - selectedAddress?: string; - isConnected?: () => boolean; -} - -// Add type definition for window.ethereum -declare global { - interface Window { - ethereum?: MetaMaskEthereumProvider; - } -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/utils/metamask.ts b/examples/viper-duel/client/src/utils/metamask.ts deleted file mode 100644 index b57e8fba0..000000000 --- a/examples/viper-duel/client/src/utils/metamask.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * MetaMask utility functions - */ - -/** - * Check if the MetaMask extension is installed - * @returns boolean indicating whether MetaMask is installed - */ -export function isMetaMaskInstalled(): boolean { - return typeof window !== 'undefined' && - typeof window.ethereum !== 'undefined' && - window.ethereum.isMetaMask === true; -} - -/** - * Format an Ethereum address for display - * @param address Full Ethereum address - * @returns Shortened address with ellipsis (e.g., 0x1234...5678) - */ -export function formatEthAddress(address: string): string { - if (!address) return ''; - if (address.length < 10) return address; - - return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; -} \ No newline at end of file diff --git a/examples/viper-duel/client/src/vite-env.d.ts b/examples/viper-duel/client/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a..000000000 --- a/examples/viper-duel/client/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/viper-duel/client/src/websocket/index.ts b/examples/viper-duel/client/src/websocket/index.ts deleted file mode 100644 index d00f8374f..000000000 --- a/examples/viper-duel/client/src/websocket/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Export everything from our consolidated websocket implementation -export * from './websocket'; \ No newline at end of file diff --git a/examples/viper-duel/client/src/websocket/websocket.ts b/examples/viper-duel/client/src/websocket/websocket.ts deleted file mode 100644 index 44b81de7b..000000000 --- a/examples/viper-duel/client/src/websocket/websocket.ts +++ /dev/null @@ -1,644 +0,0 @@ -import { type Hex } from "viem"; -import { ethers } from "ethers"; -import { - createAuthRequestMessage, - NitroliteRPC, - createAuthVerifyMessage, - createPingMessage, - createAuthVerifyMessageWithJWT, - createEIP712AuthMessageSigner, - parseAnyRPCResponse, - RPCMethod, -} from "@erc7824/nitrolite"; -import type { Channel } from "@erc7824/nitrolite"; -import { WalletStore } from "../store"; - -// ===== Types ===== - -/** - * WebSocket ready states - */ -export const WebSocketReadyState = { - CONNECTING: 0, - OPEN: 1, - CLOSING: 2, - CLOSED: 3, -} as const; - -export type WebSocketReadyState = (typeof WebSocketReadyState)[keyof typeof WebSocketReadyState]; - -/** - * WebSocket connection status - */ -export type WSStatus = "connected" | "connecting" | "disconnected" | "reconnecting" | "reconnect_failed" | "auth_failed" | "authenticating"; - -/** - * WebSocket client configuration options - */ -export interface WebSocketClientOptions { - autoReconnect: boolean; - reconnectDelay: number; - maxReconnectAttempts: number; - requestTimeout: number; -} - -/** - * Wallet signer interface - */ -export interface WalletSigner { - address: Hex; - sign: (payload: any) => Promise; -} - -/** - * Gets address from a public key - */ -export const getAddressFromPublicKey = (publicKey: string): string => { - const formattedKey = publicKey.startsWith("0x") ? publicKey : `0x${publicKey}`; - const hash = ethers.keccak256(formattedKey); - const address = `0x${hash.slice(-40)}`; - return ethers.getAddress(address); -}; - -/** - * EIP-712 domain for auth_verify challenge - */ -const getAuthDomain = () => { - return { - name: "Viper Duel", - }; -}; - -const expire = String(Math.floor(Date.now() / 1000) + 24 * 60 * 60); - -// ===== Connection ===== - -/** - * Core WebSocket client for browser applications - */ -export class WebSocketClient { - private ws: WebSocket | null = null; - private pendingRequests = new Map void; reject: (reason: Error) => void }>(); - // private requestCounter = 0; - private reconnectAttempts = 0; - private reconnectTimeout: any = null; - private statusHandlers: ((status: WSStatus) => void)[] = []; - private messageHandlers: ((message: unknown) => void)[] = []; - private errorHandlers: ((error: Error) => void)[] = []; - private currentChannel: any = null; - private nitroliteChannel: Channel | null = null; - private pingInterval: any = null; - - /** - * Creates a new WebSocket client - */ - private url: string; - private signer: WalletSigner; - private options: WebSocketClientOptions; - - constructor( - url: string, - signer: WalletSigner, - options: WebSocketClientOptions = { - autoReconnect: true, - reconnectDelay: 1000, - maxReconnectAttempts: 5, - requestTimeout: 10000, - } - ) { - this.url = url; - this.signer = signer; - this.options = options; - } - - /** - * Registers a status change callback - */ - onStatusChange(callback: (status: WSStatus) => void): void { - this.statusHandlers.push(callback); - } - - /** - * Registers a message handler - */ - onMessage(callback: (message: unknown) => void): void { - this.messageHandlers.push(callback); - } - - /** - * Registers an error handler - */ - onError(callback: (error: Error) => void): void { - this.errorHandlers.push(callback); - } - - /** - * Gets whether the client is connected - */ - get isConnected(): boolean { - return this.ws !== null && this.ws.readyState === WebSocketReadyState.OPEN; - } - - /** - * Gets the current WebSocket ready state - */ - get readyState(): WebSocketReadyState { - return this.ws ? (this.ws.readyState as WebSocketReadyState) : WebSocketReadyState.CLOSED; - } - - /** - * Gets the current channel - */ - get currentSubscribedChannel(): any { - return this.currentChannel; - } - - /** - * Gets the current Nitrolite channel - */ - get currentNitroliteChannel(): Channel | null { - return this.nitroliteChannel; - } - - /** - * Sets the Nitrolite channel - */ - setNitroliteChannel(channel: Channel): void { - this.nitroliteChannel = channel; - } - - /** - * Connects to the WebSocket server - */ - async connect(): Promise { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - - if (this.isConnected) return; - - return new Promise((resolve, reject) => { - try { - this.ws = new WebSocket(this.url); - this.emitStatus("connecting"); - - this.ws.onopen = async () => { - try { - this.emitStatus("authenticating"); - await this.authenticate(); - this.emitStatus("connected"); - this.reconnectAttempts = 0; - this.startPingInterval(); - resolve(); - } catch (error) { - this.emitStatus("auth_failed"); - this.emitError(error instanceof Error ? error : new Error(String(error))); - reject(error); - this.close(); - this.handleReconnect(); - } - }; - - this.ws.onmessage = this.handleMessage.bind(this); - - this.ws.onerror = () => { - this.emitError(new Error("WebSocket connection error")); - reject(new Error("WebSocket connection error")); - }; - - this.ws.onclose = () => { - this.emitStatus("disconnected"); - this.ws = null; - this.currentChannel = null; - this.stopPingInterval(); - - this.pendingRequests.forEach(({ reject }) => reject(new Error("WebSocket connection closed"))); - this.pendingRequests.clear(); - - this.handleReconnect(); - }; - } catch (error) { - reject(error); - this.handleReconnect(); - } - }); - } - - /** - * Waits for wallet client to be available - */ - private async waitForWalletClient(timeout: number = 10000): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const walletClient = WalletStore.getWalletClient(); - if (walletClient?.account?.address) { - return walletClient; - } - - // Wait 100ms before checking again - await new Promise((resolve) => setTimeout(resolve, 100)); - } - - throw new Error("Timeout waiting for wallet client to be available"); - } - - /** - * Authenticates with the WebSocket server - */ - private async authenticate(): Promise { - // Wait for wallet client to be available - const walletClient = await this.waitForWalletClient(); - console.log("Authenticating with wallet client:", walletClient); - if (!this.ws) throw new Error("WebSocket not connected"); - - if (!walletClient?.account?.address) throw new Error("Wallet client not initialized or address not available"); - - const privyWalletAddress = walletClient.account.address; - - console.log("Starting authentication with:"); - console.log("- Privy wallet address:", privyWalletAddress); - - // Check for JWT token first - const jwtToken = typeof window !== "undefined" ? window.localStorage?.getItem("jwtToken") : null; - - let authRequest: string; - - if (jwtToken) { - console.log("JWT token found, sending auth request with token"); - authRequest = await createAuthVerifyMessageWithJWT(jwtToken); - } else { - console.log("No JWT token found, proceeding with challenge-response authentication"); - authRequest = await createAuthRequestMessage({ - wallet: ethers.getAddress(privyWalletAddress) as `0x${string}`, // wallet - participant: this.signer.address, //session key - app_name: "Viper Duel", - expire: expire, - scope: "app.nitro.aura", - application: ethers.getAddress(privyWalletAddress) as `0x${string}`, - allowances: [], - }); - } - - this.ws.send(authRequest); - - return new Promise((resolve, reject) => { - const authTimeout = setTimeout(() => { - this.ws?.removeEventListener("message", handleAuthResponse); - reject(new Error("Authentication timeout")); - }, this.options.requestTimeout); - - const handleAuthResponse = async (event: MessageEvent) => { - const response = parseAnyRPCResponse(event.data); - - try { - // Check for challenge response: {"res": [id, "auth_challenge", {"challenge": "uuid"}, timestamp]} - if (response.method === RPCMethod.AuthChallenge) { - console.log("Received auth_challenge, preparing EIP-712 auth_verify..."); - - try { - console.log("Creating EIP-712 signing function..."); - const eip712SigningFunction = createEIP712AuthMessageSigner( - walletClient, - { - scope: "app.nitro.aura", - application: privyWalletAddress, - participant: this.signer.address, - expire: expire, - allowances: [], - }, - getAuthDomain() - ); - - console.log("Calling createAuthVerifyMessage..."); - const authVerify = await createAuthVerifyMessage(eip712SigningFunction, response); - - console.log("Sending auth_verify with EIP-712 signature"); - this.ws?.send(authVerify); - console.log("auth_verify sent successfully"); - } catch (eip712Error) { - console.error("Error creating EIP-712 auth_verify:", eip712Error); - console.error("Error stack:", (eip712Error as Error)?.stack); - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - reject( - new Error(`EIP-712 auth_verify failed: ${eip712Error instanceof Error ? eip712Error.message : String(eip712Error)}`) - ); - return; - } - } - // Check for success response - else if (response.method === RPCMethod.AuthVerify) { - if (!response.params.success) { - return; - } - console.log("Authentication successful"); - - // If response contains a JWT token, store it - if (response.params.jwtToken) { - console.log("JWT token received:", response.params.jwtToken); - if (typeof window !== "undefined") { - window.localStorage?.setItem("jwtToken", response.params.jwtToken); - } - } - - // Authentication successful - const paramsForChannels = [{ participant: ethers.getAddress(privyWalletAddress) as `0x${string}` }]; - const getChannelsMessage = NitroliteRPC.createRequest(10, RPCMethod.GetChannels, paramsForChannels); - const getChannelMessage = await NitroliteRPC.signRequestMessage(getChannelsMessage, this.signer.sign); - console.log("getChannelMessage", getChannelMessage); - this.ws?.send(JSON.stringify(getChannelMessage)); - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - resolve(); - } - // Check for error response - else if (response.method === RPCMethod.Error) { - const errorMsg = response.params.error || "Authentication failed"; - console.error("Authentication failed:", errorMsg); - if (typeof window !== "undefined") { - window.localStorage?.removeItem("jwtToken"); - } - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - reject(new Error(String(errorMsg))); - } else { - console.log("Received non-auth message during auth, continuing to listen:", response); - // Keep listening if it wasn't a final success/error - } - } catch (error) { - // Ignore non-auth methods during authentication - if (error instanceof Error && error.message && error.message.includes("Unknown method:")) { - console.log("Ignoring non-auth message during authentication:", error.message); - return; - } - - clearTimeout(authTimeout); - this.ws?.removeEventListener("message", handleAuthResponse); - reject(new Error(`Authentication error: ${error instanceof Error ? error.message : String(error)}`)); - } - }; - - this.ws?.addEventListener("message", handleAuthResponse); - }); - } - - /** - * Handles reconnection logic - */ - private handleReconnect(): void { - if (!this.options.autoReconnect || this.reconnectAttempts >= this.options.maxReconnectAttempts) { - if (this.reconnectAttempts >= this.options.maxReconnectAttempts) { - this.emitStatus("reconnect_failed"); - } - return; - } - - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - } - - this.reconnectAttempts++; - const delay = this.options.reconnectDelay * this.reconnectAttempts; - - this.emitStatus("reconnecting"); - - this.reconnectTimeout = setTimeout(() => { - this.connect().catch(() => { - // Silent catch to prevent unhandled rejections - }); - }, delay); - } - - /** - * Starts ping interval to keep connection alive - */ - private startPingInterval(): void { - this.stopPingInterval(); - this.pingInterval = setInterval(async () => { - if (this.isConnected) { - try { - await this.ping(); - } catch (error) { - console.error("Error sending ping:", error); - } - } - }, 20000); - } - - /** - * Stops the ping interval - */ - private stopPingInterval(): void { - if (this.pingInterval) { - clearInterval(this.pingInterval); - this.pingInterval = null; - } - } - - /** - * Closes the WebSocket connection - */ - close(): void { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = null; - } - - this.stopPingInterval(); - - if (this.ws && (this.ws.readyState === WebSocketReadyState.OPEN || this.ws.readyState === WebSocketReadyState.CONNECTING)) { - try { - this.ws.close(1000, "Normal closure"); - } catch (err) { - console.error("Error while closing WebSocket:", err); - } - } - - this.ws = null; - this.currentChannel = null; - - this.pendingRequests.forEach(({ reject }) => reject(new Error("WebSocket connection closed by client"))); - this.pendingRequests.clear(); - this.emitStatus("disconnected"); - } - - /** - * Emits a status change to all registered handlers - */ - private emitStatus(status: WSStatus): void { - this.statusHandlers.forEach((handler) => handler(status)); - } - - /** - * Emits a message to all registered handlers - */ - private emitMessage(message: unknown): void { - this.messageHandlers.forEach((handler) => handler(message)); - } - - /** - * Emits an error to all registered handlers - */ - private emitError(error: Error): void { - this.errorHandlers.forEach((handler) => handler(error)); - } - - /** - * Handles incoming WebSocket messages - */ - private handleMessage(event: MessageEvent): void { - let message; - - try { - message = JSON.parse(event.data); - } catch (error) { - this.emitError(new Error(`Failed to parse message: ${event.data}`)); - return; - } - - try { - // Notify message handlers - this.emitMessage(message); - - if (typeof message !== "object" || message === null) { - return; - } - - // Type guard to check for property existence - const hasProperty = (obj: T, prop: K): obj is T & Record => { - return prop in obj; - }; - - // Handle standard RPC responses (success) - if (hasProperty(message, "res") && Array.isArray(message.res) && message.res.length >= 3) { - const requestId = typeof message.res[0] === "number" ? message.res[0] : -1; - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.get(requestId)!.resolve(message.res[2]); - this.pendingRequests.delete(requestId); - } - return; - } - - // Handle error responses - if (hasProperty(message, "err") && Array.isArray(message.err) && message.err.length >= 3) { - const requestId = typeof message.err[0] === "number" ? message.err[0] : -1; - const errorMessage = `Error ${message.err[1]}: ${message.err[2]}`; - - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.get(requestId)!.reject(new Error(errorMessage)); - this.pendingRequests.delete(requestId); - } - return; - } - - // Handle typed messages - if (hasProperty(message, "type") && typeof message.type === "string") { - // Handle channel subscription - if ( - message.type === "subscribe_success" && - hasProperty(message, "data") && - typeof message.data === "object" && - message.data && - hasProperty(message.data, "channel") - ) { - this.currentChannel = message.data.channel; - } - - // Handle request responses with requestId - if (hasProperty(message, "requestId") && typeof message.requestId === "number") { - const requestId = message.requestId; - if (this.pendingRequests.has(requestId)) { - const result = hasProperty(message, "data") ? message.data : message; - this.pendingRequests.get(requestId)!.resolve(result); - this.pendingRequests.delete(requestId); - } - } - } - } catch (error) { - this.emitError(new Error(`Error processing message: ${error instanceof Error ? error.message : String(error)}`)); - } - } - - /** - * Sends a request to the server - */ - async sendRequest(signedRequest: string): Promise { - if (!this.isConnected || !this.ws) { - throw new Error("WebSocket not connected"); - } - - let requestId: number; - - try { - const parsedRequest = JSON.parse(signedRequest); - - if ( - !parsedRequest || - !parsedRequest.req || - !Array.isArray(parsedRequest.req) || - parsedRequest.req.length < 2 || - typeof parsedRequest.req[0] !== "number" || - typeof parsedRequest.req[1] !== "string" - ) { - throw new Error("Invalid request format"); - } - - requestId = parsedRequest.req[0]; - } catch (parseError) { - throw new Error(`Failed to parse request: ${parseError instanceof Error ? parseError.message : String(parseError)}`); - } - - return new Promise((resolve, reject) => { - const requestTimeout = setTimeout(() => { - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.delete(requestId); - reject(new Error(`Request timeout`)); - } - }, this.options.requestTimeout); - - this.pendingRequests.set(requestId, { - resolve: (result: unknown) => { - clearTimeout(requestTimeout); - resolve(result); - }, - reject: (error: Error) => { - clearTimeout(requestTimeout); - reject(error); - }, - }); - - try { - if (!this.ws) { - throw new Error("WebSocket is not initialized"); - } - this.ws.send(signedRequest); - } catch (error) { - clearTimeout(requestTimeout); - this.pendingRequests.delete(requestId); - reject(new Error(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`)); - } - }); - } - - /** - * Sends a ping to the server - */ - async ping(): Promise { - return this.sendRequest(await createPingMessage(this.signer.sign)); - } -} - -/** - * Creates a new WebSocket client - */ -export function createWebSocketClient(url: string, signer: WalletSigner, options?: Partial): WebSocketClient { - return new WebSocketClient(url, signer, { - autoReconnect: true, - reconnectDelay: 1000, - maxReconnectAttempts: 5, - requestTimeout: 10000, - ...options, - }); -} diff --git a/examples/viper-duel/client/tailwind.config.js b/examples/viper-duel/client/tailwind.config.js deleted file mode 100644 index b94949d64..000000000 --- a/examples/viper-duel/client/tailwind.config.js +++ /dev/null @@ -1,214 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - darkMode: ["class"], - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - prefix: "", - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - fontFamily: { - 'pixel': ['"Press Start 2P"', 'monospace'], - 'mono': ['"JetBrains Mono"', 'monospace'], - 'sans': ['Inter', 'system-ui', 'sans-serif'], - }, - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - // Viper Duel brand colors - viper: { - green: { - DEFAULT: "#2AFF6B", - dark: "#1ACC53", - light: "#5CFF8A", - }, - purple: { - DEFAULT: "#B425FF", - dark: "#8B1ACC", - light: "#C850FF", - }, - charcoal: { - DEFAULT: "#0E0F11", - light: "#1A1C1F", - lighter: "#262A2F", - }, - yellow: { - DEFAULT: "#FFED00", - dark: "#E6D400", - light: "#FFF033", - }, - grey: { - DEFAULT: "#9DA0A5", - dark: "#7A7D82", - light: "#B8BBC0", - }, - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - boxShadow: { - glow: { - green: "0 0 20px 5px rgba(42, 255, 107, 0.3)", - purple: "0 0 20px 5px rgba(180, 37, 255, 0.3)", - yellow: "0 0 20px 5px rgba(255, 237, 0, 0.3)", - }, - }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - pulse: { - "0%, 100%": { opacity: 0.8 }, - "50%": { opacity: 1 }, - }, - sparkle: { - "0%": { transform: "translateY(0) rotate(0deg)" }, - "100%": { transform: "translateY(-100px) rotate(20deg)" }, - }, - float: { - "0%": { transform: "translateY(0px)" }, - "50%": { transform: "translateY(-10px)" }, - "100%": { transform: "translateY(0px)" }, - }, - "pulse-green": { - "0%": { boxShadow: "0 0 10px rgba(42, 255, 107, 0.5)" }, - "50%": { - boxShadow: "0 0 20px rgba(42, 255, 107, 0.7), 0 0 30px rgba(42, 255, 107, 0.3)" - }, - "100%": { boxShadow: "0 0 10px rgba(42, 255, 107, 0.5)" }, - }, - "pulse-purple": { - "0%": { boxShadow: "0 0 10px rgba(180, 37, 255, 0.5)" }, - "50%": { - boxShadow: "0 0 20px rgba(180, 37, 255, 0.7), 0 0 30px rgba(180, 37, 255, 0.3)" - }, - "100%": { boxShadow: "0 0 10px rgba(180, 37, 255, 0.5)" }, - }, - "pulse-yellow": { - "0%": { boxShadow: "0 0 10px rgba(255, 237, 0, 0.5)" }, - "50%": { - boxShadow: "0 0 20px rgba(255, 237, 0, 0.7), 0 0 30px rgba(255, 237, 0, 0.3)" - }, - "100%": { boxShadow: "0 0 10px rgba(255, 237, 0, 0.5)" }, - }, - "explode-green": { - "0%": { - transform: "scale(0.3)", - opacity: 1, - filter: "blur(2px)" - }, - "80%": { - opacity: 0.7 - }, - "100%": { - transform: "scale(3)", - opacity: 0, - filter: "blur(1px)" - }, - }, - "explode-purple": { - "0%": { - transform: "scale(0.3)", - opacity: 1, - filter: "blur(2px)" - }, - "80%": { - opacity: 0.7 - }, - "100%": { - transform: "scale(3)", - opacity: 0, - filter: "blur(1px)" - }, - }, - "orbit": { - "0%": { transform: "rotate(0deg) translateX(10px) rotate(0deg)" }, - "100%": { transform: "rotate(360deg) translateX(10px) rotate(-360deg)" }, - }, - fadeIn: { - "0%": { opacity: 0 }, - "100%": { opacity: 1 }, - }, - fadeInUp: { - "0%": { - opacity: 0, - transform: "translateY(10px)", - }, - "100%": { - opacity: 1, - transform: "translateY(0)", - }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "pulse": "pulse 3s infinite ease-in-out", - "pulse-fast": "pulse 1.5s infinite ease-in-out", - "sparkle": "sparkle 8s infinite linear", - "float": "float 6s infinite ease-in-out", - "pulse-green": "pulse-green 2s infinite ease-in-out", - "pulse-purple": "pulse-purple 2s infinite ease-in-out", - "pulse-yellow": "pulse-yellow 2s infinite ease-in-out", - "explode-green": "explode-green 1.5s ease-out forwards", - "explode-purple": "explode-purple 1.5s ease-out forwards", - "orbit": "orbit 3s infinite linear", - "orbit-reverse": "orbit 3s infinite linear reverse", - "fadeIn": "fadeIn 0.3s ease-in-out", - "fadeInUp": "fadeInUp 0.5s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} \ No newline at end of file diff --git a/examples/viper-duel/client/tsconfig.app.json b/examples/viper-duel/client/tsconfig.app.json deleted file mode 100644 index c9ccbd4c5..000000000 --- a/examples/viper-duel/client/tsconfig.app.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] -} diff --git a/examples/viper-duel/client/tsconfig.json b/examples/viper-duel/client/tsconfig.json deleted file mode 100644 index 1ffef600d..000000000 --- a/examples/viper-duel/client/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/examples/viper-duel/client/tsconfig.node.json b/examples/viper-duel/client/tsconfig.node.json deleted file mode 100644 index 9728af2d8..000000000 --- a/examples/viper-duel/client/tsconfig.node.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/examples/viper-duel/client/vite.config.ts b/examples/viper-duel/client/vite.config.ts deleted file mode 100644 index b17502bfe..000000000 --- a/examples/viper-duel/client/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -}); diff --git a/examples/viper-duel/server/.env b/examples/viper-duel/server/.env deleted file mode 100644 index 2b8895fc4..000000000 --- a/examples/viper-duel/server/.env +++ /dev/null @@ -1,18 +0,0 @@ -# Viper Duel Server Environment Variables - -# WebSocket URL for the Nitrolite service -WS_URL=wss://clearnet.yellow.com/ws - -# Server private key (replace with your actual private key) -SERVER_PRIVATE_KEY=0x - -# Nitrolite configuration -CHAIN_ID=137 -USDC_TOKEN_ADDRESS=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 -CUSTODY_ADDRESS=0x4C8Bd8877C3b403BA9f9ECfaAD910AF0d8CA2c4D -ADJUDICATOR_ADDRESS=0x2627644ae08aa21Da0Fb458D8879729743D1bB51 -DEFAULT_GUEST_ADDRESS=0x3c93C321634a80FB3657CFAC707718A11cA57cBf -POLYGON_RPC_URL=https://polygon-rpc.com - -# Server port -PORT=8080 \ No newline at end of file diff --git a/examples/viper-duel/server/.gitignore b/examples/viper-duel/server/.gitignore deleted file mode 100644 index a547bf36d..000000000 --- a/examples/viper-duel/server/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/viper-duel/server/README.md b/examples/viper-duel/server/README.md deleted file mode 100644 index 166c2b874..000000000 --- a/examples/viper-duel/server/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Viper Duel Server - -A WebSocket server for the Viper Duel 1-vs-1 Snake game. This server handles game rooms, player connections, and game state management. - -## Quick Start - -```bash -# Install dependencies -npm install - -# Start the server (production) -npm start - -# Start the server with hot reload (development) -npm run dev - -# Run linting -npm run lint -``` - -Server runs on port 8080 by default. - -## Protocol - -### Client → Server Messages - -```js -// Join a room -{ - "type": "joinRoom", - "payload": { - "roomId": "uuid-string", // Optional - system will create if not provided - "eoa": "0x..." // Ethereum address - } -} - -// Make a move -{ - "type": "move", - "payload": { - "roomId": "uuid-string", // Required - "pos": 0 // Position on board (0-8) - } -} -``` - -### Server → Client Messages - -```js -// Room state update (after join or move) -{ - "type": "room:state", - "roomId": "uuid-string", - "board": [null,null,null,null,"X",null,null,null,null], - "nextTurn": "O", - "players": { - "X": "0x...", // Host EOA - "O": "0x..." // Guest EOA - } -} - -// Room ready notification (2 players joined) -{ - "type": "room:ready", - "roomId": "uuid-string" -} - -// Game over notification -{ - "type": "game:over", - "winner": "X", // "X", "O", or null (draw) - "board": ["X","O","X","X","O","O","X",null,null] -} - -// Error message -{ - "type": "error", - "code": "ERROR_CODE", - "msg": "Error description" -} -``` - -## Sequence Diagram - -``` -┌──────┐ ┌─────────┐ ┌──────┐ -│Client│ │ Server │ │Client│ -└──┬───┘ └────┬────┘ └──┬───┘ - │ │ │ - │ {"type":"joinRoom",...} │ │ - ├────────────────────────────► │ - │ │ │ - │ {"type":"room:state",...} │ │ - ◄├───────────────────────────┤ │ - │ │ │ - │ │ {"type":"joinRoom",...} │ - │ ◄───────────────────────────┤ - │ │ │ - │ {"type":"room:state",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ {"type":"room:ready",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ {"type":"move",...} │ │ - ├────────────────────────────► │ - │ │ │ - │ {"type":"room:state",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ │ {"type":"move",...} │ - │ ◄───────────────────────────┤ - │ │ │ - │ {"type":"room:state",...}│ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ - │ (moves continue...) │ │ - ├────────────────────────────► │ - │ │ │ - │ {"type":"game:over",...} │ │ - ◄├───────────────────────────┼───────────────────────────► - │ │ │ -``` - -## Technical Notes - -- Uses in-memory storage for rooms and game state (will be replaced with @erc7824/nitrolite in future) -- First player is assigned as host (X), second as guest (O) -- Rooms are automatically created if a non-existent room ID is provided -- Rooms are automatically cleaned up after games complete -- Game rules enforce alternating turns and valid move placement -- Validation for Ethereum addresses and game actions \ No newline at end of file diff --git a/examples/viper-duel/server/package-lock.json b/examples/viper-duel/server/package-lock.json deleted file mode 100644 index aace94528..000000000 --- a/examples/viper-duel/server/package-lock.json +++ /dev/null @@ -1,1734 +0,0 @@ -{ - "name": "viper-duel-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "viper-duel-server", - "version": "1.0.0", - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "chalk": "5.4.1", - "dotenv": "16.5.0", - "ethers": "^6.11.1", - "uuid": "^9.0.1", - "viem": "^2.7.15", - "ws": "^8.16.0" - }, - "devDependencies": { - "eslint": "^8.56.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT" - }, - "node_modules/@erc7824/nitrolite": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@erc7824/nitrolite/-/nitrolite-0.2.20.tgz", - "integrity": "sha512-iYzgHh6Acd3Pq1tjQ+yzEiZQh68xHZ4P+r5UMrweqBMU93Fr39pHEYe/PkdAGppREOHoXvuIDDp6S7ORTxfkug==", - "license": "MIT", - "dependencies": { - "abitype": "^0.10.3", - "viem": "^2.5.0", - "zod": "^3.25.67" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@scure/base": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.5.tgz", - "integrity": "sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", - "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.2" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/abitype": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.10.3.tgz", - "integrity": "sha512-tRN+7XIa7J9xugdbRzFv/95ka5ivR/sRe01eiWvM0HWWjHuigSZEACgKa0sj4wGuekTDtghCx+5Izk/cOi78pQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ethers": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.0.tgz", - "integrity": "sha512-KgHwltNSMdbrGWEyKkM0Rt2s+u1nDH/5BVDQakLinzGEJi4bWindBzZSCC4gKsbZjwDTI6ex/8suR9Ihbmz4IQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ox/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ox/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/viem": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.29.2.tgz", - "integrity": "sha512-cukRxab90jvQ+TDD84sU3qB3UmejYqgCw4cX8SfWzvh7JPfZXI3kAMUaT5OSR2As1Mgvx1EJawccwPjGqkSSwA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.2", - "@noble/hashes": "1.7.2", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.9", - "ws": "8.18.1" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/@noble/curves": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", - "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.2" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", - "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.25.67", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", - "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/examples/viper-duel/server/package.json b/examples/viper-duel/server/package.json deleted file mode 100644 index c04abef52..000000000 --- a/examples/viper-duel/server/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "viper-duel-server", - "version": "1.0.0", - "type": "module", - "description": "WebSocket server for Viper Duel Snake game", - "main": "src/server.js", - "scripts": { - "start": "node src/server.js", - "dev": "node --watch src/server.js", - "lint": "eslint src/**/*.js" - }, - "dependencies": { - "@erc7824/nitrolite": "0.2.20", - "chalk": "5.4.1", - "dotenv": "16.5.0", - "ethers": "^6.11.1", - "uuid": "^9.0.1", - "viem": "^2.7.15", - "ws": "^8.16.0" - }, - "devDependencies": { - "eslint": "^8.56.0" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/examples/viper-duel/server/src/config/websocket.js b/examples/viper-duel/server/src/config/websocket.js deleted file mode 100644 index 3eaa2efe3..000000000 --- a/examples/viper-duel/server/src/config/websocket.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * WebSocket server configuration - */ -import { WebSocketServer } from 'ws'; -import dotenv from 'dotenv'; -import logger from '../utils/logger.js'; - -// Load environment variables -dotenv.config(); - -/** - * Creates a new WebSocket server - * @returns {WebSocketServer} The WebSocket server instance - */ -export function createWebSocketServer() { - const port = process.env.PORT || 8080; - logger.system(`Creating WebSocket server on port ${port}`); - - return new WebSocketServer({ - host: '0.0.0.0', - port - }); -} - -/** - * Utility function to send an error message - * @param {WebSocket} ws - WebSocket connection - * @param {string} code - Error code - * @param {string} msg - Error message - */ -export function sendError(ws, code, msg) { - ws.send(JSON.stringify({ - type: 'error', - code, - msg - })); -} - -/** - * Starts a ping interval to keep connections alive - * @param {WebSocketServer} wss - The WebSocket server instance - * @param {number} interval - Ping interval in milliseconds (default: 30000) - * @returns {NodeJS.Timeout} The interval timer - */ -export function startPingInterval(wss, interval = 30000) { - return setInterval(() => { - wss.clients.forEach((client) => { - if (client.readyState === 1) { // WebSocket.OPEN - client.send(JSON.stringify({ type: 'ping' })); - } - }); - }, interval); -} \ No newline at end of file diff --git a/examples/viper-duel/server/src/routes/gameRoutes.js b/examples/viper-duel/server/src/routes/gameRoutes.js deleted file mode 100644 index 97e3c2142..000000000 --- a/examples/viper-duel/server/src/routes/gameRoutes.js +++ /dev/null @@ -1,368 +0,0 @@ -/** - * Game-related WebSocket message handlers - */ - -import { validateDirectionPayload } from '../utils/validators.js'; -import { - formatGameState, - formatGameOverMessage, - createGame, - createAppSession, - closeAppSession, - hasAppSession, - generateAppSessionMessage, - addAppSessionSignature, - createAppSessionWithSignatures -} from '../services/index.js'; -import logger from '../utils/logger.js'; - -/** - * Handles a start game request - * @param {WebSocket} ws - WebSocket connection - * @param {Object} payload - Request payload - * @param {Object} context - Application context containing roomManager and connections - */ -export async function handleStartGame(ws, payload, { roomManager, connections, sendError }) { - console.log(`🎯 handleStartGame called for payload:`, payload); - - if (!payload || typeof payload !== 'object') { - return sendError(ws, 'INVALID_PAYLOAD', 'Invalid payload format'); - } - - const { roomId } = payload; - - if (!roomId) { - return sendError(ws, 'INVALID_PAYLOAD', 'Room ID is required'); - } - - console.log(`🎯 Processing start game for room: ${roomId}`); - - // Find the player trying to start the game - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - // Get the room - const room = roomManager.rooms.get(roomId); - if (!room) { - return sendError(ws, 'ROOM_NOT_FOUND', 'Room not found'); - } - - // Only the host can start the game - if (room.players.host !== playerEoa) { - return sendError(ws, 'NOT_AUTHORIZED', 'Only the host can start the game'); - } - - // Need both players - if (!room.players.host || !room.players.guest) { - return sendError(ws, 'ROOM_NOT_FULL', 'Room must have two players to start the game'); - } - - // Initialize game state if not already done - if (!room.gameState) { - console.log(`🎮 Creating game state for room ${roomId}`); - room.gameState = createGame(room.players.host, room.players.guest); - console.log(`✅ Game state created:`, { - player1: room.gameState.players.player1, - player2: room.gameState.players.player2, - snakesCount: Object.keys(room.gameState.snakes).length - }); - } else { - console.log(`♻️ Game state already exists for room ${roomId}`); - } - - // Create an app session for this game if not already created - if (!hasAppSession(roomId)) { - try { - logger.nitro(`Creating app session for room ${roomId}`); - const appId = await createAppSession(roomId, room.players.host, room.players.guest, room.betAmount); - logger.nitro(`App session created with ID ${appId}`); - - // Store the app ID in the room object - room.appId = appId; - } catch (error) { - logger.error(`Failed to create app session for room ${roomId}:`, error); - // Continue with the game even if app session creation fails - // This allows the game to work in a fallback mode - } - } - - // Broadcast game started - roomManager.broadcastToRoom( - roomId, - 'game:started', - { roomId } - ); - - // Start the automatic movement game loop - console.log(`🚀 Starting automatic movement game loop for room ${roomId}`); - startGameLoop(roomId, roomManager); - - // Send the initial game state - roomManager.broadcastToRoom( - roomId, - 'room:state', - formatGameState(room.gameState, roomId, room.betAmount) - ); -} - -/** - * Game loop intervals for each room - */ -const gameLoops = new Map(); - -/** - * Handles app session closure when game ends - * @param {string} roomId - Room ID - * @param {Object} gameState - Final game state - * @param {Object} roomManager - Room manager instance - */ -async function handleGameOverAppSession(roomId, gameState, roomManager) { - try { - const room = roomManager.rooms.get(roomId); - - // First check if the room has an appId directly - if (room && room.appId) { - logger.nitro(`Closing app session with ID ${room.appId} for room ${roomId}`); - - // Determine winner based on game result - let winnerId = null; - if (gameState.winner === 'player1') { - winnerId = 'A'; // player1 is player A (host) - } else if (gameState.winner === 'player2') { - winnerId = 'B'; // player2 is player B (guest) - } - // null winner means tie - - // Calculate allocations based on winner and room bet amount - const betAmount = room.betAmount || 0; - const betAmountStr = betAmount.toString(); - const totalPot = (betAmount * 2).toString(); - - let finalAllocations; - if (winnerId === 'A') { - // Player A wins - gets all the funds - finalAllocations = [totalPot, '0', '0']; // A gets both initial allocations - } else if (winnerId === 'B') { - // Player B wins - gets all the funds - finalAllocations = ['0', totalPot, '0']; // B gets both initial allocations - } else { - // Tie or no winner - split evenly (return original amounts) - finalAllocations = [betAmountStr, betAmountStr, '0']; - } - - logger.data(`Game over allocation calculation for room ${roomId}:`, { - betAmount, - betAmountStr, - totalPot, - winnerId, - finalAllocations - }); - - await closeAppSession(roomId, finalAllocations); - logger.nitro(`App session closed for room ${roomId}`); - } - // Otherwise check the app sessions storage - else if (hasAppSession(roomId)) { - logger.nitro(`Closing app session from storage for room ${roomId}`); - - // Determine winner based on game result - let winnerId = null; - if (gameState.winner === 'player1') { - winnerId = 'A'; // player1 is player A (host) - } else if (gameState.winner === 'player2') { - winnerId = 'B'; // player2 is player B (guest) - } - // null winner means tie - - // Calculate allocations based on winner and room bet amount - const betAmount = room.betAmount || 0; - const betAmountStr = betAmount.toString(); - const totalPot = (betAmount * 2).toString(); - - let finalAllocations; - if (winnerId === 'A') { - // Player A wins - gets all the funds - finalAllocations = [totalPot, '0', '0']; // A gets both initial allocations - } else if (winnerId === 'B') { - // Player B wins - gets all the funds - finalAllocations = ['0', totalPot, '0']; // B gets both initial allocations - } else { - // Tie or no winner - split evenly (return original amounts) - finalAllocations = [betAmountStr, betAmountStr, '0']; - } - - logger.data(`Game over allocation calculation for room ${roomId}:`, { - betAmount, - betAmountStr, - totalPot, - winnerId, - finalAllocations - }); - - await closeAppSession(roomId, finalAllocations); - logger.nitro(`App session closed for room ${roomId}`); - } - } catch (error) { - logger.error(`Failed to close app session for room ${roomId}:`, error); - // Continue with room cleanup even if app session closure fails - } -} - -/** - * Starts the game loop for a room - * @param {string} roomId - Room ID - * @param {Object} roomManager - Room manager instance - */ -export function startGameLoop(roomId, roomManager) { - console.log(`🔄 startGameLoop called for room ${roomId}`); - - // Clear any existing loop - if (gameLoops.has(roomId)) { - console.log(`🧹 Clearing existing game loop for room ${roomId}`); - clearInterval(gameLoops.get(roomId)); - } - - const interval = setInterval(() => { - console.log(`⏰ Game loop tick for room ${roomId}`); - const result = roomManager.updateGameState(roomId); - if (!result.success) { - console.error(`❌ Game loop failed for room ${roomId}:`, result.error); - clearInterval(interval); - gameLoops.delete(roomId); - return; - } - - // Broadcast updated game state - const currentRoom = roomManager.rooms.get(roomId); - const formattedState = formatGameState(result.gameState, roomId, currentRoom?.betAmount || 0); - console.log("🚀 Broadcasting game:update:", { - roomId, - gameTime: formattedState.gameTime, - player1Pos: formattedState.snakes?.player1?.body?.[0], - player2Pos: formattedState.snakes?.player2?.body?.[0], - foodCount: formattedState.food?.length - }); - roomManager.broadcastToRoom( - roomId, - 'game:update', - formattedState - ); - - // Handle game over condition - if (result.isGameOver) { - clearInterval(interval); - gameLoops.delete(roomId); - - roomManager.broadcastToRoom( - roomId, - 'game:over', - formatGameOverMessage(result.gameState) - ); - - // Close the app session if one was created - handleGameOverAppSession(roomId, result.gameState, roomManager); - - // Clean up room after delay - setTimeout(() => { - roomManager.closeRoom(roomId); - }, 5000); - } - }, 150); // Update every 150ms for real-time speed - - gameLoops.set(roomId, interval); - console.log(`✅ Game loop started for room ${roomId}, interval ID:`, interval); -} - -/** - * Starts a minimal game over detection loop for real-time movement games - * @param {string} roomId - Room ID - * @param {Object} roomManager - Room manager instance - */ -export function startGameOverDetectionLoop(roomId, roomManager) { - console.log(`🔄 startGameOverDetectionLoop called for room ${roomId}`); - - // Clear any existing loop - if (gameLoops.has(roomId)) { - console.log(`🧹 Clearing existing game loop for room ${roomId}`); - clearInterval(gameLoops.get(roomId)); - } - - const interval = setInterval(() => { - // Check if the room still exists - const room = roomManager.rooms.get(roomId); - if (!room || !room.gameState) { - clearInterval(interval); - gameLoops.delete(roomId); - return; - } - - // Only check for game over condition, don't move snakes - if (room.gameState.isGameOver) { - clearInterval(interval); - gameLoops.delete(roomId); - - roomManager.broadcastToRoom( - roomId, - 'game:over', - formatGameOverMessage(room.gameState) - ); - - // Close the app session if one was created - handleGameOverAppSession(roomId, room.gameState, roomManager); - - // Clean up room after delay - setTimeout(() => { - roomManager.closeRoom(roomId); - }, 5000); - } - }, 1000); // Check every 1 second - - gameLoops.set(roomId, interval); - console.log(`✅ Game over detection loop started for room ${roomId}, interval ID:`, interval); -} - -/** - * Handles a direction change request - * @param {WebSocket} ws - WebSocket connection - * @param {Object} payload - Request payload - * @param {Object} context - Application context containing roomManager and connections - */ -export async function handleDirectionChange(ws, payload, { roomManager, connections, sendError }) { - // Validate payload - const validation = validateDirectionPayload(payload); - if (!validation.success) { - return sendError(ws, 'INVALID_PAYLOAD', validation.error); - } - - const { roomId, direction } = payload; - - // Find the player making the move - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - // Process the direction change - const result = roomManager.processDirectionChange(roomId, direction, playerEoa); - if (!result.success) { - return sendError(ws, 'DIRECTION_CHANGE_FAILED', result.error); - } - - // Direction change processed - the automatic game loop will handle movement updates -} \ No newline at end of file diff --git a/examples/viper-duel/server/src/routes/roomRoutes.js b/examples/viper-duel/server/src/routes/roomRoutes.js deleted file mode 100644 index d4a976807..000000000 --- a/examples/viper-duel/server/src/routes/roomRoutes.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Room-related WebSocket message handlers - */ - -import { validateJoinRoomPayload } from '../utils/validators.js'; -import { formatGameState, generateAppSessionMessage } from '../services/index.js'; -import logger from '../utils/logger.js'; - -/** - * Handles a request to join a room - * @param {WebSocket} ws - WebSocket connection - * @param {Object} payload - Request payload - * @param {Object} context - Application context containing roomManager and connections - */ -export async function handleJoinRoom(ws, payload, { roomManager, connections, sendError }) { - // Validate payload - const validation = validateJoinRoomPayload(payload); - if (!validation.success) { - return sendError(ws, 'INVALID_PAYLOAD', validation.error); - } - - const { roomId, eoa, betAmount = 0 } = payload; - console.log(`Processing ${validation.isCreating ? 'CREATE' : 'JOIN'} request for EOA: ${eoa}, roomId: ${roomId || 'NEW'}, betAmount: ${betAmount}`); - - // Validate bet amount - const validBetAmounts = [0, 0.01, 0.1, 1, 2]; - if (!validBetAmounts.includes(betAmount)) { - return sendError(ws, 'INVALID_BET_AMOUNT', 'Invalid bet amount. Must be 0, 0.01, 0.1, 1, or 2'); - } - - // Check if address is already connected - if (connections.has(eoa)) { - return sendError(ws, 'ALREADY_CONNECTED', 'Address already connected'); - } - - let result; - if (validation.isCreating) { - // Creating a new room - const newRoomId = roomManager.createRoom(betAmount); - console.log(`Created new room with ID: ${newRoomId}, bet amount: ${betAmount}`); - - // Join the newly created room as host - result = roomManager.joinRoom(newRoomId, eoa, ws, betAmount); - - if (result.success) { - console.log(`New room created: ${newRoomId} for player (host): ${eoa}`); - - // Send room ID to client immediately so they can share it - ws.send(JSON.stringify({ - type: 'room:created', - roomId: newRoomId, - role: 'host' - })); - } - } else { - // Joining an existing room - result = roomManager.joinRoom(roomId, eoa, ws, betAmount); - - if (result.success) { - console.log(`Player ${eoa} joined room: ${roomId} as ${result.role}`); - } - } - - if (!result.success) { - return sendError(ws, 'JOIN_FAILED', result.error); - } - - // Store connection - connections.set(eoa, { ws, roomId: result.roomId }); - - // Get room - const room = roomManager.rooms.get(result.roomId); - - // Send room state to all players - if (room.gameState) { - roomManager.broadcastToRoom( - result.roomId, - 'room:state', - formatGameState(room.gameState, result.roomId, room.betAmount) - ); - } - - // Notify all players that room is ready if applicable - if (result.isRoomReady) { - roomManager.broadcastToRoom(result.roomId, 'room:ready', { roomId: result.roomId }); - - logger.nitro(`Room ${result.roomId} is ready - starting signature collection flow`); - logger.data(`Room players:`, { host: room.players.host, guest: room.players.guest }); - - // Generate app session message for signature collection when room becomes ready (both players joined) - try { - const appSessionMessage = await generateAppSessionMessage( - result.roomId, - room.players.host, - room.players.guest, - room.betAmount - ); - - logger.nitro(`Generated app session message for room ${result.roomId}`); - - // Send the message to participant B (guest) for signature - const guestConnection = room.connections.get(room.players.guest); - if (guestConnection && guestConnection.ws.readyState === 1) { - guestConnection.ws.send(JSON.stringify({ - type: 'appSession:signatureRequest', - roomId: result.roomId, - appSessionData: appSessionMessage.appSessionData, - appDefinition: appSessionMessage.appDefinition, - participants: appSessionMessage.participants, - requestToSign: appSessionMessage.requestToSign - })); - } - - } catch (error) { - logger.error(`Failed to generate app session message for room ${result.roomId}:`, error); - } - } -} - -/** - * Handles a request to get available rooms - * @param {WebSocket} ws - WebSocket connection - * @param {Object} context - Application context containing roomManager - */ -export async function handleGetAvailableRooms(ws, { roomManager }) { - // Filter rooms that are not full - const availableRooms = []; - - // Get current timestamp - const now = Date.now(); - - // Iterate through all rooms and find available ones - for (const [roomId, room] of roomManager.rooms.entries()) { - // Room is available if it has a host but no guest, and game is not started - if (room.players.host && !room.players.guest && !room.gameState) { - availableRooms.push({ - roomId, - hostAddress: room.players.host, - createdAt: room.createdAt || now, // Use tracked creation time or fall back to now - betAmount: room.betAmount || 0 - }); - } - } - - // Send available rooms to client - ws.send(JSON.stringify({ - type: 'room:available', - rooms: availableRooms - })); -} \ No newline at end of file diff --git a/examples/viper-duel/server/src/server.js b/examples/viper-duel/server/src/server.js deleted file mode 100644 index 41d0913f3..000000000 --- a/examples/viper-duel/server/src/server.js +++ /dev/null @@ -1,322 +0,0 @@ -/** - * WebSocket server for Viper Duel Snake game - */ - -import { createWebSocketServer, sendError, startPingInterval } from './config/websocket.js'; -import { initializeRPCClient, createRoomManager } from './services/index.js'; -import { handleJoinRoom, handleGetAvailableRooms } from './routes/roomRoutes.js'; -import { handleStartGame, handleDirectionChange } from './routes/gameRoutes.js'; -import { addAppSessionSignature, createAppSessionWithSignatures, getPendingAppSessionMessage } from './services/index.js'; -import logger from './utils/logger.js'; - -// Create WebSocket server -const wss = createWebSocketServer(); -const roomManager = createRoomManager(); - -// Track active connections -// TODO: Use @erc7824/nitrolite for connection tracking when available -const connections = new Map(); - -// Track online users count -let onlineUsersCount = 0; - -/** - * Handles app session signature submission - */ -async function handleAppSessionSignature(ws, payload, { roomManager, connections, sendError }) { - if (!payload || typeof payload !== 'object') { - return sendError(ws, 'INVALID_PAYLOAD', 'Invalid payload format'); - } - - const { roomId, signature } = payload; - - if (!roomId || !signature) { - return sendError(ws, 'INVALID_PAYLOAD', 'Room ID and signature are required'); - } - - // Find the player submitting the signature - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - try { - const allSignaturesCollected = await addAppSessionSignature(roomId, playerEoa, signature); - - logger.nitro(`Signature added for ${playerEoa} in room ${roomId}`); - - // Send confirmation to the signing player - ws.send(JSON.stringify({ - type: 'appSession:signatureConfirmed', - roomId - })); - - // Check if this was participant B (guest) signing, and if so, request signature from participant A (host) - const room = roomManager.rooms.get(roomId); - if (room && playerEoa === room.players.guest && !allSignaturesCollected) { - logger.nitro(`Participant B signed, now requesting signature from participant A (host)`); - - // Send signature request to participant A (host) - const hostConnection = room.connections.get(room.players.host); - if (hostConnection && hostConnection.ws.readyState === 1) { - // Get the existing pending app session message (don't generate a new one!) - const appSessionMessage = getPendingAppSessionMessage(roomId); - - if (!appSessionMessage) { - logger.error(`No pending app session found for room ${roomId}`); - return; - } - - hostConnection.ws.send(JSON.stringify({ - type: 'appSession:startGameRequest', - roomId, - appSessionData: appSessionMessage.appSessionData, - appDefinition: appSessionMessage.appDefinition, - participants: appSessionMessage.participants, - requestToSign: appSessionMessage.requestToSign - })); - - logger.nitro(`Sent start game request to host ${room.players.host}`); - } else { - logger.error(`Host connection not found or not ready for room ${roomId}`); - } - } - - // If all signatures are collected, create the app session (this happens after participant A signs) - if (allSignaturesCollected) { - logger.nitro(`All signatures collected for room ${roomId}, creating app session`); - // The app session creation will be handled by the handleAppSessionStartGame function - } - - } catch (error) { - logger.error(`Error handling app session signature for room ${roomId}:`, error); - return sendError(ws, 'SIGNATURE_ERROR', error.message); - } -} - -/** - * Handles app session start game request (with host signature) - */ -async function handleAppSessionStartGame(ws, payload, { roomManager, connections, sendError }) { - if (!payload || typeof payload !== 'object') { - return sendError(ws, 'INVALID_PAYLOAD', 'Invalid payload format'); - } - - const { roomId, signature } = payload; - - if (!roomId || !signature) { - return sendError(ws, 'INVALID_PAYLOAD', 'Room ID and signature are required'); - } - - // Find the player submitting the signature (should be host) - let playerEoa = null; - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - playerEoa = eoa; - break; - } - } - - if (!playerEoa) { - return sendError(ws, 'NOT_AUTHENTICATED', 'Player not authenticated'); - } - - // Get the room - const room = roomManager.rooms.get(roomId); - if (!room) { - return sendError(ws, 'ROOM_NOT_FOUND', 'Room not found'); - } - - // Only the host can start the game - if (room.players.host !== playerEoa) { - return sendError(ws, 'NOT_AUTHORIZED', 'Only the host can start the game'); - } - - try { - // Add the host's signature - const allSignaturesCollected = await addAppSessionSignature(roomId, playerEoa, signature); - - if (!allSignaturesCollected) { - return sendError(ws, 'SIGNATURES_INCOMPLETE', 'Not all signatures collected'); - } - - logger.nitro(`Host signature added for room ${roomId}, creating app session`); - - // Create the app session with all collected signatures - const appId = await createAppSessionWithSignatures(roomId); - - // Store the app ID in the room object - room.appId = appId; - - // Initialize game state - if (!room.gameState) { - const { createGame } = await import('./services/index.js'); - room.gameState = createGame(room.players.host, room.players.guest); - } - - // Broadcast game started - roomManager.broadcastToRoom( - roomId, - 'game:started', - { roomId, appId } - ); - - // Start the automatic movement game loop - console.log(`🚀 Starting automatic movement game loop for room ${roomId} (app session flow)`); - const { startGameLoop } = await import('./routes/gameRoutes.js'); - startGameLoop(roomId, roomManager); - - // Send the initial game state - const { formatGameState } = await import('./services/index.js'); - roomManager.broadcastToRoom( - roomId, - 'room:state', - formatGameState(room.gameState, roomId, room.betAmount) - ); - - } catch (error) { - logger.error(`Error handling app session start game for room ${roomId}:`, error); - return sendError(ws, 'START_GAME_ERROR', error.message); - } -} - -// Function to broadcast online users count to all clients -const broadcastOnlineUsersCount = () => { - const message = JSON.stringify({ - type: 'onlineUsers', - count: onlineUsersCount - }); - - wss.clients.forEach((client) => { - if (client.readyState === 1) { // WebSocket.OPEN - client.send(message); - } - }); - - logger.ws(`Broadcasting online users count: ${onlineUsersCount}`); -}; - -// Create context object to share between route handlers -const context = { - roomManager, - connections, - sendError: (ws, code, msg) => sendError(ws, code, msg) -}; - -wss.on('connection', (ws) => { - logger.ws('Client connected'); - - // Increment online users count and broadcast to all clients - onlineUsersCount++; - broadcastOnlineUsersCount(); - - // Handle client messages - ws.on('message', async (message) => { - let data; - try { - data = JSON.parse(message); - } catch (e) { - return sendError(ws, 'INVALID_JSON', 'Invalid JSON format'); - } - - // Process message based on type - try { - switch (data.type) { - case 'joinRoom': - await handleJoinRoom(ws, data.payload, context); - break; - case 'startGame': - await handleStartGame(ws, data.payload, context); - break; - case 'changeDirection': - await handleDirectionChange(ws, data.payload, context); - break; - case 'getAvailableRooms': - await handleGetAvailableRooms(ws, context); - break; - case 'appSession:signature': - await handleAppSessionSignature(ws, data.payload, context); - break; - case 'appSession:startGame': - await handleAppSessionStartGame(ws, data.payload, context); - break; - default: - sendError(ws, 'INVALID_MESSAGE_TYPE', 'Invalid message type'); - } - } catch (error) { - logger.error(`Error handling message type ${data.type}:`, error); - sendError(ws, 'INTERNAL_ERROR', 'An internal error occurred'); - } - }); - - // Handle disconnection - ws.on('close', () => { - // Find and remove the player from any room - for (const [eoa, connection] of connections.entries()) { - if (connection.ws === ws) { - const result = roomManager.leaveRoom(eoa); - if (result.success && result.roomId) { - roomManager.broadcastToRoom(result.roomId, 'room:state', { - roomId: result.roomId, - // Send updated room state here - }); - } - connections.delete(eoa); - break; - } - } - - // Decrement online users count and broadcast to all clients - onlineUsersCount = Math.max(0, onlineUsersCount - 1); - broadcastOnlineUsersCount(); - - logger.ws('Client disconnected'); - }); -}); - -// Initialize Nitrolite client and channel when server starts -async function initializeNitroliteServices() { - try { - logger.nitro('Initializing Nitrolite services...'); - const rpcClient = await initializeRPCClient(); - logger.nitro('Nitrolite RPC client initialized successfully'); - - // Check if we have an existing channel - if (rpcClient.channel) { - logger.nitro('Connected to existing channel'); - logger.data('Channel info', rpcClient.channel); - } else { - logger.warn('No channel established after initialization'); - logger.nitro('Channels will be created as needed via getChannelInfo'); - } - } catch (error) { - logger.error('Failed to initialize Nitrolite services:', error); - logger.system('Continuing in mock mode without Nitrolite channel'); - } -} - -// Start server -const port = process.env.PORT || 8080; -logger.system(`WebSocket server starting on port ${port}`); - -// Initialize Nitrolite client and channel -initializeNitroliteServices().then(() => { - logger.system('Server initialization complete'); -}).catch(error => { - logger.error('Server initialization failed:', error); -}); - -// Start keepalive mechanism -startPingInterval(wss); - -// Broadcast online users count periodically to ensure all clients have the latest count -setInterval(() => { - broadcastOnlineUsersCount(); -}, 30000); \ No newline at end of file diff --git a/examples/viper-duel/server/src/services/appSessions.js b/examples/viper-duel/server/src/services/appSessions.js deleted file mode 100644 index dbe40a850..000000000 --- a/examples/viper-duel/server/src/services/appSessions.js +++ /dev/null @@ -1,723 +0,0 @@ -/** - * Nitrolite app sessions for game rooms - * This file handles creating and closing app sessions for games - */ -import { - parseAnyRPCResponse, - RPCMethod, - createAppSessionMessage, - createCloseAppSessionMessage, -} from "@erc7824/nitrolite"; -import { ethers } from 'ethers'; -import dotenv from 'dotenv'; -import logger from '../utils/logger.js'; -import { getRPCClient } from './nitroliteRPC.js'; - -// Load environment variables -dotenv.config(); - -// Map to store app sessions by room ID -const roomAppSessions = new Map(); - -// Map to store pending app session signatures by room ID -const pendingAppSessions = new Map(); - -/** - * Generate app session message for multi-signature collection - * @param {string} roomId - Room ID - * @param {string} participantA - First player's address - * @param {string} participantB - Second player's address - * @param {number} betAmount - Bet amount for each player (0, 0.01, 0.1, 1, 2) - * @returns {Promise} The unsigned app session message and app definition - */ -export async function generateAppSessionMessage(roomId, participantA, participantB, betAmount = 0) { - try { - // Format addresses to proper checksum format - const formattedParticipantA = ethers.getAddress(participantA); - const formattedParticipantB = ethers.getAddress(participantB); - - logger.nitro(`Generating app session message for room ${roomId} with participants A: ${formattedParticipantA}, B: ${formattedParticipantB}`); - - // Check if we already have a pending session (to ensure consistency) - let pendingSession = pendingAppSessions.get(roomId); - - if (pendingSession) { - logger.nitro(`Using existing app session message for room ${roomId} - nonce: ${pendingSession.nonce}, requestToSign: ${JSON.stringify(pendingSession.requestToSign)}`); - return { - appSessionData: pendingSession.appSessionData, - appDefinition: pendingSession.appDefinition, - participants: [pendingSession.participantA, pendingSession.participantB, pendingSession.serverAddress], - requestToSign: pendingSession.requestToSign - }; - } - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Get the server's address and format it - const serverAddress = ethers.getAddress(rpcClient.address); - - // Create app definition with fixed nonce to ensure all participants sign the same message - const nonce = Date.now(); - const appDefinition = { - protocol: "viper_duel_nitrolite_v0", - participants: [formattedParticipantA, formattedParticipantB, serverAddress], - weights: [0, 0, 100], - quorum: 100, - challenge: 0, - nonce: nonce, - }; - - const appSessionData = [{ - definition: appDefinition, - allocations: [ - { - participant: formattedParticipantA, - asset: 'usdc', - amount: betAmount.toString(), - }, - { - participant: formattedParticipantB, - asset: 'usdc', - amount: betAmount.toString(), - }, - { - participant: serverAddress, - asset: 'usdc', - amount: '0', - }, - ] - }]; - - // Generate the complete request structure that everyone will sign - const sign = rpcClient.signMessage.bind(rpcClient); - const signedMessage = await createAppSessionMessage(sign, appSessionData); - const parsedMessage = JSON.parse(signedMessage); - - // Extract the request structure that clients should sign (same as what server signs) - const requestToSign = parsedMessage.req; - - logger.data(`Generated request structure for room ${roomId}:`, requestToSign); - - // Store the pending app session data including the request structure - pendingAppSessions.set(roomId, { - appSessionData, - appDefinition, - participantA: formattedParticipantA, - participantB: formattedParticipantB, - serverAddress, - betAmount, - signatures: new Map(), - createdAt: Date.now(), - nonce: nonce, - requestToSign: requestToSign, - originalSignedMessage: signedMessage - }); - - logger.nitro(`App session message generated for room ${roomId} with nonce ${nonce}`); - return { - appSessionData, - appDefinition, - participants: [formattedParticipantA, formattedParticipantB, serverAddress], - requestToSign: requestToSign - }; - - } catch (error) { - logger.error(`Error generating app session message for room ${roomId}:`, error); - throw error; - } -} - -/** - * Add a signature to the pending app session - * @param {string} roomId - Room ID - * @param {string} participantAddress - Address of the signing participant - * @param {string} signature - The participant's signature - * @returns {Promise} Whether all signatures are collected - */ -export async function addAppSessionSignature(roomId, participantAddress, signature) { - try { - // Format the participant address to proper checksum format - const formattedParticipantAddress = ethers.getAddress(participantAddress); - - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - throw new Error(`No pending app session found for room ${roomId}`); - } - - // Verify the participant is part of this session - const isValidParticipant = [pendingSession.participantA, pendingSession.participantB].includes(formattedParticipantAddress); - if (!isValidParticipant) { - throw new Error(`Invalid participant ${formattedParticipantAddress} for room ${roomId}`); - } - - // Store the signature - pendingSession.signatures.set(formattedParticipantAddress, signature); - - logger.nitro(`Added signature for ${formattedParticipantAddress} in room ${roomId} (${pendingSession.signatures.size}/2 collected)`); - logger.data(`Signature details:`, { participantAddress: formattedParticipantAddress, signature: signature.substring(0, 10) + '...', signatureLength: signature.length }); - - // Check if we have all participant signatures (not including server) - const allParticipantsSigned = pendingSession.signatures.has(pendingSession.participantA) && - pendingSession.signatures.has(pendingSession.participantB); - - return allParticipantsSigned; - - } catch (error) { - logger.error(`Error adding signature for room ${roomId}:`, error); - throw error; - } -} - -/** - * Create an app session with collected signatures - * @param {string} roomId - Room ID - * @returns {Promise} The app session ID - */ -export async function createAppSessionWithSignatures(roomId) { - try { - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - throw new Error(`No pending app session found for room ${roomId}`); - } - - // Verify all signatures are collected - const allSigned = pendingSession.signatures.has(pendingSession.participantA) && - pendingSession.signatures.has(pendingSession.participantB); - - if (!allSigned) { - throw new Error(`Not all signatures collected for room ${roomId}`); - } - - logger.nitro(`Creating app session with collected signatures for room ${roomId}`); - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Collect all signatures including server signature - const participantASignature = pendingSession.signatures.get(pendingSession.participantA); - const participantBSignature = pendingSession.signatures.get(pendingSession.participantB); - - logger.data(`Participant signatures for room ${roomId}:`, { - participantA: pendingSession.participantA, - participantB: pendingSession.participantB, - participantASignature, - participantBSignature, - allStoredSignatures: Array.from(pendingSession.signatures.entries()) - }); - - // Validate that we have all participant signatures - if (!participantASignature) { - throw new Error(`Missing signature from participant A: ${pendingSession.participantA}`); - } - if (!participantBSignature) { - throw new Error(`Missing signature from participant B: ${pendingSession.participantB}`); - } - - // Don't create a new server signature - use the existing signed message structure - // but replace the single server signature with all collected signatures - - // Create a properly formatted message with all signatures - // The signatures should be in the same order as participants: [participantA, participantB, server] - const allSignatures = [participantASignature, participantBSignature]; - - // Now let the server sign the same request structure as the clients - const sign = rpcClient.signMessage.bind(rpcClient); - - logger.data(`Server signing request structure for room ${roomId}:`, pendingSession.requestToSign); - - // Sign the same request structure that clients signed - const serverSignature = await sign(pendingSession.requestToSign); - - logger.data(`Server signature created:`, serverSignature); - - // Add server signature to complete the array - allSignatures.push(serverSignature); - - logger.data(`Combined signatures for room ${roomId}:`, allSignatures); - - // Send the message directly using ws.send - logger.nitro(`Sending app session creation message for room ${roomId}`); - - if (!rpcClient.ws || rpcClient.ws.readyState !== 1) { - throw new Error('WebSocket not connected or not in OPEN state'); - } - - // Create the final message with all signatures - const finalMessage = JSON.parse(pendingSession.originalSignedMessage); - finalMessage.sig = allSignatures; - - logger.data(`Final message structure:`, { - req: finalMessage.req, - signatures: finalMessage.sig, - participantsOrder: pendingSession.appSessionData[0].definition.participants, - messageToSend: JSON.stringify(finalMessage) - }); - - // Set up a promise to handle the response from the WebSocket - const appSessionResponsePromise = new Promise((resolve, reject) => { - const handleAppSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - logger.data(`Received app session creation response:`, message); - - - if (message.method === RPCMethod.CreateAppSession) { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - resolve(message.params); - } - - if (message.method === RPCMethod.Error) { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error(`Error: ${message.params.error}`)); - } - } catch (error) { - logger.error('Error handling app session response:', error); - } - }; - - rpcClient.ws.on('message', handleAppSessionResponse); - - setTimeout(() => { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error('App session creation timeout')); - }, 10000); - }); - - // Send the final message (convert to string) - rpcClient.ws.send(JSON.stringify(finalMessage)); - - // Wait for the response - const response = await appSessionResponsePromise; - - logger.data(`App session creation response for room ${roomId}:`, response); - - const appId = response?.app_session_id || response?.[0]?.app_session_id; - - if (!appId) { - throw new Error('Failed to get app ID from response'); - } - - // Store the app ID for this room - roomAppSessions.set(roomId, { - appId, - participantA: pendingSession.participantA, - participantB: pendingSession.participantB, - serverAddress: pendingSession.serverAddress, - tokenAddress: process.env.USDC_TOKEN_ADDRESS, - betAmount: pendingSession.betAmount, - createdAt: Date.now() - }); - - // Clean up pending session - pendingAppSessions.delete(roomId); - - logger.nitro(`Created app session with ID ${appId} for room ${roomId}`); - return appId; - - } catch (error) { - logger.error(`Error creating app session with signatures for room ${roomId}:`, error); - throw error; - } -} - -/** - * Create an app session for a game room (original function for backward compatibility) - * @param {string} roomId - Room ID - * @param {string} participantA - First player's address - * @param {string} participantB - Second player's address - * @param {number} betAmount - Bet amount for each player (0, 0.01, 0.1, 1, 2) - * @returns {Promise} The app session ID - */ -export async function createAppSession(roomId, participantA, participantB, betAmount = 0) { - try { - logger.nitro(`Creating app session for room ${roomId}`); - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Get the server's address - const serverAddress = rpcClient.address; - - // Check if token address is available - const tokenAddress = process.env.USDC_TOKEN_ADDRESS; - if (!tokenAddress) { - throw new Error('Token address not set in environment variables'); - } - - // Create app definition - const appDefinition = { - protocol: "app_aura_nitrolite_v0", - participants: [participantA, participantB, serverAddress], - weights: [0, 0, 100], - quorum: 100, - challenge: 0, - nonce: Date.now(), - }; - - // Use the RPC client's signMessage method for consistent signing - const sign = rpcClient.signMessage.bind(rpcClient); - - // Create the signed message - const signedMessage = await createAppSessionMessage( - sign, - [ - { - definition: appDefinition, - allocations: [ - { - participant: participantA, - asset: 'usdc', - amount: betAmount.toString(), - }, - { - participant: participantB, - asset: 'usdc', - amount: betAmount.toString(), - }, - { - participant: serverAddress, - asset: 'usdc', - amount: '0', - }, - ] - }, - ] - ); - logger.data(`Signed app session message for room ${roomId}:`, signedMessage); - // Send the message directly using ws.send, similar to authentication - logger.nitro(`Sending app session creation message for room ${roomId}`); - - if (!rpcClient.ws || rpcClient.ws.readyState !== 1) { // WebSocket.OPEN - throw new Error('WebSocket not connected or not in OPEN state'); - } - - // Set up a promise to handle the response from the WebSocket - const appSessionResponsePromise = new Promise((resolve, reject) => { - // Create a one-time message handler for the app session response - const handleAppSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - - logger.data(`Received app session creation response:`, message); - - // Check if this is an app session response - if (message.method === RPCMethod.CreateAppSession) { - // Remove the listener once we get the response - rpcClient.ws.removeListener('message', handleAppSessionResponse); - resolve(message.params); - } - - // Also check for error responses - if (message.method === RPCMethod.Error) { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error(`Error: ${message.params.error}`)); - } - } catch (error) { - logger.error('Error handling app session response:', error); - } - }; - - // Add the message handler - rpcClient.ws.on('message', handleAppSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - rpcClient.ws.removeListener('message', handleAppSessionResponse); - reject(new Error('App session creation timeout')); - }, 10000); - }); - - // Send the signed message directly - rpcClient.ws.send(signedMessage); - - // Wait for the response - const response = await appSessionResponsePromise; - - // Log the response - logger.data(`App session creation response for room ${roomId}:`, response); - - // The response structure might vary, adapt this based on actual response - const appId = response?.app_session_id || response?.[0]?.app_session_id; - - if (!appId) { - throw new Error('Failed to get app ID from response'); - } - - // Store the app ID for this room - roomAppSessions.set(roomId, { - appId, - participantA, - participantB, - serverAddress, - tokenAddress, - betAmount, - createdAt: Date.now() - }); - - logger.nitro(`Created app session with ID ${appId} for room ${roomId}`); - return appId; - - } catch (error) { - logger.error(`Error creating app session for room ${roomId}:`, error); - throw error; - } -} - -/** - * Close an app session with winner taking the allocation - * @param {string} roomId - Room ID - * @param {string} winnerId - Winner's participant ID ('A' or 'B'), null for tie - * @returns {Promise} Success status - */ -export async function closeAppSessionWithWinner(roomId, winnerId = null) { - try { - // Get the app session for this room - const appSession = roomAppSessions.get(roomId); - if (!appSession) { - logger.warn(`No app session found for room ${roomId}`); - return false; - } - - const { participantA, participantB, betAmount = 0 } = appSession; - - // Calculate allocations based on winner and bet amount - // Need to ensure exact redistribution of what was initially allocated - const betAmountStr = betAmount.toString(); - const totalPot = (betAmount * 2).toString(); - - let allocations; - if (winnerId === 'A') { - // Player A wins - gets all the funds - allocations = [totalPot, '0', '0']; // A gets both initial allocations - logger.nitro(`Player A (${participantA}) wins room ${roomId} - taking full allocation of ${totalPot}`); - } else if (winnerId === 'B') { - // Player B wins - gets all the funds - allocations = ['0', totalPot, '0']; // B gets both initial allocations - logger.nitro(`Player B (${participantB}) wins room ${roomId} - taking full allocation of ${totalPot}`); - } else { - // Tie or no winner - split evenly (return original amounts) - allocations = [betAmountStr, betAmountStr, '0']; - logger.nitro(`Tie in room ${roomId} - splitting allocation evenly: ${betAmountStr} each`); - } - - logger.data(`Allocation calculation for room ${roomId}:`, { - betAmount, - betAmountStr, - totalPot, - winnerId, - finalAllocations: allocations - }); - - // Use the existing closeAppSession function with calculated allocations - return await closeAppSession(roomId, allocations); - - } catch (error) { - logger.error(`Error closing app session with winner for room ${roomId}:`, error); - return false; - } -} - -/** - * Close an app session for a game room - * @param {string} roomId - Room ID - * @param {Array} [allocations=[0,0,0]] - Final allocations - * @returns {Promise} Success status - */ -export async function closeAppSession(roomId, allocations) { - try { - // Get the app session for this room - const appSession = roomAppSessions.get(roomId); - if (!appSession) { - logger.warn(`No app session found for room ${roomId}`); - return false; - } - - // Make sure appId exists and is properly extracted - const appId = appSession.appId; - if (!appId) { - logger.error(`No appId found in app session for room ${roomId}`); - return false; - } - - logger.nitro(`Closing app session ${appId} for room ${roomId}`); - - // Get the RPC client - const rpcClient = await getRPCClient(); - if (!rpcClient) { - throw new Error('RPC client not initialized'); - } - - // Extract participant addresses from the stored app session - const { participantA, participantB, serverAddress } = appSession; - - // Check if we have all the required participants - if (!participantA || !participantB || !serverAddress) { - throw new Error('Missing participant information in app session'); - } - - const finalAllocations = [ - { - participant: participantA, - asset: 'usdc', - amount: allocations[0].toString(), - }, - { - participant: participantB, - asset: 'usdc', - amount: allocations[1].toString(), - }, - { - participant: serverAddress, - asset: 'usdc', - amount: allocations[2].toString(), - }, - ]; - - // Log the allocations for debugging - const totalAmount = parseFloat(allocations[0]) + parseFloat(allocations[1]) + parseFloat(allocations[2]); - const expectedTotal = (appSession.betAmount || 0) * 2; - logger.data(`Final allocations for room ${roomId}:`, { - participantA: `${participantA} = ${allocations[0]}`, - participantB: `${participantB} = ${allocations[1]}`, - serverAddress: `${serverAddress} = ${allocations[2]}`, - totalAmount: totalAmount.toString(), - expectedTotal: expectedTotal.toString() - }); - - // Final allocations and close request - const closeRequest = { - app_session_id: appId, - allocations: finalAllocations, - }; - - // Use the RPC client's signMessage method for consistent signing - const sign = rpcClient.signMessage.bind(rpcClient); - - // Create the signed message - const signedMessage = await createCloseAppSessionMessage( - sign, - [closeRequest], - ); - - logger.data(`Signed app session close message for room ${roomId}:`, signedMessage); - - // Send the message directly using ws.send, similar to authentication - logger.nitro(`Sending app session close message for room ${roomId}`); - - if (!rpcClient.ws || rpcClient.ws.readyState !== 1) { // WebSocket.OPEN - throw new Error('WebSocket not connected or not in OPEN state'); - } - - // Set up a promise to handle the response from the WebSocket - const closeSessionResponsePromise = new Promise((resolve, reject) => { - // Create a one-time message handler for the close session response - const handleCloseSessionResponse = (data) => { - try { - const rawData = typeof data === 'string' ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - logger.data(`Received close session response:`, message); - - // Check if this is a close session response - if (message.method === RPCMethod.CloseAppSession) { - // Remove the listener once we get the response - rpcClient.ws.removeListener('message', handleCloseSessionResponse); - resolve(message.params); - } - - // Also check for error responses - if (message.method === RPCMethod.Error) { - rpcClient.ws.removeListener('message', handleCloseSessionResponse); - reject(new Error(`Error: ${message.params.error}`)); - } - } catch (error) { - logger.error('Error handling close session response:', error); - } - }; - - // Add the message handler - rpcClient.ws.on('message', handleCloseSessionResponse); - - // Set timeout to prevent hanging - setTimeout(() => { - rpcClient.ws.removeListener('message', handleCloseSessionResponse); - reject(new Error('Close session timeout')); - }, 10000); - }); - - // Send the signed message directly - rpcClient.ws.send(signedMessage); - - // Wait for the response - const response = await closeSessionResponsePromise; - - // Log the response - logger.data(`App session close response for room ${roomId}:`, response); - - // Remove the app session - roomAppSessions.delete(roomId); - - logger.nitro(`Closed app session ${appId} for room ${roomId}`); - return true; - - } catch (error) { - logger.error(`Error closing app session for room ${roomId}:`, error); - return false; - } -} - -/** - * Get the app session for a room - * @param {string} roomId - Room ID - * @returns {Object|null} The app session or null if not found - */ -export function getAppSession(roomId) { - return roomAppSessions.get(roomId) || null; -} - -/** - * Get existing pending app session message for a room - * @param {string} roomId - Room ID - * @returns {Object|null} The existing app session message or null if not found - */ -export function getPendingAppSessionMessage(roomId) { - const pendingSession = pendingAppSessions.get(roomId); - if (!pendingSession) { - return null; - } - - return { - appSessionData: pendingSession.appSessionData, - appDefinition: pendingSession.appDefinition, - participants: [pendingSession.participantA, pendingSession.participantB, pendingSession.serverAddress], - requestToSign: pendingSession.requestToSign - }; -} - -/** - * Check if a room has an app session - * @param {string} roomId - Room ID - * @returns {boolean} Whether the room has an app session - */ -export function hasAppSession(roomId) { - return roomAppSessions.has(roomId); -} - -/** - * Get all app sessions - * @returns {Map} Map of all app sessions - */ -export function getAllAppSessions() { - return roomAppSessions; -} diff --git a/examples/viper-duel/server/src/services/index.js b/examples/viper-duel/server/src/services/index.js deleted file mode 100644 index 75e923f09..000000000 --- a/examples/viper-duel/server/src/services/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Services index - exports all service modules - */ - -// Nitrolite RPC (WebSocket) client -export { - initializeRPCClient, - getRPCClient, - NitroliteRPCClient, - WSStatus -} from './nitroliteRPC.js'; - - -// App sessions for game rooms -export { - createAppSession, - closeAppSession, - getAppSession, - hasAppSession, - getAllAppSessions, - generateAppSessionMessage, - getPendingAppSessionMessage, - addAppSessionSignature, - createAppSessionWithSignatures -} from './appSessions.js'; - -// Room management -export { createRoomManager } from './roomManager.js'; - -// Snake game logic -export { createGame, changeDirection, updateGame, formatGameState, formatGameOverMessage } from './snake.js'; \ No newline at end of file diff --git a/examples/viper-duel/server/src/services/nitroliteOnChain.js b/examples/viper-duel/server/src/services/nitroliteOnChain.js deleted file mode 100644 index 8b5261788..000000000 --- a/examples/viper-duel/server/src/services/nitroliteOnChain.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Nitrolite on-chain operations (separate from WebSocket RPC) - * This file handles all interactions with the blockchain - */ -import { createPublicClient, createWalletClient, http } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { polygon } from "viem/chains"; -import dotenv from "dotenv"; -import logger from "../utils/logger.js"; - -// Load environment variables -dotenv.config(); - -// Singleton instances -let walletClient = null; -let publicClient = null; - -/** - * Initialize wallet and public clients - * @param {string} privateKey - Private key for the server wallet - * @returns {Object} The initialized clients - */ -export async function initializeClients(privateKey) { - try { - if (walletClient && publicClient) { - logger.nitro("Clients already initialized"); - return { walletClient, publicClient }; - } - - logger.nitro("Initializing wallet and public clients..."); - - // Create wallet from private key - const wallet = privateKeyToAccount(privateKey); - const address = wallet.address; - - logger.system(`Server wallet initialized with address: ${address}`); - - // Create public client - publicClient = createPublicClient({ - transport: http(process.env.POLYGON_RPC_URL), - chain: polygon, - }); - - // Create wallet client - walletClient = createWalletClient({ - transport: http(process.env.POLYGON_RPC_URL), - chain: polygon, - account: wallet, - }); - - logger.nitro("Wallet and public clients initialized successfully"); - return { walletClient, publicClient }; - } catch (error) { - logger.error("Error initializing clients:", error); - throw error; - } -} - -/** - * Get the existing wallet client or initialize a new one - * @param {string} privateKey - Private key for the server wallet - * @returns {Object} The wallet client instance - */ -export async function getWalletClient(privateKey) { - if (!walletClient && privateKey) { - await initializeClients(privateKey); - } - return walletClient; -} - -/** - * Get the existing public client or initialize a new one - * @param {string} privateKey - Private key for the server wallet - * @returns {Object} The public client instance - */ -export async function getPublicClient(privateKey) { - if (!publicClient && privateKey) { - await initializeClients(privateKey); - } - return publicClient; -} diff --git a/examples/viper-duel/server/src/services/nitroliteRPC.js b/examples/viper-duel/server/src/services/nitroliteRPC.js deleted file mode 100644 index 08a14a162..000000000 --- a/examples/viper-duel/server/src/services/nitroliteRPC.js +++ /dev/null @@ -1,552 +0,0 @@ -/** - * Nitrolite RPC (WebSocket) client - * This file handles all WebSocket communication with Nitrolite server - */ -import { - createAuthRequestMessage, - createAuthVerifyMessage, - createEIP712AuthMessageSigner, - createPingMessage, - NitroliteRPC, - parseAnyRPCResponse, - RPCMethod -} from "@erc7824/nitrolite"; -import dotenv from "dotenv"; -import { ethers } from "ethers"; -import WebSocket from "ws"; - -import logger from "../utils/logger.js"; - -import { getWalletClient } from "./nitroliteOnChain.js"; - -/** - * EIP-712 domain for auth_verify challenge - */ -const getAuthDomain = () => { - return { - name: "Viper Duel", - }; -}; - -const expire = String(Math.floor(Date.now() / 1000) + 24 * 60 * 60); - -// Load environment variables -dotenv.config(); - -// Connection status -export const WSStatus = { - CONNECTED: "connected", - CONNECTING: "connecting", - DISCONNECTED: "disconnected", - RECONNECTING: "reconnecting", - RECONNECT_FAILED: "reconnect_failed", - AUTH_FAILED: "auth_failed", - AUTHENTICATING: "authenticating", -}; - -// Server-side WebSocket client with authentication -export class NitroliteRPCClient { - constructor(url, privateKey) { - this.url = url; - this.privateKey = privateKey; - this.ws = null; - this.status = WSStatus.DISCONNECTED; - this.channel = null; - this.wallet = new ethers.Wallet(privateKey); - this.address = this.wallet.address; - this.pendingRequests = new Map(); - this.nextRequestId = 1; - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; - this.reconnectDelay = 1000; - this.reconnectTimeout = null; - this.onMessageCallbacks = []; - this.onStatusChangeCallbacks = []; - this.walletClient = null; - - logger.system(`RPC client initialized with address: ${this.address}`); - } - - // Register message callback - onMessage(callback) { - this.onMessageCallbacks.push(callback); - } - - // Register status change callback - onStatusChange(callback) { - this.onStatusChangeCallbacks.push(callback); - } - - // Connect to WebSocket server - async connect() { - if (this.status === WSStatus.CONNECTED || this.status === WSStatus.CONNECTING) { - logger.ws("Already connected or connecting..."); - return; - } - - try { - logger.ws(`Connecting to ${this.url}...`); - this.setStatus(WSStatus.CONNECTING); - - this.ws = new WebSocket(this.url); - - this.ws.on("open", async () => { - logger.ws("WebSocket connection established"); - this.setStatus(WSStatus.AUTHENTICATING); - try { - await this.authenticate(); - logger.auth("Successfully authenticated with the WebSocket server"); - this.reconnectAttempts = 0; - this.startPingInterval(); - } catch (error) { - logger.error("Authentication failed:", error); - this.setStatus(WSStatus.AUTH_FAILED); - this.ws.close(); - } - }); - - this.ws.on("message", (data) => { - this.handleMessage(data); - }); - - this.ws.on("error", (error) => { - logger.error("WebSocket error:", error); - }); - - this.ws.on("close", () => { - logger.ws("WebSocket connection closed"); - this.setStatus(WSStatus.DISCONNECTED); - clearInterval(this.pingInterval); - this.handleReconnect(); - }); - } catch (error) { - logger.error("Failed to connect:", error); - this.setStatus(WSStatus.DISCONNECTED); - this.handleReconnect(); - } - } - - // Update status and notify listeners - setStatus(status) { - const prevStatus = this.status; - this.status = status; - logger.ws(`Status changed: ${prevStatus} -> ${status}`); - this.onStatusChangeCallbacks.forEach((callback) => callback(status)); - } - - // Sign message function for non-auth requests - async signMessage(data) { - const messageStr = typeof data === "string" ? data : JSON.stringify(data); - const digestHex = ethers.id(messageStr); - const messageBytes = ethers.getBytes(digestHex); - const { serialized: signature } = this.wallet.signingKey.sign(messageBytes); - return signature; - } - - /** - * Authenticates with the WebSocket server using: - * 1. auth_request: empty signature with wallet address - * 2. auth_verify: EIP-712 signature for challenge verification - * - * @param timeout - Timeout in milliseconds for the entire process - * @returns A Promise that resolves when authenticated - */ - async authenticate(timeout = 10000) { - if (!this.ws) { - throw new Error("WebSocket not connected"); - } - - logger.auth("Starting authentication with SDK 0.2.11 flow..."); - logger.auth("- Wallet address:", this.address); - logger.auth("- EIP-712 signature for auth_verify challenge"); - - const authMessage = { - wallet: this.address, - participant: this.address, - app_name: "Viper Duel", - expire: expire, // 24 hours in seconds - scope: "console", - application: this.address, - allowances: [], - }; - - return new Promise((resolve, reject) => { - let authTimeoutId = null; - - const cleanup = () => { - if (authTimeoutId) { - clearTimeout(authTimeoutId); - authTimeoutId = null; - } - this.ws.removeEventListener("message", handleAuthResponse); - }; - - const resetTimeout = () => { - if (authTimeoutId) { - clearTimeout(authTimeoutId); - } - authTimeoutId = setTimeout(() => { - cleanup(); - reject(new Error("Authentication timeout")); - }, timeout); - }; - - const handleAuthResponse = async (event) => { - const data = event.data || event; - - try { - const response = parseAnyRPCResponse(data); - - // Check for challenge response: {"res": [id, "auth_challenge", {"challenge": "uuid"}, timestamp]} - if (response.method === RPCMethod.AuthChallenge) { - logger.auth("Received auth_challenge, preparing EIP-712 auth_verify..."); - resetTimeout(); // Reset timeout while we process and send verify - - try { - logger.auth("Creating EIP-712 signing function..."); - - // Ensure we have a wallet client for EIP-712 signing - if (!this.walletClient) { - logger.auth("Initializing wallet client for EIP-712 signing..."); - this.walletClient = await getWalletClient(this.privateKey); - } - - const eip712SigningFunction = createEIP712AuthMessageSigner( - this.walletClient, - { - scope: authMessage.scope, - application: authMessage.application, - participant: authMessage.participant, - expire: authMessage.expire, - allowances: authMessage.allowances.map((allowance) => ({ - asset: allowance.symbol || allowance.asset, - amount: allowance.amount.toString(), - })), - }, - getAuthDomain(), - ); - - logger.auth("Calling createAuthVerifyMessage..."); - const authVerify = await createAuthVerifyMessage(eip712SigningFunction, response); - - logger.auth("Sending auth_verify with EIP-712 signature"); - this.ws.send(authVerify); - logger.auth("auth_verify sent successfully"); - } catch (eip712Error) { - logger.error("Error creating EIP-712 auth_verify:", eip712Error); - logger.error("Error stack:", eip712Error.stack); - - cleanup(); - reject(new Error(`EIP-712 auth_verify failed: ${eip712Error.message}`)); - return; - } - } - // Check for success response - else if (response.method === RPCMethod.AuthVerify) { - if (!response.params.success) { - return; - } - logger.auth("Authentication successful"); - - cleanup(); - - // Set status to connected - this.setStatus(WSStatus.CONNECTED); - - try { - // Request channel information for our address and check if we - // need to create one - const channels = await this.getChannelInfo(); - // Check if we have valid channels - const hasValidChannel = channels && Array.isArray(channels) && channels.length > 0 && channels[0] !== null; - - if (!hasValidChannel) { - logger.nitro("No valid channels found after authentication, will create one"); - } - } catch (error) { - logger.error("Failed to get channel info, continuing anyway:", error); - } - - resolve(); - } - // Check for error response - else if (response.method === RPCMethod.Error) { - const errorMsg = response.params.error || "Authentication failed"; - - logger.error("Authentication failed:", errorMsg); - cleanup(); - reject(new Error(String(errorMsg))); - } else { - logger.auth("Received non-auth message during auth, continuing to listen:", response); - // Keep listening if it wasn't a final success/error - } - } catch (error) { - // Ignore non-auth methods during authentication - if (error.message && error.message.includes("Unknown method:")) { - logger.auth("Ignoring non-auth message during authentication:", error.message); - return; - } - - logger.error("Error handling auth response:", error); - logger.error("Error stack:", error.stack); - cleanup(); - reject(new Error(`Authentication error: ${error instanceof Error ? error.message : String(error)}`)); - } - }; - - // Step 1: Send auth_request - const sendAuthRequest = async () => { - try { - logger.auth("Sending auth_request..."); - const authRequest = await createAuthRequestMessage(authMessage); - this.ws.send(authRequest); - logger.auth("auth_request sent successfully"); - } catch (requestError) { - logger.error("Error creating auth_request:", requestError); - cleanup(); - reject(new Error(`Failed to create auth_request: ${requestError.message}`)); - } - }; - - this.ws.addEventListener("message", handleAuthResponse); - resetTimeout(); // Start the initial timeout - - // Start authentication process - sendAuthRequest(); - }); - } - - // Handle incoming WebSocket messages - handleMessage(data) { - try { - // Ensure data is properly handled as string - const rawData = typeof data === "string" ? data : data.toString(); - const message = parseAnyRPCResponse(rawData); - logger.data("Received message", message); - - // Notify callbacks first to allow for authentication handling - this.onMessageCallbacks.forEach((callback) => callback(message)); - const requestId = message.requestId; - - // Handle response to pending requests - if (message.method === RPCMethod.GetChannels || message.method === RPCMethod.ChannelsUpdate) { - let channels = []; - if (message.method === RPCMethod.GetChannels) { - channels = message.params; - } else { - channels = [message.params]; - } - if (this.pendingRequests.has(requestId)) { - const { resolve } = this.pendingRequests.get(requestId); - resolve(channels); - this.pendingRequests.delete(requestId); - } - } - - // Handle errors - if (message.method === RPCMethod.Error) { - if (this.pendingRequests.has(requestId)) { - const { reject } = this.pendingRequests.get(requestId); - reject(new Error(`Error: ${message.params[0].error}`)); - this.pendingRequests.delete(requestId); - } - } - } catch (error) { - logger.error("Error handling message:", error); - } - } - - // Send a request to the WebSocket server - async sendRequest(method, params = {}) { - if (!this.ws) { - throw new Error("WebSocket instance not initialized"); - } - - if (this.ws.readyState !== WebSocket.OPEN) { - logger.error(`WebSocket not in OPEN state. Current state: ${this.ws.readyState}, Status: ${this.status}`); - throw new Error(`WebSocket not in OPEN state. Current readyState: ${this.ws.readyState}`); - } - - if (this.status !== WSStatus.CONNECTED) { - logger.warn(`WebSocket status is ${this.status}, should be ${WSStatus.CONNECTED}. Proceeding anyway.`); - if (this.status === WSStatus.AUTHENTICATING) { - logger.system("Fixing status to CONNECTED for authenticated connection"); - this.setStatus(WSStatus.CONNECTED); - } - } - - const requestId = this.nextRequestId++; - const sign = this.signMessage.bind(this); - - return new Promise(async (resolve, reject) => { - try { - const request = NitroliteRPC.createRequest(requestId, method, params); - const signedRequest = await NitroliteRPC.signRequestMessage(request, sign); - - logger.ws(`Sending request: ${JSON.stringify(signedRequest).slice(0, 100)}...`); - - this.pendingRequests.set(requestId, { resolve, reject }); - - setTimeout(() => { - if (this.pendingRequests.has(requestId)) { - this.pendingRequests.delete(requestId); - reject(new Error("Request timeout")); - } - }, 10000); - - this.ws.send(typeof signedRequest === "string" ? signedRequest : JSON.stringify(signedRequest)); - } catch (error) { - logger.error("Error sending request:", error); - this.pendingRequests.delete(requestId); - reject(error); - } - }); - } - - // Start ping interval to keep connection alive - startPingInterval() { - clearInterval(this.pingInterval); - this.pingInterval = setInterval(async () => { - if (this.status === WSStatus.CONNECTED) { - try { - const sign = this.signMessage.bind(this); - const pingMessage = await createPingMessage(sign); - this.ws.send(pingMessage); - } catch (error) { - logger.error("Error sending ping:", error); - } - } - }, 30000); - } - - // Handle reconnection - handleReconnect() { - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - logger.ws("Maximum reconnect attempts reached"); - this.setStatus(WSStatus.RECONNECT_FAILED); - return; - } - - this.reconnectAttempts++; - const delay = this.reconnectDelay * this.reconnectAttempts; - - logger.ws(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); - this.setStatus(WSStatus.RECONNECTING); - - clearTimeout(this.reconnectTimeout); - this.reconnectTimeout = setTimeout(() => { - this.connect(); - }, delay); - } - - // Close connection - close() { - clearInterval(this.pingInterval); - clearTimeout(this.reconnectTimeout); - - if (this.ws) { - this.ws.close(); - this.ws = null; - } - - logger.ws("WebSocket connection closed manually"); - this.setStatus(WSStatus.DISCONNECTED); - } - - // Get channel information - async getChannelInfo() { - try { - logger.nitro("Requesting channel information..."); - const response = await this.sendRequest("get_channels", [{ participant: this.address }]); - logger.data("Channel info received", response); - - logger.system("Debug - Raw channel response:"); - logger.system(`- response type: ${typeof response}`); - logger.system(`- is array: ${Array.isArray(response)}`); - logger.system(`- stringified: ${JSON.stringify(response)}`); - - let channels = response; - - if (Array.isArray(response) && response.length === 1 && response[0] === null) { - logger.system("Debug - Got array with single null item"); - } - - if (channels && Array.isArray(channels) && channels.length > 0 && channels[0] !== null) { - logger.nitro(`Found ${channels.length} valid existing channels`); - this.channel = channels[0]; - return channels; - } - - logger.nitro("No valid channels found"); - - if (!this.walletClient) { - logger.nitro("Getting wallet client..."); - this.walletClient = await getWalletClient(this.privateKey); - } - - return []; - } catch (error) { - logger.error("Error getting channel info:", error); - throw error; - } - } -} - -// Initialize and export the client instance -let rpcClient = null; - -export async function initializeRPCClient() { - logger.system("Initializing Nitrolite RPC client..."); - if (rpcClient) { - logger.system("Nitrolite RPC client already initialized, returning existing instance..."); - return rpcClient; - } - - try { - logger.system("Initializing new Nitrolite RPC client..."); - - if (!process.env.SERVER_PRIVATE_KEY) { - throw new Error("SERVER_PRIVATE_KEY environment variable is not set"); - } - - if (!process.env.WS_URL) { - throw new Error("WS_URL environment variable is not set"); - } - - rpcClient = new NitroliteRPCClient(process.env.WS_URL, process.env.SERVER_PRIVATE_KEY); - - rpcClient.onMessage((message) => { - logger.ws("RPC Message:", JSON.stringify(message, null, 2)); - }); - - rpcClient.onStatusChange((status) => { - logger.ws("RPC Status changed:", status); - }); - - // Initialize wallet client before connecting (needed for authentication) - rpcClient.walletClient = await getWalletClient(process.env.SERVER_PRIVATE_KEY); - - await rpcClient.connect(); - - logger.system("Checking for existing channels..."); - const channels = await rpcClient.getChannelInfo(); - - const hasValidChannel = channels && Array.isArray(channels) && channels.length > 0 && channels[0] !== null; - - if (hasValidChannel) { - logger.nitro(`Found ${channels.length} existing valid channels`); - logger.data("Channel data", channels[0]); - rpcClient.channel = channels[0]; - } else { - logger.nitro("No valid channels found in initializeRPCClient"); - } - } catch (error) { - logger.error("Error during RPC client initialization:", error); - } - - return rpcClient; -} - -export function getRPCClient() { - return rpcClient; -} diff --git a/examples/viper-duel/server/src/services/roomManager.js b/examples/viper-duel/server/src/services/roomManager.js deleted file mode 100644 index c3d330caf..000000000 --- a/examples/viper-duel/server/src/services/roomManager.js +++ /dev/null @@ -1,318 +0,0 @@ -/** - * Room manager for the tic-tac-toe game - */ - -import { v4 as uuidv4 } from 'uuid'; -import { ethers } from 'ethers'; -import { changeDirection, updateGame } from './snake.js'; - -/** - * @typedef {Object} Room - * @property {string} id - Unique room identifier - * @property {Object} players - Map of player roles - * @property {string|null} players.host - Host's Ethereum address (X player) - * @property {string|null} players.guest - Guest's Ethereum address (O player) - * @property {Map} connections - Map of player connections by EOA - * @property {Object|null} gameState - Current game state - * @property {boolean} isReady - Whether the room is ready to start - */ - -/** - * @typedef {Object} RoomManager - * @property {Map} rooms - Map of active rooms by ID - * @property {Map} addressToRoom - Map of addresses to room IDs - */ - -/** - * Creates a new room manager - * @returns {RoomManager} Room manager instance - */ -export function createRoomManager() { - // In-memory storage for rooms and address-to-room mapping - const rooms = new Map(); - const addressToRoom = new Map(); - - /** - * Creates a new room - * @param {number} betAmount - Bet amount for the room (0, 0.01, 0.1, 1, 2) - * @returns {string} Room ID - */ - function createRoom(betAmount = 0) { - const roomId = uuidv4(); - rooms.set(roomId, { - id: roomId, - players: { - host: null, - guest: null - }, - connections: new Map(), - gameState: null, - isReady: false, - createdAt: Date.now(), - betAmount: betAmount - }); - return roomId; - } - - /** - * Adds a player to a room - * @param {string} roomId - Room ID - * @param {string} eoa - Player's Ethereum address - * @param {Object} ws - WebSocket connection - * @param {number} betAmount - Bet amount for joining (must match room bet) - * @returns {Object} Result with success flag and additional info - */ - function joinRoom(roomId, eoa, ws, betAmount = 0) { - // Format address to proper checksum format - const formattedEoa = ethers.getAddress(eoa); - - // Check if player is already in another room - if (addressToRoom.has(formattedEoa)) { - return { - success: false, - error: 'Player already in another room' - }; - } - - // Get the room - if roomId doesn't exist, return error - if (!rooms.has(roomId)) { - return { - success: false, - error: 'Room not found' - }; - } - - let room = rooms.get(roomId); - - // Check if bet amount matches room bet amount - if (room.betAmount !== betAmount) { - return { - success: false, - error: `Bet amount mismatch. Room requires $${room.betAmount}` - }; - } - - // Check if room is full - if (room.players.host && room.players.guest) { - return { - success: false, - error: 'Room is full' - }; - } - - // Assign player to available role - let role; - if (!room.players.host) { - room.players.host = formattedEoa; - role = 'host'; - } else if (!room.players.guest) { - room.players.guest = formattedEoa; - role = 'guest'; - } - - // Store connection and map address to room - room.connections.set(formattedEoa, { ws, role }); - addressToRoom.set(formattedEoa, roomId); - - // Check if room has both players (ready to potentially start) - const isRoomFull = room.players.host && room.players.guest; - - // Mark as ready when both players are present, but don't auto-start the game - if (isRoomFull && !room.isReady) { - room.isReady = true; - console.log(`Room ${roomId} is ready with players: ${room.players.host} (host) and ${room.players.guest} (guest)`); - // The game will be created when the host clicks "Start Game" - } - - return { - success: true, - roomId, - role, - isRoomReady: isRoomFull - }; - } - - /** - * Process a direction change in the snake game - * @param {string} roomId - Room ID - * @param {string} direction - New direction ('UP', 'DOWN', 'LEFT', 'RIGHT') - * @param {string} eoa - Player's Ethereum address - * @returns {Object} Result with success flag and additional info - */ - function processDirectionChange(roomId, direction, eoa) { - // Format address to proper checksum format - const formattedEoa = ethers.getAddress(eoa); - - if (!rooms.has(roomId)) { - return { - success: false, - error: 'Room not found' - }; - } - - const room = rooms.get(roomId); - - // Check if player is in this room - if (!room.connections.has(formattedEoa)) { - return { - success: false, - error: 'Player not in this room' - }; - } - - // Check if the game has started - if (!room.gameState) { - return { - success: false, - error: 'Game has not started' - }; - } - - // Change direction - const result = changeDirection(room.gameState, direction, formattedEoa); - if (!result.success) { - return result; - } - - // Update game state - room.gameState = result.gameState; - - return { - success: true, - gameState: room.gameState - }; - } - - /** - * Removes a player from a room - * @param {string} eoa - Player's Ethereum address - * @returns {Object} Result with success flag and removed room info - */ - function leaveRoom(eoa) { - // Format address to proper checksum format - const formattedEoa = ethers.getAddress(eoa); - - if (!addressToRoom.has(formattedEoa)) { - return { - success: false, - error: 'Player not in any room' - }; - } - - const roomId = addressToRoom.get(formattedEoa); - const room = rooms.get(roomId); - - // Clean up player connections - if (room) { - room.connections.delete(formattedEoa); - - // Update player list - if (room.players.host === formattedEoa) { - room.players.host = null; - } else if (room.players.guest === formattedEoa) { - room.players.guest = null; - } - - // Clean up room if empty - if (!room.players.host && !room.players.guest) { - rooms.delete(roomId); - } - } - - addressToRoom.delete(formattedEoa); - - return { - success: true, - roomId - }; - } - - /** - * Broadcasts a message to all players in a room - * @param {string} roomId - Room ID - * @param {string} type - Message type - * @param {Object} data - Message data - */ - function broadcastToRoom(roomId, type, data) { - if (!rooms.has(roomId)) return; - - const room = rooms.get(roomId); - const message = JSON.stringify({ type, ...data }); - - for (const connection of room.connections.values()) { - if (connection.ws.readyState === 1) { // WebSocket.OPEN - connection.ws.send(message); - } - } - } - - /** - * Closes a room and notifies all players - * @param {string} roomId - Room ID - */ - function closeRoom(roomId) { - if (!rooms.has(roomId)) return; - - const room = rooms.get(roomId); - - // Remove all players from the room - for (const eoa of room.connections.keys()) { - addressToRoom.delete(eoa); - } - - // Delete the room - rooms.delete(roomId); - } - - /** - * Updates the game state (moves snakes, handles collisions) - * @param {string} roomId - Room ID - * @returns {Object} Result with success flag and additional info - */ - function updateGameState(roomId) { - if (!rooms.has(roomId)) { - return { - success: false, - error: 'Room not found' - }; - } - - const room = rooms.get(roomId); - - // Check if the game has started - if (!room.gameState) { - return { - success: false, - error: 'Game has not started' - }; - } - - // Update game state - const result = updateGame(room.gameState); - if (!result.success) { - return result; - } - - // Update room's game state - room.gameState = result.gameState; - - return { - success: true, - gameState: room.gameState, - isGameOver: room.gameState.isGameOver - }; - } - - // Return public API - return { - rooms, - addressToRoom, - createRoom, - joinRoom, - processDirectionChange, - updateGameState, - leaveRoom, - broadcastToRoom, - closeRoom - }; -} \ No newline at end of file diff --git a/examples/viper-duel/server/src/services/snake.js b/examples/viper-duel/server/src/services/snake.js deleted file mode 100644 index 67aaa9a4e..000000000 --- a/examples/viper-duel/server/src/services/snake.js +++ /dev/null @@ -1,502 +0,0 @@ -/** - * Snake game engine - */ -import { ethers } from 'ethers'; - -const GRID_WIDTH = 20; -const GRID_HEIGHT = 20; -const INITIAL_SNAKE_LENGTH = 3; - -/** - * @typedef {Object} Position - * @property {number} x - X coordinate - * @property {number} y - Y coordinate - */ - -/** - * @typedef {Object} Snake - * @property {Array} body - Array of positions representing snake segments - * @property {string} direction - Current direction ('UP', 'DOWN', 'LEFT', 'RIGHT') - * @property {boolean} alive - Whether the snake is alive - * @property {number} score - Player's score - */ - -/** - * @typedef {Object} GameState - * @property {Object} snakes - Object with snake data for each player - * @property {Snake} snakes.player1 - Player 1's snake - * @property {Snake} snakes.player2 - Player 2's snake - * @property {Array} food - Array of food positions - * @property {string|null} winner - The winner of the game ('player1', 'player2', or null if no winner yet) - * @property {boolean} isGameOver - Whether the game is over - * @property {Object} players - Object with player information - * @property {string} players.player1 - EOA address of player 1 - * @property {string} players.player2 - EOA address of player 2 - * @property {number} gameTime - Game time in seconds - */ - -/** - * Creates initial snake at starting position - * @param {number} startX - Starting X position - * @param {number} startY - Starting Y position - * @param {string} direction - Initial direction - * @returns {Snake} Initial snake - */ -function createInitialSnake(startX, startY, direction) { - const body = []; - - // Create snake segments based on direction - for (let i = 0; i < INITIAL_SNAKE_LENGTH; i++) { - switch (direction) { - case 'RIGHT': - body.push({ x: startX - i, y: startY }); - break; - case 'LEFT': - body.push({ x: startX + i, y: startY }); - break; - case 'DOWN': - body.push({ x: startX, y: startY - i }); - break; - case 'UP': - body.push({ x: startX, y: startY + i }); - break; - } - } - - return { - body, - direction, - alive: true, - score: 0 - }; -} - -/** - * Creates a new game state - * @param {string} hostEoa - Host's Ethereum address (player 1) - * @param {string} guestEoa - Guest's Ethereum address (player 2) - * @returns {GameState} Initial game state - */ -export function createGame(hostEoa, guestEoa) { - // Format addresses to proper checksum format - const formattedHostEoa = ethers.getAddress(hostEoa); - const formattedGuestEoa = ethers.getAddress(guestEoa); - - return { - snakes: { - player1: createInitialSnake(3, 3, 'RIGHT'), - player2: createInitialSnake(GRID_WIDTH - 4, GRID_HEIGHT - 4, 'LEFT') - }, - food: [spawnFood(), spawnFood(), spawnFood()], // Start with 3 food items - winner: null, - isGameOver: false, - players: { - player1: formattedHostEoa, - player2: formattedGuestEoa - }, - gameTime: 0 - }; -} - -/** - * Spawns food at a random empty position - * @param {GameState} gameState - Current game state (optional) - * @returns {Position} Food position - */ -function spawnFood(gameState = null) { - let position; - let attempts = 0; - const maxAttempts = 100; - - do { - position = { - x: Math.floor(Math.random() * GRID_WIDTH), - y: Math.floor(Math.random() * GRID_HEIGHT) - }; - attempts++; - } while (gameState && isPositionOccupied(position, gameState) && attempts < maxAttempts); - - return position; -} - -/** - * Checks if a position is occupied by a snake - * @param {Position} position - Position to check - * @param {GameState} gameState - Current game state - * @returns {boolean} Whether position is occupied - */ -function isPositionOccupied(position, gameState) { - for (const snake of Object.values(gameState.snakes)) { - for (const segment of snake.body) { - if (segment.x === position.x && segment.y === position.y) { - return true; - } - } - } - return false; -} - -/** - * Changes snake direction - * @param {GameState} gameState - Current game state - * @param {string} direction - New direction ('UP', 'DOWN', 'LEFT', 'RIGHT') - * @param {string} playerEoa - Player's Ethereum address - * @returns {Object} Result with updated game state or error - */ -export function changeDirection(gameState, direction, playerEoa) { - // Format player address to proper checksum format - const formattedPlayerEoa = ethers.getAddress(playerEoa); - - // Check if the game is already over - if (gameState.isGameOver) { - return { success: false, error: 'Game is already over' }; - } - - // Determine which player is making the move - const playerId = gameState.players.player1 === formattedPlayerEoa ? 'player1' : 'player2'; - if (!gameState.players[playerId]) { - return { success: false, error: 'Player not in this game' }; - } - - const snake = gameState.snakes[playerId]; - if (!snake.alive) { - return { success: false, error: 'Snake is dead' }; - } - - // Update direction only - automatic timer will handle movement - const updatedGameState = { - ...gameState, - snakes: { - ...gameState.snakes, - [playerId]: { - ...snake, - direction - } - } - }; - - return { - success: true, - gameState: updatedGameState - }; -} - -/** - * Moves a single snake forward and handles collisions - * @param {GameState} gameState - Current game state - * @param {string} playerId - The player whose snake to move - * @returns {Object} Result with updated game state - */ -function moveSnake(gameState, playerId) { - if (gameState.isGameOver) { - return { success: true, gameState }; - } - - const snake = gameState.snakes[playerId]; - if (!snake.alive) { - return { success: true, gameState }; - } - - const head = snake.body[0]; - let newHead; - - // Calculate new head position - switch (snake.direction) { - case 'UP': - newHead = { x: head.x, y: head.y - 1 }; - break; - case 'DOWN': - newHead = { x: head.x, y: head.y + 1 }; - break; - case 'LEFT': - newHead = { x: head.x - 1, y: head.y }; - break; - case 'RIGHT': - newHead = { x: head.x + 1, y: head.y }; - break; - } - - // Handle screen wraparound (no wall collision) - if (newHead.x < 0) { - newHead.x = GRID_WIDTH - 1; // Wrap to right side - } else if (newHead.x >= GRID_WIDTH) { - newHead.x = 0; // Wrap to left side - } - - if (newHead.y < 0) { - newHead.y = GRID_HEIGHT - 1; // Wrap to bottom - } else if (newHead.y >= GRID_HEIGHT) { - newHead.y = 0; // Wrap to top - } - - // Wraparound completed - - // Check self collision - const selfCollision = snake.body.some(segment => - segment.x === newHead.x && segment.y === newHead.y - ); - if (selfCollision) { - console.log(`🔄 ${playerId} collided with self at (${newHead.x}, ${newHead.y})`); - const updatedGameState = { - ...gameState, - snakes: { - ...gameState.snakes, - [playerId]: { ...snake, alive: false } - } - }; - return { success: true, gameState: updatedGameState }; - } - - // Check collision with other snake - const otherPlayerId = playerId === 'player1' ? 'player2' : 'player1'; - const otherSnake = gameState.snakes[otherPlayerId]; - const otherSnakeCollision = otherSnake.body.some(segment => - segment.x === newHead.x && segment.y === newHead.y - ); - if (otherSnakeCollision) { - console.log(`🐍 ${playerId} collided with ${otherPlayerId} at (${newHead.x}, ${newHead.y})`); - const updatedGameState = { - ...gameState, - snakes: { - ...gameState.snakes, - [playerId]: { ...snake, alive: false } - } - }; - return { success: true, gameState: updatedGameState }; - } - - // Check food collision - let updatedFood = [...gameState.food]; - const foodIndex = updatedFood.findIndex(food => - food.x === newHead.x && food.y === newHead.y - ); - - let newBody; - let newScore = snake.score; - if (foodIndex !== -1) { - // Ate food - grow snake - newBody = [newHead, ...snake.body]; - updatedFood.splice(foodIndex, 1); - newScore = snake.score + 1; - - console.log(`🍎 ${playerId} ate food! New score: ${newScore}`); - - // Spawn new food to maintain at least 3 food items - while (updatedFood.length < 3) { - updatedFood.push(spawnFood({ ...gameState, food: updatedFood })); - } - } else { - // Normal move - don't grow - newBody = [newHead, ...snake.body.slice(0, -1)]; - } - - const updatedGameState = { - ...gameState, - snakes: { - ...gameState.snakes, - [playerId]: { - ...snake, - body: newBody, - score: newScore - } - }, - food: updatedFood, - gameTime: gameState.gameTime + 1 - }; - - // Check for game over conditions - const aliveSnakes = Object.values(updatedGameState.snakes).filter(s => s.alive); - console.log(`🔍 Game state check: player1 alive=${updatedGameState.snakes.player1.alive}, player2 alive=${updatedGameState.snakes.player2.alive}, alive count=${aliveSnakes.length}`); - - if (aliveSnakes.length === 0) { - console.log(`🏁 Game over - Both snakes died`); - updatedGameState.isGameOver = true; - updatedGameState.winner = null; - } else if (aliveSnakes.length === 1) { - const winningPlayerId = Object.keys(updatedGameState.snakes).find( - id => updatedGameState.snakes[id].alive - ); - console.log(`🏆 Game over - ${winningPlayerId} wins!`); - updatedGameState.winner = winningPlayerId; - updatedGameState.isGameOver = true; - } else { - console.log(`✅ Game continues - both snakes alive`); - } - - return { success: true, gameState: updatedGameState }; -} - -/** - * Moves all snakes forward and handles collisions - * @param {GameState} gameState - Current game state - * @returns {Object} Result with updated game state - */ -export function updateGame(gameState) { - if (gameState.isGameOver) { - return { success: true, gameState }; - } - - const updatedSnakes = { ...gameState.snakes }; - let updatedFood = [...gameState.food]; - - // Move each alive snake - for (const [playerId, snake] of Object.entries(updatedSnakes)) { - if (!snake.alive) continue; - - const head = snake.body[0]; - let newHead; - - // Calculate new head position - switch (snake.direction) { - case 'UP': - newHead = { x: head.x, y: head.y - 1 }; - break; - case 'DOWN': - newHead = { x: head.x, y: head.y + 1 }; - break; - case 'LEFT': - newHead = { x: head.x - 1, y: head.y }; - break; - case 'RIGHT': - newHead = { x: head.x + 1, y: head.y }; - break; - } - - // Handle screen wraparound (no wall collision) - if (newHead.x < 0) { - newHead.x = GRID_WIDTH - 1; // Wrap to right side - } else if (newHead.x >= GRID_WIDTH) { - newHead.x = 0; // Wrap to left side - } - - if (newHead.y < 0) { - newHead.y = GRID_HEIGHT - 1; // Wrap to bottom - } else if (newHead.y >= GRID_HEIGHT) { - newHead.y = 0; // Wrap to top - } - - // Wraparound completed - - // Check self collision - const selfCollision = snake.body.some(segment => - segment.x === newHead.x && segment.y === newHead.y - ); - if (selfCollision) { - console.log(`🔄 ${playerId} collided with self at (${newHead.x}, ${newHead.y})`); - updatedSnakes[playerId] = { ...snake, alive: false }; - continue; - } - - // Check collision with other snake - const otherPlayerId = playerId === 'player1' ? 'player2' : 'player1'; - const otherSnake = updatedSnakes[otherPlayerId]; - const otherSnakeCollision = otherSnake.body.some(segment => - segment.x === newHead.x && segment.y === newHead.y - ); - if (otherSnakeCollision) { - console.log(`🐍 ${playerId} collided with ${otherPlayerId} at (${newHead.x}, ${newHead.y})`); - updatedSnakes[playerId] = { ...snake, alive: false }; - continue; - } - - // Check food collision - const foodIndex = updatedFood.findIndex(food => - food.x === newHead.x && food.y === newHead.y - ); - - let newBody; - if (foodIndex !== -1) { - // Ate food - grow snake - newBody = [newHead, ...snake.body]; - updatedFood.splice(foodIndex, 1); - updatedSnakes[playerId] = { - ...snake, - body: newBody, - score: snake.score + 1 - }; - - // Spawn new food to maintain at least 3 food items - const newGameState = { - ...gameState, - snakes: updatedSnakes, - food: updatedFood - }; - - // Always keep at least 3 food items on the board - while (updatedFood.length < 3) { - updatedFood.push(spawnFood(newGameState)); - } - } else { - // Normal move - don't grow - newBody = [newHead, ...snake.body.slice(0, -1)]; - updatedSnakes[playerId] = { - ...snake, - body: newBody - }; - } - } - - // Check for game over conditions - const aliveSnakes = Object.values(updatedSnakes).filter(snake => snake.alive); - let winner = null; - let isGameOver = false; - - if (aliveSnakes.length === 0) { - // Both snakes died - tie - isGameOver = true; - } else if (aliveSnakes.length === 1) { - // One snake alive - winner - const winningPlayerId = Object.keys(updatedSnakes).find( - playerId => updatedSnakes[playerId].alive - ); - winner = winningPlayerId; - isGameOver = true; - } - - const updatedGameState = { - ...gameState, - snakes: updatedSnakes, - food: updatedFood, - winner, - isGameOver, - gameTime: gameState.gameTime + 1 - }; - - return { - success: true, - gameState: updatedGameState - }; -} - -/** - * Formats game state for client consumption - * @param {GameState} gameState - Current game state - * @param {string} roomId - Room ID - * @returns {Object} Formatted game state for client - */ -export function formatGameState(gameState, roomId, betAmount = 0) { - return { - roomId, - snakes: gameState.snakes, - food: gameState.food, - players: gameState.players, - gameTime: gameState.gameTime, - betAmount: betAmount - }; -} - -/** - * Formats game over message - * @param {GameState} gameState - Current game state - * @returns {Object} Game over message - */ -export function formatGameOverMessage(gameState) { - return { - winner: gameState.winner, - finalScores: { - player1: gameState.snakes.player1.score, - player2: gameState.snakes.player2.score - }, - gameTime: gameState.gameTime - }; -} \ No newline at end of file diff --git a/examples/viper-duel/server/src/utils/logger.js b/examples/viper-duel/server/src/utils/logger.js deleted file mode 100644 index 3836876f5..000000000 --- a/examples/viper-duel/server/src/utils/logger.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Logger utility with color-coded console outputs - */ -import chalk from 'chalk'; - -// Log level colors -const colors = { - info: chalk.blue, - success: chalk.green, - warn: chalk.yellow, - error: chalk.red, - debug: chalk.magenta, - system: chalk.cyan, - auth: chalk.hex('#FF8800'), - ws: chalk.hex('#00AAFF'), - nitro: chalk.hex('#9900FF'), - game: chalk.hex('#00FF99') -}; - -// Timestamp generator -const timestamp = () => { - const now = new Date(); - return chalk.gray(`[${now.toISOString().split('T')[1].slice(0, -1)}]`); -}; - -// Logger implementation -export const logger = { - info: (message, ...args) => console.log(timestamp(), colors.info('INFO'), message, ...args), - success: (message, ...args) => console.log(timestamp(), colors.success('SUCCESS'), message, ...args), - warn: (message, ...args) => console.warn(timestamp(), colors.warn('WARNING'), message, ...args), - error: (message, ...args) => console.error(timestamp(), colors.error('ERROR'), message, ...args), - debug: (message, ...args) => console.debug(timestamp(), colors.debug('DEBUG'), message, ...args), - system: (message, ...args) => console.log(timestamp(), colors.system('SYSTEM'), message, ...args), - auth: (message, ...args) => console.log(timestamp(), colors.auth('AUTH'), message, ...args), - ws: (message, ...args) => console.log(timestamp(), colors.ws('WEBSOCKET'), message, ...args), - nitro: (message, ...args) => console.log(timestamp(), colors.nitro('NITROLITE'), message, ...args), - game: (message, ...args) => console.log(timestamp(), colors.game('GAME'), message, ...args), - - // Special format for objects/data - data: (label, data) => { - console.log( - timestamp(), - chalk.hex('#888888')('DATA'), - chalk.cyan(label + ':'), - typeof data === 'object' ? '\n' + JSON.stringify(data, null, 2) : data - ); - } -}; - -export default logger; \ No newline at end of file diff --git a/examples/viper-duel/server/src/utils/validators.js b/examples/viper-duel/server/src/utils/validators.js deleted file mode 100644 index ea369e90b..000000000 --- a/examples/viper-duel/server/src/utils/validators.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Validators for game inputs - */ - -/** - * Validates Ethereum address format - * @param {string} address - Ethereum address to validate - * @returns {boolean} True if the address is valid - */ -export function isValidEthereumAddress(address) { - // Check if it's a string and matches Ethereum address pattern (0x followed by 40 hex chars) - return typeof address === 'string' - && /^0x[a-fA-F0-9]{40}$/.test(address); -} - -/** - * Validates room ID format - * @param {string} roomId - Room ID to validate - * @returns {boolean} True if the room ID is valid - */ -export function isValidRoomId(roomId) { - // Basic UUID v4 format check - return typeof roomId === 'string' - && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(roomId); -} - -/** - * Validates snake direction format - * @param {string} direction - Direction ('UP', 'DOWN', 'LEFT', 'RIGHT') - * @returns {boolean} True if the direction is valid - */ -export function isValidDirection(direction) { - return typeof direction === 'string' && ['UP', 'DOWN', 'LEFT', 'RIGHT'].includes(direction); -} - -/** - * Validates bet amount - * @param {number} betAmount - Bet amount to validate - * @returns {boolean} True if the bet amount is valid - */ -export function isValidBetAmount(betAmount) { - const validAmounts = [0, 0.01, 0.1, 1, 2]; - return typeof betAmount === 'number' && validAmounts.includes(betAmount); -} - -/** - * Validates join room payload - * @param {object} payload - The payload to validate - * @param {string} payload.roomId - Room ID - * @param {string} payload.eoa - Ethereum address - * @param {number} payload.betAmount - Bet amount - * @returns {object} Validation result with success flag and optional error message - */ -export function validateJoinRoomPayload(payload) { - if (!payload || typeof payload !== 'object') { - return { success: false, error: 'Invalid payload format' }; - } - - // EOA validation - if (!payload.eoa) { - return { success: false, error: 'Ethereum address is required' }; - } - - if (!isValidEthereumAddress(payload.eoa)) { - return { success: false, error: 'Invalid Ethereum address format' }; - } - - // Bet amount validation - if (payload.betAmount !== undefined && !isValidBetAmount(payload.betAmount)) { - return { success: false, error: 'Invalid bet amount. Must be 0, 0.01, 0.1, 1, or 2' }; - } - - // Room ID validation - // If roomId is undefined, we're creating a new room - // If roomId is provided, we're joining an existing room - if (payload.roomId === undefined) { - // Creating a new room - no further validation needed - console.log("Creating new room with bet amount:", payload.betAmount || 0); - return { success: true, isCreating: true }; - } else { - // Joining a room - validate room ID - if (!isValidRoomId(payload.roomId)) { - return { success: false, error: 'Invalid room ID format' }; - } - console.log("Joining existing room:", payload.roomId, "with bet amount:", payload.betAmount || 0); - return { success: true, isJoining: true }; - } -} - -/** - * Validates direction change payload - * @param {object} payload - The payload to validate - * @param {string} payload.roomId - Room ID - * @param {string} payload.direction - Direction ('UP', 'DOWN', 'LEFT', 'RIGHT') - * @returns {object} Validation result with success flag and optional error message - */ -export function validateDirectionPayload(payload) { - if (!payload || typeof payload !== 'object') { - return { success: false, error: 'Invalid payload format' }; - } - - if (!payload.roomId) { - return { success: false, error: 'Room ID is required' }; - } - - if (!isValidRoomId(payload.roomId)) { - return { success: false, error: 'Invalid room ID format' }; - } - - if (!payload.direction) { - return { success: false, error: 'Direction is required' }; - } - - if (!isValidDirection(payload.direction)) { - return { success: false, error: 'Invalid direction format (must be UP, DOWN, LEFT, or RIGHT)' }; - } - - return { success: true }; -} \ No newline at end of file From dda2364e703f7920077626548a66c3a68822a764 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Aug 2025 16:38:48 +0300 Subject: [PATCH 15/18] fix(main-pr): remove test-examples action --- .github/workflows/main-pr.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/main-pr.yml b/.github/workflows/main-pr.yml index d2a86f60b..46efa13be 100644 --- a/.github/workflows/main-pr.yml +++ b/.github/workflows/main-pr.yml @@ -34,28 +34,6 @@ jobs: project-path: 'integration' project-name: 'Integration' - test-examples: - name: Test Examples Compilation - needs: test-sdk - uses: ./.github/workflows/build-node-project.yml - strategy: - matrix: - include: - - path: examples/snake/client - name: Snake Client - - path: examples/snake/server - name: Snake Server - - path: examples/tictactoe/client - name: Tic Tac Toe Client - # NOTE: TicTacToe does not require to be compiled, - # still it's present here just to make sure all - # components of example apps are listed. - # - path: examples/tictactoe/server - # name: Tic Tac Toe Server - with: - project-path: ${{ matrix.path }} - project-name: ${{ matrix.name }} - build-and-publish-clearnode: name: Build and Publish (Clearnode) needs: test-clearnode @@ -88,7 +66,7 @@ jobs: # TODO: Enable this job if docs preview are needed (do not forget to provide GITHUB_TOKEN and GH access to receive preview URLs). # https://stackoverflow.com/questions/75514653/firebase-action-hosting-deploy-fails-with-requesterror-resource-not-accessible - + # build-and-preview-docs-firebase: # name: Deploy to Firebase Hosting on PR # if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} From 4f626357119f537a0fc10d7121a422822bfc3d22 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Aug 2025 17:45:22 +0300 Subject: [PATCH 16/18] build(contract): add separate config for linea --- contract/foundry.toml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/contract/foundry.toml b/contract/foundry.toml index 58d6b4747..2dc1eac28 100644 --- a/contract/foundry.toml +++ b/contract/foundry.toml @@ -7,14 +7,6 @@ optimizer = true # As per v0.3.0 Custody size becomes a concern. The runs value should not be raised higher. optimizer_runs = 45000 -[rpc_endpoints] -unichain_sepolia = "https://sepolia.unichain.org" -unichain_mainnet = "https://mainnet.unichain.org" -base_sepolia = "https://sepolia.base.org" -base_mainnet = "https://mainnet.base.org" -world_mainnet = "https://worldchain-mainnet.g.alchemy.com/public" -polygon_mainnet = "https://polygon-mainnet.infura.io/v3/d00b3e0d75e644458fad6ed5e886b84e" -ethereum_mainnet = "https://mainnet.infura.io/v3/d00b3e0d75e644458fad6ed5e886b84e" -optimism_mainnet = "https://optimism-mainnet.infura.io/v3/d00b3e0d75e644458fad6ed5e886b84e" -celo_mainnet = "https://celo-mainnet.infura.io/v3/d00b3e0d75e644458fad6ed5e886b84e" -# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +# Use the below settings when compiling for Linea, as it is EVM-compatible pre-London only +# evm_version = "london" +# optimizer_runs = 27000 From cc9326a33dffa5be3468364650ba4c2a2f7868b9 Mon Sep 17 00:00:00 2001 From: nksazonov Date: Wed, 20 Aug 2025 17:46:01 +0300 Subject: [PATCH 17/18] docs(contract): add v0.3.0 deployments for mainnet --- .../1/Custody.sol:Custody/2025-08-20T14:11:23.json | 11 +++++++++++ .../2025-08-20T14:13:25.json | 14 ++++++++++++++ .../Custody.sol:Custody/2025-08-20T14:17:02.json | 11 +++++++++++ .../2025-08-20T14:17:19.json | 14 ++++++++++++++ .../Custody.sol:Custody/2025-08-20T14:44:12.json | 11 +++++++++++ .../2025-08-20T14:44:34.json | 14 ++++++++++++++ .../Custody.sol:Custody/2025-08-20T14:18:36.json | 11 +++++++++++ .../2025-08-20T14:18:54.json | 14 ++++++++++++++ .../Custody.sol:Custody/2025-08-20T15:10:12.json | 11 +++++++++++ .../2025-08-20T15:10:44.json | 14 ++++++++++++++ 10 files changed, 125 insertions(+) create mode 100644 contract/deployments/1/Custody.sol:Custody/2025-08-20T14:11:23.json create mode 100644 contract/deployments/1/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:13:25.json create mode 100644 contract/deployments/480/Custody.sol:Custody/2025-08-20T14:17:02.json create mode 100644 contract/deployments/480/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:17:19.json create mode 100644 contract/deployments/59144/Custody.sol:Custody/2025-08-20T14:44:12.json create mode 100644 contract/deployments/59144/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:44:34.json create mode 100644 contract/deployments/747/Custody.sol:Custody/2025-08-20T14:18:36.json create mode 100644 contract/deployments/747/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:18:54.json create mode 100644 contract/deployments/8453/Custody.sol:Custody/2025-08-20T15:10:12.json create mode 100644 contract/deployments/8453/SimpleConsensus.sol:SimpleConsensus/2025-08-20T15:10:44.json diff --git a/contract/deployments/1/Custody.sol:Custody/2025-08-20T14:11:23.json b/contract/deployments/1/Custody.sol:Custody/2025-08-20T14:11:23.json new file mode 100644 index 000000000..3a19ae784 --- /dev/null +++ b/contract/deployments/1/Custody.sol:Custody/2025-08-20T14:11:23.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025e9c0f85e34a2b141e99557355c5b3ea1e244", + "deployedTo": "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f", + "transactionHash": "0x8eb4a3ca0dd73c0785d7c401e14f400db2de82157c4272b7840876e3d3accfc8", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755699083, + "chainId": 1, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/1/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:13:25.json b/contract/deployments/1/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:13:25.json new file mode 100644 index 000000000..f5faa4f02 --- /dev/null +++ b/contract/deployments/1/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:13:25.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x14980dF216722f14c42CA7357b06dEa7eB408b10", + "transactionHash": "0x70fc8208643c78d74081b74b86c3c24912fdf9a7761230413f9fa2db6b8d922a", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755699205, + "chainId": 1, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x6f71a38d919ad713d0afe0eb712b95064fc2616f" + ], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/480/Custody.sol:Custody/2025-08-20T14:17:02.json b/contract/deployments/480/Custody.sol:Custody/2025-08-20T14:17:02.json new file mode 100644 index 000000000..52da10de2 --- /dev/null +++ b/contract/deployments/480/Custody.sol:Custody/2025-08-20T14:17:02.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f", + "transactionHash": "0xfdfc2468698d0f44dfcd9f890c9979ab116fd913f49e1d1cf3915a32cb030190", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755699422, + "chainId": 480, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/480/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:17:19.json b/contract/deployments/480/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:17:19.json new file mode 100644 index 000000000..15cb2d43d --- /dev/null +++ b/contract/deployments/480/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:17:19.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x14980dF216722f14c42CA7357b06dEa7eB408b10", + "transactionHash": "0xe94623e210c5548e003b32dc3f7f3c9d3191c42e6dd08ea545322d1695295e74", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755699439, + "chainId": 480, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x6f71a38d919ad713d0afe0eb712b95064fc2616f" + ], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/59144/Custody.sol:Custody/2025-08-20T14:44:12.json b/contract/deployments/59144/Custody.sol:Custody/2025-08-20T14:44:12.json new file mode 100644 index 000000000..1d90c2e4d --- /dev/null +++ b/contract/deployments/59144/Custody.sol:Custody/2025-08-20T14:44:12.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f", + "transactionHash": "0x9ed3f609087f63057687318948bddb84c52f4cc8b29fe8bc2b2de7fbd80cb0fd", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755701052, + "chainId": 59144, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/59144/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:44:34.json b/contract/deployments/59144/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:44:34.json new file mode 100644 index 000000000..9b29c218c --- /dev/null +++ b/contract/deployments/59144/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:44:34.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x14980dF216722f14c42CA7357b06dEa7eB408b10", + "transactionHash": "0x8b502ef46c72d7e9628a6b815dca6f6d52540ee62ba411c66043f910b6b460ea", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755701074, + "chainId": 59144, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x6f71a38d919ad713d0afe0eb712b95064fc2616f" + ], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/747/Custody.sol:Custody/2025-08-20T14:18:36.json b/contract/deployments/747/Custody.sol:Custody/2025-08-20T14:18:36.json new file mode 100644 index 000000000..774a7f047 --- /dev/null +++ b/contract/deployments/747/Custody.sol:Custody/2025-08-20T14:18:36.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f", + "transactionHash": "0xffd4aca3ebeb5fff53f9ee1da2b7c43905f704c69d414d7f96e180cd747e5930", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755699516, + "chainId": 747, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/747/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:18:54.json b/contract/deployments/747/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:18:54.json new file mode 100644 index 000000000..21f3ec9e6 --- /dev/null +++ b/contract/deployments/747/SimpleConsensus.sol:SimpleConsensus/2025-08-20T14:18:54.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x14980dF216722f14c42CA7357b06dEa7eB408b10", + "transactionHash": "0x66baa003f68896babc4ac00f8092671a47159a5fe153cb260c14044d72f7c06b", + "commit": "dda2364e703f7920077626548a66c3a68822a764", + "timestamp": 1755699534, + "chainId": 747, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x6f71a38d919ad713d0afe0eb712b95064fc2616f" + ], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/8453/Custody.sol:Custody/2025-08-20T15:10:12.json b/contract/deployments/8453/Custody.sol:Custody/2025-08-20T15:10:12.json new file mode 100644 index 000000000..68e5aee3d --- /dev/null +++ b/contract/deployments/8453/Custody.sol:Custody/2025-08-20T15:10:12.json @@ -0,0 +1,11 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6", + "transactionHash": "0x2beb2f8c77fb88db9b031a6e88d354f575b33048b756114bc6f26a5a267cfad7", + "commit": "f9791c146e11e3715da8d96a4b7296b5ab7dada6", + "timestamp": 1755702612, + "chainId": 8453, + "contractPath": "./src/Custody.sol:Custody", + "constructorArgs": [], + "comment": "PROD v0.3.0" +} diff --git a/contract/deployments/8453/SimpleConsensus.sol:SimpleConsensus/2025-08-20T15:10:44.json b/contract/deployments/8453/SimpleConsensus.sol:SimpleConsensus/2025-08-20T15:10:44.json new file mode 100644 index 000000000..3a40dcf99 --- /dev/null +++ b/contract/deployments/8453/SimpleConsensus.sol:SimpleConsensus/2025-08-20T15:10:44.json @@ -0,0 +1,14 @@ +{ + "deployer": "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "deployedTo": "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C", + "transactionHash": "0x2b8ca629905d934be6887b3b3ef4dbfef3e41f795ce358365eb649c943b76867", + "commit": "f9791c146e11e3715da8d96a4b7296b5ab7dada6", + "timestamp": 1755702644, + "chainId": 8453, + "contractPath": "./src/adjudicators/SimpleConsensus.sol:SimpleConsensus", + "constructorArgs": [ + "0x2025E9c0F85E34A2B141E99557355C5b3Ea1e244", + "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + ], + "comment": "PROD v0.3.0" +} From f0d88b8de0f6243c13ab37e299c4ff0a03a142a1 Mon Sep 17 00:00:00 2001 From: Anton Filonenko Date: Thu, 21 Aug 2025 12:34:22 +0300 Subject: [PATCH 18/18] add linea support & update prod config --- clearnode/chart/config/prod/clearnode.yaml | 23 ++++++++++++---------- clearnode/chart/config/prod/secrets.yaml | 1 + clearnode/config.go | 21 ++++++++++---------- go.work.sum | 5 +++++ 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/clearnode/chart/config/prod/clearnode.yaml b/clearnode/chart/config/prod/clearnode.yaml index d0d7a32a2..b0be5a36e 100644 --- a/clearnode/chart/config/prod/clearnode.yaml +++ b/clearnode/chart/config/prod/clearnode.yaml @@ -10,22 +10,25 @@ config: envSecret: "" extraEnvs: CLEARNODE_LOG_LEVEL: "debug" - POLYGON_CUSTODY_CONTRACT_ADDRESS: "0x783d72Fb71289e788905DB4bcb9FDeD7e1c7f150" - POLYGON_ADJUDICATOR_ADDRESS: "0x60f0c4f3C13a05FE37759B736E56dAe9853Ac0E8" + POLYGON_CUSTODY_CONTRACT_ADDRESS: "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f" + POLYGON_ADJUDICATOR_ADDRESS: "0x14980dF216722f14c42CA7357b06dEa7eB408b10" POLYGON_BALANCE_CHECKER_ADDRESS: "0x2352c63A83f9Fd126af8676146721Fa00924d7e4" - WORLD_CHAIN_CUSTODY_CONTRACT_ADDRESS: "0x783d72Fb71289e788905DB4bcb9FDeD7e1c7f150" - WORLD_CHAIN_ADJUDICATOR_ADDRESS: "0x60f0c4f3C13a05FE37759B736E56dAe9853Ac0E8" + WORLD_CHAIN_CUSTODY_CONTRACT_ADDRESS: "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f" + WORLD_CHAIN_ADJUDICATOR_ADDRESS: "0x14980dF216722f14c42CA7357b06dEa7eB408b10" WORLD_CHAIN_BALANCE_CHECKER_ADDRESS: "0x6D3B5EFa1f81f65037cD842F48E44BcBCa48CBEF" - FLOW_CUSTODY_CONTRACT_ADDRESS: "0x783d72Fb71289e788905DB4bcb9FDeD7e1c7f150" - FLOW_ADJUDICATOR_ADDRESS: "0x60f0c4f3C13a05FE37759B736E56dAe9853Ac0E8" + FLOW_CUSTODY_CONTRACT_ADDRESS: "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f" + FLOW_ADJUDICATOR_ADDRESS: "0x14980dF216722f14c42CA7357b06dEa7eB408b10" FLOW_BALANCE_CHECKER_ADDRESS: "0xa3f2f64455c9f8D68d9dCAeC2605D64680FaF898" FLOW_BLOCK_STEP: "499" - BASE_CUSTODY_CONTRACT_ADDRESS: "0x783d72Fb71289e788905DB4bcb9FDeD7e1c7f150" - BASE_ADJUDICATOR_ADDRESS: "0x60f0c4f3C13a05FE37759B736E56dAe9853Ac0E8" + BASE_CUSTODY_CONTRACT_ADDRESS: "0x490fb189DdE3a01B00be9BA5F41e3447FbC838b6" + BASE_ADJUDICATOR_ADDRESS: "0x7de4A0736Cf5740fD3Ca2F2e9cc85c9AC223eF0C" BASE_BALANCE_CHECKER_ADDRESS: "0x3ba5A41eA17fd4950a641a057dC0bEb8E8ff1521" - ETH_MAINNET_CUSTODY_CONTRACT_ADDRESS: "0x14F699F8B31a84CC38617B8c31B78a271099beCD" - ETH_MAINNET_ADJUDICATOR_ADDRESS: "0x60f0c4f3c13a05fe37759b736e56dae9853ac0e8" + ETH_MAINNET_CUSTODY_CONTRACT_ADDRESS: "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f" + ETH_MAINNET_ADJUDICATOR_ADDRESS: "0x14980dF216722f14c42CA7357b06dEa7eB408b10" ETH_MAINNET_BALANCE_CHECKER_ADDRESS: "0xb1f8e55c7f64d203c1400b9d8555d050f94adf39" + LINEA_MAINNET_CUSTODY_CONTRACT_ADDRESS: "0x6F71a38d919ad713D0AfE0eB712b95064Fc2616f" + LINEA_MAINNET_ADJUDICATOR_ADDRESS: "0x14980dF216722f14c42CA7357b06dEa7eB408b10" + LINEA_MAINNET_BALANCE_CHECKER_ADDRESS: "0xF62e6a41561b3650a69Bb03199C735e3E3328c0D" MSG_EXPIRY_TIME: "60" image: diff --git a/clearnode/chart/config/prod/secrets.yaml b/clearnode/chart/config/prod/secrets.yaml index ad990ad98..3a4faf92f 100644 --- a/clearnode/chart/config/prod/secrets.yaml +++ b/clearnode/chart/config/prod/secrets.yaml @@ -8,3 +8,4 @@ config: FLOW_INFURA_URL: ref+gcpsecrets://ynet-stage/clearnet-prod-flow-infura-url?version=latest BASE_INFURA_URL: ref+gcpsecrets://ynet-stage/clearnet-prod-base-infura-url?version=latest ETH_MAINNET_INFURA_URL: ref+gcpsecrets://ynet-stage/clearnet-prod-eth-mainnet-infura-url?version=latest + LINEA_MAINNET_INFURA_URL: ref+gcpsecrets://ynet-stage/clearnet-prod-linea-mainnet-infura-url?version=latest diff --git a/clearnode/config.go b/clearnode/config.go index abe275b5b..78344a149 100644 --- a/clearnode/config.go +++ b/clearnode/config.go @@ -14,16 +14,17 @@ import ( // - {PREFIX}_INFURA_URL: The Infura endpoint URL for the network // - {PREFIX}_CUSTODY_CONTRACT_ADDRESS: The custody contract address var knownNetworks = map[string]uint32{ - "POLYGON": 137, - "ETH_SEPOLIA": 11155111, - "CELO": 42220, - "BASE": 8453, - "WORLD_CHAIN": 480, - "ROOTSTOCK": 30, - "FLOW": 747, - "LOCALNET": 1337, - "ETH_MAINNET": 1, - "ANVIL": 31337, + "POLYGON": 137, + "ETH_SEPOLIA": 11155111, + "LINEA_MAINNET": 59144, + "CELO": 42220, + "BASE": 8453, + "WORLD_CHAIN": 480, + "ROOTSTOCK": 30, + "FLOW": 747, + "LOCALNET": 1337, + "ETH_MAINNET": 1, + "ANVIL": 31337, } // NetworkConfig represents configuration for a blockchain network diff --git a/go.work.sum b/go.work.sum index f0f066534..2e6680604 100644 --- a/go.work.sum +++ b/go.work.sum @@ -163,7 +163,9 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= @@ -380,7 +382,9 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzb github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -464,6 +468,7 @@ github.com/tdewolff/parse/v2 v2.6.4/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOd github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=

1-dDWL+;(dpt^$21FIq57&GZgA5As2@kH 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 diff --git a/examples/tictactoe/client/public/vite.svg b/examples/tictactoe/client/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/examples/tictactoe/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/tictactoe/client/src/App.css b/examples/tictactoe/client/src/App.css deleted file mode 100644 index f6d0f955b..000000000 --- a/examples/tictactoe/client/src/App.css +++ /dev/null @@ -1,94 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - width: 100%; - height: 100%; -} - -/* Enhanced Glowing X and O elements */ -.cell-x { - text-shadow: 0 0 10px #00e5ff, 0 0 20px #00e5ff50, 0 0 30px #00e5ff30; - position: relative; -} - -.cell-o { - text-shadow: 0 0 10px #ff49e1, 0 0 20px #ff49e150, 0 0 30px #ff49e130; - position: relative; -} - -/* Cell with X - before/after decorations */ -.cell-x::before { - content: ""; - position: absolute; - inset: -5px; - border-radius: 8px; - background: radial-gradient(circle at center, rgba(0, 229, 255, 0.2) 0%, transparent 70%); - opacity: 0.5; - z-index: -1; -} - -/* Cell with O - before/after decorations */ -.cell-o::before { - content: ""; - position: absolute; - inset: -5px; - border-radius: 8px; - background: radial-gradient(circle at center, rgba(255, 73, 225, 0.2) 0%, transparent 70%); - opacity: 0.5; - z-index: -1; -} - -/* Particle effects for cells */ -.cell-x::after, -.cell-o::after { - content: ""; - position: absolute; - width: 100%; - height: 100%; - background-image: radial-gradient(circle, #ffffff 1px, transparent 1px); - background-size: 16px 16px; - opacity: 0.05; - mix-blend-mode: screen; - animation: float 8s infinite linear; - pointer-events: none; -} - -@keyframes float { - 0% { background-position: 0 0; } - 100% { background-position: 100px 100px; } -} - -/* Improved pulse animation for cells */ -@keyframes pulse-cyan { - 0% { box-shadow: 0 0 10px rgba(0, 229, 255, 0.5); } - 50% { box-shadow: 0 0 20px rgba(0, 229, 255, 0.7), 0 0 30px rgba(0, 229, 255, 0.3); } - 100% { box-shadow: 0 0 10px rgba(0, 229, 255, 0.5); } -} - -@keyframes pulse-magenta { - 0% { box-shadow: 0 0 10px rgba(255, 73, 225, 0.5); } - 50% { box-shadow: 0 0 20px rgba(255, 73, 225, 0.7), 0 0 30px rgba(255, 73, 225, 0.3); } - 100% { box-shadow: 0 0 10px rgba(255, 73, 225, 0.5); } -} - -/* Line growth animation */ -@keyframes lineGrow { - 0% { - width: 0; - opacity: 0.8; - } - 100% { - width: 50px; - opacity: 0; - } -} - -/* Fade out animation */ -@keyframes fadeOut { - 0% { - opacity: 0.7; - } - 100% { - opacity: 0; - } -} diff --git a/examples/tictactoe/client/src/App.tsx b/examples/tictactoe/client/src/App.tsx deleted file mode 100644 index ea27b8f17..000000000 --- a/examples/tictactoe/client/src/App.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useWebSocket } from "./hooks/useWebSocket"; -import { useGameState } from "./hooks/useGameState"; -import { GameScreen } from "./components/GameScreen"; -import { ErrorModal } from "./components/ErrorModal"; -import { BackgroundAnimation } from "./components/BackgroundAnimation"; -import { GameLobbyIntegrated } from "./components/GameLobbyIntegrated"; -import type { JoinRoomPayload, AvailableRoom, AvailableRoomsMessage, PlayerSymbol } from "./types"; -import "./App.css"; -import { useWebSocketNitrolite } from "./hooks/useWebSocketNitrolite"; -import { useNitroliteIntegration } from "./hooks/useNitroliteIntegration"; -import { useNitrolite } from "./context/NitroliteClientWrapper"; - -function App() { - // Player's Ethereum address - now managed by useMetaMask hook in Lobby - const [eoaAddress, setEoaAddress] = useState(""); - - // Game view state - const [gameView, setGameView] = useState<"lobby" | "game">("lobby"); - - // WebSocket connection - const { - error: wsError, - lastMessage, - joinRoom, - makeMove, - startGame, - getAvailableRooms, - sendAppSessionSignature, - sendAppSessionStartGame - } = useWebSocket(); - useWebSocketNitrolite(); - const { client, loading: nitroliteLoading, error: nitroliteError } = useNitrolite(); - - // Initialize the Nitrolite integration - const { initializeNitroliteClient } = useNitroliteIntegration(); - - // When the Nitrolite client is available, initialize it - useEffect(() => { - if (client && !nitroliteLoading && !nitroliteError) { - console.log("Initializing Nitrolite client in App component"); - initializeNitroliteClient(client); - } else if (nitroliteError) { - console.error("Nitrolite client error:", nitroliteError); - } - }, [client, nitroliteLoading, nitroliteError, initializeNitroliteClient]); - - // Removed this reference as we're now using destructuring above - - // Available rooms state - const [availableRooms, setAvailableRooms] = useState([]); - const [onlineUsers, setOnlineUsers] = useState(1); - - // Game state - const { - gameState, - gameOver, - roomId, - errorMessage, - isRoomReady, - isGameStarted, - isHost, - playerSymbol, - isPlayerTurn, - formatShortAddress, - getOpponentAddress, - resetGame, - awaitingHostStart, - signAndStartGame, - isSigningInProgress, - signatureError - } = useGameState(lastMessage, eoaAddress, sendAppSessionSignature, sendAppSessionStartGame); - - // Handle errors - const [showError, setShowError] = useState(false); - const [errorDisplay, setErrorDisplay] = useState(null); - - useEffect(() => { - // Combine all possible error sources - const combinedError = wsError || errorMessage || nitroliteError || signatureError; - - if (combinedError) { - console.log("Error detected:", combinedError); - - // Don't show error modal for MetaMask connection message - if (combinedError === "MetaMask not connected. Please connect your wallet.") { - setShowError(false); - setErrorDisplay(null); - } else { - setShowError(true); - setErrorDisplay(combinedError); - } - } else { - setShowError(false); - setErrorDisplay(null); - } - }, [wsError, errorMessage, nitroliteError, signatureError]); - - // Process available rooms from websocket messages - useEffect(() => { - if (lastMessage && lastMessage.type === "room:available") { - const roomsMessage = lastMessage as AvailableRoomsMessage; - setAvailableRooms(roomsMessage.rooms); - } - - if (lastMessage && lastMessage.type === "onlineUsers") { - setOnlineUsers(lastMessage.count); - } - }, [lastMessage]); - - // Handle fetching available rooms - const handleGetAvailableRooms = useCallback(() => { - getAvailableRooms(); - }, [getAvailableRooms]); - - // Handle joining a room - const handleJoinRoom = (payload: JoinRoomPayload) => { - setEoaAddress(payload.eoa); - - // If creating a new room, mark as host - if (payload.roomId === undefined) { - console.log("Creating new room as host, payload:", payload); - } else { - console.log("Joining existing room:", payload.roomId, "payload:", payload); - } - - // Join room via WebSocket - pass the payload directly - console.log("Sending WebSocket joinRoom with payload:", { - roomId: payload.roomId, - eoa: payload.eoa, - }); - - joinRoom({ - roomId: payload.roomId, - eoa: payload.eoa, - }); - - // Switch to game view - setGameView("game"); - }; - - // Handle cell click - const handleCellClick = (position: number) => { - if (!roomId || !isPlayerTurn || gameOver) return; - - makeMove({ - roomId, - pos: position, - }); - }; - - // Handle starting the game (host only) - const handleStartGame = () => { - if (!roomId || !isHost) { - console.error("Cannot start game: not host or no room ID"); - return; - } - - // If we're awaiting host signature for app session, sign and start - if (awaitingHostStart) { - console.log("Signing app session and starting game for room:", roomId); - signAndStartGame(); - } else { - console.log("Starting game as host for room:", roomId); - startGame(roomId); - } - }; - - // Handle play again - const handlePlayAgain = () => { - // For now, just reload the page - window.location.reload(); - - // TODO: Implement proper reset logic when @erc7824/nitrolite is integrated - }; - - // Handle error close - const handleErrorClose = () => { - setShowError(false); - resetGame(); - setGameView("lobby"); - }; - - return ( -