diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/cairo_program/Scarb.toml b/cairo_program/Scarb.toml index b6c900a..7be1394 100644 --- a/cairo_program/Scarb.toml +++ b/cairo_program/Scarb.toml @@ -5,6 +5,8 @@ edition = "2025_12" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html +[lib] + [executable] [cairo] @@ -12,5 +14,3 @@ enable-gas = false [dependencies] cairo_execute = "2.18.0" - - diff --git a/cairo_program/src/integer.cairo b/cairo_program/src/integer.cairo index 9501eca..cf39e24 100644 --- a/cairo_program/src/integer.cairo +++ b/cairo_program/src/integer.cairo @@ -1,20 +1,49 @@ #[executable] fn main() { - let result: u8 = add_num(5, 6); + let result: u32 = add_num(5, 6); println!("the sum of x & y is: {}", result); - assert(result == 11, 'invalid sum logic'); + assert!(result == 11, "invalid add logic"); - let sub_result: u8 = sub_num(10, 5); + let sub_result: u32 = sub_num(3, 5); println!("sub result is: {}", sub_result); - assert(sub_result == 5, 'invalid sub logic'); + assert!(sub_result == 5, "invalid sub logic"); + + let mul_result: u8 = mul_num(5, 6); + println!("mul result is: {}", mul_result); + assert!(mul_result == 30, "invalid mul logic"); + + let div_result: u8 = div_num(10, 5); + println!("div result is: {}", div_result); + assert!(div_result == 2, "invalid div logic"); } // addition logic -fn add_num(x: u8, y: u8) -> u8 { +pub fn add_num(x: u32, y: u32) -> u32 { x + y } // subtraction logic -fn sub_num(x: u8, y: u8) -> u8 { +pub fn sub_num(x: u32, y: u32) -> u32 { + assert!(x >= y, "y should be less than or equal to x"); return x - y; } + +// multiplication logic +pub fn mul_num(x: u8, y: u8) -> u8 { + if (x == 0 || y == 0) { + assert!(y != 0 && x != 0, "x or y should not be zero"); + } + x * y +} + +// division logic +pub fn div_num(x: u8, y: u8) -> u8 { + if y == 0 || x == 0 { + assert!(y != 0 && x != 0, "x or y should not be zero"); + } + + if (y > x) { + assert!(y <= x, "y should be less than or equal to x"); + } + x / y +} diff --git a/cairo_program/src/lib.cairo b/cairo_program/src/lib.cairo index acc7644..e2bf7da 100644 --- a/cairo_program/src/lib.cairo +++ b/cairo_program/src/lib.cairo @@ -1,5 +1,7 @@ // mod hello_world; // mod short_string; -// mod integer; +pub mod integer; // mod bool; -mod bytearray; \ No newline at end of file +// mod bytearray; + + diff --git a/erc20/.gitignore b/erc20/.gitignore new file mode 100644 index 0000000..4096f8b --- /dev/null +++ b/erc20/.gitignore @@ -0,0 +1,5 @@ +target +.snfoundry_cache/ +snfoundry_trace/ +coverage/ +profile/ diff --git a/erc20/Scarb.lock b/erc20/Scarb.lock new file mode 100644 index 0000000..20652a5 --- /dev/null +++ b/erc20/Scarb.lock @@ -0,0 +1,24 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "erc20" +version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.59.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:871fba677c03b66a1bf40815dac0ab1b385eb1b9be6e6c3cf2ad9788eeb2b6bb" + +[[package]] +name = "snforge_std" +version = "0.59.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:3620924fa08bd2d740b2b5b01ef86c8dab3d4b9c2206387c8dbdc8d2ec15133e" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/erc20/Scarb.toml b/erc20/Scarb.toml new file mode 100644 index 0000000..a7a7690 --- /dev/null +++ b/erc20/Scarb.toml @@ -0,0 +1,52 @@ +[package] +name = "erc20" +version = "0.1.0" +edition = "2024_07" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +starknet = "2.17.0" + +[dev-dependencies] +snforge_std = "0.59.0" +assert_macros = "2.17.0" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std"] + +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/scarb-toml.html for more information + +# [tool.snforge] # Define `snforge` tool section +# exit_first = true # Stop tests execution immediately upon the first failure +# fuzzer_runs = 1234 # Number of runs of the random fuzzer +# fuzzer_seed = 1111 # Seed for the random fuzzer + +# [[tool.snforge.fork]] # Used for fork testing +# name = "SOME_NAME" # Fork name +# url = "http://your.rpc.url" # Url of the RPC provider +# block_id.tag = "latest" # Block to fork from (block tag) + +# [[tool.snforge.fork]] +# name = "SOME_SECOND_NAME" +# url = "http://your.second.rpc.url" +# block_id.number = "123" # Block to fork from (block number) + +# [[tool.snforge.fork]] +# name = "SOME_THIRD_NAME" +# url = "http://your.third.rpc.url" +# block_id.hash = "0x123" # Block to fork from (block hash) + +# [profile.dev.cairo] # Configure Cairo compiler +# unstable-add-statements-code-locations-debug-info = true # Should be used if you want to use coverage +# unstable-add-statements-functions-debug-info = true # Should be used if you want to use coverage/profiler +# inlining-strategy = "avoid" # Should be used if you want to use coverage + +# [features] # Used for conditional compilation +# enable_for_tests = [] # Feature name and list of other features that should be enabled with it diff --git a/erc20/snfoundry.toml b/erc20/snfoundry.toml new file mode 100644 index 0000000..629ac43 --- /dev/null +++ b/erc20/snfoundry.toml @@ -0,0 +1,13 @@ +[sncast.default] +account = "erc-account" +# Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html +# and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information + +# [sncast.default] # Define a profile name +# url = "https://api.zan.top/public/starknet-sepolia/rpc/v0_10" # Url of the RPC provider +# accounts-file = "../account-file" # Path to the file with the account data +# account = "mainuser" # Account from `accounts_file` or default account file that will be used for the transactions +# keystore = "~/keystore" # Path to the keystore file +# wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters +# block-explorer = "Voyager" # Block explorer service used to display links to transaction details +# show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer diff --git a/erc20/src/event.cairo b/erc20/src/event.cairo new file mode 100644 index 0000000..8302a8c --- /dev/null +++ b/erc20/src/event.cairo @@ -0,0 +1,22 @@ +use starknet::ContractAddress; + +#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] +pub struct Transfer { + pub from: ContractAddress, + pub to: ContractAddress, + pub value: u256, +} + +#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] +pub struct Approval { + pub owner: ContractAddress, + pub spender: ContractAddress, + pub value: u256, +} + +#[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] +pub struct Revoke { + pub owner: ContractAddress, + pub spender: ContractAddress, + pub value: u256, +} diff --git a/erc20/src/interface.cairo b/erc20/src/interface.cairo new file mode 100644 index 0000000..34e9019 --- /dev/null +++ b/erc20/src/interface.cairo @@ -0,0 +1,22 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IERC20 { + fn name(self: @TContractState) -> felt252; + fn symbol(self: @TContractState) -> felt252; + fn decimals(self: @TContractState) -> u8; + fn total_supply(self: @TContractState) -> u256; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256); + fn transfer_from( + ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256, + ); + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256); + fn revoke(ref self: TContractState, spender: ContractAddress); + fn increase_allowance(ref self: TContractState, spender: ContractAddress, added_value: u256); + fn decrease_allowance( + ref self: TContractState, spender: ContractAddress, subtracted_value: u256, + ); + fn burn(ref self: TContractState, amount: u256); +} diff --git a/erc20/src/lib.cairo b/erc20/src/lib.cairo new file mode 100644 index 0000000..180c879 --- /dev/null +++ b/erc20/src/lib.cairo @@ -0,0 +1,211 @@ +pub mod event; +pub mod interface; + +pub use interface::IERC20; + +#[starknet::contract] +pub mod erc20 { + use core::num::traits::Zero; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_caller_address}; + use super::event::{Approval, Revoke, Transfer}; + + #[storage] + struct Storage { + name: felt252, + symbol: felt252, + decimals: u8, + total_supply: u256, + max_limit: u256, + balances: Map, + allowances: Map<(ContractAddress, ContractAddress), u256>, + admin: ContractAddress, + } + + #[event] + #[derive(Copy, Drop, Debug, PartialEq, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + Revoke: Revoke, + } + + mod Errors { + pub const APPROVE_FROM_ZERO: felt252 = 'ERC20: approve from 0'; + pub const APPROVE_TO_ZERO: felt252 = 'ERC20: approve to 0'; + pub const TRANSFER_FROM_ZERO: felt252 = 'ERC20: transfer from 0'; + pub const TRANSFER_TO_ZERO: felt252 = 'ERC20: transfer to 0'; + pub const BURN_FROM_ZERO: felt252 = 'ERC20: burn from 0'; + pub const MINT_TO_ZERO: felt252 = 'ERC20: mint to 0'; + pub const MAX_TRANSFER_LIMIT: felt252 = 'exceed transfer limit'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + recipient: ContractAddress, + name: felt252, + decimals: u8, + initial_supply: u256, + symbol: felt252, + admin: ContractAddress, + max_limit: u256, + ) { + self.name.write(name); + self.symbol.write(symbol); + self.decimals.write(decimals); + self.admin.write(admin); + self.max_limit.write(max_limit); + self.mint(recipient, initial_supply); + } + + #[abi(embed_v0)] + impl IERC20Impl of super::IERC20 { + fn name(self: @ContractState) -> felt252 { + self.name.read() + } + + fn symbol(self: @ContractState) -> felt252 { + self.symbol.read() + } + + fn decimals(self: @ContractState) -> u8 { + self.decimals.read() + } + + fn total_supply(self: @ContractState) -> u256 { + self.total_supply.read() + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account) + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> u256 { + self.allowances.read((owner, spender)) + } + + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) { + assert(recipient.is_non_zero(), Errors::TRANSFER_TO_ZERO); + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + } + + fn burn(ref self: ContractState, amount: u256) { + let caller = get_caller_address(); + assert(caller.is_non_zero(), Errors::BURN_FROM_ZERO); + let zero: ContractAddress = Zero::zero(); + self._transfer(caller, zero, amount); + self.total_supply.write(self.total_supply.read() - amount); + } + + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + ) { + let caller = get_caller_address(); + self.spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) { + let caller = get_caller_address(); + self.approve_helper(caller, spender, amount); + } + + fn revoke(ref self: ContractState, spender: ContractAddress) { + self.only_admin(); + let caller = get_caller_address(); + self.revoke_helper(caller, spender); + } + + fn increase_allowance( + ref self: ContractState, spender: ContractAddress, added_value: u256, + ) { + let caller = get_caller_address(); + self + .approve_helper( + caller, spender, self.allowances.read((caller, spender)) + added_value, + ); + } + + fn decrease_allowance( + ref self: ContractState, spender: ContractAddress, subtracted_value: u256, + ) { + let caller = get_caller_address(); + self + .approve_helper( + caller, spender, self.allowances.read((caller, spender)) - subtracted_value, + ); + } + } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _transfer( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256, + ) { + assert(sender.is_non_zero(), Errors::TRANSFER_FROM_ZERO); + assert(amount <= self.max_limit.read(), Errors::MAX_TRANSFER_LIMIT); + self.balances.write(sender, self.balances.read(sender) - amount); + self.balances.write(recipient, self.balances.read(recipient) + amount); + self.emit(Transfer { from: sender, to: recipient, value: amount }); + } + + fn spend_allowance( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256, + ) { + let allowance = self.allowances.read((owner, spender)); + self.allowances.write((owner, spender), allowance - amount); + } + + fn approve_helper( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256, + ) { + assert(spender.is_non_zero(), Errors::APPROVE_TO_ZERO); + self.allowances.write((owner, spender), amount); + self.emit(Approval { owner, spender, value: amount }); + } + + fn revoke_helper( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, + ) { + self.allowances.write((owner, spender), 0); + self.emit(Revoke { owner, spender, value: 0 }); + } + + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + assert(recipient.is_non_zero(), Errors::MINT_TO_ZERO); + let supply = self.total_supply.read() + amount; + self.total_supply.write(supply); + let balance = self.balances.read(recipient) + amount; + self.balances.write(recipient, balance); + self + .emit( + Event::Transfer(Transfer { from: Zero::zero(), to: recipient, value: amount }), + ); + } + + fn only_admin(ref self: ContractState) { + let caller = get_caller_address(); + assert(caller == self.admin.read(), 'ERC20: caller is not admin'); + } + + + + fn update_max_limit(ref self: ContractState, new_limit: u256) { + self.only_admin(); + self.max_limit.write(new_limit); + } + } +} diff --git a/erc20/tests/test_contract.cairo b/erc20/tests/test_contract.cairo new file mode 100644 index 0000000..f475cc3 --- /dev/null +++ b/erc20/tests/test_contract.cairo @@ -0,0 +1,491 @@ +use erc20::erc20::Event; +use erc20::event::{Approval, Revoke, Transfer}; +use erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_caller_address, stop_cheat_caller_address, +}; +use starknet::ContractAddress; + +const NAME: felt252 = 'Jigah'; +const SYMBOL: felt252 = 'JIG'; +const DECIMALS: u8 = 18; +const INITIAL_SUPPLY: u256 = 1_000_000; +const MAX_LIMIT: u256 = 1_000_000; + +fn owner() -> ContractAddress { + 'owner'.try_into().unwrap() +} +fn admin() -> ContractAddress { + 'admin'.try_into().unwrap() +} +fn alice() -> ContractAddress { + 'alice'.try_into().unwrap() +} +fn bob() -> ContractAddress { + 'bob'.try_into().unwrap() +} + + +fn deploy() -> (IERC20Dispatcher, ContractAddress) { + deploy_with(INITIAL_SUPPLY, MAX_LIMIT) +} + +fn deploy_with(initial_supply: u256, max_limit: u256) -> (IERC20Dispatcher, ContractAddress) { + let contract = declare("erc20").unwrap().contract_class(); + + let calldata: Array = array![ + owner().into(), NAME, DECIMALS.into(), initial_supply.low.into(), + initial_supply.high.into(), SYMBOL, admin().into(), max_limit.low.into(), + max_limit.high.into(), + ]; + + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + (IERC20Dispatcher { contract_address }, contract_address) +} + + +#[test] +fn test_constructor_initial_state() { + let (d, _) = deploy(); + assert_eq!(d.name(), NAME); + assert_eq!(d.symbol(), SYMBOL); + assert_eq!(d.decimals(), DECIMALS); + assert_eq!(d.total_supply(), INITIAL_SUPPLY); + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY); + assert_eq!(d.balance_of(alice()), 0); +} + +#[test] +fn test_constructor_sets_max_limit_via_transfer_at_exact_limit() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), MAX_LIMIT); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(alice()), MAX_LIMIT); + assert_eq!(d.balance_of(owner()), 0); +} + +#[test] +#[should_panic(expected: 'exceed transfer limit')] +fn test_constructor_max_limit_enforced_above_limit() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), MAX_LIMIT + 1); + stop_cheat_caller_address(ca); +} + +#[test] +#[should_panic(expected: 'exceed transfer limit')] +fn test_constructor_zero_max_limit_blocks_all_transfers() { + let (d, ca) = deploy_with(INITIAL_SUPPLY, 0); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), 1); + stop_cheat_caller_address(ca); +} + +#[test] +fn test_constructor_custom_max_limit_is_respected() { + let custom_limit: u256 = 500; + let (d, ca) = deploy_with(INITIAL_SUPPLY, custom_limit); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), custom_limit); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(alice()), custom_limit); +} + +#[test] +#[should_panic(expected: 'exceed transfer limit')] +fn test_constructor_custom_max_limit_rejects_above() { + let custom_limit: u256 = 500; + let (d, ca) = deploy_with(INITIAL_SUPPLY, custom_limit); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), custom_limit + 1); + stop_cheat_caller_address(ca); +} + +#[test] +fn test_get_name() { + let (d, _) = deploy(); + assert_eq!(d.name(), NAME); +} + +#[test] +fn test_get_symbol() { + let (d, _) = deploy(); + assert_eq!(d.symbol(), SYMBOL); +} + +#[test] +fn test_get_decimals() { + let (d, _) = deploy(); + assert_eq!(d.decimals(), DECIMALS); +} + +#[test] +fn test_get_total_supply() { + let (d, _) = deploy(); + assert_eq!(d.total_supply(), INITIAL_SUPPLY); +} + +#[test] +fn test_balance_of_recipient_after_deploy() { + let (d, _) = deploy(); + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY); +} + +#[test] +fn test_balance_of_unknown_is_zero() { + let (d, _) = deploy(); + assert_eq!(d.balance_of(alice()), 0); +} + +#[test] +fn test_transfer_updates_balances() { + let (d, ca) = deploy(); + let amount: u256 = 500; + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), amount); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(alice()), amount); + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY - amount); +} + +#[test] +fn test_transfer_total_supply_unchanged() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), 400); + stop_cheat_caller_address(ca); + + assert_eq!(d.total_supply(), INITIAL_SUPPLY); +} + +#[test] +fn test_transfer_multiple_recipients_accumulate() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), 300); + d.transfer(bob(), 200); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(alice()), 300); + assert_eq!(d.balance_of(bob()), 200); + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY - 500); +} + +#[test] +fn test_transfer_to_self_balance_unchanged() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.transfer(owner(), 100); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY); +} + +#[test] +fn test_transfer_emits_event() { + let (d, ca) = deploy(); + let amount: u256 = 100; + + let mut spy = spy_events(); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), amount); + stop_cheat_caller_address(ca); + + spy + .assert_emitted( + @array![(ca, Event::Transfer(Transfer { from: owner(), to: alice(), value: amount }))], + ); +} + +#[test] +#[should_panic(expected: 'exceed transfer limit')] +fn test_transfer_panics_when_exceeds_max_limit() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.transfer(alice(), MAX_LIMIT + 1); + stop_cheat_caller_address(ca); +} + +#[test] +#[should_panic(expected: 'ERC20: transfer to 0')] +fn test_transfer_panics_to_zero_address() { + let (d, ca) = deploy(); + let zero: ContractAddress = 0_felt252.try_into().unwrap(); + + start_cheat_caller_address(ca, owner()); + d.transfer(zero, 100); + stop_cheat_caller_address(ca); +} + +#[test] +#[should_panic] +fn test_transfer_panics_on_insufficient_balance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, alice()); // alice has 0 + d.transfer(bob(), 1); + stop_cheat_caller_address(ca); +} + + +#[test] +fn test_approve_sets_allowance() { + let (d, ca) = deploy(); + let amount: u256 = 300; + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), amount); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), amount); +} + +#[test] +fn test_approve_overwrites_existing_allowance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 300); + d.approve(alice(), 100); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), 100); +} + +#[test] +fn test_approve_does_not_affect_other_spenders() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 200); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), bob()), 0); +} + +#[test] +fn test_approve_emits_event() { + let (d, ca) = deploy(); + let amount: u256 = 300; + + let mut spy = spy_events(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), amount); + stop_cheat_caller_address(ca); + + spy + .assert_emitted( + @array![ + (ca, Event::Approval(Approval { owner: owner(), spender: alice(), value: amount })), + ], + ); +} + +#[test] +#[should_panic(expected: 'ERC20: approve to 0')] +fn test_approve_panics_to_zero_address() { + let (d, ca) = deploy(); + let zero: ContractAddress = 0_felt252.try_into().unwrap(); + + start_cheat_caller_address(ca, owner()); + d.approve(zero, 100); + stop_cheat_caller_address(ca); +} + +#[test] +fn test_transfer_from_moves_funds_and_consumes_allowance() { + let (d, ca) = deploy(); + let amount: u256 = 200; + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), amount); + stop_cheat_caller_address(ca); + + start_cheat_caller_address(ca, alice()); + d.transfer_from(owner(), bob(), amount); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(bob()), amount); + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY - amount); + assert_eq!(d.allowance(owner(), alice()), 0); +} + +#[test] +fn test_transfer_from_partial_spend_leaves_remaining_allowance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 300); + stop_cheat_caller_address(ca); + + start_cheat_caller_address(ca, alice()); + d.transfer_from(owner(), bob(), 100); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), 200); + assert_eq!(d.balance_of(bob()), 100); +} + +#[test] +#[should_panic] +fn test_transfer_from_panics_on_insufficient_allowance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, alice()); + d.transfer_from(owner(), bob(), 1); + stop_cheat_caller_address(ca); +} + +#[test] +fn test_increase_allowance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 100); + d.increase_allowance(alice(), 50); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), 150); +} + +#[test] +fn test_decrease_allowance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 100); + d.decrease_allowance(alice(), 40); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), 60); +} + +#[test] +fn test_decrease_allowance_to_zero() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 100); + d.decrease_allowance(alice(), 100); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), 0); +} + +#[test] +fn test_revoke_clears_admin_allowance_to_spender() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, admin()); + d.approve(alice(), 500); + assert_eq!(d.allowance(admin(), alice()), 500); + d.revoke(alice()); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(admin(), alice()), 0); +} + +#[test] +fn test_revoke_does_not_affect_other_allowances() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, owner()); + d.approve(alice(), 777); + stop_cheat_caller_address(ca); + + start_cheat_caller_address(ca, admin()); + d.approve(alice(), 100); + d.revoke(alice()); + stop_cheat_caller_address(ca); + + assert_eq!(d.allowance(owner(), alice()), 777); +} + +#[test] +fn test_revoke_emits_event() { + let (d, ca) = deploy(); + + let mut spy = spy_events(); + + start_cheat_caller_address(ca, admin()); + d.approve(alice(), 100); + d.revoke(alice()); + stop_cheat_caller_address(ca); + + spy + .assert_emitted( + @array![(ca, Event::Revoke(Revoke { owner: admin(), spender: alice(), value: 0 }))], + ); +} + +#[test] +#[should_panic(expected: 'ERC20: caller is not admin')] +fn test_revoke_panics_for_non_admin() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, alice()); + d.revoke(bob()); + stop_cheat_caller_address(ca); +} + +// ─── Burn +// ───────────────────────────────────────────────────────────────────── + +#[test] +fn test_burn_decreases_balance_and_total_supply() { + let (d, ca) = deploy(); + let amount: u256 = 100; + + start_cheat_caller_address(ca, owner()); + d.burn(amount); + stop_cheat_caller_address(ca); + + assert_eq!(d.balance_of(owner()), INITIAL_SUPPLY - amount); + assert_eq!(d.total_supply(), INITIAL_SUPPLY - amount); +} + +#[test] +fn test_burn_emits_transfer_to_zero() { + let (d, ca) = deploy(); + let amount: u256 = 200; + let zero: ContractAddress = 0_felt252.try_into().unwrap(); + + let mut spy = spy_events(); + + start_cheat_caller_address(ca, owner()); + d.burn(amount); + stop_cheat_caller_address(ca); + + spy + .assert_emitted( + @array![(ca, Event::Transfer(Transfer { from: owner(), to: zero, value: amount }))], + ); +} + +#[test] +#[should_panic] +fn test_burn_panics_on_insufficient_balance() { + let (d, ca) = deploy(); + + start_cheat_caller_address(ca, alice()); // alice has 0 + d.burn(1); + stop_cheat_caller_address(ca); +} diff --git a/starknet_agent/.env.example b/starknet_agent/.env.example new file mode 100644 index 0000000..ffa4095 --- /dev/null +++ b/starknet_agent/.env.example @@ -0,0 +1,23 @@ +# ─── Starknet wallet ────────────────────────────────────────────────────────── +# Mainnet RPC (swap to sepolia for testing) +STARKNET_RPC_URL=https://starknet-mainnet.public.blastapi.io/rpc/v0_7 +STARKNET_ACCOUNT_ADDRESS=0xYOUR_ACCOUNT_ADDRESS +STARKNET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY + +# ─── Transfer parameters ────────────────────────────────────────────────────── +# Token to send: ETH | STRK | USDC | USDT | +TRANSFER_TOKEN=ETH + +# Recipient wallet address (must start with 0x) +TRANSFER_RECIPIENT=0xRECIPIENT_ADDRESS + +# Human-readable amount to transfer (e.g. "0.001" = 0.001 ETH) +TRANSFER_AMOUNT=0.001 + +# Balance must be GREATER THAN this threshold to trigger the transfer. +# If balance ≤ threshold the agent logs "skipped" and exits cleanly. +TRANSFER_THRESHOLD=0.005 + +# ─── Secondary agent token ──────────────────────────────────────────────────── +# Token the post-transfer verification agent will also report on. +SECONDARY_TOKEN=STRK diff --git a/starknet_agent/.gitignore b/starknet_agent/.gitignore new file mode 100644 index 0000000..e054423 --- /dev/null +++ b/starknet_agent/.gitignore @@ -0,0 +1,24 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* diff --git a/starknet_agent/README.md b/starknet_agent/README.md new file mode 100644 index 0000000..f1a4722 --- /dev/null +++ b/starknet_agent/README.md @@ -0,0 +1,48 @@ +# my-starknet-agent + +A Starknet AI Agent built with [starknet-agentic](https://github.com/keep-starknet-strange/starknet-agentic). + +## Quick Start + +1. Install dependencies: + ```bash + npm install + # or + pnpm install + ``` + +2. Configure your environment: + ```bash + cp .env.example .env + # Edit .env with your account address and private key + ``` + +3. Run the agent: + ```bash + npm start + # or for development with auto-reload: + npm run dev + ``` + +## Configuration + +This agent is configured for **sepolia**. + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `STARKNET_RPC_URL` | Starknet RPC endpoint | +| `STARKNET_ACCOUNT_ADDRESS` | Your agent's account address | +| `STARKNET_PRIVATE_KEY` | Private key for signing transactions | + + +## Template: minimal + +The **minimal** template includes basic wallet operations (balance check, transfers). Perfect for getting started. + +## Resources + +- [Starknet Agentic Docs](https://starknet-agentic.vercel.app) +- [starknet.js Documentation](https://www.starknetjs.com/) +- [AVNU SDK](https://github.com/avnu-labs/avnu-sdk) diff --git a/starknet_agent/agent-log.jsonl b/starknet_agent/agent-log.jsonl new file mode 100644 index 0000000..5ee9c1b --- /dev/null +++ b/starknet_agent/agent-log.jsonl @@ -0,0 +1,35 @@ +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","STRK Balance":"800 STRK"},"timestamp":"2026-05-20T15:28:19.956Z","elapsedMs":1016} +{"stepName":"Validate Transfer Condition","status":"success","message":"Condition MET — balance > threshold","data":{"Current Balance":"800 STRK","Threshold":"0.005 STRK","Transfer Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Decision":"PROCEED — submitting transfer"},"timestamp":"2026-05-20T15:28:19.957Z","elapsedMs":1017} +{"stepName":"Execute Token Transfer","status":"failed","message":"Unhandled error — aborting pipeline","error":"RPC: starknet_getClassAt with params {\n \"block_id\": \"latest\",\n \"contract_address\": \"0x6a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b\"\n}\n\n 20: Contract not found: undefined","timestamp":"2026-05-20T15:28:21.538Z","elapsedMs":2598} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","STRK Balance":"800 STRK"},"timestamp":"2026-05-20T15:34:20.682Z","elapsedMs":921} +{"stepName":"Validate Transfer Condition","status":"success","message":"Condition MET — balance > threshold","data":{"Current Balance":"800 STRK","Threshold":"0.005 STRK","Transfer Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Decision":"PROCEED — submitting transfer"},"timestamp":"2026-05-20T15:34:20.689Z","elapsedMs":928} +{"stepName":"Execute Token Transfer","status":"failed","message":"Unhandled error — aborting pipeline","error":"RPC: starknet_estimateFee with params {\n \"request\": [\n {\n \"type\": \"INVOKE\",\n \"sender_address\": \"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b\",\n \"calldata\": [\n \"0x1\",\n \"0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d\",\n \"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e\",\n \"0x3\",\n \"0x56e7e799e0fcff5be0d452ef9cc7ce89d876de4aded207d3f439af172dafd\",\n \"0x38d7ea4c68000\",\n \"0x0\"\n ],\n \"signature\": [],\n \"nonce\": \"0x0\",\n \"resource_bounds\": {\n \"l2_gas\": {\n \"max_amount\": \"0x0\",\n \"max_price_per_unit\": \"0x0\"\n },\n \"l1_gas\": {\n \"max_amount\": \"0x0\",\n \"max_price_per_unit\": \"0x0\"\n },\n \"l1_data_gas\": {\n \"max_amount\": \"0x0\",\n \"max_price_per_unit\": \"0x0\"\n }\n },\n \"tip\": \"0x5f5e100\",\n \"paymaster_data\": [],\n \"nonce_data_availability_mode\": \"L1\",\n \"fee_data_availability_mode\": \"L1\",\n \"account_deployment_data\": [],\n \"version\": \"0x100000000000000000000000000000003\"\n }\n ],\n \"block_id\": \"latest\",\n \"simulation_flags\": [\n \"SKIP_VALIDATE\"\n ]\n}\n\n 41: Transaction execution error: {\"execution_error\":{\"class_hash\":\"0x0\",\"contract_address\":\"0x6a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b\",\"error\":\"Requested contract address 0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b is not deployed.\\n\",\"selector\":\"0x15d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad\"},\"transaction_index\":0}","timestamp":"2026-05-20T15:34:22.229Z","elapsedMs":2468} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","STRK Balance":"800 STRK"},"timestamp":"2026-05-20T15:34:52.947Z","elapsedMs":990} +{"stepName":"Validate Transfer Condition","status":"success","message":"Condition MET — balance > threshold","data":{"Current Balance":"800 STRK","Threshold":"0.005 STRK","Transfer Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Decision":"PROCEED — submitting transfer"},"timestamp":"2026-05-20T15:34:52.990Z","elapsedMs":1033} +{"stepName":"Execute Token Transfer","status":"failed","message":"Unhandled error — aborting pipeline","error":"RPC: starknet_estimateFee with params {\n \"request\": [\n {\n \"type\": \"INVOKE\",\n \"sender_address\": \"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b\",\n \"calldata\": [\n \"0x1\",\n \"0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d\",\n \"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e\",\n \"0x3\",\n \"0x56e7e799e0fcff5be0d452ef9cc7ce89d876de4aded207d3f439af172dafd\",\n \"0x38d7ea4c68000\",\n \"0x0\"\n ],\n \"signature\": [],\n \"nonce\": \"0x0\",\n \"resource_bounds\": {\n \"l2_gas\": {\n \"max_amount\": \"0x0\",\n \"max_price_per_unit\": \"0x0\"\n },\n \"l1_gas\": {\n \"max_amount\": \"0x0\",\n \"max_price_per_unit\": \"0x0\"\n },\n \"l1_data_gas\": {\n \"max_amount\": \"0x0\",\n \"max_price_per_unit\": \"0x0\"\n }\n },\n \"tip\": \"0x5f5e100\",\n \"paymaster_data\": [],\n \"nonce_data_availability_mode\": \"L1\",\n \"fee_data_availability_mode\": \"L1\",\n \"account_deployment_data\": [],\n \"version\": \"0x100000000000000000000000000000003\"\n }\n ],\n \"block_id\": \"latest\",\n \"simulation_flags\": [\n \"SKIP_VALIDATE\"\n ]\n}\n\n 41: Transaction execution error: {\"execution_error\":{\"class_hash\":\"0x0\",\"contract_address\":\"0x6a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b\",\"error\":\"Requested contract address 0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b is not deployed.\\n\",\"selector\":\"0x15d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad\"},\"transaction_index\":0}","timestamp":"2026-05-20T15:34:54.406Z","elapsedMs":2449} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","STRK Balance":"800 STRK"},"timestamp":"2026-05-20T16:15:38.476Z","elapsedMs":1284} +{"stepName":"Validate Transfer Condition","status":"failed","message":"Account contract is not deployed on this network","data":{"Account Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","Action Required":"Deploy the account via Argent X / Braavos, then re-run the agent","Argent X":"https://www.argent.xyz/argent-x/ → import key → Deploy account","Braavos":"https://braavos.app/ → import key → Deploy account","Note":"You need a small amount of ETH (or STRK) at this address for gas"},"timestamp":"2026-05-20T16:15:39.191Z","elapsedMs":1999} +{"stepName":"Validate Transfer Condition","status":"failed","message":"Unhandled error — aborting pipeline","error":"Account not deployed — see action required above","timestamp":"2026-05-20T16:15:39.192Z","elapsedMs":2000} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","STRK Balance":"800 STRK"},"timestamp":"2026-05-21T09:39:03.428Z","elapsedMs":1070} +{"stepName":"Validate Transfer Condition","status":"failed","message":"Account contract is not deployed on this network","data":{"Account Address":"0x06a0fc0b6baab6b2c1bbb445e686d594467d07c731b92acdc8f676d21e56c13b","Action Required":"Deploy the account via Argent X / Braavos, then re-run the agent","Argent X":"https://www.argent.xyz/argent-x/ → import key → Deploy account","Braavos":"https://braavos.app/ → import key → Deploy account","Note":"You need a small amount of ETH (or STRK) at this address for gas"},"timestamp":"2026-05-21T09:39:03.710Z","elapsedMs":1352} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x4a5650ed9ee3ff2d181b22ce905b91536bde89bdc4bec400f4cf21d0374a7b8","STRK Balance":"99.98219391888276544 STRK"},"timestamp":"2026-05-21T09:45:25.883Z","elapsedMs":985} +{"stepName":"Validate Transfer Condition","status":"success","message":"Condition MET — balance > threshold","data":{"Current Balance":"99.98219391888276544 STRK","Threshold":"0.005 STRK","Transfer Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Decision":"PROCEED — submitting transfer"},"timestamp":"2026-05-21T09:45:26.469Z","elapsedMs":1571} +{"stepName":"Execute Token Transfer","status":"success","message":"Transfer confirmed on-chain","data":{"Token":"STRK","Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Tx Hash":"0x736635af745a8b6f8f3cf4d2adb0b06200a40b5c408b1990302d3db9288a6e5","Explorer":"https://starkscan.co/tx/0x736635af745a8b6f8f3cf4d2adb0b06200a40b5c408b1990302d3db9288a6e5"},"timestamp":"2026-05-21T09:45:45.762Z","elapsedMs":20864} +{"stepName":"Call Downstream Agent","status":"success","message":"Local downstream function invoked","data":{"Function":"rebalancePortfolio (stub)","Tx Hash":"0x736635af745a8b6f8f3cf4d2adb0b06200a40b5c408b1990302d3db9288a6e5"},"timestamp":"2026-05-21T09:45:45.765Z","elapsedMs":20867} +{"stepName":"Post-Transfer Verification Agent","status":"success","message":"Verification passed — balance reduced as expected","data":{"Pre-Transfer STRK":"99.98219391888276544 STRK","Post-Transfer STRK":"99.96709262804487856 STRK","Expected (approx) STRK":"99.98119391888276544 STRK","STRK Balance":"99.96709262804487856 STRK","Balance Decreased":"YES ✓","Verification Status":"PASSED"},"timestamp":"2026-05-21T09:45:46.392Z","elapsedMs":21494} +{"stepName":"Alert Check","status":"success","message":"Balance above alert threshold — no alert needed","data":{"Balance":"99.96709262804487856 STRK","Alert Threshold":"0.002 STRK"},"timestamp":"2026-05-21T09:45:46.393Z","elapsedMs":21495} +{"stepName":"Log Execution Result","status":"success","message":"Workflow execution recorded","data":{"Transfer Executed":"YES","Transaction Hash":"0x736635af745a8b6f8f3cf4d2adb0b06200a40b5c408b1990302d3db9288a6e5","Amount Sent":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Initial STRK":"99.98219391888276544 STRK","Final STRK":"99.96709262804487856 STRK","Alert Triggered":"NO"},"timestamp":"2026-05-21T09:45:46.394Z","elapsedMs":21496} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x4a5650ed9ee3ff2d181b22ce905b91536bde89bdc4bec400f4cf21d0374a7b8","STRK Balance":"99.96709262804487856 STRK"},"timestamp":"2026-05-21T11:21:06.978Z","elapsedMs":2311} +{"stepName":"Validate Transfer Condition","status":"success","message":"Condition MET — balance > threshold","data":{"Current Balance":"99.96709262804487856 STRK","Threshold":"0.005 STRK","Transfer Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Decision":"PROCEED — submitting transfer"},"timestamp":"2026-05-21T11:21:07.598Z","elapsedMs":2931} +{"stepName":"Execute Token Transfer","status":"success","message":"Transfer confirmed on-chain","data":{"Token":"STRK","Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Tx Hash":"0x69f57f36ed3c563964190c51b84b6ecf52c6eb434e40863014c37a18beab0ff","Explorer":"https://starkscan.co/tx/0x69f57f36ed3c563964190c51b84b6ecf52c6eb434e40863014c37a18beab0ff"},"timestamp":"2026-05-21T11:21:27.066Z","elapsedMs":22399} +{"stepName":"Call Downstream Agent","status":"success","message":"Local downstream function invoked","data":{"Function":"rebalancePortfolio (stub)","Tx Hash":"0x69f57f36ed3c563964190c51b84b6ecf52c6eb434e40863014c37a18beab0ff"},"timestamp":"2026-05-21T11:21:27.069Z","elapsedMs":22402} +{"stepName":"Post-Transfer Verification Agent","status":"success","message":"Verification passed — balance reduced as expected","data":{"Pre-Transfer STRK":"99.96709262804487856 STRK","Post-Transfer STRK":"99.955259468673107232 STRK","Expected (approx) STRK":"99.96609262804487856 STRK","STRK Balance":"99.955259468673107232 STRK","Balance Decreased":"YES ✓","Verification Status":"PASSED"},"timestamp":"2026-05-21T11:21:28.247Z","elapsedMs":23580} +{"stepName":"Alert Check","status":"success","message":"Balance above alert threshold — no alert needed","data":{"Balance":"99.955259468673107232 STRK","Alert Threshold":"0.002 STRK"},"timestamp":"2026-05-21T11:21:28.249Z","elapsedMs":23582} +{"stepName":"Log Execution Result","status":"success","message":"Workflow execution recorded","data":{"Transfer Executed":"YES","Transaction Hash":"0x69f57f36ed3c563964190c51b84b6ecf52c6eb434e40863014c37a18beab0ff","Amount Sent":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Initial STRK":"99.96709262804487856 STRK","Final STRK":"99.955259468673107232 STRK","Alert Triggered":"NO"},"timestamp":"2026-05-21T11:21:28.249Z","elapsedMs":23582} +{"stepName":"Fetch Wallet Balances","status":"success","message":"Retrieved 1 token balance(s)","data":{"Wallet Address":"0x4a5650ed9ee3ff2d181b22ce905b91536bde89bdc4bec400f4cf21d0374a7b8","STRK Balance":"99.955259468673107232 STRK"},"timestamp":"2026-05-21T12:11:15.886Z","elapsedMs":1074} +{"stepName":"Validate Transfer Condition","status":"success","message":"Condition MET — balance > threshold","data":{"Current Balance":"99.955259468673107232 STRK","Threshold":"0.005 STRK","Transfer Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Decision":"PROCEED — submitting transfer"},"timestamp":"2026-05-21T12:11:16.597Z","elapsedMs":1785} +{"stepName":"Execute Token Transfer","status":"success","message":"Transfer confirmed on-chain","data":{"Token":"STRK","Amount":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Tx Hash":"0x1bef221ca71309d6f3b03f0a0f8e5ebff3a6e89e3c52154f2e3e29901725c98","Explorer":"https://starkscan.co/tx/0x1bef221ca71309d6f3b03f0a0f8e5ebff3a6e89e3c52154f2e3e29901725c98"},"timestamp":"2026-05-21T12:11:36.226Z","elapsedMs":21414} +{"stepName":"Call Downstream Agent","status":"success","message":"Local downstream function invoked","data":{"Function":"rebalancePortfolio (stub)","Tx Hash":"0x1bef221ca71309d6f3b03f0a0f8e5ebff3a6e89e3c52154f2e3e29901725c98"},"timestamp":"2026-05-21T12:11:36.228Z","elapsedMs":21416} +{"stepName":"Post-Transfer Verification Agent","status":"success","message":"Verification passed — balance reduced as expected","data":{"Pre-Transfer STRK":"99.955259468673107232 STRK","Post-Transfer STRK":"99.943422457616498144 STRK","Expected (approx) STRK":"99.954259468673107232 STRK","STRK Balance":"99.943422457616498144 STRK","Balance Decreased":"YES ✓","Verification Status":"PASSED"},"timestamp":"2026-05-21T12:11:36.980Z","elapsedMs":22168} +{"stepName":"Alert Check","status":"success","message":"Balance above alert threshold — no alert needed","data":{"Balance":"99.943422457616498144 STRK","Alert Threshold":"0.002 STRK"},"timestamp":"2026-05-21T12:11:36.981Z","elapsedMs":22169} +{"stepName":"Log Execution Result","status":"success","message":"Workflow execution recorded","data":{"Transfer Executed":"YES","Transaction Hash":"0x1bef221ca71309d6f3b03f0a0f8e5ebff3a6e89e3c52154f2e3e29901725c98","Amount Sent":"0.001 STRK","Recipient":"0x00056e7E799E0fcFf5BE0d452eF9cC7CE89d876De4AdED207d3f439aF172DafD","Initial STRK":"99.955259468673107232 STRK","Final STRK":"99.943422457616498144 STRK","Alert Triggered":"NO"},"timestamp":"2026-05-21T12:11:36.981Z","elapsedMs":22169} diff --git a/starknet_agent/package-lock.json b/starknet_agent/package-lock.json new file mode 100644 index 0000000..33fee18 --- /dev/null +++ b/starknet_agent/package-lock.json @@ -0,0 +1,973 @@ +{ + "name": "my-starknet-agent", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-starknet-agent", + "version": "0.1.0", + "dependencies": { + "dotenv": "^16.4.7", + "starknet": "^8.9.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.0.0", + "typescript": "^5.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@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/@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/@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/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/@starknet-io/starknet-types-08": { + "name": "@starknet-io/types-js", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.8.4.tgz", + "integrity": "sha512-0RZ3TZHcLsUTQaq1JhDSCM8chnzO4/XNsSCozwDET64JK5bjFDIf2ZUkta+tl5Nlbf4usoU7uZiDI/Q57kt2SQ==", + "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/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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/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": "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/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/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/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/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "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.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "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/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/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/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/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/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/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "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/starknet": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/starknet/-/starknet-8.9.2.tgz", + "integrity": "sha512-+dp+o2w67fV6JyVOVkYeM1Ec71aORHc/JrF4VHLlfeGee0nLilooCQLE2u6hUcSGQG2x2/fvzkxYpIN+k1JBvA==", + "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/starknet-types-08": "npm:@starknet-io/types-js@~0.8.4", + "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@~0.9.1", + "abi-wan-kanabi": "2.2.4", + "lossless-json": "^4.2.0", + "pako": "^2.0.4", + "ts-mixer": "^6.0.3" + }, + "engines": { + "node": ">=22" + } + }, + "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/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/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/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" + } + } + } +} diff --git a/starknet_agent/package.json b/starknet_agent/package.json new file mode 100644 index 0000000..434e7e6 --- /dev/null +++ b/starknet_agent/package.json @@ -0,0 +1,25 @@ +{ + "name": "my-starknet-agent", + "version": "0.1.0", + "description": "my-starknet-agent - Starknet AI Agent", + "private": true, + "type": "module", + "main": "src/index.ts", + "scripts": { + "start": "tsx src/index.ts", + "dev": "tsx watch src/index.ts", + "build": "tsc" + }, + "dependencies": { + "dotenv": "^16.4.7", + "starknet": "^8.9.1" + }, + "devDependencies": { + "tsx": "^4.0.0", + "typescript": "^5.9.0", + "@types/node": "^22.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/starknet_agent/src/agent.ts b/starknet_agent/src/agent.ts new file mode 100644 index 0000000..ddbf5d8 --- /dev/null +++ b/starknet_agent/src/agent.ts @@ -0,0 +1,172 @@ +import { Account, RpcProvider, Contract, CallData, cairo } from "starknet"; +import { formatAmount } from "./utils.js"; +import { TokenBalance } from "./types.js"; + +interface TokenMeta { + address: string; + decimals: number; +} + +export const KNOWN_TOKENS: Record = { + ETH: { + address: + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + decimals: 18, + }, + STRK: { + address: + "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + decimals: 18, + }, + USDC: { + address: + "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8", + decimals: 6, + }, + USDT: { + address: + "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8", + decimals: 6, + }, +}; + +export function resolveTokenAddress(token: string): string { + return KNOWN_TOKENS[token.toUpperCase()]?.address ?? token; +} + +export function resolveTokenDecimals(token: string): number { + return KNOWN_TOKENS[token.toUpperCase()]?.decimals ?? 18; +} + +const ERC20_ABI = [ + { + type: "interface", + name: "IERC20", + items: [ + { + type: "function", + name: "balance_of", + inputs: [ + { + name: "account", + type: "core::starknet::contract_address::ContractAddress", + }, + ], + outputs: [{ type: "core::integer::u256" }], + state_mutability: "view", + }, + { + type: "function", + name: "decimals", + inputs: [], + outputs: [{ type: "core::integer::u8" }], + state_mutability: "view", + }, + { + type: "function", + name: "transfer", + inputs: [ + { + name: "recipient", + type: "core::starknet::contract_address::ContractAddress", + }, + { name: "amount", type: "core::integer::u256" }, + ], + outputs: [{ type: "core::bool" }], + state_mutability: "external", + }, + ], + }, +] as const; + +export class StarknetAgent { + private readonly provider: RpcProvider; + private readonly account: Account; + + constructor(rpcUrl: string, accountAddress: string, privateKey: string) { + this.provider = new RpcProvider({ nodeUrl: rpcUrl }); + this.account = new Account({ + provider: this.provider, + address: accountAddress, + signer: privateKey, + cairoVersion: "1", + }); + } + + get address(): string { + return this.account.address; + } + + async isDeployed(): Promise { + try { + await this.provider.getClassAt(this.account.address); + return true; + } catch { + return false; + } + } + + async getBalance(token: string): Promise { + const tokenAddress = resolveTokenAddress(token); + const contract = new Contract({ + abi: ERC20_ABI as any, + address: tokenAddress, + providerOrAccount: this.provider, + }); + + const [rawBalance, rawDecimals] = await Promise.all([ + contract.balance_of(this.account.address) as Promise, + contract.decimals() as Promise, + ]); + + const balance = + typeof rawBalance === "bigint" ? rawBalance : BigInt(String(rawBalance)); + const decimals = Number(rawDecimals); + + return { + token: token.toUpperCase(), + address: tokenAddress, + balance, + decimals, + formatted: formatAmount(balance, decimals), + }; + } + + async getBalances(tokens: string[]): Promise> { + const results = await Promise.all(tokens.map((t) => this.getBalance(t))); + return new Map(results.map((r) => [r.token, r])); + } + + async transfer( + token: string, + recipient: string, + amount: bigint, + ): Promise { + const tokenAddress = resolveTokenAddress(token); + const call = { + contractAddress: tokenAddress, + entrypoint: "transfer", + calldata: CallData.compile({ recipient, amount: cairo.uint256(amount) }), + }; + + const response = await this.account.execute(call); + await this.provider.waitForTransaction(response.transaction_hash); + return response.transaction_hash; + } + + async estimateTransferFee( + token: string, + recipient: string, + amount: bigint, + ): Promise { + const tokenAddress = resolveTokenAddress(token); + const call = { + contractAddress: tokenAddress, + entrypoint: "transfer", + calldata: CallData.compile({ recipient, amount: cairo.uint256(amount) }), + }; + + const estimate = await this.account.estimateInvokeFee(call); + return estimate.overall_fee; + } +} diff --git a/starknet_agent/src/index.ts b/starknet_agent/src/index.ts new file mode 100644 index 0000000..41c3cd1 --- /dev/null +++ b/starknet_agent/src/index.ts @@ -0,0 +1,88 @@ +import dotenv from "dotenv"; +import { fileURLToPath } from "url"; +import { dirname, join, resolve } from "path"; + +import { StarknetAgent } from "./agent.js"; +import { ExecutionLogger } from "./logger.js"; +import { WorkflowPipeline } from "./workflow.js"; +import { TransferConfig } from "./types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: join(__dirname, "..", ".env") }); + +function loadConfig(): TransferConfig { + const required = [ + "STARKNET_RPC_URL", + "STARKNET_ACCOUNT_ADDRESS", + "STARKNET_PRIVATE_KEY", + "TRANSFER_RECIPIENT", + ] as const; + + const missing = required.filter((k) => !process.env[k]); + if (missing.length > 0) { + console.error("\nMissing required environment variables:"); + missing.forEach((k) => console.error(` ✗ ${k}`)); + console.error("\nCopy .env.example → .env and fill in the values.\n"); + process.exit(1); + } + + return { + rpcUrl: process.env.STARKNET_RPC_URL!, + accountAddress: process.env.STARKNET_ACCOUNT_ADDRESS!, + privateKey: process.env.STARKNET_PRIVATE_KEY!, + transferToken: process.env.TRANSFER_TOKEN ?? "ETH", + transferRecipient: process.env.TRANSFER_RECIPIENT!, + transferAmount: process.env.TRANSFER_AMOUNT ?? "0.001", + thresholdAmount: process.env.TRANSFER_THRESHOLD ?? "0.005", + secondaryToken: process.env.SECONDARY_TOKEN ?? "STRK", + alertThreshold: process.env.ALERT_THRESHOLD ?? "0.002", + alertWebhookUrl: process.env.ALERT_WEBHOOK_URL, + downstreamAgentUrl: process.env.DOWNSTREAM_AGENT_URL, + pollIntervalMs: Number(process.env.POLL_INTERVAL_MS ?? "0"), + }; +} + +async function main(): Promise { + const config = loadConfig(); + const logFile = resolve(process.cwd(), "agent-log.jsonl"); + + const agent = new StarknetAgent( + config.rpcUrl, + config.accountAddress, + config.privateKey, + ); + const pipeline = new WorkflowPipeline(); + + async function runOnce(): Promise { + const logger = new ExecutionLogger(logFile); + await pipeline.run(agent, config, logger); + return logger.hasFailures(); + } + + const hadFailures = await runOnce(); + + if (config.pollIntervalMs <= 0) { + process.exit(hadFailures ? 1 : 0); + return; + } + const intervalSec = (config.pollIntervalMs / 1000).toFixed(0); + console.log(`\nPolling every ${intervalSec}s. Press Ctrl+C to stop.\n`); + + const handle = setInterval(async () => { + await runOnce().catch((err) => console.error("Run error:", err)); + }, config.pollIntervalMs); + + const shutdown = (): void => { + clearInterval(handle); + console.log("\nAgent stopped."); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((err) => { + console.error("Fatal:", err); + process.exit(1); +}); diff --git a/starknet_agent/src/logger.ts b/starknet_agent/src/logger.ts new file mode 100644 index 0000000..f055e94 --- /dev/null +++ b/starknet_agent/src/logger.ts @@ -0,0 +1,138 @@ +import { appendFileSync } from "fs"; +import { resolve } from "path"; +import { StepResult, StepStatus } from "./types.js"; + +const C = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + yellow: "\x1b[33m", + red: "\x1b[31m", + cyan: "\x1b[36m", + blue: "\x1b[34m", + white: "\x1b[37m", +} as const; + +const ICONS: Record = { + pending: "⏳", + running: "🔄", + success: "✅", + skipped: "⏭ ", + failed: "❌", +}; + +const STEP_COLOR: Record = { + pending: C.dim, + running: C.cyan, + success: C.green, + skipped: C.yellow, + failed: C.red, +}; + +function ts(): string { + return new Date().toISOString().replace("T", " ").slice(0, 23); +} + +export class ExecutionLogger { + private results: StepResult[] = []; + private readonly startedAt = Date.now(); + private readonly logFile: string; + + constructor(logFile = resolve(process.cwd(), "agent-log.jsonl")) { + this.logFile = logFile; + } + + step( + name: string, + status: StepStatus, + message: string, + data?: Record, + error?: string, + ): StepResult { + const result: StepResult = { + stepName: name, + status, + message, + data, + error, + timestamp: new Date(), + elapsedMs: Date.now() - this.startedAt, + }; + this.results.push(result); + + appendFileSync( + this.logFile, + JSON.stringify({ ...result, timestamp: result.timestamp.toISOString() }) + + "\n", + ); + + const col = STEP_COLOR[status]; + console.log( + `\n${C.dim}[${ts()}]${C.reset} ${ICONS[status]} ${col}${C.bold}${name}${C.reset}`, + ); + console.log(` ${col}${message}${C.reset}`); + + if (data) { + for (const [k, v] of Object.entries(data)) { + console.log( + ` ${C.dim}${k.padEnd(25)}${C.reset}${C.white}${v}${C.reset}`, + ); + } + } + if (error) { + console.log(` ${C.red}⚠ ${error}${C.reset}`); + } + + return result; + } + + info(message: string): void { + console.log(`${C.dim}[${ts()}]${C.reset} ${C.blue}ℹ ${C.reset} ${message}`); + } + + separator(title?: string): void { + const line = "─".repeat(58); + if (title) { + console.log(`\n${C.cyan}${line}${C.reset}`); + console.log(`${C.cyan}${C.bold} ${title}${C.reset}`); + console.log(`${C.cyan}${line}${C.reset}`); + } else { + console.log(`\n${C.dim}${line}${C.reset}`); + } + } + + printReport(): void { + const totalMs = Date.now() - this.startedAt; + const success = this.results.filter((r) => r.status === "success").length; + const skipped = this.results.filter((r) => r.status === "skipped").length; + const failed = this.results.filter((r) => r.status === "failed").length; + + this.separator("EXECUTION REPORT"); + console.log(` ${C.bold}Total Steps ${C.reset}: ${this.results.length}`); + console.log(` ${C.green}Succeeded ${C.reset}: ${success}`); + console.log(` ${C.yellow}Skipped ${C.reset}: ${skipped}`); + console.log(` ${C.red}Failed ${C.reset}: ${failed}`); + console.log( + ` ${C.bold}Duration ${C.reset}: ${(totalMs / 1000).toFixed(2)}s`, + ); + console.log(); + + for (const r of this.results) { + const col = STEP_COLOR[r.status]; + console.log( + ` ${ICONS[r.status]} ${col}${r.stepName.padEnd(38)}${C.reset}` + + `${C.dim}+${r.elapsedMs}ms${C.reset}`, + ); + } + console.log(); + } + + getResults(): readonly StepResult[] { + return this.results; + } + + hasFailures(): boolean { + return this.results.some((r) => r.status === "failed"); + } +} diff --git a/starknet_agent/src/types.ts b/starknet_agent/src/types.ts new file mode 100644 index 0000000..fcaea98 --- /dev/null +++ b/starknet_agent/src/types.ts @@ -0,0 +1,67 @@ +/** + * Shared type definitions for the autonomous transfer agent workflow. + */ + +export type StepStatus = + | "pending" + | "running" + | "success" + | "skipped" + | "failed"; + +export type AlertChannel = "console" | "webhook"; + +export interface StepResult { + stepName: string; + status: StepStatus; + message: string; + data?: Record; + error?: string; + timestamp: Date; + elapsedMs: number; +} + +export interface TokenBalance { + token: string; + address: string; + balance: bigint; + decimals: number; + formatted: string; +} + +export interface TransferConfig { + rpcUrl: string; + accountAddress: string; + privateKey: string; + /** Token symbol (ETH / STRK) or contract address to transfer */ + transferToken: string; + /** Recipient wallet address */ + transferRecipient: string; + /** Human-readable amount to send, e.g. "0.001" */ + transferAmount: string; + /** Transfer only when balance exceeds this amount, e.g. "0.005" */ + thresholdAmount: string; + /** Secondary token whose balance is verified post-transfer */ + secondaryToken: string; + /** Human-readable balance that triggers an alert, e.g. "0.002" */ + alertThreshold: string; + /** Optional webhook URL for low-balance alerts (Slack, Discord, PagerDuty …) */ + alertWebhookUrl?: string; + /** Optional A2A endpoint — POST is sent here after a confirmed transfer */ + downstreamAgentUrl?: string; + /** Polling interval in ms; 0 means run once and exit */ + pollIntervalMs: number; +} + +export interface WorkflowState { + /** Balances fetched in step 1, keyed by uppercase token symbol */ + balances: Map; + /** Set to true when balance > threshold and transfer should proceed */ + conditionMet: boolean; + /** Transaction hash returned by the transfer (step 3) */ + transferTxHash?: string; + /** Post-transfer balance fetched by the verification agent (step 5) */ + postTransferBalance?: TokenBalance; + /** Set to true when balance dropped below alertThreshold and an alert fired */ + alertTriggered?: boolean; +} diff --git a/starknet_agent/src/utils.ts b/starknet_agent/src/utils.ts new file mode 100644 index 0000000..f7fbe55 --- /dev/null +++ b/starknet_agent/src/utils.ts @@ -0,0 +1,22 @@ +export function formatAmount(raw: bigint, decimals: number): string { + const str = raw.toString(); + if (decimals === 0) return str; + const padded = str.padStart(decimals + 1, "0"); + const whole = padded.slice(0, -decimals); + const frac = padded.slice(-decimals).replace(/0+$/, ""); + return frac ? `${whole}.${frac}` : whole; +} + +export function parseAmount(amount: string, decimals: number): bigint { + const [whole, frac = ""] = amount.split("."); + const paddedFrac = frac.padEnd(decimals, "0").slice(0, decimals); + return BigInt(whole + paddedFrac); +} + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function isValidAddress(address: string): boolean { + return /^0x[0-9a-fA-F]{1,64}$/.test(address); +} diff --git a/starknet_agent/src/workflow.ts b/starknet_agent/src/workflow.ts new file mode 100644 index 0000000..bfa4645 --- /dev/null +++ b/starknet_agent/src/workflow.ts @@ -0,0 +1,457 @@ +import { StarknetAgent, resolveTokenDecimals } from "./agent.js"; +import { ExecutionLogger } from "./logger.js"; +import { TransferConfig, WorkflowState } from "./types.js"; +import { parseAmount, formatAmount, isValidAddress } from "./utils.js"; + +class AlreadyLoggedError extends Error {} + +interface WorkflowContext { + agent: StarknetAgent; + config: TransferConfig; + state: WorkflowState; + logger: ExecutionLogger; +} + +interface WorkflowStep { + readonly name: string; + run(ctx: WorkflowContext): Promise; +} + +class FetchBalancesStep implements WorkflowStep { + readonly name = "Fetch Wallet Balances"; + + async run({ agent, config, state, logger }: WorkflowContext): Promise { + const tokens = [ + ...new Set([ + config.transferToken.toUpperCase(), + config.secondaryToken.toUpperCase(), + ]), + ]; + + const balances = await agent.getBalances(tokens); + state.balances = balances; + + const data: Record = { + "Wallet Address": agent.address, + }; + for (const [sym, bal] of balances) { + data[`${sym} Balance`] = `${bal.formatted} ${sym}`; + } + + logger.step( + this.name, + "success", + `Retrieved ${tokens.length} token balance(s)`, + data, + ); + } +} + +class ValidateConditionStep implements WorkflowStep { + readonly name = "Validate Transfer Condition"; + + async run({ agent, config, state, logger }: WorkflowContext): Promise { + const deployed = await agent.isDeployed(); + if (!deployed) { + logger.step( + this.name, + "failed", + "Account contract is not deployed on this network", + { + "Account Address": agent.address, + "Action Required": + "Deploy the account via Argent X / Braavos, then re-run the agent", + "Argent X": + "https://www.argent.xyz/argent-x/ → import key → Deploy account", + Braavos: "https://braavos.app/ → import key → Deploy account", + Note: "You need a small amount of ETH (or STRK) at this address for gas", + }, + ); + throw new AlreadyLoggedError( + "Account not deployed — see action required above", + ); + } + + const sym = config.transferToken.toUpperCase(); + const tokenBal = state.balances.get(sym); + + if (!tokenBal) { + logger.step(this.name, "failed", `Balance not available for ${sym}`); + throw new AlreadyLoggedError(`Balance unavailable for token: ${sym}`); + } + + if (!isValidAddress(config.transferRecipient)) { + logger.step(this.name, "failed", "Invalid recipient address format", { + Recipient: config.transferRecipient, + }); + throw new AlreadyLoggedError( + `Invalid recipient address: ${config.transferRecipient}`, + ); + } + + const decimals = resolveTokenDecimals(config.transferToken); + const threshold = parseAmount(config.thresholdAmount, decimals); + const transferAmt = parseAmount(config.transferAmount, decimals); + + state.conditionMet = tokenBal.balance > threshold; + + if (!state.conditionMet) { + logger.step( + this.name, + "skipped", + `Condition NOT met — balance ≤ threshold`, + { + "Current Balance": `${tokenBal.formatted} ${sym}`, + Threshold: `${config.thresholdAmount} ${sym}`, + "Transfer Amount": `${config.transferAmount} ${sym}`, + Decision: "SKIP — threshold not reached", + }, + ); + return; + } + + if (tokenBal.balance < transferAmt) { + logger.step( + this.name, + "failed", + "Insufficient balance to cover transfer amount", + { + "Current Balance": `${tokenBal.formatted} ${sym}`, + "Required Amount": `${config.transferAmount} ${sym}`, + }, + ); + throw new AlreadyLoggedError( + "Insufficient balance: cannot cover the requested transfer amount", + ); + } + + logger.step(this.name, "success", `Condition MET — balance > threshold`, { + "Current Balance": `${tokenBal.formatted} ${sym}`, + Threshold: `${config.thresholdAmount} ${sym}`, + "Transfer Amount": `${config.transferAmount} ${sym}`, + Recipient: config.transferRecipient, + Decision: "PROCEED — submitting transfer", + }); + } +} + +class ExecuteTransferStep implements WorkflowStep { + readonly name = "Execute Token Transfer"; + + async run({ agent, config, state, logger }: WorkflowContext): Promise { + if (!state.conditionMet) { + logger.step( + this.name, + "skipped", + "Transfer skipped — condition was not met", + ); + return; + } + + const sym = config.transferToken.toUpperCase(); + const decimals = resolveTokenDecimals(config.transferToken); + const amount = parseAmount(config.transferAmount, decimals); + + logger.info( + `Submitting ${config.transferAmount} ${sym} → ${config.transferRecipient} …`, + ); + + const txHash = await agent.transfer( + config.transferToken, + config.transferRecipient, + amount, + ); + + state.transferTxHash = txHash; + + logger.step(this.name, "success", "Transfer confirmed on-chain", { + Token: sym, + Amount: `${config.transferAmount} ${sym}`, + Recipient: config.transferRecipient, + "Tx Hash": txHash, + Explorer: `https://starkscan.co/tx/${txHash}`, + }); + } +} + +class DownstreamAgentStep implements WorkflowStep { + readonly name = "Call Downstream Agent"; + + async run({ agent, config, state, logger }: WorkflowContext): Promise { + if (!state.conditionMet || !state.transferTxHash) { + logger.step( + this.name, + "skipped", + "No transfer executed — downstream call skipped", + ); + return; + } + + const payload = { + event: "transfer_complete", + txHash: state.transferTxHash, + token: config.transferToken.toUpperCase(), + amount: config.transferAmount, + from: agent.address, + to: config.transferRecipient, + timestamp: new Date().toISOString(), + }; + + if (config.downstreamAgentUrl) { + const res = await fetch(config.downstreamAgentUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + throw new Error( + `Downstream agent returned HTTP ${res.status}: ${await res.text()}`, + ); + } + + logger.step(this.name, "success", "A2A call delivered", { + Endpoint: config.downstreamAgentUrl, + "HTTP Status": String(res.status), + "Tx Hash": state.transferTxHash, + }); + } else { + logger.info( + "DOWNSTREAM_AGENT_URL not set — invoking local rebalance stub …", + ); + await rebalancePortfolio(payload, logger); + logger.step(this.name, "success", "Local downstream function invoked", { + Function: "rebalancePortfolio (stub)", + "Tx Hash": state.transferTxHash, + }); + } + } +} + +async function rebalancePortfolio( + context: Record, + logger: ExecutionLogger, +): Promise { + logger.info(`rebalancePortfolio stub — payload: ${JSON.stringify(context)}`); +} + +class PostTransferAgentStep implements WorkflowStep { + readonly name = "Post-Transfer Verification Agent"; + + async run({ agent, config, state, logger }: WorkflowContext): Promise { + if (!state.conditionMet || !state.transferTxHash) { + logger.step( + this.name, + "skipped", + "No transfer executed — verification not needed", + ); + return; + } + + logger.info("Secondary agent: verifying on-chain state after transfer …"); + + const sym = config.transferToken.toUpperCase(); + const decimals = resolveTokenDecimals(config.transferToken); + + const [updatedPrimary, updatedSecondary] = await Promise.all([ + agent.getBalance(config.transferToken), + agent.getBalance(config.secondaryToken), + ]); + + state.postTransferBalance = updatedPrimary; + + const prevBal = state.balances.get(sym)!; + const transferAmt = parseAmount(config.transferAmount, decimals); + const expectedBal = prevBal.balance - transferAmt; + const balDecreased = updatedPrimary.balance <= prevBal.balance; + + const secSym = config.secondaryToken.toUpperCase(); + + logger.step( + this.name, + balDecreased ? "success" : "failed", + balDecreased + ? "Verification passed — balance reduced as expected" + : "Verification FAILED — balance anomaly detected", + { + [`Pre-Transfer ${sym}`]: `${prevBal.formatted} ${sym}`, + [`Post-Transfer ${sym}`]: `${updatedPrimary.formatted} ${sym}`, + [`Expected (approx) ${sym}`]: `${formatAmount(expectedBal > 0n ? expectedBal : 0n, decimals)} ${sym}`, + [`${secSym} Balance`]: `${updatedSecondary.formatted} ${secSym}`, + "Balance Decreased": balDecreased ? "YES ✓" : "NO ✗", + "Verification Status": balDecreased ? "PASSED" : "ANOMALY", + }, + ); + + if (!balDecreased) { + throw new AlreadyLoggedError( + "Post-transfer balance anomaly: balance did not decrease after confirmed transfer", + ); + } + } +} + +class AlertCheckStep implements WorkflowStep { + readonly name = "Alert Check"; + + async run({ config, state, logger }: WorkflowContext): Promise { + const sym = config.transferToken.toUpperCase(); + const decimals = resolveTokenDecimals(config.transferToken); + const alertThreshold = parseAmount(config.alertThreshold, decimals); + + const balance = state.postTransferBalance ?? state.balances.get(sym); + if (!balance) { + logger.step( + this.name, + "skipped", + "No balance data available — alert check skipped", + ); + return; + } + + if (balance.balance >= alertThreshold) { + logger.step( + this.name, + "success", + "Balance above alert threshold — no alert needed", + { + Balance: `${balance.formatted} ${sym}`, + "Alert Threshold": `${config.alertThreshold} ${sym}`, + }, + ); + return; + } + + state.alertTriggered = true; + + const alertPayload = { + text: `ALERT: ${sym} balance (${balance.formatted}) dropped below threshold (${config.alertThreshold})`, + token: sym, + balance: balance.formatted, + threshold: config.alertThreshold, + timestamp: new Date().toISOString(), + }; + + if (config.alertWebhookUrl) { + const res = await fetch(config.alertWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(alertPayload), + }); + + logger.step( + this.name, + res.ok ? "success" : "failed", + res.ok + ? "Alert webhook delivered" + : `Webhook delivery failed (HTTP ${res.status})`, + { + Balance: `${balance.formatted} ${sym}`, + "Alert Threshold": `${config.alertThreshold} ${sym}`, + Webhook: config.alertWebhookUrl, + "HTTP Status": String(res.status), + }, + ); + } else { + logger.step( + this.name, + "success", + `LOW BALANCE — ${sym} (${balance.formatted}) is below alert threshold (${config.alertThreshold})`, + { + Balance: `${balance.formatted} ${sym}`, + "Alert Threshold": `${config.alertThreshold} ${sym}`, + Channel: "console (set ALERT_WEBHOOK_URL to enable webhook)", + }, + ); + } + } +} + +class LogExecutionResultStep implements WorkflowStep { + readonly name = "Log Execution Result"; + + async run({ config, state, logger }: WorkflowContext): Promise { + const sym = config.transferToken.toUpperCase(); + + const summary: Record = { + "Transfer Executed": state.conditionMet + ? "YES" + : "NO (threshold not met)", + }; + + if (state.transferTxHash) { + summary["Transaction Hash"] = state.transferTxHash; + summary["Amount Sent"] = `${config.transferAmount} ${sym}`; + summary["Recipient"] = config.transferRecipient; + } + + for (const [token, bal] of state.balances) { + summary[`Initial ${token}`] = `${bal.formatted} ${token}`; + } + + if (state.postTransferBalance) { + summary[`Final ${sym}`] = `${state.postTransferBalance.formatted} ${sym}`; + } + + summary["Alert Triggered"] = state.alertTriggered ? "YES ⚠" : "NO"; + + logger.step(this.name, "success", "Workflow execution recorded", summary); + } +} + +export class WorkflowPipeline { + private readonly steps: WorkflowStep[] = [ + new FetchBalancesStep(), + new ValidateConditionStep(), + new ExecuteTransferStep(), + new DownstreamAgentStep(), + new PostTransferAgentStep(), + new AlertCheckStep(), + new LogExecutionResultStep(), + ]; + + async run( + agent: StarknetAgent, + config: TransferConfig, + logger: ExecutionLogger, + ): Promise { + const ctx: WorkflowContext = { + agent, + config, + state: { balances: new Map(), conditionMet: false }, + logger, + }; + + logger.separator("AUTONOMOUS TRANSFER AGENT — WORKFLOW START"); + logger.info(`Agent Address : ${agent.address}`); + logger.info(`Transfer Token : ${config.transferToken.toUpperCase()}`); + logger.info( + `Threshold : ${config.thresholdAmount} ${config.transferToken.toUpperCase()}`, + ); + logger.info( + `Transfer Amount: ${config.transferAmount} ${config.transferToken.toUpperCase()}`, + ); + logger.info(`Recipient : ${config.transferRecipient}`); + logger.info(`Secondary Token: ${config.secondaryToken.toUpperCase()}`); + + for (const step of this.steps) { + try { + await step.run(ctx); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (!(err instanceof AlreadyLoggedError)) { + logger.step( + step.name, + "failed", + "Unhandled error — aborting pipeline", + undefined, + msg, + ); + } + break; + } + } + + logger.printReport(); + } +} diff --git a/starknet_agent/tsconfig.json b/starknet_agent/tsconfig.json new file mode 100644 index 0000000..80e4b2a --- /dev/null +++ b/starknet_agent/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": [ + "ES2022" + ], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "declaration": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/starknet_contracts/Scarb.lock b/starknet_contracts/Scarb.lock index 6c8c64a..2733aea 100644 --- a/starknet_contracts/Scarb.lock +++ b/starknet_contracts/Scarb.lock @@ -1,6 +1,10 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "cairo_6" +version = "0.1.0" + [[package]] name = "snforge_scarb_plugin" version = "0.56.0" @@ -20,5 +24,6 @@ dependencies = [ name = "starknet_contracts" version = "0.1.0" dependencies = [ + "cairo_6", "snforge_std", ] diff --git a/starknet_contracts/Scarb.toml b/starknet_contracts/Scarb.toml index 29cf20f..124c7d3 100644 --- a/starknet_contracts/Scarb.toml +++ b/starknet_contracts/Scarb.toml @@ -7,6 +7,7 @@ edition = "2024_07" [dependencies] starknet = "2.18.0" +cairo_6 = { path = "../cairo_program" } [dev-dependencies] snforge_std = "0.56.0" diff --git a/starknet_contracts/snfoundry.toml b/starknet_contracts/snfoundry.toml index c06aab7..9461947 100644 --- a/starknet_contracts/snfoundry.toml +++ b/starknet_contracts/snfoundry.toml @@ -1,3 +1,5 @@ +[sncast.default] +account = "counter_account" # Visit https://foundry-rs.github.io/starknet-foundry/appendix/snfoundry-toml.html # and https://foundry-rs.github.io/starknet-foundry/projects/configuration.html for more information diff --git a/starknet_contracts/src/lib.cairo b/starknet_contracts/src/lib.cairo index f1b82bb..7e716c2 100644 --- a/starknet_contracts/src/lib.cairo +++ b/starknet_contracts/src/lib.cairo @@ -4,25 +4,49 @@ pub trait ICounter { /// Increase count. fn increase_count(ref self: T, amount: u32); + fn decrease_count(ref self: T, amount: u32); /// Retrieve count. fn get_count(self: @T) -> u32; } + /// Simple contract for managing count. #[starknet::contract] mod Counter { + use cairo_6::integer::{add_num, sub_num}; use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; #[storage] struct Storage { count: u32, + owner: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress) { + self.count.write(0); + self.owner.write(owner); } #[abi(embed_v0)] impl CounterImpl of super::ICounter { fn increase_count(ref self: ContractState, amount: u32) { + let caller = get_caller_address(); + assert!(caller == self.owner.read(), "Only owner can transfer"); + assert(amount != 0, 'Amount cannot be 0'); + + let current_count = self.count.read(); + add_num(current_count, amount); + } + + fn decrease_count(ref self: ContractState, amount: u32) { + let caller = get_caller_address(); + assert!(caller == self.owner.read(), "Only owner can transfer"); assert(amount != 0, 'Amount cannot be 0'); - self.count.write(self.count.read() + amount); + + let current_count = self.count.read(); + sub_num(current_count, amount); } fn get_count(self: @ContractState) -> u32 {