diff --git a/Cargo.lock b/Cargo.lock index ff33ceb8b1..593aa09fa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11120,6 +11120,7 @@ dependencies = [ "tari_transaction", "tari_validator_node_rpc", "thiserror 1.0.69", + "time", "tokio", "tower-http 0.4.4", "url", @@ -11130,6 +11131,7 @@ name = "tari_indexer_client" version = "0.11.2" dependencies = [ "anyhow", + "bounded-vec", "multiaddr 0.18.1", "reqwest 0.11.27", "serde", @@ -11514,7 +11516,6 @@ name = "tari_ootle_wallet_storage_sqlite" version = "0.11.2" dependencies = [ "bigdecimal", - "chrono", "diesel", "diesel_migrations", "hex", diff --git a/applications/tari_indexer/Cargo.toml b/applications/tari_indexer/Cargo.toml index a4509eba60..c1e884bbfa 100644 --- a/applications/tari_indexer/Cargo.toml +++ b/applications/tari_indexer/Cargo.toml @@ -66,6 +66,7 @@ serde = { workspace = true, features = ["default", "derive"] } serde_json = { workspace = true } serde_with = { workspace = true, features = ["indexmap"] } thiserror = { workspace = true } +time = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = [ "default", "macros", diff --git a/applications/tari_indexer/src/json_rpc/handlers.rs b/applications/tari_indexer/src/json_rpc/handlers.rs index 3fe5ede109..9a35defd47 100644 --- a/applications/tari_indexer/src/json_rpc/handlers.rs +++ b/applications/tari_indexer/src/json_rpc/handlers.rs @@ -50,6 +50,8 @@ use tari_indexer_client::types::{ GetNonFungiblesResponse, GetSubstateRequest, GetSubstateResponse, + GetSubstatesRequest, + GetSubstatesResponse, GetTemplateDefinitionRequest, GetTemplateDefinitionResponse, GetTransactionResultRequest, @@ -377,6 +379,29 @@ impl JsonRpcHandlers { } } + pub async fn get_substates(&self, value: JsonRpcExtractor) -> JrpcResult { + let answer_id = value.get_answer_id(); + let req: GetSubstatesRequest = value.parse_params()?; + + const MAX_REQUESTS: usize = 20; + + let GetSubstatesRequest { requests } = req; + + if requests.len() > MAX_REQUESTS { + return Err(Self::invalid_params( + answer_id, + format!("Cannot request more than {MAX_REQUESTS} substates at once"), + )); + } + + let substates = self.substate_manager.get_substates(requests.as_slice()).map_err(|e| { + warn!(target: LOG_TARGET, "Error getting substate: {}", e); + Self::internal_error(answer_id, format!("Error getting substate: {}", e)) + })?; + + Ok(JsonRpcResponse::success(answer_id, GetSubstatesResponse { substates })) + } + pub async fn inspect_substate(&self, value: JsonRpcExtractor) -> JrpcResult { let answer_id = value.get_answer_id(); let request: InspectSubstateRequest = value.parse_params()?; @@ -791,7 +816,7 @@ impl JsonRpcHandlers { let transactions = self .transaction_manager - .list_recent_transactions(req.last_id, limit as usize) + .list_recent_transactions(None, limit as usize) .map_err(|e| Self::internal_error(answer_id, e))?; let resp = ListRecentTransactionsResponse { transactions }; @@ -820,6 +845,10 @@ impl JsonRpcHandlers { Self::error_response(answer_id, JsonRpcErrorReason::ApplicationError(404), details) } + fn invalid_params(answer_id: i64, details: T) -> JsonRpcResponse { + Self::error_response(answer_id, JsonRpcErrorReason::InvalidParams, details) + } + fn internal_error(answer_id: i64, error: T) -> JsonRpcResponse { error!(target: LOG_TARGET, "Internal error: {}", error); Self::error_response(answer_id, JsonRpcErrorReason::InternalError, error) diff --git a/applications/tari_indexer/src/json_rpc/server.rs b/applications/tari_indexer/src/json_rpc/server.rs index df3a77762f..514af881c5 100644 --- a/applications/tari_indexer/src/json_rpc/server.rs +++ b/applications/tari_indexer/src/json_rpc/server.rs @@ -75,6 +75,7 @@ async fn handler(Extension(handlers): Extension>, value: Js // Substates "list_substates" => handlers.list_substates(value).await, "get_substate" => handlers.get_substate(value).await, + "get_substates" => handlers.get_substates(value).await, "inspect_substate" => handlers.inspect_substate(value).await, "get_non_fungibles" => handlers.get_non_fungibles(value).await, "get_utxo_updates" => handlers.get_utxo_updates(value).await, diff --git a/applications/tari_indexer/src/storage_sqlite/models/substate.rs b/applications/tari_indexer/src/storage_sqlite/models/substate.rs index db895dc6a9..8e193cdc0d 100644 --- a/applications/tari_indexer/src/storage_sqlite/models/substate.rs +++ b/applications/tari_indexer/src/storage_sqlite/models/substate.rs @@ -23,10 +23,14 @@ use std::convert::TryFrom; -use tari_ootle_storage::time::PrimitiveDateTime; +use tari_ootle_storage::{time::PrimitiveDateTime, StorageError}; use crate::{ - storage_sqlite::{models::substate::SubstateRecord as SubstateRow, schema::substates}, + storage_sqlite::{ + models::substate::SubstateRecord as SubstateRow, + schema::substates, + serialization::deserialize_json, + }, substate_manager::SubstateResponse, }; @@ -45,13 +49,17 @@ pub struct SubstateRecord { } impl TryFrom for SubstateResponse { - type Error = anyhow::Error; + type Error = StorageError; fn try_from(row: SubstateRow) -> Result { Ok(SubstateResponse { - address: row.address.parse()?, + address: row.address.parse().map_err(|e| StorageError::DecodingError { + operation: "TryFrom for SubstateResponse", + item: "Substate", + details: format!("Invalid substate address {}: {}", row.address, e), + })?, version: row.version as u32, - substate: serde_json::from_str(&row.data)?, + substate: deserialize_json(&row.data)?, }) } } diff --git a/applications/tari_indexer/src/storage_sqlite/reader.rs b/applications/tari_indexer/src/storage_sqlite/reader.rs index 3ae372ab55..3269ca2bfd 100644 --- a/applications/tari_indexer/src/storage_sqlite/reader.rs +++ b/applications/tari_indexer/src/storage_sqlite/reader.rs @@ -34,11 +34,14 @@ use tari_template_lib::{ }; use tari_transaction::{Transaction, TransactionId}; -use crate::storage_sqlite::{ - models, - models::{EventRecord, KeyValue, ScannedBlockId, SubstateRecord}, - serialization::{deserialize_hex_try_from, deserialize_json, serialize_hex}, - IndexerStoreReadTransaction, +use crate::{ + storage_sqlite::{ + models, + models::{EventRecord, KeyValue, ScannedBlockId, SubstateRecord}, + serialization::{deserialize_hex_try_from, deserialize_json, serialize_hex}, + IndexerStoreReadTransaction, + }, + substate_manager::SubstateResponse, }; const LOG_TARGET: &str = "tari::indexer::storage_sqlite::reader"; @@ -140,6 +143,22 @@ impl IndexerStoreReadTransaction for SqliteStoreReadTransaction<'_> { }) } + fn get_substates(&mut self, ids: &[SubstateId]) -> Result, StorageError> { + use crate::storage_sqlite::schema::substates; + + let str_ids = ids.iter().map(|id| id.to_string()); + + let rows = substates::table + .select(substates::all_columns) + .filter(substates::address.eq_any(str_ids)) + .get_results::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("get_substate: {}", e), + })?; + + rows.into_iter().map(TryInto::try_into).collect() + } + fn get_non_fungible_count(&mut self, resource_address: String) -> Result { use crate::storage_sqlite::schema::non_fungible_indexes; diff --git a/applications/tari_indexer/src/storage_sqlite/store_factory.rs b/applications/tari_indexer/src/storage_sqlite/store_factory.rs index 26b0b27c55..614917d086 100644 --- a/applications/tari_indexer/src/storage_sqlite/store_factory.rs +++ b/applications/tari_indexer/src/storage_sqlite/store_factory.rs @@ -31,10 +31,13 @@ use tari_template_lib::{ }; use tari_transaction::{Transaction, TransactionId}; -use crate::storage_sqlite::{ - models::{EventRecord, KeyValue, NewScannedBlockId, NewSubstate, SubstateRecord, UtxoUpdateRecord}, - reader::SqliteStoreReadTransaction, - writer::SqliteStoreWriteTransaction, +use crate::{ + storage_sqlite::{ + models::{EventRecord, KeyValue, NewScannedBlockId, NewSubstate, SubstateRecord, UtxoUpdateRecord}, + reader::SqliteStoreReadTransaction, + writer::SqliteStoreWriteTransaction, + }, + substate_manager::SubstateResponse, }; const LOG_TARGET: &str = "tari::indexer::storage_sqlite"; @@ -134,6 +137,8 @@ pub trait IndexerStoreReadTransaction { address: &SubstateId, version: Option, ) -> Result, StorageError>; + + fn get_substates(&mut self, ids: &[SubstateId]) -> Result, StorageError>; fn get_non_fungible_count(&mut self, resource_address: String) -> Result; fn get_non_fungibles_by_resource_address( &mut self, diff --git a/applications/tari_indexer/src/substate_manager.rs b/applications/tari_indexer/src/substate_manager.rs index 80379ed16e..c0a1cb7203 100644 --- a/applications/tari_indexer/src/substate_manager.rs +++ b/applications/tari_indexer/src/substate_manager.rs @@ -20,12 +20,12 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::convert::TryInto; +use std::{collections::HashMap, convert::TryInto}; use serde::{Deserialize, Serialize}; use tari_common_types::types::FixedHash; use tari_engine_types::{ - substate::{SubstateId, SubstateValue}, + substate::{Substate, SubstateId, SubstateValue}, Utxo, UtxoId, }; @@ -160,6 +160,15 @@ impl SubstateManager { } } + pub fn get_substates(&self, substates: &[SubstateId]) -> Result, anyhow::Error> { + let mut tx = self.substate_store.create_read_tx()?; + let substates = tx.get_substates(substates)?; + Ok(substates + .into_iter() + .map(|rec| (rec.address, Substate::new(rec.version, rec.substate))) + .collect()) + } + async fn get_substate_from_db( &self, substate_address: &SubstateId, diff --git a/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx b/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx index 491a66c8f9..9d54f07aa0 100644 --- a/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx +++ b/applications/tari_indexer/web_ui/src/routes/VN/Components/Resources.tsx @@ -86,13 +86,14 @@ function Resources() { } if (nftData) { console.log(nftData); - let { image_url, name } = nftData; - const nftSubstateId = { NonFungible: nft.address }; - let address = substateIdToString(nftSubstateId).split("_", 4); + const { image_url, name } = nftData; + const nftId = nft.address.id; + const key = Object.keys(nftId)[0]; + const address = `${key}_${nftId[key as keyof typeof nftId]}`; nfts.push({ img: image_url, title: name, - address: `${address[2]}_${address[3]}`, + address, version: nft.version, }); } @@ -125,9 +126,9 @@ function Resources() { {nfts.length > 0 && ( - {nfts.map((item) => + {nfts.map((item, i) => item.img ? ( - + ) : ( - + - ) + ), )} )} diff --git a/applications/tari_walletd/src/handlers/nfts.rs b/applications/tari_walletd/src/handlers/nfts.rs index 81a2fea71e..91fbd4da08 100644 --- a/applications/tari_walletd/src/handlers/nfts.rs +++ b/applications/tari_walletd/src/handlers/nfts.rs @@ -231,8 +231,10 @@ pub async fn handle_transfer( inputs.extend(fee_payer_account_inputs); let source_account_address = *source_account.component_address(); - let target_account_address = - derive_component_address_from_public_key(&ACCOUNT_TEMPLATE_ADDRESS, &req.target_account_public_key); + let target_account_address = derive_component_address_from_public_key( + &ACCOUNT_TEMPLATE_ADDRESS, + req.target_account_address.account_public_key(), + ); // TODO: this can be simplified let mut builder = context.transaction_builder(); @@ -241,7 +243,7 @@ pub async fn handle_transfer( if !try_find_target_account(context, &mut inputs, target_account_address, req.resource_address).await? { // We need to create the target account - builder = builder.create_account(req.target_account_public_key) + builder = builder.create_account(*req.target_account_address.account_public_key()) } // add the input for the source account vault substate let src_vault = sdk diff --git a/applications/tari_walletd/src/handlers/substates.rs b/applications/tari_walletd/src/handlers/substates.rs index 459c84cd21..37c5562de9 100644 --- a/applications/tari_walletd/src/handlers/substates.rs +++ b/applications/tari_walletd/src/handlers/substates.rs @@ -12,7 +12,7 @@ use tari_wallet_daemon_client::{ SubstatesGetResponse, SubstatesListRequest, SubstatesListResponse, - WalletSubstateRecord, + WalletSubstateInfo, }, }; @@ -43,14 +43,14 @@ pub async fn handle_get( } Ok(SubstatesGetResponse { - record: record.map(|record| WalletSubstateRecord { + local_record: record.map(|record| WalletSubstateInfo { version: record.substate_id.version(), substate_id: record.substate_id.into_substate_id(), parent_id: record.parent_address, module_name: record.module_name, template_address: record.template_address, }), - substate: substate.map(|s| Substate::new(s.version, s.substate)), + substate_from_remote: substate.map(|s| Substate::new(s.version, s.substate)), }) } @@ -61,21 +61,19 @@ pub async fn handle_list( ) -> Result { let sdk = context.wallet_sdk().clone(); context.check_auth(token, &[JrpcPermission::SubstatesRead])?; + let substates = sdk.substate_api().list_substates( + req.filter_by_type, + req.filter_by_template.as_ref(), + req.limit, + req.offset, + )?; - let result = sdk - .get_network_interface() - .list_substates(req.filter_by_template, req.filter_by_type, req.limit, req.offset) - .await?; - - let substates = result - .substates + let substates = substates .into_iter() - // TODO: should also add the "timestamp" and "type" fields from the indexer list items? - .map(|s| WalletSubstateRecord { - substate_id: s.substate_id, - // TODO: should we remove the "parent_id" field from the wallet API? is it really needed somewhere? - parent_id: None, - version: s.version, + .map(|s| WalletSubstateInfo { + version: s.substate_id.version(), + substate_id: s.substate_id.into_substate_id(), + parent_id: s.parent_address, template_address: s.template_address, module_name: s.module_name, }) diff --git a/applications/tari_walletd/src/jrpc_server.rs b/applications/tari_walletd/src/jrpc_server.rs index 6d50c2e05b..359a8b934b 100644 --- a/applications/tari_walletd/src/jrpc_server.rs +++ b/applications/tari_walletd/src/jrpc_server.rs @@ -184,7 +184,7 @@ async fn handler( }, Some(("stealth_utxos", method)) => { - #[allow(clippy::match_single_binding)] + #[allow(clippy::collapsible_match)] match method { "list" => call_handler(context, value, token, stealth_utxos::handle_list).await, _ => Ok(value.method_not_found(&value.method)), diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx index dae334548e..ac7fe3772c 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/components/SendNft.tsx @@ -155,7 +155,7 @@ export function TransferNftDialog(props: TransferNftDialogProps) { max_fee: 3000, nfts: transferFormState.nfts, source_account: sourceAccount!, - target_account_public_key: transferFormState.targetAccountAddress, + target_account_address: transferFormState.targetAccountAddress, fee_payer_account: feePayerAccount!, resource_address: transferFormState.resourceAddress, }), @@ -172,7 +172,7 @@ export function TransferNftDialog(props: TransferNftDialogProps) { () => ({ nfts: transferFormState.nfts, source_account: sourceAccount!, - target_account_public_key: transferFormState.targetAccountAddress, + target_account_address: transferFormState.targetAccountAddress, dry_run: false, max_fee: parseInt(transferFormState.maxFee) || 3000, fee_payer_account: feePayerAccount!, diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx index c0b590388e..65591f794b 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/NFTs/steps/FormStep.tsx @@ -117,10 +117,9 @@ export default function FormStep({ )} }; diff --git a/bindings/src/types/tari-indexer-client/GetSubstatesResponse.ts b/bindings/src/types/tari-indexer-client/GetSubstatesResponse.ts new file mode 100644 index 0000000000..2c2b50108d --- /dev/null +++ b/bindings/src/types/tari-indexer-client/GetSubstatesResponse.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Substate } from "../Substate"; +import type { SubstateId } from "../SubstateId"; + +export type GetSubstatesResponse = { substates: { [key in SubstateId]?: Substate } }; diff --git a/bindings/src/types/tari-indexer-client/InspectSubstateRequest.ts b/bindings/src/types/tari-indexer-client/InspectSubstateRequest.ts index 013df5b9cd..780436d6c4 100644 --- a/bindings/src/types/tari-indexer-client/InspectSubstateRequest.ts +++ b/bindings/src/types/tari-indexer-client/InspectSubstateRequest.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SubstateId } from "../SubstateId"; -export type InspectSubstateRequest = { address: string; version: number | null }; +export type InspectSubstateRequest = { address: SubstateId; version: number | null }; diff --git a/bindings/src/types/wallet-daemon-client/SubstatesGetResponse.ts b/bindings/src/types/wallet-daemon-client/SubstatesGetResponse.ts index d6fd98eea0..ac6ced3cfa 100644 --- a/bindings/src/types/wallet-daemon-client/SubstatesGetResponse.ts +++ b/bindings/src/types/wallet-daemon-client/SubstatesGetResponse.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Substate } from "../Substate"; -import type { WalletSubstateRecord } from "./WalletSubstateRecord"; +import type { WalletSubstateInfo } from "./WalletSubstateInfo"; -export type SubstatesGetResponse = { record: WalletSubstateRecord | null; substate: Substate | null }; +export type SubstatesGetResponse = { local_record: WalletSubstateInfo | null; substate_from_remote: Substate | null }; diff --git a/bindings/src/types/wallet-daemon-client/SubstatesListResponse.ts b/bindings/src/types/wallet-daemon-client/SubstatesListResponse.ts index f140ce0b97..01d136f831 100644 --- a/bindings/src/types/wallet-daemon-client/SubstatesListResponse.ts +++ b/bindings/src/types/wallet-daemon-client/SubstatesListResponse.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WalletSubstateRecord } from "./WalletSubstateRecord"; +import type { WalletSubstateInfo } from "./WalletSubstateInfo"; -export type SubstatesListResponse = { substates: Array }; +export type SubstatesListResponse = { substates: Array }; diff --git a/bindings/src/types/wallet-daemon-client/TransferNftRequest.ts b/bindings/src/types/wallet-daemon-client/TransferNftRequest.ts index 32186ed6ba..e213f5a5a4 100644 --- a/bindings/src/types/wallet-daemon-client/TransferNftRequest.ts +++ b/bindings/src/types/wallet-daemon-client/TransferNftRequest.ts @@ -1,5 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { NonFungibleId } from "../NonFungibleId"; +import type { OotleAddress } from "../OotleAddress"; import type { ResourceAddress } from "../ResourceAddress"; import type { ComponentAddressOrName } from "./ComponentAddressOrName"; @@ -8,7 +9,7 @@ export type TransferNftRequest = { nfts: Array; fee_payer_account: ComponentAddressOrName; source_account: ComponentAddressOrName; - target_account_public_key: string; + target_account_address: OotleAddress; max_fee: number; dry_run: boolean; }; diff --git a/bindings/src/types/wallet-daemon-client/WalletSubstateRecord.ts b/bindings/src/types/wallet-daemon-client/WalletSubstateInfo.ts similarity index 89% rename from bindings/src/types/wallet-daemon-client/WalletSubstateRecord.ts rename to bindings/src/types/wallet-daemon-client/WalletSubstateInfo.ts index 0f0b901985..3a6406f57c 100644 --- a/bindings/src/types/wallet-daemon-client/WalletSubstateRecord.ts +++ b/bindings/src/types/wallet-daemon-client/WalletSubstateInfo.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { SubstateId } from "../SubstateId"; -export type WalletSubstateRecord = { +export type WalletSubstateInfo = { substate_id: SubstateId; parent_id: SubstateId | null; module_name: string | null; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index 056670345c..c8b30ca51f 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -1,7 +1,6 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -export * from "./types/wallet-daemon-client/WalletSubstateRecord"; export * from "./types/wallet-daemon-client/ProofsFinalizeResponse"; export * from "./types/wallet-daemon-client/WalletGetInfoResponse"; export * from "./types/wallet-daemon-client/CallInstructionRequest"; @@ -16,6 +15,7 @@ export * from "./types/wallet-daemon-client/AuthGetAllJwtRequest"; export * from "./types/wallet-daemon-client/KeyBranch"; export * from "./types/wallet-daemon-client/PublishTemplateResponse"; export * from "./types/wallet-daemon-client/AccountsAssociateStealthResourceResponse"; +export * from "./types/wallet-daemon-client/WalletSubstateInfo"; export * from "./types/wallet-daemon-client/WebauthnAlreadyRegisteredResponse"; export * from "./types/wallet-daemon-client/AccountGetResponse"; export * from "./types/wallet-daemon-client/SettingsSetRequest"; diff --git a/clients/tari_indexer_client/Cargo.toml b/clients/tari_indexer_client/Cargo.toml index 95d17fed6c..53e900b446 100644 --- a/clients/tari_indexer_client/Cargo.toml +++ b/clients/tari_indexer_client/Cargo.toml @@ -19,6 +19,7 @@ tari_consensus_types = { workspace = true } tari_ootle_wallet_sdk = { workspace = true } anyhow = { workspace = true, optional = true } +bounded-vec = { workspace = true } reqwest = { workspace = true, features = ["json"], optional = true } multiaddr = { workspace = true } serde = { workspace = true, default-features = true } diff --git a/clients/tari_indexer_client/src/json_rpc_client.rs b/clients/tari_indexer_client/src/json_rpc_client.rs index 1d7d8ade7b..efc5f74b81 100644 --- a/clients/tari_indexer_client/src/json_rpc_client.rs +++ b/clients/tari_indexer_client/src/json_rpc_client.rs @@ -35,6 +35,8 @@ use crate::{ GetNonFungiblesResponse, GetSubstateRequest, GetSubstateResponse, + GetSubstatesRequest, + GetSubstatesResponse, GetTemplateDefinitionRequest, GetTemplateDefinitionResponse, GetTransactionResultRequest, @@ -90,6 +92,20 @@ impl IndexerJsonRpcClient { self.send_request("get_substate", req).await } + pub async fn get_substates( + &mut self, + req: GetSubstatesRequest, + ) -> Result { + self.send_request("get_substates", req).await + } + + pub async fn fetch_substates( + &mut self, + req: GetSubstatesRequest, + ) -> Result { + self.send_request("fetch_substates", req).await + } + pub async fn list_substates( &mut self, req: ListSubstatesRequest, diff --git a/clients/tari_indexer_client/src/types.rs b/clients/tari_indexer_client/src/types.rs index 8834989b63..d80a5bbb12 100644 --- a/clients/tari_indexer_client/src/types.rs +++ b/clients/tari_indexer_client/src/types.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, time::Duration}; +use bounded_vec::BoundedVec; use multiaddr::Multiaddr; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, Seq}; @@ -10,7 +11,7 @@ use tari_common_types::types::FixedHash; use tari_consensus_types::Decision; use tari_engine_types::{ commit_result::ExecuteResult, - substate::{SubstateId, SubstateValue}, + substate::{Substate, SubstateId, SubstateValue}, template_lib_models::{NonFungibleAddress, ResourceAddress}, Utxo, UtxoId, @@ -77,10 +78,23 @@ pub struct GetSubstateResponse { pub substate: SubstateValue, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] +pub struct GetSubstatesRequest { + // Note that we may permit less than 50 in the handler, but this is the max we'll deserialize for DoS mitigation + #[cfg_attr(feature = "ts", ts(as = "Vec"))] + pub requests: BoundedVec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] +pub struct GetSubstatesResponse { + pub substates: HashMap, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] pub struct InspectSubstateRequest { - #[cfg_attr(feature = "ts", ts(type = "string"))] pub address: SubstateId, pub version: Option, } diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index f6a0fd505a..0cf3bdae68 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -860,7 +860,7 @@ pub struct SubstatesListRequest { #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct SubstatesListResponse { - pub substates: Vec, + pub substates: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -873,13 +873,14 @@ pub struct SubstatesGetRequest { #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct SubstatesGetResponse { - pub record: Option, - pub substate: Option, + // NOTE either of these can be None, but never both (instead, NotFound error) + pub local_record: Option, + pub substate_from_remote: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] -pub struct WalletSubstateRecord { +pub struct WalletSubstateInfo { pub substate_id: SubstateId, pub parent_id: Option, pub module_name: Option, @@ -1049,8 +1050,7 @@ pub struct TransferNftRequest { pub fee_payer_account: ComponentAddressOrName, #[serde(deserialize_with = "string_or_struct")] pub source_account: ComponentAddressOrName, - #[cfg_attr(feature = "ts", ts(type = "string"))] - pub target_account_public_key: RistrettoPublicKeyBytes, + pub target_account_address: OotleAddress, #[cfg_attr(feature = "ts", ts(type = "number"))] pub max_fee: u64, pub dry_run: bool, diff --git a/crates/engine_types/src/substate.rs b/crates/engine_types/src/substate.rs index 8849eecfd4..c17e4653a4 100644 --- a/crates/engine_types/src/substate.rs +++ b/crates/engine_types/src/substate.rs @@ -112,7 +112,7 @@ pub fn hash_substate(substate: &SubstateValue, version: u32) -> FixedHash { // BorshDeserialize is implemented for this struct because we de/encode keys in the database using this format /// Base object address, version tuples #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, BorshSerialize, BorshDeserialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, type = "string"))] pub enum SubstateId { Component(ComponentAddress), Resource(ResourceAddress), diff --git a/crates/wallet/sdk/src/network.rs b/crates/wallet/sdk/src/network.rs index 40ba86e34f..f67fd15060 100644 --- a/crates/wallet/sdk/src/network.rs +++ b/crates/wallet/sdk/src/network.rs @@ -7,11 +7,11 @@ use serde::{Deserialize, Serialize}; use tari_consensus_types::Decision; use tari_engine_types::{ commit_result::ExecuteResult, - substate::{SubstateId, SubstateValue}, + substate::{Substate, SubstateId, SubstateValue}, Utxo, UtxoId, }; -use tari_ootle_common_types::{shard::Shard, substate_type::SubstateType, StateVersion}; +use tari_ootle_common_types::{shard::Shard, StateVersion}; use tari_template_abi::TemplateDef; use tari_template_lib::{ models::ResourceAddress, @@ -33,13 +33,10 @@ pub trait WalletNetworkInterface { local_search_only: bool, ) -> impl Future> + Send; - fn list_substates( + fn get_substates( &self, - filter_by_template: Option, - filter_by_type: Option, - limit: Option, - offset: Option, - ) -> impl Future> + Send; + substate_ids: Vec, + ) -> impl Future, Self::Error>> + Send; fn submit_transaction( &self, @@ -110,20 +107,6 @@ pub struct SubstateQueryResult { pub substate: SubstateValue, } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct SubstateListResult { - pub substates: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct SubstateListItem { - pub substate_id: SubstateId, - pub module_name: Option, - pub version: u32, - pub template_address: Option, - pub timestamp: PrimitiveDateTime, -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TransactionQueryResult { pub result: TransactionFinalizedResult, diff --git a/crates/wallet/sdk/tests/support/harness.rs b/crates/wallet/sdk/tests/support/harness.rs index 33cd1d5e18..b62ca1d811 100644 --- a/crates/wallet/sdk/tests/support/harness.rs +++ b/crates/wallet/sdk/tests/support/harness.rs @@ -1,10 +1,16 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{collections::HashMap, convert::Infallible, str::FromStr}; +use std::{collections::HashMap, convert::Infallible, future::Future, str::FromStr}; use tari_crypto::tari_utilities::SafePassword; -use tari_engine_types::{crypto::commit_amount_checked, substate::SubstateId, ToByteType, Utxo, UtxoId}; +use tari_engine_types::{ + crypto::commit_amount_checked, + substate::{Substate, SubstateId}, + ToByteType, + Utxo, + UtxoId, +}; use tari_ootle_common_types::{optional::Optional, shard::Shard, Network, StateVersion}; use tari_ootle_wallet_sdk::{ models::{ConfidentialOutputModel, OutputStatus, UtxoUpdateSet, WalletLockId}, @@ -137,13 +143,7 @@ impl WalletNetworkInterface for PanicNetworkInterface { panic!("PanicNetworkInterface called") } - async fn list_substates( - &self, - _filter_by_template: Option, - _filter_by_type: Option, - _limit: Option, - _offset: Option, - ) -> Result { + async fn get_substates(&self, _: Vec) -> Result, Self::Error> { panic!("PanicNetworkInterface called") } diff --git a/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs b/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs index af379915ab..f497e487c0 100644 --- a/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs +++ b/crates/wallet/sdk_services/src/indexer_jrpc_impl.rs @@ -7,27 +7,29 @@ use std::{ }; use reqwest::{IntoUrl, Url}; -use tari_engine_types::{substate::SubstateId, Utxo, UtxoId}; +use tari_engine_types::{ + substate::{Substate, SubstateId}, + Utxo, + UtxoId, +}; use tari_indexer_client::{ error::IndexerClientError, json_rpc_client::IndexerJsonRpcClient, types::{ GetSubstateRequest, + GetSubstatesRequest, GetTransactionResultRequest, GetUnspentUtxosRequest, GetUtxoUpdatesRequest, IndexerTransactionFinalizedResult, - ListSubstatesRequest, SubmitTransactionRequest, }, }; -use tari_ootle_common_types::{optional::IsNotFoundError, shard::Shard, substate_type::SubstateType, StateVersion}; +use tari_ootle_common_types::{optional::IsNotFoundError, shard::Shard, StateVersion}; use tari_ootle_wallet_sdk::{ models::UtxoUpdateSet, network::{ StatusResponseError, - SubstateListItem, - SubstateListResult, SubstateQueryResult, TransactionFinalizedResult, TransactionQueryResult, @@ -100,34 +102,20 @@ impl WalletNetworkInterface for IndexerJsonRpcNetworkInterface { }) } - async fn list_substates( - &self, - filter_by_template: Option, - filter_by_type: Option, - limit: Option, - offset: Option, - ) -> Result { + async fn get_substates(&self, substate_ids: Vec) -> Result, Self::Error> { let mut client = self.get_client()?; - let result = client - .list_substates(ListSubstatesRequest { - filter_by_template, - filter_by_type, - limit, - offset, + let resp = client + .get_substates(GetSubstatesRequest { + requests: substate_ids.try_into().map_err(|_| { + IndexerJrpcError::IndexerClientError(IndexerClientError::RequestFailedWithStatus { + code: INVALID_REQUEST_CODE, + message: "Too many substate IDs requested".to_string(), + }) + })?, }) .await?; - let substates = result - .substates - .into_iter() - .map(|s| SubstateListItem { - substate_id: s.substate_id, - module_name: s.module_name, - version: s.version, - template_address: s.template_address, - timestamp: s.timestamp, - }) - .collect(); - Ok(SubstateListResult { substates }) + + Ok(resp.substates) } async fn submit_transaction(&self, transaction: Transaction) -> Result {