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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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...)"
}
/>

Expand Down
2 changes: 1 addition & 1 deletion bindings/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
12 changes: 6 additions & 6 deletions bindings/src/helpers/ootleAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions bindings/src/types/Instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> } }
| { CallFunction: { address: Hash; function: string; args: Array<any> } }
| { CallMethod: { call: ComponentCall; method: string; args: Array<any> } }
| { 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 } }
Expand Down
3 changes: 2 additions & 1 deletion bindings/src/types/InstructionArg.ts
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 1 addition & 1 deletion bindings/test/ootleAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/transaction/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ impl<TTemplateProvider: TemplateProvider<Template = LoadedTemplate> + '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 } => {
Expand Down
20 changes: 19 additions & 1 deletion crates/engine/src/wasm/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,25 @@ impl Invokable<Store> 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 {
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!(
"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,
Expand Down
2 changes: 2 additions & 0 deletions crates/engine_types/src/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions crates/ootle_address/src/hrp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 7 additions & 4 deletions crates/p2p/src/conversions/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,12 @@ impl TryFrom<proto::transaction::Instruction> 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 {
Expand Down Expand Up @@ -342,7 +345,7 @@ impl From<Instruction> 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(),
},
)),
},
Expand Down
45 changes: 44 additions & 1 deletion crates/template_lib_types/src/max_string.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -65,12 +65,55 @@ impl<'de, const N: usize> serde::Deserialize<'de> for MaxString<N> {
}
}

impl<const N: usize> TryFrom<String> for MaxString<N> {
type Error = MaxStringError<N>;

fn try_from(value: String) -> Result<Self, Self::Error> {
MaxString::new_checked(value).ok_or(MaxStringError)
}
}

impl<const N: usize> Display for MaxString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.s.as_ref().fmt(f)
}
}

pub struct MaxStringError<const N: usize>;

impl<const N: usize> Display for MaxStringError<N> {
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<const N: usize> borsh::BorshSerialize for MaxString<N> {
fn serialize<W: Write>(&self, writer: &mut W) -> Result<()> {
self.s.serialize(writer)
}
}

impl<const N: usize> borsh::BorshDeserialize for MaxString<N> {
fn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self> {
let s = Box::<str>::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::*;
Expand Down
2 changes: 1 addition & 1 deletion crates/transaction/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 4 additions & 5 deletions crates/transaction/src/v1/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use tari_engine_types::{
confidential::{ClaimBurnOutputData, MinotariBurnClaimProof},
limits,
ValidatorFeePoolAddress,
};
use tari_template_lib::{
args::LogLevel,
auth::OwnerRule,
models::{ResourceAddress, StealthTransferStatement},
prelude::{AccessRules, Amount},
types::{crypto::RistrettoPublicKeyBytes, TemplateAddress},
types::{crypto::RistrettoPublicKeyBytes, MaxString, TemplateAddress},
};

use crate::{
Expand All @@ -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<OwnerRule>,
access_rules: Option<AccessRules>,
workspace_id: Option<WorkspaceOffsetId>,
},
CallFunction {
#[cfg_attr(feature = "ts", ts(type = "string"))]
address: TemplateAddress,
function: String,
#[serde(deserialize_with = "crate::special_json_arg_syntax::json_deserialize")]
Expand All @@ -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<MinotariBurnClaimProof>,
output_data: ClaimBurnOutputData,
},
ClaimValidatorFees {
#[cfg_attr(feature = "ts", ts(type = "string"))]
address: ValidatorFeePoolAddress,
},
DropAllProofsInWorkspace,
Expand Down
2 changes: 2 additions & 0 deletions crates/transaction_manifest/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}
4 changes: 3 additions & 1 deletion crates/transaction_manifest/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
}
Expand Down
Loading