diff --git a/cmd/crates/soroban-test/tests/it/integration.rs b/cmd/crates/soroban-test/tests/it/integration.rs index 8e2bb89a4..3e8521869 100644 --- a/cmd/crates/soroban-test/tests/it/integration.rs +++ b/cmd/crates/soroban-test/tests/it/integration.rs @@ -1,4 +1,5 @@ mod bindings; +mod cookbook; mod custom_types; mod dotenv; mod fund; diff --git a/cmd/crates/soroban-test/tests/it/integration/cookbook.rs b/cmd/crates/soroban-test/tests/it/integration/cookbook.rs new file mode 100644 index 000000000..65855d775 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/cookbook.rs @@ -0,0 +1,292 @@ +use predicates::prelude::*; +use soroban_cli::config::network::passphrase::LOCAL; +use soroban_test::TestEnv; +use std::fs; +use std::path::PathBuf; + +fn parse_command(command: &str) -> Vec { + command + .replace("\\\n", " ") + .split_whitespace() + .map(String::from) + .collect() +} + +async fn run_command( + sandbox: &TestEnv, + command: &str, + wasm_path: &str, + wasm_hash: &str, + source: &str, + contract_id: &str, + bob_id: &str, + native_id: &str, + key_xdr: &str, +) -> Result<(), String> { + if command.contains("export") { + return Ok(()); + } + let args = parse_command(command); + if args.is_empty() { + return Err("Empty command".to_string()); + } + if command.contains("contract asset deploy") { + return Ok(()); + } + /*if command.contains("keys generate"){ + return Ok(()); + }*/ + let cmd = args[1].clone(); + let mut modified_args: Vec = Vec::new(); + let mut skip_next = false; + + for (index, arg) in args[2..].iter().enumerate() { + if skip_next { + skip_next = false; + continue; + } + + match arg.as_str() { + "--wasm" => { + modified_args.push(arg.to_string()); + modified_args.push(wasm_path.to_string()); + skip_next = true; + } + "--wasm-hash" => { + modified_args.push(arg.to_string()); + modified_args.push(wasm_hash.to_string()); + skip_next = true; + } + "--source" | "--source-account" => { + modified_args.push(arg.to_string()); + modified_args.push(source.to_string()); + skip_next = true; + } + "--contract-id" | "--id" => { + modified_args.push(arg.to_string()); + modified_args.push(contract_id.to_string()); + skip_next = true; + } + "--network-passphrase" => { + modified_args.push(arg.to_string()); + modified_args.push(LOCAL.to_string()); + skip_next = true; + } + "--network" => { + modified_args.push(arg.to_string()); + modified_args.push("local".to_string()); + skip_next = true; + } + "--key-xdr" => { + modified_args.push(arg.to_string()); + modified_args.push(key_xdr.to_string()); + skip_next = true; + } + "" => { + modified_args.push("persistent".to_string()); + skip_next = false; + } + "" => { + modified_args.push("COUNTER".to_string()); + skip_next = false; + } + "" => { + modified_args.push(bob_id.to_string()); + skip_next = false; + } + "" => { + modified_args.push(native_id.to_string()); + skip_next = false; + } + _ => modified_args.push(arg.to_string()), + } + + // If this is the last argument, don't skip the next one + if index == args[2..].len() - 1 { + skip_next = false; + } + } + + println!("Executing command: {} {}", cmd, modified_args.join(" ")); + let result = sandbox.new_assert_cmd(&cmd).args(&modified_args).assert(); + + if command.contains("keys generate") { + result + .code(predicates::ord::eq(0).or(predicates::ord::eq(1))) + .stderr( + predicate::str::is_empty().or(predicates::str::contains("Generated new key for") + .or(predicates::str::contains("The identity") + .and(predicates::str::contains("already exists")))), + ); + } else if command.contains("contract invoke") { + result + .failure() + .stderr(predicates::str::contains("error: unrecognized subcommand")); + } else if command.contains("contract restore") { + result + .failure() + .stderr(predicates::str::contains("TxSorobanInvalid")); + } else { + result.success(); + } + + Ok(()) +} + +async fn test_mdx_file( + sandbox: &TestEnv, + file_path: &str, + wasm_path: &str, + wasm_hash: &str, + source: &str, + contract_id: &str, + bob_id: &str, + native_id: &str, + key_xdr: &str, +) -> Result<(), String> { + let content = fs::read_to_string(file_path) + .map_err(|e| format!("Failed to read file {}: {}", file_path, e))?; + + let commands: Vec<&str> = content + .split("```bash") + .skip(1) + .filter_map(|block| block.split("```").next()) + .collect(); + + println!("Testing commands from file: {}", file_path); + + for (i, command) in commands.iter().enumerate() { + println!("Running command {}: {}", i + 1, command); + run_command( + sandbox, + command, + wasm_path, + wasm_hash, + source, + contract_id, + bob_id, + native_id, + key_xdr, + ) + .await?; + } + + Ok(()) +} + +fn get_repo_root() -> PathBuf { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + let mut path = PathBuf::from(manifest_dir); + for _ in 0..3 { + path.pop(); + } + path +} + +#[cfg(test)] +mod tests { + use soroban_test::AssertExt; + + use crate::integration::util::{deploy_hello, HELLO_WORLD}; + + use super::*; + + #[tokio::test] + async fn test_all_mdx_files() { + let sandbox = TestEnv::new(); + let wasm = HELLO_WORLD; + let wasm_path = wasm.path(); + let wasm_hash = wasm.hash().expect("should exist").to_string(); + let source = "test"; + + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg(source) + .assert() + .success(); + + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("bob") + .assert() + .success(); + let bob_id = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("bob") + .assert() + .success() + .stdout_as_str(); + sandbox + .new_assert_cmd("contract") + .arg("asset") + .arg("deploy") + .arg("--asset") + .arg("native") + .arg("--source-account") + .arg(source) + .output() + .expect("Failed to execute command"); + let native_id = sandbox + .new_assert_cmd("contract") + .arg("id") + .arg("asset") + .arg("--asset") + .arg("native") + .arg("--source-account") + .arg(source) + .assert() + .stdout_as_str(); + let contract_id = deploy_hello(&sandbox).await; + sandbox + .invoke_with_test(&["--id", &contract_id, "--", "inc"]) + .await + .unwrap(); + let read_xdr = sandbox + .new_assert_cmd("contract") + .arg("read") + .arg("--id") + .arg(contract_id.clone()) + .arg("--output") + .arg("xdr") + .arg("--key") + .arg("COUNTER") + .arg("--source-account") + .arg(source) + .assert() + .stdout_as_str(); + let key_xdr = read_xdr.split(',').next().unwrap_or("").trim(); + let repo_root = get_repo_root(); + let docs_dir = repo_root.join("cookbook"); + if !docs_dir.is_dir() { + panic!("docs directory not found"); + } + + for entry in fs::read_dir(docs_dir).expect("Failed to read docs directory") { + let entry = entry.expect("Failed to read directory entry"); + let path = entry.path(); + + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("mdx") { + let file_path = path.to_str().unwrap(); + match test_mdx_file( + &sandbox, + file_path, + &wasm_path.to_str().unwrap(), + &wasm_hash, + source, + &contract_id, + &bob_id, + &native_id, + &key_xdr, + ) + .await + { + Ok(_) => println!("Successfully tested all commands in {}", file_path), + Err(e) => panic!("Error testing {}: {}", file_path, e), + } + } + } + } +} diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index c7f348e20..dec7941f6 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -94,7 +94,7 @@ pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { "--durability", "persistent", "--ledgers-to-extend", - "100000", + "100001", ]; if let Some(value) = value { args.push("--key"); diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index a0e563e58..5901c71b1 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -131,7 +131,6 @@ impl NetworkRunnable for Cmd { let network = config.get_network()?; tracing::trace!(?network); let keys = self.key.parse_keys(&config.locator, &network)?; - let network = &config.get_network()?; let client = Client::new(&network.rpc_url)?; let key = config.key_pair()?; let extend_to = self.ledgers_to_extend(); diff --git a/cookbook/contract-lifecycle.mdx b/cookbook/contract-lifecycle.mdx new file mode 100644 index 000000000..b64262b34 --- /dev/null +++ b/cookbook/contract-lifecycle.mdx @@ -0,0 +1,61 @@ +--- +title: Contract Lifecycle +hide_table_of_contents: true +description: Manage the lifecycle of a Stellar smart contract using the CLI +--- + +To manage the lifecycle of a Stellar smart contract using the CLI, follow these steps: + +1. Create an identity for Alice: + +```bash +stellar keys generate alice +``` + +2. Fund the identity: + +```bash +stellar keys fund alice +``` + +3. Deploy a contract: + +```bash +stellar contract deploy --wasm /path/to/contract.wasm --source alice --network testnet +``` + +This will display the resulting contract ID, e.g.: + +``` +CBB65ZLBQBZL5IYHDHEEPCVUUMFOQUZSQKAJFV36R7TZETCLWGFTRLOQ +``` + +To learn more about how to build contract `.wasm` files, take a look at our [getting started tutorial](https://developers.stellar.org/docs/build/smart-contracts/getting-started/setup). + +4. Initialize the contract: + +```bash +stellar contract invoke --id --source alice --network testnet -- initialize --param1 value1 --param2 value2 +``` + +5. Invoke a contract function: + +```bash +stellar contract invoke --id --source alice --network testnet -- function_name --arg1 value1 --arg2 value2 +``` + +6. View the contract's state: + +```bash +stellar contract read --id --network testnet --source alice --durability --key +``` + +Note: `` is either `persistent` or `temporary`. `KEY` provides the key of the storage entry being read. + +7. Manage expired states: + +```bash +stellar contract extend --id --ledgers-to-extend 1000 --source alice --network testnet --durability --key +``` + +This extends the state of the instance provided by the given key to at least 1000 ledgers from the current ledger. diff --git a/cookbook/deploy-contract.mdx b/cookbook/deploy-contract.mdx new file mode 100644 index 000000000..ec0accf7b --- /dev/null +++ b/cookbook/deploy-contract.mdx @@ -0,0 +1,14 @@ +--- +title: Deploy a contract from installed Wasm bytecode +hide_table_of_contents: true +description: Deploy an instance of a compiled contract that is already installed on the network +--- + +To deploy an instance of a compiled smart contract that has already been installed onto the Stellar network, use the `stellar contract deploy` command: + +```bash +stellar contract deploy \ + --source S... \ + --network testnet \ + --wasm-hash +``` diff --git a/cookbook/deploy-stellar-asset-contract.mdx b/cookbook/deploy-stellar-asset-contract.mdx new file mode 100644 index 000000000..7233acc60 --- /dev/null +++ b/cookbook/deploy-stellar-asset-contract.mdx @@ -0,0 +1,55 @@ +--- +title: Deploy the Stellar Asset Contract for a Stellar asset +hide_table_of_contents: true +description: Deploy an SAC for a Stellar asset so that it can interact with smart contracts +--- + +The Stellar CLI can deploy a [Stellar Asset Contract] for a Stellar asset so that any Stellar smart contract can interact with the asset. + +Every Stellar asset has reserved a contract that anyone can deploy. Once deployed any contract can interact with that asset by holding a balance of the asset, receiving the asset, or sending the asset. + +Deploying the Stellar Asset Contract for a Stellar asset enables that asset for use in smart contracts. + +The Stellar Asset Contract can be deployed for any possible Stellar asset, either assets already in use on Stellar or assets that have never seen any activity. This means that the issuer doesn't need to have been created, and no one needs to be yet holding the asset on Stellar. + +To perform the deploy, use the following command: + +```bash +stellar contract asset deploy \ + --source S... \ + --network testnet \ + --asset USDC:GCYEIQEWOCTTSA72VPZ6LYIZIK4W4KNGJR72UADIXUXG45VDFRVCQTYE +``` + +The `asset` argument corresponds to the symbol and it's issuer address, which is how assets are identified on Stellar. + +The same can be done for the native [Lumens] asset: + +```bash +stellar contract asset deploy \ + --source S... \ + --network testnet \ + --asset native +``` + +:::note + +Deploying the native asset will fail on testnet or mainnet as a Stellar Asset Contract already exists. + +::: + +For any asset, the contract address can be fetched with: + +```bash +stellar contract id asset \ + --source S... \ + --network testnet \ + --asset native +``` + +[stellar asset contract]: ../../../tokens/stellar-asset-contract.mdx +[lumens]: ../../../learn/fundamentals/lumens.mdx +[sep-41]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md +[`soroban_sdk::token`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/ +[`token::tokenclient`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.TokenClient.html +[`token::stellarassetclient`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.StellarAssetClient.html diff --git a/cookbook/extend-contract-instance.mdx b/cookbook/extend-contract-instance.mdx new file mode 100644 index 000000000..3c456a39a --- /dev/null +++ b/cookbook/extend-contract-instance.mdx @@ -0,0 +1,24 @@ +--- +title: Extend a deployed contract instance's TTL +hide_table_of_contents: true +description: Use the CLI to extend the time to live (TTL) of a contract instance +--- + +You can use the Stellar CLI to extend the TTL of a contract instance like so: + +```bash +stellar contract extend \ + --source S... \ + --network testnet \ + --id C... \ + --ledgers-to-extend 535679 \ + --durability persistent +``` + +This example uses 535,679 ledgers as the new archival TTL. This is the maximum allowable value for this argument on the CLI. This corresponds to roughly 30 days (averaging 5 second ledger close times). + +When you extend a contract instance, this includes: + +- the contract instance itself +- any `env.storage().instance()` entries in the contract +- the contract's Wasm code diff --git a/cookbook/extend-contract-storage.mdx b/cookbook/extend-contract-storage.mdx new file mode 100644 index 000000000..21ce9c186 --- /dev/null +++ b/cookbook/extend-contract-storage.mdx @@ -0,0 +1,37 @@ +--- +title: Extend a deployed contract's storage entry TTL +hide_table_of_contents: true +description: Use the CLI to extend the time to live (TTL) of a contract's persistent storage entry +--- + +You can use the Stellar CLI to extend the TTL of a contract's persistent storage entry. For a storage entry that uses a simple `Symbol` as its storage key, you can run a command like so: + +```bash +stellar contract extend \ + --source S... \ + --network testnet \ + --id C... \ + --key COUNTER \ + --ledgers-to-extend 535679 \ + --durability persistent +``` + +This example uses 535,679 ledgers as the new archival TTL. This is the maximum allowable value for this argument on the CLI. This corresponds to roughly 30 days (averaging 5 second ledger close times). + +If your storage entry uses a more advanced storage key, such as `Balance(Address)` in a token contract, you'll need to provide the key in a base64-encoded XDR form: + +```bash +stellar contract extend \ + --source S... \ + --network testnet \ + --id C... \ + --key-xdr AAAABgAAAAHXkotywnA8z+r365/0701QSlWouXn8m0UOoshCtNHOYQAAAA4AAAAHQmFsYW5jZQAAAAAB \ + --ledgers-to-extend 535679 \ + --durability persistent +``` + +:::info + +Be sure to check out our [guide on creating XDR ledger keys](../rpc/generate-ledger-keys-python.mdx) for help generating them. + +::: diff --git a/cookbook/extend-contract-wasm.mdx b/cookbook/extend-contract-wasm.mdx new file mode 100644 index 000000000..9c4763b92 --- /dev/null +++ b/cookbook/extend-contract-wasm.mdx @@ -0,0 +1,35 @@ +--- +title: Extend a deployed contract's Wasm code TTL +hide_table_of_contents: true +description: Use Stellar CLI to extend contract's Wasm bytecode TTL, with or without local binary +--- + +You can use the Stellar CLI to extend the TTL of a contract's Wasm bytecode. This can be done in two forms: if you do or do not have the compiled contract locally. If you do have the compiled binary on your local machine: + +```bash +stellar contract extend \ + --source S... \ + --network testnet \ + --wasm ../relative/path/to/soroban_contract.wasm \ + --ledgers-to-extend 535679 \ + --durability persistent +``` + +This example uses 535,679 ledgers as the new archival TTL. This is the maximum allowable value for this argument on the CLI. This corresponds to roughly 30 days (averaging 5 second ledger close times). + +If you do not have the compiled binary on your local machine, you can still use the CLI to extend the bytecode TTL. You'll need to know the Wasm hash of the installed contract code: + +```bash +stellar contract extend \ + --source S... \ + --network testnet \ + --wasm-hash \ + --ledgers-to-extend 535679 \ + --durability persistent +``` + +:::info + +You can learn more about finding the correct Wasm hash for a contract instance [here (JavaScript)](../rpc/retrieve-contract-code-js.mdx) and [here (Python)](../rpc/retrieve-contract-code-python.mdx). + +::: diff --git a/cookbook/install-deploy.mdx b/cookbook/install-deploy.mdx new file mode 100644 index 000000000..3eb84b357 --- /dev/null +++ b/cookbook/install-deploy.mdx @@ -0,0 +1,14 @@ +--- +title: Install and deploy a smart contract +hide_table_of_contents: true +description: Combine the install and deploy commands in the Stellar CLI to accomplish both tasks +--- + +You can combine the `install` and `deploy` commands of the Stellar CLI to accomplish both tasks: + +```bash +stellar contract deploy \ + --source S... \ + --network testnet \ + --wasm ../relative/path/to/soroban_contract.wasm +``` diff --git a/cookbook/install-wasm.mdx b/cookbook/install-wasm.mdx new file mode 100644 index 000000000..96a4913ef --- /dev/null +++ b/cookbook/install-wasm.mdx @@ -0,0 +1,20 @@ +--- +title: Install Wasm bytecode +hide_table_of_contents: true +description: Use the Stellar CLI to install a compiled smart contract on the ledger +--- + +To use the Stellar CLI to install a compiled smart contract on the ledger, use the `stellar contract install` command: + +```bash +stellar contract install \ + --source S... \ + --network testnet \ + --wasm ../relative/path/to/soroban_contract.wasm +``` + +:::note + +Note this command will return the hash ID of the Wasm bytecode, rather than an address for a contract instance. + +::: diff --git a/cookbook/payments-and-assets.mdx b/cookbook/payments-and-assets.mdx new file mode 100644 index 000000000..9849bf3d9 --- /dev/null +++ b/cookbook/payments-and-assets.mdx @@ -0,0 +1,59 @@ +--- +title: Payments and Assets +hide_table_of_contents: true +description: Send XLM, stellar classic, or a soroban asset using the Stellar CLI +--- + +To send payments and work with assets using the Stellar CLI, follow these steps: + +1. Set your preferred network. For this guide, we will use `testnet`. A list of available networks can be found [here](https://developers.stellar.org/docs/networks) + +```bash +export STELLAR_NETWORK=testnet +``` + +By setting the `STELLAR_NETWORK` environment variable, we will not have to set the `--network` argument when using the CLI. + +2. Fund the accounts: + +```bash +stellar keys generate alice +``` + +```bash +stellar keys generate bob +``` + +```bash +stellar keys fund alice +``` + +```bash +stellar keys fund bob +``` + +3. Obtain the stellar asset contract ID: + +```bash +stellar contract id asset --asset native --source-account alice +``` + +4. Get Bob's public key: + +```bash +stellar keys address bob +``` + +5. Send 100 XLM from Alice to Bob: + +```bash +stellar contract invoke --id --source-account alice -- transfer --to --from alice --amount 100 +``` + +6. Check account balance: + +```bash +stellar contract invoke --id --source-account alice -- balance --id +``` + +For more information on the functions available to the stellar asset contract, see the [token interface code](https://developers.stellar.org/docs/tokens/token-interface#code). diff --git a/cookbook/restore-contract-instance.mdx b/cookbook/restore-contract-instance.mdx new file mode 100644 index 000000000..488760ba4 --- /dev/null +++ b/cookbook/restore-contract-instance.mdx @@ -0,0 +1,15 @@ +--- +title: Restore an archived contract using the Stellar CLI +hide_table_of_contents: true +description: Restore an archived contract instance using the Stellar CLI +--- + +If your contract instance has been archived, it can easily be restored using the Stellar CLI. + +```bash +stellar contract restore \ + --source S... \ + --network testnet \ + --id C... \ + --durability persistent +``` diff --git a/cookbook/restore-contract-storage.mdx b/cookbook/restore-contract-storage.mdx new file mode 100644 index 000000000..48fd86b9d --- /dev/null +++ b/cookbook/restore-contract-storage.mdx @@ -0,0 +1,33 @@ +--- +title: Restore archived contract data using the Stellar CLI +hide_table_of_contents: true +description: Restore archived contract storage entries using Stellar CLI +--- + +If a contract's persistent storage entry has been archived, you can restore it using the Stellar CLI. For a storage entry that uses a simple `Symbol` as its storage key, you can run a command like so: + +```bash +stellar contract restore \ + --source S... \ + --network testnet \ + --id C... \ + --key COUNTER \ + --durability persistent +``` + +If your storage entry uses a more advanced storage key, such as `Balance(Address)` in a token contract, you'll need to provide the key in a base64-encoded XDR form: + +```bash +stellar contract restore \ + --source S... \ + --network testnet \ + --id C... \ + --key-xdr AAAABgAAAAHXkotywnA8z+r365/0701QSlWouXn8m0UOoshCtNHOYQAAAA4AAAAHQmFsYW5jZQAAAAAB \ + --durability persistent +``` + +:::info + +Be sure to check out our [guide on creating XDR ledger keys](../rpc/generate-ledger-keys-python.mdx) for help generating them. + +:::