-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a very simple account contract example. (#227)
* Add a very simple account contract example. This is close to minimal possible valid account contract that is aimed at demonstrating the most basic concepts.
- Loading branch information
Showing
6 changed files
with
163 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ members = [ | |
"atomic_multiswap", | ||
"account", | ||
"alloc", | ||
"simple_account", | ||
] | ||
|
||
[profile.release-with-logs] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[package] | ||
name = "soroban-simple-account-contract" | ||
version = "0.0.0" | ||
authors = ["Stellar Development Foundation <[email protected]>"] | ||
license = "Apache-2.0" | ||
edition = "2021" | ||
publish = false | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
doctest = false | ||
|
||
[dependencies] | ||
soroban-sdk = { workspace = true } | ||
soroban-auth = { workspace = true } | ||
|
||
[dev_dependencies] | ||
soroban-sdk = { workspace = true, features = ["testutils"] } | ||
soroban-auth = { workspace = true, features = ["testutils"] } | ||
ed25519-dalek = { version = "1.0.1" } | ||
rand = { version = "0.7.3" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
//! This a minimal exapmle of an account contract. | ||
//! | ||
//! The account is owned by a single ed25519 public key that is also used for | ||
//! authentication. | ||
//! | ||
//! For a more advanced example that demonstrates all the capabilities of the | ||
//! Soroban account contracts see `src/account` example. | ||
#![no_std] | ||
|
||
struct SimpleAccount; | ||
|
||
use soroban_auth::AuthorizationContext; | ||
use soroban_sdk::{contractimpl, contracttype, BytesN, Env, Vec}; | ||
|
||
#[derive(Clone)] | ||
#[contracttype] | ||
pub enum DataKey { | ||
Owner, | ||
} | ||
|
||
#[contractimpl] | ||
impl SimpleAccount { | ||
// Initialize the contract with an owner's ed25519 public key. | ||
pub fn init(env: Env, public_key: BytesN<32>) { | ||
if env.storage().has(&DataKey::Owner) { | ||
panic!("owner is already set"); | ||
} | ||
env.storage().set(&DataKey::Owner, &public_key); | ||
} | ||
|
||
// This is the 'entry point' of the account contract and every account | ||
// contract has to implement it. `require_auth` calls for the Address of | ||
// this contract will result in calling this `__check_auth` function with | ||
// the appropriate arguments. | ||
// | ||
// This should return `()` if authentication and authorization checks have | ||
// been passed and return an error (or panic) otherwise. | ||
// | ||
// `__check_auth` takes the payload that needed to be signed, arbitrarily | ||
// typed signatures (`BytesN<64>` type here) and authorization | ||
// context that contains all the invocations that this call tries to verify | ||
// (not used in this example). | ||
// | ||
// In this example `__check_auth` only verifies the signature. | ||
// | ||
// Note, that `__check_auth` function shouldn't call `require_auth` on the | ||
// contract's own address in order to avoid infinite recursion. | ||
#[allow(non_snake_case)] | ||
pub fn __check_auth( | ||
env: Env, | ||
signature_payload: BytesN<32>, | ||
signature_args: Vec<BytesN<64>>, | ||
_auth_context: Vec<AuthorizationContext>, | ||
) { | ||
if signature_args.len() != 1 { | ||
panic!("incorrect number of signature args"); | ||
} | ||
let public_key: BytesN<32> = env.storage().get(&DataKey::Owner).unwrap().unwrap(); | ||
env.crypto().ed25519_verify( | ||
&public_key, | ||
&signature_payload.into(), | ||
&signature_args.get(0).unwrap().unwrap(), | ||
); | ||
} | ||
} | ||
|
||
mod test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
#![cfg(test)] | ||
extern crate std; | ||
|
||
use ed25519_dalek::Keypair; | ||
use ed25519_dalek::Signer; | ||
use rand::thread_rng; | ||
use soroban_auth::testutils::EnvAuthUtils; | ||
use soroban_sdk::RawVal; | ||
use soroban_sdk::Status; | ||
use soroban_sdk::{testutils::BytesN as _, vec, BytesN, Env, IntoVal}; | ||
|
||
use crate::SimpleAccount; | ||
use crate::SimpleAccountClient; | ||
|
||
fn generate_keypair() -> Keypair { | ||
Keypair::generate(&mut thread_rng()) | ||
} | ||
|
||
fn create_account_contract(e: &Env) -> SimpleAccountClient { | ||
SimpleAccountClient::new(e, &e.register_contract(None, SimpleAccount {})) | ||
} | ||
|
||
fn sign(e: &Env, signer: &Keypair, payload: &BytesN<32>) -> RawVal { | ||
let signature: BytesN<64> = signer | ||
.sign(payload.to_array().as_slice()) | ||
.to_bytes() | ||
.into_val(e); | ||
signature.into_val(e) | ||
} | ||
|
||
#[test] | ||
fn test_account() { | ||
let env: Env = Default::default(); | ||
|
||
let account_contract = create_account_contract(&env); | ||
|
||
let signer = generate_keypair(); | ||
account_contract.init(&signer.public.to_bytes().into_val(&env)); | ||
|
||
let payload = BytesN::random(&env); | ||
// `__check_auth` can't be called directly, hence we need to use | ||
// `invoke_account_contract_check_auth` testing utility that emulates being | ||
// called by the Soroban host during a `require_auth` call. | ||
env.invoke_account_contract_check_auth::<Status>( | ||
&account_contract.contract_id, | ||
&payload, | ||
&vec![&env, sign(&env, &signer, &payload)], | ||
&vec![&env], | ||
) | ||
// Unwrap the result to make sure there is no error. | ||
.unwrap(); | ||
|
||
// Now pass a random bytes array instead of the signature - this should | ||
// result in an error as this is not a valid signature. | ||
assert!(env | ||
.invoke_account_contract_check_auth::<Status>( | ||
&account_contract.contract_id, | ||
&payload, | ||
&vec![&env, BytesN::<64>::random(&env).into()], | ||
&vec![&env], | ||
) | ||
.is_err()); | ||
} |