Skip to content

Commit 50e0f19

Browse files
Cookbook documentation (#1541)
* add cookbook file * cookbook entries for testing and copying to docs * test for cli guide entries * formatting * fixes remaining commands * fmt --------- Co-authored-by: Willem Wyndham <[email protected]>
1 parent e3a3214 commit 50e0f19

15 files changed

+661
-2
lines changed

cmd/crates/soroban-test/tests/it/integration.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod bindings;
2+
mod cookbook;
23
mod custom_types;
34
mod dotenv;
45
mod fund;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
use predicates::prelude::*;
2+
use soroban_cli::config::network::passphrase::LOCAL;
3+
use soroban_test::TestEnv;
4+
use std::fs;
5+
use std::path::PathBuf;
6+
7+
fn parse_command(command: &str) -> Vec<String> {
8+
command
9+
.replace("\\\n", " ")
10+
.split_whitespace()
11+
.map(String::from)
12+
.collect()
13+
}
14+
15+
async fn run_command(
16+
sandbox: &TestEnv,
17+
command: &str,
18+
wasm_path: &str,
19+
wasm_hash: &str,
20+
source: &str,
21+
contract_id: &str,
22+
bob_id: &str,
23+
native_id: &str,
24+
key_xdr: &str,
25+
) -> Result<(), String> {
26+
if command.contains("export") {
27+
return Ok(());
28+
}
29+
let args = parse_command(command);
30+
if args.is_empty() {
31+
return Err("Empty command".to_string());
32+
}
33+
if command.contains("contract asset deploy") {
34+
return Ok(());
35+
}
36+
/*if command.contains("keys generate"){
37+
return Ok(());
38+
}*/
39+
let cmd = args[1].clone();
40+
let mut modified_args: Vec<String> = Vec::new();
41+
let mut skip_next = false;
42+
43+
for (index, arg) in args[2..].iter().enumerate() {
44+
if skip_next {
45+
skip_next = false;
46+
continue;
47+
}
48+
49+
match arg.as_str() {
50+
"--wasm" => {
51+
modified_args.push(arg.to_string());
52+
modified_args.push(wasm_path.to_string());
53+
skip_next = true;
54+
}
55+
"--wasm-hash" => {
56+
modified_args.push(arg.to_string());
57+
modified_args.push(wasm_hash.to_string());
58+
skip_next = true;
59+
}
60+
"--source" | "--source-account" => {
61+
modified_args.push(arg.to_string());
62+
modified_args.push(source.to_string());
63+
skip_next = true;
64+
}
65+
"--contract-id" | "--id" => {
66+
modified_args.push(arg.to_string());
67+
modified_args.push(contract_id.to_string());
68+
skip_next = true;
69+
}
70+
"--network-passphrase" => {
71+
modified_args.push(arg.to_string());
72+
modified_args.push(LOCAL.to_string());
73+
skip_next = true;
74+
}
75+
"--network" => {
76+
modified_args.push(arg.to_string());
77+
modified_args.push("local".to_string());
78+
skip_next = true;
79+
}
80+
"--key-xdr" => {
81+
modified_args.push(arg.to_string());
82+
modified_args.push(key_xdr.to_string());
83+
skip_next = true;
84+
}
85+
"<DURABILITY>" => {
86+
modified_args.push("persistent".to_string());
87+
skip_next = false;
88+
}
89+
"<KEY>" => {
90+
modified_args.push("COUNTER".to_string());
91+
skip_next = false;
92+
}
93+
"<Bob_ID>" => {
94+
modified_args.push(bob_id.to_string());
95+
skip_next = false;
96+
}
97+
"<asset_contract_ID>" => {
98+
modified_args.push(native_id.to_string());
99+
skip_next = false;
100+
}
101+
_ => modified_args.push(arg.to_string()),
102+
}
103+
104+
// If this is the last argument, don't skip the next one
105+
if index == args[2..].len() - 1 {
106+
skip_next = false;
107+
}
108+
}
109+
110+
println!("Executing command: {} {}", cmd, modified_args.join(" "));
111+
let result = sandbox.new_assert_cmd(&cmd).args(&modified_args).assert();
112+
113+
if command.contains("keys generate") {
114+
result
115+
.code(predicates::ord::eq(0).or(predicates::ord::eq(1)))
116+
.stderr(
117+
predicate::str::is_empty().or(predicates::str::contains("Generated new key for")
118+
.or(predicates::str::contains("The identity")
119+
.and(predicates::str::contains("already exists")))),
120+
);
121+
} else if command.contains("contract invoke") {
122+
result
123+
.failure()
124+
.stderr(predicates::str::contains("error: unrecognized subcommand"));
125+
} else if command.contains("contract restore") {
126+
result
127+
.failure()
128+
.stderr(predicates::str::contains("TxSorobanInvalid"));
129+
} else {
130+
result.success();
131+
}
132+
133+
Ok(())
134+
}
135+
136+
async fn test_mdx_file(
137+
sandbox: &TestEnv,
138+
file_path: &str,
139+
wasm_path: &str,
140+
wasm_hash: &str,
141+
source: &str,
142+
contract_id: &str,
143+
bob_id: &str,
144+
native_id: &str,
145+
key_xdr: &str,
146+
) -> Result<(), String> {
147+
let content = fs::read_to_string(file_path)
148+
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))?;
149+
150+
let commands: Vec<&str> = content
151+
.split("```bash")
152+
.skip(1)
153+
.filter_map(|block| block.split("```").next())
154+
.collect();
155+
156+
println!("Testing commands from file: {}", file_path);
157+
158+
for (i, command) in commands.iter().enumerate() {
159+
println!("Running command {}: {}", i + 1, command);
160+
run_command(
161+
sandbox,
162+
command,
163+
wasm_path,
164+
wasm_hash,
165+
source,
166+
contract_id,
167+
bob_id,
168+
native_id,
169+
key_xdr,
170+
)
171+
.await?;
172+
}
173+
174+
Ok(())
175+
}
176+
177+
fn get_repo_root() -> PathBuf {
178+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
179+
let mut path = PathBuf::from(manifest_dir);
180+
for _ in 0..3 {
181+
path.pop();
182+
}
183+
path
184+
}
185+
186+
#[cfg(test)]
187+
mod tests {
188+
use soroban_test::AssertExt;
189+
190+
use crate::integration::util::{deploy_hello, HELLO_WORLD};
191+
192+
use super::*;
193+
194+
#[tokio::test]
195+
async fn test_all_mdx_files() {
196+
let sandbox = TestEnv::new();
197+
let wasm = HELLO_WORLD;
198+
let wasm_path = wasm.path();
199+
let wasm_hash = wasm.hash().expect("should exist").to_string();
200+
let source = "test";
201+
202+
sandbox
203+
.new_assert_cmd("keys")
204+
.arg("fund")
205+
.arg(source)
206+
.assert()
207+
.success();
208+
209+
sandbox
210+
.new_assert_cmd("keys")
211+
.arg("generate")
212+
.arg("bob")
213+
.assert()
214+
.success();
215+
let bob_id = sandbox
216+
.new_assert_cmd("keys")
217+
.arg("address")
218+
.arg("bob")
219+
.assert()
220+
.success()
221+
.stdout_as_str();
222+
sandbox
223+
.new_assert_cmd("contract")
224+
.arg("asset")
225+
.arg("deploy")
226+
.arg("--asset")
227+
.arg("native")
228+
.arg("--source-account")
229+
.arg(source)
230+
.output()
231+
.expect("Failed to execute command");
232+
let native_id = sandbox
233+
.new_assert_cmd("contract")
234+
.arg("id")
235+
.arg("asset")
236+
.arg("--asset")
237+
.arg("native")
238+
.arg("--source-account")
239+
.arg(source)
240+
.assert()
241+
.stdout_as_str();
242+
let contract_id = deploy_hello(&sandbox).await;
243+
sandbox
244+
.invoke_with_test(&["--id", &contract_id, "--", "inc"])
245+
.await
246+
.unwrap();
247+
let read_xdr = sandbox
248+
.new_assert_cmd("contract")
249+
.arg("read")
250+
.arg("--id")
251+
.arg(contract_id.clone())
252+
.arg("--output")
253+
.arg("xdr")
254+
.arg("--key")
255+
.arg("COUNTER")
256+
.arg("--source-account")
257+
.arg(source)
258+
.assert()
259+
.stdout_as_str();
260+
let key_xdr = read_xdr.split(',').next().unwrap_or("").trim();
261+
let repo_root = get_repo_root();
262+
let docs_dir = repo_root.join("cookbook");
263+
if !docs_dir.is_dir() {
264+
panic!("docs directory not found");
265+
}
266+
267+
for entry in fs::read_dir(docs_dir).expect("Failed to read docs directory") {
268+
let entry = entry.expect("Failed to read directory entry");
269+
let path = entry.path();
270+
271+
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("mdx") {
272+
let file_path = path.to_str().unwrap();
273+
match test_mdx_file(
274+
&sandbox,
275+
file_path,
276+
&wasm_path.to_str().unwrap(),
277+
&wasm_hash,
278+
source,
279+
&contract_id,
280+
&bob_id,
281+
&native_id,
282+
&key_xdr,
283+
)
284+
.await
285+
{
286+
Ok(_) => println!("Successfully tested all commands in {}", file_path),
287+
Err(e) => panic!("Error testing {}: {}", file_path, e),
288+
}
289+
}
290+
}
291+
}
292+
}

cmd/crates/soroban-test/tests/it/integration/util.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) {
9494
"--durability",
9595
"persistent",
9696
"--ledgers-to-extend",
97-
"100000",
97+
"100001",
9898
];
9999
if let Some(value) = value {
100100
args.push("--key");

cmd/soroban-cli/src/commands/contract/extend.rs

-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ impl NetworkRunnable for Cmd {
131131
let network = config.get_network()?;
132132
tracing::trace!(?network);
133133
let keys = self.key.parse_keys(&config.locator, &network)?;
134-
let network = &config.get_network()?;
135134
let client = Client::new(&network.rpc_url)?;
136135
let key = config.key_pair()?;
137136
let extend_to = self.ledgers_to_extend();

cookbook/contract-lifecycle.mdx

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
title: Contract Lifecycle
3+
hide_table_of_contents: true
4+
description: Manage the lifecycle of a Stellar smart contract using the CLI.
5+
---
6+
7+
To manage the lifecycle of a Stellar smart contract using the CLI, follow these steps:
8+
9+
1. Create an identity for Alice:
10+
11+
```bash
12+
stellar keys generate alice
13+
```
14+
15+
2. Fund the identity:
16+
17+
```bash
18+
stellar keys fund alice
19+
```
20+
21+
3. Deploy a contract:
22+
23+
```bash
24+
stellar contract deploy --wasm /path/to/contract.wasm --source alice --network testnet
25+
```
26+
27+
This will display the resulting contract ID, e.g.:
28+
29+
```
30+
CBB65ZLBQBZL5IYHDHEEPCVUUMFOQUZSQKAJFV36R7TZETCLWGFTRLOQ
31+
```
32+
33+
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).
34+
35+
4. Initialize the contract:
36+
37+
```bash
38+
stellar contract invoke --id <CONTRACT_ID> --source alice --network testnet -- initialize --param1 value1 --param2 value2
39+
```
40+
41+
5. Invoke a contract function:
42+
43+
```bash
44+
stellar contract invoke --id <CONTRACT_ID> --source alice --network testnet -- function_name --arg1 value1 --arg2 value2
45+
```
46+
47+
6. View the contract's state:
48+
49+
```bash
50+
stellar contract read --id <CONTRACT_ID> --network testnet --source alice --durability <DURABILITY> --key <KEY>
51+
```
52+
53+
Note: `<DURABILITY>` is either `persistent` or `temporary`. `KEY` provides the key of the storage entry being read.
54+
55+
7. Manage expired states:
56+
57+
```bash
58+
stellar contract extend --id <CONTRACT_ID> --ledgers-to-extend 1000 --source alice --network testnet --durability <DURABILITY> --key <KEY>
59+
```
60+
61+
This extends the state of the instance provided by the given key to at least 1000 ledgers from the current ledger.

cookbook/deploy-contract.mdx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Deploy a contract from installed Wasm bytecode
3+
hide_table_of_contents: true
4+
description: Deploy an instance of a compiled contract that is already installed on the network.
5+
---
6+
7+
To deploy an instance of a compiled smart contract that has already been installed onto the Stellar network, use the `stellar contract deploy` command:
8+
9+
```bash
10+
stellar contract deploy \
11+
--source S... \
12+
--network testnet \
13+
--wasm-hash <hex-encoded-wasm-hash>
14+
```

0 commit comments

Comments
 (0)