From 885c4e219e0b0c2ec51387819bc281dee94b42dd Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 13 May 2026 09:38:36 +0530 Subject: [PATCH 1/4] feat(tier2): unlinkable Starknet account contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce the Tier 2 unlinkable-account scheme: the on-chain factory and account contracts no longer reference the user's MetaMask address. Instead, a client-derived secp256k1 "session key" plays the role of account owner. Anything observable on chain — calldata, events, storage, signatures — is keyed off the session address, which has no a-priori relationship to the MetaMask wallet that produced it. Cairo changes: - account_factory: rename eth_address → session_eth_address through the IAccountFactory trait, impl, and tests; drop eth_address from the AccountDeployed event so indexers can't trivially link account ↔ owner; bump PRIMER_CLASS_HASH to the scarb 2.16.1 release-profile output. - eth_712_account: rename initialize param to owner_eth_address; add Tier-2-oriented doc comments; storage var name unchanged for layout stability across upgrades. - New packages: counter/ — minimal demo target the Tier 2 account will exercise. helpers/ — privacy-pool helper contracts: - EarnDeployHelper: privacy_invoke(session_eth, signature, note_id) forwards to factory.deploy_account, pool-caller-validated. Lets the Starknet privacy pool deploy a Tier 2 account inside a private, proof-validated transaction. - EarnInvokeHelper: privacy_invoke(target_account, outside_execution, signature, note_id) forwards to ISRC9_V2.execute_from_outside_v2, so any sponsored private invoke can land on the new account without an MM signature. - testing_utils: MockOutsideExecutionTarget for the invoke-helper tests. - Build config: casm = true on every starknet-contract target so deploy scripts can hash the casm; release profile produces deterministic class hashes used by the Tier 2 wallet client. - Workspace: drop strategy_implementation (incompatible with the new param shape; replaced by counter for the demo); add counter + helpers. Tier 2 alone does NOT make an account unlinkable — it removes the on-chain references to MetaMask, but funding, gas-payer, and relayer metadata can still re-link. That side is handled by the wallet client + sponsored-private flow added in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- Scarb.lock | 32 ++-- Scarb.toml | 2 +- account_factory/Scarb.toml | 1 + account_factory/src/account_factory.cairo | 33 +++- account_factory/src/test.cairo | 46 ++--- account_factory/src/utils.cairo | 11 +- contracts/Scarb.toml | 1 + counter/Scarb.toml | 24 +++ counter/src/counter.cairo | 73 ++++++++ counter/src/lib.cairo | 4 + counter/src/test.cairo | 91 ++++++++++ eth_712_account/Scarb.toml | 1 + eth_712_account/src/eth_712_account.cairo | 21 ++- eth_712_account/src/interface.cairo | 11 +- helpers/Scarb.toml | 38 ++++ helpers/src/earn_deploy_helper.cairo | 132 ++++++++++++++ helpers/src/earn_invoke_helper.cairo | 108 ++++++++++++ helpers/src/lib.cairo | 5 + helpers/src/test.cairo | 202 ++++++++++++++++++++++ testing_utils/src/dummy_contracts.cairo | 79 +++++++++ 20 files changed, 857 insertions(+), 58 deletions(-) create mode 100644 counter/Scarb.toml create mode 100644 counter/src/counter.cairo create mode 100644 counter/src/lib.cairo create mode 100644 counter/src/test.cairo create mode 100644 helpers/Scarb.toml create mode 100644 helpers/src/earn_deploy_helper.cairo create mode 100644 helpers/src/earn_invoke_helper.cairo create mode 100644 helpers/src/lib.cairo create mode 100644 helpers/src/test.cairo diff --git a/Scarb.lock b/Scarb.lock index 09ef38b..f9e215b 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -21,6 +21,13 @@ dependencies = [ "starkware_utils_testing", ] +[[package]] +name = "counter" +version = "0.1.0" +dependencies = [ + "snforge_std", +] + [[package]] name = "earn_reporter" version = "0.1.0" @@ -43,6 +50,17 @@ dependencies = [ "testing_utils", ] +[[package]] +name = "helpers" +version = "0.1.0" +dependencies = [ + "account_factory", + "openzeppelin", + "snforge_std", + "starkware_utils_testing", + "testing_utils", +] + [[package]] name = "openzeppelin" version = "2.0.0" @@ -194,20 +212,6 @@ dependencies = [ "starkware_utils", ] -[[package]] -name = "strategy_implementation" -version = "0.1.0" -dependencies = [ - "account_factory", - "contracts", - "earn_reporter", - "openzeppelin", - "snforge_std", - "starkware_utils", - "starkware_utils_testing", - "testing_utils", -] - [[package]] name = "testing_utils" version = "0.1.0" diff --git a/Scarb.toml b/Scarb.toml index f366b93..6086681 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,5 +1,5 @@ [workspace] -members = ["contracts", "eth_712_account", "account_factory", "earn_reporter", "strategy_implementation", "testing_utils"] +members = ["contracts", "eth_712_account", "account_factory", "earn_reporter", "counter", "helpers", "testing_utils"] [workspace.package] edition = "2024_07" diff --git a/account_factory/Scarb.toml b/account_factory/Scarb.toml index 4b09068..1ef3fc2 100644 --- a/account_factory/Scarb.toml +++ b/account_factory/Scarb.toml @@ -19,6 +19,7 @@ testing_utils = { path = "../testing_utils" } [[target.starknet-contract]] sierra = true +casm = true [[test]] build-external-contracts = [ diff --git a/account_factory/src/account_factory.cairo b/account_factory/src/account_factory.cairo index f273f71..707d0c5 100644 --- a/account_factory/src/account_factory.cairo +++ b/account_factory/src/account_factory.cairo @@ -5,8 +5,12 @@ use starknet::{ClassHash, ContractAddress, EthAddress}; pub trait IAccountFactory { fn account_class_hash(self: @TContractState) -> ClassHash; fn set_account_class_hash(ref self: TContractState, new_class_hash: ClassHash); + /// `session_eth_address` is the Ethereum-format address of a fresh secp256k1 key + /// the client derived from a one-time MetaMask bootstrap signature. It is NOT the + /// user's MetaMask address; it has no on-chain history outside this account. + /// Passing the real MM address here would defeat the unlinkability property. fn deploy_account( - ref self: TContractState, eth_address: EthAddress, signature: Signature, + ref self: TContractState, session_eth_address: EthAddress, signature: Signature, ) -> ContractAddress; } @@ -74,10 +78,14 @@ pub mod AccountFactory { pub new_class_hash: ClassHash, } + /// Emitted on first deploy of an account. `eth_address` is intentionally NOT + /// included so that on-chain indexers cannot trivially associate the account + /// with any externally-meaningful Ethereum identity. The deterministic salt + /// (`session_eth_address`) is recoverable off-chain only by the wallet that + /// holds the bootstrap signature. #[derive(Drop, starknet::Event, Debug, PartialEq)] pub struct AccountDeployed { pub account_class_hash: ClassHash, - pub eth_address: EthAddress, pub account_address: ContractAddress, } @@ -114,12 +122,18 @@ pub mod AccountFactory { ); } - /// Returns the deterministic account contract address for the given Ethereum - /// address, deploying and upgrading a Primer contract on first use. + /// Returns the deterministic account contract address for the given session + /// Ethereum-format address, deploying and upgrading a Primer contract on + /// first use. + /// + /// Tier 2 unlinkability: callers MUST pass `session_eth_address` — the + /// address of a fresh secp256k1 key derived from a one-time MetaMask + /// bootstrap signature — NOT the user's real MetaMask address. The whole + /// privacy property collapses if the real MM address is used here. fn deploy_account( - ref self: ContractState, eth_address: EthAddress, signature: Signature, + ref self: ContractState, session_eth_address: EthAddress, signature: Signature, ) -> ContractAddress { - let account_address = eth_address_to_account(:eth_address); + let account_address = eth_address_to_account(eth_address: session_eth_address); // If the account contract is deployed, return the address. if is_deployed(addr: account_address) { return account_address; @@ -131,7 +145,7 @@ pub mod AccountFactory { // 3. Initialize the account contract. let (deployed_address, _retdata) = syscalls::deploy_syscall( class_hash: PRIMER_CLASS_HASH, - contract_address_salt: eth_address.into(), + contract_address_salt: session_eth_address.into(), calldata: [].span(), deploy_from_zero: false, ) @@ -147,11 +161,12 @@ pub mod AccountFactory { let eth_account_initializer = IEthAccountInitializerDispatcher { contract_address: account_address, }; - eth_account_initializer.initialize(:eth_address, :signature); + eth_account_initializer + .initialize(owner_eth_address: session_eth_address, :signature); self .emit( Event::AccountDeployed( - AccountDeployed { account_class_hash, eth_address, account_address }, + AccountDeployed { account_class_hash, account_address }, ), ); diff --git a/account_factory/src/test.cairo b/account_factory/src/test.cairo index 95a2f12..5dd06aa 100644 --- a/account_factory/src/test.cairo +++ b/account_factory/src/test.cairo @@ -15,11 +15,11 @@ use testing_utils::event_helpers::{get_event_by_selector, get_event_by_selector_ fn deploy_account_wrapper( - account_factory_addr: ContractAddress, eth_address: EthAddress, + account_factory_addr: ContractAddress, session_eth_address: EthAddress, ) -> ContractAddress { let account_factory = IAccountFactoryDispatcher { contract_address: account_factory_addr }; let signature = Signature { r: 0x1012, s: 0x1012, y_parity: true }; - account_factory.deploy_account(:eth_address, :signature) + account_factory.deploy_account(:session_eth_address, :signature) } @@ -106,16 +106,16 @@ fn test_deploy_account_deploys_once_and_reuses() { let account_factory = IAccountFactoryDispatcher { contract_address: account_factory_addr }; let mut spy = snforge_std::spy_events(); - let eth_address: EthAddress = '0x1012'.try_into().unwrap(); + let session_eth_address: EthAddress = '0x1012'.try_into().unwrap(); // Compute the expected account address using the same derivation logic as the contract. let expected_account = eth_address_to_account( - account_factory: account_factory_addr, :eth_address, + account_factory: account_factory_addr, eth_address: session_eth_address, ); // First call: lazily deploys the primer, upgrades it to the account_class_hash, and // leaves the account contract at the expected address. - let account_address = deploy_account_wrapper(:account_factory_addr, :eth_address); + let account_address = deploy_account_wrapper(:account_factory_addr, :session_eth_address); assert!(account_address == expected_account, "unexpected account address after first deploy"); let class_hash_after_first = get_class_hash_at_syscall(account_address).unwrap_syscall(); let expected_class_hash = account_factory.account_class_hash(); @@ -130,7 +130,7 @@ fn test_deploy_account_deploys_once_and_reuses() { ) .unwrap(); let expected_event = AccountDeployed { - account_class_hash: class_hash_after_first, eth_address, account_address, + account_class_hash: class_hash_after_first, account_address, }; assert_expected_event_emitted( spied_event: spied_event, @@ -141,7 +141,7 @@ fn test_deploy_account_deploys_once_and_reuses() { // Second call with the same parameters should reuse the same account and leave the // class hash unchanged. - let second_account_address = deploy_account_wrapper(:account_factory_addr, :eth_address); + let second_account_address = deploy_account_wrapper(:account_factory_addr, :session_eth_address); assert!(second_account_address == account_address, "account address changed on reuse"); let class_hash_after_second = get_class_hash_at_syscall(account_address).unwrap_syscall(); assert!( @@ -162,13 +162,13 @@ fn test_after_change_account_class_hash_reuses_existing_account() { let account_factory_addr = setup_account_factory_test_env(); let account_factory = IAccountFactoryDispatcher { contract_address: account_factory_addr }; - let eth_address: EthAddress = '0x1012'.try_into().unwrap(); + let session_eth_address: EthAddress = '0x1012'.try_into().unwrap(); let expected_account = eth_address_to_account( - account_factory: account_factory_addr, :eth_address, + account_factory: account_factory_addr, eth_address: session_eth_address, ); // First call: deploys and upgrades to the initial account_class_hash. - let account_address = deploy_account_wrapper(:account_factory_addr, :eth_address); + let account_address = deploy_account_wrapper(:account_factory_addr, :session_eth_address); assert!(account_address == expected_account, "unexpected account address after first deploy"); let class_hash_after_first = get_class_hash_at_syscall(account_address).unwrap_syscall(); let initial_class_hash = account_factory.account_class_hash(); @@ -187,7 +187,7 @@ fn test_after_change_account_class_hash_reuses_existing_account() { // Second call with the same parameters should reuse the same account and leave the // class hash unchanged. let mut second_spy = snforge_std::spy_events(); - let second_account_address = deploy_account_wrapper(:account_factory_addr, :eth_address); + let second_account_address = deploy_account_wrapper(:account_factory_addr, :session_eth_address); assert!(second_account_address == account_address, "account address changed on reuse"); let class_hash_after_second = get_class_hash_at_syscall(account_address).unwrap_syscall(); assert!( @@ -213,13 +213,15 @@ fn test_change_account_class_hash_affects_only_new_users() { let account_factory = IAccountFactoryDispatcher { contract_address: account_factory_addr }; let mut spy = snforge_std::spy_events(); - let eth_address: EthAddress = '0x1012'.try_into().unwrap(); + let session_eth_address: EthAddress = '0x1012'.try_into().unwrap(); let expected_account = eth_address_to_account( - account_factory: account_factory_addr, :eth_address, + account_factory: account_factory_addr, eth_address: session_eth_address, ); // First call: deploys and upgrades to the initial account_class_hash. - let first_account_address = deploy_account_wrapper(:account_factory_addr, :eth_address); + let first_account_address = deploy_account_wrapper( + :account_factory_addr, :session_eth_address, + ); assert!( first_account_address == expected_account, "unexpected account address after first deploy", ); @@ -238,18 +240,18 @@ fn test_change_account_class_hash_affects_only_new_users() { ); account_factory.set_account_class_hash(new_class_hash: new_hash); - // Build parameters for a *new* Eth address so the contract derives a second account. - let new_eth_address: EthAddress = '0x1013'.try_into().unwrap(); + // Build parameters for a *new* session Eth address so the contract derives a second account. + let new_session_eth_address: EthAddress = '0x1013'.try_into().unwrap(); let expected_new_account = eth_address_to_account( - account_factory: account_factory_addr, eth_address: new_eth_address, + account_factory: account_factory_addr, eth_address: new_session_eth_address, ); let new_account_address = deploy_account_wrapper( - :account_factory_addr, eth_address: new_eth_address, + :account_factory_addr, session_eth_address: new_session_eth_address, ); assert!( new_account_address == expected_new_account, - "new account should be derived as expected from new Eth address", + "new account should be derived as expected from new session Eth address", ); let class_hash_new_account = get_class_hash_at_syscall(new_account_address).unwrap_syscall(); assert!( @@ -262,9 +264,7 @@ fn test_change_account_class_hash_affects_only_new_users() { let spied_event = get_event_by_selector_n(:events, selector: selector!("AccountDeployed"), n: 1) .unwrap(); let expected_event = AccountDeployed { - account_class_hash: new_hash, - eth_address: new_eth_address, - account_address: new_account_address, + account_class_hash: new_hash, account_address: new_account_address, }; assert_expected_event_emitted( spied_event: spied_event, @@ -275,7 +275,7 @@ fn test_change_account_class_hash_affects_only_new_users() { // Check that the original account doesn't change even though the account class hash has // changed. - let prev_account_address = deploy_account_wrapper(:account_factory_addr, :eth_address); + let prev_account_address = deploy_account_wrapper(:account_factory_addr, :session_eth_address); assert!(prev_account_address == expected_account, "original account should not change"); let class_hash_prev_account = get_class_hash_at_syscall(prev_account_address).unwrap_syscall(); assert!( diff --git a/account_factory/src/utils.cairo b/account_factory/src/utils.cairo index 9ffd9e1..558a122 100644 --- a/account_factory/src/utils.cairo +++ b/account_factory/src/utils.cairo @@ -12,9 +12,14 @@ pub const PRIMER_CLASS_HASH: ClassHash = .try_into() .unwrap(); +// Recomputed for scarb 2.16.1 release-profile builds. If you upgrade scarb +// or change Primer source, rerun `sncast utils class-hash --package contracts +// --contract-name Primer` and update this constant — the on-chain Primer +// declaration's class hash MUST match this value or the factory's +// deploy_syscall will fail with CLASS_NOT_DECLARED. #[cfg(not(target: "test"))] pub const PRIMER_CLASS_HASH: ClassHash = - 0x00123e6bc1c14ae9934e933d3f64916a6116dd6b036a922b2b1f0815e0d1d300 + 0x03edae2158f4aea6295470678fc7de27e19d7e40f295cda90d786d17b3531fdf .try_into() .unwrap(); @@ -22,7 +27,9 @@ const CONTRACT_ADDRESS_PREFIX: felt252 = 'STARKNET_CONTRACT_ADDRESS'; #[starknet::interface] pub(crate) trait IEthAccountInitializer { - fn initialize(ref self: TContractState, eth_address: EthAddress, signature: Signature); + fn initialize( + ref self: TContractState, owner_eth_address: EthAddress, signature: Signature, + ); } /// Computes the Pedersen hash on the elements of the span using a hash state. diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index 44ddcf3..029084d 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -16,6 +16,7 @@ starkware_utils_testing.workspace = true [[target.starknet-contract]] sierra = true +casm = true [profile.dev.cairo] unstable-add-statements-functions-debug-info = true diff --git a/counter/Scarb.toml b/counter/Scarb.toml new file mode 100644 index 0000000..29ac6f6 --- /dev/null +++ b/counter/Scarb.toml @@ -0,0 +1,24 @@ +[package] +name = "counter" +edition.workspace = true +version.workspace = true + +[lib] + +[dependencies] +starknet.workspace = true + +[dev-dependencies] +snforge_std.workspace = true + +[[target.starknet-contract]] +sierra = true +casm = true + +[profile.dev.cairo] +unstable-add-statements-functions-debug-info = true +unstable-add-statements-code-locations-debug-info = true +inlining-strategy = "avoid" + +[scripts] +test = "snforge test" diff --git a/counter/src/counter.cairo b/counter/src/counter.cairo new file mode 100644 index 0000000..c22e015 --- /dev/null +++ b/counter/src/counter.cairo @@ -0,0 +1,73 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait ICounter { + fn increment(ref self: TContractState); + fn increment_by(ref self: TContractState, amount: u64); + fn get_count(self: @TContractState) -> u64; + fn get_last_caller(self: @TContractState) -> ContractAddress; +} + +/// Minimal counter contract used as a demo target for the Tier 2 unlinkable +/// account flow: a Tier 2 Starknet account calls `increment` (via the privacy +/// pool + paymaster) so we can confirm end-to-end that an action originated +/// from the unlinkable account without any on-chain link back to the user's +/// MetaMask address. The contract itself has no privacy logic — it only +/// records the count and the last caller so tests can assert who invoked it. +#[starknet::contract] +pub mod Counter { + use core::num::traits::Zero; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + use super::ICounter; + + #[storage] + struct Storage { + count: u64, + last_caller: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Incremented: Incremented, + } + + #[derive(Drop, starknet::Event, Debug, PartialEq)] + pub struct Incremented { + pub caller: ContractAddress, + pub new_count: u64, + pub amount: u64, + } + + #[abi(embed_v0)] + impl CounterImpl of ICounter { + fn increment(ref self: ContractState) { + self.do_increment(1); + } + + fn increment_by(ref self: ContractState, amount: u64) { + assert(!amount.is_zero(), 'ZERO_AMOUNT'); + self.do_increment(amount); + } + + fn get_count(self: @ContractState) -> u64 { + self.count.read() + } + + fn get_last_caller(self: @ContractState) -> ContractAddress { + self.last_caller.read() + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn do_increment(ref self: ContractState, amount: u64) { + let caller = get_caller_address(); + let new_count = self.count.read() + amount; + self.count.write(new_count); + self.last_caller.write(caller); + self.emit(Event::Incremented(Incremented { caller, new_count, amount })); + } + } +} diff --git a/counter/src/lib.cairo b/counter/src/lib.cairo new file mode 100644 index 0000000..c8c8dc9 --- /dev/null +++ b/counter/src/lib.cairo @@ -0,0 +1,4 @@ +pub mod counter; + +#[cfg(test)] +mod test; diff --git a/counter/src/test.cairo b/counter/src/test.cairo new file mode 100644 index 0000000..b2eec54 --- /dev/null +++ b/counter/src/test.cairo @@ -0,0 +1,91 @@ +use counter::counter::Counter::{Event, Incremented}; +use counter::counter::{ICounterDispatcher, ICounterDispatcherTrait}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_caller_address, stop_cheat_caller_address, +}; +use starknet::{ContractAddress, SyscallResultTrait}; + +fn ANY_CALLER() -> ContractAddress { + 'CALLER_1'.try_into().unwrap() +} + +fn OTHER_CALLER() -> ContractAddress { + 'CALLER_2'.try_into().unwrap() +} + +fn deploy_counter() -> ICounterDispatcher { + let contract = declare("Counter").unwrap_syscall().contract_class(); + let (address, _) = contract.deploy(@array![]).unwrap_syscall(); + ICounterDispatcher { contract_address: address } +} + +#[test] +fn test_counter_starts_at_zero() { + let counter = deploy_counter(); + assert!(counter.get_count() == 0, "fresh counter should start at 0"); +} + +#[test] +fn test_increment_by_one() { + let counter = deploy_counter(); + let mut spy = spy_events(); + + start_cheat_caller_address(counter.contract_address, ANY_CALLER()); + counter.increment(); + stop_cheat_caller_address(counter.contract_address); + + assert!(counter.get_count() == 1, "count should be 1 after one increment"); + assert!(counter.get_last_caller() == ANY_CALLER(), "last_caller should reflect invoker"); + + spy + .assert_emitted( + @array![ + ( + counter.contract_address, + Event::Incremented( + Incremented { caller: ANY_CALLER(), new_count: 1, amount: 1 }, + ), + ), + ], + ); +} + +#[test] +fn test_increment_by_amount() { + let counter = deploy_counter(); + + start_cheat_caller_address(counter.contract_address, ANY_CALLER()); + counter.increment_by(5); + counter.increment_by(3); + stop_cheat_caller_address(counter.contract_address); + + assert!(counter.get_count() == 8, "expected 5+3=8"); +} + +#[test] +#[should_panic(expected: 'ZERO_AMOUNT')] +fn test_increment_by_zero_reverts() { + let counter = deploy_counter(); + start_cheat_caller_address(counter.contract_address, ANY_CALLER()); + counter.increment_by(0); +} + +#[test] +fn test_last_caller_updates() { + let counter = deploy_counter(); + + start_cheat_caller_address(counter.contract_address, ANY_CALLER()); + counter.increment(); + stop_cheat_caller_address(counter.contract_address); + + start_cheat_caller_address(counter.contract_address, OTHER_CALLER()); + counter.increment(); + stop_cheat_caller_address(counter.contract_address); + + assert!(counter.get_count() == 2, "two increments expected"); + assert!( + counter.get_last_caller() == OTHER_CALLER(), + "last_caller should reflect most recent invoker", + ); +} diff --git a/eth_712_account/Scarb.toml b/eth_712_account/Scarb.toml index d5553aa..3a40bee 100644 --- a/eth_712_account/Scarb.toml +++ b/eth_712_account/Scarb.toml @@ -16,6 +16,7 @@ testing_utils = { path = "../testing_utils" } [[target.starknet-contract]] sierra = true +casm = true [[test]] name = "eth_712_account_unittest" diff --git a/eth_712_account/src/eth_712_account.cairo b/eth_712_account/src/eth_712_account.cairo index bb1f89e..2eb6ae9 100644 --- a/eth_712_account/src/eth_712_account.cairo +++ b/eth_712_account/src/eth_712_account.cairo @@ -4,11 +4,14 @@ /// StarknetEth712Account /// /// Account contract that supports ISRC9_V2 (Execute from outside v2) and ISRC5 (Introspection). -/// The Account contract is initialized with an Ethereum address. -/// The transaction executed by the account is validated using EIP-712. -/// and signed using Secp256k1. -/// This allows the account to sign the txs from the wallet of a remote chain, -/// and execute them locally on Starknet. +/// The account is initialized with an "owner" Ethereum-format address — the address +/// derived from whichever secp256k1 public key authorizes outside executions. +/// Transactions are validated using EIP-712 and signed using Secp256k1. +/// +/// In the Tier 2 unlinkable flow the owner key is a fresh, client-derived session key, +/// NOT the user's MetaMask key. The storage var is still named `eth_address` for +/// storage-layout compatibility, but semantically it holds the *session* owner address +/// and contains no information about the user's real Ethereum identity. #[starknet::contract(account)] pub mod StarknetEth712Account { @@ -69,10 +72,12 @@ pub mod StarknetEth712Account { #[abi(embed_v0)] impl AdminImpl of IAccount712Admin { - fn initialize(ref self: ContractState, eth_address: EthAddress, signature: Signature) { + fn initialize( + ref self: ContractState, owner_eth_address: EthAddress, signature: Signature, + ) { assert(self.eth_address.read().is_zero(), 'ALREADY_INITIALIZED'); - assert_valid_owner(:eth_address, :signature); - self.eth_address.write(eth_address); + assert_valid_owner(eth_address: owner_eth_address, :signature); + self.eth_address.write(owner_eth_address); // Register 'execute_from_outside_v2' interface, as paymaster requires this. self.src5.register_interface(ISRC9_V2_ID); diff --git a/eth_712_account/src/interface.cairo b/eth_712_account/src/interface.cairo index 0c0ee0b..b0c0cba 100644 --- a/eth_712_account/src/interface.cairo +++ b/eth_712_account/src/interface.cairo @@ -3,7 +3,16 @@ use starknet::{ClassHash, EthAddress}; #[starknet::interface] pub trait IAccount712Admin { - fn initialize(ref self: TContractState, eth_address: EthAddress, signature: Signature); + /// Bind the account to an owner key, identified by its Ethereum-format address. + /// + /// `owner_eth_address` is the secp256k1 public-key derived Ethereum-format address + /// of whichever key will sign future `execute_from_outside_v2` calls. Under the + /// Tier 2 unlinkable flow this is a fresh "session" key derived from a one-time + /// MetaMask bootstrap signature — it is intentionally NOT the user's real MM + /// address; nothing on chain ties this value back to the user's Ethereum identity. + fn initialize( + ref self: TContractState, owner_eth_address: EthAddress, signature: Signature, + ); fn upgrade( ref self: TContractState, new_class_hash: ClassHash, diff --git a/helpers/Scarb.toml b/helpers/Scarb.toml new file mode 100644 index 0000000..ef890e4 --- /dev/null +++ b/helpers/Scarb.toml @@ -0,0 +1,38 @@ +[package] +name = "helpers" +edition.workspace = true +version.workspace = true + +[lib] + +[dependencies] +starknet.workspace = true +openzeppelin.workspace = true +account_factory = { path = "../account_factory" } + +[dev-dependencies] +snforge_std.workspace = true +starkware_utils_testing.workspace = true +testing_utils = { path = "../testing_utils" } + +[[target.starknet-contract]] +sierra = true +casm = true + +[[test]] +build-external-contracts = [ + "contracts::primer::primer::Primer", + "testing_utils::dummy_contracts::DummyEthAddressContract", + "testing_utils::dummy_contracts::SecondDummyEthAddressContract", + "testing_utils::dummy_contracts::MockOutsideExecutionTarget", + "account_factory::account_factory::AccountFactory", +] +name = "helpers_unittest" + +[profile.dev.cairo] +unstable-add-statements-functions-debug-info = true +unstable-add-statements-code-locations-debug-info = true +inlining-strategy = "avoid" + +[scripts] +test = "snforge test" diff --git a/helpers/src/earn_deploy_helper.cairo b/helpers/src/earn_deploy_helper.cairo new file mode 100644 index 0000000..8716206 --- /dev/null +++ b/helpers/src/earn_deploy_helper.cairo @@ -0,0 +1,132 @@ +use starknet::secp256_trait::Signature; +use starknet::{ContractAddress, EthAddress}; + +/// Return type expected by the Starknet privacy pool from any `privacy_invoke` +/// helper. We re-declare it locally with the same on-the-wire layout the pool +/// uses (`{ note_id: felt252, token: ContractAddress, amount: u256 }`) so this +/// crate stays decoupled from the privacy-pool repo. EarnDeployHelper never +/// produces deposits — it always returns `array![].span()` — so this struct's +/// fields are only ever serialized as a zero-length prefix and never carry data. +#[derive(Drop, Serde)] +pub struct OpenNoteDeposit { + pub note_id: felt252, + pub token: ContractAddress, + pub amount: u256, +} + +#[starknet::interface] +pub trait IEarnDeployHelper { + /// Selector hit by the privacy pool's `_apply_invoke` syscall. + /// + /// Calldata layout (provided by the SDK when the user builds a private tx): + /// - session_eth_address: EthAddress -- owner key for the new account + /// - signature: Signature -- secp256k1 ownership signature + /// over OWNERSHIP_TRANSFER_MSG_HASH + /// signed by the session key + /// - note_id: felt252 -- unused here, but the privacy + /// pool's helper-contract + /// protocol always serializes a + /// note_id as the last argument + /// of any `privacy_invoke` call + fn privacy_invoke( + ref self: TContractState, + session_eth_address: EthAddress, + signature: Signature, + note_id: felt252, + ) -> Span; + + fn privacy_pool(self: @TContractState) -> ContractAddress; + fn account_factory(self: @TContractState) -> ContractAddress; +} + +/// Invoke helper that lets a private-pool transaction deploy a Tier 2 unlinkable +/// Starknet account in the same atomic proof that pays the paymaster fee. +/// +/// The helper's only job is to forward `(session_eth_address, signature)` into +/// `IAccountFactory::deploy_account`. It accepts no tokens and forwards none — +/// if the caller wants to fund the new account in the same private tx, they +/// add an extra `withdraw` action to the same proof, targeting the account's +/// deterministic address (computable client-side from `session_eth_address` +/// before any on-chain state changes). +/// +/// Privacy property: nothing this helper touches on-chain references the user's +/// real MetaMask address. The helper itself doesn't even know the MM address +/// exists — that's the whole point. +#[starknet::contract] +pub mod EarnDeployHelper { + use account_factory::account_factory::{IAccountFactoryDispatcher, IAccountFactoryDispatcherTrait}; + use starknet::secp256_trait::Signature; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, EthAddress, get_caller_address}; + use super::{IEarnDeployHelper, OpenNoteDeposit}; + + #[storage] + struct Storage { + privacy_pool: ContractAddress, + account_factory: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + DeployedViaPool: DeployedViaPool, + } + + /// Emitted on a successful helper invocation. Note: indexed by the helper's + /// own address, not the user's; combined with the privacy-pool calldata + /// being opaque, this leaks nothing back to a MetaMask identity. + #[derive(Drop, starknet::Event, Debug, PartialEq)] + pub struct DeployedViaPool { + pub account_address: ContractAddress, + } + + #[constructor] + pub fn constructor( + ref self: ContractState, + privacy_pool: ContractAddress, + account_factory: ContractAddress, + ) { + self.privacy_pool.write(privacy_pool); + self.account_factory.write(account_factory); + } + + #[abi(embed_v0)] + pub impl EarnDeployHelperImpl of IEarnDeployHelper { + fn privacy_invoke( + ref self: ContractState, + session_eth_address: EthAddress, + signature: Signature, + note_id: felt252, + ) -> Span { + // note_id is part of the privacy-pool helper protocol but we have + // no use for it here (we do not move funds through this helper). + let _ = note_id; + + // Pool-only entry: any direct external caller would defeat the + // unlinkability story by bypassing the proof verification step. + let caller = get_caller_address(); + assert(caller == self.privacy_pool.read(), 'CALLER_NOT_PRIVACY_POOL'); + + let factory = IAccountFactoryDispatcher { + contract_address: self.account_factory.read(), + }; + let account_address = factory + .deploy_account(:session_eth_address, :signature); + + self.emit(Event::DeployedViaPool(DeployedViaPool { account_address })); + + // No surplus tokens to re-deposit into the pool. The pool's + // Serde::deserialize> on an empty span reads a + // single zero-length felt — the inner struct shape is irrelevant. + array![].span() + } + + fn privacy_pool(self: @ContractState) -> ContractAddress { + self.privacy_pool.read() + } + + fn account_factory(self: @ContractState) -> ContractAddress { + self.account_factory.read() + } + } +} diff --git a/helpers/src/earn_invoke_helper.cairo b/helpers/src/earn_invoke_helper.cairo new file mode 100644 index 0000000..f0db486 --- /dev/null +++ b/helpers/src/earn_invoke_helper.cairo @@ -0,0 +1,108 @@ +use openzeppelin::account::extensions::src9::OutsideExecution; +use starknet::ContractAddress; + +/// Mirror of `privacy::objects::OpenNoteDeposit`. We always return an empty +/// span; the fields here are never serialized with real data. +#[derive(Drop, Serde)] +pub struct OpenNoteDeposit { + pub note_id: felt252, + pub token: ContractAddress, + pub amount: u256, +} + +#[starknet::interface] +pub trait IEarnInvokeHelper { + /// Privacy-pool helper selector. The privacy pool calls + /// `target.privacy_invoke(...)` and the function name is fixed. + /// + /// We forward the (outside_execution, signature) pair to + /// `target_account.execute_from_outside_v2`. The account validates the + /// EIP-712 signature against its stored owner_eth_address and runs the + /// inner calls (e.g. counter.increment) atomically. + fn privacy_invoke( + ref self: TContractState, + target_account: ContractAddress, + outside_execution: OutsideExecution, + signature: Array, + note_id: felt252, + ) -> Span; + + fn privacy_pool(self: @TContractState) -> ContractAddress; +} + +/// Privacy-pool helper that lets a private transaction trigger +/// `execute_from_outside_v2` on a Tier 2 account. +/// +/// On-chain trace after a sponsored execute: +/// - The deploy tx is signed by AVNU's relayer. +/// - The pool calls THIS helper's `privacy_invoke`. +/// - This helper calls `target_account.execute_from_outside_v2(...)`. +/// - The account validates the signature with the session key's eth address +/// and executes the inner calls. +/// +/// The account contract's caller-validation in `execute_from_outside_v2` +/// requires either `caller == ANY_CALLER` or `caller == this_helper_addr`. +/// In the SDK flow we set `caller = ANY_CALLER` so we don't have to pin the +/// helper address into the signed OutsideExecution. +#[starknet::contract] +pub mod EarnInvokeHelper { + use openzeppelin::account::extensions::src9::{ + ISRC9_V2Dispatcher, ISRC9_V2DispatcherTrait, OutsideExecution, + }; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + use super::{IEarnInvokeHelper, OpenNoteDeposit}; + + #[storage] + struct Storage { + privacy_pool: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + InvokedThroughPool: InvokedThroughPool, + } + + #[derive(Drop, starknet::Event, Debug, PartialEq)] + pub struct InvokedThroughPool { + pub target_account: ContractAddress, + } + + #[constructor] + pub fn constructor(ref self: ContractState, privacy_pool: ContractAddress) { + self.privacy_pool.write(privacy_pool); + } + + #[abi(embed_v0)] + pub impl EarnInvokeHelperImpl of IEarnInvokeHelper { + fn privacy_invoke( + ref self: ContractState, + target_account: ContractAddress, + outside_execution: OutsideExecution, + signature: Array, + note_id: felt252, + ) -> Span { + // note_id is part of the privacy-pool protocol but unused here. + let _ = note_id; + + // Only the pool may invoke. Without this an arbitrary caller could + // bypass the proof system and submit arbitrary execute_from_outside_v2 + // calls — still authorized by the account's signature check, but + // not protected by Tier 2's relayer-paid envelope. + let caller = get_caller_address(); + assert(caller == self.privacy_pool.read(), 'CALLER_NOT_PRIVACY_POOL'); + + let account = ISRC9_V2Dispatcher { contract_address: target_account }; + account.execute_from_outside_v2(outside_execution, signature.span()); + + self.emit(Event::InvokedThroughPool(InvokedThroughPool { target_account })); + + array![].span() + } + + fn privacy_pool(self: @ContractState) -> ContractAddress { + self.privacy_pool.read() + } + } +} diff --git a/helpers/src/lib.cairo b/helpers/src/lib.cairo new file mode 100644 index 0000000..a4e01e2 --- /dev/null +++ b/helpers/src/lib.cairo @@ -0,0 +1,5 @@ +pub mod earn_deploy_helper; +pub mod earn_invoke_helper; + +#[cfg(test)] +mod test; diff --git a/helpers/src/test.cairo b/helpers/src/test.cairo new file mode 100644 index 0000000..15e8148 --- /dev/null +++ b/helpers/src/test.cairo @@ -0,0 +1,202 @@ +use helpers::earn_deploy_helper::{IEarnDeployHelperDispatcher, IEarnDeployHelperDispatcherTrait}; +use helpers::earn_invoke_helper::{IEarnInvokeHelperDispatcher, IEarnInvokeHelperDispatcherTrait}; +use openzeppelin::account::extensions::src9::OutsideExecution; +use snforge_std::{ContractClassTrait, DeclareResultTrait, declare}; +use starknet::account::Call; +use starknet::secp256_trait::Signature; +use starknet::syscalls::get_class_hash_at_syscall; +use starknet::{ContractAddress, EthAddress, SyscallResultTrait}; +use starkware_utils_testing::test_utils::cheat_caller_address_once; +use testing_utils::account_factory_utils::{eth_address_to_account, setup_account_factory_test_env}; +use testing_utils::dummy_contracts::MockOutsideExecutionTarget::{ + IMockTargetDispatcher, IMockTargetDispatcherTrait, +}; + +fn MOCK_PRIVACY_POOL() -> ContractAddress { + 'MOCK_PRIVACY_POOL'.try_into().unwrap() +} + +fn NOT_POOL() -> ContractAddress { + 'RANDO_CALLER'.try_into().unwrap() +} + +fn deploy_helper(privacy_pool: ContractAddress, account_factory: ContractAddress) -> ContractAddress { + let contract = declare("EarnDeployHelper").unwrap_syscall().contract_class(); + let mut calldata = array![]; + Serde::serialize(@privacy_pool, ref calldata); + Serde::serialize(@account_factory, ref calldata); + let (helper_address, _) = contract.deploy(@calldata).unwrap_syscall(); + helper_address +} + +#[test] +fn test_privacy_invoke_deploys_account_when_called_by_pool() { + /// The pool can call privacy_invoke with (session_eth_address, signature), + /// and the helper deploys the deterministic Tier 2 account. + let factory_addr = setup_account_factory_test_env(); + let helper_addr = deploy_helper(MOCK_PRIVACY_POOL(), factory_addr); + let helper = IEarnDeployHelperDispatcher { contract_address: helper_addr }; + + let session_eth_address: EthAddress = '0xC0FFEE'.try_into().unwrap(); + let signature = Signature { r: 0x1012, s: 0x1012, y_parity: true }; + + let expected_account = eth_address_to_account( + account_factory: factory_addr, eth_address: session_eth_address, + ); + + // Pre-call: nothing deployed at the predicted address yet. + let class_hash_before = get_class_hash_at_syscall(expected_account).unwrap_syscall(); + assert!(class_hash_before.into() == 0, "account should not be deployed yet"); + + // Caller MUST be the configured privacy pool. + cheat_caller_address_once(contract_address: helper_addr, caller_address: MOCK_PRIVACY_POOL()); + let deposits = helper.privacy_invoke(:session_eth_address, :signature, note_id: 0); + + // The helper never returns surplus. + assert!(deposits.len() == 0, "privacy_invoke must return an empty deposit span"); + + // The factory deployed the deterministic account. + let class_hash_after = get_class_hash_at_syscall(expected_account).unwrap_syscall(); + assert!(class_hash_after.into() != 0, "account should be deployed at expected address"); +} + +#[test] +#[should_panic(expected: 'CALLER_NOT_PRIVACY_POOL')] +fn test_privacy_invoke_rejects_non_pool_caller() { + /// Any caller other than the configured privacy pool must be rejected; + /// otherwise an external caller could deploy accounts at attacker-chosen + /// session addresses outside the proof-validated path. + let factory_addr = setup_account_factory_test_env(); + let helper_addr = deploy_helper(MOCK_PRIVACY_POOL(), factory_addr); + let helper = IEarnDeployHelperDispatcher { contract_address: helper_addr }; + + let session_eth_address: EthAddress = '0xC0FFEE'.try_into().unwrap(); + let signature = Signature { r: 0x1012, s: 0x1012, y_parity: true }; + + cheat_caller_address_once(contract_address: helper_addr, caller_address: NOT_POOL()); + helper.privacy_invoke(:session_eth_address, :signature, note_id: 0); +} + +#[test] +fn test_privacy_invoke_is_idempotent() { + /// Calling privacy_invoke twice with the same session_eth_address should + /// return the same account address (factory.deploy_account is idempotent). + let factory_addr = setup_account_factory_test_env(); + let helper_addr = deploy_helper(MOCK_PRIVACY_POOL(), factory_addr); + let helper = IEarnDeployHelperDispatcher { contract_address: helper_addr }; + + let session_eth_address: EthAddress = '0xC0FFEE'.try_into().unwrap(); + let signature = Signature { r: 0x1012, s: 0x1012, y_parity: true }; + let expected_account = eth_address_to_account( + account_factory: factory_addr, eth_address: session_eth_address, + ); + + cheat_caller_address_once(contract_address: helper_addr, caller_address: MOCK_PRIVACY_POOL()); + helper.privacy_invoke(:session_eth_address, :signature, note_id: 0); + let class_hash_after_first = get_class_hash_at_syscall(expected_account).unwrap_syscall(); + + cheat_caller_address_once(contract_address: helper_addr, caller_address: MOCK_PRIVACY_POOL()); + helper.privacy_invoke(:session_eth_address, :signature, note_id: 0); + let class_hash_after_second = get_class_hash_at_syscall(expected_account).unwrap_syscall(); + + assert!( + class_hash_after_first == class_hash_after_second, + "class hash must be stable across duplicate invocations", + ); +} + +#[test] +fn test_constructor_stores_pool_and_factory() { + let factory_addr = setup_account_factory_test_env(); + let helper_addr = deploy_helper(MOCK_PRIVACY_POOL(), factory_addr); + let helper = IEarnDeployHelperDispatcher { contract_address: helper_addr }; + + assert!(helper.privacy_pool() == MOCK_PRIVACY_POOL(), "pool not stored correctly"); + assert!(helper.account_factory() == factory_addr, "factory not stored correctly"); +} + +// ─── EarnInvokeHelper tests ───────────────────────────────────────────────── + +fn deploy_invoke_helper(privacy_pool: ContractAddress) -> ContractAddress { + let contract = declare("EarnInvokeHelper").unwrap_syscall().contract_class(); + let mut calldata = array![]; + Serde::serialize(@privacy_pool, ref calldata); + let (addr, _) = contract.deploy(@calldata).unwrap_syscall(); + addr +} + +fn deploy_mock_target() -> ContractAddress { + let contract = declare("MockOutsideExecutionTarget").unwrap_syscall().contract_class(); + let (addr, _) = contract.deploy(@array![]).unwrap_syscall(); + addr +} + +fn dummy_outside_execution(nonce: felt252) -> OutsideExecution { + OutsideExecution { + // ANY_CALLER — the mock target doesn't validate the caller. + caller: 'ANY_CALLER'.try_into().unwrap(), + nonce, + execute_after: 0, + execute_before: 0xFFFFFFFF, + calls: array![].span(), + } +} + +fn _unused_call_silencer() -> Call { + // Just to keep the Call import wired in for future tests; the mock + // doesn't read calls in the OutsideExecution. + Call { to: 0x0.try_into().unwrap(), selector: 0, calldata: array![].span() } +} + +#[test] +fn test_invoke_helper_forwards_to_target_account() { + let pool = MOCK_PRIVACY_POOL(); + let helper_addr = deploy_invoke_helper(pool); + let helper = IEarnInvokeHelperDispatcher { contract_address: helper_addr }; + let target_addr = deploy_mock_target(); + let target = IMockTargetDispatcher { contract_address: target_addr }; + + let ose = dummy_outside_execution(nonce: 42); + let signature: Array = array![0x11, 0x22, 0x33]; + + cheat_caller_address_once(contract_address: helper_addr, caller_address: pool); + let deposits = helper + .privacy_invoke( + target_account: target_addr, + outside_execution: ose, + signature: signature, + note_id: 0, + ); + assert!(deposits.len() == 0, "must return empty deposit span"); + + // The mock recorded the helper as msg.sender and stored the nonce we passed. + assert!(target.call_count() == 1, "target should be called exactly once"); + assert!(target.last_caller() == helper_addr, "target should see helper as caller"); + assert!(target.last_nonce() == 42, "target should receive our outside_execution nonce"); +} + +#[test] +#[should_panic(expected: 'CALLER_NOT_PRIVACY_POOL')] +fn test_invoke_helper_rejects_non_pool_caller() { + let pool = MOCK_PRIVACY_POOL(); + let helper_addr = deploy_invoke_helper(pool); + let helper = IEarnInvokeHelperDispatcher { contract_address: helper_addr }; + let target_addr = deploy_mock_target(); + + cheat_caller_address_once(contract_address: helper_addr, caller_address: NOT_POOL()); + helper + .privacy_invoke( + target_account: target_addr, + outside_execution: dummy_outside_execution(nonce: 1), + signature: array![], + note_id: 0, + ); +} + +#[test] +fn test_invoke_helper_constructor_stores_pool() { + let pool = MOCK_PRIVACY_POOL(); + let helper_addr = deploy_invoke_helper(pool); + let helper = IEarnInvokeHelperDispatcher { contract_address: helper_addr }; + assert!(helper.privacy_pool() == pool, "pool not stored correctly"); +} diff --git a/testing_utils/src/dummy_contracts.cairo b/testing_utils/src/dummy_contracts.cairo index 8d3dd8d..70ebe40 100644 --- a/testing_utils/src/dummy_contracts.cairo +++ b/testing_utils/src/dummy_contracts.cairo @@ -49,3 +49,82 @@ pub fn declare_second_dummy_eth_address_contract() -> ClassHash { .contract_class() .class_hash } + +/// Minimal ISRC9_V2 stub used to test the EarnInvokeHelper. Records the +/// number of times `execute_from_outside_v2` was called and the caller at +/// each invocation, so tests can assert that the helper forwarded properly. +#[starknet::contract] +pub mod MockOutsideExecutionTarget { + use openzeppelin::account::extensions::src9::OutsideExecution; + use starknet::ContractAddress; + use starknet::get_caller_address; + use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + + #[storage] + struct Storage { + call_count: u64, + last_caller: ContractAddress, + last_nonce: felt252, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Recorded: Recorded, + } + + #[derive(Drop, starknet::Event, Debug, PartialEq)] + pub struct Recorded { + pub caller: ContractAddress, + pub nonce: felt252, + } + + #[starknet::interface] + pub trait IMockTarget { + fn call_count(self: @TContractState) -> u64; + fn last_caller(self: @TContractState) -> ContractAddress; + fn last_nonce(self: @TContractState) -> felt252; + fn execute_from_outside_v2( + ref self: TContractState, + outside_execution: OutsideExecution, + signature: Span, + ) -> Array>; + } + + #[abi(embed_v0)] + impl MockImpl of IMockTarget { + fn call_count(self: @ContractState) -> u64 { + self.call_count.read() + } + + fn last_caller(self: @ContractState) -> ContractAddress { + self.last_caller.read() + } + + fn last_nonce(self: @ContractState) -> felt252 { + self.last_nonce.read() + } + + fn execute_from_outside_v2( + ref self: ContractState, + outside_execution: OutsideExecution, + signature: Span, + ) -> Array> { + let _ = signature; + let caller = get_caller_address(); + self.call_count.write(self.call_count.read() + 1); + self.last_caller.write(caller); + self.last_nonce.write(outside_execution.nonce); + self.emit(Event::Recorded(Recorded { caller, nonce: outside_execution.nonce })); + array![] + } + } +} + +/// Declare the `MockOutsideExecutionTarget` contract and return its class hash. +pub fn declare_mock_outside_execution_target() -> ClassHash { + *snforge_std::declare("MockOutsideExecutionTarget") + .unwrap_syscall() + .contract_class() + .class_hash +} From 94e19ac187d86065940c82e5887658a14bc62fe4 Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 13 May 2026 09:39:03 +0530 Subject: [PATCH 2/4] feat(tier2_wallet): client library, browser demo, sponsored-private flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TypeScript side of the Tier 2 stack. Lives in tier2_wallet/, a self-contained npm package built on @noble/curves + starknet.js v10 + the Starknet privacy SDK (path-dep to ../../privacy/starknet-privacy/sdk). Wallet primitives (src/): - derive.ts: MetaMask personal_sign(bootstrap) → keccak → secp256k1 session keypair. Deterministic and re-derivable from MM alone, no second backup. - address.ts: computeAccountAddress() mirrors Cairo eth_address_to_account so the deterministic Tier 2 account address is known client-side before any on-chain action. - sign.ts: signOwnership for factory.deploy_account, plus a hand-rolled EIP-712 OutsideExecution signer that mirrors eth_712_utils. cairo byte-for-byte. Cross-checked against the existing eth_712_account/scripts/generate_test_signatures.py (4 byte-identical parity tests live in tests/sign.test.ts). - mm-signer.ts: MmSigner interface with FixedKeyMmSigner (tests) and BrowserMmSigner (window.ethereum) implementations. - constants.ts: typehashes, primer class hashes (test + prod), bootstrap message, masks. End-to-end sponsored flows (src/): - paymaster.ts: AVNU paymaster JSON-RPC client (vendored / adapted from paymaster/examples/private-sponsored-web). Sponsored-private apply_action mode only — fees paid from inside the privacy pool, never from the MetaMask user's STRK. - sponsored-deploy.ts: privateSponsoredDeployTier2Account — Alice's pool notes pay AVNU gas; EarnDeployHelper.privacy_invoke forwards (session_eth, signature) into the factory. MetaMask never signs a Starknet tx. - sponsored-execute.ts: privateSponsoredExecuteOnTier2Account — same envelope but invoking EarnInvokeHelper, which relays an EIP-712-signed OutsideExecution into the deployed account. Includes counterIncrementCall + readCounterCount helpers. Browser demo (demo/): - index.html / style.css — 6 step walkthrough. - demo.ts — Connect MM → derive session → compute Starknet addr → sign ownership → sponsored deploy → call Counter.increment via Tier 2. - esbuild bundle, python3 -m http.server for serving. Deploy infrastructure (scripts/): - deploy-contracts.ts: declares Primer, StarknetEth712Account, AccountFactory, EarnDeployHelper, Counter; deploys AccountFactory + EarnDeployHelper instances. Idempotent declares; persists addresses to sepolia-deployments.json and .env. - deploy-extras.ts: declares EarnInvokeHelper; deploys a Counter instance + EarnInvokeHelper instance for the demo. Tests: 20 passing (3 files). The 4 EIP-712 parity tests are the load-bearing ones: if they regress, signatures will fail is_valid_signature on chain. Secrets handling: - .env / demo/config.ts are gitignored (.env.example is the template). - Alice's key being shipped to the browser is documented in the demo config as throwaway-only; production must lift this into a backend. Co-Authored-By: Claude Opus 4.7 (1M context) --- tier2_wallet/.env.example | 44 + tier2_wallet/.gitignore | 15 + tier2_wallet/README.md | 95 + tier2_wallet/demo/demo.ts | 379 +++ tier2_wallet/demo/index.html | 165 + tier2_wallet/demo/serve.sh | 23 + tier2_wallet/demo/style.css | 156 + tier2_wallet/demo/tsconfig.json | 14 + tier2_wallet/package-lock.json | 3476 ++++++++++++++++++++++ tier2_wallet/package.json | 39 + tier2_wallet/scripts/deploy-contracts.ts | 274 ++ tier2_wallet/scripts/deploy-extras.ts | 236 ++ tier2_wallet/src/address.ts | 55 + tier2_wallet/src/constants.ts | 65 + tier2_wallet/src/derive.ts | 82 + tier2_wallet/src/index.ts | 27 + tier2_wallet/src/mm-signer.ts | 145 + tier2_wallet/src/paymaster.ts | 175 ++ tier2_wallet/src/sign.ts | 183 ++ tier2_wallet/src/sponsored-deploy.ts | 237 ++ tier2_wallet/src/sponsored-execute.ts | 216 ++ tier2_wallet/src/types.ts | 71 + tier2_wallet/src/util.ts | 81 + tier2_wallet/tests/address.test.ts | 74 + tier2_wallet/tests/derive.test.ts | 56 + tier2_wallet/tests/sign.test.ts | 289 ++ tier2_wallet/tsconfig.json | 26 + 27 files changed, 6698 insertions(+) create mode 100644 tier2_wallet/.env.example create mode 100644 tier2_wallet/.gitignore create mode 100644 tier2_wallet/README.md create mode 100644 tier2_wallet/demo/demo.ts create mode 100644 tier2_wallet/demo/index.html create mode 100755 tier2_wallet/demo/serve.sh create mode 100644 tier2_wallet/demo/style.css create mode 100644 tier2_wallet/demo/tsconfig.json create mode 100644 tier2_wallet/package-lock.json create mode 100644 tier2_wallet/package.json create mode 100644 tier2_wallet/scripts/deploy-contracts.ts create mode 100644 tier2_wallet/scripts/deploy-extras.ts create mode 100644 tier2_wallet/src/address.ts create mode 100644 tier2_wallet/src/constants.ts create mode 100644 tier2_wallet/src/derive.ts create mode 100644 tier2_wallet/src/index.ts create mode 100644 tier2_wallet/src/mm-signer.ts create mode 100644 tier2_wallet/src/paymaster.ts create mode 100644 tier2_wallet/src/sign.ts create mode 100644 tier2_wallet/src/sponsored-deploy.ts create mode 100644 tier2_wallet/src/sponsored-execute.ts create mode 100644 tier2_wallet/src/types.ts create mode 100644 tier2_wallet/src/util.ts create mode 100644 tier2_wallet/tests/address.test.ts create mode 100644 tier2_wallet/tests/derive.test.ts create mode 100644 tier2_wallet/tests/sign.test.ts create mode 100644 tier2_wallet/tsconfig.json diff --git a/tier2_wallet/.env.example b/tier2_wallet/.env.example new file mode 100644 index 0000000..5ad7272 --- /dev/null +++ b/tier2_wallet/.env.example @@ -0,0 +1,44 @@ +## DO NOT commit a populated copy of this file. +## Copy to .env and fill in your secrets locally. tier2_wallet/.gitignore +## excludes .env. + +## ── Starknet network ──────────────────────────────────────────────────── +STARKNET_RPC_URL=https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/REPLACE_ME +# Encoded short-string for Starknet Sepolia. Used in EIP-712 domain `name`. +STARKNET_CHAIN_ID=0x534e5f5345504f4c4941 + +## ── Admin (declares + deploys all contracts on first setup) ───────────── +## Fund this account with ~0.05 STRK on Sepolia before running deploy scripts. +STARKNET_ADMIN_ADDRESS=0x... +STARKNET_ADMIN_PRIVATE_KEY=0x... + +## ── Alice (pool sponsor for all MetaMask Tier 2 deploys) ──────────────── +## Fund this account with ~0.1 STRK on Sepolia + the deposit amount. +## In the current demo Alice's key is shipped to the browser — anyone who +## inspects the bundled JS can drain her pool notes. Use a throwaway key +## with limited funds; never reuse a real wallet. +ALICE_ADDRESS=0x... +ALICE_PRIVATE_KEY=0x... +# Any non-zero bigint. Must stay stable across runs or Alice loses access +# to her existing notes. +ALICE_VIEWING_KEY=0xA11CE +# How much pool-token Alice deposits on initial setup (wei). 10 STRK = 1e19. +ALICE_INITIAL_DEPOSIT=10000000000000000000 + +## ── Privacy pool + paymaster (Sepolia) ───────────────────────────────── +PRIVACY_POOL_ADDRESS=0x254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91 +POOL_TOKEN_ADDRESS=0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d +PAYMASTER_URL=https://sepolia.paymaster.avnu.fi +PAYMASTER_API_KEY= +DISCOVERY_URL=http://35.192.48.142:8080 +PROVING_URL=http://34.29.249.119:3000 + +## ── Standard Starknet UDC (mainnet + sepolia) ────────────────────────── +UDC_ADDRESS=0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf + +## ── Output addresses (populated automatically by scripts/deploy-contracts.ts) ── +## You can also fill these in manually if you've deployed before. +FACTORY_ADDRESS= +HELPER_ADDRESS= +COUNTER_CLASS_HASH= +STARKNET_ETH712_ACCOUNT_CLASS_HASH= diff --git a/tier2_wallet/.gitignore b/tier2_wallet/.gitignore new file mode 100644 index 0000000..c26d972 --- /dev/null +++ b/tier2_wallet/.gitignore @@ -0,0 +1,15 @@ +node_modules/ +dist/ +demo/dist/ +*.log +.DS_Store + +# Secrets — never commit +.env +.env.local +.env.*.local +demo/config.ts + +# Local deployment artifacts (per-developer addresses; uncomment if you +# want to share them via git instead). +# sepolia-deployments.json diff --git a/tier2_wallet/README.md b/tier2_wallet/README.md new file mode 100644 index 0000000..f9b3846 --- /dev/null +++ b/tier2_wallet/README.md @@ -0,0 +1,95 @@ +# tier2_wallet + +Client primitives for the Tier 2 unlinkable Starknet account flow. Pure +client-side cryptography — no Starknet RPCs, no paymaster, no privacy pool. +Everything in this package is composable building blocks the higher-level +demo / wallet code will assemble. + +``` +MetaMask wallet ─── personal_sign(BOOTSTRAP_MESSAGE) ───► this lib + │ + ▼ + secp256k1 session key + (independent of MM) + │ + ┌─────────────────┼──────────────────┐ + ▼ ▼ ▼ + computeAccountAddress signOwnership signOutsideExecution + (Starknet addr) (deploy_account) (execute_from_outside_v2) +``` + +## Public API + +```ts +import { + deriveSessionKey, // MmSigner → SessionKey (deterministic) + sessionKeyFromPriv, // raw priv → SessionKey (tests only) + computeAccountAddress, // session addr + factory → deterministic Starknet addr + signOwnership, // SessionKey → { r, s, yParity } for factory.deploy_account + encodeOwnershipSignature, // → 5 felts in the order Cairo Serde expects + signOutsideExecution, // OutsideExecution → 6 felts for execute_from_outside_v2 + computeOutsideExecutionHash, // EIP-712 digest, useful for cross-checks + BrowserMmSigner, // window.ethereum adapter + FixedKeyMmSigner, // test-only signer with a fixed private key +} from "tier2-wallet"; +``` + +## Tests + +```bash +npm install +npm run test +``` + +20 tests including 4 byte-identical parity checks against +`eth_712_account/scripts/generate_test_signatures.py`. If those fail, the +on-chain `is_valid_signature` check will reject the wallet — they are the +contract. + +## Browser demo + +```bash +npm install +npm run demo # bundles + serves on http://localhost:8088 +# or step by step: +npm run demo:bundle # esbuild → demo/dist/demo.js +npm run demo:serve # python3 -m http.server 8088 +``` + +Open the URL in a browser with MetaMask installed. The page: + +1. Connects MetaMask (`eth_requestAccounts`). +2. Asks MM to `personal_sign` the fixed bootstrap message. +3. Derives the session key, shows its ETH-format address. +4. Computes the deterministic Starknet account address for a configurable + factory. +5. Produces the ownership signature felts to pass to `deploy_account`. + +Nothing is broadcast on-chain in the demo. The next milestone wires this +into the AVNU paymaster + `starknet-privacy-sdk` to actually deploy. + +## Security notes + +- **The session key is reproducible from the MM bootstrap signature alone.** + This means the user only needs MetaMask (any RFC-6979 wallet — Ledger + notably differs) to recover access. There is no second seed to back up. +- **The session key is never equal to the MM key.** It's derived from the + signature, which is a one-way function of (msgHash, MMpriv). MetaMask + doesn't even know its key produced this scalar. +- **Determinism gotcha.** If a future wallet rotates to non-deterministic + ECDSA, the same MetaMask seed will produce a different session key on each + call → loss of access. Test against the wallets you intend to support. +- **The bootstrap message is versioned (`v1`).** Bump to `v2` to rotate the + derivation; existing users keep `v1` keys. +- **Cairo `y_parity` convention quirk.** Cairo uses `y_parity = (v % 2 == 0)` + (so v=27 → false, v=28 → true). The encoding helpers handle this — don't + pass raw EVM `v` to Cairo without going through `signOwnership` / + `signOutsideExecution`. + +## Cross-references + +- Cairo factory: `account_factory/src/account_factory.cairo` (deploys via + `session_eth_address` salt) +- Cairo account: `eth_712_account/src/eth_712_account.cairo` +- EIP-712 hash logic: `eth_712_account/src/eth_712_utils.cairo` +- Python reference signatures: `eth_712_account/scripts/generate_test_signatures.py` diff --git a/tier2_wallet/demo/demo.ts b/tier2_wallet/demo/demo.ts new file mode 100644 index 0000000..34a8b56 --- /dev/null +++ b/tier2_wallet/demo/demo.ts @@ -0,0 +1,379 @@ +import { + Account, + RpcProvider, + constants as starknetConstants, +} from "starknet"; +import { + IndexerDiscoveryProvider, + ProvingServiceProofProvider, + createPrivateTransfers, +} from "@starkware-libs/starknet-privacy-sdk"; + +import { + BOOTSTRAP_MESSAGE, + BrowserMmSigner, + PRIMER_CLASS_HASH, + PaymasterClient, + computeAccountAddress, + counterIncrementCall, + deriveSessionKey, + encodeOwnershipSignature, + privateSponsoredDeployTier2Account, + privateSponsoredExecuteOnTier2Account, + readCounterCount, + signOwnership, +} from "../src/index.js"; +import { DEMO_CONFIG } from "./config.js"; + +/** + * Browser demo of the full Tier 2 unlinkable deploy flow. + * + * Steps: + * 1. Connect MetaMask → `eth_requestAccounts` + * 2. Derive session key → personal_sign + keccak + secp256k1 + * 3. Compute deterministic Starknet account → mirror Cairo derivation + * 4. Sign ownership → secp256k1 over OWNERSHIP_TRANSFER_MSG_HASH + * 5. Deploy via Alice + privacy pool + paymaster (sponsored_private) + * — Alice pays the gas from her pool notes; nothing on chain references + * the user's MetaMask address. + */ + +const els = { + status: byId("status"), + mmAddr: byId("mm-addr"), + sessionAddr: byId("session-addr"), + accountAddr: byId("account-addr"), + ownershipSig: byId("ownership-sig"), + sponsoredOutput: byId("sponsored-output"), + sponsoredLog: byId("sponsored-log"), + counterOutput: byId("counter-output"), + counterLog: byId("counter-log"), + counterValueBefore: byId("counter-value-before"), + counterValueAfter: byId("counter-value-after"), + connectBtn: byId("connect-btn") as HTMLButtonElement, + deriveBtn: byId("derive-btn") as HTMLButtonElement, + computeBtn: byId("compute-btn") as HTMLButtonElement, + signBtn: byId("sign-btn") as HTMLButtonElement, + sponsoredBtn: byId("sponsored-btn") as HTMLButtonElement, + counterBtn: byId("counter-btn") as HTMLButtonElement, + counterReadBtn: byId("counter-read-btn") as HTMLButtonElement, + factoryInput: byId("factory-input") as HTMLInputElement, + networkSelect: byId("network-select") as HTMLSelectElement, + bootstrapMsg: byId("bootstrap-msg"), +}; + +function byId(id: string): HTMLElement { + const el = document.getElementById(id); + if (!el) throw new Error(`#${id} not in DOM`); + return el; +} + +function status(msg: string, kind: "info" | "ok" | "err" = "info"): void { + els.status.textContent = msg; + els.status.className = `status status--${kind}`; +} + +function ensureMm(): unknown { + const eth = (globalThis as { ethereum?: unknown }).ethereum; + if (!eth) throw new Error("MetaMask not detected. Install the extension and refresh."); + return eth; +} + +let signer: BrowserMmSigner | null = null; +let sessionKey: Awaited> | null = null; +let accountAddrBigInt: bigint | null = null; +let lastDeploy: + | Awaited> + | null = null; + +// Prefill factory address + bootstrap msg + counter address from config. +els.bootstrapMsg.textContent = BOOTSTRAP_MESSAGE; +els.factoryInput.value = DEMO_CONFIG.factoryAddress; +els.networkSelect.value = DEMO_CONFIG.network; +const counterAddrDisplay = document.getElementById("counter-addr-display"); +if (counterAddrDisplay) counterAddrDisplay.textContent = DEMO_CONFIG.counterAddress; + +els.connectBtn.addEventListener("click", async () => { + try { + status("Requesting accounts from MetaMask…"); + signer = new BrowserMmSigner(ensureMm()); + const addr = await signer.address(); + els.mmAddr.textContent = addr; + els.deriveBtn.disabled = false; + status(`Connected as ${addr}`, "ok"); + } catch (e) { + status(errMsg(e), "err"); + } +}); + +els.deriveBtn.addEventListener("click", async () => { + try { + if (!signer) throw new Error("Connect MetaMask first"); + status(`Signing "${BOOTSTRAP_MESSAGE}" in MetaMask…`); + sessionKey = await deriveSessionKey(signer); + els.sessionAddr.textContent = sessionKey.ethAddress; + els.computeBtn.disabled = false; + els.signBtn.disabled = false; + els.sponsoredBtn.disabled = false; + status("Session key derived. NOTHING was broadcast — pure client crypto.", "ok"); + } catch (e) { + status(errMsg(e), "err"); + } +}); + +els.computeBtn.addEventListener("click", () => { + try { + if (!sessionKey) throw new Error("Derive session key first"); + const factoryAddress = els.factoryInput.value.trim(); + if (!factoryAddress.startsWith("0x")) { + throw new Error("Factory address must be 0x-prefixed hex"); + } + const network = + els.networkSelect.value === "test" ? ("test" as const) : ("prod" as const); + const addr = computeAccountAddress({ + sessionEthAddress: sessionKey.ethAddress, + factoryAddress: factoryAddress as `0x${string}`, + network, + }); + accountAddrBigInt = addr; + els.accountAddr.textContent = "0x" + addr.toString(16).padStart(64, "0"); + status( + `Account would be deployed at this address by factory ${factoryAddress}.`, + "ok", + ); + } catch (e) { + status(errMsg(e), "err"); + } +}); + +els.signBtn.addEventListener("click", () => { + try { + if (!sessionKey) throw new Error("Derive session key first"); + const sig = signOwnership(sessionKey); + const felts = encodeOwnershipSignature(sig).map((f) => "0x" + f.toString(16)); + els.ownershipSig.textContent = JSON.stringify(felts, null, 2); + status( + "Ownership signature ready. Pass these 5 felts as the `signature` field to factory.deploy_account.", + "ok", + ); + } catch (e) { + status(errMsg(e), "err"); + } +}); + +els.sponsoredBtn.addEventListener("click", async () => { + els.sponsoredBtn.disabled = true; + try { + if (!sessionKey) throw new Error("Derive session key first"); + appendLog(""); + appendLog("── Sponsored private deploy ─────────────────────────────"); + + const provider = new RpcProvider({ nodeUrl: DEMO_CONFIG.starknetRpcUrl }); + const alice = new Account({ + provider, + address: DEMO_CONFIG.alice.address, + signer: DEMO_CONFIG.alice.privateKey, + cairoVersion: "1", + }); + appendLog(`alice account: ${DEMO_CONFIG.alice.address}`); + + const discovery = new IndexerDiscoveryProvider( + DEMO_CONFIG.discoveryUrl, + DEMO_CONFIG.poolAddress, + ); + const provingProvider = new ProvingServiceProofProvider( + DEMO_CONFIG.provingUrl, + DEMO_CONFIG.starknetChainId as unknown as starknetConstants.StarknetChainId, + ); + + const viewingKey = BigInt(DEMO_CONFIG.alice.viewingKey); + const transfers = createPrivateTransfers({ + account: alice, + viewingKeyProvider: { getViewingKey: async () => viewingKey }, + provingProvider, + discoveryProvider: discovery, + poolContractAddress: DEMO_CONFIG.poolAddress, + }); + + const paymaster = new PaymasterClient( + DEMO_CONFIG.paymasterUrl, + DEMO_CONFIG.paymasterApiKey, + ); + + status("Building sponsored private deploy via Alice…"); + const result = await privateSponsoredDeployTier2Account( + { + starknetChainId: DEMO_CONFIG.starknetChainId, + poolAddress: DEMO_CONFIG.poolAddress, + poolFeeToken: DEMO_CONFIG.poolFeeToken, + helperAddress: DEMO_CONFIG.helperAddress, + factoryAddress: DEMO_CONFIG.factoryAddress, + network: DEMO_CONFIG.network, + paymasterApiKey: DEMO_CONFIG.paymasterApiKey, + paymasterUrl: DEMO_CONFIG.paymasterUrl, + }, + { + alice, + provider, + transfers, + paymaster, + session: sessionKey, + log: appendLog, + }, + ); + lastDeploy = result; + + els.sponsoredOutput.textContent = + `tx: ${result.transactionHash}\n` + + `account: ${result.accountAddressHex}\n` + + `class hash at account: ${result.classHashAtAddress}\n` + + `fee paid by Alice: ${result.feePaidByAlice}`; + status( + `Deployed! Tier 2 account live at ${result.accountAddressHex}. MetaMask never paid gas, never signed a Starknet tx, and is not referenced on-chain.`, + "ok", + ); + } catch (e) { + appendLog(`ERROR: ${errMsg(e)}`); + status(errMsg(e), "err"); + } finally { + els.sponsoredBtn.disabled = false; + } +}); + +function appendLog(line: string): void { + const ts = new Date().toISOString().slice(11, 23); + els.sponsoredLog.textContent += `[${ts}] ${line}\n`; + els.sponsoredLog.scrollTop = els.sponsoredLog.scrollHeight; +} + +function appendCounterLog(line: string): void { + const ts = new Date().toISOString().slice(11, 23); + els.counterLog.textContent += `[${ts}] ${line}\n`; + els.counterLog.scrollTop = els.counterLog.scrollHeight; +} + +function errMsg(e: unknown): string { + if (e instanceof Error) return e.message; + return String(e); +} + +// ─── Step 6: Increment Counter via Tier 2 account ────────────────────────── + +let lastIncrement: Awaited< + ReturnType +> | null = null; + +els.counterReadBtn.addEventListener("click", async () => { + try { + const provider = new RpcProvider({ nodeUrl: DEMO_CONFIG.starknetRpcUrl }); + const count = await readCounterCount(provider, DEMO_CONFIG.counterAddress); + els.counterValueBefore.textContent = count.toString(); + status(`Counter is currently at ${count}.`, "ok"); + } catch (e) { + status(errMsg(e), "err"); + } +}); + +els.counterBtn.addEventListener("click", async () => { + els.counterBtn.disabled = true; + try { + if (!sessionKey) throw new Error("Derive session key first (step 2)"); + if (!lastDeploy) + throw new Error("Deploy the Tier 2 account first (step 5)"); + + appendCounterLog(""); + appendCounterLog("── Sponsored private execute ────────────────────────"); + + const provider = new RpcProvider({ nodeUrl: DEMO_CONFIG.starknetRpcUrl }); + const beforeCount = await readCounterCount( + provider, + DEMO_CONFIG.counterAddress, + ); + els.counterValueBefore.textContent = beforeCount.toString(); + appendCounterLog(`counter before: ${beforeCount}`); + + const alice = new Account({ + provider, + address: DEMO_CONFIG.alice.address, + signer: DEMO_CONFIG.alice.privateKey, + cairoVersion: "1", + }); + const discovery = new IndexerDiscoveryProvider( + DEMO_CONFIG.discoveryUrl, + DEMO_CONFIG.poolAddress, + ); + const provingProvider = new ProvingServiceProofProvider( + DEMO_CONFIG.provingUrl, + DEMO_CONFIG.starknetChainId as unknown as starknetConstants.StarknetChainId, + ); + const viewingKey = BigInt(DEMO_CONFIG.alice.viewingKey); + const transfers = createPrivateTransfers({ + account: alice, + viewingKeyProvider: { getViewingKey: async () => viewingKey }, + provingProvider, + discoveryProvider: discovery, + poolContractAddress: DEMO_CONFIG.poolAddress, + }); + const paymaster = new PaymasterClient( + DEMO_CONFIG.paymasterUrl, + DEMO_CONFIG.paymasterApiKey, + ); + + status("Building sponsored private Counter.increment…"); + const result = await privateSponsoredExecuteOnTier2Account( + { + starknetChainId: DEMO_CONFIG.starknetChainId, + poolAddress: DEMO_CONFIG.poolAddress, + poolFeeToken: DEMO_CONFIG.poolFeeToken, + invokeHelperAddress: DEMO_CONFIG.invokeHelperAddress, + paymasterApiKey: DEMO_CONFIG.paymasterApiKey, + paymasterUrl: DEMO_CONFIG.paymasterUrl, + }, + { + alice, + provider, + transfers, + paymaster, + session: sessionKey, + accountAddress: lastDeploy.accountAddressHex, + calls: [counterIncrementCall(DEMO_CONFIG.counterAddress)], + nonce: BigInt(Date.now()) * 1000n + BigInt(Math.floor(Math.random() * 1000)), + log: appendCounterLog, + }, + ); + lastIncrement = result; + + const afterCount = await readCounterCount( + provider, + DEMO_CONFIG.counterAddress, + ); + els.counterValueAfter.textContent = afterCount.toString(); + appendCounterLog(`counter after: ${afterCount}`); + + els.counterOutput.textContent = + `tx: ${result.transactionHash}\n` + + `nonce used: ${result.outsideExecution.nonce}\n` + + `fee paid by Alice: ${result.feePaidByAlice}\n` + + `counter: ${beforeCount} → ${afterCount}`; + status( + `Counter incremented via Tier 2 account ${lastDeploy.accountAddressHex}. MetaMask never signed a Starknet tx; Alice's notes paid the gas.`, + "ok", + ); + } catch (e) { + appendCounterLog(`ERROR: ${errMsg(e)}`); + status(errMsg(e), "err"); + } finally { + els.counterBtn.disabled = false; + } +}); + +// Expose for console poking. +(globalThis as unknown as { tier2?: unknown }).tier2 = { + get signer() { return signer; }, + get sessionKey() { return sessionKey; }, + get accountAddrBigInt() { return accountAddrBigInt; }, + get lastDeploy() { return lastDeploy; }, + get lastIncrement() { return lastIncrement; }, + PRIMER_CLASS_HASH, + config: DEMO_CONFIG, +}; diff --git a/tier2_wallet/demo/index.html b/tier2_wallet/demo/index.html new file mode 100644 index 0000000..0a627af --- /dev/null +++ b/tier2_wallet/demo/index.html @@ -0,0 +1,165 @@ + + + + + + Tier 2 Wallet — MetaMask demo + + + +
+

Tier 2 Wallet — MetaMask demo

+

+ Derive an unlinkable Starknet account key from a single MetaMask + signature, then deploy it on Starknet Sepolia via the privacy pool + + AVNU paymaster (Alice's pool notes pay the gas — MetaMask never + signs a Starknet tx and is not referenced on chain). +

+ +
+

1. Connect

+ +
+ MetaMask address + +
+
+ +
+

2. Derive session key

+

+ MetaMask is asked to sign the fixed bootstrap message below. The + signature is hashed with keccak256 to seed a fresh secp256k1 + keypair. The resulting key is independent of your MetaMask + address — the on-chain account will hold this fresh key, + not the MetaMask one. +

+
+ Bootstrap message + +
+ +
+ Session ETH address + +
+
+ +
+

3. Compute Starknet account address

+

+ Deterministic Pedersen hash over the session address + factory + address + Primer class hash. This matches what + account_factory::deploy_account would derive on-chain. +

+
+ + +
+ +
+ Starknet account address + +
+
+ +
+

4. Ownership signature

+

+ Produce the secp256k1 signature over + OWNERSHIP_TRANSFER_MSG_HASH that the factory's + deploy_account consumes. The output is the 5-felt + calldata layout of Signature { r, s, y_parity }. +

+ +
+ Signature felts (calldata for deploy_account) +
+
+
+ +
+

5. Sponsored private deploy

+

+ Push the session key on chain via Alice's privacy-pool notes. The + AVNU paymaster's sponsored_private mode bundles a + fee-withdraw from Alice's notes into the same proof that calls + EarnDeployHelper.privacy_invoke → + factory.deploy_account. Alice signs the proof off-chain; + AVNU's relayer submits the tx; MetaMask is not involved at + all. +

+ +
+ Result + +
+
+ Live progress + +
+
+ +
+

6. Call Counter via your Tier 2 account

+

+ Sign an EIP-712 OutsideExecution with the session key + (no MetaMask popup — the session key is in memory from step 2). + The signature is forwarded through EarnInvokeHelper in + the same kind of sponsored_private envelope as the + deploy: Alice's notes pay the gas, AVNU's relayer signs and submits + the tx, the Tier 2 account validates the signature and runs + Counter.increment(). +

+
+ Counter address + +
+
+ Count (before) + + + Count (after) + +
+ +
+ Result +
+
+
+ Live progress +

+        
+
+ +

Ready.

+ +
+

+ End-to-end Tier 2 unlinkable deploy. The user holds only MetaMask; + Alice holds the pool notes that pay every user's deploy gas. On + chain, the deployed account's owner is a fresh secp256k1 key whose + relationship to any MetaMask address is recoverable only by the + browser session that derived it. +

+
+
+ + + + diff --git a/tier2_wallet/demo/serve.sh b/tier2_wallet/demo/serve.sh new file mode 100755 index 0000000..0005b31 --- /dev/null +++ b/tier2_wallet/demo/serve.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Serve the bundled demo on http://localhost:8088 +# +# Usage: +# npm run demo:bundle && npm run demo:serve +# or +# npm run demo +# +# MetaMask requires a real HTTP origin — `file://` URLs do NOT work. + +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +PORT="${PORT:-8088}" + +if [[ ! -f "$DIR/dist/demo.js" ]]; then + echo "demo bundle missing — run \`npm run demo:bundle\` first" >&2 + exit 1 +fi + +echo "Serving $DIR on http://localhost:$PORT/ (Ctrl-C to stop)" +cd "$DIR" +python3 -m http.server "$PORT" diff --git a/tier2_wallet/demo/style.css b/tier2_wallet/demo/style.css new file mode 100644 index 0000000..6aca02c --- /dev/null +++ b/tier2_wallet/demo/style.css @@ -0,0 +1,156 @@ +:root { + --fg: #1a1a1a; + --muted: #6b7280; + --bg: #f8fafc; + --card: #ffffff; + --border: #e5e7eb; + --accent: #4338ca; + --ok: #047857; + --err: #b91c1c; + --code-bg: #f3f4f6; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + padding: 2rem 1rem 4rem; + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", + "Helvetica Neue", Arial, sans-serif; + background: var(--bg); + color: var(--fg); + line-height: 1.5; +} + +main { + max-width: 760px; + margin: 0 auto; +} + +h1 { + font-size: 1.75rem; + margin-top: 0; +} + +h2 { + font-size: 1.1rem; + margin-bottom: 0.5rem; + color: var(--accent); +} + +.lead { + color: var(--muted); + margin-bottom: 2rem; +} + +section { + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.25rem 1.5rem; + margin-bottom: 1.25rem; +} + +button { + font: inherit; + background: var(--accent); + color: white; + border: 0; + padding: 0.55rem 1.1rem; + border-radius: 8px; + cursor: pointer; + margin: 0.4rem 0; +} + +button:disabled { + background: #cbd5e1; + cursor: not-allowed; +} + +button:hover:not(:disabled) { + filter: brightness(1.06); +} + +.kv { + display: flex; + align-items: baseline; + gap: 0.75rem; + margin-top: 0.6rem; + font-size: 0.92rem; + flex-wrap: wrap; +} + +.kv--block { + flex-direction: column; + align-items: stretch; + gap: 0.3rem; +} + +.kv .k { + color: var(--muted); + min-width: 11rem; +} + +code, pre { + background: var(--code-bg); + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-size: 0.85rem; + word-break: break-all; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +pre { + padding: 0.75rem 1rem; + margin: 0; + white-space: pre-wrap; +} + +.form { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 0.75rem; + margin: 0.75rem 0; +} + +.form label { + display: flex; + flex-direction: column; + font-size: 0.85rem; + color: var(--muted); + gap: 0.25rem; +} + +input, select { + font: inherit; + padding: 0.45rem 0.6rem; + border: 1px solid var(--border); + border-radius: 6px; + background: white; +} + +.status { + padding: 0.7rem 1rem; + border-radius: 8px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.85rem; +} + +.status--info { background: #eef2ff; color: #312e81; } +.status--ok { background: #ecfdf5; color: var(--ok); } +.status--err { background: #fef2f2; color: var(--err); } + +pre.log { + max-height: 14rem; + overflow-y: auto; + background: #0f172a; + color: #d1fae5; + font-size: 0.78rem; + line-height: 1.45; +} + +footer { + margin-top: 2rem; + color: var(--muted); + font-size: 0.85rem; +} diff --git a/tier2_wallet/demo/tsconfig.json b/tier2_wallet/demo/tsconfig.json new file mode 100644 index 0000000..dd67b38 --- /dev/null +++ b/tier2_wallet/demo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "..", + "declaration": false, + "declarationMap": false, + "sourceMap": true, + "noEmit": false, + "module": "ESNext", + "moduleResolution": "Bundler" + }, + "include": ["../src/**/*.ts", "./demo.ts"] +} diff --git a/tier2_wallet/package-lock.json b/tier2_wallet/package-lock.json new file mode 100644 index 0000000..08c2ba8 --- /dev/null +++ b/tier2_wallet/package-lock.json @@ -0,0 +1,3476 @@ +{ + "name": "tier2-wallet", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tier2-wallet", + "version": "0.1.0", + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@starkware-libs/starknet-privacy-sdk": "file:../../privacy/starknet-privacy/sdk", + "dotenv": "^17.4.2", + "starknet": "^10.0.2" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "esbuild": "^0.23.1", + "tsx": "^4.7.0", + "typescript": "^5.4.0", + "vitest": "^1.6.0" + }, + "engines": { + "node": ">=20" + } + }, + "../../privacy/starknet-privacy/sdk": { + "name": "@starkware-libs/starknet-privacy-sdk", + "version": "0.14.2", + "license": "ISC", + "dependencies": { + "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.1", + "starknet": "github:starkware-libs/starknet.js#PRIVACY-0.14.2-RC.2", + "starknet-devnet": "^0.7.2", + "zod": "^3.24.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/node": "^24.10.1", + "@vitest/browser": "^4.0.17", + "@vitest/browser-playwright": "^4.0.17", + "@vitest/coverage-v8": "^4.0.17", + "ajv": "^8.18.0", + "esbuild": "^0.27.2", + "eslint": "^9.39.2", + "playwright": "^1.58.1", + "prettier": "^3.7.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.51.0", + "vitest": "^4.0.15" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/starknet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@scure/starknet/-/starknet-1.1.0.tgz", + "integrity": "sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.7.0", + "@noble/hashes": "~1.6.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/starknet/node_modules/@noble/curves": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/starknet/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/starknet/node_modules/@noble/hashes": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@starknet-io/get-starknet-wallet-standard": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-wallet-standard/-/get-starknet-wallet-standard-5.0.0.tgz", + "integrity": "sha512-isDNGDlp16W24HE4IuweYXLDRZN0JbsDnazAieeKXE87Mn+jqhsjgTsMxcwWTjX7v906Bjz39FiDjGUddnr36g==", + "license": "MIT", + "dependencies": { + "@starknet-io/types-js": "^0.7.10", + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0", + "ox": "^0.4.4" + } + }, + "node_modules/@starknet-io/starknet-types-0101": { + "name": "@starknet-io/types-js", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.10.2.tgz", + "integrity": "sha512-AtUFPYdmo9DqVus++aBSoY9W13/2PZmillPr8/mXZjc+V0iYJ/QTmkTsbw+es2mnLeLhYWSymW9ivQzyyyKdog==", + "license": "MIT" + }, + "node_modules/@starknet-io/starknet-types-09": { + "name": "@starknet-io/types-js", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.9.2.tgz", + "integrity": "sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg==", + "license": "MIT" + }, + "node_modules/@starknet-io/types-js": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.7.10.tgz", + "integrity": "sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==", + "license": "MIT" + }, + "node_modules/@starkware-libs/starknet-privacy-sdk": { + "resolved": "../../privacy/starknet-privacy/sdk", + "link": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", + "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/abi-wan-kanabi": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/abi-wan-kanabi/-/abi-wan-kanabi-2.2.4.tgz", + "integrity": "sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg==", + "license": "ISC", + "dependencies": { + "ansicolors": "^0.3.2", + "cardinal": "^2.1.1", + "fs-extra": "^10.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "generate": "dist/generate.js" + } + }, + "node_modules/abitype": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", + "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "license": "MIT", + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "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.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/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/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "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/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/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-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.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-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lossless-json": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz", + "integrity": "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "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/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ox": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.4.4.tgz", + "integrity": "sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw==", + "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/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "license": "MIT", + "dependencies": { + "esprima": "~4.0.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/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/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/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/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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/starknet": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/starknet/-/starknet-10.0.2.tgz", + "integrity": "sha512-bXdmvHiQ60XpwPb5mNOPfGg4MS+ppLiHj83yvhNnc+kDGyuYUUrnfZEU9O53/WFU8nP9XUtn0RwJpf47aafyKA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.7.0", + "@noble/hashes": "~1.6.0", + "@scure/base": "~1.2.1", + "@scure/starknet": "1.1.0", + "@starknet-io/get-starknet-wallet-standard": "^5.0.0", + "@starknet-io/starknet-types-0101": "npm:@starknet-io/types-js@0.10.2", + "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.2", + "abi-wan-kanabi": "2.2.4", + "lossless-json": "^4.2.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/starknet/node_modules/@noble/curves": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/starknet/node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "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/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/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "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/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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tier2_wallet/package.json b/tier2_wallet/package.json new file mode 100644 index 0000000..20e0f70 --- /dev/null +++ b/tier2_wallet/package.json @@ -0,0 +1,39 @@ +{ + "name": "tier2-wallet", + "version": "0.1.0", + "private": true, + "description": "Client primitives for the Tier 2 unlinkable Starknet account flow: derive a fresh session key from a MetaMask bootstrap signature, compute the deterministic Starknet account address, and sign ownership + EIP-712 OutsideExecution payloads.", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "demo:bundle": "esbuild demo/demo.ts --bundle --format=esm --target=es2022 --outfile=demo/dist/demo.js --sourcemap", + "demo:serve": "bash demo/serve.sh", + "demo": "npm run demo:bundle && npm run demo:serve", + "build:contracts": "cd .. && SCARB_PROFILE=release scarb build", + "deploy:contracts": "npm run build:contracts && tsx scripts/deploy-contracts.ts", + "deploy:extras": "npm run build:contracts && tsx scripts/deploy-extras.ts" + }, + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@starkware-libs/starknet-privacy-sdk": "file:../../privacy/starknet-privacy/sdk", + "dotenv": "^17.4.2", + "starknet": "^10.0.2" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "esbuild": "^0.23.1", + "tsx": "^4.7.0", + "typescript": "^5.4.0", + "vitest": "^1.6.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/tier2_wallet/scripts/deploy-contracts.ts b/tier2_wallet/scripts/deploy-contracts.ts new file mode 100644 index 0000000..5ac3c5a --- /dev/null +++ b/tier2_wallet/scripts/deploy-contracts.ts @@ -0,0 +1,274 @@ +/** + * Declare + deploy the Tier 2 stack to Starknet Sepolia: + * + * 1. Declare Primer (account_factory factory step 1) + * 2. Declare StarknetEth712Account (the upgrade target the Primer becomes) + * 3. Declare AccountFactory (factory bytecode) + * 4. Declare EarnDeployHelper (privacy_invoke shim) + * 5. Declare Counter (demo target — instances created later) + * 6. Deploy AccountFactory(governance_admin=ADMIN, upgrade_delay=0, + * account_class_hash=StarknetEth712Account class) + * 7. Deploy EarnDeployHelper(privacy_pool=PRIVACY_POOL, account_factory=factory_addr) + * + * The script is idempotent: it skips declarations / deployments that already + * exist on chain. Run it as many times as needed while iterating. + * + * Output: sepolia-deployments.json with every address + class hash. The + * script also rewrites tier2_wallet/.env's FACTORY_ADDRESS / HELPER_ADDRESS + * / COUNTER_CLASS_HASH / STARKNET_ETH712_ACCOUNT_CLASS_HASH lines so the + * runtime picks them up without you editing by hand. + * + * Private keys: NEVER printed to stdout. Reads only from .env via dotenv. + */ +import "dotenv/config"; +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + Account, + CallData, + RpcProvider, + hash, + type CompiledSierra, + type CompiledSierraCasm, +} from "starknet"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WORKSPACE_ROOT = resolve(__dirname, "../.."); + +function required(name: string): string { + const v = process.env[name]; + if (!v) { + console.error(`✗ Missing env var: ${name}. Set it in tier2_wallet/.env`); + process.exit(1); + } + return v; +} + +function readClass(relPath: string): { sierra: CompiledSierra; casm: CompiledSierraCasm } { + const sierraPath = resolve(WORKSPACE_ROOT, `target/release/${relPath}.contract_class.json`); + const casmPath = resolve( + WORKSPACE_ROOT, + `target/release/${relPath}.compiled_contract_class.json`, + ); + if (!existsSync(sierraPath) || !existsSync(casmPath)) { + console.error( + `✗ Missing build artifact: ${relPath}. Run \`SCARB_PROFILE=release scarb build\` from the workspace root first.`, + ); + process.exit(1); + } + return { + sierra: JSON.parse(readFileSync(sierraPath, "utf8")) as CompiledSierra, + casm: JSON.parse(readFileSync(casmPath, "utf8")) as CompiledSierraCasm, + }; +} + +async function isDeclared(provider: RpcProvider, classHash: string): Promise { + try { + await provider.getClassByHash(classHash); + return true; + } catch { + return false; + } +} + +async function declareClass( + provider: RpcProvider, + account: Account, + name: string, + relPath: string, +): Promise { + const { sierra, casm } = readClass(relPath); + const classHash = hash.computeContractClassHash(sierra); + const compiledClassHash = hash.computeCompiledClassHash(casm); + + if (await isDeclared(provider, classHash)) { + console.log(` ✓ ${name.padEnd(28)} class ${classHash} (already declared)`); + return classHash; + } + + console.log(` → declaring ${name} class ${classHash}…`); + const declared = await account.declareIfNot({ + contract: sierra, + casm, + classHash, + compiledClassHash, + }); + if (declared.transaction_hash) { + console.log(` tx: ${declared.transaction_hash}`); + await provider.waitForTransaction(declared.transaction_hash); + } + console.log(` ✓ ${name.padEnd(28)} declared`); + return classHash; +} + +async function deployContract( + provider: RpcProvider, + account: Account, + name: string, + udcAddress: string, + classHash: string, + constructorCalldata: string[], +): Promise { + // unique=false → deterministic on (deployer, salt, classHash, calldata). + // Salt is the current millisecond so repeated runs produce a new address; + // we don't try to be idempotent at the address level here (the deploy step + // is cheap once classes are declared). + const salt = "0x" + BigInt(Date.now()).toString(16); + const calldata = CallData.compile([ + classHash, + salt, + "0", + constructorCalldata.length.toString(), + ...constructorCalldata, + ]); + + console.log(` → deploying ${name} via UDC (salt ${salt})…`); + const tx = await account.execute({ + contractAddress: udcAddress, + entrypoint: "deployContract", + calldata, + }); + console.log(` tx: ${tx.transaction_hash}`); + const receipt = (await provider.waitForTransaction(tx.transaction_hash)) as unknown as { + events?: { from_address?: string; data?: string[] }[]; + }; + // RPC returns from_address without leading zero ("0x41a78…"), our env has + // the full padded form ("0x041a78…"). Compare as bigints to ignore that. + const udcBig = BigInt(udcAddress); + const udcEvent = (receipt.events ?? []).find( + (e) => + e.from_address != null && + BigInt(e.from_address) === udcBig && + (e.data?.length ?? 0) >= 1, + ); + const addr = udcEvent?.data?.[0]; + if (!addr) { + console.error("✗ UDC deploy event missing — could not extract deployed address."); + console.error(JSON.stringify(receipt, null, 2)); + process.exit(1); + } + console.log(` ✓ ${name.padEnd(28)} deployed at ${addr}`); + return addr; +} + +/** Persist deployment addresses to disk + back into .env so callers pick them up. */ +function persistAddresses(addresses: Record) { + const outPath = resolve(__dirname, "../sepolia-deployments.json"); + writeFileSync(outPath, JSON.stringify(addresses, null, 2) + "\n"); + console.log(`\n✓ Wrote ${outPath}`); + + const envPath = resolve(__dirname, "../.env"); + if (!existsSync(envPath)) { + console.log("(.env not found — skipping env rewrite; copy values from the JSON above)"); + return; + } + let env = readFileSync(envPath, "utf8"); + for (const [key, value] of Object.entries(addresses)) { + const re = new RegExp(`^${key}=.*$`, "m"); + if (re.test(env)) { + env = env.replace(re, `${key}=${value}`); + } else { + env += `\n${key}=${value}`; + } + } + writeFileSync(envPath, env); + console.log(`✓ Updated ${envPath} with deployed addresses`); +} + +async function main() { + const rpcUrl = required("STARKNET_RPC_URL"); + const adminAddress = required("STARKNET_ADMIN_ADDRESS"); + const adminPriv = required("STARKNET_ADMIN_PRIVATE_KEY"); + const udcAddress = required("UDC_ADDRESS"); + const privacyPool = required("PRIVACY_POOL_ADDRESS"); + + // The Primer class hash is baked into account_factory/src/utils.cairo. If + // our local build doesn't produce that hash, the factory will not be able + // to deploy_syscall the Primer on chain → abort early with a clear error. + const EXPECTED_PRIMER = + "0x03edae2158f4aea6295470678fc7de27e19d7e40f295cda90d786d17b3531fdf"; + + const provider = new RpcProvider({ nodeUrl: rpcUrl }); + const chainId = await provider.getChainId(); + console.log(`Chain: ${chainId}`); + console.log(`Admin: ${adminAddress}\n`); + + const account = new Account({ + provider, + address: adminAddress, + signer: adminPriv, + cairoVersion: "1", + }); + + console.log("── Declaring classes ──────────────────────────────────────"); + const primerHash = await declareClass(provider, account, "Primer", "contracts_Primer"); + if (BigInt(primerHash) !== BigInt(EXPECTED_PRIMER)) { + console.error( + `\n✗ Primer class hash drift!\n expected: ${EXPECTED_PRIMER}\n got: ${primerHash}\n` + + ` Update account_factory/src/utils.cairo::PRIMER_CLASS_HASH AND\n` + + ` tier2_wallet/src/constants.ts::PRIMER_CLASS_HASH.prod to the new value,\n` + + ` then rebuild and re-run.`, + ); + process.exit(1); + } + const accountClass = await declareClass( + provider, + account, + "StarknetEth712Account", + "eth_712_account_StarknetEth712Account", + ); + const factoryClass = await declareClass( + provider, + account, + "AccountFactory", + "account_factory_AccountFactory", + ); + const helperClass = await declareClass( + provider, + account, + "EarnDeployHelper", + "helpers_EarnDeployHelper", + ); + const counterClass = await declareClass(provider, account, "Counter", "counter_Counter"); + + console.log("\n── Deploying singletons ───────────────────────────────────"); + // AccountFactory(governance_admin: ContractAddress, upgrade_delay: u64, account_class_hash: ClassHash) + const factoryAddr = await deployContract( + provider, + account, + "AccountFactory", + udcAddress, + factoryClass, + [adminAddress, "0", accountClass], + ); + + // EarnDeployHelper(privacy_pool: ContractAddress, account_factory: ContractAddress) + const helperAddr = await deployContract( + provider, + account, + "EarnDeployHelper", + udcAddress, + helperClass, + [privacyPool, factoryAddr], + ); + + persistAddresses({ + PRIMER_CLASS_HASH: primerHash, + STARKNET_ETH712_ACCOUNT_CLASS_HASH: accountClass, + FACTORY_CLASS_HASH: factoryClass, + HELPER_CLASS_HASH: helperClass, + COUNTER_CLASS_HASH: counterClass, + FACTORY_ADDRESS: factoryAddr, + HELPER_ADDRESS: helperAddr, + }); + + console.log("\n✓ All deployments complete."); +} + +main().catch((e) => { + console.error("\n✗ deploy-contracts failed:", e instanceof Error ? e.message : String(e)); + if (e instanceof Error && e.stack) console.error(e.stack); + process.exit(1); +}); diff --git a/tier2_wallet/scripts/deploy-extras.ts b/tier2_wallet/scripts/deploy-extras.ts new file mode 100644 index 0000000..a92b526 --- /dev/null +++ b/tier2_wallet/scripts/deploy-extras.ts @@ -0,0 +1,236 @@ +/** + * Phase F deploy: declare + deploy the *runtime* artifacts that go on top of + * the core Tier 2 stack: + * + * - Declare EarnInvokeHelper class (idempotent) + * - Deploy a single EarnInvokeHelper instance bound to the privacy pool + * - Deploy a single Counter instance (Counter class declared + * in deploy-contracts.ts) + * + * Output: appends to sepolia-deployments.json and patches .env with + * INVOKE_HELPER_ADDRESS + COUNTER_ADDRESS so the runtime picks them up. + * + * Re-runnable. Each invocation creates a NEW Counter+helper pair (we don't + * try to be idempotent at the address level here — Counter is cheap; if + * you want a stable address, comment out the deploy and reuse what's in + * .env / sepolia-deployments.json). + */ +import "dotenv/config"; +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + Account, + CallData, + RpcProvider, + hash, + type CompiledSierra, + type CompiledSierraCasm, +} from "starknet"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WORKSPACE_ROOT = resolve(__dirname, "../.."); + +function required(name: string): string { + const v = process.env[name]; + if (!v) { + console.error(`✗ Missing env var: ${name}. Set it in tier2_wallet/.env`); + process.exit(1); + } + return v; +} + +function readClass(relPath: string): { sierra: CompiledSierra; casm: CompiledSierraCasm } { + const sierraPath = resolve(WORKSPACE_ROOT, `target/release/${relPath}.contract_class.json`); + const casmPath = resolve( + WORKSPACE_ROOT, + `target/release/${relPath}.compiled_contract_class.json`, + ); + if (!existsSync(sierraPath) || !existsSync(casmPath)) { + console.error( + `✗ Missing build artifact: ${relPath}. Run \`SCARB_PROFILE=release scarb build\` from the workspace root first.`, + ); + process.exit(1); + } + return { + sierra: JSON.parse(readFileSync(sierraPath, "utf8")) as CompiledSierra, + casm: JSON.parse(readFileSync(casmPath, "utf8")) as CompiledSierraCasm, + }; +} + +async function isDeclared(provider: RpcProvider, classHash: string): Promise { + try { + await provider.getClassByHash(classHash); + return true; + } catch { + return false; + } +} + +async function declareClass( + provider: RpcProvider, + account: Account, + name: string, + relPath: string, +): Promise { + const { sierra, casm } = readClass(relPath); + const classHash = hash.computeContractClassHash(sierra); + const compiledClassHash = hash.computeCompiledClassHash(casm); + + if (await isDeclared(provider, classHash)) { + console.log(` ✓ ${name.padEnd(28)} class ${classHash} (already declared)`); + return classHash; + } + + console.log(` → declaring ${name} class ${classHash}…`); + const declared = await account.declareIfNot({ + contract: sierra, + casm, + classHash, + compiledClassHash, + }); + if (declared.transaction_hash) { + console.log(` tx: ${declared.transaction_hash}`); + await provider.waitForTransaction(declared.transaction_hash); + } + console.log(` ✓ ${name.padEnd(28)} declared`); + return classHash; +} + +async function deployViaUDC( + provider: RpcProvider, + account: Account, + name: string, + udcAddress: string, + classHash: string, + constructorCalldata: string[], +): Promise { + const salt = "0x" + BigInt(Date.now()).toString(16); + const calldata = CallData.compile([ + classHash, + salt, + "0", + constructorCalldata.length.toString(), + ...constructorCalldata, + ]); + + console.log(` → deploying ${name} via UDC (salt ${salt})…`); + const tx = await account.execute({ + contractAddress: udcAddress, + entrypoint: "deployContract", + calldata, + }); + console.log(` tx: ${tx.transaction_hash}`); + const receipt = (await provider.waitForTransaction(tx.transaction_hash)) as unknown as { + events?: { from_address?: string; data?: string[] }[]; + }; + const udcBig = BigInt(udcAddress); + const udcEvent = (receipt.events ?? []).find( + (e) => + e.from_address != null && + BigInt(e.from_address) === udcBig && + (e.data?.length ?? 0) >= 1, + ); + const addr = udcEvent?.data?.[0]; + if (!addr) { + console.error("✗ UDC deploy event missing — could not extract deployed address."); + console.error(JSON.stringify(receipt, null, 2)); + process.exit(1); + } + console.log(` ✓ ${name.padEnd(28)} deployed at ${addr}`); + return addr; +} + +function persistAddresses(addresses: Record) { + const jsonPath = resolve(__dirname, "../sepolia-deployments.json"); + let existing: Record = {}; + if (existsSync(jsonPath)) { + existing = JSON.parse(readFileSync(jsonPath, "utf8")); + } + const merged = { ...existing, ...addresses }; + writeFileSync(jsonPath, JSON.stringify(merged, null, 2) + "\n"); + console.log(`\n✓ Updated ${jsonPath}`); + + const envPath = resolve(__dirname, "../.env"); + if (!existsSync(envPath)) { + console.log("(.env not found — skipping env rewrite; copy values from the JSON above)"); + return; + } + let env = readFileSync(envPath, "utf8"); + for (const [key, value] of Object.entries(addresses)) { + const re = new RegExp(`^${key}=.*$`, "m"); + if (re.test(env)) { + env = env.replace(re, `${key}=${value}`); + } else { + env += `\n${key}=${value}`; + } + } + writeFileSync(envPath, env); + console.log(`✓ Updated ${envPath}`); +} + +async function main() { + const rpcUrl = required("STARKNET_RPC_URL"); + const adminAddress = required("STARKNET_ADMIN_ADDRESS"); + const adminPriv = required("STARKNET_ADMIN_PRIVATE_KEY"); + const udcAddress = required("UDC_ADDRESS"); + const privacyPool = required("PRIVACY_POOL_ADDRESS"); + + const provider = new RpcProvider({ nodeUrl: rpcUrl }); + console.log(`Chain: ${await provider.getChainId()}`); + console.log(`Admin: ${adminAddress}\n`); + + const account = new Account({ + provider, + address: adminAddress, + signer: adminPriv, + cairoVersion: "1", + }); + + console.log("── Declaring classes ──────────────────────────────────────"); + // Counter was already declared by deploy-contracts.ts; we just need its + // class hash here. EarnInvokeHelper is new in Phase F. + const counterClass = await declareClass(provider, account, "Counter", "counter_Counter"); + const invokeHelperClass = await declareClass( + provider, + account, + "EarnInvokeHelper", + "helpers_EarnInvokeHelper", + ); + + console.log("\n── Deploying singletons ───────────────────────────────────"); + // Counter has no constructor args. + const counterAddr = await deployViaUDC( + provider, + account, + "Counter", + udcAddress, + counterClass, + [], + ); + // EarnInvokeHelper(privacy_pool). + const invokeHelperAddr = await deployViaUDC( + provider, + account, + "EarnInvokeHelper", + udcAddress, + invokeHelperClass, + [privacyPool], + ); + + persistAddresses({ + COUNTER_CLASS_HASH: counterClass, + INVOKE_HELPER_CLASS_HASH: invokeHelperClass, + COUNTER_ADDRESS: counterAddr, + INVOKE_HELPER_ADDRESS: invokeHelperAddr, + }); + + console.log("\n✓ Phase F deploys complete."); +} + +main().catch((e) => { + console.error("\n✗ deploy-extras failed:", e instanceof Error ? e.message : String(e)); + if (e instanceof Error && e.stack) console.error(e.stack); + process.exit(1); +}); diff --git a/tier2_wallet/src/address.ts b/tier2_wallet/src/address.ts new file mode 100644 index 0000000..d3856ed --- /dev/null +++ b/tier2_wallet/src/address.ts @@ -0,0 +1,55 @@ +import { hash } from "starknet"; + +import { PRIMER_CLASS_HASH } from "./constants.js"; +import type { Hex } from "./types.js"; +import { hexToBigInt } from "./util.js"; + +/** + * Network the Tier 2 deployment targets. Determines which PRIMER_CLASS_HASH + * is used (test vs prod). + */ +export type Network = "test" | "prod"; + +/** + * Compute the deterministic Starknet account contract address the factory + * will deploy for a given session Ethereum-format address. + * + * Mirrors `account_factory/src/utils.cairo::eth_address_to_account`: + * address = pedersen_on_elements([ + * 'STARKNET_CONTRACT_ADDRESS', + * deployer = factory_address, + * salt = session_eth_address as felt252, + * class_hash = PRIMER_CLASS_HASH, + * calldata_hash = pedersen_on_elements([]) + * ]) + * + * Pre-computing this client-side is essential for the Tier 2 flow: the + * frontend builds private-pool actions that transfer funds to this address + * BEFORE the account is actually deployed. + */ +export function computeAccountAddress(opts: { + sessionEthAddress: bigint | Hex; + factoryAddress: bigint | Hex; + network?: Network; + /** Override `PRIMER_CLASS_HASH` for unusual deployments. */ + primerClassHash?: bigint; +}): bigint { + const network = opts.network ?? "prod"; + const salt = toBigInt(opts.sessionEthAddress); + const deployer = toBigInt(opts.factoryAddress); + const classHash = + opts.primerClassHash ?? PRIMER_CLASS_HASH[network]; + + const addrHex = hash.calculateContractAddressFromHash( + "0x" + salt.toString(16), + "0x" + classHash.toString(16), + [], + "0x" + deployer.toString(16), + ); + + return BigInt(addrHex); +} + +function toBigInt(v: bigint | Hex): bigint { + return typeof v === "bigint" ? v : hexToBigInt(v); +} diff --git a/tier2_wallet/src/constants.ts b/tier2_wallet/src/constants.ts new file mode 100644 index 0000000..6712553 --- /dev/null +++ b/tier2_wallet/src/constants.ts @@ -0,0 +1,65 @@ +/** + * Constants mirroring the on-chain Cairo definitions. + * + * Any change to a typehash, class hash, or message string on the Cairo side + * MUST be reflected here verbatim. The wallet computes signatures off-chain + * that must match what `eth_712_utils.cairo` and `account_factory` consume. + */ + +// keccak256("\x19Ethereum Signed Message:\n41Sign to verify that you own this account.") +// from eth_712_utils.cairo:27 +export const OWNERSHIP_TRANSFER_MSG_HASH = + 0x3ce976d55131cd0bdd49f20afbded052d8e907dc6034d95cdf117a8fd7752e3cn; + +// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") +// from eth_712_utils.cairo:12 +export const EIP712_DOMAIN_TYPE_HASH = + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fn; + +// keccak256("Call(uint256 address,uint256 selector,uint256[] data)") +// from eth_712_utils.cairo:16 +export const CALL_TYPE_HASH = + 0x7793b9bed3b87c6119fe923f0da4e85e1f97a03272a446514622ee7bd62ad25fn; + +// keccak256 of the OutsideExecution struct typestring; see eth_712_utils.cairo:19 +export const OUTSIDE_EXECUTION_TYPE_HASH = + 0x57fbef2abe14202f3651b3935a8feddd357b8f83a862e046239d196ec76f281en; + +// keccak256("2") — domain version +export const VERSION_HASH = + 0xad7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5n; + +// account_factory/src/utils.cairo: PRIMER_CLASS_HASH. Two values exist — +// the test-target hash and the production hash. Wallet code must pick +// the right one for the deployment target. +export const PRIMER_CLASS_HASH = { + // Must match account_factory/src/utils.cairo::PRIMER_CLASS_HASH and the + // on-chain Primer class declaration. If you upgrade scarb or change Primer, + // bump both sides. + prod: + 0x03edae2158f4aea6295470678fc7de27e19d7e40f295cda90d786d17b3531fdfn, + test: + 0x0279a9bb18604f4ae57633373d56656063203f236cc5aeceea8f2cf40f6336d7n, +} as const; + +// Fixed message MM signs to derive the session key. Versioning lets us +// rotate the derivation in the future without breaking existing users — +// they keep their v1 key, new users get v2. +export const BOOTSTRAP_MESSAGE = "Derive Earn Starknet account key v1"; + +// 'ANY_CALLER' as a felt (ASCII-encoded). +// Use TextEncoder so this module is browser-safe (Buffer is Node-only). +export const ANY_CALLER = bytesToBigIntBE( + new TextEncoder().encode("ANY_CALLER"), +); + +function bytesToBigIntBE(bytes: Uint8Array): bigint { + let v = 0n; + for (const b of bytes) v = (v << 8n) | BigInt(b); + return v; +} + +export const MASK_128 = (1n << 128n) - 1n; +export const MASK_250 = (1n << 250n) - 1n; +export const SECP256K1_N = + 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; diff --git a/tier2_wallet/src/derive.ts b/tier2_wallet/src/derive.ts new file mode 100644 index 0000000..aacb0f9 --- /dev/null +++ b/tier2_wallet/src/derive.ts @@ -0,0 +1,82 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { keccak_256 } from "@noble/hashes/sha3"; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; + +import { BOOTSTRAP_MESSAGE, SECP256K1_N } from "./constants.js"; +import type { MmSigner } from "./mm-signer.js"; +import type { Hex, SessionKey } from "./types.js"; +import { addressFromPubKey } from "./util.js"; + +/** + * Derive the Tier 2 session keypair from a MetaMask bootstrap signature. + * + * Algorithm: + * 1. Have the MM signer produce a deterministic personal_sign over the + * fixed BOOTSTRAP_MESSAGE. + * 2. seed = keccak256(signature_bytes). + * 3. privKey = (seed mod (N - 1)) + 1 where N = secp256k1 order. + * (`+ 1` ensures the scalar is non-zero; rejecting the ~0% probability + * that seed-mod-N collides on 0 keeps the derivation total.) + * 4. pubKey = G * privKey. + * 5. ethAddress = lower-20-bytes of keccak256(pubKey_uncompressed_xy). + * + * The whole privacy property of Tier 2 hinges on this private key NEVER + * being equal to the user's MetaMask key — i.e., that the user signed an + * arbitrary message rather than handing over their seed. The two keys + * share entropy via the signature, but the public key derived here has + * no on-chain history outside this Starknet account. + */ +export async function deriveSessionKey( + signer: MmSigner, + bootstrapMessage: string = BOOTSTRAP_MESSAGE, +): Promise { + const sig = await signer.personalSign(bootstrapMessage); + return deriveSessionKeyFromSignature(sig); +} + +/** + * Construct a SessionKey from a raw 32-byte secp256k1 private key. Mostly + * useful for tests that need to use the same key as a non-JS reference + * implementation (e.g. the Python signature generator). Production callers + * should never invoke this directly — they should derive via + * {@link deriveSessionKey}. + */ +export function sessionKeyFromPriv(privHex: Hex): SessionKey { + if (!privHex.startsWith("0x") || privHex.length !== 66) { + throw new Error("expected 32-byte hex private key"); + } + const privBytes = hexToBytes(privHex.slice(2)); + const pub = secp256k1.getPublicKey(privBytes, false); + return { + privKey: privHex, + pubKeyUncompressed: ("0x" + bytesToHex(pub.subarray(1))) as Hex, + ethAddress: addressFromPubKey(pub), + }; +} + +/** + * Variant of {@link deriveSessionKey} for when the caller already holds the + * raw `personal_sign` output. Useful in tests and when the bootstrap signature + * is cached. + */ +export function deriveSessionKeyFromSignature(sig: Hex): SessionKey { + if (!sig.startsWith("0x") || sig.length !== 132) { + throw new Error(`expected 65-byte hex signature, got length ${sig.length}`); + } + const sigBytes = hexToBytes(sig.slice(2)); + const seedBytes = keccak_256(sigBytes); + const seed = BigInt("0x" + bytesToHex(seedBytes)); + + // privKey ∈ [1, N-1]. + const privScalar = (seed % (SECP256K1_N - 1n)) + 1n; + const privHex = privScalar.toString(16).padStart(64, "0"); + const privBytes = hexToBytes(privHex); + + const pub = secp256k1.getPublicKey(privBytes, false); // 65-byte 0x04 || X || Y + const pubXY = pub.subarray(1); + return { + privKey: ("0x" + privHex) as Hex, + pubKeyUncompressed: ("0x" + bytesToHex(pubXY)) as Hex, + ethAddress: addressFromPubKey(pub), + }; +} diff --git a/tier2_wallet/src/index.ts b/tier2_wallet/src/index.ts new file mode 100644 index 0000000..33a4d44 --- /dev/null +++ b/tier2_wallet/src/index.ts @@ -0,0 +1,27 @@ +/** + * Tier 2 wallet primitives. + * + * The public API is intentionally narrow. Compose these four building blocks + * to flow a user from "I clicked Connect MetaMask" to "I have an unlinkable + * Starknet account that can sign txs": + * + * 1. `deriveSessionKey(mmSigner)` → SessionKey + * 2. `computeAccountAddress({ sessionEthAddress, factoryAddress })` + * → Starknet account addr + * 3. `signOwnership(session)` → CairoSignature for + * factory.deploy_account + * 4. `signOutsideExecution({ session, outsideExecution, domain })` + * → 6-felt span for + * execute_from_outside_v2 + */ + +export * from "./constants.js"; +export * from "./types.js"; +export * from "./util.js"; +export * from "./mm-signer.js"; +export * from "./derive.js"; +export * from "./address.js"; +export * from "./sign.js"; +export * from "./paymaster.js"; +export * from "./sponsored-deploy.js"; +export * from "./sponsored-execute.js"; diff --git a/tier2_wallet/src/mm-signer.ts b/tier2_wallet/src/mm-signer.ts new file mode 100644 index 0000000..e498858 --- /dev/null +++ b/tier2_wallet/src/mm-signer.ts @@ -0,0 +1,145 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { keccak_256 } from "@noble/hashes/sha3"; +import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils"; + +import type { Hex } from "./types.js"; +import { addressFromPubKey } from "./util.js"; + +/** + * Abstraction over whatever produces a `personal_sign` signature. + * + * In production this is a `window.ethereum.request({ method: 'personal_sign' })` + * wrapper. In tests it is a stand-in that uses a known private key so that the + * derived session key (and therefore every downstream artifact) is reproducible. + * + * `personalSign` MUST be deterministic for the same key + message + * (RFC-6979). Wallets that use non-deterministic ECDSA will produce a + * different session key on each call and will not be supported. + */ +export interface MmSigner { + /** + * Return the Ethereum-format address that backs this signer. Used purely + * for sanity checks in the demo — never recorded on Starknet. + */ + address(): Promise; + + /** + * Sign `message` with the EIP-191 personal_sign envelope: + * keccak256("\x19Ethereum Signed Message:\n" || len(message) || message) + * Returns a hex string `0x{r}{s}{v}` (65 bytes / 130 hex chars). + */ + personalSign(message: string): Promise; +} + +/** + * Compute the EIP-191 personal_sign digest for a message. + */ +export function personalSignDigest(message: string): Uint8Array { + const msgBytes = utf8ToBytes(message); + const prefix = utf8ToBytes( + `\x19Ethereum Signed Message:\n${msgBytes.length}`, + ); + const combined = new Uint8Array(prefix.length + msgBytes.length); + combined.set(prefix, 0); + combined.set(msgBytes, prefix.length); + return keccak_256(combined); +} + +/** + * Test signer backed by a fixed private key. Never use this in production. + * + * The default key matches `eth_712_account/scripts/generate_test_signatures.py` + * (`0xa6d86467...e8`, address `0xbF60187c5dFfA627249f1C3000A4168dbB9D7A1A`) + * so Python-generated reference signatures and JS-generated signatures can be + * compared directly as cross-checks. + */ +export class FixedKeyMmSigner implements MmSigner { + readonly privKey: Uint8Array; + + constructor( + privKeyHex: Hex = "0xa6d86467b6ec9e161649b27edfd8519e75a2e1cf5f4c309c628706e6999780e8", + ) { + this.privKey = hexToBytes(privKeyHex.slice(2)); + } + + async address(): Promise { + const pub = secp256k1.getPublicKey(this.privKey, false); + return addressFromPubKey(pub); + } + + async personalSign(message: string): Promise { + const digest = personalSignDigest(message); + const sig = secp256k1.sign(digest, this.privKey, { lowS: true }); + // Build EVM-style 65-byte signature: r || s || v (v = 27 + recovery) + const r = sig.r.toString(16).padStart(64, "0"); + const s = sig.s.toString(16).padStart(64, "0"); + const v = (27 + (sig.recovery ?? 0)).toString(16).padStart(2, "0"); + return ("0x" + r + s + v) as Hex; + } + + /** Convenience: return the address synchronously for tests. */ + addressSync(): Hex { + const pub = secp256k1.getPublicKey(this.privKey, false); + return addressFromPubKey(pub); + } + + /** Convenience: return the signature synchronously for tests. */ + personalSignSync(message: string): Hex { + const digest = personalSignDigest(message); + const sig = secp256k1.sign(digest, this.privKey, { lowS: true }); + const r = sig.r.toString(16).padStart(64, "0"); + const s = sig.s.toString(16).padStart(64, "0"); + const v = (27 + (sig.recovery ?? 0)).toString(16).padStart(2, "0"); + return ("0x" + r + s + v) as Hex; + } +} + +/** + * Adapter for `window.ethereum` (EIP-1193 provider). Only available in + * browsers; this file is import-safe in Node because the class is lazy. + */ +export class BrowserMmSigner implements MmSigner { + private cachedAddress: Hex | null = null; + private readonly provider: { + request: (args: { method: string; params?: unknown[] }) => Promise; + }; + + constructor(provider: unknown) { + if ( + provider == null || + typeof (provider as { request?: unknown }).request !== "function" + ) { + throw new Error("provider does not look like an EIP-1193 provider"); + } + this.provider = provider as { + request: ( + args: { method: string; params?: unknown[] }, + ) => Promise; + }; + } + + async address(): Promise { + if (this.cachedAddress) return this.cachedAddress; + const accounts = (await this.provider.request({ + method: "eth_requestAccounts", + })) as Hex[]; + if (!Array.isArray(accounts) || accounts.length === 0) { + throw new Error("eth_requestAccounts returned no accounts"); + } + this.cachedAddress = accounts[0]; + return accounts[0]; + } + + async personalSign(message: string): Promise { + const addr = await this.address(); + const hexMsg = ("0x" + bytesToHex(utf8ToBytes(message))) as Hex; + const sig = (await this.provider.request({ + method: "personal_sign", + params: [hexMsg, addr], + })) as Hex; + if (typeof sig !== "string" || !sig.startsWith("0x") || sig.length !== 132) { + throw new Error(`personal_sign returned malformed signature: ${sig}`); + } + return sig; + } +} diff --git a/tier2_wallet/src/paymaster.ts b/tier2_wallet/src/paymaster.ts new file mode 100644 index 0000000..a2bf4f0 --- /dev/null +++ b/tier2_wallet/src/paymaster.ts @@ -0,0 +1,175 @@ +/** + * AVNU paymaster JSON-RPC client (sponsored_private flow only). + * + * Vendored from `paymaster/examples/private-sponsored-web/src/paymaster.ts` + * with light edits — the original is published under AGPLv3. + * + * Two RPCs we use: + * - `paymaster_buildTransaction` → fee quote + forwarder address + * - `paymaster_executeTransaction` → submit the proof + apply_actions call + * + * `sponsored_private` mode means the user pays the paymaster's fee from + * inside their own pool notes (the SDK bundles a `withdraw` action targeting + * the forwarder into the same proof). Nothing leaves the user's funded + * Starknet wallet — Alice's identity stays inside the pool. + */ + +import { hash, type Call } from "starknet"; + +export type PaymasterCall = { + to: string; + selector: string; + calldata: string[]; +}; + +export type ApplyActionBuildRequest = { + transaction: { + type: "apply_action"; + apply_action: { pool_address: string }; + }; + parameters: { + version: "0x1"; + fee_mode: { + mode: "sponsored_private"; + pool_fee_token: string; + tip?: "slow" | "normal" | "fast"; + }; + time_bounds?: { execute_after: number; execute_before: number }; + }; +}; + +export type FeeEstimate = { + gas_token_price_in_strk: string; + estimated_fee_in_strk: string; + estimated_fee_in_gas_token: string; + suggested_max_fee_in_strk: string; + suggested_max_fee_in_gas_token: string; +}; + +export type FeeAction = { + type: "withdraw"; + recipient: string; + token: string; + amount: string; +}; + +export type ApplyActionBuildResponse = { + type: "apply_action"; + parameters: ApplyActionBuildRequest["parameters"]; + fee: FeeEstimate; + fee_action: FeeAction; +}; + +export type ApplyActionExecuteRequest = { + transaction: { + type: "apply_action"; + apply_action: { + apply_actions_call: PaymasterCall; + proof: string; + proof_facts: string[]; + }; + }; + parameters: ApplyActionBuildRequest["parameters"]; +}; + +export type ExecuteResponse = { + transaction_hash: string; + tracking_id: string; +}; + +export class PaymasterClient { + constructor( + private readonly url: string, + private readonly apiKey: string = "", + ) {} + + async buildApplyAction( + poolAddress: string, + poolFeeToken: string, + ): Promise { + const params: ApplyActionBuildRequest = { + transaction: { type: "apply_action", apply_action: { pool_address: poolAddress } }, + parameters: { + version: "0x1", + fee_mode: { mode: "sponsored_private", pool_fee_token: poolFeeToken, tip: "normal" }, + }, + }; + return this.rpc("paymaster_buildTransaction", params); + } + + async executeApplyAction( + applyActionsCall: Call | PaymasterCall, + proof: string, + proofFacts: string[], + parameters: ApplyActionBuildRequest["parameters"], + ): Promise { + const params: ApplyActionExecuteRequest = { + transaction: { + type: "apply_action", + apply_action: { + apply_actions_call: normalizeCall(applyActionsCall), + proof, + proof_facts: proofFacts, + }, + }, + parameters, + }; + return this.rpc("paymaster_executeTransaction", params); + } + + private async rpc(method: string, params: unknown): Promise { + const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }); + const headers: Record = { "content-type": "application/json" }; + if (this.apiKey) headers["x-paymaster-api-key"] = this.apiKey; + const response = await fetch(this.url, { method: "POST", headers, body }); + if (!response.ok) { + const text = await response.text(); + throw new Error(`Paymaster HTTP ${response.status}: ${text}`); + } + const json = (await response.json()) as + | { result: T } + | { error: { code: number; message: string; data?: unknown } }; + if ("error" in json) { + throw new Error( + `Paymaster ${method} error (${json.error.code}): ${json.error.message}${ + json.error.data ? ` :: ${JSON.stringify(json.error.data)}` : "" + }`, + ); + } + return json.result; + } +} + +/** + * The Rust paymaster server deserializes Felts with serde_with `UfeHex`, which + * requires `0x`-prefixed hex strings. starknet.js's BigNumberish often comes + * through as a decimal string; normalize everything to hex. + */ +export function toFelt0x(value: unknown): string { + if (typeof value === "bigint") return "0x" + value.toString(16); + if (typeof value === "number") return "0x" + value.toString(16); + if (typeof value === "string") { + if (value.startsWith("0x") || value.startsWith("0X")) return value; + if (/^[0-9]+$/.test(value)) return "0x" + BigInt(value).toString(16); + return "0x" + BigInt(value).toString(16); + } + throw new Error(`Cannot convert ${typeof value} to felt: ${String(value)}`); +} + +/** + * starknet.js exposes Call as `{ contractAddress, entrypoint, calldata }` + * but the paymaster RPC uses `{ to, selector, calldata }`. The Privacy SDK + * returns the latter shape, but we accept both defensively. + */ +function normalizeCall(call: Call | PaymasterCall): PaymasterCall { + const anyCall = call as Record; + const to = (anyCall.to ?? anyCall.contractAddress ?? anyCall.contract_address) as string; + const selectorRaw = (anyCall.selector ?? anyCall.entry_point_selector ?? anyCall.entrypoint) as string; + const selectorIsHex = + /^0x[0-9a-fA-F]+$/.test(selectorRaw) || /^[0-9]+$/.test(selectorRaw); + const selector = selectorIsHex + ? toFelt0x(selectorRaw) + : hash.getSelectorFromName(selectorRaw); + const calldata = ((anyCall.calldata ?? []) as unknown[]).map((x) => toFelt0x(x)); + return { to: toFelt0x(to), selector, calldata }; +} diff --git a/tier2_wallet/src/sign.ts b/tier2_wallet/src/sign.ts new file mode 100644 index 0000000..12660f1 --- /dev/null +++ b/tier2_wallet/src/sign.ts @@ -0,0 +1,183 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { keccak_256 } from "@noble/hashes/sha3"; +import { hexToBytes } from "@noble/hashes/utils"; + +import { + CALL_TYPE_HASH, + EIP712_DOMAIN_TYPE_HASH, + MASK_128, + OUTSIDE_EXECUTION_TYPE_HASH, + OWNERSHIP_TRANSFER_MSG_HASH, + VERSION_HASH, +} from "./constants.js"; +import type { + CairoSignature, + Call, + OutsideExecution, + OutsideExecutionDomain, + OutsideExecutionSignature, + SessionKey, +} from "./types.js"; +import { + bigintToBytes32, + bytes32ToBigInt, + concat, + feltToByteArray, +} from "./util.js"; + +// ─── Ownership signature ──────────────────────────────────────────────────── + +/** + * Produce the signature `factory.deploy_account` expects. + * + * The Cairo side validates with: + * `assert_valid_owner(eth_address: owner_eth_address, signature)` + * → `recover_eth_address(OWNERSHIP_TRANSFER_MSG_HASH, sig) == owner_eth_address` + * + * So we sign `OWNERSHIP_TRANSFER_MSG_HASH` (a fixed pre-hashed message) with + * the session private key and serialize as `{r, s, y_parity}`. + * + * Note the Cairo `y_parity` convention: `y_parity = (v % 2 == 0)`. With the + * EVM `recovery` byte ∈ {0, 1}, this translates to `recovery == 1`. + */ +export function signOwnership(session: SessionKey): CairoSignature { + const priv = hexToBytes(session.privKey.slice(2)); + const msgHash = bigintToBytes32(OWNERSHIP_TRANSFER_MSG_HASH); + const sig = secp256k1.sign(msgHash, priv, { lowS: true }); + const recovery = sig.recovery ?? 0; + // Cairo: y_parity = (v % 2 == 0). With v = 27 + recovery → y_parity true iff + // recovery == 1 (v == 28). Encode with that meaning. + return { + r: sig.r, + s: sig.s, + yParity: recovery === 1, + }; +} + +/** Encode a {@link CairoSignature} as the calldata felts the dispatcher expects. */ +export function encodeOwnershipSignature(sig: CairoSignature): bigint[] { + return [ + sig.r & MASK_128, + sig.r >> 128n, + sig.s & MASK_128, + sig.s >> 128n, + sig.yParity ? 1n : 0n, + ]; +} + +// ─── EIP-712 OutsideExecution signature ───────────────────────────────────── + +/** Hash a single Call per `push_call` in eth_712_utils.cairo. */ +export function hashCall(call: Call): Uint8Array { + const calldataConcat = concat( + ...call.calldata.map((felt) => bigintToBytes32(felt)), + ); + const calldataHash = keccak_256(calldataConcat); + return keccak_256( + concat( + bigintToBytes32(CALL_TYPE_HASH), + bigintToBytes32(call.to), + bigintToBytes32(call.selector), + calldataHash, + ), + ); +} + +/** Hash an array of Calls per `push_call_array`. */ +export function hashCallArray(calls: Call[]): Uint8Array { + return keccak_256(concat(...calls.map(hashCall))); +} + +/** Hash the OutsideExecution struct per `push_outside_execution`. */ +export function hashOutsideExecution(ose: OutsideExecution): Uint8Array { + const callsHash = hashCallArray(ose.calls); + return keccak_256( + concat( + bigintToBytes32(OUTSIDE_EXECUTION_TYPE_HASH), + callsHash, + bigintToBytes32(ose.caller), + bigintToBytes32(ose.nonce), + bigintToBytes32(ose.executeAfter), + bigintToBytes32(ose.executeBefore), + ), + ); +} + +/** + * Compute the EIP-712 domain separator per `push_domain_separator`. + * `name` uses the keccak of the Starknet chain id encoded as its byte + * representation (matching Cairo's `sn_chain_id_keccak`). + */ +export function computeDomainSeparator(domain: OutsideExecutionDomain): Uint8Array { + const nameHash = keccak_256(feltToByteArray(domain.starknetChainId)); + const verifyingContract = domain.accountAddress & MASK_128; // low 128 bits only + return keccak_256( + concat( + bigintToBytes32(EIP712_DOMAIN_TYPE_HASH), + nameHash, + bigintToBytes32(VERSION_HASH), + bigintToBytes32(domain.evmChainId), + bigintToBytes32(verifyingContract), + ), + ); +} + +/** + * Compute the final EIP-712 message hash that `is_valid_signature` recovers + * against. Format: `keccak(0x19 || 0x01 || domain_separator || struct_hash)`. + */ +export function computeOutsideExecutionHash( + ose: OutsideExecution, + domain: OutsideExecutionDomain, +): Uint8Array { + return keccak_256( + concat( + new Uint8Array([0x19, 0x01]), + computeDomainSeparator(domain), + hashOutsideExecution(ose), + ), + ); +} + +/** + * Sign an OutsideExecution with the session key and produce the 6-felt span + * `execute_from_outside_v2` expects. + * + * Span layout (matches `extract_signature` in eth_712_utils.cairo): + * [r_high, r_low, s_high, s_low, v, evm_chain_id] + * + * `v` is encoded as `27 + recovery` so that, after the Cairo `(v % 2 == 0)` + * check, the resulting `y_parity` matches what secp256k1 recovery actually + * produced. + */ +export function signOutsideExecution(opts: { + session: SessionKey; + outsideExecution: OutsideExecution; + domain: OutsideExecutionDomain; +}): OutsideExecutionSignature { + const { session, outsideExecution, domain } = opts; + const digest = computeOutsideExecutionHash(outsideExecution, domain); + const priv = hexToBytes(session.privKey.slice(2)); + const sig = secp256k1.sign(digest, priv, { lowS: true }); + const recovery = sig.recovery ?? 0; + const v = BigInt(27 + recovery); + return [ + sig.r >> 128n, // r_high + sig.r & MASK_128, // r_low + sig.s >> 128n, // s_high + sig.s & MASK_128, // s_low + v, // v + domain.evmChainId, // chain_id (EVM) + ]; +} + +/** + * Convenience: return the EIP-712 digest as a bigint (matches what the Cairo + * `get_outside_execution_hash` returns). + */ +export function outsideExecutionHashAsBigInt( + ose: OutsideExecution, + domain: OutsideExecutionDomain, +): bigint { + return bytes32ToBigInt(computeOutsideExecutionHash(ose, domain)); +} diff --git a/tier2_wallet/src/sponsored-deploy.ts b/tier2_wallet/src/sponsored-deploy.ts new file mode 100644 index 0000000..660fe7d --- /dev/null +++ b/tier2_wallet/src/sponsored-deploy.ts @@ -0,0 +1,237 @@ +/** + * Tier 2 unlinkable account deploy via Alice + the AVNU sponsored_private flow. + * + * What this orchestrates (see `paymaster/examples/private-sponsored-web/src/flow.ts` + * for the canonical recipe — we adapt the "private deploy a Counter" pattern + * to "private deploy a Tier 2 account through EarnDeployHelper"): + * + * 1. Ask the AVNU paymaster for a fee quote (mode = sponsored_private). + * Server returns a fee_action: withdraw of + * to . + * 2. Use the Privacy SDK builder to construct apply_actions containing: + * a. The fee withdraw to the AVNU forwarder (pays gas). + * b. An InvokeExternal targeting EarnDeployHelper.privacy_invoke with + * the user's session_eth_address + ownership signature + a placeholder + * note_id. The helper forwards to factory.deploy_account. + * Generate a ZK proof covering both actions in a single atomic tx. + * 3. Submit (apply_actions_call + proof + proof_facts) to + * `paymaster_executeTransaction`. AVNU's relayer signs and submits the + * tx — Alice's address never appears as msg.sender. The new Tier 2 + * account is deployed at the deterministic address derived from the + * session_eth_address; the MetaMask user's identity is never on chain. + * + * The "Alice" account in this module is the *pool sponsor* — she has notes + * inside the privacy pool, she generates and signs the proof, and the + * paymaster's `withdraw` to its forwarder is debited against her notes. She + * does NOT touch the MM user's session key, and the MM user does NOT need + * any Starknet balance to be deployed. + */ + +import { hash, type Account, type RpcProvider } from "starknet"; +import type { + PrivateTransfersInterface, +} from "@starkware-libs/starknet-privacy-sdk"; + +import { computeAccountAddress, type Network } from "./address.js"; +import { signOwnership } from "./sign.js"; +import { PaymasterClient, toFelt0x } from "./paymaster.js"; +import type { CairoSignature, Hex, SessionKey } from "./types.js"; + +/** Per-tx tuning. Mirrors the AVNU PoC defaults. */ +const NOTE_MATURITY_BLOCKS = 10; + +export interface SponsoredDeployConfig { + /** Sepolia chain id ("0x534e5f5345504f4c4941"). */ + starknetChainId: bigint | Hex | string; + /** Privacy pool contract address. */ + poolAddress: Hex | string; + /** The token Alice's notes hold AND the token paymaster fees are paid in. */ + poolFeeToken: Hex | string; + /** Deployed EarnDeployHelper address (calls factory.deploy_account). */ + helperAddress: Hex | string; + /** Deployed AccountFactory address. Used only to predict the resulting + * Starknet account address client-side. */ + factoryAddress: Hex | string; + /** "prod" → PRIMER_CLASS_HASH.prod, "test" → PRIMER_CLASS_HASH.test. */ + network?: Network; + /** Optional paymaster API key (AVNU managed instance). */ + paymasterApiKey?: string; + /** Optional custom paymaster URL (defaults to AVNU sepolia). */ + paymasterUrl?: string; +} + +export interface SponsoredDeployArgs { + /** Connected starknet.js account for Alice (the pool sponsor). */ + alice: Account; + /** RpcProvider pointing at the chain Alice's account uses. */ + provider: RpcProvider; + /** Initialized SDK builder (call createPrivateTransfers({...}) externally). */ + transfers: PrivateTransfersInterface; + /** AVNU paymaster client. */ + paymaster: PaymasterClient; + /** The Tier 2 user's session keypair (from deriveSessionKey). */ + session: SessionKey; + /** Override: provide a pre-built ownership signature. Defaults to signing + * with `session` here. */ + ownershipSignature?: CairoSignature; + /** Optional one-line progress logger (browser demo uses this). */ + log?: (line: string) => void; +} + +export interface SponsoredDeployResult { + /** The Starknet account address that was just deployed. */ + accountAddress: bigint; + /** Hex form of the same address (0x-prefixed, padded). */ + accountAddressHex: string; + /** Paymaster's transaction hash for the deploy. */ + transactionHash: string; + /** Class hash now sitting at the deployed address. */ + classHashAtAddress: string; + /** Privacy-pool fee that Alice paid. */ + feePaidByAlice: bigint; +} + +/** + * Encode `EarnDeployHelper.privacy_invoke` calldata. + * + * Cairo signature: + * fn privacy_invoke( + * session_eth_address: EthAddress, + * signature: Signature, -- u256 r, u256 s, bool y_parity + * note_id: felt252, + * ) + * + * Cairo Serde for u256 is `[low_felt, high_felt]`. Signature serializes as + * `[r.low, r.high, s.low, s.high, y_parity ? 1 : 0]` → 5 felts. So the full + * calldata is 7 felts. + */ +export function encodePrivacyInvokeCalldata(args: { + sessionEthAddress: bigint | Hex | string; + signature: CairoSignature; + noteId?: bigint; +}): string[] { + const session = + typeof args.sessionEthAddress === "bigint" + ? args.sessionEthAddress + : BigInt(args.sessionEthAddress); + const { r, s, yParity } = args.signature; + const noteId = args.noteId ?? 0n; + const MASK_128 = (1n << 128n) - 1n; + return [ + "0x" + session.toString(16), + "0x" + (r & MASK_128).toString(16), + "0x" + (r >> 128n).toString(16), + "0x" + (s & MASK_128).toString(16), + "0x" + (s >> 128n).toString(16), + yParity ? "0x1" : "0x0", + "0x" + noteId.toString(16), + ]; +} + +/** + * Run the full private sponsored deploy for one MetaMask-derived session key. + */ +export async function privateSponsoredDeployTier2Account( + cfg: SponsoredDeployConfig, + args: SponsoredDeployArgs, +): Promise { + const log = args.log ?? (() => {}); + + // Predict the Starknet account address before we touch the chain so we can + // verify the deploy landed where it should. + const accountAddress = computeAccountAddress({ + sessionEthAddress: args.session.ethAddress, + factoryAddress: cfg.factoryAddress as Hex, + network: cfg.network ?? "prod", + }); + const accountAddressHex = + "0x" + accountAddress.toString(16).padStart(64, "0"); + log(`expected Tier 2 account address: ${accountAddressHex}`); + + // 1. Fee quote. + log("requesting paymaster fee quote (sponsored_private)…"); + const build = await args.paymaster.buildApplyAction( + String(cfg.poolAddress), + String(cfg.poolFeeToken), + ); + const feeAmount = BigInt(build.fee_action.amount); + const forwarder = build.fee_action.recipient; + log(` fee=${feeAmount} of ${build.fee_action.token}, forwarder=${forwarder}`); + + // 2. Build apply_actions: fee withdraw + invoke EarnDeployHelper.privacy_invoke. + const ownership = + args.ownershipSignature ?? signOwnership(args.session); + const invokeCalldata = encodePrivacyInvokeCalldata({ + sessionEthAddress: args.session.ethAddress, + signature: ownership, + noteId: 0n, + }); + log( + `invoke calldata: helper=${cfg.helperAddress} session=${args.session.ethAddress}`, + ); + + const provingBlockId = + (await args.provider.getBlockNumber()) - NOTE_MATURITY_BLOCKS; + log(`generating proof against block ${provingBlockId}…`); + + const aliceAddress = (args.alice as unknown as { address: string }).address; + const { callAndProof } = await args.transfers + .build({ + autoSetup: true, + autoDiscover: { notes: "refresh", channels: "refresh" }, + autoSelectNotes: "all", + }) + .surplusTo(aliceAddress) + .with(String(cfg.poolFeeToken), (t) => { + t.withdraw({ recipient: forwarder, amount: feeAmount }); + }) + .invoke(() => ({ + contractAddress: String(cfg.helperAddress), + entrypoint: "privacy_invoke", + calldata: invokeCalldata, + })) + .execute({ provingBlockId }); + log( + `proof generated (${callAndProof.proof.proofFacts?.length ?? 0} facts)`, + ); + + // 3. Submit via paymaster. + log("paymaster_executeTransaction…"); + const result = await args.paymaster.executeApplyAction( + callAndProof.call, + callAndProof.proof.data ?? "", + (callAndProof.proof.proofFacts ?? []).map((x) => toFelt0x(x)), + build.parameters, + ); + log(` submitted: ${result.transaction_hash}`); + await args.provider.waitForTransaction(result.transaction_hash); + log(` confirmed`); + + // 4. Verify the deploy actually landed. + let classHashAtAddress = "0x0"; + try { + classHashAtAddress = await args.provider.getClassHashAt(accountAddressHex); + } catch (e) { + log( + `WARN: getClassHashAt(${accountAddressHex}) failed: ${ + (e as Error).message + }`, + ); + } + log(`account class at deployed address: ${classHashAtAddress}`); + + return { + accountAddress, + accountAddressHex, + transactionHash: result.transaction_hash, + classHashAtAddress, + feePaidByAlice: feeAmount, + }; +} + +/** + * Cheap selector check — handy for sanity logging. + */ +export function privacyInvokeSelector(): string { + return hash.getSelectorFromName("privacy_invoke"); +} diff --git a/tier2_wallet/src/sponsored-execute.ts b/tier2_wallet/src/sponsored-execute.ts new file mode 100644 index 0000000..dc680ff --- /dev/null +++ b/tier2_wallet/src/sponsored-execute.ts @@ -0,0 +1,216 @@ +/** + * Tier 2 Counter call via Alice + the AVNU sponsored_private flow. + * + * Sibling of sponsored-deploy.ts. Where the deploy invokes EarnDeployHelper + * (which calls factory.deploy_account), this flow invokes EarnInvokeHelper + * (which calls target_account.execute_from_outside_v2). The signature + * verified by the Tier 2 account is the session key's EIP-712 signature over + * the OutsideExecution — not anything MetaMask produced directly. + * + * relayer tx (signed by AVNU, NOT the user) + * └─ forwarder.execute_private_sponsored + * └─ pool.apply_actions + * ├─ withdraw(fee_token, recipient=forwarder) (Alice's gas) + * └─ InvokeExternal → EarnInvokeHelper.privacy_invoke( + * target_account=tier2_account_addr, + * outside_execution=, + * signature=<6 felts signed by session_key>, + * note_id=0, + * ) + * └─ tier2_account.execute_from_outside_v2(...) + * └─ counter.increment() + */ + +import type { Account, RpcProvider } from "starknet"; +import { hash } from "starknet"; +import type { PrivateTransfersInterface } from "@starkware-libs/starknet-privacy-sdk"; + +import { ANY_CALLER } from "./constants.js"; +import { signOutsideExecution } from "./sign.js"; +import { PaymasterClient, toFelt0x } from "./paymaster.js"; +import type { + Call, + Hex, + OutsideExecution, + OutsideExecutionDomain, + SessionKey, +} from "./types.js"; + +export interface SponsoredExecuteConfig { + starknetChainId: bigint | Hex | string; + poolAddress: Hex | string; + poolFeeToken: Hex | string; + /** Deployed EarnInvokeHelper address. */ + invokeHelperAddress: Hex | string; + paymasterApiKey?: string; + paymasterUrl?: string; +} + +export interface SponsoredExecuteArgs { + alice: Account; + provider: RpcProvider; + transfers: PrivateTransfersInterface; + paymaster: PaymasterClient; + /** The Tier 2 user's session keypair. */ + session: SessionKey; + /** The deployed Tier 2 account address (returned from sponsored-deploy). */ + accountAddress: Hex | string; + /** Inner calls the account should execute (e.g. one Counter.increment). */ + calls: Call[]; + /** Unique-per-account nonce. Tip: use Date.now() converted to bigint. */ + nonce: bigint; + /** Optional sliding window for the OutsideExecution. Defaults to ~10 minutes. */ + executeAfter?: bigint; + executeBefore?: bigint; + log?: (line: string) => void; +} + +export interface SponsoredExecuteResult { + transactionHash: string; + feePaidByAlice: bigint; + outsideExecution: OutsideExecution; +} + +const NOTE_MATURITY_BLOCKS = 10; + +/** + * Run one Tier 2 outside-execution sponsored privately by Alice. + */ +export async function privateSponsoredExecuteOnTier2Account( + cfg: SponsoredExecuteConfig, + args: SponsoredExecuteArgs, +): Promise { + const log = args.log ?? (() => {}); + + // 1. Build the OutsideExecution payload + session-key signature. + const nowSec = BigInt(Math.floor(Date.now() / 1000)); + const ose: OutsideExecution = { + caller: ANY_CALLER, + nonce: args.nonce, + executeAfter: args.executeAfter ?? nowSec - 60n, + executeBefore: args.executeBefore ?? nowSec + 600n, + calls: args.calls, + }; + const domain: OutsideExecutionDomain = { + accountAddress: BigInt(args.accountAddress as Hex), + evmChainId: 1n, // The EIP-712 domain's `chainId` field. Cairo doesn't care; we keep "1" for parity with the Python reference. + starknetChainId: + typeof cfg.starknetChainId === "bigint" + ? cfg.starknetChainId + : BigInt(cfg.starknetChainId), + }; + const sig = signOutsideExecution({ + session: args.session, + outsideExecution: ose, + domain, + }); + log(`signed OutsideExecution nonce=${args.nonce} calls=${args.calls.length}`); + + // 2. Quote the paymaster fee. + log("requesting paymaster fee quote (sponsored_private)…"); + const build = await args.paymaster.buildApplyAction( + String(cfg.poolAddress), + String(cfg.poolFeeToken), + ); + const feeAmount = BigInt(build.fee_action.amount); + const forwarder = build.fee_action.recipient; + log(` fee=${feeAmount} of ${build.fee_action.token}, forwarder=${forwarder}`); + + // 3. Build the EarnInvokeHelper.privacy_invoke calldata. Cairo signature: + // privacy_invoke( + // target_account: ContractAddress, + // outside_execution: OutsideExecution, + // signature: Array, + // note_id: felt252, + // ) + // Order of serialized felts: + // target_account + // caller, nonce, execute_after, execute_before + // calls.len(), [for each call: to, selector, calldata.len(), ...calldata] + // signature.len(), ...signature + // note_id + const calldata: string[] = []; + calldata.push("0x" + BigInt(args.accountAddress as Hex).toString(16)); + calldata.push("0x" + ose.caller.toString(16)); + calldata.push("0x" + ose.nonce.toString(16)); + calldata.push("0x" + ose.executeAfter.toString(16)); + calldata.push("0x" + ose.executeBefore.toString(16)); + calldata.push("0x" + BigInt(ose.calls.length).toString(16)); + for (const c of ose.calls) { + calldata.push("0x" + c.to.toString(16)); + calldata.push("0x" + c.selector.toString(16)); + calldata.push("0x" + BigInt(c.calldata.length).toString(16)); + for (const d of c.calldata) calldata.push("0x" + d.toString(16)); + } + calldata.push("0x" + BigInt(sig.length).toString(16)); + for (const f of sig) calldata.push("0x" + f.toString(16)); + calldata.push("0x0"); // note_id + + log(`invoke helper=${cfg.invokeHelperAddress} target=${args.accountAddress}`); + + // 4. Generate proof. + const provingBlockId = + (await args.provider.getBlockNumber()) - NOTE_MATURITY_BLOCKS; + log(`generating proof against block ${provingBlockId}…`); + + const aliceAddress = (args.alice as unknown as { address: string }).address; + const { callAndProof } = await args.transfers + .build({ + autoSetup: true, + autoDiscover: { notes: "refresh", channels: "refresh" }, + autoSelectNotes: "all", + }) + .surplusTo(aliceAddress) + .with(String(cfg.poolFeeToken), (t) => { + t.withdraw({ recipient: forwarder, amount: feeAmount }); + }) + .invoke(() => ({ + contractAddress: String(cfg.invokeHelperAddress), + entrypoint: "privacy_invoke", + calldata, + })) + .execute({ provingBlockId }); + log( + `proof generated (${callAndProof.proof.proofFacts?.length ?? 0} facts)`, + ); + + // 5. Submit via paymaster. + log("paymaster_executeTransaction…"); + const result = await args.paymaster.executeApplyAction( + callAndProof.call, + callAndProof.proof.data ?? "", + (callAndProof.proof.proofFacts ?? []).map((x) => toFelt0x(x)), + build.parameters, + ); + log(` submitted: ${result.transaction_hash}`); + await args.provider.waitForTransaction(result.transaction_hash); + log(" confirmed"); + + return { + transactionHash: result.transaction_hash, + feePaidByAlice: feeAmount, + outsideExecution: ose, + }; +} + +/** Helper: build a Call for Counter.increment(). */ +export function counterIncrementCall(counterAddress: Hex | string): Call { + return { + to: BigInt(counterAddress as Hex), + selector: BigInt(hash.getSelectorFromName("increment")), + calldata: [], + }; +} + +/** Helper: read Counter.get_count() — view-only, no tx. */ +export async function readCounterCount( + provider: RpcProvider, + counterAddress: Hex | string, +): Promise { + const out = await provider.callContract({ + contractAddress: String(counterAddress), + entrypoint: "get_count", + calldata: [], + }); + return BigInt(out[0]); +} diff --git a/tier2_wallet/src/types.ts b/tier2_wallet/src/types.ts new file mode 100644 index 0000000..e38c004 --- /dev/null +++ b/tier2_wallet/src/types.ts @@ -0,0 +1,71 @@ +/** + * Shared types for the Tier 2 wallet primitives. + */ + +/** Hex string with `0x` prefix. */ +export type Hex = `0x${string}`; + +/** A secp256k1 session keypair derived from the MM bootstrap signature. */ +export interface SessionKey { + /** 32-byte private scalar as a hex string (0x-prefixed). */ + privKey: Hex; + /** + * 64-byte uncompressed public key (X || Y, no 0x04 prefix), hex-encoded. + * Useful for ecrecover-style verification. + */ + pubKeyUncompressed: Hex; + /** 20-byte Ethereum-format address derived from the public key. */ + ethAddress: Hex; +} + +/** Cairo `Signature { r: u256, s: u256, y_parity: bool }`. */ +export interface CairoSignature { + r: bigint; + s: bigint; + /** Cairo convention: `y_parity = (v % 2 == 0)`, i.e. EVM v=28 → true. */ + yParity: boolean; +} + +/** A single Starknet call inside an OutsideExecution. */ +export interface Call { + to: bigint; + selector: bigint; + calldata: bigint[]; +} + +/** OutsideExecution payload as defined by ISRC9_V2. */ +export interface OutsideExecution { + caller: bigint; + nonce: bigint; + executeAfter: bigint; + executeBefore: bigint; + calls: Call[]; +} + +/** Domain parameters required to compute an OutsideExecution EIP-712 hash. */ +export interface OutsideExecutionDomain { + /** Starknet account contract address (used as `verifyingContract`). */ + accountAddress: bigint; + /** EVM source chain ID encoded in the domain separator. */ + evmChainId: bigint; + /** + * Starknet chain identifier as a short-string-encoded felt + * (e.g. for "SN_SEPOLIA": 0x534e5f5345504f4c4941). The Cairo side + * keccaks the byte form of this felt to compute the domain `name`. + */ + starknetChainId: bigint; +} + +/** + * The 6-felt span ISRC9_V2 expects as the `signature` argument to + * `execute_from_outside_v2`. Layout per `eth_712_utils.cairo:136-147`: + * [r_high, r_low, s_high, s_low, v, evm_chain_id] + */ +export type OutsideExecutionSignature = [ + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, +]; diff --git a/tier2_wallet/src/util.ts b/tier2_wallet/src/util.ts new file mode 100644 index 0000000..d086fab --- /dev/null +++ b/tier2_wallet/src/util.ts @@ -0,0 +1,81 @@ +import { keccak_256 } from "@noble/hashes/sha3"; +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; + +import type { Hex } from "./types.js"; + +/** + * Derive the Ethereum-format address from a secp256k1 public key. + * Accepts either uncompressed (65-byte with 0x04 prefix) or 64-byte raw + * (X || Y) public keys. + */ +export function addressFromPubKey(pubKey: Uint8Array): Hex { + const raw = pubKey.length === 65 ? pubKey.subarray(1) : pubKey; + if (raw.length !== 64) { + throw new Error(`unexpected pubkey length: ${pubKey.length}`); + } + const hash = keccak_256(raw); + return ("0x" + bytesToHex(hash.subarray(12))) as Hex; +} + +/** Convert a 0x-prefixed hex string to bigint. */ +export function hexToBigInt(hex: string): bigint { + return BigInt(hex.startsWith("0x") ? hex : "0x" + hex); +} + +/** Pack a bigint as 32 big-endian bytes. */ +export function bigintToBytes32(val: bigint): Uint8Array { + if (val < 0n || val >= 1n << 256n) { + throw new RangeError(`bigintToBytes32: value out of u256 range: ${val}`); + } + const out = new Uint8Array(32); + let v = val; + for (let i = 31; i >= 0; i--) { + out[i] = Number(v & 0xffn); + v >>= 8n; + } + return out; +} + +/** Concatenate Uint8Arrays. */ +export function concat(...parts: Uint8Array[]): Uint8Array { + let total = 0; + for (const p of parts) total += p.length; + const out = new Uint8Array(total); + let off = 0; + for (const p of parts) { + out.set(p, off); + off += p.length; + } + return out; +} + +/** + * Cairo `felt_to_byte_array`: returns the felt as its big-endian byte + * representation with leading zero bytes stripped. Matches the helper + * used by `sn_chain_id_keccak`. + */ +export function feltToByteArray(felt: bigint): Uint8Array { + if (felt < 0n) throw new RangeError("felt must be non-negative"); + if (felt === 0n) return new Uint8Array(0); + const hex = felt.toString(16); + const padded = hex.length % 2 === 0 ? hex : "0" + hex; + return hexToBytes(padded); +} + +/** Compute keccak256 over the byte representation of a felt (used for chain id). */ +export function keccakFelt(felt: bigint): Uint8Array { + return keccak_256(feltToByteArray(felt)); +} + +/** Compute keccak256 over a UTF-8 string. */ +export function keccakString(s: string): Uint8Array { + return keccak_256(new TextEncoder().encode(s)); +} + +/** Read 32 bytes big-endian as bigint. */ +export function bytes32ToBigInt(bytes: Uint8Array): bigint { + if (bytes.length !== 32) { + throw new Error(`expected 32 bytes, got ${bytes.length}`); + } + return BigInt("0x" + bytesToHex(bytes)); +} diff --git a/tier2_wallet/tests/address.test.ts b/tier2_wallet/tests/address.test.ts new file mode 100644 index 0000000..cea29c7 --- /dev/null +++ b/tier2_wallet/tests/address.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { hash } from "starknet"; + +import { + PRIMER_CLASS_HASH, + computeAccountAddress, + sessionKeyFromPriv, +} from "../src/index.js"; + +const TEST_PRIV = + "0xa6d86467b6ec9e161649b27edfd8519e75a2e1cf5f4c309c628706e6999780e8" as const; +// Stark-curve addresses must be < 2^251 + ε. Picked something inside that range. +const FACTORY_ADDR = + 0x01a2b3c4d5e6f7890123456789abcdef0123456789abcdef0123456789abcdefn; + +describe("computeAccountAddress", () => { + it("matches starknet.js calculateContractAddressFromHash", () => { + const session = sessionKeyFromPriv(TEST_PRIV); + const ours = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: FACTORY_ADDR, + network: "prod", + }); + const ref = BigInt( + hash.calculateContractAddressFromHash( + session.ethAddress, + "0x" + PRIMER_CLASS_HASH.prod.toString(16), + [], + "0x" + FACTORY_ADDR.toString(16), + ), + ); + expect(ours).toEqual(ref); + }); + + it("is deterministic for the same inputs", () => { + const session = sessionKeyFromPriv(TEST_PRIV); + const a = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: FACTORY_ADDR, + }); + const b = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: FACTORY_ADDR, + }); + expect(a).toEqual(b); + }); + + it("differs across networks (test vs prod primer)", () => { + const session = sessionKeyFromPriv(TEST_PRIV); + const prod = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: FACTORY_ADDR, + network: "prod", + }); + const test = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: FACTORY_ADDR, + network: "test", + }); + expect(prod).not.toEqual(test); + }); + + it("differs across different session addresses", () => { + const a = computeAccountAddress({ + sessionEthAddress: 0x1111111111111111111111111111111111111111n, + factoryAddress: FACTORY_ADDR, + }); + const b = computeAccountAddress({ + sessionEthAddress: 0x2222222222222222222222222222222222222222n, + factoryAddress: FACTORY_ADDR, + }); + expect(a).not.toEqual(b); + }); +}); diff --git a/tier2_wallet/tests/derive.test.ts b/tier2_wallet/tests/derive.test.ts new file mode 100644 index 0000000..6aa2fe1 --- /dev/null +++ b/tier2_wallet/tests/derive.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import { + BOOTSTRAP_MESSAGE, + FixedKeyMmSigner, + deriveSessionKey, + deriveSessionKeyFromSignature, + sessionKeyFromPriv, +} from "../src/index.js"; + +describe("deriveSessionKey", () => { + it("is deterministic for the same MM key + message", async () => { + const signer = new FixedKeyMmSigner(); + const a = await deriveSessionKey(signer); + const b = await deriveSessionKey(signer); + expect(a).toEqual(b); + }); + + it("never equals the underlying MM address", async () => { + const signer = new FixedKeyMmSigner(); + const mmAddr = (await signer.address()).toLowerCase(); + const session = await deriveSessionKey(signer); + expect(session.ethAddress.toLowerCase()).not.toEqual(mmAddr); + }); + + it("changes when the bootstrap message changes", async () => { + const signer = new FixedKeyMmSigner(); + const a = await deriveSessionKey(signer, BOOTSTRAP_MESSAGE); + const b = await deriveSessionKey(signer, BOOTSTRAP_MESSAGE + "x"); + expect(a.ethAddress).not.toEqual(b.ethAddress); + }); + + it("deriveSessionKeyFromSignature agrees with deriveSessionKey", async () => { + const signer = new FixedKeyMmSigner(); + const sig = await signer.personalSign(BOOTSTRAP_MESSAGE); + const fromSig = deriveSessionKeyFromSignature(sig); + const fromSigner = await deriveSessionKey(signer); + expect(fromSig).toEqual(fromSigner); + }); + + it("sessionKeyFromPriv round-trips through the noble curve", () => { + const priv = + "0xa6d86467b6ec9e161649b27edfd8519e75a2e1cf5f4c309c628706e6999780e8" as const; + const k = sessionKeyFromPriv(priv); + expect(k.privKey).toEqual(priv); + // Address derived from priv key 0xa6d8...e8 is fixed and well-known + // (this is the Python test vector's address). + expect(k.ethAddress.toLowerCase()).toEqual( + "0xbf60187c5dffa627249f1c3000a4168dbb9d7a1a", + ); + }); + + it("rejects malformed signatures", () => { + expect(() => deriveSessionKeyFromSignature("0xdeadbeef" as any)).toThrow(); + }); +}); diff --git a/tier2_wallet/tests/sign.test.ts b/tier2_wallet/tests/sign.test.ts new file mode 100644 index 0000000..b335644 --- /dev/null +++ b/tier2_wallet/tests/sign.test.ts @@ -0,0 +1,289 @@ +/** + * EIP-712 cross-check against `eth_712_account/scripts/generate_test_signatures.py`. + * + * The Python script signs four canonical OutsideExecutions with a known + * private key and prints the signature felts. The JS sign module must + * produce byte-identical output. If any of these tests fail, the on-chain + * `is_valid_signature` check will reject signatures from this wallet — + * meaning the wallet cannot drive the existing Cairo contract. + */ +import { describe, expect, it } from "vitest"; +import { hash } from "starknet"; + +import { + ANY_CALLER, + MASK_128, + OWNERSHIP_TRANSFER_MSG_HASH, + computeAccountAddress, + computeOutsideExecutionHash, + encodeOwnershipSignature, + sessionKeyFromPriv, + signOutsideExecution, + signOwnership, +} from "../src/index.js"; + +import type { Call, OutsideExecution } from "../src/index.js"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { bigintToBytes32 } from "../src/util.js"; +import { keccak_256 } from "@noble/hashes/sha3"; + +// ─── Python-script constants (must stay in lockstep with generate_test_signatures.py) ── + +const PYTHON_TEST_PRIV = + "0xa6d86467b6ec9e161649b27edfd8519e75a2e1cf5f4c309c628706e6999780e8" as const; +const PYTHON_TEST_ETH_ADDRESS = + "0xbf60187c5dffa627249f1c3000a4168dbb9d7a1a"; + +const CONTRACT_ADDR = + 0x0651b6cc1595bcd7edddc42163b57e066956b8fba487dd781cd7e4b3a671ffe4n; +const TOKEN_ADDR = + 0x0405ea0439568d265140400aa7b31e896604406bdfa7e73e18dec06303c31c6cn; +const SPENDER = 0x1234n; +const RECIPIENT = 0x5678n; +const SPECIFIC_CALLER = 0xcafen; + +const ETH_CHAIN_ID = 1n; +const SN_CHAIN_ID_FELT = BigInt( + "0x" + Buffer.from("SN_MAIN", "utf8").toString("hex"), +); // 0x534e5f4d41494e + +const EXECUTE_AFTER = 1000n; +const EXECUTE_BEFORE = 3000n; + +const APPROVE_AMOUNT = 500n; +const TRANSFER_AMOUNT = 100n; +const INITIAL_SUPPLY = 1000n; + +function selector(name: string): bigint { + return BigInt(hash.starknetKeccak(name)); +} + +function approveCall(amount: bigint): Call { + return { + to: TOKEN_ADDR, + selector: selector("approve"), + calldata: [SPENDER, amount & MASK_128, amount >> 128n], + }; +} + +function transferCall(amount: bigint): Call { + return { + to: TOKEN_ADDR, + selector: selector("transfer"), + calldata: [RECIPIENT, amount & MASK_128, amount >> 128n], + }; +} + +const DOMAIN = { + accountAddress: CONTRACT_ADDR, + evmChainId: ETH_CHAIN_ID, + starknetChainId: SN_CHAIN_ID_FELT, +}; + +const SESSION = sessionKeyFromPriv(PYTHON_TEST_PRIV); + +describe("signOwnership", () => { + it("session-key bookkeeping matches the Python test vector", () => { + expect(SESSION.ethAddress.toLowerCase()).toEqual(PYTHON_TEST_ETH_ADDRESS); + }); + + it("signature recovers to the session ETH address", () => { + const sig = signOwnership(SESSION); + const recovered = recoverEthAddress( + bigintToBytes32(OWNERSHIP_TRANSFER_MSG_HASH), + sig.r, + sig.s, + sig.yParity, + ); + expect(recovered.toLowerCase()).toEqual(PYTHON_TEST_ETH_ADDRESS); + }); + + it("encodes to five felts in [r_low, r_high, s_low, s_high, y_parity] order", () => { + const sig = signOwnership(SESSION); + const felts = encodeOwnershipSignature(sig); + expect(felts).toHaveLength(5); + expect(felts[0]).toEqual(sig.r & MASK_128); + expect(felts[1]).toEqual(sig.r >> 128n); + expect(felts[2]).toEqual(sig.s & MASK_128); + expect(felts[3]).toEqual(sig.s >> 128n); + expect(felts[4]).toEqual(sig.yParity ? 1n : 0n); + }); +}); + +describe("signOutsideExecution — Python parity", () => { + it("Test 1: single approve(500) at nonce=100 matches Python", () => { + const ose: OutsideExecution = { + caller: ANY_CALLER, + nonce: 100n, + executeAfter: EXECUTE_AFTER, + executeBefore: EXECUTE_BEFORE, + calls: [approveCall(APPROVE_AMOUNT)], + }; + const sig = signOutsideExecution({ + session: SESSION, + outsideExecution: ose, + domain: DOMAIN, + }); + expect(sig).toEqual([ + 0x1df31ee91675558108ce242888ccbf09n, + 0xce39f1955774fd02a7c93cb9a7ccf33bn, + 0x1b35465d87708c6d1c70d216e91b6a79n, + 0x916b0be291ada83ecea84291854f2e98n, + 27n, + ETH_CHAIN_ID, + ]); + }); + + it("Test 2: approve(500) + transfer(100) at nonce=101 matches Python", () => { + const ose: OutsideExecution = { + caller: ANY_CALLER, + nonce: 101n, + executeAfter: EXECUTE_AFTER, + executeBefore: EXECUTE_BEFORE, + calls: [approveCall(APPROVE_AMOUNT), transferCall(TRANSFER_AMOUNT)], + }; + const sig = signOutsideExecution({ + session: SESSION, + outsideExecution: ose, + domain: DOMAIN, + }); + expect(sig).toEqual([ + 0xe823335915d3f9c1a8cf05182f4d5366n, + 0xa83e96990bab81e018fd27aa284c0ad0n, + 0x7c4d2c87ace56a14eb444063a5bdb343n, + 0xf23b9d84fc44d9a10fbb0abb7cfa2badn, + 28n, + ETH_CHAIN_ID, + ]); + }); + + it("Test 3: atomicity (approve + over-transfer) at nonce=102 matches Python", () => { + const ose: OutsideExecution = { + caller: ANY_CALLER, + nonce: 102n, + executeAfter: EXECUTE_AFTER, + executeBefore: EXECUTE_BEFORE, + calls: [ + approveCall(APPROVE_AMOUNT), + transferCall(INITIAL_SUPPLY + 1n), + ], + }; + const sig = signOutsideExecution({ + session: SESSION, + outsideExecution: ose, + domain: DOMAIN, + }); + expect(sig).toEqual([ + 0xd0cbbfc6638e59a5e3b576ae9b2305ddn, + 0xbabcee977ce0c6b0ee9ece7f944dbce3n, + 0x6f27c710ab23db5cf1e101e87602d5fdn, + 0x9170f7129929d19c9a6234d0797ca300n, + 28n, + ETH_CHAIN_ID, + ]); + }); + + it("Test 4: specific caller (empty calls) at nonce=103 matches Python", () => { + const ose: OutsideExecution = { + caller: SPECIFIC_CALLER, + nonce: 103n, + executeAfter: EXECUTE_AFTER, + executeBefore: EXECUTE_BEFORE, + calls: [], + }; + const sig = signOutsideExecution({ + session: SESSION, + outsideExecution: ose, + domain: DOMAIN, + }); + expect(sig).toEqual([ + 0x661b512c1158c255c61c5f0214f167c7n, + 0x961538890df420779799bd7a8d236e06n, + 0x3fbcc975607ffe4640fc6c175ee5cdaen, + 0x5fc913ba9a2de3bbe95914339a7a8666n, + 27n, + ETH_CHAIN_ID, + ]); + }); +}); + +describe("EIP-712 hash determinism", () => { + it("changes when nonce changes", () => { + const baseOse: OutsideExecution = { + caller: ANY_CALLER, + nonce: 100n, + executeAfter: EXECUTE_AFTER, + executeBefore: EXECUTE_BEFORE, + calls: [approveCall(APPROVE_AMOUNT)], + }; + const h1 = computeOutsideExecutionHash(baseOse, DOMAIN); + const h2 = computeOutsideExecutionHash( + { ...baseOse, nonce: 101n }, + DOMAIN, + ); + expect(h1).not.toEqual(h2); + }); + + it("changes when verifying contract changes", () => { + const ose: OutsideExecution = { + caller: ANY_CALLER, + nonce: 100n, + executeAfter: EXECUTE_AFTER, + executeBefore: EXECUTE_BEFORE, + calls: [approveCall(APPROVE_AMOUNT)], + }; + const h1 = computeOutsideExecutionHash(ose, DOMAIN); + const h2 = computeOutsideExecutionHash(ose, { + ...DOMAIN, + accountAddress: DOMAIN.accountAddress + 1n, + }); + expect(h1).not.toEqual(h2); + }); +}); + +describe("integration: derive → address → sign", () => { + it("end-to-end pipeline produces consistent artifacts", () => { + const session = sessionKeyFromPriv(PYTHON_TEST_PRIV); + const factoryAddr = 0xfacade_facade_facaden; + const accountAddr = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: factoryAddr, + network: "prod", + }); + const ownership = signOwnership(session); + + expect(typeof accountAddr).toEqual("bigint"); + expect(ownership.r).toBeGreaterThan(0n); + expect(ownership.s).toBeGreaterThan(0n); + + // Re-deriving must yield the same artifacts. + const accountAddr2 = computeAccountAddress({ + sessionEthAddress: session.ethAddress, + factoryAddress: factoryAddr, + network: "prod", + }); + expect(accountAddr2).toEqual(accountAddr); + }); +}); + +// ─── helpers ──────────────────────────────────────────────────────────────── + +/** + * Recover the Ethereum-format address that produced an (r, s, y_parity) + * signature over `msgHash`. We can't reach into the wallet's `recover_eth_address` + * Cairo helper from JS, so we re-derive using @noble/curves. + */ +function recoverEthAddress( + msgHash: Uint8Array, + r: bigint, + s: bigint, + yParity: boolean, +): string { + // Cairo convention: y_parity == true ↔ v == 28 ↔ recovery == 1. + const recovery = yParity ? 1 : 0; + const sig = new secp256k1.Signature(r, s).addRecoveryBit(recovery); + const pub = sig.recoverPublicKey(msgHash).toRawBytes(false); // 65-byte 0x04 || X || Y + const xy = pub.subarray(1); + const hashBytes = keccak_256(xy); + return "0x" + Buffer.from(hashBytes.subarray(12)).toString("hex"); +} diff --git a/tier2_wallet/tsconfig.json b/tier2_wallet/tsconfig.json new file mode 100644 index 0000000..e5b6805 --- /dev/null +++ b/tier2_wallet/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "tests", "demo"] +} From 884ef0c31b60b6040e153a740d30f0bc68a0d480 Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 13 May 2026 09:39:11 +0530 Subject: [PATCH 3/4] chore(tier2_wallet): pin Sepolia deployment addresses Output of `npm run deploy:contracts` + `npm run deploy:extras` on Starknet Sepolia (custom node at 34.133.167.123). Captures every class hash + singleton address so subsequent runs of the demo / wallet client can reuse them without redeploying. If anyone redeploys, the scripts will rewrite this file in place. Co-Authored-By: Claude Opus 4.7 (1M context) --- tier2_wallet/sepolia-deployments.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tier2_wallet/sepolia-deployments.json diff --git a/tier2_wallet/sepolia-deployments.json b/tier2_wallet/sepolia-deployments.json new file mode 100644 index 0000000..7aae9bb --- /dev/null +++ b/tier2_wallet/sepolia-deployments.json @@ -0,0 +1,12 @@ +{ + "PRIMER_CLASS_HASH": "0x3edae2158f4aea6295470678fc7de27e19d7e40f295cda90d786d17b3531fdf", + "STARKNET_ETH712_ACCOUNT_CLASS_HASH": "0x438ec500e11aca3549838081ca429242428595a26da52d0a9b42a64dd0cf4e9", + "FACTORY_CLASS_HASH": "0x2a50de00fbffda9e734d95e08900d0bc1782d108a40faf60433dac3d2874768", + "HELPER_CLASS_HASH": "0x7d7aa47b7173e48272846e2394039c3362e17059e172ebff38bd28ba5b9ed81", + "COUNTER_CLASS_HASH": "0x1a666a518ddaec8361e708a59b16e2a7375ec11da49308a4f74a5ccd0ac132", + "FACTORY_ADDRESS": "0x59e7f08740ee76ce48106a86928cc41314c37ba6904c6aa648eb498b434cb8", + "HELPER_ADDRESS": "0x3fa9bd2acc8ea70864bdc95aadb8a91b15eee5a8ecf6641020167600e908cc4", + "INVOKE_HELPER_CLASS_HASH": "0x648ddb6411a217afeed9d883aaa8d61b918f934f8cf0619a8cc1838d619a4f4", + "COUNTER_ADDRESS": "0x524789a678b36cceb337aeb27b196e70f704fa8a0a02f581536f3f76ad4e230", + "INVOKE_HELPER_ADDRESS": "0x5f05a41c263d13df989169d9067cbbc329a2ede1e1e16b3ac65d673c8926df0" +} From f7eba40595b3b4b5e75c0500c723c16ef135dbea Mon Sep 17 00:00:00 2001 From: Akash Date: Wed, 13 May 2026 13:36:16 +0530 Subject: [PATCH 4/4] chore: gitignore .gstack/ tooling cache Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b39e5de..4b575b8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ venv*/ target/ .spr.yml .snfoundry_cache/ +.gstack/