From 7b0d834c9ed31ef05d045cdfdc65ba28ee478f88 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 6 Nov 2025 15:34:54 +0400 Subject: [PATCH 1/3] fix!: change hrp prefix on ootle address to otl_ --- .../AssetVault/NFTs/components/SendNft.tsx | 17 +------ .../routes/AssetVault/NFTs/steps/FormStep.tsx | 4 +- bindings/package.json | 2 +- bindings/src/helpers/ootleAddress.ts | 12 ++--- bindings/src/types/Instruction.ts | 9 ++-- bindings/src/types/InstructionArg.ts | 3 +- bindings/test/ootleAddress.test.ts | 2 +- crates/engine/src/transaction/processor.rs | 2 +- crates/ootle_address/src/hrp.rs | 12 ++--- crates/p2p/src/conversions/transaction.rs | 11 +++-- crates/template_lib_types/src/max_string.rs | 45 ++++++++++++++++++- crates/transaction/src/args.rs | 2 +- crates/transaction/src/v1/instruction.rs | 9 ++-- crates/transaction_manifest/src/error.rs | 2 + crates/transaction_manifest/src/generator.rs | 4 +- 15 files changed, 87 insertions(+), 49 deletions(-) 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 7179b4f9b1..56380e58c2 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 @@ -26,7 +26,7 @@ import Dialog from "@mui/material/Dialog"; import DialogContent from "@mui/material/DialogContent"; import { SelectChangeEvent } from "@mui/material/Select/Select"; import useAccountStore from "@store/accountStore"; -import type { +import { Account, ComponentAddressOrName, ResourceAddress, @@ -368,21 +368,6 @@ export function TransferNftDialog(props: TransferNftDialogProps) { } }; - // Handle target account changes for auto fee estimation - useEffect(() => { - const targetAccount = transferFormState.targetAccountAddress; - if (targetAccount.trim() && targetAccount.startsWith("xtr_")) { - // Small delay to let state update, then estimate fee - const timeoutId = setTimeout(() => { - estimateFeeWithTargetAccount(targetAccount).catch(() => { - // Fee estimation failed, but don't block the user - }); - }, 500); - - return () => clearTimeout(timeoutId); - } - }, [transferFormState.targetAccountAddress]); - useEffect(() => { if (loadedNfts !== undefined) { setAvailableNfts(loadedNfts); 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 3c501325e9..3031c107b1 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 @@ -153,8 +153,8 @@ export default function FormStep({ error={transferFormState.targetAccountAddress !== "" && !isAddressValid} helperText={ transferFormState.targetAccountAddress !== "" && !isAddressValid - ? "Invalid address format. Expected format: xtr_loc_..." - : "Enter the recipient's address (e.g., xtr_loc_1enpsfkx...)" + ? "Invalid address format. Expected format: otl_loc_..." + : "Enter the recipient's address (e.g., otl_loc_1enpsfkx...)" } /> diff --git a/bindings/package.json b/bindings/package.json index 65d19a0f82..9628a2fa0e 100644 --- a/bindings/package.json +++ b/bindings/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/typescript-bindings", - "version": "1.20.1", + "version": "1.21.0", "description": "TypeScript types synchronized to the Tari Ootle Rust codebase", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/bindings/src/helpers/ootleAddress.ts b/bindings/src/helpers/ootleAddress.ts index 3b79f5556e..59ffd1ac99 100644 --- a/bindings/src/helpers/ootleAddress.ts +++ b/bindings/src/helpers/ootleAddress.ts @@ -17,12 +17,12 @@ export type DecodedOotleAddress = { }; export enum NetworkHrp { - MainNet = "xtr_", - StageNet = "xtr_stg_", - NextNet = "xtr_nxt_", - LocalNet = "xtr_loc_", - Igor = "xtr_igr_", - Esmeralda = "xtr_esm_", + MainNet = "otl_", + StageNet = "otl_stg_", + NextNet = "otl_nxt_", + LocalNet = "otl_loc_", + Igor = "otl_igr_", + Esmeralda = "otl_esm_", } export function decodeOotleAddressOrNull(address: OotleAddress): DecodedOotleAddress | null { diff --git a/bindings/src/types/Instruction.ts b/bindings/src/types/Instruction.ts index 6ac4ff3457..d261ed4446 100644 --- a/bindings/src/types/Instruction.ts +++ b/bindings/src/types/Instruction.ts @@ -4,29 +4,32 @@ import type { Amount } from "./Amount"; import type { ClaimBurnOutputData } from "./ClaimBurnOutputData"; import type { ComponentAccessRules } from "./ComponentAccessRules"; import type { ComponentCall } from "./ComponentCall"; +import type { Hash } from "./Hash"; import type { LogLevel } from "./LogLevel"; import type { MinotariBurnClaimProof } from "./MinotariBurnClaimProof"; import type { OwnerRule } from "./OwnerRule"; import type { ResourceAddress } from "./ResourceAddress"; import type { ResourceAddressRef } from "./ResourceAddressRef"; +import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes"; import type { StealthTransferStatement } from "./StealthTransferStatement"; +import type { ValidatorFeePoolAddress } from "./ValidatorFeePoolAddress"; import type { WorkspaceOffsetId } from "./WorkspaceOffsetId"; export type Instruction = | { CreateAccount: { - owner_public_key: string; + owner_public_key: RistrettoPublicKeyBytes; owner_rule: OwnerRule | null; access_rules: ComponentAccessRules | null; workspace_id: WorkspaceOffsetId | null; }; } - | { CallFunction: { address: string; function: string; args: Array } } + | { CallFunction: { address: Hash; function: string; args: Array } } | { CallMethod: { call: ComponentCall; method: string; args: Array } } | { PutLastInstructionOutputOnWorkspace: { key: number } } | { EmitLog: { level: LogLevel; message: string } } | { ClaimBurn: { claim: MinotariBurnClaimProof; output_data: ClaimBurnOutputData } } - | { ClaimValidatorFees: { address: string } } + | { ClaimValidatorFees: { address: ValidatorFeePoolAddress } } | "DropAllProofsInWorkspace" | { AssertBucketContains: { key: WorkspaceOffsetId; resource_address: ResourceAddress; min_amount: Amount } } | { TakeFromBucket: { input_bucket: WorkspaceOffsetId; amount: Amount; output_bucket: number } } diff --git a/bindings/src/types/InstructionArg.ts b/bindings/src/types/InstructionArg.ts index 974c119762..2f90adf779 100644 --- a/bindings/src/types/InstructionArg.ts +++ b/bindings/src/types/InstructionArg.ts @@ -1,7 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WorkspaceOffsetId } from "./WorkspaceOffsetId"; /** * Represents an argument that can be passed to a transaction instruction. Either a literal value or a reference to a * item on the runtime's workspace. */ -export type InstructionArg = { Workspace: number } | { Literal: string }; +export type InstructionArg = { Workspace: WorkspaceOffsetId } | { Literal: string }; diff --git a/bindings/test/ootleAddress.test.ts b/bindings/test/ootleAddress.test.ts index c61631e729..1d2da0e945 100644 --- a/bindings/test/ootleAddress.test.ts +++ b/bindings/test/ootleAddress.test.ts @@ -74,7 +74,7 @@ describe("OotleAddress de/encoding", () => { it("decodes a known address correctly", () => { // This address was generated by the rust version of OotleAddress let s = - "xtr_loc_1ffs0tmejptxzujfnp4f6r44md2cnr9p9d9ayphcmkjxfmmapf3g9cghy8dlpnlwtc04t2jyk6kaz6a9eh2zww9flsl73ydue3qmwyuq5djpu2"; + "otl_loc_1ffs0tmejptxzujfnp4f6r44md2cnr9p9d9ayphcmkjxfmmapf3g9cghy8dlpnlwtc04t2jyk6kaz6a9eh2zww9flsl73ydue3qmwyuq5djpu2"; let r = decodeOotleAddress(s); expect(r.network).toEqual("localnet"); expect(r.accountPublicKey).toEqual("4a60f5ef320acc2e49330d53a1d6bb6ab1319425697a40df1bb48c9defa14c50"); diff --git a/crates/engine/src/transaction/processor.rs b/crates/engine/src/transaction/processor.rs index 76120a0f33..4d6198241a 100644 --- a/crates/engine/src/transaction/processor.rs +++ b/crates/engine/src/transaction/processor.rs @@ -294,7 +294,7 @@ impl + 'static> T Ok(InstructionResult::empty()) }, Instruction::EmitLog { level, message } => { - runtime.interface().emit_log(level, message)?; + runtime.interface().emit_log(level, message.into_string())?; Ok(InstructionResult::empty()) }, Instruction::ClaimBurn { claim, output_data } => { diff --git a/crates/ootle_address/src/hrp.rs b/crates/ootle_address/src/hrp.rs index 934c48403c..1508aafd0d 100644 --- a/crates/ootle_address/src/hrp.rs +++ b/crates/ootle_address/src/hrp.rs @@ -4,12 +4,12 @@ use bech32::Hrp; use tari_ootle_common_types::Network; -pub(crate) const HRP_MAINNET: Hrp = Hrp::parse_unchecked("xtr_"); -pub(crate) const HRP_LOCALNET: Hrp = Hrp::parse_unchecked("xtr_loc_"); -pub(crate) const HRP_ESME: Hrp = Hrp::parse_unchecked("xtr_esm_"); -pub(crate) const HRP_IGOR: Hrp = Hrp::parse_unchecked("xtr_igr_"); -pub(crate) const HRP_NEXTNET: Hrp = Hrp::parse_unchecked("xtr_nxt_"); -pub(crate) const HRP_STAGENET: Hrp = Hrp::parse_unchecked("xtr_stg_"); +pub(crate) const HRP_MAINNET: Hrp = Hrp::parse_unchecked("otl_"); +pub(crate) const HRP_LOCALNET: Hrp = Hrp::parse_unchecked("otl_loc_"); +pub(crate) const HRP_ESME: Hrp = Hrp::parse_unchecked("otl_esm_"); +pub(crate) const HRP_IGOR: Hrp = Hrp::parse_unchecked("otl_igr_"); +pub(crate) const HRP_NEXTNET: Hrp = Hrp::parse_unchecked("otl_nxt_"); +pub(crate) const HRP_STAGENET: Hrp = Hrp::parse_unchecked("otl_stg_"); pub fn hrp_from_network(network: Network) -> Hrp { match network { diff --git a/crates/p2p/src/conversions/transaction.rs b/crates/p2p/src/conversions/transaction.rs index 4d596bb067..c6cfb92235 100644 --- a/crates/p2p/src/conversions/transaction.rs +++ b/crates/p2p/src/conversions/transaction.rs @@ -163,9 +163,12 @@ impl TryFrom for Instruction { Some(PutLastInstructionOutputOnWorkspace(id)) => Ok(Instruction::PutLastInstructionOutputOnWorkspace { key: u16::try_from(id).context("workspace_put_key overflowed")?, }), - Some(EmitLog(emit_lgo)) => Ok(Instruction::EmitLog { - level: emit_lgo.log_level.parse()?, - message: emit_lgo.log_message, + Some(EmitLog(emit_log)) => Ok(Instruction::EmitLog { + level: emit_log.log_level.parse()?, + message: emit_log + .log_message + .try_into() + .map_err(|e| anyhow!("emit_log_message: {}", e))?, }), Some(ClaimBurn(claim_burn)) => Ok(Instruction::ClaimBurn { claim: Box::new(MinotariBurnClaimProof { @@ -342,7 +345,7 @@ impl From for proto::transaction::Instruction { instruction: Some(proto::transaction::instruction::Instruction::EmitLog( proto::transaction::EmitLog { log_level: level.to_string(), - log_message: message, + log_message: message.into_string(), }, )), }, diff --git a/crates/template_lib_types/src/max_string.rs b/crates/template_lib_types/src/max_string.rs index 562bb8a544..c26fa8c1e6 100644 --- a/crates/template_lib_types/src/max_string.rs +++ b/crates/template_lib_types/src/max_string.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{ +use tari_template_abi::rust::{ fmt, fmt::Display, ops::{Deref, DerefMut}, @@ -65,12 +65,55 @@ impl<'de, const N: usize> serde::Deserialize<'de> for MaxString { } } +impl TryFrom for MaxString { + type Error = MaxStringError; + + fn try_from(value: String) -> Result { + MaxString::new_checked(value).ok_or(MaxStringError) + } +} + impl Display for MaxString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.s.as_ref().fmt(f) } } +pub struct MaxStringError; + +impl Display for MaxStringError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "string length exceeds maximum of {}", N) + } +} + +#[cfg(feature = "borsh")] +mod borsh_impl { + use borsh::io::{Error, ErrorKind, Read, Result, Write}; + + use super::*; + + impl borsh::BorshSerialize for MaxString { + fn serialize(&self, writer: &mut W) -> Result<()> { + self.s.serialize(writer) + } + } + + impl borsh::BorshDeserialize for MaxString { + fn deserialize_reader(reader: &mut R) -> Result { + let s = Box::::deserialize_reader(reader)?; + if s.len() <= N { + Ok(Self { s }) + } else { + Err(Error::new( + ErrorKind::InvalidData, + format!("string length exceeds maximum of {}: got {}", N, s.len()), + )) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/transaction/src/args.rs b/crates/transaction/src/args.rs index 3cf8713ac9..4e4271995e 100644 --- a/crates/transaction/src/args.rs +++ b/crates/transaction/src/args.rs @@ -62,7 +62,7 @@ impl fmt::Display for WorkspaceOffsetId { pub enum InstructionArg { /// The argument is in the transaction execution's workspace, which means it is the result of a previous /// instruction - Workspace(#[cfg_attr(feature = "ts", ts(type = "number"))] WorkspaceOffsetId), + Workspace(WorkspaceOffsetId), /// The argument is a value specified in the transaction Literal( #[serde( diff --git a/crates/transaction/src/v1/instruction.rs b/crates/transaction/src/v1/instruction.rs index b8dc14535f..1290f591f5 100644 --- a/crates/transaction/src/v1/instruction.rs +++ b/crates/transaction/src/v1/instruction.rs @@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter}; use serde::{Deserialize, Serialize}; use tari_engine_types::{ confidential::{ClaimBurnOutputData, MinotariBurnClaimProof}, + limits, ValidatorFeePoolAddress, }; use tari_template_lib::{ @@ -13,7 +14,7 @@ use tari_template_lib::{ auth::OwnerRule, models::{ResourceAddress, StealthTransferStatement}, prelude::{AccessRules, Amount}, - types::{crypto::RistrettoPublicKeyBytes, TemplateAddress}, + types::{crypto::RistrettoPublicKeyBytes, MaxString, TemplateAddress}, }; use crate::{ @@ -27,14 +28,12 @@ use crate::{ #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] pub enum Instruction { CreateAccount { - #[cfg_attr(feature = "ts", ts(type = "string"))] owner_public_key: RistrettoPublicKeyBytes, owner_rule: Option, access_rules: Option, workspace_id: Option, }, CallFunction { - #[cfg_attr(feature = "ts", ts(type = "string"))] address: TemplateAddress, function: String, #[serde(deserialize_with = "crate::special_json_arg_syntax::json_deserialize")] @@ -56,14 +55,14 @@ pub enum Instruction { }, EmitLog { level: LogLevel, - message: String, + #[cfg_attr(feature = "ts", ts(type = "string"))] + message: MaxString<{ limits::ENGINE_LIMITS.max_log_size_bytes }>, }, ClaimBurn { claim: Box, output_data: ClaimBurnOutputData, }, ClaimValidatorFees { - #[cfg_attr(feature = "ts", ts(type = "string"))] address: ValidatorFeePoolAddress, }, DropAllProofsInWorkspace, diff --git a/crates/transaction_manifest/src/error.rs b/crates/transaction_manifest/src/error.rs index 4b7958a06c..4a20872f75 100644 --- a/crates/transaction_manifest/src/error.rs +++ b/crates/transaction_manifest/src/error.rs @@ -40,4 +40,6 @@ pub enum ManifestError { InvalidVariableType(String), #[error("Template alias '{alias}' not defined")] TemplateAliasNotDefined { alias: String }, + #[error("Invalid instruction: {reason}")] + InvalidInstruction { reason: String }, } diff --git a/crates/transaction_manifest/src/generator.rs b/crates/transaction_manifest/src/generator.rs index 9988a494d7..8f17b2264e 100644 --- a/crates/transaction_manifest/src/generator.rs +++ b/crates/transaction_manifest/src/generator.rs @@ -140,7 +140,9 @@ impl ManifestInstructionGenerator { }, ManifestIntent::Log(log) => Ok(vec![Instruction::EmitLog { level: log.level, - message: log.message, + message: log.message.try_into().map_err(|e| ManifestError::InvalidInstruction { + reason: format!("Log message is too long: {}", e), + })?, }]), ManifestIntent::DropAllProofs => Ok(vec![Instruction::DropAllProofsInWorkspace]), } From 0a543946bd4ab93a64d904b83371cebd8f13a947 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 6 Nov 2025 15:45:17 +0400 Subject: [PATCH 2/3] limits to panic message size --- crates/engine/src/wasm/process.rs | 14 +++++++++++++- crates/engine_types/src/limits.rs | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/wasm/process.rs b/crates/engine/src/wasm/process.rs index 681ad1b11f..5177535d2b 100644 --- a/crates/engine/src/wasm/process.rs +++ b/crates/engine/src/wasm/process.rs @@ -320,7 +320,19 @@ impl Invokable for WasmProcess { if let Some(err) = self.env.take_last_engine_error() { return Err(WasmExecutionError::RuntimeError(err)); } - if let Some(message) = self.env.take_last_panic_message() { + if let Some(mut message) = self.env.take_last_panic_message() { + if message.len() > limits::ENGINE_LIMITS.max_panic_message_size { + message.truncate(limits::ENGINE_LIMITS.max_panic_message_size); + error!(target: LOG_TARGET, "Panic message size limit exceeded: for panic {}", message); + return Err(WasmExecutionError::Panic { + message: format!( + "Panic message size limit of {} bytes exceeded", + limits::ENGINE_LIMITS.max_panic_message_size + ), + runtime_error: err, + }); + } + return Err(WasmExecutionError::Panic { message, runtime_error: err, diff --git a/crates/engine_types/src/limits.rs b/crates/engine_types/src/limits.rs index 7f94e44bc1..8d0bf18427 100644 --- a/crates/engine_types/src/limits.rs +++ b/crates/engine_types/src/limits.rs @@ -27,6 +27,7 @@ pub struct EngineLimits { pub max_logs: usize, pub max_log_size_bytes: usize, pub max_events: usize, + pub max_panic_message_size: usize, } pub const ENGINE_LIMITS: EngineLimits = EngineLimits { @@ -37,6 +38,7 @@ pub const ENGINE_LIMITS: EngineLimits = EngineLimits { max_logs: 256, max_log_size_bytes: 32 * 1024, // 32 KiB max_events: 256, + max_panic_message_size: 32 * 1024, // 32 KiB }; pub const MAX_DIVISIBILITY: u8 = 18; From d8c0375741163a33597cf4352090391f52166309 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 7 Nov 2025 09:33:55 +0400 Subject: [PATCH 3/3] review comments --- crates/engine/src/wasm/process.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/wasm/process.rs b/crates/engine/src/wasm/process.rs index 5177535d2b..498cc54c6a 100644 --- a/crates/engine/src/wasm/process.rs +++ b/crates/engine/src/wasm/process.rs @@ -322,7 +322,13 @@ impl Invokable for WasmProcess { } if let Some(mut message) = self.env.take_last_panic_message() { if message.len() > limits::ENGINE_LIMITS.max_panic_message_size { - message.truncate(limits::ENGINE_LIMITS.max_panic_message_size); + let limit = limits::ENGINE_LIMITS.max_panic_message_size; + let mut end = limit; + // Ensure we truncate at a char boundary (to avoid a panic when calling truncate) + while end > 0 && !message.is_char_boundary(end) { + end -= 1; + } + message.truncate(end); error!(target: LOG_TARGET, "Panic message size limit exceeded: for panic {}", message); return Err(WasmExecutionError::Panic { message: format!(