diff --git a/docs/SDKs/rust.mdx b/docs/SDKs/rust.mdx index 58295299..4b051fc8 100644 --- a/docs/SDKs/rust.mdx +++ b/docs/SDKs/rust.mdx @@ -29,5 +29,5 @@ Add the following sections to the `Cargo.toml` to import the `soroban-sdk`. testutils = ["soroban-sdk/testutils"] [dependencies] -soroban-sdk = "0.0.3" +soroban-sdk = "0.0.4" ``` diff --git a/docs/examples/authorization.mdx b/docs/examples/authorization.mdx new file mode 100644 index 00000000..f8f65612 --- /dev/null +++ b/docs/examples/authorization.mdx @@ -0,0 +1,253 @@ +--- +sidebar_position: 5 +title: Authorization +--- + +The [authorization example] demonstrates how to write a contract function that +verifies an `Identifiers` signature before proceeding with the rest of the +function. In this example, data is stored under an `Identifier` after +authorization has been verified. + +[authorization example]: https://github.com/stellar/soroban-examples/tree/main/authorization + +## Run the Example + +First go through the [Setup] process to get your development environment +configured, then clone the examples repository: + +[Setup]: ../getting-started/setup.mdx + +``` +git clone https://github.com/stellar/soroban-examples +``` + +To run the tests for the example, navigate to the `authorization` directory, and use `cargo test`. + +``` +cd authorization +cargo test +``` + +You should see the output: + +``` +running 2 tests +test test::test ... ok +test test::bad_data - should panic ... ok +``` + +## Code + +```rust title="authorization/src/lib.rs" +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Acc(Identifier), + Nonce(Identifier), + Admin, +} + +fn read_nonce(e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Nonce(id); + if let Some(nonce) = e.contract_data().get(key) { + nonce.unwrap() + } else { + BigInt::zero(e) + } +} +struct WrappedAuth(Signature); + +impl NonceAuth for WrappedAuth { + fn read_nonce(e: &Env, id: Identifier) -> BigInt { + read_nonce(e, id) + } + + fn read_and_increment_nonce(&self, e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Nonce(id.clone()); + let nonce = Self::read_nonce(e, id); + e.contract_data() + .set(key, nonce.clone() + BigInt::from_u32(e, 1)); + nonce + } + + fn signature(&self) -> &Signature { + &self.0 + } +} + +pub struct AuthContract; + +#[cfg_attr(feature = "export", contractimpl)] +#[cfg_attr(not(feature = "export"), contractimpl(export = false))] +impl AuthContract { + // Sets the admin identifier + pub fn set_admin(e: Env, admin: Identifier) { + if e.contract_data().has(DataKey::Admin) { + panic!("admin is already set") + } + + e.contract_data().set(DataKey::Admin, admin); + } + + // Saves data that corresponds to an Identifier, with that Identifiers authorization + pub fn save_data(e: Env, auth: Signature, nonce: BigInt, num: BigInt) { + let auth_id = auth.get_identifier(&e); + + check_auth( + &e, + &WrappedAuth(auth), + nonce.clone(), + Symbol::from_str("save_data"), + (auth_id.clone(), nonce, num.clone()).into_val(&e), + ); + + e.contract_data().set(DataKey::Acc(auth_id), num); + } + + // The admin can write data for any Identifier + pub fn overwrite(e: Env, auth: Signature, nonce: BigInt, id: Identifier, num: BigInt) { + let auth_id = auth.get_identifier(&e); + if auth_id != e.contract_data().get_unchecked(DataKey::Admin).unwrap() { + panic!("not authorized by admin") + } + + check_auth( + &e, + &WrappedAuth(auth), + nonce.clone(), + Symbol::from_str("overwrite"), + (auth_id, nonce, id.clone(), num.clone()).into_val(&e), + ); + + e.contract_data().set(DataKey::Acc(id), num); + } + + pub fn nonce(e: Env, to: Identifier) -> BigInt { + read_nonce(&e, to) + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/main/authorization + +## How it Works + +### Implement NonceAuth +`NonceAuth` is a trait in the soroban_sdk_auth crate that manages the nonce and +wraps the `Signature` that the contract will try to verifiy. A struct that +implements `NonceAuth` is expected by the `check_auth` sdk function. You can see +below that we have a `DataKey` for the nonce tied to an `Identifier`, and this +`DataKey` is used to manage the nonces for this contract. + +```rust +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Acc(Identifier), + Nonce(Identifier), + Admin, +} + +fn read_nonce(e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Nonce(id); + if let Some(nonce) = e.contract_data().get(key) { + nonce.unwrap() + } else { + BigInt::zero(e) + } +} +struct WrappedAuth(Signature); + +impl NonceAuth for WrappedAuth { + fn read_nonce(e: &Env, id: Identifier) -> BigInt { + read_nonce(e, id) + } + + fn read_and_increment_nonce(&self, e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Nonce(id.clone()); + let nonce = Self::read_nonce(e, id); + e.contract_data() + .set(key, nonce.clone() + BigInt::from_u32(e, 1)); + nonce + } + + fn signature(&self) -> &Signature { + &self.0 + } +} +``` + +### Check authorization in contract function +The `save_data` function stores data in a `DataKey::Acc` tied to an `Identifier` +with it's authorization. + +The `check_auth` method in the SDK is used for signature verification, and here +are the important authorization takeaways from the example below - +1. The `nonce` is included in the list of parameters for the contract function. +2. The `Signature` is passed into `check_auth` wrapped in `WrappedAuth`. +3. The `function` parameter to `check_auth` is the name of the invoked function. +4. The last argument passed to `check_auth` is a list of arguments that are + expected in the signed payload. The interesting thing to note here is that it + includes the `Identifier` from the `auth` and the nonce. + +```rust +// Saves data that corresponds to an Identifier, with that Identifiers authorization +pub fn save_data(e: Env, auth: Signature, nonce: BigInt, num: BigInt) { + let auth_id = auth.get_identifier(&e); + + check_auth( + &e, + &WrappedAuth(auth), + nonce.clone(), + Symbol::from_str("save_data"), + (auth_id.clone(), nonce, num.clone()).into_val(&e), + ); + + e.contract_data().set(DataKey::Acc(auth_id), num); +} +``` + +### Admin privileges + +Some contracts may want to set an admin account that is allowed special +privilege. The `set_admin` function here stores an `Identifier` as an admin, and +that admin is the only one that can call `overwrite`. + +```rust +// Sets the admin identifier +pub fn set_admin(e: Env, admin: Identifier) { + if e.contract_data().has(DataKey::Admin) { + panic!("admin is already set") + } + + e.contract_data().set(DataKey::Admin, admin); +} + +// The admin can write data for any Identifier +pub fn overwrite(e: Env, auth: Signature, nonce: BigInt, id: Identifier, num: BigInt) { + let auth_id = auth.get_identifier(&e); + if auth_id != e.contract_data().get_unchecked(DataKey::Admin).unwrap() { + panic!("not authorized by admin") + } + + check_auth( + &e, + &WrappedAuth(auth), + nonce.clone(), + Symbol::from_str("overwrite"), + (auth_id, nonce, id.clone(), num.clone()).into_val(&e), + ); + + e.contract_data().set(DataKey::Acc(id), num); +} +``` + +### Retrieving the Nonce +Users of this contract will need to know which nonce to use, so the contract +exposes this information. + +```rust +pub fn nonce(e: Env, to: Identifier) -> BigInt { + read_nonce(&e, to) +} +``` diff --git a/docs/examples/custom-types.mdx b/docs/examples/custom-types.mdx index 0f3d7977..e38bdab0 100644 --- a/docs/examples/custom-types.mdx +++ b/docs/examples/custom-types.mdx @@ -53,7 +53,7 @@ pub struct FirstLast { pub struct CustomTypesContract; -const NAME: Symbol = Symbol::from_str("NAME"); +const NAME: Symbol = symbol!("NAME"); #[contractimpl] impl CustomTypesContract { @@ -123,9 +123,9 @@ retrieved later. ```rust pub struct CustomTypesContract; -const NAME: Symbol = Symbol::from_str("NAME"); +const NAME: Symbol = symbol!("NAME"); -#[contractimpl(export_if = "export")] +#[contractimpl] impl CustomTypesContract { pub fn store(env: Env, name: Name) { env.contract_data().set(NAME, name); @@ -148,25 +148,22 @@ Open the `custom_types/src/test.rs` file to follow along. #[test] fn test() { let env = Env::default(); - let contract_id = FixedBinary::from_array(&env, [0; 32]); + let contract_id = BytesN::from_array(&env, &[0; 32]); env.register_contract(&contract_id, CustomTypesContract); + let client = CustomTypesContractClient::new(&env, &contract_id); - assert_eq!(retrieve::invoke(&env, &contract_id), Name::None); + assert_eq!(client.retrieve(), Name::None); - store::invoke( - &env, - &contract_id, - &Name::FirstLast(FirstLast { - first: Symbol::from_str("first"), - last: Symbol::from_str("last"), - }), - ); + client.store(&Name::FirstLast(FirstLast { + first: symbol!("first"), + last: symbol!("last"), + })); assert_eq!( - retrieve::invoke(&env, &contract_id), + client.retrieve(), Name::FirstLast(FirstLast { - first: Symbol::from_str("first"), - last: Symbol::from_str("last"), + first: symbol!("first"), + last: symbol!("last"), }), ); } @@ -183,33 +180,35 @@ Contracts must be registered with the environment with a contract ID, which is a 32-byte value. ```rust -let contract_id = FixedBinary::from_array(&env, [0; 32]); -env.register_contract(&contract_id, HelloContract); +let contract_id = BytesN::from_array(&env, [0; 32]); +env.register_contract(&contract_id, CustomTypesContract); ``` All public functions within an `impl` block that is annotated with the -`#[contractimpl]` attribute have an `invoke` function generated, that can be -used to invoke the contract function within the environment. +`#[contractimpl]` attribute have a corresponding function generated in a +generated client type. The client type will be named the same as the contract +type with `Client` appended. For example, in our contract the contract type is +`CustomTypesContract`, and the client is named `CustomTypesContractClient`. + +```rust +let client = CustomTypesContractClient::new(&env, &contract_id); +``` The test invokes the `retrieve` function on the registered contract, and asserts that it returns `Name::None`. ```rust -assert_eq!(retrieve::invoke(&env, &contract_id), Name::None); +assert_eq!(client.retrieve(), Name::None); ``` The test then invokes the `store` function on the registered contract, to change the name that is stored. ```rust -store::invoke( - &env, - &contract_id, - &Name::FirstLast(FirstLast { - first: Symbol::from_str("first"), - last: Symbol::from_str("last"), - }), -); +client.store(&Name::FirstLast(FirstLast { + first: symbol!("first"), + last: symbol!("last"), +})); ``` The test invokes the `retrieve` function again, to assert that it returns the @@ -217,10 +216,10 @@ name that was previously stored. ```rust assert_eq!( - retrieve::invoke(&env, &contract_id), + client.retrieve(), Name::FirstLast(FirstLast { - first: Symbol::from_str("first"), - last: Symbol::from_str("last"), + first: symbol!("first"), + last: symbol!("last"), }), ); ``` diff --git a/docs/examples/hello-world.mdx b/docs/examples/hello-world.mdx index 5edeec63..d9675f4e 100644 --- a/docs/examples/hello-world.mdx +++ b/docs/examples/hello-world.mdx @@ -37,15 +37,14 @@ test test::test ... ok ```rust title="hello_world/src/lib.rs" #![no_std] -use soroban_sdk::{contractimpl, vec, Env, Symbol, Vec}; +use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec}; pub struct HelloContract; #[contractimpl] impl HelloContract { pub fn hello(env: Env, to: Symbol) -> Vec { - const GREETING: Symbol = Symbol::from_str("Hello"); - vec![&env, GREETING, to] + vec![&env, symbol!("Hello"), to] } } ``` @@ -75,7 +74,7 @@ pub struct HelloContract; Contract functions look much like regular Rust functions. They mave have any number of arguments, but arguments must support being transmitted to and from the Soroban environment that the contract runs in. The Soroban SDK provides some -types like `Vec`, `Map`, `BigInt`, `Symbol`, `Binary`, `FixedBinary`, etc that +types like `Vec`, `Map`, `BigInt`, `Symbol`, `Bytes`, `BytesN`, etc that can be used. Primitive values like `u64`, `i64`, `u32`, `i32`, and `bool` can also be used. Floats are not supported. @@ -103,14 +102,12 @@ Open the `hello_world/src/test.rs` file to follow along. #[test] fn test() { let env = Env::default(); - let contract_id = FixedBinary::from_array(&env, [0; 32]); + let contract_id = BytesN::from_array(&env, &[0; 32]); env.register_contract(&contract_id, HelloContract); + let client = HelloContractClient::new(&env, &contract_id); - let words = hello::invoke(&env, &contract_id, &Symbol::from_str("SourBun")); - assert_eq!( - words, - vec![&env, Symbol::from_str("Hello"), Symbol::from_str("SourBun"),] - ); + let words = client.hello(&symbol!("SourBun")); + assert_eq!(words, vec![&env, symbol!("Hello"), symbol!("Dev"),]); } ``` @@ -130,19 +127,19 @@ env.register_contract(&contract_id, HelloContract); ``` All public functions within an `impl` block that is annotated with the -`#[contractimpl]` attribute have an `invoke` function generated, that can be -used to invoke the contract function within the environment. +`#[contractimpl]` attribute have a corresponding function generated in a +generated client type. The client type will be named the same as the contract +type with `Client` appended. For example, in our contract the contract type is +`HelloContract`, and the client is named `HelloContractClient`. ```rust -let words = hello::invoke(&env, &contract_id, &Symbol::from_str("SourBun")); +let client = HelloContractClient::new(&env, &contract_id); +let words = client.hello(&symbol!("Dev")); ``` The values returned by functions can be asserted on: ```rust -assert_eq!( - words, - vec![&env, Symbol::from_str("Hello"), Symbol::from_str("SourBun"),] -); +assert_eq!(words, vec![&env, symbol!("Hello"), symbol!("Dev"),]); ``` ## Build the Contract diff --git a/docs/examples/increment.mdx b/docs/examples/increment.mdx index 7ae3b121..e32ea065 100644 --- a/docs/examples/increment.mdx +++ b/docs/examples/increment.mdx @@ -36,11 +36,11 @@ test test::test ... ok ## Code ```rust title="increment/src/lib.rs" -const COUNTER: Symbol = Symbol::from_str("COUNTER"); +const COUNTER: Symbol = symbol!("COUNTER"); pub struct IncrementContract; -#[contractimpl(export_if = "export")] +#[contractimpl] impl IncrementContract { pub fn increment(env: Env) -> u32 { let mut count: u32 = env @@ -66,12 +66,12 @@ Contract data that is stored is stored associated with a key. The key is the value that can be used at a later time to lookup the value. `Symbol`s are a space and execution efficient value to use as static keys or -names of things. They can also be used as short strings. When produced in a -`const` variable they are computed at compile time and stored in code as a -64-bit value. Their maximum character length is 10. +names of things. They can also be used as short strings. When produced using +`symbol!(...)` they are computed at compile time and stored in code as a 64-bit +value. Their maximum character length is 10. ```rust -const COUNTER: Symbol = Symbol::from_str("COUNTER"); +const COUNTER: Symbol = symbol!("COUNTER"); ``` ### Contract Data Access @@ -117,17 +117,13 @@ Open the `increment/src/test.rs` file to follow along. #[test] fn test() { let env = Env::default(); - let contract_id = FixedBinary::from_array(&env, [0; 32]); + let contract_id = BytesN::from_array(&env, &[0; 32]); env.register_contract(&contract_id, IncrementContract); + let client = IncrementContractClient::new(&env, &contract_id); - let count = increment::invoke(&env, &contract_id); - assert_eq!(count, 1); - - let count = increment::invoke(&env, &contract_id); - assert_eq!(count, 2); - - let count = increment::invoke(&env, &contract_id); - assert_eq!(count, 3); + assert_eq!(client.increment(), 1); + assert_eq!(client.increment(), 2); + assert_eq!(client.increment(), 3); } ``` @@ -147,16 +143,18 @@ env.register_contract(&contract_id, HelloContract); ``` All public functions within an `impl` block that is annotated with the -`#[contractimpl]` attribute have an `invoke` function generated, that can be -used to invoke the contract function within the environment. +`#[contractimpl]` attribute have a corresponding function generated in a +generated client type. The client type will be named the same as the contract +type with `Client` appended. For example, in our contract the contract type is +`IncrementContract`, and the client is named `IncrementContractClient`. ```rust -let count = increment::invoke(&env, &contract_id); +let client = IncrementContractClient::new(&env, &contract_id); ``` The values returned by functions can be asserted on: ```rust -assert_eq!(count, 1); +assert_eq!(client.increment(), 1); ``` ## Build the Contract diff --git a/docs/examples/liquidity-pool.mdx b/docs/examples/liquidity-pool.mdx index d34bb08b..6e77edb3 100644 --- a/docs/examples/liquidity-pool.mdx +++ b/docs/examples/liquidity-pool.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 title: Liquidity Pool --- diff --git a/docs/examples/single-offer-sale.mdx b/docs/examples/single-offer-sale.mdx index ec4027b5..0be8436f 100644 --- a/docs/examples/single-offer-sale.mdx +++ b/docs/examples/single-offer-sale.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 title: Single Offer Sale --- diff --git a/docs/getting-started/quick-start.mdx b/docs/getting-started/quick-start.mdx index 5bf6d153..3e6e1acd 100644 --- a/docs/getting-started/quick-start.mdx +++ b/docs/getting-started/quick-start.mdx @@ -39,15 +39,13 @@ The `soroban-sdk` is in early development. Report issues crate-type = ["cdylib", "rlib"] [features] -default = ["export"] -export = [] testutils = ["soroban-sdk/testutils"] [dependencies] -soroban-sdk = "0.0.3" +soroban-sdk = "0.0.4" [dev_dependencies] -first-project = { path = ".", features = ["testutils"] } +soroban-sdk = { version = "0.0.4", features = ["testutils"] } [profile.release] opt-level = "z" @@ -60,17 +58,17 @@ codegen-units = 1 lto = true ``` -The `features` list and `dev_dependencies` configure three variations that the -contract can be built with: -- By `default`, with `export` enabled, contract functions will be exported and -available to be invoked when the contract is deployed. -- Optionally without `export` enabled, contract functions will not be exported. -Types will be still exposed, which is useful when developing multiple contracts -together and this contract is to be imported into another but its functions are -not intended to be invoked. -- And `testutils` which will cause additional test utilities to be generated for -calling the contract in tests. The library itself is added as a `dev_dependencies` -so that whenever its tests are running the `testutils` feature is enabled. +The `features` list includes a `testutils` feature, which will cause additional +test utilities to be generated for calling the contract in tests. + +:::info +The `testutils` test utilities are automatically enabled inside [Rust unit +tests] inside the same crate as your contract. If you write [Rust integration +tests], or write tests from another crate, you'll need to add `#[cfg(feature = +"testutils")]` to those tests and enable the `testutils` feature when running +your tests with `cargo test --features testutils` to be able to use those test +utilities. +::: The config for the `release` profile configures the Rust toolchain to produce smaller contracts. @@ -81,33 +79,33 @@ Open the `src/lib.rs` file, and copy-paste the following code. ```rust #![no_std] -use soroban_sdk::{contractimpl, vec, Env, Symbol, Vec}; +use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec}; pub struct Contract; -#[contractimpl(export_if = "export")] +#[contractimpl] impl Contract { pub fn hello(env: Env, to: Symbol) -> Vec { - const GREETING: Symbol = Symbol::from_str("Hello"); - vec![&env, GREETING, to] + vec![&env, symbol!("Hello"), to] } } #[cfg(test)] mod test { - use super::{Contract, hello}; - use soroban_sdk::{vec, Env, FixedBinary, Symbol}; + use super::{Contract, ContractClient}; + use soroban_sdk::{symbol, vec, BytesN, Env}; #[test] fn test() { let env = Env::default(); - let contract_id = FixedBinary::from_array(&env, [0; 32]); + let contract_id = BytesN::from_array(&env, &[0; 32]); env.register_contract(&contract_id, Contract); + let client = ContractClient::new(&env, &contract_id); - let words = hello::invoke(&env, &contract_id, &Symbol::from_str("Dev")); + let words = client.hello(&symbol!("Dev")); assert_eq!( words, - vec![&env, Symbol::from_str("Hello"), Symbol::from_str("Dev"),] + vec![&env, symbol!("Hello"), symbol!("Dev"),] ); } } @@ -166,3 +164,6 @@ You should see the following output: ``` [`soroban-cli`]: setup#install-the-soroban-cli + +[Rust unit tests]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html +[Rust integration tests]: https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html diff --git a/docs/getting-started/setup.mdx b/docs/getting-started/setup.mdx index d5b0eaf4..4070513b 100644 --- a/docs/getting-started/setup.mdx +++ b/docs/getting-started/setup.mdx @@ -44,7 +44,7 @@ contract will execute on network, however in a local sandbox. Install the Soroban CLI using `cargo install`. ```sh -cargo install --locked --version 0.0.2 soroban-cli +cargo install --locked --version 0.0.4 soroban-cli ``` :::caution @@ -61,7 +61,9 @@ soroban-cli ``` ``` -soroban-cli 0.0.2 +❯ soroban-cli +soroban-cli 0.0.4 +https://soroban.stellar.org USAGE: soroban-cli @@ -70,10 +72,14 @@ OPTIONS: -h, --help Print help information SUBCOMMANDS: - invoke Invoke a contract function in a WASM file - inspect Inspect a WASM file listing contract functions, meta, etc - deploy Deploy a WASM file as a contract - version Print version information + invoke Invoke a contract function in a WASM file + inspect Inspect a WASM file listing contract functions, meta, etc + read Print the current value of a contract-data ledger entry + serve Run a local webserver for web app development and testing + deploy Deploy a WASM file as a contract + gen Generate code client bindings for a contract + version Print version information + completion Print shell completion code for the specified shell ``` diff --git a/docs/learn/authorization.mdx b/docs/learn/authorization.mdx index c9f8b0b2..d27ebdb0 100644 --- a/docs/learn/authorization.mdx +++ b/docs/learn/authorization.mdx @@ -3,8 +3,6 @@ sidebar_position: 7 title: Authorization --- -The authorization model of Soroban is closely related to its [Standard Token Contract](../standard-contracts/token-contract.mdx); the two are best understood together. - # Authorization Authorization is the process of judging which operations "should" or "should not" be allowed to occur; it is about judging _permission_. @@ -33,12 +31,12 @@ Instead, we focus on facilities provided _to_ contracts to _support them_ in mak Several mechanisms are provided to each contract to make authorization decisions. The mechanisms fall into two categories: - - Common data structures and functions in the SDK and "standard asset" contract. + - Common data structures and functions in the SDK. - Host functions that assist in validating aspects of these data structures. ### Common data structures and functions -The common data structures involved in authorization model key concepts used in authorization judgments. They are provided (and used) by the "standard asset" contract, and form a base set of functionality that should be sufficient for expressing many authorization patterns in other contracts, as well as interacting with instances of the standard asset. +The common data structures involved in authorization model key concepts used in authorization judgments. They are provided by the SDK, and form a base set of functionality that should be sufficient for expressing many authorization patterns in other contracts. These concepts are: @@ -46,13 +44,19 @@ These concepts are: - **Single-key users**, represented by a single Ed25519 public key - **Account users**, represented by a reference to an existing account on the Stellar network (which stores, in an account ledger entry, a list of weighted authorized signing keys) - **Contracts**, represented by a contract ID (not a public key) - - **Messages**: these encode a request from an identity to perform some action, such that the message can have an authorization claim made about it, and an authorization judgment applied to it. Messages include a nonce, a "domain" code number indicating the action to take, and a set of general parameters to that action. - - **Authorizations**: these are statements made _about messages_. Each authorization encodes the claim that the action described by the message is authorized to occur, on the authority of some identity. Authorizations may have three forms, corresponding to the three forms of identity: single-key, account, and contract. + - **Payloads**: these encode a request from an identity to perform + some action, such that the payload can have an authorization claim made + about it, and an authorization judgment applied to it. Payloads + include the name of the contract function being invoked, the contract ID, + the network passphrase, and a set of general parameters to that action. The + set of general parameters should include the Identifier on the Signature, as + well as the nonce. + - **Authorizations**: these are statements made _about payloads_. Each authorization encodes the claim that the action described by the payload is authorized to occur, on the authority of some identity. Authorizations may have three forms, corresponding to the three forms of identity: single-key, account, and contract. -Contracts decide when a message is authorized with two separate steps: +Contracts decide when a payload is authorized with two separate steps: - Validate the provided authorization, by some mixture of checking signatures or examining the invocation context. - - Evaluate the operation requested by the message to see if it fits the contract's unique rules for that operation. + - Evaluate the operation requested by the payload to see if it fits the contract's unique rules for that operation. The first step often requires host-function support. The second step is always contract-specific, and cannot be provided by the platform in general. diff --git a/docs/tutorials/create-a-project.mdx b/docs/tutorials/create-a-project.mdx index a1bc76b1..27e2eb88 100644 --- a/docs/tutorials/create-a-project.mdx +++ b/docs/tutorials/create-a-project.mdx @@ -44,28 +44,26 @@ The `soroban-sdk` is in early development. Report issues ```toml [dependencies] -soroban-sdk = "0.0.3" +soroban-sdk = "0.0.4" [dev_dependencies] -project-name = { path = ".", features = ["testutils"] } +soroban-sdk = { version = "0.0.4", features = ["testutils"] } [features] -default = ["export"] -export = [] testutils = ["soroban-sdk/testutils"] ``` -The `features` list and `dev_dependencies` configure three variations that the -contract can be built with: -- By `default`, with `export` enabled, contract functions will be exported and -available to be invoked when the contract is deployed. -- Optionally without `export` enabled, contract functions will not be exported. -Types will be still exposed, which is useful when developing multiple contracts -together and this contract is to be imported into another but its functions are -not intended to be invoked. -- And `testutils` which will cause additional test utilities to be generated for -calling the contract in tests. The library itself is added as a `dev_dependencies` -so that whenever its tests are running the `testutils` feature is enabled. +The `features` list includes a `testutils` feature, which will cause additional +test utilities to be generated for calling the contract in tests. + +:::info +The `testutils` test utilities are automatically enabled inside [Rust unit +tests] inside the same crate as your contract. If you write [Rust integration +tests], or write tests from another crate, you'll need to add `#[cfg(feature = +"testutils")]` to those tests and enable the `testutils` feature when running +your tests with `cargo test --features testutils` to be able to use those test +utilities. +::: ## Configure the `release` Profile @@ -92,24 +90,17 @@ lto = true The steps below should produce a `Cargo.toml` that looks like so. ```toml title="Cargo.toml" -[package] -name = "project-name" -version = "0.1.0" -edition = "2021" - [lib] crate-type = ["cdylib", "rlib"] +[features] +testutils = ["soroban-sdk/testutils"] + [dependencies] -soroban-sdk = "0.0.3" +soroban-sdk = "0.0.4" [dev_dependencies] -project-name = { path = ".", features = ["testutils"] } - -[features] -default = ["export"] -export = [] -testutils = ["soroban-sdk/testutils"] +soroban-sdk = { version = "0.0.4", features = ["testutils"] } [profile.release] opt-level = "z" @@ -121,3 +112,6 @@ panic = "abort" codegen-units = 1 lto = true ``` + +[Rust unit tests]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html +[Rust integration tests]: https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html diff --git a/docs/tutorials/testing.mdx b/docs/tutorials/testing.mdx index cafdc262..5653bc0d 100644 --- a/docs/tutorials/testing.mdx +++ b/docs/tutorials/testing.mdx @@ -17,15 +17,14 @@ Contract](write-a-contract.mdx), a simple test will look like this. ```rust #![no_std] -use soroban_sdk::{contractimpl, vec, Env, Symbol, Vec}; +use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec}; pub struct Contract; -#[contractimpl(export_if = "export")] +#[contractimpl] impl Contract { pub fn hello(env: Env, to: Symbol) -> Vec { - const GREETING: Symbol = Symbol::from_str("Hello"); - vec![&env, GREETING, to] + vec![&env, symbol!("Hello"), to] } } ``` @@ -36,26 +35,38 @@ impl Contract { ```rust #![cfg(test)] -use super::{Contract, hello}; -use soroban_sdk::{vec, Env, FixedBinary}; +use super::{Contract, ContractClient}; +use soroban_sdk::{symbol, vec, BytesN, Env}; #[test] fn test() { let env = Env::default(); - let contract_id = FixedBinary::from_array(&env, [0; 32]); + let contract_id = BytesN::from_array(&env, &[0; 32]); env.register_contract(&contract_id, Contract); + let client = ContractClient::new(&env, &contract_id); - let words = hello::invoke(&env, &contract_id, &Symbol::from_str("Dev")); + let words = client.hello(&symbol!("Dev")); assert_eq!( words, - vec![&env, Symbol::from_str("Hello"), Symbol::from_str("Dev"),] + vec![&env, symbol!("Hello"), symbol!("Dev"),] ); } ``` + +:::info +The above example is a [Rust unit test] that lives inside the `src/` directory. +Note that if you place the test in the `tests/` directory it becomes a [Rust +integration test] with the test being compiled separately. Integration tests +require `#![cfg(feature = "testutils")]` at the top of the file and to be run +with the `testutils` feature enabled, e.g. `cargo test --features testutils`, to +enable the generated Soroban test utilities. +::: + + In any test the first thing that is always required is an `Env`, which is the Soroban environment that the contract will run inside of. @@ -69,23 +80,26 @@ if the test will deploy the contract multiple times, or deploy multiple contracts, each should use their own IDs. ```rust -let contract_id = FixedBinary::from_array(&env, [0; 32]); -env.register_contract(&contract_id, HelloContract); +let contract_id = BytesN::from_array(&env, &[0; 32]); +env.register_contract(&contract_id, Contract); ``` All public functions within an `impl` block that is annotated with the -`#[contractimpl]` attribute have an `invoke` function generated, that can be -used to invoke the contract function within the environment. +`#[contractimpl]` attribute have a corresponding function generated in a +generated client type. The client type will be named the same as the contract +type with `Client` appended. For example, in our contract the contract type is +`Contract`, and the client is named `ContractClient`. ```rust -let words = hello::invoke(&env, &contract_id, &Symbol::from_str("SourBun")); +let client = ContractClient::new(&env, &contract_id); +let words = client.hello(&symbol!("Dev")); ``` The values returned by functions can be asserted on: ```rust assert_eq!( words, - vec![&env, Symbol::from_str("Hello"), Symbol::from_str("SourBun"),] + vec![&env, symbol!("Hello"), symbol!("Dev"),] ); ``` @@ -103,3 +117,6 @@ test test::test ... ok ``` Try changing the values in the test to see how it works. + +[Rust unit test]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html +[Rust integration test]: https://doc.rust-lang.org/rust-by-example/testing/integration_testing.html diff --git a/docs/tutorials/write-a-contract.mdx b/docs/tutorials/write-a-contract.mdx index 3165ebbd..b617e3a6 100644 --- a/docs/tutorials/write-a-contract.mdx +++ b/docs/tutorials/write-a-contract.mdx @@ -16,7 +16,7 @@ well suited to being deployed into small programs like those deployed to blockchains. ```rust -use soroban_sdk::{contractimpl, vec, Env, Symbol, Vec}; +use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec}; ``` The contract will need to import the types and macros that it needs from the @@ -26,13 +26,13 @@ how to setup a project. Many of the types available in typical Rust programs, such as `std::vec::Vec`, are not available, as there is no allocator and no heap memory in Soroban contracts. The `soroban-sdk` provides a variety of types like `Vec`, `Map`, -`BigInt`, `Binary`, `FixedBinary`, that all utilize the Soroban environment's -memory and native capabilities. +`BigInt`, `Bytes`, `BytesN`, `Symbol`, that all utilize the Soroban +environment's memory and native capabilities. ```rust pub struct Contract; -#[contractimpl(export_if = "export")] +#[contractimpl] impl Contract { pub fn hello(env: Env, to: Symbol) -> Vec { todo!() @@ -46,22 +46,18 @@ externally should be marked with `pub` visibility. The first argument can be an `Env` argument to get a copy of the Soroban environment, which is necessary for most things. -Implementations annotated can be configured to export the contract functions -only if a feature is enabled, with `export_if = "[feature-name]"`. - Putting those pieces together a simple contract will look like this. ```rust title="src/lib.rs" #![no_std] -use soroban_sdk::{contractimpl, vec, Env, Symbol, Vec}; +use soroban_sdk::{contractimpl, symbol, vec, Env, Symbol, Vec}; pub struct Contract; -#[contractimpl(export_if = "export")] +#[contractimpl] impl Contract { pub fn hello(env: Env, to: Symbol) -> Vec { - const GREETING: Symbol = Symbol::from_str("Hello"); - vec![&env, GREETING, to] + vec![&env, symbol!("Hello"), to] } } ```