From a23bf93552134a46ab937da06f3ee5adee5db974 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Fri, 2 Sep 2022 10:07:39 -0700 Subject: [PATCH] Add auth sdk page (#108) * noop * noop * Update sdk and cli versions and output (#87) * Update code samples in quickstart (#88) * empty * Update the tutorial for creating a project (#89) * Update the tutorial for writing a contract (#90) * Update the tutorial for testing (#91) * Update the hello_world contract (#92) * Small fix to hello contract * Update the increment contract (#93) * Small fix to increment example * Update the custom_types contract (#94) * Add auth example and update auth learn page (#95) * Put auth example earlier in list of examples (#98) (cherry picked from commit fc2fc75d34aad6eecae9d5f8de10f16268edd7c2) * Add events example (#100) * Update the cross_contract contract (#99) * Small tweak to contract call doc * Fix to contract call code sample * Fix to contract call code sample * noop to try and make it deploy properly * Reorder examples so that contract events appears higher in list * Fix headings of cross contract example (#102) * Add soroban-cli read example * Tweak language about developer discord * Some changes to the events example page (#101) * Rename standard-contracts and update token-contract (#103) * Rename Standard Contracts to Built-In Contracts * Update built-in-contracts/token-contract and examples/authorization * Fix title capitalization of built-in contracts page (#104) * Add stub for token example (#105) * Update authorization docs to line up with contract examples (#106) * Small fixes for token contract doc * Make authorization docs line up with the example * Instruct users to checkout v0.0.4 tag for examples (#107) * fix pages and sdk instructions * Add auth sdk page Co-authored-by: Tyler van der Hoeven Co-authored-by: Siddharth Suresh Co-authored-by: Jay Geng Co-authored-by: jonjove <31668823+jonjove@users.noreply.github.com> --- docs/SDKs/rust-auth.mdx | 40 ++ docs/SDKs/rust.mdx | 5 +- .../_category_.json | 4 +- docs/built-in-contracts/token-contract.mdx | 172 ++++++++ docs/examples/authorization.mdx | 370 ++++++++++++++++++ docs/examples/cross-contract-call.mdx | 236 +++++------ docs/examples/custom-types.mdx | 71 ++-- docs/examples/events.mdx | 161 ++++++++ docs/examples/hello-world.mdx | 39 +- docs/examples/increment.mdx | 50 +-- docs/examples/liquidity-pool.mdx | 6 +- docs/examples/single-offer-sale.mdx | 6 +- docs/examples/token.mdx | 10 + docs/getting-started/quick-start.mdx | 49 +-- docs/getting-started/setup.mdx | 18 +- docs/index.mdx | 2 +- docs/learn/_category_.json | 2 +- docs/learn/authorization.mdx | 20 +- docs/standard-contracts/token-contract.mdx | 320 --------------- docs/tutorials/create-a-project.mdx | 48 +-- docs/tutorials/testing.mdx | 47 ++- docs/tutorials/write-a-contract.mdx | 18 +- 22 files changed, 1083 insertions(+), 611 deletions(-) create mode 100644 docs/SDKs/rust-auth.mdx rename docs/{standard-contracts => built-in-contracts}/_category_.json (50%) create mode 100644 docs/built-in-contracts/token-contract.mdx create mode 100644 docs/examples/authorization.mdx create mode 100644 docs/examples/events.mdx create mode 100644 docs/examples/token.mdx delete mode 100644 docs/standard-contracts/token-contract.mdx diff --git a/docs/SDKs/rust-auth.mdx b/docs/SDKs/rust-auth.mdx new file mode 100644 index 00000000..de2de5c7 --- /dev/null +++ b/docs/SDKs/rust-auth.mdx @@ -0,0 +1,40 @@ +--- +sidebar_position: 2 +title: Soroban Rust Auth SDK +--- + +The `soroban-auth` Rust crate contains the Soroban Rust Auth SDK. It provides +utilities for verifying signatures on invocations of smart contracts. It is +intended for use alongside the [`soroban-sdk`]. + +[`soroban-sdk`]: rust + +:::caution +The `soroban-auth` crate is in early development. Report issues +[here](https://github.com/stellar/rs-soroban-sdk/issues/new/choose). +::: + +## SDK Documentation + +Auth documentation is available at: +https://docs.rs/soroban-auth + +## Subscribe to Releases + +Subscribe to releases on the GitHub repository: +https://github.com/stellar/rs-soroban-sdk + +## Add `soroban-auth` as a Dependency + +Add the following sections to the `Cargo.toml` to import `soroban-auth`. + +```toml +[features] +testutils = ["soroban-auth/testutils"] + +[dependencies] +soroban-auth = "0.0.4" + +[dev_dependencies] +soroban-auth = { version = "0.0.4", features = ["soroban-auth/testutils"] } +``` diff --git a/docs/SDKs/rust.mdx b/docs/SDKs/rust.mdx index 58295299..a469b338 100644 --- a/docs/SDKs/rust.mdx +++ b/docs/SDKs/rust.mdx @@ -29,5 +29,8 @@ 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" + +[dev_dependencies] +soroban-sdk = { version = "0.0.4", features = ["soroban-sdk/testutils"] } ``` diff --git a/docs/standard-contracts/_category_.json b/docs/built-in-contracts/_category_.json similarity index 50% rename from docs/standard-contracts/_category_.json rename to docs/built-in-contracts/_category_.json index 51a24210..ab621c18 100644 --- a/docs/standard-contracts/_category_.json +++ b/docs/built-in-contracts/_category_.json @@ -1,6 +1,6 @@ { - "position": 3, - "label": "Standard Contracts", + "position": 5, + "label": "Built-in Contracts", "link": { "type": "generated-index" } diff --git a/docs/built-in-contracts/token-contract.mdx b/docs/built-in-contracts/token-contract.mdx new file mode 100644 index 00000000..1b8ef264 --- /dev/null +++ b/docs/built-in-contracts/token-contract.mdx @@ -0,0 +1,172 @@ +--- +sidebar_position: 1 +title: Token Contract +--- + +# Token Contract + +The token contract is an implementation of [CAP-54 Smart Contract Standardized Asset]. + +[CAP-54 Smart Contract Standardized Asset]: https://stellar.org/protocol/cap-54 + +:::caution +The token contract is in early development, has not been audited, and is not +intended for use. Report issues +[here](https://github.com/stellar/soroban-token-contract/issues/new/choose). +::: + +## Overview + +Tokens are a vital part of blockchains, and contracts that implement token +functionality are inevitable on any smart contract platform. A built-in token +contract has a number of advantages over token contracts written by the +ecosystem. First, we can special case this contract and run it natively instead +of running in a WASM VM, reducing the cost of using the contract. Second, we +can use this built-in contract to allow "classic" Stellar assets to interoperate +with Soroban. The current iteration of the standard token contract doesn't +interoperate with "classic" Stellar assets, but this feature will be available +in the future. Note that this standard token contract does not prevent the +ecosystem from developing other token contracts if the standard is missing +functionality they require. + +The standard token contract is similar to the widely used ERC-20 token standard, +which should make it easier for existing smart contract developers to get +started on Stellar. + +## Token contract authorization semantics + +See the [authorization example](../examples/authorization) for an overview of +authorization. + +### Token operations + +The token contract contains three kinds of operations + +- getters, such as `balance`, which do not change the state of the contract +- unprivileged mutators, such as `approve` and `xfer`, which change the state of +the contract but do not require special privileges +- privileged mutators, such as `burn` and `set_admin`, which change the state of +the contract but require special privileges + +Gettors require no authorization because they do not change the state of the +contract and all contract data is public. For example, `balance` simply returns +the balance of the specified identity without changing it. + +Unprivileged mutators require authorization from some identity. The identity +which must provide authorization will vary depending on the unprivileged +mutator. For example, a "grantor" can use `approve` to allow a "spender" to spend +the grantor's money up to some limit. So for approve, the grantor must provide +authorization. Similarly, a "sender" can use `xfer` to send money to a +"recipient". So for `xfer`, the sender must provide authorization. + +Priviliged mutators require authorization from a specific privileged identity, +known as the "administrator". For example, only the administrator can `mint` more +of the asset. Similarly, only the administrator can appoint a new administrator. + +### Replay prevention + +The token contract provides replay prevention by using a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). +The messages that are signed to provide authorization contain a nonce. The +contract also stores a nonce per identity. When checking signatures, the +contract loads the nonce for the relevant identity. When an operation succeeds, +the nonce stored in the contract is incremented. This makes it impossible to +reuse a signature. + +The current nonce for an identity can be retrieved using the `nonce` contract +function. + +### Example: Signing payloads + +The [example](https://github.com/stellar/soroban-token-contract/blob/d24fbeaa3cad5406bee8b64e748a71fa38c7c2f3/src/testutils.rs#L48-L70) +from the test `Token` wrapper class demonstrate how you sign a payload for the +token contract. + +```rust +pub fn approve(&self, from: &Keypair, spender: &Identifier, amount: &BigInt) { + let from_id = to_ed25519(&self.env, from); + let nonce = self.nonce(&from_id); + + let msg = SignaturePayload::V0(SignaturePayloadV0 { + function: symbol!("approve"), + contract: self.contract_id.clone(), + network: self.env.ledger().network_passphrase(), + args: (from_id, &nonce, spender, amount).into_val(&self.env), + }); + + let auth = Signature::Ed25519(Ed25519Signature { + public_key: from.public.to_bytes().into_val(&self.env), + signature: from.sign(msg).unwrap().into_val(&self.env), + }); + TokenClient::new(&self.env, &self.contract_id).approve(&auth, &nonce, &spender, &amount) +} +``` + +## Contract Interface + +```rust +// Sets the administrator to "admin". Also sets some metadata +fn initialize(e: Env, admin: Identifier, decimal: u32, name: Bytes, symbol: Bytes); + +// Admin interface -- these functions are privileged + +// If "admin" is the administrator, burn "amount" from "from" +fn burn(e: Env, admin: Signature, nonce: BigInt, from: Identifier, amount: BigInt); + +// If "admin" is the administrator, mint "amount" to "to" +fn mint(e: Env, admin: Signature, nonce: BigInt, to: Identifier, amount: BigInt); + +// If "admin" is the administrator, set the administrator to "id" +fn set_admin(e: Env, admin: Signature, nonce: BigInt, new_admin: Identifier); + +// If "admin" is the administrator, freeze "id" +fn freeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier); + +// If "admin" is the administrator, unfreeze "id" +fn unfreeze(e: Env, admin: Signature, nonce: BigInt, id: Identifier); + +// Token Interface + +// Get the allowance for "spender" to transfer from "from" +fn allowance(e: Env, from: Identifier, spender: Identifier) -> BigInt; + +// Set the allowance to "amount" for "spender" to transfer from "from" +fn approve(e: Env, from: Signature, nonce: BigInt, spender: Identifier, amount: BigInt); + +// Get the balance of "id" +fn balance(e: Env, id: Identifier) -> BigInt; + +// Transfer "amount" from "from" to "to" +fn xfer(e: Env, from: Signature, nonce: BigInt, to: Identifier, amount: BigInt); + +// Transfer "amount" from "from" to "to", consuming the allowance of "spender" +fn xfer_from( + e: Env, + spender: Signature, + nonce: BigInt, + from: Identifier, + to: Identifier, + amount: BigInt, +); + +// Returns true if "id" is frozen +fn is_frozen(e: Env, id: Identifier) -> bool; + +// Returns the current nonce for "id" +fn nonce(e: Env, id: Identifier) -> BigInt; + +// Descriptive Interface + +// Get the number of decimals used to represent amounts of this token +fn decimals(e: Env) -> u32; + +// Get the name for this token +fn name(e: Env) -> Bytes; + +// Get the symbol for this token +fn symbol(e: Env) -> Bytes; +``` + +## Interacting with the token contract in tests + +See [interacting with contracts in tests](../learn/interacting-with-contracts#interacting-with-contracts-in-tests) +for more general information on this topic. diff --git a/docs/examples/authorization.mdx b/docs/examples/authorization.mdx new file mode 100644 index 00000000..13f70ba7 --- /dev/null +++ b/docs/examples/authorization.mdx @@ -0,0 +1,370 @@ +--- +sidebar_position: 6 +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/v0.0.4/authorization + +## Run the Example + +First go through the [Setup] process to get your development environment +configured, then clone the `v0.0.4` tag of `soroban-examples` repository: + +[Setup]: ../getting-started/setup.mdx + +``` +git clone -b v0.0.4 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 +``` + +## Dependencies + +The authorization example uses the Soroban auth SDK, and has the following +Soroban dependencies in its Cargo.toml file. + +```toml title="authorization/src/Cargo.toml +[dependencies] +soroban-sdk = "0.0.4" +soroban-auth = "0.0.4" + +[dev_dependencies] +soroban-sdk = { version = "0.0.4", features = ["testutils"] } +soroban-auth = { version = "0.0.4", features = ["testutils"] } +``` + +## Code + +```rust title="authorization/src/lib.rs" +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + SavedNum(Identifier), + Nonce(Identifier), + Admin, +} + +fn read_nonce(e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Nonce(id); + e.contract_data() + .get(key) + .unwrap_or_else(|| Ok(BigInt::zero(e))) + .unwrap() +} +struct NonceForSignature(Signature); + +impl NonceAuth for NonceForSignature { + 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 + 1); + nonce + } + + fn signature(&self) -> &Signature { + &self.0 + } +} + +pub struct ExampleContract; + +#[contractimpl] +impl ExampleContract { + /// Set the admin identifier. May be called only once. + 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); + } + + /// Save the number for an authenticated [Identifier]. + pub fn save_num(e: Env, sig: Signature, nonce: BigInt, num: BigInt) { + let auth_id = sig.get_identifier(&e); + + check_auth( + &e, + &NonceForSignature(sig), + nonce.clone(), + symbol!("save_num"), + (&auth_id, nonce, &num).into_val(&e), + ); + + e.contract_data().set(DataKey::SavedNum(auth_id), num); + } + + // The admin can write data for any Identifier + pub fn overwrite(e: Env, sig: Signature, nonce: BigInt, id: Identifier, num: BigInt) { + let auth_id = sig.get_identifier(&e); + if auth_id != e.contract_data().get_unchecked(DataKey::Admin).unwrap() { + panic!("not authorized by admin") + } + + check_auth( + &e, + &NonceForSignature(sig), + nonce.clone(), + symbol!("overwrite"), + (auth_id, nonce, &id, &num).into_val(&e), + ); + + e.contract_data().set(DataKey::SavedNum(id), num); + } + + pub fn nonce(e: Env, id: Identifier) -> BigInt { + read_nonce(&e, id) + } +} +``` + +Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/authorization + +## Authorization semantics + +This section describes a specific implementation of the general principles +discussed in [authorization](../learn/authorization.mdx). + +### Identities + +```rust +#[derive(Clone)] +#[contracttype] +pub enum Identifier { + Contract(BytesN<32>), + Ed25519(BytesN<32>), + Account(BytesN<32>), +} +``` + +The token contract understands three kinds of identities: contracts, Ed25519 +public keys, and Stellar accounts. For each kind of identity, there is a +corresponding authorization mechanism. + +#### Contract authorization + +A contract identity provides authorization simply by being the invoker of the +token contract. + +#### Ed25519 public key authorization + +```rust +#[derive(Clone)] +#[contracttype(lib = "soroban_sdk_auth")] +pub struct Ed25519Signature { + pub public_key: BytesN<32>, + pub signature: BytesN<64>, +} +``` + +An Ed25519 public key identity can provide authorization by signing an +appropriate message with the associated private key. The authorization is +just the 64-byte signature, represented as `BytesN<64>` in the contract. + +#### Stellar account authorization + +```rust +#[derive(Clone)] +#[contracttype(lib = "soroban_sdk_auth")] +pub struct AccountSignatures { + pub account_id: BytesN<32>, + pub signatures: Vec, +} +``` + +A Stellar account identity can provide authorization by signing an appropriate +message with the private keys associated with the signers of that Stellar +account. The total signing weight of the signers must exceed the medium +threshold of that Stellar account. The authorization is a vector, where each +element of the vector contains an `Ed25519Signature` corresponding to the +authorization provided by an account signer. + +### Payloads + +```rust +#[derive(Clone)] +#[contracttype(lib = "soroban_sdk_auth")] +pub struct SignaturePayloadV0 { + pub function: Symbol, + pub contract: BytesN<32>, + pub network: Bytes, + pub args: Vec, +} + +#[derive(Clone)] +#[contracttype(lib = "soroban_sdk_auth")] +pub enum SignaturePayload { + V0(SignaturePayloadV0), +} +``` + +Signatures are derived by signing the `SignaturePayload` enum, which has one +value at the moment, `V0`. `SignaturePayloadV0` uses the `function`, `contract`, +and `network` to determine where the signature can be used and the `args` to +provide additional information specific to that usage. + +:::warning +`SignaturePayloadV0` does not include any +[replay prevention](#replay-prevention) by default. We recommend using +nonce-based replay prevention and including the nonce in the `args`. +::: + +### Replay prevention + +Whenever signatures are used to permit an operation, there is a risk of +"replay". Replay occurs when a single signature is used to permit an operation +multiple times. Such a situation can be catastrophic. For example, imagine that +you sign a message permitting 1 dollar to be sent to an acquantaince. If there +were no replay prevention, then a malicious acquantaince could use that message +to repeatedly transfer 1 dollar from you to them. In the end, your bank account +would be empty. + +Contracts can provide replay prevention by using a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). +The payloads that are signed to provide authorization contain a nonce. The +contract also stores a nonce (typically per identity). When checking signatures, the +contract loads the nonce for the relevant identity. When an operation succeeds, +the nonce stored in the contract is incremented. This makes it impossible to +reuse a signature. + +## 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 { + SavedNum(Identifier), + Nonce(Identifier), + Admin, +} + +fn read_nonce(e: &Env, id: Identifier) -> BigInt { + let key = DataKey::Nonce(id); + e.contract_data() + .get(key) + .unwrap_or_else(|| Ok(BigInt::zero(e))) + .unwrap() +} + +struct NonceForSignature(Signature); + +impl NonceAuth for NonceForSignature { + 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 + 1); + nonce + } + + fn signature(&self) -> &Signature { + &self.0 + } +} +``` + +### Check authorization in contract function +The `save_num` function stores a number in a `DataKey::SavedNum` 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 `NonceForSignature`. +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 `sig` and the nonce. + +```rust +/// Save the number for an authenticated [Identifier]. +pub fn save_num(e: Env, sig: Signature, nonce: BigInt, num: BigInt) { + let auth_id = sig.get_identifier(&e); + + check_auth( + &e, + &NonceForSignature(sig), + nonce.clone(), + symbol!("save_num"), + (&auth_id, nonce, &num).into_val(&e), + ); + + e.contract_data().set(DataKey::SavedNum(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 the number for any [Identifier] +pub fn overwrite(e: Env, sig: Signature, nonce: BigInt, id: Identifier, num: BigInt) { + let auth_id = sig.get_identifier(&e); + if auth_id != e.contract_data().get_unchecked(DataKey::Admin).unwrap() { + panic!("not authorized by admin") + } + + check_auth( + &e, + &NonceForSignature(sig), + nonce.clone(), + symbol!("overwrite"), + (auth_id, nonce, &id, &num).into_val(&e), + ); + + e.contract_data().set(DataKey::SavedNum(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/cross-contract-call.mdx b/docs/examples/cross-contract-call.mdx index da0e0da9..3d548f52 100644 --- a/docs/examples/cross-contract-call.mdx +++ b/docs/examples/cross-contract-call.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 title: Cross Contract Calls --- @@ -7,29 +7,31 @@ The [cross contract call example] demonstrates how to call a contract from another contract. :::info -In this example both contracts will be compiled into a single -contract binary, but the same principles apply for contracts compiled -separately. +In this example there are two contracts that are compiled separately, deployed +separately, and then tested together. There are a variety of ways to develop and +test contracts with dependencies on other contracts, and the Soroban SDK and +tooling is still building out the tools to support these workflows. Feedback +appreciated [here](https://github.com/stellar/rs-soroban-sdk/issues/new/choose). ::: -[cross contract call example]: https://github.com/stellar/soroban-examples/tree/main/cross_contract_calls +[cross contract call example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/cross_contract_calls ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the examples repository: +configured, then clone the `v0.0.4` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone https://github.com/stellar/soroban-examples +git clone -b v0.0.4 https://github.com/stellar/soroban-examples ``` -To run the tests for the example, navigate to the `cross_contract_calls` +To run the tests for the example, navigate to the `cross_contract/contract_b` directory, and use `cargo test`. ``` -cd cross_contract_calls +cd cross_contract/contract_b cargo test ``` @@ -42,10 +44,10 @@ test test::test ... ok ## Code -```rust title="cross_contract_calls/src/a.rs" +```rust title="cross_contract/contract_a/src/lib.rs" pub struct ContractA; -#[contractimpl(export_if = "export")] +#[contractimpl] impl ContractA { pub fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") @@ -53,40 +55,51 @@ impl ContractA { } ``` -```rust title="cross_contract_calls/src/b.rs" +```rust title="cross_contract/contract_b/src/lib.rs" +mod contract_a { + soroban_sdk::contractimport!(file = "../../target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm"); +} + pub struct ContractB; -#[contractimpl(export_if = "export")] +#[contractimpl] impl ContractB { - pub fn add_with(env: Env, x: u32, y: u32, contract_id: FixedBinary<32>) -> u32 { - env.invoke_contract( - &contract_id, - &Symbol::from_str("add"), - vec![&env, x.into_env_val(&env), y.into_env_val(&env)], - ) + pub fn add_with(env: Env, contract_id: BytesN<32>, x: u32, y: u32) -> u32 { + let client = contract_a::ContractClient::new(&env, contract_id); + client.add(&x, &y) } } ``` -Ref: https://github.com/stellar/soroban-examples/tree/main/cross_contract_calls +Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/cross_contract ## How it Works -Cross contract calls are made by invoking another contract by its contract ID, -specifying the function to call as a `Symbol`, and passing a series of -arguments. +Cross contract calls are made by invoking another contract by its contract ID. + +Contracts to invoke can be imported into your contract with the use of +`contractimport!(file = "...")`. The import will code generate: +- A `ContractClient` type that can be used to invoke functions on the contract. +- Any types in the contract that were annotated with `#[contracttype]`. -Open the `cross_contract_calls/src/lib.rs` file to follow along. +:::tip +The `contractimport!` macro will generate the types in the module it is used, so +it's a good idea to use the macro inside a `mod { ... }` block, or inside its +own file, so that the names of generated types don't collide with names of types +in your own contract. +::: -## The Contract to be Called +Open the files above to follow along. -The contract to be called is a simple contract that accepts `x` and `y` -parameters, adds them together and returns the result. +### Contract A: The Contract to be Called -```rust title="cross_contract_calls/src/a.rs" +The contract to be called is Contract A. It is a simple contract that accepts +`x` and `y` parameters, adds them together and returns the result. + +```rust title="cross_contract/contract_a/src/lib.rs" pub struct ContractA; -#[contractimpl(export_if = "export")] +#[contractimpl] impl ContractA { pub fn add(x: u32, y: u32) -> u32 { x.checked_add(y).expect("no overflow") @@ -101,63 +114,63 @@ Rust's primitive integer types all have checked operations available as functions with the prefix `checked_`. ::: -## The Contract doing the Calling +### Contract B: The Contract doing the Calling -The contract that does the calling accepts the same parameters to pass through, -but also accepts a contract ID of the contract to call. In many contracts the -contract to call might have been stored as contract data and be retrieved, but -in this simple example it is being passed in as a parameter each time. +The contract that does the calling is Contract B. It accepts a contract ID that +it will call, as well as the same parameters to pass through. In many contracts +the contract to call might have been stored as contract data and be retrieved, +but in this simple example it is being passed in as a parameter each time. -The `Env` `invoke_contract` function is used to invoke the other contract. +The contract imports Contract A into the `contract_a` module. -The function name of the other contract is specified as a `Symbol`. +The `contract_a::ContractClient` is constructed pointing at the contract ID +passed in. -The arguments are specified as a `Vec`, which can be created using the -the `vec![&env, ...]` macro. Each value can be converted into an `EnvVal` using -the `.into_env_val(&env)` function. +The client is used to execute the `add` function with the `x` and `y` parameters +on Contract A. ```rust title="cross_contract_calls/src/a.rs" +mod contract_a { + soroban_sdk::contractimport!( + file = "../../target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm" + ); +} + pub struct ContractB; -#[contractimpl(export_if = "export")] +#[contractimpl] impl ContractB { - pub fn add_with(env: Env, x: u32, y: u32, contract_id: FixedBinary<32>) -> u32 { - env.invoke_contract( - &contract_id, - &Symbol::from_str("add"), - vec![&env, x.into_env_val(&env), y.into_env_val(&env)], - ) + pub fn add_with(env: Env, contract_id: BytesN<32>, x: u32, y: u32) -> u32 { + let client = contract_a::ContractClient::new(&env, contract_id); + client.add(&x, &y) } } ``` -## Tests +### Tests -Open the `cross_contract_calls/src/test.rs` file to follow along. +Open the `cross_contract/contract_b/src/test.rs` file to follow along. -```rust title="cross_contract_calls/src/test.rs" +```rust title="cross_contract/contract_b/src/test.rs" #[test] fn test() { let env = Env::default(); - let contract_a = FixedBinary::from_array(&env, [0; 32]); - env.register_contract(&contract_a, ContractA); - - let contract_b = FixedBinary::from_array(&env, [1; 32]); - env.register_contract(&contract_b, ContractB); - - // Invoke 'add_with' on contract B. - let sum = add_with::invoke( - &env, - &contract_b, - // Value X. - &5, - // Value Y. - &7, - // Tell contract B to call contract A. - &contract_a, - ); + // Define IDs for contract A and B. + let contract_a_id = BytesN::from_array(&env, &[0; 32]); + let contract_b_id = BytesN::from_array(&env, &[1; 32]); + + // Register contract A using the imported WASM. + env.register_contract_wasm(&contract_a_id, contract_a::WASM); + + // Register contract B defined in this crate. + env.register_contract(&contract_b_id, ContractB); + + // Create a client for calling contract B. + let client = ContractBClient::new(&env, &contract_b_id); + // Invoke contract B via its client. Contract B will invoke contract A. + let sum = client.add_with(&contract_a_id, &5, &7); assert_eq!(sum, 12); } ``` @@ -170,37 +183,44 @@ let env = Env::default(); ``` Contracts must be registered with the environment with a contract ID, which is a -32-byte value. Both contracts `a` and `b` are registered with unique IDs. +32-byte value. Both contracts `a` and `b` have IDs defined that are used in the +rest of the test. ```rust -let contract_a = FixedBinary::from_array(&env, [0; 32]); -env.register_contract(&contract_a, ContractA); +let contract_a_id = BytesN::from_array(&env, &[0; 32]); +let contract_b_id = BytesN::from_array(&env, &[1; 32]); ``` +Contract A is registered with the environment using the imported WASM. + ```rust -let contract_b = FixedBinary::from_array(&env, [1; 32]); -env.register_contract(&contract_b, ContractB); +env.register_contract_wasm(&contract_a_id, contract_a::WASM); +``` + +Contract B is registered with the environment using the type that is in the +crate. + +```rust +env.register_contract(&contract_b_id, ContractB); ``` 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 +`ContractB`, and the client is named `ContractBClient`. The client can be +constructed and used in the same way that client generated for Contract A can +be. + +```rust +let client = ContractBClient::new(&env, &contract_b_id); +``` -The test invokes contract `b`'s `add_with` function with two values to add, and -the contract ID of contract `a`. +The client is used to invoke the `add_with` function on Contract B. Contract B +will invoke Contract A, and the result will be returned. ```rust -// Invoke 'add_with' on contract B. -let sum = add_with::invoke( - &env, - &contract_b, - // Value X. - &5, - // Value Y. - &7, - // Tell contract B to call contract A. - &contract_a, -); +let sum = client.add_with(&contract_a_id, &5, &7); ``` The test asserts that the result that is returned is as we expect. @@ -209,49 +229,53 @@ The test asserts that the result that is returned is as we expect. assert_eq!(sum, 12); ``` -## Build the Contract +## Build the Contracts -To build the contract into a `.wasm` file, use the `cargo build` command. +To build the contract into a `.wasm` file, use the `cargo build` command. Both +`contract_call/contract_a` and `contract_call/contract_b` must be built. ```sh cargo build --target wasm32-unknown-unknown --release ``` -A `.wasm` file should be outputted in the `../target` directory: +Both `.wasm` files should be found in the `../target` directory after building +both contracts: + +``` +target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm +``` ``` -../target/wasm32-unknown-unknown/release/soroban_cross_contract_calls_contract.wasm +target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm ``` ## Run the Contract If you have [`soroban-cli`] installed, you can invoke contract functions. Both -contracts live in the same compiled contract and so we'll deploy the contract -twice. The first deployment we'll use as the callee, and the second as the -caller. +contracts must be deployed. ```sh soroban-cli deploy \ - --wasm ../target/wasm32-unknown-unknown/release/soroban_cross_contract_calls_contract.wasm \ - --id 0 + --wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm \ + --id a ``` ```sh soroban-cli deploy \ - --wasm ../target/wasm32-unknown-unknown/release/soroban_cross_contract_calls_contract.wasm \ - --id 1 + --wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm \ + --id b ``` -Invoke contract `1`'s `add_with` function, passing in values for `x` and `y` -(e.g. as `5` and `7`), and then pass in the contract ID of contract 0. +Invoke Contract B's `add_with` function, passing in values for `x` and `y` +(e.g. as `5` and `7`), and then pass in the contract ID of Contract A. ```sh soroban-cli invoke \ - --id 1 \ + --id b \ --fn add_with \ + --arg a \ --arg 5 \ - --arg 7 \ - --arg '[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]' + --arg 7 ``` The following output should occur using the code above. @@ -260,11 +284,5 @@ The following output should occur using the code above. 12 ``` -Contract `1`'s `add_with` function invoked contract `0`'s `add` function to do +Contract B's `add_with` function invoked Contract A's `add` function to do the addition. - -:::info -The `soroban-cli` is under active development and at this time accepts contract -IDs as JSON formatted number arrays. That's what the long `[0,0,0,...]` value is -in the invoke command above. -::: diff --git a/docs/examples/custom-types.mdx b/docs/examples/custom-types.mdx index 0f3d7977..d1d26209 100644 --- a/docs/examples/custom-types.mdx +++ b/docs/examples/custom-types.mdx @@ -7,17 +7,17 @@ The [custom types example] demonstrates how to define your own data structures that can be stored on the ledger, or used as inputs and outputs to contract invocations. -[custom types example]: https://github.com/stellar/soroban-examples/tree/main/custom_types +[custom types example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/custom_types ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the examples repository: +configured, then clone the `v0.0.4` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone https://github.com/stellar/soroban-examples +git clone -b v0.0.4 https://github.com/stellar/soroban-examples ``` To run the tests for the example, navigate to the `custom_types` directory, and use `cargo test`. @@ -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 { @@ -70,7 +70,7 @@ impl CustomTypesContract { } ``` -Ref: https://github.com/stellar/soroban-examples/tree/main/custom_types +Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/custom_types ## How it Works @@ -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/events.mdx b/docs/examples/events.mdx new file mode 100644 index 00000000..62a3e019 --- /dev/null +++ b/docs/examples/events.mdx @@ -0,0 +1,161 @@ +--- +sidebar_position: 4 +title: Events +--- + +The [events example] demonstrates how to publish events from a contract. + +[events example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/events + +## Run the Example + +First go through the [Setup] process to get your development environment +configured, then clone the `v0.0.4` tag of `soroban-examples` repository: + +[Setup]: ../getting-started/setup.mdx + +``` +git clone -b v0.0.4 https://github.com/stellar/soroban-examples +``` + +To run the tests for the example, navigate to the `events` directory, and use `cargo test`. + +``` +cd events +cargo test +``` + +You should see the output: + +``` +running 1 test +test test::test ... ok +``` + +## Code + +```rust title="events/src/lib.rs" +#![no_std] +use soroban_sdk::{contractimpl, map, symbol, Env, Symbol}; + +pub struct EventsContract; + +#[contractimpl] +impl EventsContract { + pub fn hello(env: Env, to: Symbol) -> () { + let events = env.events(); + let topics = (symbol!("Hello"), to); + let data = map![&env, (1u32, 2u32)]; + events.publish(topics, data); + } +} +``` +Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/events + +## How it Works + +This example contract is similar to the [hello world example]. It also contains one +contract function named `hello`. However, instead of returning the greeting message to the caller, it +publishes the message (along with some data) as a contract event. + +[hello world example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/hello_world + +Contract events let smart contract developers emit information about what their +contract is doing. + +Contracts can publish events using the `Events` object retrieved from the +environment. + +```rust +let events = env.events(); +``` + +### Event Topics +An event may contain up to four topics, each topic can be any type except: +- `Vec` +- `Map` +- `Bytes` longer than 32 bytes +- `[contracttype]` + + +Topics are conveniently defined using a tuple. In the sample code two topics of +`Symbol` type are used. + +```rust +let topics = (symbol!("Hello"), to); +``` + +### Event Data +An event also contains a data object of any value or type including types +defined by contracts using `[contracttype]`. In the sample code the data is a +small map where key `1` maps to value `2`. + +```rust +let data = map![&env, (1u32, 2u32)]; +``` + +:::tip +The topics don't have to be made of the same type. You can mix different types as long as the total topic count stays below the limit. +::: + +### Publishing +Publishing an event is done by calling the `publish` function and giving it the +topics and data. The function returns nothing on success, and panics on +failure. Possible failure reasons can include malformed inputs (e.g. topic count +exceeds limit) and running over the resource budget (TBD). Once successfully +published, the new event will be recorded by the [host environment]. + +```rust +events.publish(topics, data); +``` + +[host environment]: ../learn/high-level-overview#host-environment + +## Build the Contract + +To build the contract, use the `cargo build` command. + +```sh +cargo build --target wasm32-unknown-unknown --release +``` + +A `.wasm` file should be outputted in the `../target` directory: + +``` +../target/wasm32-unknown-unknown/release/soroban_events_contract.wasm +``` + +## Run the Contract + +If you have [`soroban-cli`] installed, you can invoke contract functions in the +using it. + +```sh +soroban-cli invoke \ + --wasm ../target/wasm32-unknown-unknown/release/soroban_events_contract.wasm \ + --id 1 \ + --fn hello \ + --arg friend +``` + +The following output should occur using the code above. + +```json +null +Event #0: +{"ext":"v0","contractId":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"type":"contract","body":{"v0":{ + "topics":[{"symbol":[72,101,108,108,111]},{"symbol":[102,114,105,101,110,100]}], + "data":{"object":{"map":[{"key":{"u32":1},"val":{"u32":2}}]}}}}} +``` + +The `null` just indicates the `hello` contract function returned nothing (which +means success). + +A single event `Event #0` is outputted, which is the contract event the contract +published. The event contains the two topics, each a `symbol` (displayed as +bytes), and the data object containing the `map`. + +:::info +The soroban-cli is under active development and at this time outputs events in +an unstable JSON format. The format is TBD. +::: diff --git a/docs/examples/hello-world.mdx b/docs/examples/hello-world.mdx index 5edeec63..05f79d27 100644 --- a/docs/examples/hello-world.mdx +++ b/docs/examples/hello-world.mdx @@ -6,17 +6,17 @@ title: Hello World The [hello world example] demonstrates how to write a simple contract, with a single function that takes one input and returns it as an output. -[hello world example]: https://github.com/stellar/soroban-examples/tree/main/hello_world +[hello world example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/hello_world ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the examples repository: +configured, then clone the `v0.0.4` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone https://github.com/stellar/soroban-examples +git clone -b v0.0.4 https://github.com/stellar/soroban-examples ``` To run the tests for the example, navigate to the `hello_world` directory, and use `cargo test`. @@ -37,19 +37,18 @@ 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] } } ``` -Ref: https://github.com/stellar/soroban-examples/tree/main/hello_world +Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/hello_world ## How it Works @@ -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..98d8896a 100644 --- a/docs/examples/increment.mdx +++ b/docs/examples/increment.mdx @@ -6,17 +6,17 @@ title: Increment The [increment example] demonstrates how to write a simple contract, with a single function that increments an internal counter and returns the value. -[increment example]: https://github.com/stellar/soroban-examples/tree/main/increment +[increment example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/increment ## Run the Example First go through the [Setup] process to get your development environment -configured, then clone the examples repository: +configured, then clone the `v0.0.4` tag of `soroban-examples` repository: [Setup]: ../getting-started/setup.mdx ``` -git clone https://github.com/stellar/soroban-examples +git clone -b v0.0.4 https://github.com/stellar/soroban-examples ``` To run the tests for the example, navigate to the `increment` directory, and use `cargo test`. @@ -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 @@ -54,7 +54,7 @@ impl IncrementContract { } } ``` -Ref: https://github.com/stellar/soroban-examples/tree/main/increment +Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/increment ## How it Works @@ -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 @@ -192,3 +190,9 @@ The following output should occur using the code above. ``` Run it a few more times to watch the count change. + +Use the `soroban-cli` to inspect what the counter is after a few runs. + +```sh +soroban-cli read --id 1 --key COUNTER +``` diff --git a/docs/examples/liquidity-pool.mdx b/docs/examples/liquidity-pool.mdx index d34bb08b..ea3977bb 100644 --- a/docs/examples/liquidity-pool.mdx +++ b/docs/examples/liquidity-pool.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 9 title: Liquidity Pool --- @@ -7,5 +7,5 @@ The [liquidity pool example] demonstrates how to write a constant product liquidity pool contract. The comments in the [source code] explain how the contract should be used. -[liquidity pool example]: https://github.com/stellar/soroban-examples/tree/main/liquidity_pool -[source code]: https://github.com/stellar/soroban-examples/blob/main/liquidity_pool/src/lib.rs#L143 +[liquidity pool example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/liquidity_pool +[source code]: https://github.com/stellar/soroban-examples/blob/v0.0.4/liquidity_pool/src/lib.rs#L143 diff --git a/docs/examples/single-offer-sale.mdx b/docs/examples/single-offer-sale.mdx index ec4027b5..4ecad406 100644 --- a/docs/examples/single-offer-sale.mdx +++ b/docs/examples/single-offer-sale.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 8 title: Single Offer Sale --- @@ -7,5 +7,5 @@ The [single offer sale example] demonstrates how to write a contract that allows a seller to set up an offer to sell token A for token B. The comments in the [source code] explain how the contract should be used. -[single offer sale example]: https://github.com/stellar/soroban-examples/tree/main/single_offer -[source code]: https://github.com/stellar/soroban-examples/blob/main/single_offer/src/lib.rs#L131 +[single offer sale example]: https://github.com/stellar/soroban-examples/tree/v0.0.4/single_offer +[source code]: https://github.com/stellar/soroban-examples/blob/v0.0.4/single_offer/src/lib.rs#L131 diff --git a/docs/examples/token.mdx b/docs/examples/token.mdx new file mode 100644 index 00000000..9b665975 --- /dev/null +++ b/docs/examples/token.mdx @@ -0,0 +1,10 @@ +--- +sidebar_position: 7 +title: Token +--- + +The [token example] demonstrates how to write a token contract that implements +the same logic as the [built-in token contract]. + +[token example]: https://github.com/stellar/soroban-token-contract/tree/v0.0.4/ +[built-in token contract]: ../built-in-contracts/token-contract 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/index.mdx b/docs/index.mdx index f9a3144a..d2f1bf06 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -21,7 +21,7 @@ We’re releasing this very early version of Soroban because we believe it’s i Please test it out, see what you can do, and let us know what you think. Keep in mind a lot will change between now and the production release, so expect your code to break, and prepare for updates to shift things. Experiment, but don't build to last just yet. ## How to leave feedback -Please feel free to file issues in the Soroban repos, or, if you prefer, to raise them in the jump-cannon-dev channel in the [Stellar Developer Discord](https://discord.gg/sp8zfb4qH6) (Jump Cannon was the secret project name for the work that led to Soroban). The more specific and detailed your questions and suggestions are, the more helpful they will be +Please feel free to file issues in the Soroban repos, or, if you prefer, to raise them in the `soroban` channel in the [Stellar Developer Discord](https://discord.gg/sp8zfb4qH6). ## Why it's called Soroban The Japanese soroban is the smallest and simplest abacus. Its design is intentionally compact and minimalist, focused on doing the essentials — and doing them well. It set the global standard for abacuses and is still in practical use today. The name aligns with the goals of this platform because we, too, want to build something that focuses on doing the essentials well, that sets a global standard, and that proves useful far into the future. diff --git a/docs/learn/_category_.json b/docs/learn/_category_.json index 54e1c616..193d2dbe 100644 --- a/docs/learn/_category_.json +++ b/docs/learn/_category_.json @@ -1,5 +1,5 @@ { - "position": 5, + "position": 6, "label": "Learn", "link": { "type": "generated-index" 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/standard-contracts/token-contract.mdx b/docs/standard-contracts/token-contract.mdx deleted file mode 100644 index 50260235..00000000 --- a/docs/standard-contracts/token-contract.mdx +++ /dev/null @@ -1,320 +0,0 @@ ---- -sidebar_position: 1 -title: Token Contract ---- - -# Standard Token Contract - -The token contract is an implementation of [CAP-54 Smart Contract Standardized Asset]. - -[CAP-54 Smart Contract Standardized Asset]: https://stellar.org/protocol/cap-54 - -:::caution -The token contract is in early development, has not been audited, and is not -intended for use. Report issues -[here](https://github.com/stellar/soroban-token-contract/issues/new/choose). -::: - -## Overview - -Tokens are a vital part of blockchains, and contracts that implement token -functionality are inevitable on any smart contract platform. A standard token -contract has a number of advantages over token contracts written by the -ecosystem. First, we can special case this contract and run it natively instead -of running in a WASM VM, reducing the cost of using the contract. Second, we -can use this native contract to allow "classic" Stellar assets to interoperate -with Soroban. The current iteration of the standard token contract doesn't run -natively or interoperate with "classic" Stellar assets, but these improvements -will be made in the future. Note that this standard token contract does not -prevent the ecosystem from developing other token contracts if the standard is -missing functionality they require. - -The standard token contract is similar to the widely used ERC-20 token standard, -which should make it easier for existing smart contract developers to get -started on Stellar. - -## Token contract authorization semantics - -This section describes a specific implementation of the general principles -discussed in [authorization](../learn/authorization.mdx). - -### Identities - -```rust -#[derive(Clone)] -#[contracttype] -pub enum Identifier { - Contract(U256), - Ed25519(U256), - Account(U256), -} -``` - -The token contract understands three kinds of identities: contracts, Ed25519 -public keys, and Stellar accounts. For each kind of identity, there is a -corresponding authorization mechanism. - -#### Contract authorization - -A contract identity provides -authorization simply by being the invoker of the token contract. - -#### Ed25519 public key authorization - -An Ed25519 public key identity can provide authorization by signing an -appropriate message with the associated private key. The authorization is -just the 512-bit signature, referred to as a `U512` in the contract. - -#### Stellar account authorization - -```rust -#[derive(Clone)] -#[contracttype] -pub struct KeyedEd25519Signature { - pub public_key: U256, - pub signature: U512, -} - -pub type AccountAuthorization = Vec; -``` - -A Stellar account identity can provide authorization by signing an appropripate -message with the private keys associated with the signers of that Stellar -account. The total signing weight of the signers must exceed the medium -threshold of that Stellar account. The authorization is a vector, where each -element of the vector contains a public key used to sign the message and the -corresponding 512-bit signature. - -### Token operations - -The token contract contains three kinds of operations - -- getters, such as `balance`, which do not change the state of the contract -- unprivileged mutators, such as `approve` and `xfer`, which change the state of -the contract but do not require special privileges -- privileged mutators, such as `burn` and `set_admin`, which change the state of -the contract but require special privileges - -Gettors require no authorization because they do not change the state of the -contract and all contract data is public. For example, `balance` simply returns -the balance of the specified identity without changing it. - -Unprivileged mutators require authorization from some identity. The identity -which must provide authorization will vary depending on the unprivileged -mutator. For example, a "grantor" can use `approve` to allow a "spender" to spend -the grantor's money up to some limit. So for approve, the grantor must provide -authorization. Similarly, a "sender" can use `xfer` to send money to a -"recipient". So for `xfer`, the sender must provide authorization. The -authorization for an unprivileged mutator is a [KeyedAuthorization](#keyedauthorization) -because the identity that is providing the authorization must be specified. - -Priviliged mutators require authorization from a specific privileged identity, -known as the "administrator". For example, only the administrator can `mint` more -of the asset. Similarly, only the administrator can appoint a new administrator. -The authorization for a privileged mutator is an [Authorization](#authorization) -because the identity that is providing the authorization is known to be the -administrator. - -### Messages - -```rust -#[derive(Clone)] -#[contracttype] -pub struct MessageV0 { - pub nonce: BigInt, - pub domain: u32, - pub parameters: Vec, -} - -#[derive(Clone)] -#[contracttype] -pub enum Message { - V0(MessageV0), -} -``` - -Those signatures are derived by signing the `Message` enum, which has one value -at the moment, `V0`. `MessageV0` requires the [current nonce](#replay-prevention) -for the identity providing authorization, a -[domain](https://github.com/stellar/soroban-token-contract/blob/main/src/cryptography.rs), -and the function parameters. - -### Replay prevention - -Whenever signatures are used to permit an operation, there is a risk of -"replay". Replay occurs when a single signature is used to permit an operation -multiple times. Such a situation can be catastrophic. For example, imagine that -you sign a message permitting 1 dollar to be sent to an acquantaince. If there -were no replay prevention, then a malicious acquantaince could use that message -to repeatedly transfer 1 dollar from you to them. In the end, your bank account -would be empty. - -The token contract provides replay prevention by using a [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce). -The messages that are signed to provide authorization contain a nonce. The -contract also stores a nonce per identity. When checking signatures, the -contract loads the nonce for the relevant identity. When an operation succeeds, -the nonce stored in the contract is incremented. This makes it impossible to -reuse a signature. - -The current nonce for an identity can be retrieved using the nonce contract -function. - -### Authorization - -```rust -#[derive(Clone)] -#[contracttype] -pub enum Authorization { - Contract, - Ed25519(U512), - Account(AccountAuthorization), -} -``` - -Used by `mint`, `burn`, `freeze`, `unfreeze`, and `set_admin`. The content of -the authorization depends on the [identity](#identities) of the administrator. - -### KeyedAuthorization - -```rust -#[derive(Clone)] -#[contracttype] -pub struct KeyedEd25519Signature { - pub public_key: U256, - pub signature: U512, -} - -#[derive(Clone)] -#[contracttype] -pub struct KeyedAccountAuthorization { - pub public_key: U256, - pub signatures: AccountAuthorization, -} - -#[derive(Clone)] -#[contracttype] -pub enum KeyedAuthorization { - Contract, - Ed25519(KeyedEd25519Signature), - Account(KeyedAccountAuthorization), -} -``` - -Used by `allowance`, `approve`, `xfer`, and `xfer_from`. The content of -the authorization depends on the [identity](#identity) of the administrator. -This is similar to an [Authorization](#authorization) but also includes -additional information so the contract knows what identity is providing -authorization. - -### Example: Signing messages - -The two -[examples](https://github.com/stellar//soroban-token-contract/blob/37e2cd3580f8e28beea9e8f9194b9cf8547472dd/src/testutils.rs#L55) -from the test `Token` wrapper class demonstrate how you sign a contract -function that expects an `Authorization` and a `KeyedAuthorization` type. - -#### Authorization -The admin must match the `Identifier` that was used to `initialize` the contract. -```rust -pub fn mint(&self, admin: &Keypair, to: &Identifier, amount: &BigInt) { - let mut args: Vec = Vec::new(&self.env); - args.push(to.clone().into_env_val(&self.env)); - args.push(amount.clone().into_env_val(&self.env)); - let msg = Message::V0(MessageV0 { - nonce: self.nonce(&to_ed25519(&self.env, admin)), - domain: Domain::Mint as u32, - parameters: args, - }); - let auth = Authorization::Ed25519(admin.sign(msg).unwrap().into_val(&self.env)); - mint(&self.env, &self.contract_id, &auth, to, amount) -} -``` - -#### KeyedAuthorization -```rust - pub fn approve(&self, from: &Keypair, spender: &Identifier, amount: &BigInt) { - let mut args: Vec = Vec::new(&self.env); - args.push(spender.clone().into_env_val(&self.env)); - args.push(amount.clone().into_env_val(&self.env)); - let msg = Message::V0(MessageV0 { - nonce: self.nonce(&to_ed25519(&self.env, from)), - domain: Domain::Approve as u32, - parameters: args, - }); - let auth = KeyedAuthorization::Ed25519(KeyedEd25519Authorization { - public_key: from.public.to_bytes().into_val(&self.env), - signature: from.sign(msg).unwrap().into_val(&self.env), - }); - approve(&self.env, &self.contract_id, &auth, spender, amount) -} -``` - -## Contract Interface - -```rust -// Admin interface - -// Sets the administrator to "admin". Also sets some metadata -fn initialize(e: Env, admin: Identifier, decimal: u32, name: Binary, symbol: Binary); - -// If "admin" is the administrator, burn "amount" from "from" -fn burn(e: Env, admin: Authorization, from: Identifier, amount: BigInt); - -// If "admin" is the administrator, mint "amount" to "to" -fn mint(e: Env, admin: Authorization, to: Identifier, amount: BigInt); - -// If "admin" is the administrator, set the administrator to "id" -fn set_admin(e: Env, admin: Authorization, new_admin: Identifier); - -// If "admin" is the administrator, freeze "id" -fn freeze(e: Env, admin: Authorization, id: Identifier); - -// If "admin" is the administrator, unfreeze "id" -fn unfreeze(e: Env, admin: Authorization, id: Identifier); - -// Token Interface - -// Get the allowance for "spender" to transfer from "from" -fn allowance(e: Env, from: Identifier, spender: Identifier) -> BigInt; - -// Set the allowance to "amount" for "spender" to transfer from "from" -fn approve(e: Env, from: KeyedAuthorization, spender: Identifier, amount: BigInt); - -// Get the balance of "id" -fn balance(e: Env, id: Identifier) -> BigInt; - -// Transfer "amount" from "from" to "to" -fn xfer(e: Env, from: KeyedAuthorization, to: Identifier, amount: BigInt); - -// Transfer "amount" from "from" to "to", consuming the allowance of "spender" -fn xfer_from(e: Env, spender: KeyedAuthorization, from: Identifier, to: Identifier, amount: BigInt); - -// Returns true if "id" is frozen -fn is_frozen(e: Env, id: Identifier) -> bool; - -// Returns the current nonce for "id" -fn nonce(e: Env, id: Identifier) -> BigInt; - -// Descriptive Interface - -// Get the number of decimals used to represent amounts of this token -fn decimals(e: Env) -> u32; - -// Get the name for this token -fn name(e: Env) -> Binary; - -// Get the symbol for this token -fn symbol(e: Env) -> Binary; -``` - -## Interacting with the token contract in tests - -See [interacting with contracts in tests](../learn/interacting-with-contracts#interacting-with-contracts-in-tests) -for more general information on this topic. - -This [test -case](https://github.com/stellar/soroban-token-contract/blob/37e2cd3580f8e28beea9e8f9194b9cf8547472dd/tests/test.rs#L20) -shows how the token contract can be used easily in tests. Note that the actual call into the -contract is in the -[Token](https://github.com/stellar/soroban-token-contract/blob/37e2cd3580f8e28beea9e8f9194b9cf8547472dd/src/testutils.rs#L26) -wrapper test class. 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] } } ```