From 82c29fba8e18cc7dac3fd8866f5551b9c8e2e683 Mon Sep 17 00:00:00 2001 From: Nasr Date: Thu, 21 Dec 2023 12:01:38 -0500 Subject: [PATCH 1/4] feat: bindings for deploying accounts / burners --- dojo.h | 15 ++++++ src/constants.rs | 6 +++ src/lib.rs | 120 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/constants.rs diff --git a/dojo.h b/dojo.h index a9dec9a..66ba1fe 100644 --- a/dojo.h +++ b/dojo.h @@ -560,6 +560,15 @@ struct Result_____Account account_new(struct CJsonRpcClient *rpc, struct FieldElement private_key, const char *address); +struct Result_bool account_deploy(struct Account *account, + struct FieldElement class_hash, + const struct FieldElement *constructor_calldata, + uintptr_t constructor_calldata_len, + struct FieldElement salt); + +struct Result_____Account account_deploy_burner(struct CJsonRpcClient *rpc, + struct Account *master_account); + struct FieldElement account_address(struct Account *account); struct FieldElement account_chain_id(struct Account *account); @@ -570,6 +579,12 @@ struct Result_bool account_execute_raw(struct Account *account, const struct Call *calldata, uintptr_t calldata_len); +struct FieldElement hash_get_contract_address(struct FieldElement class_hash, + struct FieldElement salt, + const struct FieldElement *constructor_calldata, + uintptr_t constructor_calldata_len, + struct FieldElement deployer_address); + void client_free(struct ToriiClient *t); void jsonrpc_client_free(struct CJsonRpcClient *rpc); diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..780efc5 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,6 @@ +use starknet::macros::felt; +use starknet_crypto::FieldElement; + +pub const PREFUND_AMOUNT: FieldElement = felt!("0x2386f26fc10000"); // 0.001 ETH +pub const KATANA_ETH_CONTRACT_ADDRESS: FieldElement = felt!("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"); +pub const KATANA_ACCOUNT_CLASS_HASH: FieldElement = felt!("0x04d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f"); \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 70ae6b9..6a24c04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,13 @@ mod types; +mod constants; use futures_util::StreamExt; -use starknet::accounts::{Account as StarknetAccount, ExecutionEncoding, SingleOwnerAccount}; -use starknet::core::utils::cairo_short_string_to_felt; +use starknet::accounts::{ + Account as StarknetAccount, AccountDeployment, AccountFactory, ExecutionEncoding, + SingleOwnerAccount, +}; +use starknet::contract::ContractFactory; +use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; use starknet::signers::{LocalWallet, SigningKey, VerifyingKey}; @@ -379,6 +384,96 @@ pub unsafe extern "C" fn account_new( Result::Ok(Box::into_raw(Box::new(Account(account)))) } +#[no_mangle] +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn account_deploy( + account: *mut Account<'static>, + class_hash: types::FieldElement, + constructor_calldata: *const FieldElement, + constructor_calldata_len: usize, + salt: types::FieldElement, +) -> Result { + let class_hash = (&class_hash).into(); + let constructor_calldata = unsafe { + std::slice::from_raw_parts(constructor_calldata, constructor_calldata_len).to_vec() + }; + let salt = (&salt).into(); + let factory = ContractFactory::new(class_hash, &(*account).0); + + + let deployment = factory.deploy(constructor_calldata, salt, false); + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(deployment.send()) + .unwrap(); + + Result::Ok(true) +} + +#[no_mangle] +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn account_deploy_burner( + rpc: *mut CJsonRpcClient, + master_account: *mut Account<'static>, +) -> Result<*mut Account<'static>> { + let signing_key = SigningKey::from_random(); + let verifying_key = signing_key.verifying_key(); + let address = get_contract_address( + signing_key.verifying_key().scalar(), + constants::KATANA_ACCOUNT_CLASS_HASH, + &[verifying_key.scalar()], + FieldElement::ZERO, + ); + let signer = LocalWallet::from_signing_key(signing_key); + + let chain_id = (*master_account).0.chain_id(); + + let account = SingleOwnerAccount::new( + &(*rpc).0, + signer, + address, + chain_id, + ExecutionEncoding::Legacy, + ); + + // prefund the account + let exec = account.execute(vec![starknet::accounts::Call{ + to: constants::KATANA_ETH_CONTRACT_ADDRESS, + calldata: vec![address, constants::PREFUND_AMOUNT, FieldElement::ZERO], + selector: cairo_short_string_to_felt("transfer").unwrap(), + }]); + + let result = tokio::runtime::Runtime::new() + .unwrap() + .block_on(exec.send()); + + if let Err(e) = result { + return Result::Err(Error { + message: CString::new( + format!("Failed to prefund the account: {}", e.to_string()), + ).unwrap().into_raw(), + }); + } + + // deploy the burner + let factory = ContractFactory::new(constants::KATANA_ACCOUNT_CLASS_HASH, account.clone()); + let deployment = factory.deploy(vec![verifying_key.scalar()], verifying_key.scalar(), false); + + let result = tokio::runtime::Runtime::new() + .unwrap() + .block_on(deployment.send()); + + match result { + Ok(_) => Result::Ok(Box::into_raw(Box::new(Account(account)))), + Err(e) => Result::Err(Error { + message: CString::new(e.to_string()).unwrap().into_raw(), + }), + } + + +} + #[no_mangle] #[allow(clippy::missing_safety_doc)] pub unsafe extern "C" fn account_address(account: *mut Account<'static>) -> types::FieldElement { @@ -424,6 +519,27 @@ pub unsafe extern "C" fn account_execute_raw( } } +#[no_mangle] +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn hash_get_contract_address( + class_hash: types::FieldElement, + salt: types::FieldElement, + constructor_calldata: *const FieldElement, + constructor_calldata_len: usize, + deployer_address: types::FieldElement, +) -> types::FieldElement { + let class_hash = (&class_hash).into(); + let salt = (&salt).into(); + let constructor_calldata = unsafe { + std::slice::from_raw_parts(constructor_calldata, constructor_calldata_len).to_vec() + }; + let deployer_address = (&deployer_address).into(); + + let address = get_contract_address(salt, class_hash, &constructor_calldata, deployer_address); + + (&address).into() +} + // This function takes a raw pointer to ToriiClient as an argument. // It checks if the pointer is not null. If it's not, it converts the raw pointer // back into a Box, which gets dropped at the end of the scope, From ffe284c841511b698de61cf2488dac5cc49d5c63 Mon Sep 17 00:00:00 2001 From: Nasr Date: Thu, 21 Dec 2023 13:37:47 -0500 Subject: [PATCH 2/4] feat: wait for txn confirmation and return account --- Cargo.lock | 5 ++-- Cargo.toml | 1 + example/main.c | 17 ++++++++++---- src/constants.rs | 7 +++--- src/lib.rs | 60 ++++++++++++++++++++++-------------------------- src/utils.rs | 30 ++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 0cf69f5..8fbc6ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" [[package]] name = "ark-ff" @@ -4627,6 +4627,7 @@ dependencies = [ name = "torii-c" version = "0.0.7" dependencies = [ + "anyhow", "cbindgen", "csbindgen", "dojo-types", diff --git a/Cargo.toml b/Cargo.toml index 9aa6ea4..560e7ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ url = "2.5.0" tonic = "0.10.2" futures-util = "0.3.29" futures-channel = "0.3.29" +anyhow = "1.0.76" [build-dependencies] cbindgen = "0.24.0" diff --git a/example/main.c b/example/main.c index ea9d6fb..e5b164f 100644 --- a/example/main.c +++ b/example/main.c @@ -89,9 +89,18 @@ int main() printf("Failed to create account: %s\n", resAccount.err.message); return 1; } - Account *account = resAccount.ok; + Account *master_account = resAccount.ok; - FieldElement address = account_address(account); + Result_____Account resBurner = account_deploy_burner(provider, master_account); + if (resBurner.tag == Err_____Account) + { + printf("Failed to create burner: %s\n", resBurner.err.message); + return 1; + } + + Account *burner = resBurner.ok; + + FieldElement address = account_address(burner); printf("New account: 0x"); for (size_t i = 0; i < 32; i++) { @@ -208,7 +217,7 @@ int main() move.calldata.data[0] = moveLeft.ok; - Result_bool resSpawn = account_execute_raw(account, &spawn, 1); + Result_bool resSpawn = account_execute_raw(burner, &spawn, 1); if (resSpawn.tag == Err_bool) { printf("Failed to execute call: %s\n", resSpawn.err.message); @@ -217,7 +226,7 @@ int main() sleep(5); - Result_bool resMove = account_execute_raw(account, &move, 1); + Result_bool resMove = account_execute_raw(burner, &move, 1); if (resMove.tag == Err_bool) { printf("Failed to execute call: %s\n", resMove.err.message); diff --git a/src/constants.rs b/src/constants.rs index 780efc5..e1bece4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,6 +1,7 @@ use starknet::macros::felt; use starknet_crypto::FieldElement; -pub const PREFUND_AMOUNT: FieldElement = felt!("0x2386f26fc10000"); // 0.001 ETH -pub const KATANA_ETH_CONTRACT_ADDRESS: FieldElement = felt!("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"); -pub const KATANA_ACCOUNT_CLASS_HASH: FieldElement = felt!("0x04d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f"); \ No newline at end of file +pub const KATANA_ACCOUNT_CLASS_HASH: FieldElement = + felt!("0x04d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f"); +pub const UDC_ADDRESS: FieldElement = + felt!("0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"); diff --git a/src/lib.rs b/src/lib.rs index 6a24c04..3482b43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,13 @@ -mod types; mod constants; +mod types; +mod utils; use futures_util::StreamExt; -use starknet::accounts::{ - Account as StarknetAccount, AccountDeployment, AccountFactory, ExecutionEncoding, - SingleOwnerAccount, -}; +use starknet::accounts::{Account as StarknetAccount, ExecutionEncoding, SingleOwnerAccount}; use starknet::contract::ContractFactory; -use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; +use starknet::core::utils::{ + cairo_short_string_to_felt, get_contract_address, get_selector_from_name, +}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; use starknet::signers::{LocalWallet, SigningKey, VerifyingKey}; @@ -21,6 +21,7 @@ use types::{ Account, BlockId, CArray, CJsonRpcClient, COption, Call, Entity, Error, KeysClause, Model, Query, Result, Signature, ToriiClient, Ty, WorldMetadata, }; +use utils::watch_tx; #[no_mangle] #[allow(clippy::missing_safety_doc)] @@ -400,7 +401,6 @@ pub unsafe extern "C" fn account_deploy( let salt = (&salt).into(); let factory = ContractFactory::new(class_hash, &(*account).0); - let deployment = factory.deploy(constructor_calldata, salt, false); tokio::runtime::Runtime::new() @@ -420,15 +420,15 @@ pub unsafe extern "C" fn account_deploy_burner( let signing_key = SigningKey::from_random(); let verifying_key = signing_key.verifying_key(); let address = get_contract_address( - signing_key.verifying_key().scalar(), - constants::KATANA_ACCOUNT_CLASS_HASH, + verifying_key.scalar(), + constants::KATANA_ACCOUNT_CLASS_HASH, &[verifying_key.scalar()], FieldElement::ZERO, ); let signer = LocalWallet::from_signing_key(signing_key); - + let chain_id = (*master_account).0.chain_id(); - + let account = SingleOwnerAccount::new( &(*rpc).0, signer, @@ -437,11 +437,17 @@ pub unsafe extern "C" fn account_deploy_burner( ExecutionEncoding::Legacy, ); - // prefund the account - let exec = account.execute(vec![starknet::accounts::Call{ - to: constants::KATANA_ETH_CONTRACT_ADDRESS, - calldata: vec![address, constants::PREFUND_AMOUNT, FieldElement::ZERO], - selector: cairo_short_string_to_felt("transfer").unwrap(), + // deploy the burner + let exec = (*master_account).0.execute(vec![starknet::accounts::Call { + to: constants::UDC_ADDRESS, + calldata: vec![ + constants::KATANA_ACCOUNT_CLASS_HASH, // class_hash + verifying_key.scalar(), // salt + FieldElement::ZERO, // deployer_address + FieldElement::ONE, // constructor calldata length (1) + verifying_key.scalar(), // constructor calldata + ], + selector: get_selector_from_name("deployContract").unwrap(), }]); let result = tokio::runtime::Runtime::new() @@ -450,28 +456,18 @@ pub unsafe extern "C" fn account_deploy_burner( if let Err(e) = result { return Result::Err(Error { - message: CString::new( - format!("Failed to prefund the account: {}", e.to_string()), - ).unwrap().into_raw(), + message: CString::new(e.to_string()).unwrap().into_raw(), }); } - // deploy the burner - let factory = ContractFactory::new(constants::KATANA_ACCOUNT_CLASS_HASH, account.clone()); - let deployment = factory.deploy(vec![verifying_key.scalar()], verifying_key.scalar(), false); + let result = result.unwrap(); - let result = tokio::runtime::Runtime::new() + tokio::runtime::Runtime::new() .unwrap() - .block_on(deployment.send()); - - match result { - Ok(_) => Result::Ok(Box::into_raw(Box::new(Account(account)))), - Err(e) => Result::Err(Error { - message: CString::new(e.to_string()).unwrap().into_raw(), - }), - } + .block_on(watch_tx(&(*rpc).0, result.transaction_hash)) + .unwrap(); - + Result::Ok(Box::into_raw(Box::new(Account(account)))) } #[no_mangle] diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5034cc3 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,30 @@ +use anyhow::Result; + +use starknet::{ + core::types::StarknetError, + providers::{Provider, ProviderError}, +}; +use starknet_crypto::FieldElement; + +pub async fn watch_tx

(provider: P, transaction_hash: FieldElement) -> Result<()> +where + P: Provider, + ProviderError: 'static, +{ + loop { + // TODO: check with sequencer gateway if it's not confirmed after an extended period of + // time, as full nodes don't have access to failed transactions and would report them + // as `NotReceived`. + match provider.get_transaction_receipt(transaction_hash).await { + Ok(_) => { + // With JSON-RPC, once we get a receipt, the transaction must have been confirmed. + // Rejected transactions simply aren't available. This needs to be changed once we + // implement the sequencer fallback. + + return Ok(()); + } + Err(ProviderError::StarknetError(StarknetError::TransactionHashNotFound)) => {} + Err(err) => return Err(err.into()), + } + } +} From f9708fa5bd45897689f7193191b8f82c7da5cbfd Mon Sep 17 00:00:00 2001 From: Nasr Date: Thu, 21 Dec 2023 13:57:28 -0500 Subject: [PATCH 3/4] refactor: remove deploy account binding --- dojo.h | 6 ------ src/lib.rs | 26 -------------------------- 2 files changed, 32 deletions(-) diff --git a/dojo.h b/dojo.h index 66ba1fe..af61f26 100644 --- a/dojo.h +++ b/dojo.h @@ -560,12 +560,6 @@ struct Result_____Account account_new(struct CJsonRpcClient *rpc, struct FieldElement private_key, const char *address); -struct Result_bool account_deploy(struct Account *account, - struct FieldElement class_hash, - const struct FieldElement *constructor_calldata, - uintptr_t constructor_calldata_len, - struct FieldElement salt); - struct Result_____Account account_deploy_burner(struct CJsonRpcClient *rpc, struct Account *master_account); diff --git a/src/lib.rs b/src/lib.rs index 3482b43..0b946b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -385,32 +385,6 @@ pub unsafe extern "C" fn account_new( Result::Ok(Box::into_raw(Box::new(Account(account)))) } -#[no_mangle] -#[allow(clippy::missing_safety_doc)] -pub unsafe extern "C" fn account_deploy( - account: *mut Account<'static>, - class_hash: types::FieldElement, - constructor_calldata: *const FieldElement, - constructor_calldata_len: usize, - salt: types::FieldElement, -) -> Result { - let class_hash = (&class_hash).into(); - let constructor_calldata = unsafe { - std::slice::from_raw_parts(constructor_calldata, constructor_calldata_len).to_vec() - }; - let salt = (&salt).into(); - let factory = ContractFactory::new(class_hash, &(*account).0); - - let deployment = factory.deploy(constructor_calldata, salt, false); - - tokio::runtime::Runtime::new() - .unwrap() - .block_on(deployment.send()) - .unwrap(); - - Result::Ok(true) -} - #[no_mangle] #[allow(clippy::missing_safety_doc)] pub unsafe extern "C" fn account_deploy_burner( From c169357e4b4b91a13e5ef95de9b710f5de6956bb Mon Sep 17 00:00:00 2001 From: Nasr Date: Thu, 21 Dec 2023 14:03:03 -0500 Subject: [PATCH 4/4] chore: unused import --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0b946b5..84d6004 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ mod utils; use futures_util::StreamExt; use starknet::accounts::{Account as StarknetAccount, ExecutionEncoding, SingleOwnerAccount}; -use starknet::contract::ContractFactory; use starknet::core::utils::{ cairo_short_string_to_felt, get_contract_address, get_selector_from_name, };