From 72cfa62b01fbada9359c3fe80bc4ebc60adebc38 Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Fri, 20 Dec 2024 16:39:27 -0500 Subject: [PATCH] Synchronize example docs and v22 examples. This omits token and liquidity pool examples. --- docs/build/apps/guestbook/frontend.mdx | 49 +-- docs/build/apps/guestbook/setup-passkeys.mdx | 47 ++- .../example-contracts/auth.mdx | 4 +- .../example-contracts/cross-contract-call.mdx | 12 +- .../example-contracts/custom-account.mdx | 98 +++--- .../example-contracts/deployer.mdx | 280 +++++------------- .../example-contracts/errors.mdx | 8 +- .../example-contracts/events.mdx | 4 +- .../example-contracts/workspace.mdx | 4 +- 9 files changed, 196 insertions(+), 310 deletions(-) diff --git a/docs/build/apps/guestbook/frontend.mdx b/docs/build/apps/guestbook/frontend.mdx index 0ce9ae49c..978c2ff44 100644 --- a/docs/build/apps/guestbook/frontend.mdx +++ b/docs/build/apps/guestbook/frontend.mdx @@ -342,26 +342,26 @@ The `[id]` part of the filename tells this route that we expect to have a path-b ::: ```js title="src/routes/read/[id]/+page.server.ts" -import { error } from '@sveltejs/kit'; -import guestbook from '$lib/contracts/ye_olde_guestbook'; -import type { PageServerLoad } from './$types'; +import { error } from "@sveltejs/kit"; +import guestbook from "$lib/contracts/ye_olde_guestbook"; +import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ params }) => { - try { - let { result } = await guestbook.read_message({ - message_id: parseInt(params.id), - }); + try { + let { result } = await guestbook.read_message({ + message_id: parseInt(params.id), + }); - return { - id: params.id, - message: result.unwrap(), - }; - } catch (err) { - error(500, { - message: - "Sorry, something went wrong. Most likely, the message you're looking for doesn't exist.", - }); - } + return { + id: params.id, + message: result.unwrap(), + }; + } catch (err) { + error(500, { + message: + "Sorry, something went wrong. Most likely, the message you're looking for doesn't exist.", + }); + } }; ``` @@ -394,14 +394,17 @@ Great! If you know the ID of the entry you want to read. Most of the time, you p For this, we'll (again) keep as much of the query logic server-side as possible. These ledger entry results can be cached. And, the client doesn't need to make even more round trips just to query these entries. The route that performs this query is another `+page.server.ts` file: ```js title="src/routes/read/+page.server.ts" -import { getAllMessages, getWelcomeMessage } from '$lib/server/getLedgerEntries'; -import type { PageServerLoad } from './$types'; +import { + getAllMessages, + getWelcomeMessage, +} from "$lib/server/getLedgerEntries"; +import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async () => { - return { - welcomeMessage: await getWelcomeMessage(), - messages: await getAllMessages(), - }; + return { + welcomeMessage: await getWelcomeMessage(), + messages: await getAllMessages(), + }; }; ``` diff --git a/docs/build/apps/guestbook/setup-passkeys.mdx b/docs/build/apps/guestbook/setup-passkeys.mdx index 578a9caa1..03901aa2a 100644 --- a/docs/build/apps/guestbook/setup-passkeys.mdx +++ b/docs/build/apps/guestbook/setup-passkeys.mdx @@ -114,15 +114,14 @@ The implementation of this is outside the scope of this tutorial, but be sure to ::: ```js title="src/routes/api/send/+server.ts" - -import { server } from '$lib/server/passkeyServer'; -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; +import { server } from "$lib/server/passkeyServer"; +import { json } from "@sveltejs/kit"; +import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request }) => { - const { xdr } = await request.json(); - const res = await server.send(xdr); - return json(res); + const { xdr } = await request.json(); + const res = await server.send(xdr); + return json(res); }; ``` @@ -210,15 +209,15 @@ This allows us to write the `fetch` code once, and use it consistently everywher * @returns JSON object containing the RPC's response */ export async function send(xdr: string) { - return fetch('/api/send', { - method: 'POST', - body: JSON.stringify({ - xdr, - }), - }).then(async (res) => { - if (res.ok) return res.json(); - else throw await res.text(); - }); + return fetch("/api/send", { + method: "POST", + body: JSON.stringify({ + xdr, + }), + }).then(async (res) => { + if (res.ok) return res.json(); + else throw await res.text(); + }); } /** @@ -229,10 +228,10 @@ export async function send(xdr: string) { * @returns The contract address to which the specified signer has been added */ export async function getContractId(signer: string) { - return fetch(`/api/contract/${signer}`).then(async (res) => { - if (res.ok) return res.text(); - else throw await res.text(); - }); + return fetch(`/api/contract/${signer}`).then(async (res) => { + if (res.ok) return res.text(); + else throw await res.text(); + }); } /** @@ -242,10 +241,10 @@ export async function getContractId(signer: string) { * @param address - The contract address to fund on the Testnet */ export async function fundContract(address: string) { - return fetch(`/api/fund/${address}`).then(async (res) => { - if (res.ok) return res.json(); - else throw await res.text(); - }); + return fetch(`/api/fund/${address}`).then(async (res) => { + if (res.ok) return res.json(); + else throw await res.text(); + }); } ``` diff --git a/docs/build/smart-contracts/example-contracts/auth.mdx b/docs/build/smart-contracts/example-contracts/auth.mdx index d0a595f30..ee974bb82 100644 --- a/docs/build/smart-contracts/example-contracts/auth.mdx +++ b/docs/build/smart-contracts/example-contracts/auth.mdx @@ -173,7 +173,7 @@ fn test() { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register_contract(None, IncrementContract); + let contract_id = env.register(IncrementContract, {}); let client = IncrementContractClient::new(&env, &contract_id); let user_1 = Address::random(&env); @@ -222,7 +222,7 @@ env.mock_all_auths(); The contract is registered with the environment using the contract type. ```rust -let contract_id = env.register_contract(None, IncrementContract); +let contract_id = env.register(IncrementContract, {}); ``` All public functions within an `impl` block that is annotated with the `#[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`. diff --git a/docs/build/smart-contracts/example-contracts/cross-contract-call.mdx b/docs/build/smart-contracts/example-contracts/cross-contract-call.mdx index 921bddaff..fe2a32de7 100644 --- a/docs/build/smart-contracts/example-contracts/cross-contract-call.mdx +++ b/docs/build/smart-contracts/example-contracts/cross-contract-call.mdx @@ -172,11 +172,11 @@ Open the `cross_contract/contract_b/src/test.rs` file to follow along. fn test() { let env = Env::default(); - // Register contract A using the imported Wasm. - let contract_a_id = env.register_contract_wasm(None, contract_a::Wasm); + // Register contract A using the imported WASM. + let contract_a_id = env.register(contract_a::WASM, ()); // Register contract B defined in this crate. - let contract_b_id = env.register_contract(None, ContractB); + let contract_b_id = env.register(ContractB, ()); // Create a client for calling contract B. let client = ContractBClient::new(&env, &contract_b_id); @@ -196,13 +196,13 @@ let env = Env::default(); Contract A is registered with the environment using the imported Wasm. ```rust -let contract_a_id = env.register_contract_wasm(None, contract_a::Wasm); +let contract_a_id = env.register(contract_a::WASM, ()); ``` -Contract B is registered with the environment using the contract type. +Contract B is registered with the environment using the contract type and the contract instance is compiled into the Rust binary. ```rust -let contract_b_id = env.register_contract(None, ContractB); +let contract_b_id = env.register(ContractB, ()); ``` All public functions within an `impl` block that is annotated with the `#[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. diff --git a/docs/build/smart-contracts/example-contracts/custom-account.mdx b/docs/build/smart-contracts/example-contracts/custom-account.mdx index a1148d1d6..46d8549d9 100644 --- a/docs/build/smart-contracts/example-contracts/custom-account.mdx +++ b/docs/build/smart-contracts/example-contracts/custom-account.mdx @@ -88,7 +88,7 @@ enum DataKey { } ... // Initialize the contract with a list of ed25519 public key ('signers'). -pub fn init(env: Env, signers: Vec>) { +pub fn __constructor(env: Env, signers: Vec>) { // In reality this would need some additional validation on signers // (deduplication etc.). for signer in signers.iter() { @@ -102,6 +102,8 @@ pub fn init(env: Env, signers: Vec>) { This account contract needs to work with the public keys explicitly. Here we initialize the contract with ed25519 keys. +We use constructor in order to ensure that the contract instance is created and initialized atomically (without constructor there is a risk that someone frontruns the initialization of the contract and sets their own public keys). + ### Policy modification ```rust @@ -173,7 +175,7 @@ Here it is implemented in two steps. First, authentication is performed using th ```rust fn authenticate( env: &Env, - signature_payload: &BytesN<32>, + signature_payload: &Hash<32>, signatures: &Vec, ) -> Result<(), AccError> { for i in 0..signatures.len() { @@ -213,63 +215,69 @@ fn verify_authorization_policy( all_signed: bool, spend_left_per_token: &mut Map, ) -> Result<(), AccError> { - // For the account control every signer must sign the invocation. + // There are no limitations when every signers signs the transaction. + if all_signed { + return Ok(()); + } let contract_context = match context { Context::Contract(c) => { + // Allow modifying this contract only if every signer has signed for it. if &c.contract == curr_contract { - if !all_signed { - return Err(AccError::NotEnoughSigners); - } + return Err(AccError::NotEnoughSigners); } c } - Context::CreateContractHostFn(_) => return Err(AccError::InvalidContext), + // Allow creating new contracts only if every signer has signed for it. + Context::CreateContractHostFn(_) | Context::CreateContractWithCtorHostFn(_) => { + return Err(AccError::NotEnoughSigners); + } }; ``` -We verify the policy per `Context`. i.e. Per one `require_auth` call. The policy for the account contract itself enforces every signer to have signed the method call. +We verify the policy per `Context`. i.e. per one `require_auth` call for the address of this account. The policy for the account contract itself enforces every signer to have signed the method call. ```rust -// Otherwise, we're only interested in functions that spend tokens. -if contract_context.fn_name != TRANSFER_FN - && contract_context.fn_name != Symbol::new(env, "approve") -{ - return Ok(()); -} - -let spend_left: Option = - if let Some(spend_left) = spend_left_per_token.get(contract_context.contract.clone()) { - Some(spend_left) - } else if let Some(limit_left) = env - .storage() - .instance() - .get::<_, i128>(&DataKey::SpendLimit(contract_context.contract.clone())) + // Besides the checks above we're only interested in functions that spend tokens. + if contract_context.fn_name != TRANSFER_FN + && contract_context.fn_name != APPROVE_FN + && contract_context.fn_name != BURN_FN { - Some(limit_left) - } else { - None - }; - -// 'None' means that the contract is outside of the policy. -if let Some(spend_left) = spend_left { - // 'amount' is the third argument in both `approve` and `transfer`. - // If the contract has a different signature, it's safer to panic - // here, as it's expected to have the standard interface. - let spent: i128 = contract_context - .args - .get(2) - .unwrap() - .try_into_val(env) - .unwrap(); - if spent < 0 { - return Err(AccError::NegativeAmount); + return Ok(()); } - if !all_signed && spent > spend_left { - return Err(AccError::NotEnoughSigners); + + let spend_left: Option = + if let Some(spend_left) = spend_left_per_token.get(contract_context.contract.clone()) { + Some(spend_left) + } else if let Some(limit_left) = env + .storage() + .instance() + .get::<_, i128>(&DataKey::SpendLimit(contract_context.contract.clone())) + { + Some(limit_left) + } else { + None + }; + + // 'None' means that the contract is outside of the policy. + if let Some(spend_left) = spend_left { + // 'amount' is the third argument in both `approve` and `transfer`. + // If the contract has a different signature, it's safer to panic + // here, as it's expected to have the standard interface. + let spent: i128 = contract_context + .args + .get(2) + .unwrap() + .try_into_val(env) + .unwrap(); + if spent < 0 { + return Err(AccError::NegativeAmount); + } + if !all_signed && spent > spend_left { + return Err(AccError::NotEnoughSigners); + } + spend_left_per_token.set(contract_context.contract.clone(), spend_left - spent); } - spend_left_per_token.set(contract_context.contract.clone(), spend_left - spent); -} -Ok(()) + Ok(()) ``` Then we check for the standard token function names and verify that for these function we don't exceed the spending limits. diff --git a/docs/build/smart-contracts/example-contracts/deployer.mdx b/docs/build/smart-contracts/example-contracts/deployer.mdx index 2f6d78543..cfe8e39c3 100644 --- a/docs/build/smart-contracts/example-contracts/deployer.mdx +++ b/docs/build/smart-contracts/example-contracts/deployer.mdx @@ -70,40 +70,40 @@ test test::test ... ok #[contract] pub struct Deployer; +const ADMIN: Symbol = symbol_short!("admin"); + #[contractimpl] impl Deployer { - /// Deploy the contract Wasm and after deployment invoke the init function - /// of the contract with the given arguments. - /// - /// This has to be authorized by `deployer` (unless the `Deployer` instance - /// itself is used as deployer). This way the whole operation is atomic - /// and it's not possible to frontrun the contract initialization. + /// Construct the deployer with a provided administrator. + pub fn __constructor(env: Env, admin: Address) { + env.storage().instance().set(&ADMIN, &admin); + } + + /// Deploys the contract on behalf of the `Deployer` contract. /// - /// Returns the contract address and result of the init function. + /// This has to be authorized by the `Deployer`s administrator. pub fn deploy( env: Env, - deployer: Address, wasm_hash: BytesN<32>, salt: BytesN<32>, - init_fn: Symbol, - init_args: Vec, - ) -> (Address, Val) { - // Skip authorization if deployer is the current contract. - if deployer != env.current_contract_address() { - deployer.require_auth(); - } - - // Deploy the contract using the uploaded Wasm with given hash. + constructor_args: Vec, + ) -> Address { + let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); + admin.require_auth(); + + // Deploy the contract using the uploaded Wasm with given hash on behalf + // of the current contract. + // Note, that not deploying on behalf of the admin provides more + // consistent address space for the deployer contracts - the admin could + // change or it could be a completely separate contract with complex + // authorization rules, but all the contracts will still be deployed + // by the same `Deployer` contract address. let deployed_address = env .deployer() - .with_address(deployer, salt) - .deploy(wasm_hash); - - // Invoke the init function with the given arguments. - let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args); - // Return the contract ID of the deployed contract and the result of - // invoking the init result. - (deployed_address, res) + .with_address(env.current_contract_address(), salt) + .deploy_v2(wasm_hash, constructor_args); + + deployed_address } } ``` @@ -128,73 +128,54 @@ See the [tests](#tests) for an example of uploading the contract code programmat :::info -This section can be skipped for factory contracts that deploy another contract from their own address (`deployer == env.current_contract_address()``). - -::: - -:::info - For introduction to Soroban authorization see the [auth tutorial](./auth.mdx). ::: -We start with verifying authorization of the `deployer`, unless its the current contract (at which point the authorization is implied). +We start with verifying authorization of the deployer contract's admin. Without that anyone would be able to call the `deploy` function with any arguments, which may not always be desirable (however, there are contracts where it's perfectly fine to have permissionless deployments). ```rust -if deployer != env.current_contract_address() { - deployer.require_auth(); -} +let admin: Address = env.storage().instance().get(&ADMIN).unwrap(); +admin.require_auth(); ``` -While `deployer().with_address()` performs authorization as well, we want to make sure that `deployer` has also authorized the whole operation, as besides deployment it also performs atomic contract initialization. If we didn't require deployer authorization here, then it would be possible to frontrun the deployment operation performed by `deployer` and initialize it differently, thus breaking the promise of atomic initialization. +`deployer().with_address()` performs authorization as well. However, as we deploy on behalf of the current contract, the call is considered to have been implicitly authorized. See more details on the actual authorization payloads in [tests](#tests). ### `deployer()` -The `deployer()` SDK function comes with a few deployment-related utilities. Here we use the most generic deployer kind, `with_address(deployer_address, salt)`. +The `deployer()` SDK function comes with a few deployment-related utilities. Here we use the most generic deployer kind, `with_address(env.current_contract_address(), salt)`. ```rust let deployed_address = env .deployer() - .with_address(deployer, salt) - .deploy(wasm_hash); + .with_address(env.current_contract_address(), salt) + .deploy_v2(wasm_hash, constructor_args); ``` `with_address()` accepts the `deployer` address and salt. Both are used to derive the address of the deployed contract deterministically. It is not possible to re-deploy an already existing contract. -`deploy()` function performs the actual deployment using the provided `wasm_hash`. The implementation of the new contract is defined by the Wasm file uploaded under `wasm_hash`. - :::tip -Only the `wasm_hash` itself is stored per contract ID thus saving the ledger space and fees. +`deployer().with_address(env.current_contract_address(), salt)` call may be replaced with `deployer().with_current_contract(salt)` function for brevity. ::: -When only deploying the contract on behalf of the current contract, i.e. when `deployer` address is always `env.current_contract_address()` it is possible to use `deployer().with_current_contract(salt)` function for brevity. - -### Initialization +`deploy_v2()` function performs the actual deployment using the provided `wasm_hash`. The implementation of the new contract is defined by the Wasm file uploaded under `wasm_hash`. `constructor_args` are the arguments that will be passed to the constructor of the contract that is being deployed. If the deployed contract has no constructor, empty argument vector should be passed. -The contract can be called immediately after deployment, which is useful for initialization. - -```rust -let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args); -``` - -`invoke_contract` can call any defined contract function with any arguments. We pass the actual function to call and the arguments from `deploy` inputs. The result can be any value, depending on the `init_fn`'s return value. - -If the initialization fails, then the whole `deploy` call falls and thus the contract won't be deployed. This behavior is required for the atomic initialization guarantee as well. +:::tip -The contract returns the deployed contract's address and the result of executing the initialization function. +Only the `wasm_hash` itself is stored per contract ID thus saving the ledger space and fees. -```rust - (deployed_address, res) -``` +::: ### Tests Open the `deployer/deployer/src/test.rs` file to follow along. +#### Contract to deploy + Import the test contract Wasm to be deployed. ```rust @@ -207,7 +188,7 @@ mod contract { } ``` -That contract contains the following code that exports two functions: initialization function that takes a value and a getter function for the stored initialized value. +That contract contains the following code that exports two functions: constructor function that takes a value and a getter function for the stored value. ```rust title="deployer/contract/src/lib.rs" #[contract] @@ -217,28 +198,26 @@ const KEY: Symbol = symbol_short!("value"); #[contractimpl] impl Contract { - pub fn init(env: Env, value: u32) { + pub fn __constructor(env: Env, value: u32) { env.storage().instance().set(&KEY, &value); } + pub fn value(env: Env) -> u32 { env.storage().instance().get(&KEY).unwrap() } } ``` -This test contract will be used when testing the deployer. The deployer contract will deploys the test contract and invoke its `init` function. - -There are two tests: deployment from the current contract without authorization and deployment from an arbitrary address with authorization. Besides authorization, these tests are very similar. - -#### Curent contract deployer +This test contract will be used when testing the deployer. The deployer contract will deploy the test contract and invoke its constructor. -In the first test we deploy contract from the `Deployer` contract instance itself. +#### Test code ```rust #[test] -fn test_deploy_from_contract() { +fn test() { let env = Env::default(); - let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); + let admin = Address::generate(&env); + let deployer_client = DeployerClient::new(&env, &env.register(Deployer, (&admin,))); // Upload the Wasm to be deployed from the deployer contract. // This can also be called from within a contract if needed. @@ -246,19 +225,21 @@ fn test_deploy_from_contract() { // Deploy contract using deployer, and include an init function to call. let salt = BytesN::from_array(&env, &[0; 32]); - let init_fn = symbol_short!("init"); - let init_fn_args: Vec = (5u32,).into_val(&env); - let (contract_id, init_result) = deployer_client.deploy( - &deployer_client.address, - &wasm_hash, - &salt, - &init_fn, - &init_fn_args, - ); + let constructor_args: Vec = (5u32,).into_val(&env); + env.mock_all_auths(); + let contract_id = deployer_client.deploy(&wasm_hash, &salt, &constructor_args); - assert!(init_result.is_void()); - // No authorizations needed - the contract acts as a factory. - assert_eq!(env.auths(), vec![]); + // An authorization from the admin is required. + let expected_auth = AuthorizedInvocation { + // Top-level authorized function is `deploy` with all the arguments. + function: AuthorizedFunction::Contract(( + deployer_client.address, + symbol_short!("deploy"), + (wasm_hash.clone(), salt, constructor_args).into_val(&env), + )), + sub_invocations: vec![], + }; + assert_eq!(env.auths(), vec![(admin, expected_auth)]); // Invoke contract to check that it is initialized. let client = contract::Client::new(&env, &contract_id); @@ -273,10 +254,11 @@ In any test the first thing that is always required is an `Env`, which is the So let env = Env::default(); ``` -Register the deployer contract with the environment and create a client to for it. +Register the deployer contract with the environment and create a client to for it. The contract is initialized with the admin address during the registration. ```rust -let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); +let admin = Address::generate(&env); +let deployer_client = DeployerClient::new(&env, &env.register(Deployer, (&admin,))); ``` Upload the code of the test contract that we have imported above via `contractimport!` and get the hash of the uploaded Wasm code. @@ -285,148 +267,44 @@ Upload the code of the test contract that we have imported above via `contractim let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); ``` -The client is used to invoke the `deploy` function. The contract will deploy the test contract using the hash of its Wasm code, call the `init` function, and pass in a single `5u32` argument. The expected return value of `init` function is just `void` (i.e. no value). +The client is used to invoke the `deploy` function. The contract will deploy the test contract using the hash of its Wasm code and pass a single `5u32` argument to its constructor. We also need the `salt` to pass into the call in order to generate a unique identifier of the output contract. ```rust let salt = BytesN::from_array(&env, &[0; 32]); -let init_fn = symbol_short!("init"); -let init_fn_args: Vec = (5u32,).into_val(&env); -let (contract_id, init_result) = deployer_client.deploy( - &deployer_client.address, - &wasm_hash, - &salt, - &init_fn, - &init_fn_args, -); -``` - -The test checks that the test contract was deployed by using its client to invoke it and get back the value set during initialization. - -```rust -let client = contract::Client::new(&env, &contract_id); -let sum = client.value(); -assert_eq!(sum, 5); +let constructor_args: Vec = (5u32,).into_val(&env); ``` -#### External deployer - -The second test is very similar to the first one. - -```rust -#[test] -fn test_deploy_from_address() { - let env = Env::default(); - let deployer_client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); - - // Upload the Wasm to be deployed from the deployer contract. - // This can also be called from within a contract if needed. - let wasm_hash = env.deployer().upload_contract_wasm(contract::WASM); - - // Define a deployer address that needs to authorize the deployment. - let deployer = Address::random(&env); - - // Deploy contract using deployer, and include an init function to call. - let salt = BytesN::from_array(&env, &[0; 32]); - let init_fn = symbol_short!("init"); - let init_fn_args: Vec = (5u32,).into_val(&env); - env.mock_all_auths(); - let (contract_id, init_result) = - deployer_client.deploy(&deployer, &wasm_hash, &salt, &init_fn, &init_fn_args); - - assert!(init_result.is_void()); - - let expected_auth = AuthorizedInvocation { - // Top-level authorized function is `deploy` with all the arguments. - function: AuthorizedFunction::Contract(( - deployer_client.address, - symbol_short!("deploy"), - ( - deployer.clone(), - wasm_hash.clone(), - salt, - init_fn, - init_fn_args, - ) - .into_val(&env), - )), - // From `deploy` function the 'create contract' host function has to be - // authorized. - sub_invocations: vec![AuthorizedInvocation { - function: AuthorizedFunction::CreateContractHostFn(CreateContractArgs { - contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress { - address: deployer.clone().try_into().unwrap(), - salt: Uint256([0; 32]), - }), - executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash.into_val(&env))), - }), - sub_invocations: vec![], - }], - }; - assert_eq!(env.auths(), vec![(deployer, expected_auth)]); - - // Invoke contract to check that it is initialized. - let client = contract::Client::new(&env, &contract_id); - let sum = client.value(); - assert_eq!(sum, 5); -} -``` - -The main difference is that the contract is deployed on behalf of the arbitrary address. +Before invoking the contract we need to enable mock authorization in order to get the recorded authorization payload that we can verify. ```rust -// Define a deployer address that needs to authorize the deployment. -let deployer = Address::random(&env); +env.mock_all_auths(); ``` -Before invoking the contract we need to enable mock authorization in order to get the recorded authorization payload that we can verify. +After the preparations above we can actually call the `deploy` function. ```rust -env.mock_all_auths(); -let (contract_id, init_result) = - deployer_client.deploy(&deployer, &wasm_hash, &salt, &init_fn, &init_fn_args); +let contract_id = deployer_client.deploy(&wasm_hash, &salt, &constructor_args); ``` -The expected authorization tree for the `deployer` looks as follows. +The deployment requires authorization from the admin. As mentioned above, the authorization necessary for `deploy_v2` function is performed on behalf of the deployer contract and is implicit. This can be verified in the test by examining `env.auths()`. ```rust +// An authorization from the admin is required. let expected_auth = AuthorizedInvocation { // Top-level authorized function is `deploy` with all the arguments. function: AuthorizedFunction::Contract(( deployer_client.address, symbol_short!("deploy"), - ( - deployer.clone(), - wasm_hash.clone(), - salt, - init_fn, - init_fn_args, - ) - .into_val(&env), + (wasm_hash.clone(), salt, constructor_args).into_val(&env), )), - // From `deploy` function the 'create contract' host function has to be - // authorized. - sub_invocations: vec![AuthorizedInvocation { - function: AuthorizedFunction::CreateContractHostFn(CreateContractArgs { - contract_id_preimage: ContractIdPreimage::Address(ContractIdPreimageFromAddress { - address: deployer.clone().try_into().unwrap(), - salt: Uint256([0; 32]), - }), - executable: xdr::ContractExecutable::Wasm(xdr::Hash(wasm_hash.into_val(&env))), - }), - sub_invocations: vec![], - }], + sub_invocations: vec![], }; +assert_eq!(env.auths(), vec![(admin, expected_auth)]); ``` -At the top level we have the `deploy` function itself with all the arguments that we've passed to it. From the `deploy` function the `CreateContractHostFn` has to be authorized. This is the authorization payload that has to be authorized by any deployer in any context. It contains the deployer address, salt and executable. - -This authorization tree proves that the deployment and initialization are authorized atomically: actual deployment happens within the context of `deploy` and all of salt, executable, and initialization arguments are authorized together (i.e. there is one signature to authorizes this exact combination). - -Then we make sure that deployer has authorized the expected tree and that expected value has been stored. +The test checks that the test contract was deployed by using its client to invoke it and get back the value set during initialization. ```rust -assert_eq!(env.auths(), vec![(deployer, expected_auth)]); - let client = contract::Client::new(&env, &contract_id); let sum = client.value(); assert_eq!(sum, 5); @@ -481,8 +359,7 @@ stellar contract invoke --id 1 -- deploy \ --deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM --salt 123 \ --wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 \ - --init_fn init \ - --init_args '[{"u32":5}]' + --constructor_args '[{"u32":5}]' ``` @@ -494,8 +371,7 @@ stellar contract invoke --id 1 -- deploy ` --deployer CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM --salt 123 ` --wasm_hash 7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15 ` - --init_fn init ` - --init_args '[{"u32":5}]' + --constructor_args '[{"u32":5}]' ``` diff --git a/docs/build/smart-contracts/example-contracts/errors.mdx b/docs/build/smart-contracts/example-contracts/errors.mdx index 35cf5a83d..dfa80687d 100644 --- a/docs/build/smart-contracts/example-contracts/errors.mdx +++ b/docs/build/smart-contracts/example-contracts/errors.mdx @@ -205,7 +205,7 @@ Open the `errors/src/test.rs` file to follow along. #[test] fn test() { let env = Env::default(); - let contract_id = env.register_contract(None, IncrementContract); + let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.try_increment(), Ok(Ok(1))); @@ -223,7 +223,7 @@ fn test() { #E3256B fn test_panic() { let env = Env::default(); - let contract_id = env.register_contract(None, IncrementContract); + let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); @@ -244,7 +244,7 @@ let env = Env::default(); The contract is registered with the environment using the contract type. ```rust -let contract_id = env.register_contract(None, IncrementContract); +let contract_id = env.register(IncrementContract, ()); ``` All public functions within an `impl` block that is annotated with the `#[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`. @@ -253,7 +253,7 @@ All public functions within an `impl` block that is annotated with the `#[contra let client = IncrementContractClient::new(&env, &contract_id); ``` -Two functions are generated for every contract function, one that returns a `Result<>`, and the other that does not handle errors and panicks if an error occurs. +Two functions are generated for every contract function, one that returns a `Result<>`, and the other that does not handle errors and panics if an error occurs. ### `try_increment` diff --git a/docs/build/smart-contracts/example-contracts/events.mdx b/docs/build/smart-contracts/example-contracts/events.mdx index 15909d18f..0f4c656cf 100644 --- a/docs/build/smart-contracts/example-contracts/events.mdx +++ b/docs/build/smart-contracts/example-contracts/events.mdx @@ -145,7 +145,7 @@ Open the `events/src/test.rs` file to follow along. #[test] fn test() { let env = Env::default(); - let contract_id = env.register_contract(None, IncrementContract); + let contract_id = env.register(IncrementContract, ()); let client = IncrementContractClient::new(&env, &contract_id); assert_eq!(client.increment(), 1); @@ -185,7 +185,7 @@ let env = Env::default(); The contract is registered with the environment using the contract type. ```rust -let contract_id = env.register_contract(None, IncrementContract); +let contract_id = env.register(IncrementContract, ()); ``` All public functions within an `impl` block that is annotated with the `#[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`. diff --git a/docs/build/smart-contracts/example-contracts/workspace.mdx b/docs/build/smart-contracts/example-contracts/workspace.mdx index 85b3c9914..954ea125b 100644 --- a/docs/build/smart-contracts/example-contracts/workspace.mdx +++ b/docs/build/smart-contracts/example-contracts/workspace.mdx @@ -113,10 +113,10 @@ fn test() { let env = Env::default(); // Register contract A using the native contract imported. - let contract_a_id = env.register_contract(None, ContractA); + let contract_a_id = env.register(ContractA, ()); // Register contract B defined in this crate. - let contract_b_id = env.register_contract(None, ContractB); + et contract_b_id = env.register(ContractB, ()); // Create a client for calling contract B. let client = ContractBClient::new(&env, &contract_b_id);