Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions applications/tari_indexer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ tari_template_lib = { workspace = true }
tari_template_manager = { workspace = true }
tari_transaction = { workspace = true }
tari_validator_node_rpc = { workspace = true }
tari_ootle_wallet_sdk = { workspace = true }
Comment thread
sdbondi marked this conversation as resolved.

anyhow = { workspace = true }
axum = { workspace = true }
async-graphql = { workspace = true }
async-graphql-axum = { workspace = true }
axum-jrpc = { workspace = true, features = ["anyhow_error"] }
bincode = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive", "env"] }
config = { workspace = true }
diesel = { workspace = true, default-features = false, features = [
Expand Down
6 changes: 3 additions & 3 deletions applications/tari_indexer/log4rs_sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ appenders:
count: 5
pattern: "{{log_dir}}/log/indexer/network.{}.log"
encoder:
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [Thread:{I}] {l:5} {m}// {f}:{L}{n}"
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}// {f}:{L}{n}"

# An appender named "ootle" that writes to a file with a custom pattern encoder
ootle:
Expand All @@ -53,7 +53,7 @@ appenders:
count: 5
pattern: "{{log_dir}}/log/indexer/ootle.{}.log"
encoder:
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}"
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}"

# An appender named "ootle" that writes to a file with a custom pattern encoder
json_rpc:
Expand All @@ -70,7 +70,7 @@ appenders:
count: 5
pattern: "{{log_dir}}/log/indexer/json_rpc.{}.log"
encoder:
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [{X(node-public-key)},{X(node-id)}] {l:5} {m} // {f}:{L}{n}"
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m} // {f}:{L}{n}"

# An appender named "libp2p" that writes to a file with a custom pattern encoder
libp2p:
Expand Down
5 changes: 4 additions & 1 deletion applications/tari_indexer/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ use crate::{
network_state_sync::NetworkWideStateSyncConfig,
storage_sqlite::{
models::Key,
store_factory::{IndexerStore, IndexerStoreReadTransaction, IndexerStoreWriteTransaction, SqliteIndexerStore},
IndexerStore,
IndexerStoreReadTransaction,
IndexerStoreWriteTransaction,
SqliteIndexerStore,
},
ApplicationConfig,
IndexerEpochManagerSpec,
Expand Down
2 changes: 1 addition & 1 deletion applications/tari_indexer/src/event_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use log::*;
use tari_engine_types::{events::Event, substate::SubstateId};
use tari_template_lib::{models::Metadata, types::Hash};

use crate::storage_sqlite::store_factory::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore};
use crate::storage_sqlite::{IndexerStore, IndexerStoreReadTransaction, SqliteIndexerStore};

const LOG_TARGET: &str = "tari::indexer::event_manager";

Expand Down
92 changes: 67 additions & 25 deletions applications/tari_indexer/src/json_rpc/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ use tari_indexer_client::types::{
GetTemplateDefinitionResponse,
GetTransactionResultRequest,
GetTransactionResultResponse,
GetUnspentUtxosRequest,
GetUnspentUtxosResponse,
GetUtxoUpdatesRequest,
GetUtxoUpdatesResponse,
IndexerReadyResponse,
Expand All @@ -66,7 +68,6 @@ use tari_indexer_client::types::{
ListSubstatesResponse,
ListTemplatesRequest,
ListTemplatesResponse,
NonFungibleSubstate,
SubmitTransactionRequest,
SubmitTransactionResponse,
TemplateMetadata,
Expand All @@ -86,6 +87,7 @@ use tari_ootle_storage::{
time::{PrimitiveDateTime, UtcDateTime},
};
use tari_ootle_storage_sqlite::global::SqliteGlobalDbAdapter;
use tari_ootle_wallet_sdk::models::{UtxoStateUpdateSet, UtxoUpdateSet};
use tari_template_manager::{
implementation::TemplateManager,
interface::{TemplateExecutable, TemplateManagerError},
Expand All @@ -97,7 +99,7 @@ use crate::{
dry_run::processor::DryRunTransactionProcessor,
json_rpc::error::internal_error,
network_client::NetworkClientError,
storage_sqlite::store_factory::SqliteIndexerStore,
storage_sqlite::SqliteIndexerStore,
substate_manager::SubstateManager,
transaction_manager::{error::TransactionManagerError, TransactionManager},
};
Expand Down Expand Up @@ -433,19 +435,12 @@ impl JsonRpcHandlers {
)
})?;

let res = self
let non_fungibles = self
.substate_manager
.get_non_fungibles_by_resource_address(request.address, limit, offset)
.map_err(|e| Self::internal_error(answer_id, format!("Error getting non fungibles: {}", e)))?;
Ok(JsonRpcResponse::success(answer_id, GetNonFungiblesResponse {
non_fungibles: res
.into_iter()
.map(|v| NonFungibleSubstate {
address: v.address,
version: v.version,
substate: v.substate,
})
.collect(),
non_fungibles,
}))
}

Expand Down Expand Up @@ -496,18 +491,12 @@ impl JsonRpcHandlers {
));
}

let mut utxo_updates = Vec::new();
let mut utxo_updates = HashMap::new();
let mut per_shard_high_watermark = Vec::with_capacity(req.shard_state_versions.len());
for (shard, state_version) in req.shard_state_versions {
let (max_version, updates) = self
let (max_state_version, updates) = self
.substate_manager
.get_utxo_updates(
req.resource_address,
shard,
state_version,
&req.filter_tag_bytes,
req.per_shard_limit,
)
.get_utxo_updates(req.resource_address, shard, state_version, req.per_shard_limit)
.map_err(|e| {
Self::internal_error(
answer_id,
Expand All @@ -517,19 +506,72 @@ impl JsonRpcHandlers {
),
)
})?;
if max_version.as_u64() > 0 {

// TODO: this is on the hot path, figure out a better way to let the client know the max shard versions
// without sending it each time
let current_tip_version = self
.substate_manager
.get_max_state_version(&req.resource_address, shard)
.map_err(|e| {
Self::internal_error(
answer_id,
format!(
"Error getting max state version for resource_address {}, shard {}: {}",
req.resource_address, shard, e
),
)
})?;
if current_tip_version.as_u64() > 0 {
// Save a little over the wire initially by not sending 0 watermarks
per_shard_high_watermark.push((shard, max_version));
per_shard_high_watermark.push((shard, current_tip_version));
}
if !updates.is_empty() {
utxo_updates.insert(shard, UtxoStateUpdateSet {
updates,
max_state_version,
});
}
utxo_updates.extend(updates);
}

Ok(JsonRpcResponse::success(answer_id, GetUtxoUpdatesResponse {
utxo_updates,
per_shard_high_watermark,
updates: UtxoUpdateSet {
shard_updates: utxo_updates,
per_shard_high_watermark,
},
}))
}

pub async fn get_unspent_utxos(&self, value: JsonRpcExtractor) -> JrpcResult {
let answer_id = value.get_answer_id();
let req: GetUnspentUtxosRequest = value.parse_params()?;
if req.tag_and_nonce_pairs.len() > 1000 {
return Err(JsonRpcResponse::error(
answer_id,
JsonRpcError::new(
JsonRpcErrorReason::InvalidParams,
"cannot query more than 1000 UTXOs".to_string(),
Value::Null,
),
));
}
let utxos = self
.substate_manager
.get_unspent_utxos(&req.resource_address, &req.tag_and_nonce_pairs)
.map_err(|e| {
Self::internal_error(
answer_id,
format!(
"Error getting UTXOs for resource_address {}, with {} tag/nonce pair(s): {}",
req.resource_address,
req.tag_and_nonce_pairs.len(),
e
),
)
})?;

Ok(JsonRpcResponse::success(answer_id, GetUnspentUtxosResponse { utxos }))
}

Comment on lines +544 to +574

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

New get_unspent_utxos handler: validate shape, add dedup, and reconcile bindings.

  • Enforces ≤1000 items — good.
  • Consider deduplicating (tag, nonce) pairs to avoid redundant DB hits and reduce response size.
  • Ensure TS/Rust bindings use tag_and_nonce_pairs consistently (some bindings still reference commitments).
-        let req: GetUnspentUtxosRequest = value.parse_params()?;
+        let req: GetUnspentUtxosRequest = value.parse_params()?;
+        // Optional: dedupe to reduce load
+        // let mut pairs = req.tag_and_nonce_pairs.clone();
+        // pairs.sort_unstable();
+        // pairs.dedup();
+        // if pairs.len() != req.tag_and_nonce_pairs.len() { /* maybe log */ }

🏁 Script executed:

#!/bin/bash
# Check TS bindings for request/response shape parity
rg -nP --type=ts 'GetUnspentUtxos(Request|Response)' -C3
rg -nP --type=ts 'tag_and_nonce_pairs|commitments' -n -C2 bindings/

Length of output: 5054


Reconcile TS bindings with new request shape; optionally dedupe (tag,nonce) pairs

  • Optional: deduplicate (tag, nonce) pairs to avoid redundant DB hits — suggested diff below.
  • Critical: TypeScript bindings still use "commitments" — update bindings to use "tag_and_nonce_pairs" with the correct element type. See bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts (export in bindings/src/tari-indexer-client.ts).
-        let req: GetUnspentUtxosRequest = value.parse_params()?;
+        let req: GetUnspentUtxosRequest = value.parse_params()?;
+        // Optional: dedupe to reduce load
+        // let mut pairs = req.tag_and_nonce_pairs.clone();
+        // pairs.sort_unstable();
+        // pairs.dedup();
+        // if pairs.len() != req.tag_and_nonce_pairs.len() { /* maybe log */ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn get_unspent_utxos(&self, value: JsonRpcExtractor) -> JrpcResult {
let answer_id = value.get_answer_id();
let req: GetUnspentUtxosRequest = value.parse_params()?;
if req.tag_and_nonce_pairs.len() > 1000 {
return Err(JsonRpcResponse::error(
answer_id,
JsonRpcError::new(
JsonRpcErrorReason::InvalidParams,
"cannot query more than 1000 UTXOs".to_string(),
Value::Null,
),
));
}
let utxos = self
.substate_manager
.get_unspent_utxos(&req.resource_address, &req.tag_and_nonce_pairs)
.map_err(|e| {
Self::internal_error(
answer_id,
format!(
"Error getting UTXOs for resource_address {}, with {} tag/nonce pair(s): {}",
req.resource_address,
req.tag_and_nonce_pairs.len(),
e
),
)
})?;
Ok(JsonRpcResponse::success(answer_id, GetUnspentUtxosResponse { utxos }))
}
pub async fn get_unspent_utxos(&self, value: JsonRpcExtractor) -> JrpcResult {
let answer_id = value.get_answer_id();
let req: GetUnspentUtxosRequest = value.parse_params()?;
// Optional: dedupe to reduce load
// let mut pairs = req.tag_and_nonce_pairs.clone();
// pairs.sort_unstable();
// pairs.dedup();
// if pairs.len() != req.tag_and_nonce_pairs.len() { /* maybe log */ }
if req.tag_and_nonce_pairs.len() > 1000 {
return Err(JsonRpcResponse::error(
answer_id,
JsonRpcError::new(
JsonRpcErrorReason::InvalidParams,
"cannot query more than 1000 UTXOs".to_string(),
Value::Null,
),
));
}
let utxos = self
.substate_manager
.get_unspent_utxos(&req.resource_address, &req.tag_and_nonce_pairs)
.map_err(|e| {
Self::internal_error(
answer_id,
format!(
"Error getting UTXOs for resource_address {}, with {} tag/nonce pair(s): {}",
req.resource_address,
req.tag_and_nonce_pairs.len(),
e
),
)
})?;
Ok(JsonRpcResponse::success(answer_id, GetUnspentUtxosResponse { utxos }))
}
🤖 Prompt for AI Agents
In applications/tari_indexer/src/json_rpc/handlers.rs around lines 544-574, the
review requests two items: (1) update the TypeScript bindings to replace the old
"commitments" field with "tag_and_nonce_pairs" using the correct element type
(change bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts and
re-export in bindings/src/tari-indexer-client.ts accordingly), and (2)
optionally deduplicate (tag, nonce) pairs before querying the DB to avoid
redundant lookups — implement a small dedupe step on req.tag_and_nonce_pairs
(preserve order if needed) and pass the deduplicated list to
substate_manager.get_unspent_utxos, keeping the existing error handling and
response unchanged.

pub async fn submit_transaction(&self, value: JsonRpcExtractor) -> JrpcResult {
let answer_id = value.get_answer_id();
let request: SubmitTransactionRequest = value.parse_params()?;
Expand Down
1 change: 1 addition & 0 deletions applications/tari_indexer/src/json_rpc/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async fn handler(Extension(handlers): Extension<Arc<JsonRpcHandlers>>, value: Js
"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,
"get_unspent_utxos" => handlers.get_unspent_utxos(value).await,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add discovery/permission entries for get_unspent_utxos.

Ensure rpc.discover output and any JrpcPermissions map include "get_unspent_utxos" so clients can introspect and authorize it.


🏁 Script executed:

#!/bin/bash
# Verify handler exists and is referenced by rpc.discover/permissions
rg -nP 'get_unspent_utxos' applications/tari_indexer/src/json_rpc/handlers.rs -n -C3
rg -nP 'rpc\\.discover|JrpcPermissions' applications/tari_indexer/src/json_rpc -n -C3

Length of output: 563


Add rpc.discover and JrpcPermissions entries for "get_unspent_utxos"

get_unspent_utxos handler exists (applications/tari_indexer/src/json_rpc/handlers.rs:544–560) and is registered in the server mapping (applications/tari_indexer/src/json_rpc/server.rs), but no rpc.discover output or JrpcPermissions entry advertising "get_unspent_utxos" was found under applications/tari_indexer/src/json_rpc — add the method to rpc.discover and the JrpcPermissions map so clients can introspect and authorize it.


// Transactions
"submit_transaction" => handlers.submit_transaction(value).await,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use crate::{
block_data::BlockData,
storage_sqlite::{
models::{NewScannedBlockId, NewSubstate},
store_factory::{IndexerStore, IndexerStoreReadTransaction, IndexerStoreWriteTransaction, SqliteIndexerStore},
IndexerStore,
IndexerStoreReadTransaction,
IndexerStoreWriteTransaction,
SqliteIndexerStore,
},
};

Expand Down
41 changes: 20 additions & 21 deletions applications/tari_indexer/src/network_state_sync/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ use tari_ootle_common_types::{
PeerAddress,
ShardGroup,
StateVersion,
UtxoSpent,
UtxoUnspent,
UtxoUpdate,
VotePower,
};
use tari_ootle_p2p::{proto::rpc, TariMessagingSpec};
Expand All @@ -43,14 +40,12 @@ use crate::{
sync_progress::SyncProgress,
},
storage_sqlite::{
models::Key,
store_factory::{
IndexerStore,
IndexerStoreReadTransaction,
IndexerStoreWriteTransaction,
SqliteIndexerStore,
SqliteStoreWriteTransaction,
},
models::{Key, UtxoSpent, UtxoUnspent, UtxoUpdateRecord},
IndexerStore,
IndexerStoreReadTransaction,
IndexerStoreWriteTransaction,
SqliteIndexerStore,
SqliteStoreWriteTransaction,
},
};

Expand Down Expand Up @@ -327,7 +322,7 @@ impl NetworkWideStateSync {
shard: Shard,
sync_plan_mut: &mut SyncPlan,
update_buf: &mut Vec<(Epoch, SubstateUpdateProof)>,
utxos_buf: &mut Vec<UtxoUpdate>,
utxos_buf: &mut Vec<UtxoUpdateRecord>,
transactions_buf: &mut Vec<TransactionReceipt>,
shard_group: ShardGroup,
session: &mut ValidatorRpcSession,
Expand Down Expand Up @@ -452,20 +447,24 @@ fn extend_bufs_from_substate_update(
msg_epoch: Epoch,
update_buf: &mut Vec<(Epoch, SubstateUpdateProof)>,
templates_buf: &mut Vec<TemplateChange>,
utxos_buf: &mut Vec<UtxoUpdate>,
utxos_buf: &mut Vec<UtxoUpdateRecord>,
transactions_buf: &mut Vec<TransactionReceipt>,
) -> Result<(), NetworkStateSyncError> {
match &update {
SubstateUpdateProof::Create(create) => match create.substate.value().value() {
Some(SubstateValue::Utxo(utxo)) => {
if let Some(address) = create.substate.substate_id().as_utxo_address() {
utxos_buf.push(UtxoUpdate::Unspent(UtxoUnspent {
address,
version: update.version(),
shard,
state_version,
utxo: utxo.clone(),
}));
let is_frozen = utxo.is_frozen();
if let Some(ref output) = utxo.output {
utxos_buf.push(UtxoUpdateRecord::Unspent(UtxoUnspent {
address,
version: update.version(),
shard,
state_version,
utxo_output: output.clone(),
is_frozen,
}));
}
} else {
warn!(target: LOG_TARGET, "⚠️ NEVER HAPPEN: Received UTXO substate with invalid address: {}", create.substate.substate_id());
};
Expand Down Expand Up @@ -505,7 +504,7 @@ fn extend_bufs_from_substate_update(
});
},
SubstateId::Utxo(address) => {
utxos_buf.push(UtxoUpdate::Spent(UtxoSpent {
utxos_buf.push(UtxoUpdateRecord::Spent(UtxoSpent {
address: address.clone(),
shard,
version: update.version(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,20 @@ CREATE UNIQUE INDEX epoch_checkpoints_uniq_epoch_shard_group ON epoch_checkpoint
create table utxos
(
id integer not NULL primary key AUTOINCREMENT,
address text not NULL,
commitment text not NULL,
public_nonce text not NULL,
version int not NULL,
resource_address text not NULL,
shard int not NULL,
state_version bigint not NULL,
output text NULL,
utxo_tag_byte int NULL,
output blob NULL,
utxo_tag int not NULL,
is_spent boolean not NULL,
is_burnt boolean not NULL,
is_frozen boolean not NULL,
created_at timestamp not null default current_timestamp
);
Comment thread
sdbondi marked this conversation as resolved.

CREATE INDEX utxos_shard_tag_resource_state_version_idx
ON utxos (shard, utxo_tag_byte, resource_address, state_version);
CREATE INDEX utxos_resource_state_version_shard_idx ON utxos (resource_address, state_version, shard);
CREATE UNIQUE INDEX utxos_resource_public_nonce_utxo_tag_uniq_partial ON utxos (resource_address, public_nonce, utxo_tag) WHERE is_spent = false;

Loading
Loading