diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs index d15e81c..1e65745 100644 --- a/src/wasm/mod.rs +++ b/src/wasm/mod.rs @@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; -use cainome::cairo_serde::{self, ByteArray, CairoSerde}; +use cainome::cairo_serde::{self, CairoSerde}; use crypto_bigint::U256; use dojo_world::contracts::naming::compute_selector_from_tag; use futures::{FutureExt, StreamExt}; @@ -22,14 +22,13 @@ use starknet::accounts::{ Account as _, ConnectedAccount as _, ExecutionEncoding, SingleOwnerAccount, }; use starknet::core::types::{Felt, FunctionCall}; -use starknet::core::utils::get_contract_address; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider as _}; -use starknet::signers::{LocalWallet, SigningKey, VerifyingKey}; +use starknet::signers::LocalWallet; use starknet_crypto::poseidon_hash_many; use stream_cancel::{StreamExt as _, Tripwire}; +use tokio::runtime::Runtime; use torii_relay::types::Message; -use torii_typed_data::TypedData; use tsify_next::Tsify; use wasm_bindgen::prelude::*; @@ -57,189 +56,197 @@ extern "C" { fn error(s: &str); } -/// Encodes typed data according to Starknet's typed data specification -/// -/// # Parameters -/// * `typed_data` - JSON string containing the typed data -/// * `address` - Address as hex string -/// -/// # Returns -/// Result containing encoded data as hex string or error -#[wasm_bindgen(js_name = typedDataEncode)] -pub fn typed_data_encode(typed_data: &str, address: &str) -> Result { - let typed_data = serde_json::from_str::(&typed_data) - .map_err(|err| JsValue::from(format!("failed to parse typed data: {err}")))?; - - let address = Felt::from_str(&address) - .map_err(|err| JsValue::from(format!("failed to parse address: {err}")))?; - - typed_data - .encode(address) - .map(|felt| format!("{:#x}", felt)) - .map_err(|err| JsValue::from(err.to_string())) -} +#[wasm_bindgen] +pub struct SigningKey(starknet::signers::SigningKey); -/// Generates a new random signing key -/// -/// # Returns -/// Private key as hex string -#[wasm_bindgen(js_name = signingKeyNew)] -pub fn signing_key_new() -> String { - let private_key: SigningKey = SigningKey::from_random(); +#[wasm_bindgen] +pub struct VerifyingKey(starknet::signers::VerifyingKey); - format!("{:#x}", private_key.secret_scalar()) -} +#[wasm_bindgen] +pub struct TypedData(torii_typed_data::TypedData); -/// Signs a message hash with a private key -/// -/// # Parameters -/// * `private_key` - Private key as hex string -/// * `hash` - Message hash as hex string -/// -/// # Returns -/// Result containing signature or error -#[wasm_bindgen(js_name = signingKeySign)] -pub fn signing_key_sign(private_key: &str, hash: &str) -> Result { - let private_key = Felt::from_str(private_key); - if let Err(e) = private_key { - return Err(JsValue::from(format!("failed to parse private key: {e}"))); - } +#[wasm_bindgen] +pub struct ByteArray(cainome::cairo_serde::ByteArray); - let hash = Felt::from_str(hash); - if let Err(e) = hash { - return Err(JsValue::from(format!("failed to parse hash: {e}"))); +#[wasm_bindgen] +impl SigningKey { + /// Generates a new random signing key + /// + /// # Returns + /// Private key as hex string + #[wasm_bindgen(constructor)] + pub fn new() -> SigningKey { + let private_key = starknet::signers::SigningKey::from_random(); + SigningKey(private_key) } - let private_key = SigningKey::from_secret_scalar(private_key.unwrap()); - let sig = private_key.sign(&hash.unwrap()); + /// Initializes a new signing key from a secret scalar + /// + /// # Parameters + /// * `secret_scalar` - Secret scalar as hex string + /// + /// # Returns + /// Result containing signing key or error + #[wasm_bindgen(js_name = fromSecretScalar)] + pub fn from_secret_scalar(secret_scalar: &str) -> Result { + let secret_scalar = Felt::from_str(secret_scalar); + if let Err(e) = secret_scalar { + return Err(JsValue::from(format!("failed to parse secret scalar: {e}"))); + } - match sig { - Ok(sig) => Result::Ok(Signature::from(sig)), - Err(e) => Err(JsValue::from(format!("failed to sign: {e}"))), + let secret_scalar = secret_scalar.unwrap(); + let private_key = starknet::signers::SigningKey::from_secret_scalar(secret_scalar); + Ok(SigningKey(private_key)) } -} -/// Derives a verifying (public) key from a signing (private) key -/// -/// # Parameters -/// * `signing_key` - Signing key as hex string -/// -/// # Returns -/// Result containing verifying key as hex string or error -#[wasm_bindgen(js_name = verifyingKeyNew)] -pub fn verifying_key_new(signing_key: &str) -> Result { - let signing_key = Felt::from_str(signing_key); - if let Err(e) = signing_key { - return Err(JsValue::from(format!("failed to parse signing key: {e}"))); + /// Returns the secret scalar of the signing key + /// + /// # Returns + /// Result containing secret scalar as hex string or error + #[wasm_bindgen(js_name = secretScalar)] + pub fn secret_scalar(&self) -> Result { + Ok(format!("{:#x}", self.0.secret_scalar())) } - let verifying_key = starknet_crypto::get_public_key(&signing_key.unwrap()); + /// Signs a message hash with a private key + /// + /// # Parameters + /// * `private_key` - Private key as hex string + /// * `hash` - Message hash as hex string + /// + /// # Returns + /// Result containing signature or error + #[wasm_bindgen] + pub fn sign(&self, hash: &str) -> Result { + let hash = Felt::from_str(hash); + if let Err(e) = hash { + return Err(JsValue::from(format!("failed to parse hash: {e}"))); + } - Ok(format!("{:#x}", verifying_key)) -} + let hash = hash.unwrap(); -/// Verifies a signature against a message hash using a verifying key -/// -/// # Parameters -/// * `verifying_key` - Verifying key as hex string -/// * `hash` - Message hash as hex string -/// * `signature` - Signature to verify -/// -/// # Returns -/// Result containing verification success boolean or error -#[wasm_bindgen(js_name = verifyingKeyVerify)] -pub fn verifying_key_verify( - verifying_key: &str, - hash: &str, - signature: Signature, -) -> Result { - let verifying_key = Felt::from_str(verifying_key); - if let Err(e) = verifying_key { - return Err(JsValue::from(format!("failed to parse verifying key: {e}"))); - } - - let verifying_key = VerifyingKey::from_scalar(verifying_key.unwrap()); + let sig = self.0.sign(&hash); - let hash = Felt::from_str(hash); - if let Err(e) = hash { - return Err(JsValue::from(format!("failed to parse hash: {e}"))); + match sig { + Ok(sig) => Result::Ok(Signature::from(sig)), + Err(e) => Err(JsValue::from(format!("failed to sign: {e}"))), + } } - let hash = &hash.unwrap(); - - match verifying_key.verify(hash, &signature.into()) { - Ok(result) => Result::Ok(result), - Err(e) => Err(JsValue::from(format!("failed to verify: {e}"))), + /// Returns the verifying key of the signing key + /// + /// # Returns + /// Result containing verifying key or error + #[wasm_bindgen(js_name = verifyingKey)] + pub fn verifying_key(&self) -> Result { + Ok(VerifyingKey(starknet::signers::VerifyingKey::from_scalar(self.0.secret_scalar()))) } } -/// Creates a new Starknet provider instance for a given RPC URL -/// -/// # Parameters -/// * `rpc_url` - URL of the RPC endpoint -/// -/// # Returns -/// Result containing Provider instance or error -#[wasm_bindgen(js_name = createProvider)] -pub unsafe fn create_provider(rpc_url: &str) -> Result { - let rpc_url = url::Url::parse(rpc_url); - if let Err(e) = rpc_url { - return Err(JsValue::from(format!("failed to parse rpc url: {e}"))); - } - let rpc_url = rpc_url.unwrap(); +#[wasm_bindgen] +impl VerifyingKey { + /// Derives a verifying (public) key from a signing (private) key + /// + /// # Parameters + /// * `signing_key` - Signing key as hex string + /// + /// # Returns + /// Result containing verifying key as hex string or error + #[wasm_bindgen(constructor)] + pub fn new(signing_key: &str) -> Result { + let signing_key = Felt::from_str(signing_key); + if let Err(e) = signing_key { + return Err(JsValue::from(format!("failed to parse signing key: {e}"))); + } - let rpc = JsonRpcClient::new(HttpTransport::new(rpc_url)); + let verifying_key = starknet_crypto::get_public_key(&signing_key.unwrap()); - Result::Ok(Provider(Arc::new(rpc))) -} + Ok(VerifyingKey(starknet::signers::VerifyingKey::from_scalar(verifying_key))) + } -#[wasm_bindgen] -impl Provider { - /// Creates a new account instance with the given private key and address + /// Returns the scalar of the verifying key + /// + /// # Returns + /// Result containing scalar as hex string or error + pub fn scalar(&self) -> Result { + Ok(format!("{:#x}", self.0.scalar())) + } + + /// Verifies a signature against a message hash using a verifying key /// /// # Parameters - /// * `private_key` - Private key as hex string - /// * `address` - Account address as hex string + /// * `verifying_key` - Verifying key as hex string + /// * `hash` - Message hash as hex string + /// * `signature` - Signature to verify /// /// # Returns - /// Result containing Account instance or error - #[wasm_bindgen(js_name = createAccount)] - pub async unsafe fn create_account( - &self, - private_key: &str, - address: &str, - ) -> Result { - let private_key = Felt::from_str(private_key); - if let Err(e) = private_key { - return Err(JsValue::from(format!("failed to parse private key: {e}"))); + /// Result containing verification success boolean or error + #[wasm_bindgen] + pub fn verify(self, hash: &str, signature: Signature) -> Result { + let hash = Felt::from_str(hash); + if let Err(e) = hash { + return Err(JsValue::from(format!("failed to parse hash: {e}"))); } - let private_key = private_key.unwrap(); + let hash = &hash.unwrap(); - let address = Felt::from_str(address); - if let Err(e) = address { - return Err(JsValue::from(format!("failed to parse address: {e}"))); + match self.0.verify(hash, &signature.into()) { + Ok(result) => Result::Ok(result), + Err(e) => Err(JsValue::from(format!("failed to verify: {e}"))), } + } +} - let address = address.unwrap(); +#[wasm_bindgen] +impl TypedData { + #[wasm_bindgen(constructor)] + pub fn new(typed_data: &str) -> Result { + let typed_data = serde_json::from_str::(typed_data) + .map_err(|err| JsValue::from(format!("failed to parse typed data: {err}")))?; - let chain_id = self.0.chain_id().await; - if let Err(e) = chain_id { - return Err(JsValue::from(format!("failed to get chain id: {e}"))); - } + Ok(TypedData(typed_data)) + } - let chain_id = chain_id.unwrap(); + /// Encodes typed data according to Starknet's typed data specification + /// + /// # Parameters + /// * `typed_data` - JSON string containing the typed data + /// * `address` - Address as hex string + /// + /// # Returns + /// Result containing encoded data as hex string or error + #[wasm_bindgen] + pub fn encode(&self, address: &str) -> Result { + let address = Felt::from_str(&address) + .map_err(|err| JsValue::from(format!("failed to parse address: {err}")))?; + + self.0 + .encode(address) + .map(|felt| format!("{:#x}", felt)) + .map_err(|err| JsValue::from(err.to_string())) + } +} - let signer = LocalWallet::from_signing_key(SigningKey::from_secret_scalar(private_key)); - let account = SingleOwnerAccount::new( - self.0.clone(), - signer, - address, - chain_id, - ExecutionEncoding::New, - ); +#[wasm_bindgen] +impl Provider { + /// Creates a new Starknet provider instance for a given RPC URL + /// + /// # Parameters + /// * `rpc_url` - URL of the RPC endpoint + /// + /// # Returns + /// Result containing Provider instance or error + #[wasm_bindgen(constructor)] + pub fn new(rpc_url: &str) -> Result { + let rpc_url = url::Url::parse(rpc_url); + if let Err(e) = rpc_url { + return Err(JsValue::from(format!("failed to parse rpc url: {e}"))); + } + let rpc_url = rpc_url.unwrap(); - Result::Ok(Account(account)) + let rpc = JsonRpcClient::new(HttpTransport::new(rpc_url)); + + Result::Ok(Provider(Arc::new(rpc))) } /// Calls a Starknet contract view function @@ -285,6 +292,56 @@ impl Provider { #[wasm_bindgen] impl Account { + /// Creates a new account instance with the given private key and address + /// + /// # Parameters + /// * `provider` - Provider instance + /// * `private_key` - Private key as hex string + /// * `address` - Account address as hex string + /// + /// # Returns + /// Result containing Account instance or error + #[wasm_bindgen(constructor)] + pub async unsafe fn new( + provider: &Provider, + private_key: &str, + address: &str, + ) -> Result { + let private_key = Felt::from_str(private_key); + if let Err(e) = private_key { + return Err(JsValue::from(format!("failed to parse private key: {e}"))); + } + + let private_key = private_key.unwrap(); + + let address = Felt::from_str(address); + if let Err(e) = address { + return Err(JsValue::from(format!("failed to parse address: {e}"))); + } + + let address = address.unwrap(); + + let chain_id = provider.0.chain_id().await; + if let Err(e) = chain_id { + return Err(JsValue::from(format!("failed to get chain id: {e}"))); + } + + let chain_id = chain_id.unwrap(); + + let signer = LocalWallet::from_signing_key( + starknet::signers::SigningKey::from_secret_scalar(private_key), + ); + let account = SingleOwnerAccount::new( + provider.0.clone(), + signer, + address, + chain_id, + ExecutionEncoding::New, + ); + + Result::Ok(Account(account)) + } + /// Returns the account's address /// /// # Returns @@ -355,9 +412,10 @@ impl Account { Err(e) => return Err(JsValue::from(format!("failed to parse private key: {e}"))), }; - let signing_key = SigningKey::from_secret_scalar(private_key); - let verifying_key = signing_key.verifying_key(); - let address = get_contract_address( + let signing_key = starknet::signers::SigningKey::from_secret_scalar(private_key); + let verifying_key = + starknet::signers::VerifyingKey::from_scalar(signing_key.secret_scalar()); + let address = starknet::core::utils::get_contract_address( verifying_key.scalar(), constants::KATANA_ACCOUNT_CLASS_HASH, &[verifying_key.scalar()], @@ -418,8 +476,8 @@ impl Account { /// /// # Returns /// Result containing computed contract address as hex string or error -#[wasm_bindgen(js_name = hashGetContractAddress)] -pub fn hash_get_contract_address( +#[wasm_bindgen(js_name = getContractAddress)] +pub fn get_contract_address( class_hash: &str, salt: &str, constructor_calldata: Vec, @@ -441,7 +499,12 @@ pub fn hash_get_contract_address( }) .collect::, _>>()?; - let address = get_contract_address(salt, class_hash, &constructor_calldata, deployer_address); + let address = starknet::core::utils::get_contract_address( + salt, + class_hash, + &constructor_calldata, + deployer_address, + ); Ok(format!("{:#x}", address)) } @@ -459,47 +522,64 @@ pub fn get_selector_from_tag(tag: &str) -> String { format!("{:#x}", selector) } -/// Serializes a string into a Cairo byte array -/// -/// # Parameters -/// * `str` - String to serialize -/// -/// # Returns -/// Result containing array of field elements as hex strings or error -#[wasm_bindgen(js_name = byteArraySerialize)] -pub fn bytearray_serialize(str: &str) -> Result, JsValue> { - let bytearray = match ByteArray::from_string(str) { - Ok(bytearray) => bytearray, - Err(e) => return Err(JsValue::from(format!("failed to parse bytearray: {e}"))), - }; - let felts = cairo_serde::ByteArray::cairo_serialize(&bytearray); - - Ok(felts.iter().map(|f| format!("{:#x}", f)).collect()) -} +#[wasm_bindgen] +impl ByteArray { + /// Serializes a string into a Cairo byte array + /// + /// # Parameters + /// * `str` - String to serialize + /// + /// # Returns + /// Result containing array of field elements as hex strings or error + #[wasm_bindgen(constructor)] + pub fn new(str: &str) -> Result { + let bytearray = match cainome::cairo_serde::ByteArray::from_string(str) { + Ok(bytearray) => bytearray, + Err(e) => return Err(JsValue::from(format!("failed to parse bytearray: {e}"))), + }; + Ok(ByteArray(bytearray)) + } -/// Deserializes a Cairo byte array into a string -/// -/// # Parameters -/// * `felts` - Array of field elements as hex strings -/// -/// # Returns -/// Result containing deserialized string or error -#[wasm_bindgen(js_name = byteArrayDeserialize)] -pub fn bytearray_deserialize(felts: Vec) -> Result { - let felts = felts - .iter() - .map(|f| Felt::from_str(f)) - .collect::, _>>() - .map_err(|e| JsValue::from(format!("failed to parse felts: {e}")))?; + /// Serializes a Cairo byte array into a vector of field elements as hex strings + /// + /// # Returns + /// Result containing vector of field elements as hex strings or error + #[wasm_bindgen(js_name = toRaw)] + pub fn to_raw(&self) -> Result, JsValue> { + let felts = cairo_serde::ByteArray::cairo_serialize(&self.0); + Ok(felts.iter().map(|f| format!("{:#x}", f)).collect()) + } - let bytearray = match cairo_serde::ByteArray::cairo_deserialize(&felts, 0) { - Ok(bytearray) => bytearray, - Err(e) => return Err(JsValue::from(format!("failed to deserialize bytearray: {e}"))), - }; + /// Deserializes a Cairo byte array into a string + /// + /// # Parameters + /// * `felts` - Array of field elements as hex strings + /// + /// # Returns + /// Result containing deserialized string or error + #[wasm_bindgen(js_name = fromRaw)] + pub fn from_raw(felts: Vec) -> Result { + let felts = felts + .iter() + .map(|f| Felt::from_str(f)) + .collect::, _>>() + .map_err(|e| JsValue::from(format!("failed to parse felts: {e}")))?; + match cainome::cairo_serde::ByteArray::cairo_deserialize(&felts, 0) { + Ok(bytearray) => Ok(ByteArray(bytearray)), + Err(e) => Err(JsValue::from(format!("failed to deserialize bytearray: {e}"))), + } + } - match bytearray.to_string() { - Ok(s) => Ok(s), - Err(e) => Err(JsValue::from(format!("failed to serialize bytearray: {e}"))), + /// Converts a Cairo byte array to a string + /// + /// # Returns + /// Result containing string representation of the byte array or error + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> Result { + match self.0.to_string() { + Ok(s) => Ok(s), + Err(e) => Err(JsValue::from(format!("failed to serialize bytearray: {e}"))), + } } } @@ -584,6 +664,35 @@ pub fn parse_cairo_short_string(str: &str) -> Result { #[wasm_bindgen] impl ToriiClient { + /// Creates a new Torii client with the given configuration + /// + /// # Parameters + /// * `config` - Client configuration including URLs and world address + /// + /// # Returns + /// Result containing ToriiClient instance or error + #[wasm_bindgen(constructor)] + pub async fn new(config: ClientConfig) -> Result { + #[cfg(feature = "console-error-panic")] + console_error_panic_hook::set_once(); + + let ClientConfig { torii_url, relay_url, world_address } = config; + + let world_address = Felt::from_str(&world_address) + .map_err(|err| JsValue::from(format!("failed to parse world address: {err}")))?; + + let client = torii_client::client::Client::new(torii_url, relay_url, world_address) + .await + .map_err(|err| JsValue::from(format!("failed to build client: {err}")))?; + + let relay_runner = client.relay_runner(); + wasm_bindgen_futures::spawn_local(async move { + relay_runner.lock().await.run().await; + }); + + Ok(ToriiClient { inner: Arc::new(client) }) + } + /// Gets controllers along with their usernames for the given contract addresses /// /// # Parameters @@ -1309,33 +1418,3 @@ impl Subscription { self.trigger.cancel(); } } - -/// Creates a new Torii client with the given configuration -/// -/// # Parameters -/// * `config` - Client configuration including URLs and world address -/// -/// # Returns -/// Result containing ToriiClient instance or error -#[wasm_bindgen(js_name = createClient)] -#[allow(non_snake_case)] -pub async fn create_client(config: ClientConfig) -> Result { - #[cfg(feature = "console-error-panic")] - console_error_panic_hook::set_once(); - - let ClientConfig { torii_url, relay_url, world_address } = config; - - let world_address = Felt::from_str(&world_address) - .map_err(|err| JsValue::from(format!("failed to parse world address: {err}")))?; - - let client = torii_client::client::Client::new(torii_url, relay_url, world_address) - .await - .map_err(|err| JsValue::from(format!("failed to build client: {err}")))?; - - let relay_runner = client.relay_runner(); - wasm_bindgen_futures::spawn_local(async move { - relay_runner.lock().await.run().await; - }); - - Ok(ToriiClient { inner: Arc::new(client) }) -}