diff --git a/Cargo.lock b/Cargo.lock index 8bf9480..a5f36ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5167,8 +5167,8 @@ dependencies = [ [[package]] name = "torii-client" -version = "1.7.0" -source = "git+https://github.com/dojoengine/torii?rev=358004a#358004a6af9ba6bc7c4de765201539cd94cfc90a" +version = "1.7.2" +source = "git+https://github.com/dojoengine/torii?tag=v1.7.2#ca702377ffe109346560298892867a4c556e4305" dependencies = [ "async-trait", "crypto-bigint", @@ -5191,8 +5191,8 @@ dependencies = [ [[package]] name = "torii-grpc-client" -version = "1.7.0" -source = "git+https://github.com/dojoengine/torii?rev=358004a#358004a6af9ba6bc7c4de765201539cd94cfc90a" +version = "1.7.2" +source = "git+https://github.com/dojoengine/torii?tag=v1.7.2#ca702377ffe109346560298892867a4c556e4305" dependencies = [ "crypto-bigint", "dojo-types", @@ -5217,8 +5217,8 @@ dependencies = [ [[package]] name = "torii-proto" -version = "1.7.0" -source = "git+https://github.com/dojoengine/torii?rev=358004a#358004a6af9ba6bc7c4de765201539cd94cfc90a" +version = "1.7.2" +source = "git+https://github.com/dojoengine/torii?tag=v1.7.2#ca702377ffe109346560298892867a4c556e4305" dependencies = [ "chrono", "crypto-bigint", diff --git a/Cargo.toml b/Cargo.toml index 777310a..55507af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ crate-type = ["cdylib", "rlib", "staticlib"] [dependencies] dojo-world = { git = "https://github.com/dojoengine/dojo", rev = "6daa3d0" } dojo-types = { git = "https://github.com/dojoengine/dojo", rev = "6daa3d0" } -torii-proto = { git = "https://github.com/dojoengine/torii", rev = "358004a" } -torii-client = { git = "https://github.com/dojoengine/torii", rev = "358004a" } -torii-grpc-client = { git = "https://github.com/dojoengine/torii", rev = "358004a" } +torii-proto = { git = "https://github.com/dojoengine/torii", tag = "v1.7.2" } +torii-client = { git = "https://github.com/dojoengine/torii", tag = "v1.7.2" } +torii-grpc-client = { git = "https://github.com/dojoengine/torii", tag = "v1.7.2" } starknet = "0.17.0" starknet-crypto = "0.8" diff --git a/dojo.h b/dojo.h index 3eeed5d..270fe57 100644 --- a/dojo.h +++ b/dojo.h @@ -26,6 +26,7 @@ struct TokenBalance; struct TokenContract; enum ContractType; struct Contract; +struct TokenTransfer; struct Provider; struct Account; struct Ty; @@ -793,6 +794,40 @@ typedef struct ContractQuery { struct CArrayContractType contract_types; } ContractQuery; +typedef struct CArrayTokenTransfer { + struct TokenTransfer *data; + uintptr_t data_len; +} CArrayTokenTransfer; + +typedef struct PageTokenTransfer { + struct CArrayTokenTransfer items; + struct COptionc_char next_cursor; +} PageTokenTransfer; + +typedef enum ResultPageTokenTransfer_Tag { + OkPageTokenTransfer, + ErrPageTokenTransfer, +} ResultPageTokenTransfer_Tag; + +typedef struct ResultPageTokenTransfer { + ResultPageTokenTransfer_Tag tag; + union { + struct { + struct PageTokenTransfer ok; + }; + struct { + struct Error err; + }; + }; +} ResultPageTokenTransfer; + +typedef struct TokenTransferQuery { + struct CArrayFieldElement contract_addresses; + struct CArrayFieldElement account_addresses; + struct CArrayU256 token_ids; + struct Pagination pagination; +} TokenTransferQuery; + typedef enum COptionFieldElement_Tag { SomeFieldElement, NoneFieldElement, @@ -825,6 +860,17 @@ typedef struct TokenBalance { struct COptionU256 token_id; } TokenBalance; +typedef struct TokenTransfer { + const char *id; + struct FieldElement contract_address; + struct FieldElement from_address; + struct FieldElement to_address; + struct U256 amount; + struct COptionU256 token_id; + uint64_t executed_at; + struct COptionc_char event_id; +} TokenTransfer; + typedef enum Resultc_char_Tag { Okc_char, Errc_char, @@ -1051,6 +1097,7 @@ typedef struct TokenContract { const char *symbol; uint8_t decimals; const char *metadata; + const char *token_metadata; struct COptionU256 total_supply; } TokenContract; @@ -1514,6 +1561,19 @@ struct ResultPageTokenContract client_token_contracts(struct ToriiClient *client struct ResultCArrayContract client_contracts(struct ToriiClient *client, struct ContractQuery query); +/** + * Retrieves token transfers matching the given query + * + * # Parameters + * * `client` - Pointer to ToriiClient instance + * * `query` - TokenTransferQuery parameters + * + * # Returns + * Result containing array of TokenTransfer information or error + */ +struct ResultPageTokenTransfer client_token_transfers(struct ToriiClient *client, + struct TokenTransferQuery query); + /** * Subscribes to contract updates * @@ -1575,6 +1635,56 @@ struct Resultbool client_update_token_balance_subscription(struct ToriiClient *c const struct U256 *token_ids, uintptr_t token_ids_len); +/** + * Subscribes to token transfer updates + * + * # Parameters + * * `client` - Pointer to ToriiClient instance + * * `contract_addresses` - Array of contract addresses to filter (empty for all) + * * `contract_addresses_len` - Length of contract addresses array + * * `account_addresses` - Array of account addresses to filter (empty for all) + * * `account_addresses_len` - Length of account addresses array + * * `token_ids` - Array of token IDs to filter (empty for all) + * * `token_ids_len` - Length of token IDs array + * * `callback` - Function called when updates occur + * + * # Returns + * Result containing pointer to Subscription or error + */ +struct ResultSubscription client_on_token_transfer_update(struct ToriiClient *client, + const struct FieldElement *contract_addresses, + uintptr_t contract_addresses_len, + const struct FieldElement *account_addresses, + uintptr_t account_addresses_len, + const struct U256 *token_ids, + uintptr_t token_ids_len, + void (*callback)(struct TokenTransfer)); + +/** + * Updates an existing token transfer subscription + * + * # Parameters + * * `client` - Pointer to ToriiClient instance + * * `subscription` - Pointer to existing Subscription + * * `contract_addresses` - Array of contract addresses to filter (empty for all) + * * `contract_addresses_len` - Length of contract addresses array + * * `account_addresses` - Array of account addresses to filter (empty for all) + * * `account_addresses_len` - Length of account addresses array + * * `token_ids` - Array of token IDs to filter (empty for all) + * * `token_ids_len` - Length of token IDs array + * + * # Returns + * Result containing success boolean or error + */ +struct Resultbool client_update_token_transfer_subscription(struct ToriiClient *client, + struct Subscription *subscription, + const struct FieldElement *contract_addresses, + uintptr_t contract_addresses_len, + const struct FieldElement *account_addresses, + uintptr_t account_addresses_len, + const struct U256 *token_ids, + uintptr_t token_ids_len); + /** * Serializes a string into a byte array * diff --git a/dojo.hpp b/dojo.hpp index e05d175..ea84cf6 100644 --- a/dojo.hpp +++ b/dojo.hpp @@ -920,6 +920,7 @@ struct TokenContract { const char *symbol; uint8_t decimals; const char *metadata; + const char *token_metadata; COption total_supply; }; @@ -945,6 +946,24 @@ struct ContractQuery { CArray contract_types; }; +struct TokenTransfer { + const char *id; + FieldElement contract_address; + FieldElement from_address; + FieldElement to_address; + U256 amount; + COption token_id; + uint64_t executed_at; + COption event_id; +}; + +struct TokenTransferQuery { + CArray contract_addresses; + CArray account_addresses; + CArray token_ids; + Pagination pagination; +}; + struct Signature { /// The `r` value of a signature FieldElement r; @@ -1408,6 +1427,16 @@ Result> client_token_contracts(ToriiClient *client, TokenCon /// Result containing array of Contract information or error Result> client_contracts(ToriiClient *client, ContractQuery query); +/// Retrieves token transfers matching the given query +/// +/// # Parameters +/// * `client` - Pointer to ToriiClient instance +/// * `query` - TokenTransferQuery parameters +/// +/// # Returns +/// Result containing array of TokenTransfer information or error +Result> client_token_transfers(ToriiClient *client, TokenTransferQuery query); + /// Subscribes to contract updates /// /// # Parameters @@ -1463,6 +1492,52 @@ Result client_update_token_balance_subscription(ToriiClient *client, const U256 *token_ids, uintptr_t token_ids_len); +/// Subscribes to token transfer updates +/// +/// # Parameters +/// * `client` - Pointer to ToriiClient instance +/// * `contract_addresses` - Array of contract addresses to filter (empty for all) +/// * `contract_addresses_len` - Length of contract addresses array +/// * `account_addresses` - Array of account addresses to filter (empty for all) +/// * `account_addresses_len` - Length of account addresses array +/// * `token_ids` - Array of token IDs to filter (empty for all) +/// * `token_ids_len` - Length of token IDs array +/// * `callback` - Function called when updates occur +/// +/// # Returns +/// Result containing pointer to Subscription or error +Result client_on_token_transfer_update(ToriiClient *client, + const FieldElement *contract_addresses, + uintptr_t contract_addresses_len, + const FieldElement *account_addresses, + uintptr_t account_addresses_len, + const U256 *token_ids, + uintptr_t token_ids_len, + void (*callback)(TokenTransfer)); + +/// Updates an existing token transfer subscription +/// +/// # Parameters +/// * `client` - Pointer to ToriiClient instance +/// * `subscription` - Pointer to existing Subscription +/// * `contract_addresses` - Array of contract addresses to filter (empty for all) +/// * `contract_addresses_len` - Length of contract addresses array +/// * `account_addresses` - Array of account addresses to filter (empty for all) +/// * `account_addresses_len` - Length of account addresses array +/// * `token_ids` - Array of token IDs to filter (empty for all) +/// * `token_ids_len` - Length of token IDs array +/// +/// # Returns +/// Result containing success boolean or error +Result client_update_token_transfer_subscription(ToriiClient *client, + Subscription *subscription, + const FieldElement *contract_addresses, + uintptr_t contract_addresses_len, + const FieldElement *account_addresses, + uintptr_t account_addresses_len, + const U256 *token_ids, + uintptr_t token_ids_len); + /// Serializes a string into a byte array /// /// # Parameters diff --git a/dojo.pyx b/dojo.pyx index 0553e37..f956277 100644 --- a/dojo.pyx +++ b/dojo.pyx @@ -522,6 +522,29 @@ cdef extern from *: CArrayFieldElement contract_addresses; CArrayContractType contract_types; + cdef struct CArrayTokenTransfer: + TokenTransfer *data; + uintptr_t data_len; + + cdef struct PageTokenTransfer: + CArrayTokenTransfer items; + COptionc_char next_cursor; + + cdef enum ResultPageTokenTransfer_Tag: + OkPageTokenTransfer, + ErrPageTokenTransfer, + + cdef struct ResultPageTokenTransfer: + ResultPageTokenTransfer_Tag tag; + PageTokenTransfer ok; + Error err; + + cdef struct TokenTransferQuery: + CArrayFieldElement contract_addresses; + CArrayFieldElement account_addresses; + CArrayU256 token_ids; + Pagination pagination; + cdef enum COptionFieldElement_Tag: SomeFieldElement, NoneFieldElement, @@ -546,6 +569,16 @@ cdef extern from *: FieldElement contract_address; COptionU256 token_id; + cdef struct TokenTransfer: + const char *id; + FieldElement contract_address; + FieldElement from_address; + FieldElement to_address; + U256 amount; + COptionU256 token_id; + uint64_t executed_at; + COptionc_char event_id; + cdef enum Resultc_char_Tag: Okc_char, Errc_char, @@ -692,6 +725,7 @@ cdef extern from *: const char *symbol; uint8_t decimals; const char *metadata; + const char *token_metadata; COptionU256 total_supply; cdef struct Member: @@ -1084,6 +1118,16 @@ cdef extern from *: # Result containing array of Contract information or error ResultCArrayContract client_contracts(ToriiClient *client, ContractQuery query); + # Retrieves token transfers matching the given query + # + # # Parameters + # * `client` - Pointer to ToriiClient instance + # * `query` - TokenTransferQuery parameters + # + # # Returns + # Result containing array of TokenTransfer information or error + ResultPageTokenTransfer client_token_transfers(ToriiClient *client, TokenTransferQuery query); + # Subscribes to contract updates # # # Parameters @@ -1139,6 +1183,52 @@ cdef extern from *: const U256 *token_ids, uintptr_t token_ids_len); + # Subscribes to token transfer updates + # + # # Parameters + # * `client` - Pointer to ToriiClient instance + # * `contract_addresses` - Array of contract addresses to filter (empty for all) + # * `contract_addresses_len` - Length of contract addresses array + # * `account_addresses` - Array of account addresses to filter (empty for all) + # * `account_addresses_len` - Length of account addresses array + # * `token_ids` - Array of token IDs to filter (empty for all) + # * `token_ids_len` - Length of token IDs array + # * `callback` - Function called when updates occur + # + # # Returns + # Result containing pointer to Subscription or error + ResultSubscription client_on_token_transfer_update(ToriiClient *client, + const FieldElement *contract_addresses, + uintptr_t contract_addresses_len, + const FieldElement *account_addresses, + uintptr_t account_addresses_len, + const U256 *token_ids, + uintptr_t token_ids_len, + void (*callback)(TokenTransfer)); + + # Updates an existing token transfer subscription + # + # # Parameters + # * `client` - Pointer to ToriiClient instance + # * `subscription` - Pointer to existing Subscription + # * `contract_addresses` - Array of contract addresses to filter (empty for all) + # * `contract_addresses_len` - Length of contract addresses array + # * `account_addresses` - Array of account addresses to filter (empty for all) + # * `account_addresses_len` - Length of account addresses array + # * `token_ids` - Array of token IDs to filter (empty for all) + # * `token_ids_len` - Length of token IDs array + # + # # Returns + # Result containing success boolean or error + Resultbool client_update_token_transfer_subscription(ToriiClient *client, + Subscription *subscription, + const FieldElement *contract_addresses, + uintptr_t contract_addresses_len, + const FieldElement *account_addresses, + uintptr_t account_addresses_len, + const U256 *token_ids, + uintptr_t token_ids_len); + # Serializes a string into a byte array # # # Parameters diff --git a/src/c/mod.rs b/src/c/mod.rs index 7d998ab..78991f2 100644 --- a/src/c/mod.rs +++ b/src/c/mod.rs @@ -53,13 +53,13 @@ use tower_http::cors::{AllowOrigin, CorsLayer}; use types::{ BlockId, CArray, COption, Call, Clause, Contract, Controller, Entity, Error, Event, KeysClause, Page, Policy, Query, Result, Signature, Struct, Token, TokenBalance, TokenContract, - ToriiClient, Ty, World, + TokenTransfer, TokenTransferQuery, ToriiClient, Ty, World, }; use url::Url; use crate::c::types::{ - ContractQuery, ControllerQuery, TokenBalanceQuery, TokenContractQuery, TokenQuery, Transaction, - TransactionFilter, TransactionQuery, + ContractQuery, ControllerQuery, TokenBalanceQuery, TokenContractQuery, TokenQuery, + Transaction, TransactionFilter, TransactionQuery, }; use crate::constants; use crate::types::{ @@ -1378,6 +1378,28 @@ pub unsafe extern "C" fn client_contracts( } } +/// Retrieves token transfers matching the given query +/// +/// # Parameters +/// * `client` - Pointer to ToriiClient instance +/// * `query` - TokenTransferQuery parameters +/// +/// # Returns +/// Result containing array of TokenTransfer information or error +#[no_mangle] +pub unsafe extern "C" fn client_token_transfers( + client: *mut ToriiClient, + query: TokenTransferQuery, +) -> Result> { + let query = query.into(); + let token_transfers_future = unsafe { (*client).inner.token_transfers(query) }; + + match RUNTIME.block_on(token_transfers_future) { + Ok(transfers) => Result::Ok(transfers.into()), + Err(e) => Result::Err(e.into()), + } +} + /// Subscribes to contract updates /// /// # Parameters @@ -1620,6 +1642,178 @@ pub unsafe extern "C" fn client_update_token_balance_subscription( } } +/// Subscribes to token transfer updates +/// +/// # Parameters +/// * `client` - Pointer to ToriiClient instance +/// * `contract_addresses` - Array of contract addresses to filter (empty for all) +/// * `contract_addresses_len` - Length of contract addresses array +/// * `account_addresses` - Array of account addresses to filter (empty for all) +/// * `account_addresses_len` - Length of account addresses array +/// * `token_ids` - Array of token IDs to filter (empty for all) +/// * `token_ids_len` - Length of token IDs array +/// * `callback` - Function called when updates occur +/// +/// # Returns +/// Result containing pointer to Subscription or error +#[no_mangle] +pub unsafe extern "C" fn client_on_token_transfer_update( + client: *mut ToriiClient, + contract_addresses: *const types::FieldElement, + contract_addresses_len: usize, + account_addresses: *const types::FieldElement, + account_addresses_len: usize, + token_ids: *const types::U256, + token_ids_len: usize, + callback: unsafe extern "C" fn(TokenTransfer), +) -> Result<*mut Subscription> { + let client = Arc::new(unsafe { &*client }); + + // Convert account addresses array to Vec if not empty + let account_addresses = if account_addresses.is_null() || account_addresses_len == 0 { + Vec::new() + } else { + let addresses = + unsafe { std::slice::from_raw_parts(account_addresses, account_addresses_len) }; + addresses.iter().map(|f| f.clone().into()).collect::>() + }; + + // Convert contract addresses array to Vec if not empty + let contract_addresses = if contract_addresses.is_null() || contract_addresses_len == 0 { + Vec::new() + } else { + let addresses = + unsafe { std::slice::from_raw_parts(contract_addresses, contract_addresses_len) }; + addresses.iter().map(|f| f.clone().into()).collect::>() + }; + + let token_ids = if token_ids.is_null() || token_ids_len == 0 { + Vec::new() + } else { + let ids = unsafe { std::slice::from_raw_parts(token_ids, token_ids_len) }; + ids.iter().map(|f| f.clone().into()).collect::>() + }; + + let (sub_id_tx, sub_id_rx) = oneshot::channel(); + let mut sub_id_tx = Some(sub_id_tx); + let (trigger, tripwire) = Tripwire::new(); + + // Spawn a new thread to handle the stream and reconnections + let client_clone = client.clone(); + RUNTIME.spawn(async move { + let mut backoff = Duration::from_secs(1); + let max_backoff = Duration::from_secs(60); + + loop { + let rcv = client_clone + .inner + .on_token_transfer_updated( + contract_addresses.clone(), + account_addresses.clone(), + token_ids.clone(), + ) + .await; + + if let Ok(rcv) = rcv { + backoff = Duration::from_secs(1); // Reset backoff on successful connection + + let mut rcv = rcv.take_until_if(tripwire.clone()); + + while let Some(Ok(transfer)) = rcv.next().await { + if let Some(tx) = sub_id_tx.take() { + tx.send(0).expect("Failed to send subscription ID"); + } else { + let transfer: types::TokenTransfer = transfer.into(); + callback(transfer); + } + } + } + + // If we've reached this point, the stream has ended (possibly due to disconnection) + // We'll try to reconnect after a delay, unless the tripwire has been triggered + if tripwire.clone().now_or_never().unwrap_or_default() { + break; // Exit the loop if the subscription has been cancelled + } + sleep(backoff).await; + backoff = std::cmp::min(backoff * 2, max_backoff); + } + }); + + let subscription_id = match RUNTIME.block_on(sub_id_rx) { + Ok(id) => id, + Err(_) => { + return Result::Err(Error { + message: CString::new("Failed to establish token transfer subscription") + .unwrap() + .into_raw(), + }); + } + }; + + let subscription = Subscription { id: subscription_id, trigger }; + + Result::Ok(Box::into_raw(Box::new(subscription))) +} + +/// Updates an existing token transfer subscription +/// +/// # Parameters +/// * `client` - Pointer to ToriiClient instance +/// * `subscription` - Pointer to existing Subscription +/// * `contract_addresses` - Array of contract addresses to filter (empty for all) +/// * `contract_addresses_len` - Length of contract addresses array +/// * `account_addresses` - Array of account addresses to filter (empty for all) +/// * `account_addresses_len` - Length of account addresses array +/// * `token_ids` - Array of token IDs to filter (empty for all) +/// * `token_ids_len` - Length of token IDs array +/// +/// # Returns +/// Result containing success boolean or error +#[no_mangle] +pub unsafe extern "C" fn client_update_token_transfer_subscription( + client: *mut ToriiClient, + subscription: *mut Subscription, + contract_addresses: *const types::FieldElement, + contract_addresses_len: usize, + account_addresses: *const types::FieldElement, + account_addresses_len: usize, + token_ids: *const types::U256, + token_ids_len: usize, +) -> Result { + let contract_addresses = if contract_addresses.is_null() || contract_addresses_len == 0 { + Vec::new() + } else { + let addresses = + unsafe { std::slice::from_raw_parts(contract_addresses, contract_addresses_len) }; + addresses.iter().map(|f| f.clone().into()).collect::>() + }; + + let account_addresses = if account_addresses.is_null() || account_addresses_len == 0 { + Vec::new() + } else { + let addresses = + unsafe { std::slice::from_raw_parts(account_addresses, account_addresses_len) }; + addresses.iter().map(|f| f.clone().into()).collect::>() + }; + + let token_ids = if token_ids.is_null() || token_ids_len == 0 { + Vec::new() + } else { + let ids = unsafe { std::slice::from_raw_parts(token_ids, token_ids_len) }; + ids.iter().map(|f| f.clone().into()).collect::>() + }; + + match RUNTIME.block_on((*client).inner.update_token_transfer_subscription( + (*subscription).id, + contract_addresses, + account_addresses, + token_ids, + )) { + Ok(_) => Result::Ok(true), + Err(e) => Result::Err(e.into()), + } +} + /// Serializes a string into a byte array /// /// # Parameters diff --git a/src/c/types.rs b/src/c/types.rs index e4b5eaf..457db7b 100644 --- a/src/c/types.rs +++ b/src/c/types.rs @@ -183,6 +183,7 @@ pub struct TokenContract { pub symbol: *const c_char, pub decimals: u8, pub metadata: *const c_char, + pub token_metadata: *const c_char, pub total_supply: COption, } @@ -193,6 +194,7 @@ impl From for TokenContract { name: CString::new(value.name.clone()).unwrap().into_raw(), symbol: CString::new(value.symbol.clone()).unwrap().into_raw(), decimals: value.decimals, + token_metadata: CString::new(value.token_metadata.clone()).unwrap().into_raw(), total_supply: value.total_supply.into(), metadata: CString::new(value.metadata.clone()).unwrap().into_raw(), } @@ -584,6 +586,54 @@ impl From for torii_proto::TokenContractQuery { } } +#[derive(Debug, Clone)] +#[repr(C)] +pub struct TokenTransfer { + pub id: *const c_char, + pub contract_address: FieldElement, + pub from_address: FieldElement, + pub to_address: FieldElement, + pub amount: U256, + pub token_id: COption, + pub executed_at: u64, + pub event_id: COption<*const c_char>, +} + +impl From for TokenTransfer { + fn from(val: torii_proto::TokenTransfer) -> Self { + TokenTransfer { + id: CString::new(val.id.clone()).unwrap().into_raw(), + contract_address: val.contract_address.into(), + from_address: val.from_address.into(), + to_address: val.to_address.into(), + amount: val.amount.into(), + token_id: val.token_id.into(), + executed_at: val.executed_at.timestamp() as u64, + event_id: val.event_id.map(|e| CString::new(e).unwrap().into_raw() as *const c_char).into(), + } + } +} + +#[derive(Clone, Debug)] +#[repr(C)] +pub struct TokenTransferQuery { + pub contract_addresses: CArray, + pub account_addresses: CArray, + pub token_ids: CArray, + pub pagination: Pagination, +} + +impl From for torii_proto::TokenTransferQuery { + fn from(val: TokenTransferQuery) -> Self { + torii_proto::TokenTransferQuery { + contract_addresses: val.contract_addresses.into(), + account_addresses: val.account_addresses.into(), + token_ids: val.token_ids.into(), + pagination: val.pagination.into(), + } + } +} + #[derive(Clone, Debug)] #[repr(C)] pub struct TransactionFilter { diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs index 3c9134b..887dd75 100644 --- a/src/wasm/mod.rs +++ b/src/wasm/mod.rs @@ -36,7 +36,7 @@ use crate::types::{Account, Provider, Subscription, ToriiClient}; use crate::utils::watch_tx; use crate::wasm::types::{ ContractQuery, ControllerQuery, TokenBalanceQuery, TokenContractQuery, TokenQuery, - TransactionFilter, TransactionQuery, + TokenTransferQuery, TransactionFilter, TransactionQuery, }; mod types; @@ -44,7 +44,7 @@ mod types; use types::{ BlockId, Call, Calls, Clause, ClientConfig, Contract, Contracts, Controller, Controllers, Entities, Entity, KeysClause, KeysClauses, Message, Model, Page, Query, Signature, Token, - TokenBalance, TokenBalances, TokenContracts, Tokens, Transaction, Transactions, WasmU256, + TokenBalance, TokenBalances, TokenContracts, TokenTransfer, TokenTransfers, Tokens, Transaction, Transactions, WasmU256, }; const JSON_COMPAT_SERIALIZER: serde_wasm_bindgen::Serializer = @@ -998,6 +998,29 @@ impl ToriiClient { Ok(TokenContracts(token_contracts.into())) } + /// Gets token transfers for given accounts and contracts + /// + /// # Parameters + /// * `query` - TokenTransferQuery parameters + /// + /// # Returns + /// Result containing token transfers or error + #[wasm_bindgen(js_name = getTokenTransfers)] + pub async fn get_token_transfers( + &self, + query: TokenTransferQuery, + ) -> Result { + let query = query.into(); + + let token_transfers = self + .inner + .token_transfers(query) + .await + .map_err(|e| JsValue::from(format!("failed to get token transfers: {e}")))?; + + Ok(TokenTransfers(token_transfers.into())) + } + /// Queries entities based on the provided query parameters /// /// # Parameters @@ -1548,6 +1571,155 @@ impl ToriiClient { .map_err(|err| JsValue::from(format!("failed to update subscription: {err}"))) } + /// Subscribes to token transfer updates + /// + /// # Parameters + /// * `contract_addresses` - Array of contract addresses to filter (empty for all) + /// * `account_addresses` - Array of account addresses to filter (empty for all) + /// * `token_ids` - Array of token IDs to filter (empty for all) + /// * `callback` - JavaScript function to call on updates + /// + /// # Returns + /// Result containing subscription handle or error + #[wasm_bindgen(js_name = onTokenTransferUpdated)] + pub async fn on_token_transfer_updated( + &self, + contract_addresses: Option>, + account_addresses: Option>, + token_ids: Option>, + callback: js_sys::Function, + ) -> Result { + #[cfg(feature = "console-error-panic")] + console_error_panic_hook::set_once(); + + let account_addresses = account_addresses + .unwrap_or_default() + .iter() + .map(|addr| { + Felt::from_str(addr) + .map_err(|err| JsValue::from(format!("failed to parse account address: {err}"))) + }) + .collect::, _>>()?; + + let contract_addresses = contract_addresses + .unwrap_or_default() + .iter() + .map(|addr| { + Felt::from_str(addr).map_err(|err| { + JsValue::from(format!("failed to parse contract address: {err}")) + }) + }) + .collect::, _>>()?; + + let token_ids = + token_ids.unwrap_or_default().into_iter().map(|t| t.into()).collect::>(); + + let (sub_id_tx, sub_id_rx) = oneshot::channel(); + let mut sub_id_tx = Some(sub_id_tx); + let (trigger, tripwire) = Tripwire::new(); + + // Spawn a new task to handle the stream and reconnections + let client = self.inner.clone(); + wasm_bindgen_futures::spawn_local(async move { + let mut backoff = 1000; + let max_backoff = 60000; + + loop { + if let Ok(stream) = client + .on_token_transfer_updated( + contract_addresses.clone(), + account_addresses.clone(), + token_ids.clone(), + ) + .await + { + backoff = 1000; // Reset backoff on successful connection + + let mut stream = stream.take_until_if(tripwire.clone()); + + while let Some(Ok(transfer)) = stream.next().await { + if let Some(tx) = sub_id_tx.take() { + tx.send(0).expect("Failed to send subscription ID"); + } else { + let transfer: TokenTransfer = transfer.into(); + + let _ = callback.call1( + &JsValue::null(), + &transfer.serialize(&JSON_COMPAT_SERIALIZER).unwrap(), + ); + } + } + } + + // If we've reached this point, the stream has ended (possibly due to disconnection) + // We'll try to reconnect after a delay, unless the tripwire has been triggered + if tripwire.clone().now_or_never().unwrap_or_default() { + break; // Exit the loop if the subscription has been cancelled + } + gloo_timers::future::TimeoutFuture::new(backoff).await; + backoff = std::cmp::min(backoff * 2, max_backoff); + } + }); + + let subscription_id = match sub_id_rx.await { + Ok(id) => id, + Err(_) => { + return Err(JsValue::from("Failed to establish token transfer subscription")); + } + }; + let subscription = Subscription { id: subscription_id, trigger }; + + Ok(subscription) + } + + /// Updates an existing token transfer subscription + /// + /// # Parameters + /// * `subscription` - Existing subscription to update + /// * `contract_addresses` - New array of contract addresses to filter + /// * `account_addresses` - New array of account addresses to filter + /// * `token_ids` - New array of token IDs to filter + /// + /// # Returns + /// Result containing unit or error + #[wasm_bindgen(js_name = updateTokenTransferSubscription)] + pub async fn update_token_transfer_subscription( + &self, + subscription: &Subscription, + contract_addresses: Vec, + account_addresses: Vec, + token_ids: Vec, + ) -> Result<(), JsValue> { + let account_addresses = account_addresses + .iter() + .map(|addr| { + Felt::from_str(addr) + .map_err(|err| JsValue::from(format!("failed to parse account address: {err}"))) + }) + .collect::, _>>()?; + + let contract_addresses = contract_addresses + .iter() + .map(|addr| { + Felt::from_str(addr).map_err(|err| { + JsValue::from(format!("failed to parse contract address: {err}")) + }) + }) + .collect::, _>>()?; + + let token_ids = token_ids.into_iter().map(|t| t.into()).collect::>(); + + self.inner + .update_token_transfer_subscription( + subscription.id, + contract_addresses, + account_addresses, + token_ids, + ) + .await + .map_err(|err| JsValue::from(format!("failed to update subscription: {err}"))) + } + /// Publishes a message to the network /// /// # Parameters diff --git a/src/wasm/types.rs b/src/wasm/types.rs index 5f229b3..033798b 100644 --- a/src/wasm/types.rs +++ b/src/wasm/types.rs @@ -81,12 +81,7 @@ pub struct TokenContracts(pub Page); #[derive(Tsify, Serialize, Deserialize, Debug)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct TokenTransferQuery { - pub contract_addresses: Vec, - pub account_addresses: Vec, - pub token_ids: Vec, - pub pagination: Pagination, -} +pub struct TokenTransfers(pub Page); #[derive(Tsify, Serialize, Deserialize, Debug)] #[tsify(into_wasm_abi, from_wasm_abi)] @@ -122,6 +117,7 @@ pub struct TokenContract { pub symbol: String, pub decimals: u8, pub metadata: String, + pub token_metadata: String, pub total_supply: Option, } @@ -132,6 +128,7 @@ impl From for TokenContract { name: value.name.clone(), symbol: value.symbol.clone(), decimals: value.decimals, + token_metadata: value.token_metadata.clone(), total_supply: value.total_supply.map(|t| format!("0x{:x}", t)), metadata: value.metadata.clone(), } @@ -158,6 +155,62 @@ impl From for TokenBalance { } } +#[derive(Tsify, Serialize, Deserialize, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct TokenTransfer { + pub id: String, + pub contract_address: String, + pub from_address: String, + pub to_address: String, + pub amount: String, + pub token_id: Option, + pub executed_at: u64, + pub event_id: Option, +} + +impl From for TokenTransfer { + fn from(value: torii_proto::TokenTransfer) -> Self { + Self { + id: value.id, + contract_address: format!("{:#x}", value.contract_address), + from_address: format!("{:#x}", value.from_address), + to_address: format!("{:#x}", value.to_address), + amount: format!("0x{:x}", value.amount), + token_id: value.token_id.map(|t| format!("0x{:x}", t)), + executed_at: value.executed_at.timestamp() as u64, + event_id: value.event_id, + } + } +} + +#[derive(Tsify, Serialize, Deserialize, Debug)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct TokenTransferQuery { + pub contract_addresses: Vec, + pub account_addresses: Vec, + pub token_ids: Vec, + pub pagination: Pagination, +} + +impl From for torii_proto::TokenTransferQuery { + fn from(value: TokenTransferQuery) -> Self { + Self { + contract_addresses: value + .contract_addresses + .into_iter() + .map(|c| Felt::from_str(c.as_str()).unwrap()) + .collect(), + account_addresses: value + .account_addresses + .into_iter() + .map(|a| Felt::from_str(a.as_str()).unwrap()) + .collect(), + token_ids: value.token_ids.into_iter().map(|t| U256::from_be_hex(t.as_str())).collect(), + pagination: value.pagination.into(), + } + } +} + #[derive(Tsify, Serialize, Deserialize, Debug)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct TransactionFilter {