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
6 changes: 2 additions & 4 deletions applications/tari_app_utilities/src/transaction_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::sync::Arc;

use log::*;
use tari_engine::{
executables::Executable,
fees::{FeeModule, FeeTable},
runtime::{AuthParams, RuntimeModule},
state_store::{memory::ReadOnlyMemoryStateStore, StateStoreError},
Expand Down Expand Up @@ -120,10 +121,7 @@ where TTemplateProvider: TemplateProvider<Template = LoadedTemplate>
// Include signature public key badges for all transaction signers in the initial auth scope
// NOTE: we assume all signatures have already been validated.
let initial_ownership_proofs = transaction
.signatures()
.iter()
.map(|p| p.public_key())
.chain(Some(transaction.seal_signature().public_key()).filter(|_| transaction.is_seal_signer_authorized()))
.signers_iter()
.map(|pk| NonFungibleAddress::from_public_key(*pk))
.collect();
let auth_params = AuthParams {
Expand Down
31 changes: 10 additions & 21 deletions applications/tari_walletd/src/handlers/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use tari_ootle_wallet_sdk::{
stealth_transfer::StealthTransferParams,
substate::ValidatorScanResult,
},
models::{KeyBranch, KeyId, NewAccountData},
models::{KeyBranch, NewAccountData},
};
use tari_ootle_wallet_sdk_services::events::TransactionSubmittedEvent;
use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS;
Expand Down Expand Up @@ -534,11 +534,14 @@ pub async fn handle_claim_burn(
public_nonce: reciprocal_claim_public_key_expanded,
};

let public_signer_key = sdk.key_manager_api().next_public_key(KeyBranch::Nonce)?;

let pay_fee_and_mint_output = sdk.stealth_crypto_api().generate_transfer_statement(
array::from_ref(&input),
0,
array::from_ref(&output_statement),
max_fee,
public_signer_key.public_key.to_byte_type(),
)?;
// We'll create an output with the same encrypted data that was used on L1 burn. Note that this is not strictly
// necessary. The engine will create the output with whatever you give it, so we could reencrypt.
Expand All @@ -556,12 +559,9 @@ pub async fn handle_claim_burn(
.add_input(XTR)
.build();

// The signer does not authorize this transaction, as the claim burn instruction is authorized by the proofs. So we
// can sign with any key.
let nonce = sdk.key_manager_api().next_public_key(KeyBranch::Nonce)?;
let transaction = sdk
.local_signer_api()
.sign(KeyBranch::Nonce, nonce.key_id, transaction)?;
.sign(KeyBranch::Nonce, public_signer_key.key_id, transaction)?;

let tx_id = context.transaction_service().submit_transaction(transaction).await?;

Expand Down Expand Up @@ -959,7 +959,7 @@ pub async fn handle_stealth_transfer(
let network = sdk.sdk_config().network;
let notifier = context.notifier().clone();
let owner_account = get_account(&req.owner_account, &sdk.accounts_api())?;
let Some(owner_key_id) = owner_account.owner_key_id() else {
if owner_account.owner_key_id().is_none() {
return Err(invalid_params(
"owner_account",
Some("cannot transfer from an account without an owner key"),
Expand Down Expand Up @@ -987,22 +987,11 @@ pub async fn handle_stealth_transfer(
task::spawn(async move {
let transfer = sdk.stealth_transfer_api().transfer(owner_account, params).await?;

let must_sign_with_account_key =
transfer.fee_inputs.revealed.is_positive() || transfer.transfer_inputs.revealed.is_positive();

let transaction = transfer.transaction.authorized_sealed_signer().build(vec![]);

let (key_branch, key_id) = if must_sign_with_account_key {
(KeyBranch::Account, owner_key_id)
} else {
// Since we don't require account auth, use a throwaway nonce to sign the transaction
(
KeyBranch::Nonce,
KeyId::derived(sdk.key_manager_api().next_derived_key_index(KeyBranch::Nonce)?),
)
};
let transaction = transfer.transaction.authorized_sealed_signer().build();

let transaction = sdk.local_signer_api().sign(key_branch, key_id, transaction)?;
let transaction =
sdk.local_signer_api()
.sign(transfer.signing_key_branch, transfer.signing_key_id, transaction)?;

// TODO: if submitting fails we need to unlock the inputs again
if req.dry_run {
Expand Down
2 changes: 1 addition & 1 deletion applications/tari_walletd/src/handlers/nfts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ pub async fn handle_transfer(
.with_inputs(inputs.into_iter().map(|input| input.into_unversioned()))
// Seal signer is the fee payer account
.with_authorized_seal_signer()
.then(|builder| {
.map(|builder| {
sdk.local_signer_api().sign_with_context(
KeyBranch::Account,
account_owner_key_id,
Expand Down
4 changes: 2 additions & 2 deletions applications/tari_walletd/src/handlers/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ pub async fn handle_submit_manifest(
}
})
.with_instructions(instructions.instructions)
.then(|builder| {
.map(|builder| {
if signing_key_id == account_owner_key_id {
Ok(builder)
} else {
Expand All @@ -350,7 +350,7 @@ pub async fn handle_submit_manifest(
let transaction = transaction
.with_inputs(inputs)
.authorized_sealed_signer()
.build(signatures);
.build_with_signatures(signatures);

let transaction = sdk
.local_signer_api()
Expand Down
23 changes: 10 additions & 13 deletions applications/tari_walletd/src/handlers/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,26 +179,23 @@ pub async fn handle_claim_validator_fees(
.with_inputs(inputs.into_iter().map(|input| input.into_unversioned()))
.with_inputs(fee_pool_addresses.map(SubstateRequirement::unversioned))
.add_input(XTR)
.then(|builder| {
.map(|builder| {
if let Some(index) = req.claim_key_index {
if claim_public_key == *account.address.account_public_key() {
builder
Ok(builder)
} else {
// If the claim key is different from the account secret, we need to sign with both
sdk.local_signer_api()
.sign_with_context(
KeyBranch::Account,
KeyId::derived(index),
account.address.account_public_key(),
builder.with_authorized_seal_signer(),
)
// We happen to know that signing with a derived key is infallible
.expect("Signing with should work")
sdk.local_signer_api().sign_with_context(
KeyBranch::Account,
KeyId::derived(index),
account.address.account_public_key(),
builder.with_authorized_seal_signer(),
)
}
} else {
builder
Ok(builder)
}
})
})?
.build();

let transaction = sdk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
XTR,
} from "@tari-project/typescript-bindings";
import { transactionsWaitResult } from "@utils/json_rpc";
import FormStep, { SendMoneyFormState } from "../steps/FormStep";
import FormStep, { FormError, SendMoneyFormState } from "../steps/FormStep";
import ConfirmationStep from "../steps/ConfirmationStep";
import ResultStep, { TransferResult } from "../steps/ResultStep";
import PopupTitle from "@/components/PopupTitle";
Expand Down Expand Up @@ -66,6 +66,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) {
const [isEstimatingFee, setIsEstimatingFee] = useState(false);
const [transferFormState, setTransferFormState] = useState(INITIAL_VALUES);
const [transferResult, setTransferResult] = useState<TransferResult | undefined>();
const [formError, setFormError] = useState<FormError | null>(null);
const { mutateAsync: sendIt } = useAccountsTransfer();

const { account } = useAccountStore();
Expand Down Expand Up @@ -120,6 +121,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) {
const availableBalance = calculateAvailableBalance();

function setFormValue(e: React.ChangeEvent<HTMLInputElement>) {
setFormError(null);
const { name, value } = e.target;

// For amount field, parse the input to allow decimal values
Expand All @@ -145,20 +147,23 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) {
}

function setSelectFormValue(e: SelectChangeEvent<unknown>) {
setFormError(null);
setTransferFormState({
...transferFormState,
[e.target.name]: e.target.value,
});
}

function setCheckboxFormValue(e: React.ChangeEvent<HTMLInputElement>) {
setFormError(null);
setTransferFormState({
...transferFormState,
[e.target.name]: e.target.checked,
});
}

const handleUseBadgeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormError(null);
setUseBadge(e.target.checked);
if (!e.target.checked) {
setTransferFormState({
Expand Down Expand Up @@ -208,9 +213,6 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) {
fee += 100;
}
setTransferFormState((prevState) => ({ ...prevState, fee: fee.toString() }));
} catch (error) {
console.error("Fee estimation error:", error);
// Don't block the user if fee estimation fails
} finally {
setIsEstimatingFee(false);
}
Expand All @@ -232,6 +234,10 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) {
try {
await estimateFee();
} catch (error) {
setFormError({
type: "general",
message: `Failed to estimate fee: ${error}`,
});
console.error("Fee estimation failed:", error);
return;
}
Expand Down Expand Up @@ -316,6 +322,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) {
availableBalance={availableBalance}
token_symbol={props.token_symbol}
divisibility={balanceEntry.divisibility}
formError={formError}
onSubmit={handleFormSubmit}
onCancel={handleClose}
onFormValueChange={setFormValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface FormStepProps {
availableBalance?: number;
token_symbol: string;
divisibility: number;
formError?: FormError | null;
onSubmit: (e: FormEvent) => void;
onCancel: () => void;
onFormValueChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
Expand All @@ -63,6 +64,11 @@ interface FormStepProps {
onUseBadgeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export type FormError = {
type: "general" | "address" | "amount" | "fee";
message: string;
};

export default function FormStep({
resource_address,
resource_type,
Expand All @@ -74,6 +80,7 @@ export default function FormStep({
availableBalance,
token_symbol,
divisibility,
formError,
onSubmit,
onCancel,
onFormValueChange,
Expand Down Expand Up @@ -151,6 +158,7 @@ export default function FormStep({
</>
)}
<Stack direction="column" spacing={0.5}>
<DisplayFormError forType="address" formError={formError} />
<TextField
name="address"
label="To Address"
Expand Down Expand Up @@ -202,6 +210,7 @@ export default function FormStep({
</>
)}

<DisplayFormError forType="amount" formError={formError} />
<TextField
name="amount"
label="Amount"
Expand Down Expand Up @@ -248,6 +257,8 @@ export default function FormStep({
/>

<Divider />

<DisplayFormError forType="general" formError={formError} />
<Stack direction="row" justifyContent="space-between" sx={{ mt: 3 }}>
<Button variant="outlined" onClick={onCancel} disabled={disabled}>
Cancel
Expand All @@ -260,3 +271,13 @@ export default function FormStep({
</Form>
);
}

function DisplayFormError({ forType, formError }: { forType: FormError["type"]; formError?: FormError | null }) {
if (!formError) return null;
if (formError.type !== forType) return null;
return (
<Typography color="error" sx={{ mb: 2 }}>
{formError.message}
</Typography>
);
}
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.18.0",
"version": "1.18.1",
"description": "TypeScript types synchronized to the Tari Ootle Rust codebase",
"homepage": "https://github.com/tari-project/tari-ootle#readme",
"bugs": {
Expand Down
1 change: 1 addition & 0 deletions bindings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from "./types/FeeSource";
export * from "./types/FinalizeResult";
export * from "./types/ForeignProposalAtom";
export * from "./types/FunctionDef";
export * from "./types/Hash64";
export * from "./types/Hash";
export * from "./types/IndexedValue";
export * from "./types/IndexedWellKnownTypes";
Expand Down
6 changes: 6 additions & 0 deletions bindings/src/types/Hash64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Representation of a 32-byte hash value
*/
export type Hash64 = string;
Comment thread
sdbondi marked this conversation as resolved.
5 changes: 5 additions & 0 deletions bindings/src/types/StealthInputsStatement.ts
Original file line number Diff line number Diff line change
@@ -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 { Amount } from "./Amount";
import type { RistrettoPublicKeyBytes } from "./RistrettoPublicKeyBytes";
import type { StealthInput } from "./StealthInput";

/**
Expand All @@ -14,4 +15,8 @@ export type StealthInputsStatement = {
* The total amount of revealed funds being spent.
*/
revealed_amount: Amount;
/**
* The signer that must sign the transaction to allow these inputs to be spent.
*/
required_signer: RistrettoPublicKeyBytes;
};
1 change: 1 addition & 0 deletions crates/engine/src/executables/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub trait Executable {
fn all_inputs_iter(&self) -> impl Iterator<Item = SubstateRequirementRef<'_>> + '_;

fn main_signer(&self) -> Option<RistrettoPublicKeyBytes>;
fn signers_iter(&self) -> impl Iterator<Item = &RistrettoPublicKeyBytes>;

fn into_instructions(self) -> Instructions;
}
Expand Down
11 changes: 7 additions & 4 deletions crates/engine/src/executables/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ impl Executable for Transaction {
// TODO: If the seal signer is authorized we use this as the signer public key, if not we use the first
// signature as the "default" owner. This is due to limitations of the current transaction model.
// We could remove the idea of a default owner (OwnedBySigner) entirely.
Some(self.seal_signature())
self.signers_iter().next().copied()
}

fn signers_iter(&self) -> impl Iterator<Item = &RistrettoPublicKeyBytes> {
Some(self.seal_signature().public_key())
.filter(|_| self.is_seal_signer_authorized())
.map(|s| s.public_key())
.or(self.signatures().first().map(|s| s.public_key()))
.copied()
.into_iter()
.chain(self.signatures().iter().map(|s| s.public_key()))
}

fn into_instructions(self) -> Instructions {
Expand Down
3 changes: 3 additions & 0 deletions crates/engine/src/runtime/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ use tari_template_lib::{
ResourceAddress,
VaultId,
},
prelude::RistrettoPublicKeyBytes,
types::{Amount, TemplateAddress},
};
use tari_transaction::args::{WorkspaceId, WorkspaceOffsetId};
Expand Down Expand Up @@ -173,6 +174,8 @@ pub enum RuntimeError {
AccessDeniedAuthHook { action_ident: ActionIdent, details: String },
#[error("Access Denied: You must be the owner to perform this action: {action}")]
AccessDeniedOwnerRequired { action: ActionIdent },
#[error("Access Denied: Stealth transfer requires a signer with public key {required_signer}")]
AccessDeniedStealthTransferSigner { required_signer: RistrettoPublicKeyBytes },
#[error("Invalid method address rule for {template_name}: {details}")]
InvalidMethodAccessRule { template_name: String, details: String },
#[error("Runtime module error: {0}")]
Expand Down
Loading
Loading