From b1e5f30bbe6d022ba8f2aebcab0cb6ef45f79419 Mon Sep 17 00:00:00 2001 From: GianMarco Date: Wed, 22 Nov 2023 00:20:09 +0300 Subject: [PATCH] :tada: add ch02-11-starkli (#244) * Providers * add starkli content * fix broken links * add ownable contract * Add bash examples --- examples/Ownable-Starknet/.gitignore | 2 + examples/Ownable-Starknet/Scarb.lock | 14 ++ examples/Ownable-Starknet/Scarb.toml | 13 ++ examples/Ownable-Starknet/data/calldata.txt | 1 + examples/Ownable-Starknet/src/lib.cairo | 89 ++++++++ .../Ownable-Starknet/tests/test_ownable.cairo | 87 ++++++++ examples/script_devnet | 10 + examples/script_testnet | 15 ++ src/ch02-05-testnet-deployment.md | 20 +- src/ch02-11-starkli.md | 204 ++++++++++++++++++ 10 files changed, 443 insertions(+), 12 deletions(-) create mode 100644 examples/Ownable-Starknet/.gitignore create mode 100644 examples/Ownable-Starknet/Scarb.lock create mode 100644 examples/Ownable-Starknet/Scarb.toml create mode 100644 examples/Ownable-Starknet/data/calldata.txt create mode 100644 examples/Ownable-Starknet/src/lib.cairo create mode 100644 examples/Ownable-Starknet/tests/test_ownable.cairo create mode 100755 examples/script_devnet create mode 100755 examples/script_testnet diff --git a/examples/Ownable-Starknet/.gitignore b/examples/Ownable-Starknet/.gitignore new file mode 100644 index 000000000..796603f34 --- /dev/null +++ b/examples/Ownable-Starknet/.gitignore @@ -0,0 +1,2 @@ +target +.env \ No newline at end of file diff --git a/examples/Ownable-Starknet/Scarb.lock b/examples/Ownable-Starknet/Scarb.lock new file mode 100644 index 000000000..c7e6d3bff --- /dev/null +++ b/examples/Ownable-Starknet/Scarb.lock @@ -0,0 +1,14 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "ownable_starknet" +version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_std" +version = "0.1.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.7.1#d1bd8b9a361d437e8eaeb4ebffac291a48b4c920" diff --git a/examples/Ownable-Starknet/Scarb.toml b/examples/Ownable-Starknet/Scarb.toml new file mode 100644 index 000000000..61055bef9 --- /dev/null +++ b/examples/Ownable-Starknet/Scarb.toml @@ -0,0 +1,13 @@ +[package] +name = "ownable_starknet" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest + +[dependencies] +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.7.1" } #v0.7.0 +starknet = ">=2.2.0" + +[[target.starknet-contract]] +sierra = true +casm = true diff --git a/examples/Ownable-Starknet/data/calldata.txt b/examples/Ownable-Starknet/data/calldata.txt new file mode 100644 index 000000000..181a15cf7 --- /dev/null +++ b/examples/Ownable-Starknet/data/calldata.txt @@ -0,0 +1 @@ +'admin' \ No newline at end of file diff --git a/examples/Ownable-Starknet/src/lib.cairo b/examples/Ownable-Starknet/src/lib.cairo new file mode 100644 index 000000000..12d553bb1 --- /dev/null +++ b/examples/Ownable-Starknet/src/lib.cairo @@ -0,0 +1,89 @@ +use core::traits::TryInto; +use starknet::ContractAddress; + +#[starknet::interface] +trait IData { + fn get_data(self: @T) -> felt252; + fn set_data(ref self: T, new_value: felt252); + fn other_func(self: @T, other_contract: ContractAddress) -> felt252; +} + +#[starknet::interface] +trait OwnableTrait { + fn transfer_ownership(ref self: T, new_owner: ContractAddress); + fn owner(self: @T) -> ContractAddress; +} + +#[starknet::contract] +mod ownable { + use starknet::get_caller_address; + use super::{ContractAddress, IData, IDataDispatcherTrait, IDataDispatcher, OwnableTrait}; + + #[storage] + struct Storage { + owner: ContractAddress, + data: felt252, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + OwnershipTransferred: OwnershipTransferred, + } + + #[derive(Drop, starknet::Event)] + struct OwnershipTransferred { + #[key] + prev_owner: ContractAddress, + #[key] + new_owner: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, initial_owner: ContractAddress,) { + self.owner.write(initial_owner); + self.data.write(1); + // Any variable of the storage that is not initialized + // will have default value -> data = 0. + } + + #[external(v0)] + impl OwnableDataImpl of IData { + fn other_func(self: @ContractState, other_contract: ContractAddress) -> felt252 { + IDataDispatcher { contract_address: other_contract }.get_data() + } + + fn get_data(self: @ContractState) -> felt252 { + self.data.read() + } + + fn set_data(ref self: ContractState, new_value: felt252) { + self.only_owner(); + self.data.write(new_value); + } + } + + #[external(v0)] + impl OwnableTraitImpl of OwnableTrait { + fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress) { + self.only_owner(); + let prev_owner = self.owner.read(); + self.owner.write(new_owner); + + self.emit(OwnershipTransferred { prev_owner, new_owner, }); + } + + fn owner(self: @ContractState) -> ContractAddress { + self.owner.read() + } + } + + #[generate_trait] + impl PrivateMethods of PrivateMethodsTrait { + fn only_owner(self: @ContractState) { + let caller = get_caller_address(); + assert(caller == self.owner.read(), 'Caller is not the owner'); + } + } +} + diff --git a/examples/Ownable-Starknet/tests/test_ownable.cairo b/examples/Ownable-Starknet/tests/test_ownable.cairo new file mode 100644 index 000000000..53b1575fe --- /dev/null +++ b/examples/Ownable-Starknet/tests/test_ownable.cairo @@ -0,0 +1,87 @@ +use starknet::{ContractAddress, Into, TryInto, OptionTrait}; +use starknet::syscalls::deploy_syscall; +use result::ResultTrait; +use array::{ArrayTrait, SpanTrait}; +use snforge_std::{declare, ContractClassTrait}; +use snforge_std::io::{FileTrait, read_txt}; +use snforge_std::{start_prank, stop_prank}; +use snforge_std::{start_mock_call, stop_mock_call}; + +use ownable_starknet::{OwnableTraitDispatcher, OwnableTraitDispatcherTrait}; +use ownable_starknet::{IDataSafeDispatcher, IDataSafeDispatcherTrait}; + +mod Errors { + const INVALID_OWNER: felt252 = 'Caller is not the owner'; + const INVALID_DATA: felt252 = 'Invalid data'; +} + +mod Accounts { + use traits::TryInto; + use starknet::ContractAddress; + + fn admin() -> ContractAddress { + 'admin'.try_into().unwrap() + } + fn new_admin() -> ContractAddress { + 'new_admin'.try_into().unwrap() + } + fn bad_guy() -> ContractAddress { + 'bad_guy'.try_into().unwrap() + } +} + +fn deploy_contract(name: felt252) -> ContractAddress { + // let account = Accounts::admin(); + let contract = declare(name); + + let file = FileTrait::new('data/calldata.txt'); + let calldata = read_txt(@file); + //deploy contract + contract.deploy(@calldata).unwrap() +} + +#[test] +fn test_construct_with_admin() { + let contract_address = deploy_contract('ownable'); + let dispatcher = OwnableTraitDispatcher { contract_address }; + let owner = dispatcher.owner(); + assert(owner == Accounts::admin(), Errors::INVALID_OWNER); +} + +#[test] +fn test_transfer_ownership() { + let contract_address = deploy_contract('ownable'); + let dispatcher = OwnableTraitDispatcher { contract_address }; + start_prank(contract_address, Accounts::admin()); + dispatcher.transfer_ownership(Accounts::new_admin()); + + assert(dispatcher.owner() == Accounts::new_admin(), Errors::INVALID_OWNER); +} + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_transfer_ownership_bad_guy() { + let contract_address = deploy_contract('ownable'); + let dispatcher = OwnableTraitDispatcher { contract_address }; + start_prank(contract_address, Accounts::bad_guy()); + dispatcher.transfer_ownership(Accounts::bad_guy()); + + assert(dispatcher.owner() == Accounts::bad_guy(), Errors::INVALID_OWNER); +} + +#[test] +fn test_data_mock_call_get_data() { + let contract_address = deploy_contract('ownable'); + let safe_dispatcher = IDataSafeDispatcher { contract_address }; + let mock_ret_data = 100; + start_mock_call(contract_address, 'get_data', mock_ret_data); + start_prank(contract_address, Accounts::admin()); + safe_dispatcher.set_data(20); + let data = safe_dispatcher.get_data().unwrap(); + assert(data == mock_ret_data, Errors::INVALID_DATA); + stop_mock_call(contract_address, 'get_data'); + + let data2 = safe_dispatcher.get_data().unwrap(); + assert(data2 == 20, Errors::INVALID_DATA); + stop_prank(contract_address); +} diff --git a/examples/script_devnet b/examples/script_devnet new file mode 100755 index 000000000..27a57e897 --- /dev/null +++ b/examples/script_devnet @@ -0,0 +1,10 @@ +#!/bin/bash +chain="$(starkli chain-id --rpc http://0.0.0.0:5050)" +echo "Where are connected to the starknet local devnet with chain id: $chain" + +block="$(starkli block-number --rpc http://0.0.0.0:5050)" +echo "The latest block number on Katana is: $block" + +account1="0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +balance="$(starkli balance $account1 --rpc http://0.0.0.0:5050)" +echo "The balance of account $account1 is: $balance eth" \ No newline at end of file diff --git a/examples/script_testnet b/examples/script_testnet new file mode 100755 index 000000000..a08779b71 --- /dev/null +++ b/examples/script_testnet @@ -0,0 +1,15 @@ +#!/bin/bash + +echo "input your testnet API URL: " +read url +chain="$(starkli chain-id --rpc $url)" +echo "Where are connected to the starknet testnet with chain id: $chain" + +block="$(starkli block-number --rpc $url)" +echo "The latest block number on Goerli is: $block" + +echo "input your transaction hash: " +read hash +receipt="$(starkli receipt $hash --rpc $url)" +echo "The receipt of transaction $hash is: $receipt" + diff --git a/src/ch02-05-testnet-deployment.md b/src/ch02-05-testnet-deployment.md index 978ec4adc..f4b146913 100644 --- a/src/ch02-05-testnet-deployment.md +++ b/src/ch02-05-testnet-deployment.md @@ -2,7 +2,7 @@ This chapter guides developers through the process of compiling, deploying, and interacting with a Starknet smart contract written in Cairo on the testnet. Earlier, the focus was on deploying contracts using a local node, Katana. This time, the deployment and interaction target the Starknet testnet. -Ensure the following commands run successfully on your system. If not, see the 'Basic Installation' section: +Ensure the following commands run successfully on your system. If not, see the [Basic Installation](ch02-01-basic-installation.md) section: ```bash scarb --version # For Cairo code compilation @@ -17,8 +17,7 @@ the Account Descriptor is a JSON file detailing the wallet’s address and public key. In order for an account to be used as a signer it must be deployed to the appropriate network, -Starknet Goerli or mainnet, and funded. For this example we are going to use Goerli Testnet. To deploy your wallet, visit [Getting Started](ch01-00-getting-started.md) and find the `Smart Wallet Setup` section. - +Starknet Goerli or mainnet, and funded. For this example we are going to use Goerli Testnet. To deploy your wallet, visit [Smart Wallet Setup](https://book.starknet.io/ch01-00-getting-started.html#smart-wallet-setup). Now you’re ready to interact with Starknet smart contracts. ### Creating a Signer @@ -203,7 +202,7 @@ In case you face an error like this: Error: code=ContractNotFound, message="Contract with address {SMART_WALLET_ADDRESS} is not deployed." ``` -It means you probably just created a new wallet and it has not been deployed yet. To accomplish this you have to fund your wallet with tokens and transfer tokens to a different wallet address. After this process, search your wallet address on the Starknet explorer. To see the details, go back to [Getting Started](ch01-00-getting-started.md) and find the `Smart Wallet Setup` section. +It means you probably just created a new wallet and it has not been deployed yet. To accomplish this you have to fund your wallet with tokens and transfer tokens to a different wallet address. After this process, search your wallet address on the Starknet explorer. To see the details, go back to [Smart Wallet Setup](https://book.starknet.io/ch01-00-getting-started.html#smart-wallet-setup). After the acount descriptor file is generated, you can see its details, run: @@ -259,17 +258,14 @@ efficient. Deploying a smart contract on Starknet involves two steps: - Declare your contract’s code. - - Deploy an instance of the declared code. -To get started, navigate to the `contracts/` directory in the [first -chapter](https://github.com/starknet-edu/starknetbook/tree/main/chapters/book/modules/chapter_1/pages/contracts) +To get started, navigate to the `src/` directory in the [Ownable-Starknet](https://github.com/starknet-edu/starknetbook/Ownable-Starknet) directory of the Starknet Book repo. The `src/lib.cairo` file contains a basic contract to practice with. First, compile the contract using the Scarb compiler. If you haven’t -installed Scarb, follow the installation guide in the [Setting up your -Environment](https://book.starknet.io/chapter_1/environment_setup.html) +installed Scarb, follow the installation guide in the [basic instalation](./ch02-01-basic-installation) section. ```bash @@ -317,7 +313,7 @@ Run this command to declare your contract using the default Starknet Sequencer’s Gateway: ```bash - starkli declare target/dev/contracts_Ownable.sierra.json + starkli declare target/dev/contracts_Ownable.contract_class.json ``` According to the `STARKNET_RPC` url, starkli can recognize the target @@ -366,8 +362,8 @@ main components: 2. Any constructor arguments that the contract expects. In our example, the constructor expects an _owner_ address. You can -learn more about constructors in \[Chapter 12 of The Cairo -Book\](). +learn more about constructors in [Chapter 12 of The Cairo +Book](https://book.cairo-lang.org/ch99-01-03-02-contract-functions.html?highlight=constructor#1-constructors). The command would look like this: diff --git a/src/ch02-11-starkli.md b/src/ch02-11-starkli.md index f93571c01..cbf70da5c 100644 --- a/src/ch02-11-starkli.md +++ b/src/ch02-11-starkli.md @@ -1 +1,205 @@ # Starkli: A CLI interface 🚧 + +[Starkli](https://book.starkli.rs/) is a CLI tool for interacting with Starknet, powered by [starknet-rs](https://github.com/xJonathanLEI/starknet-rs). +Starkli allows you to query Starknet and make transactions with ease. It's fast, easy to install, and intuitive to use. + +## Basic setup + +Ensure the following commands run successfully on your system. If not, see the [Basic Installation](ch02-01-basic-installation.md) section: + +```bash + starkli --version # To interact with Starknet +``` + +## Connect to Starknet with Providers + +Starkli is centric around JSON-RPC provider. There are a few options to obtain access to a JSON-RPC endpoint: + +- For ease of access, consider using a provider such as +[Infura](https://docs.infura.io/networks/starknet/how-to) or +[Alchemy](https://www.alchemy.com/starknet) to get an RPC client. +- For development and testing, a temporary local node such as `katana` can be +used. + +### Interact with Katana + +Launch Katana. In a terminal, run: + +```bash + katana +``` +Let's retrieve the chain id. To accomplish this we have to pass the URL to the katana JSON-RPC endpoint via the --rpc . For example, in a new terminal: + +```bash + starkli chain-id --rpc http://0.0.0.0:5050 +``` +You will get the following output + +```bash + 0x4b4154414e41 (KATANA) +``` + +Now let's retrieve the latest block number on Katana. + +```bash + starkli block-number --rpc http://0.0.0.0:5050 +``` + +You will get the following output + +```bash + 0 +``` + + +Remember, `katana` is a temporary local node, Katana state right now is ephemeral. We have not changed the state of katana yet, thus, it makes sense the block-number is 0. Let's recap our previous example [Introduction to Starkli, Scarb and Katana](ch02-02-starkli-scarb-katana.md) and retrieve the block number after we input `starkli declare` and `starkli deploy` + +Again, to declare your contract, execute: + +```bash + starkli declare target/dev/my_contract_hello.contract_class.json +``` +```bash + Class hash declared: 0x00bfb49ff80fd7ef5e84662d6d256d49daf75e0c5bd279b20a786f058ca21418 +``` + +Now let's retrieve the latest block number on Katana. + +```bash + starkli block-number +``` +```bash + 1 +``` +You aso can check the logs on Katana +```bash +2023-11-03T04:38:48.712332Z DEBUG server: method="starknet_chainId" +2023-11-03T04:38:48.725133Z DEBUG server: method="starknet_getClass" +2023-11-03T04:38:48.726668Z DEBUG server: method="starknet_chainId" +2023-11-03T04:38:48.741588Z DEBUG server: method="starknet_getNonce" +2023-11-03T04:38:48.744718Z DEBUG server: method="starknet_estimateFee" +2023-11-03T04:38:48.766843Z DEBUG server: method="starknet_getNonce" +2023-11-03T04:38:48.770236Z DEBUG server: method="starknet_addDeclareTransaction" +2023-11-03T04:38:48.779714Z INFO txpool: Transaction received | Hash: 0x352f04ad496761c73806f92c64c267746afcbc16406bd0041ac6efa70b01a51 +2023-11-03T04:38:48.782100Z TRACE executor: Transaction resource usage: Steps: 2854 | ECDSA: 1 | L1 Gas: 3672 | Pedersen: 15 | Range Checks: 63 +2023-11-03T04:38:48.782112Z TRACE executor: Event emitted keys=[0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9] +2023-11-03T04:38:48.782399Z INFO backend: ⛏️ Block 1 mined with 1 transactions +``` +We can see a transaction has been received, we used gas and a new block has been mined. That's why the latest block number is `1`. + +Before to deploy, we have to mention that Starkli supports argument resolution, the process of expanding simple, human-readable input into actual field element arguments expected by the network. Remember the constructor inputs are in felt format, but argument resolution makes argument input easier, we can pass `str:` + +```bash + starkli deploy \ + 0x00bfb49ff80fd7ef5e84662d6d256d49daf75e0c5bd279b20a786f058ca21418 \ + str:starknet-book +``` +After this we mined a new block, we deployed our contract and we did not use `to-cairo-string` this time. + +### Interact with Testnet + +For this, we have to use a third-party JSON-RPC API provider like Infura, Alchemy. Once you have a URL, you can input the following command: +```bash + starkli block-number --rpc https://starknet-goerli.g.alchemy.com/v2/V0WI... +``` +You will get the something like this + +```bash + 896360 +``` + +At the same time you can verify this result on [Starkscan](https://testnet.starkscan.co/). And You will realize they are the same + +Starkli also offers simplified invoke commands. For example, we can transfer 1000 Wei of the ETH token to the address 0x1234. Before to do this, make sure you to setup your environment variables. + +```bash + export STARKNET_ACCOUNT=~/.starkli-wallets/deployer/my_account_1.json + export STARKNET_KEYSTORE=~/.starkli-wallets/deployer/my_keystore_1.json +``` + +Starkli makes this process a lot easier, you have to input the following command + +```bash + starkli invoke eth transfer 0x1234 u256:1000 +``` + +Make sure the `0x1234` address this different than your account in the `my_account_1.json` file + +### Execute your own bash file + +In this section, we are going to execute a bash file in order to connect with the blockchain + +#### Katana + +Make sure katana is running. We are going to show a simple way to retrieve information from katana using starkli. + +In terminal 1 +```bash + katana +``` + +Now create a file called `script_devnet`, using touch command + +In terminal 2 +```bash + touch script_devnet +``` + +Edit the file with the program of your choice. Within the file, paste the following content + +```bash +#!/bin/bash +chain="$(starkli chain-id --rpc http://0.0.0.0:5050)" +echo "Where are connected to the starknet local devnet with chain id: $chain" + +block="$(starkli block-number --rpc http://0.0.0.0:5050)" +echo "The latest block number on Katana is: $block" + +account1="0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +balance="$(starkli balance $account1 --rpc http://0.0.0.0:5050)" +echo "The balance of account $account1 is: $balance eth" +``` + +Now from the command line, run the script using the bash interpreter: + +```bash + bash script_devnet +``` + +Awesome, you get your output from devnet. + +#### Goerli + +In this example we are going to connect with Goerli testnet, read the latest block number of the network and search for the transaction receipt of and specific transaction hash. + +Create a file called `script_testnet`, using touch command + +In a new terminal +```bash + touch script_devnet +``` + +Edit the file. Within the file, paste the following content + +```bash +echo "input your testnet API URL: " +read url +chain="$(starkli chain-id --rpc $url)" +echo "Where are connected to the starknet testnet with chain id: $chain" + +block="$(starkli block-number --rpc $url)" +echo "The latest block number on Goerli is: $block" + +echo "input your transaction hash: " +read hash +receipt="$(starkli receipt $hash --rpc $url)" +echo "The receipt of transaction $hash is: $receipt" +``` +Now from the command line, run the script using the bash interpreter: + +```bash + bash script_testnet +``` +This time you have to input first a `testnet API URL`, and then you have to provide a `transaction hash`. You can use this transaction hash: 0x2dd73eb1802aef84e8d73334ce0e5856b18df6626fe1a67bb247fcaaccaac8c + +Great, now you can create your own bash file according to your interest. \ No newline at end of file