audit: cargo fmt + clippy --fix across workspace + retract_votes cooldown bug fix
Surfaced by Track #38 code audit (2026-05-09): 1. cargo fmt --all: 217 formatting diffs across 35 files. Pure whitespace; no semantic changes. 2. cargo clippy --fix: 30 warnings -> 10. Auto-applied: - useless format!() (3 sites in builder/proposal_*.rs) - needless_borrow_for_generic_args (4 sites) - cloned_ref_to_slice_refs (1 site, builder/proposal_cosign.rs) - derivable_impls (1 site, dao/config.rs) - unused imports/variables (3 sites) Remaining 10 warnings are non-trivial (too_many_arguments on a constructor at 8 args, FromStr trait shadow, doc_lazy_continuation on a few comment blocks). Filed as tech-debt; no action this pass. 3. cargo audit: 0 vulnerabilities. 2 unmaintained advisories on transitive deps: - paste 1.0.15 (RUSTSEC-2024-0436) via rmcp + pallas-traverse - proc-macro-error 1.0.4 (RUSTSEC-2024-0370) via age->i18n-embed-fl Both upstream; tracked but no action needed locally. 4. Test failure surfaced: builder::proposal_retract_votes::tests:: voting_ready_in_window_subtracts_vote_weight failed — cooldown check was applied unconditionally for RemoveVoterLockOnly mode, blocking the legitimate 'retract during voting window' path where the proposal datum mutates (vote weight subtraction). Per Agora's premoveLocks rule, cooldown only applies when retracting AFTER voting closed but BEFORE Finished — not during the active voting window. Fixed by gating cooldown on '!proposal_datum_will_change' so the in-window retract path bypasses cooldown the same way RemoveAllLocks does. Test: 87/87 aldabra-dao lib tests pass post-fix (was 86/87).
This commit is contained in:
parent
0987d5de12
commit
03b5efb3b2
35 changed files with 1125 additions and 1072 deletions
|
|
@ -136,8 +136,7 @@ fn json_to_plutus_data(v: &Value) -> Result<PlutusData, WalletError> {
|
|||
Value::Object(map) => {
|
||||
let mut pairs: Vec<(PlutusData, PlutusData)> = Vec::with_capacity(map.len());
|
||||
for (k, vv) in map {
|
||||
let key =
|
||||
PlutusData::BoundedBytes(BoundedBytes::from(k.as_bytes().to_vec()));
|
||||
let key = PlutusData::BoundedBytes(BoundedBytes::from(k.as_bytes().to_vec()));
|
||||
let value = json_to_plutus_data(vv)?;
|
||||
pairs.push((key, value));
|
||||
}
|
||||
|
|
@ -165,9 +164,8 @@ pub fn build_cip68_datum_cbor(metadata: &Value) -> Result<Vec<u8>, WalletError>
|
|||
}
|
||||
let metadata_pd = json_to_plutus_data(metadata)?;
|
||||
|
||||
let version_pd = PlutusData::BigInt(BigInt::Int(
|
||||
pallas_codec::utils::Int::from(CIP68_VERSION_2),
|
||||
));
|
||||
let version_pd =
|
||||
PlutusData::BigInt(BigInt::Int(pallas_codec::utils::Int::from(CIP68_VERSION_2)));
|
||||
|
||||
// "extra" — Constr 0 with no fields (Plutus unit).
|
||||
let extra_pd = PlutusData::Constr(Constr {
|
||||
|
|
|
|||
|
|
@ -99,10 +99,7 @@ impl StakeKey {
|
|||
/// Reward / stake address (`stake1...` or `stake_test1...`)
|
||||
/// bech32-encoded. This is the address you point at a stake pool
|
||||
/// when delegating.
|
||||
pub fn stake_address(
|
||||
&self,
|
||||
network: crate::Network,
|
||||
) -> Result<String, crate::WalletError> {
|
||||
pub fn stake_address(&self, network: crate::Network) -> Result<String, crate::WalletError> {
|
||||
use pallas_addresses::{StakeAddress, StakePayload};
|
||||
let payload = StakePayload::Stake(self.public_key_hash());
|
||||
let addr = StakeAddress::new(network.to_pallas(), payload);
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ pub fn parse_drep_target(s: &str) -> Result<DRepTarget, WalletError> {
|
|||
if s == "no_confidence" {
|
||||
return Ok(DRepTarget::NoConfidence);
|
||||
}
|
||||
let (hrp, data, _) = bech32::decode(s)
|
||||
.map_err(|e| WalletError::Address(format!("bad drep bech32: {e}")))?;
|
||||
let (hrp, data, _) =
|
||||
bech32::decode(s).map_err(|e| WalletError::Address(format!("bad drep bech32: {e}")))?;
|
||||
if hrp != "drep" && hrp != "drep_script" {
|
||||
return Err(WalletError::Address(format!(
|
||||
"expected drep / drep_script hrp, got '{hrp}'"
|
||||
|
|
@ -139,8 +139,7 @@ pub fn parse_drep_target(s: &str) -> Result<DRepTarget, WalletError> {
|
|||
}
|
||||
|
||||
fn parse_address(bech32: &str) -> Result<pallas_addresses::Address, WalletError> {
|
||||
pallas_addresses::Address::from_bech32(bech32)
|
||||
.map_err(|e| WalletError::Address(e.to_string()))
|
||||
pallas_addresses::Address::from_bech32(bech32).map_err(|e| WalletError::Address(e.to_string()))
|
||||
}
|
||||
|
||||
fn parse_tx_hash(hex_str: &str) -> Result<Hash<32>, WalletError> {
|
||||
|
|
@ -425,18 +424,19 @@ fn sign_voting_tx(
|
|||
}
|
||||
let total_in: u64 = selected.iter().map(|u| u.lovelace).sum();
|
||||
|
||||
let build_with_fee = |fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
let change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
staging = staging.output(change_out);
|
||||
staging = staging.voting_procedures(voting_procedures_cbor.clone());
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
let change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
staging = staging.output(change_out);
|
||||
staging = staging.voting_procedures(voting_procedures_cbor.clone());
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
let change_pass1 = total_in
|
||||
.checked_sub(fee_pass1)
|
||||
|
|
@ -450,11 +450,11 @@ fn sign_voting_tx(
|
|||
let est_signed = (unsigned.len() as u64) + TWO_WITNESS_OVERHEAD_BYTES;
|
||||
let real_fee = params.min_fee_for_size(est_signed);
|
||||
|
||||
let final_change = total_in
|
||||
.checked_sub(real_fee)
|
||||
.ok_or_else(|| WalletError::Derivation(format!(
|
||||
let final_change = total_in.checked_sub(real_fee).ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} fee={real_fee}"
|
||||
)))?;
|
||||
))
|
||||
})?;
|
||||
if final_change < params.min_utxo_lovelace {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"change ({final_change}) below min utxo ({})",
|
||||
|
|
@ -555,48 +555,49 @@ fn sign_cert_tx(
|
|||
}
|
||||
}
|
||||
|
||||
let build_with_fee = |fee: u64,
|
||||
change_lovelace: u64|
|
||||
-> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &input_assets {
|
||||
if *q == 0 {
|
||||
continue;
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
if k.len() < 56 {
|
||||
return Err(WalletError::Derivation("asset key shorter than 56 chars".into()));
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &input_assets {
|
||||
if *q == 0 {
|
||||
continue;
|
||||
}
|
||||
if k.len() < 56 {
|
||||
return Err(WalletError::Derivation(
|
||||
"asset key shorter than 56 chars".into(),
|
||||
));
|
||||
}
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let mut policy_bytes = [0u8; 28];
|
||||
for i in 0..28 {
|
||||
policy_bytes[i] = u8::from_str_radix(&pol_hex[i * 2..i * 2 + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid policy hex".into()))?;
|
||||
}
|
||||
let policy = Hash::<28>::new(policy_bytes);
|
||||
let mut name_bytes = Vec::with_capacity(name_hex.len() / 2);
|
||||
for i in (0..name_hex.len()).step_by(2) {
|
||||
name_bytes.push(
|
||||
u8::from_str_radix(&name_hex[i..i + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid name hex".into()))?,
|
||||
);
|
||||
}
|
||||
change_out = change_out
|
||||
.add_asset(policy, name_bytes, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
}
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let mut policy_bytes = [0u8; 28];
|
||||
for i in 0..28 {
|
||||
policy_bytes[i] = u8::from_str_radix(&pol_hex[i * 2..i * 2 + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid policy hex".into()))?;
|
||||
staging = staging.output(change_out);
|
||||
for cb in &cert_bytes_list {
|
||||
staging = staging.add_certificate(cb.clone());
|
||||
}
|
||||
let policy = Hash::<28>::new(policy_bytes);
|
||||
let mut name_bytes = Vec::with_capacity(name_hex.len() / 2);
|
||||
for i in (0..name_hex.len()).step_by(2) {
|
||||
name_bytes.push(
|
||||
u8::from_str_radix(&name_hex[i..i + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid name hex".into()))?,
|
||||
);
|
||||
}
|
||||
change_out = change_out
|
||||
.add_asset(policy, name_bytes, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
}
|
||||
staging = staging.output(change_out);
|
||||
for cb in &cert_bytes_list {
|
||||
staging = staging.add_certificate(cb.clone());
|
||||
}
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
let change_pass1 = total_in
|
||||
.checked_sub(deposit + fee_pass1)
|
||||
|
|
@ -611,11 +612,11 @@ fn sign_cert_tx(
|
|||
let real_fee = params.min_fee_for_size(est_signed);
|
||||
|
||||
let token_change = !input_assets.is_empty();
|
||||
let final_change = total_in
|
||||
.checked_sub(deposit + real_fee)
|
||||
.ok_or_else(|| WalletError::Derivation(format!(
|
||||
let final_change = total_in.checked_sub(deposit + real_fee).ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} deposit={deposit} fee={real_fee}"
|
||||
)))?;
|
||||
))
|
||||
})?;
|
||||
if final_change < params.min_utxo_lovelace && token_change {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient ADA for token-bearing change: change={final_change}, min={}",
|
||||
|
|
@ -680,20 +681,21 @@ fn sign_cert_tx_with_refund(
|
|||
}
|
||||
let total_in: u64 = selected.iter().map(|u| u.lovelace).sum();
|
||||
|
||||
let build_with_fee = |fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
let change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
staging = staging.output(change_out);
|
||||
for cb in &cert_bytes_list {
|
||||
staging = staging.add_certificate(cb.clone());
|
||||
}
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
let change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
staging = staging.output(change_out);
|
||||
for cb in &cert_bytes_list {
|
||||
staging = staging.add_certificate(cb.clone());
|
||||
}
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
// Change includes the refund: change = total_in + refund - fee.
|
||||
let change_pass1 = total_in
|
||||
|
|
@ -712,9 +714,11 @@ fn sign_cert_tx_with_refund(
|
|||
let final_change = total_in
|
||||
.checked_add(refund)
|
||||
.and_then(|x| x.checked_sub(real_fee))
|
||||
.ok_or_else(|| WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} refund={refund} fee={real_fee}"
|
||||
)))?;
|
||||
.ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} refund={refund} fee={real_fee}"
|
||||
))
|
||||
})?;
|
||||
if final_change < params.min_utxo_lovelace {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"change ({final_change}) below min utxo ({})",
|
||||
|
|
@ -756,7 +760,10 @@ mod tests {
|
|||
fn drep_target_into_pallas_round_trip() {
|
||||
let h = Hash::<28>::new([0u8; 28]);
|
||||
assert!(matches!(DRepTarget::Key(h).into_pallas(), DRep::Key(_)));
|
||||
assert!(matches!(DRepTarget::Script(h).into_pallas(), DRep::Script(_)));
|
||||
assert!(matches!(
|
||||
DRepTarget::Script(h).into_pallas(),
|
||||
DRep::Script(_)
|
||||
));
|
||||
assert!(matches!(DRepTarget::Abstain.into_pallas(), DRep::Abstain));
|
||||
assert!(matches!(
|
||||
DRepTarget::NoConfidence.into_pallas(),
|
||||
|
|
|
|||
|
|
@ -243,10 +243,8 @@ pub fn summarize_tx(cbor_bytes: &[u8]) -> Result<TxSummary, WalletError> {
|
|||
.unwrap_or(0);
|
||||
|
||||
let auxiliary_data_hash_set = body.auxiliary_data_hash.is_some();
|
||||
let auxiliary_data_present = matches!(
|
||||
tx.auxiliary_data,
|
||||
pallas_codec::utils::Nullable::Some(_)
|
||||
);
|
||||
let auxiliary_data_present =
|
||||
matches!(tx.auxiliary_data, pallas_codec::utils::Nullable::Some(_));
|
||||
|
||||
Ok(TxSummary {
|
||||
tx_hash: hex(body_hash.as_ref()),
|
||||
|
|
|
|||
|
|
@ -47,18 +47,23 @@ pub mod plutus_mint;
|
|||
pub mod sign;
|
||||
pub mod stake;
|
||||
pub mod tx;
|
||||
pub use cip68::{
|
||||
build_cip68_datum_cbor, ft_asset_name, ref_nft_asset_name, user_nft_asset_name,
|
||||
};
|
||||
pub use cip68::{build_cip68_datum_cbor, ft_asset_name, ref_nft_asset_name, user_nft_asset_name};
|
||||
pub use derive::{derive_payment_key, derive_stake_key, PaymentKey, StakeKey};
|
||||
pub use inspect::{summarize_tx, AssetEntry, CertificateSummary, MintEntry, OutputSummary, TxSummary};
|
||||
pub use inspect::{
|
||||
summarize_tx, AssetEntry, CertificateSummary, MintEntry, OutputSummary, TxSummary,
|
||||
};
|
||||
// Stake address derivation lives directly on StakeKey — exported above.
|
||||
pub use governance::{
|
||||
build_signed_drep_deregistration, build_signed_drep_registration, build_signed_drep_vote_cast,
|
||||
build_signed_vote_delegation, parse_drep_target, DRepTarget, VoteChoice,
|
||||
DREP_REGISTRATION_DEPOSIT_LOVELACE,
|
||||
};
|
||||
pub use metadata::{build_cip25_aux_data, CIP25_LABEL};
|
||||
pub use mint::{
|
||||
build_signed_cip68_nft_mint, build_signed_mint, build_signed_mint_with_metadata,
|
||||
build_unsigned_mint, PolicySpec,
|
||||
};
|
||||
pub use sign::add_witness;
|
||||
pub use pallas_txbuilder::ScriptKind;
|
||||
pub use plutus::{
|
||||
build_signed_plutus_spend, looks_like_script_address, PlutusExUnits, PlutusInput,
|
||||
PlutusVersion, DEFAULT_EX_UNITS, MIN_COLLATERAL_LOVELACE,
|
||||
|
|
@ -67,19 +72,14 @@ pub use plutus_mint::{
|
|||
build_signed_plutus_mint, build_unsigned_plutus_mint, ExtraDestAsset, PlutusMintArgs,
|
||||
PlutusMintAsset,
|
||||
};
|
||||
pub use sign::add_witness;
|
||||
pub use stake::{build_signed_stake_delegation, parse_pool_id, STAKE_KEY_DEPOSIT_LOVELACE};
|
||||
pub use governance::{
|
||||
build_signed_drep_deregistration, build_signed_drep_registration,
|
||||
build_signed_drep_vote_cast, build_signed_vote_delegation, parse_drep_target,
|
||||
DRepTarget, VoteChoice, DREP_REGISTRATION_DEPOSIT_LOVELACE,
|
||||
};
|
||||
pub use tx::{
|
||||
build_signed_payment, build_signed_payment_extras, build_signed_payment_with_assets,
|
||||
build_unsigned_payment, build_unsigned_payment_extras, build_unsigned_payment_with_assets,
|
||||
hex_decode, AssetSpec, InputUtxo, PaymentSummary, ProtocolParams, ReferenceScriptSpec,
|
||||
UnsignedPayment,
|
||||
};
|
||||
pub use pallas_txbuilder::ScriptKind;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WalletError {
|
||||
|
|
@ -171,10 +171,7 @@ impl Mnemonic {
|
|||
/// 2. Bit-clamp the first 32 bytes so the result is a valid extended
|
||||
/// Ed25519 scalar with the 3rd-highest bit cleared
|
||||
/// (`normalize_bytes_force3rd`).
|
||||
pub fn into_root_key_with_passphrase(
|
||||
self,
|
||||
passphrase: &str,
|
||||
) -> Result<RootKey, WalletError> {
|
||||
pub fn into_root_key_with_passphrase(self, passphrase: &str) -> Result<RootKey, WalletError> {
|
||||
let mut xprv_bytes = [0u8; XPRV_SIZE];
|
||||
let mut hmac = Hmac::new(Sha512::new(), passphrase.as_bytes());
|
||||
pbkdf2(&mut hmac, &self.entropy, 4096, &mut xprv_bytes);
|
||||
|
|
@ -420,8 +417,8 @@ mod tests {
|
|||
assert!(addr.starts_with("addr1"), "got: {addr}");
|
||||
// Round-trip — pallas should parse what we just emitted and
|
||||
// give back a Shelley mainnet address.
|
||||
let parsed = pallas_addresses::Address::from_bech32(&addr)
|
||||
.expect("our own bech32 output parses");
|
||||
let parsed =
|
||||
pallas_addresses::Address::from_bech32(&addr).expect("our own bech32 output parses");
|
||||
match parsed {
|
||||
pallas_addresses::Address::Shelley(s) => {
|
||||
assert_eq!(s.network(), pallas_addresses::Network::Mainnet);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ fn json_to_metadatum(v: &Value) -> Result<Metadatum, WalletError> {
|
|||
Value::Null => Err(WalletError::Derivation(
|
||||
"null is not representable in Cardano metadata".into(),
|
||||
)),
|
||||
Value::Bool(b) => Ok(Metadatum::Int(Int(CborInt::from(if *b { 1i64 } else { 0 })))),
|
||||
Value::Bool(b) => Ok(Metadatum::Int(Int(CborInt::from(if *b {
|
||||
1i64
|
||||
} else {
|
||||
0
|
||||
})))),
|
||||
Value::Number(n) => {
|
||||
let i = n.as_i64().ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
|
|
@ -166,8 +170,7 @@ pub fn build_cip25_aux_data(
|
|||
plutus_scripts: None,
|
||||
});
|
||||
|
||||
minicbor::to_vec(&aux)
|
||||
.map_err(|e| WalletError::Derivation(format!("encode CIP-25 aux: {e}")))
|
||||
minicbor::to_vec(&aux).map_err(|e| WalletError::Derivation(format!("encode CIP-25 aux: {e}")))
|
||||
}
|
||||
|
||||
fn decode_hex(s: &str) -> Result<Vec<u8>, WalletError> {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ use pallas_codec::minicbor;
|
|||
use pallas_crypto::hash::Hash;
|
||||
use pallas_primitives::alonzo::NativeScript;
|
||||
use pallas_traverse::ComputeHash;
|
||||
use pallas_txbuilder::{BuildConway, BuiltTransaction, Input, Output, ScriptKind, StagingTransaction};
|
||||
use pallas_txbuilder::{
|
||||
BuildConway, BuiltTransaction, Input, Output, ScriptKind, StagingTransaction,
|
||||
};
|
||||
use pallas_wallet::PrivateKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -161,8 +163,7 @@ fn hash_to_hex(h: &Hash<28>) -> String {
|
|||
}
|
||||
|
||||
fn parse_address(bech32: &str) -> Result<pallas_addresses::Address, WalletError> {
|
||||
pallas_addresses::Address::from_bech32(bech32)
|
||||
.map_err(|e| WalletError::Address(e.to_string()))
|
||||
pallas_addresses::Address::from_bech32(bech32).map_err(|e| WalletError::Address(e.to_string()))
|
||||
}
|
||||
|
||||
fn parse_tx_hash(hex_str: &str) -> Result<Hash<32>, WalletError> {
|
||||
|
|
@ -353,11 +354,9 @@ fn prepare_mint(
|
|||
(real_fee, c)
|
||||
}
|
||||
Some(c) => (real_fee + c, 0),
|
||||
None => {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} dest={dest_lovelace} fee={real_fee}"
|
||||
)))
|
||||
}
|
||||
None => return Err(WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} dest={dest_lovelace} fee={real_fee}"
|
||||
))),
|
||||
};
|
||||
|
||||
let staging2 = build_mint_staging(
|
||||
|
|
@ -729,63 +728,62 @@ pub fn build_signed_cip68_nft_mint(
|
|||
}
|
||||
}
|
||||
|
||||
let build_with_fee = |fee: u64,
|
||||
change_lovelace: u64|
|
||||
-> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
|
||||
// Output 1: ref NFT @ ref_address, with inline datum.
|
||||
let ref_out = Output::new(ref_addr.clone(), ref_lovelace)
|
||||
.add_asset(policy_id, ref_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("ref add_asset: {e}")))?
|
||||
.set_inline_datum(datum_cbor.clone());
|
||||
staging = staging.output(ref_out);
|
||||
|
||||
// Output 2: user NFT @ user_address.
|
||||
let user_out = Output::new(user_addr.clone(), user_lovelace)
|
||||
.add_asset(policy_id, user_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("user add_asset: {e}")))?;
|
||||
staging = staging.output(user_out);
|
||||
|
||||
// Output 3 (optional): change @ wallet, with leftover input assets.
|
||||
let nonzero_change_assets: std::collections::BTreeMap<String, u64> = input_assets
|
||||
.iter()
|
||||
.filter(|(_, q)| **q > 0)
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
if change_lovelace > 0 || !nonzero_change_assets.is_empty() {
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &nonzero_change_assets {
|
||||
if k.len() < 56 {
|
||||
return Err(WalletError::Derivation(
|
||||
"change asset key shorter than 56 chars".into(),
|
||||
));
|
||||
}
|
||||
let p = parse_pkh(&k[..56])?;
|
||||
let n = parse_asset_name(&k[56..])?;
|
||||
change_out = change_out
|
||||
.add_asset(p, n, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
staging = staging.output(change_out);
|
||||
}
|
||||
|
||||
staging = staging
|
||||
.mint_asset(policy_id, ref_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("mint ref: {e}")))?
|
||||
.mint_asset(policy_id, user_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("mint user: {e}")))?
|
||||
.script(ScriptKind::Native, script_cbor.clone())
|
||||
.disclosed_signer(payment_pkh)
|
||||
.fee(fee)
|
||||
.network_id(network_id);
|
||||
// Output 1: ref NFT @ ref_address, with inline datum.
|
||||
let ref_out = Output::new(ref_addr.clone(), ref_lovelace)
|
||||
.add_asset(policy_id, ref_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("ref add_asset: {e}")))?
|
||||
.set_inline_datum(datum_cbor.clone());
|
||||
staging = staging.output(ref_out);
|
||||
|
||||
Ok(staging)
|
||||
};
|
||||
// Output 2: user NFT @ user_address.
|
||||
let user_out = Output::new(user_addr.clone(), user_lovelace)
|
||||
.add_asset(policy_id, user_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("user add_asset: {e}")))?;
|
||||
staging = staging.output(user_out);
|
||||
|
||||
// Output 3 (optional): change @ wallet, with leftover input assets.
|
||||
let nonzero_change_assets: std::collections::BTreeMap<String, u64> = input_assets
|
||||
.iter()
|
||||
.filter(|(_, q)| **q > 0)
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
if change_lovelace > 0 || !nonzero_change_assets.is_empty() {
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &nonzero_change_assets {
|
||||
if k.len() < 56 {
|
||||
return Err(WalletError::Derivation(
|
||||
"change asset key shorter than 56 chars".into(),
|
||||
));
|
||||
}
|
||||
let p = parse_pkh(&k[..56])?;
|
||||
let n = parse_asset_name(&k[56..])?;
|
||||
change_out = change_out
|
||||
.add_asset(p, n, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
}
|
||||
staging = staging.output(change_out);
|
||||
}
|
||||
|
||||
staging = staging
|
||||
.mint_asset(policy_id, ref_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("mint ref: {e}")))?
|
||||
.mint_asset(policy_id, user_name.clone(), 1)
|
||||
.map_err(|e| WalletError::Derivation(format!("mint user: {e}")))?
|
||||
.script(ScriptKind::Native, script_cbor.clone())
|
||||
.disclosed_signer(payment_pkh)
|
||||
.fee(fee)
|
||||
.network_id(network_id);
|
||||
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
// Pass 1 — placeholder fee, measure unsigned size, recompute.
|
||||
let change_pass1 = total_in
|
||||
|
|
@ -803,25 +801,24 @@ pub fn build_signed_cip68_nft_mint(
|
|||
let real_fee = params.min_fee_for_size(est_signed);
|
||||
|
||||
let token_change = !input_assets.is_empty();
|
||||
let (final_fee, final_change) = match total_in
|
||||
.checked_sub(user_lovelace + ref_lovelace + real_fee)
|
||||
{
|
||||
Some(c) if c >= params.min_utxo_lovelace || token_change => {
|
||||
if token_change && c < params.min_utxo_lovelace {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient ADA for token-bearing change: change={c}, min={}",
|
||||
params.min_utxo_lovelace
|
||||
)));
|
||||
let (final_fee, final_change) =
|
||||
match total_in.checked_sub(user_lovelace + ref_lovelace + real_fee) {
|
||||
Some(c) if c >= params.min_utxo_lovelace || token_change => {
|
||||
if token_change && c < params.min_utxo_lovelace {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient ADA for token-bearing change: change={c}, min={}",
|
||||
params.min_utxo_lovelace
|
||||
)));
|
||||
}
|
||||
(real_fee, c)
|
||||
}
|
||||
(real_fee, c)
|
||||
}
|
||||
Some(c) => (real_fee + c, 0),
|
||||
None => {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} fee={real_fee}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
Some(c) => (real_fee + c, 0),
|
||||
None => {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} fee={real_fee}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let staging2 = build_with_fee(final_fee, final_change)?;
|
||||
let built = staging2
|
||||
|
|
@ -949,7 +946,11 @@ mod tests {
|
|||
&ProtocolParams::default(),
|
||||
)
|
||||
.expect("mint builds + signs");
|
||||
assert!(cbor.len() > 200, "mint cbor too short: {} bytes", cbor.len());
|
||||
assert!(
|
||||
cbor.len() > 200,
|
||||
"mint cbor too short: {} bytes",
|
||||
cbor.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1047,7 +1048,10 @@ mod tests {
|
|||
break;
|
||||
}
|
||||
}
|
||||
assert!(found_inline_datum, "ref NFT output must carry an inline datum");
|
||||
assert!(
|
||||
found_inline_datum,
|
||||
"ref NFT output must carry an inline datum"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1085,8 +1089,8 @@ mod tests {
|
|||
// Decode the resulting tx and confirm:
|
||||
// 1. aux_data is present
|
||||
// 2. body.auxiliary_data_hash is populated
|
||||
let tx = pallas_primitives::conway::Tx::decode_fragment(&cbor)
|
||||
.expect("decode signed mint cbor");
|
||||
let tx =
|
||||
pallas_primitives::conway::Tx::decode_fragment(&cbor).expect("decode signed mint cbor");
|
||||
assert!(
|
||||
tx.transaction_body.auxiliary_data_hash.is_some(),
|
||||
"aux_data_hash must be set when metadata is attached"
|
||||
|
|
|
|||
|
|
@ -75,8 +75,7 @@ impl PlutusVersion {
|
|||
pub const MIN_COLLATERAL_LOVELACE: u64 = 5_000_000;
|
||||
|
||||
fn parse_address(bech32: &str) -> Result<pallas_addresses::Address, WalletError> {
|
||||
pallas_addresses::Address::from_bech32(bech32)
|
||||
.map_err(|e| WalletError::Address(e.to_string()))
|
||||
pallas_addresses::Address::from_bech32(bech32).map_err(|e| WalletError::Address(e.to_string()))
|
||||
}
|
||||
|
||||
fn parse_tx_hash(hex_str: &str) -> Result<Hash<32>, WalletError> {
|
||||
|
|
@ -182,8 +181,7 @@ pub fn build_signed_plutus_spend(
|
|||
let funding = ada_only
|
||||
.iter()
|
||||
.find(|u| {
|
||||
!(u.tx_hash_hex == collateral.tx_hash_hex
|
||||
&& u.output_index == collateral.output_index)
|
||||
!(u.tx_hash_hex == collateral.tx_hash_hex && u.output_index == collateral.output_index)
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
|
|
@ -225,46 +223,43 @@ pub fn build_signed_plutus_spend(
|
|||
collateral.output_index as u64,
|
||||
);
|
||||
|
||||
let build_with_fee = |fee: u64,
|
||||
change_lovelace: u64|
|
||||
-> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
// PLUTUS-1: locked + funding as regular inputs (both consumed
|
||||
// on happy path); collateral as collateral_input only.
|
||||
staging = staging.input(locked_input.clone());
|
||||
staging = staging.input(funding_input.clone());
|
||||
staging = staging.collateral_input(collateral_input.clone());
|
||||
staging = staging.output(Output::new(payout_addr.clone(), payout_lovelace));
|
||||
if change_lovelace > 0 {
|
||||
staging = staging.output(Output::new(change_addr.clone(), change_lovelace));
|
||||
}
|
||||
staging = staging
|
||||
.script(script_version.to_script_kind(), script_cbor.to_vec())
|
||||
.add_spend_redeemer(
|
||||
locked_input.clone(),
|
||||
redeemer_cbor.to_vec(),
|
||||
Some(ex_units.into()),
|
||||
)
|
||||
.fee(fee)
|
||||
.network_id(network_id);
|
||||
if let Some(d) = witness_datum_cbor {
|
||||
staging = staging.datum(d.to_vec());
|
||||
}
|
||||
// PLUTUS-4 audit fix: pallas-txbuilder only computes
|
||||
// script_data_hash if language_view is set. Without it, the
|
||||
// body's hash is None and the chain rejects with
|
||||
// PPViewHashesDontMatch. PlutusV3 path requires a V3 cost
|
||||
// model — caller-supplied via ProtocolParams.
|
||||
if let Some(cost_model) = params.plutus_v3_cost_model.as_deref() {
|
||||
if matches!(script_version, PlutusVersion::V3) {
|
||||
staging = staging.language_view(
|
||||
script_version.to_script_kind(),
|
||||
cost_model.to_vec(),
|
||||
);
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
// PLUTUS-1: locked + funding as regular inputs (both consumed
|
||||
// on happy path); collateral as collateral_input only.
|
||||
staging = staging.input(locked_input.clone());
|
||||
staging = staging.input(funding_input.clone());
|
||||
staging = staging.collateral_input(collateral_input.clone());
|
||||
staging = staging.output(Output::new(payout_addr.clone(), payout_lovelace));
|
||||
if change_lovelace > 0 {
|
||||
staging = staging.output(Output::new(change_addr.clone(), change_lovelace));
|
||||
}
|
||||
}
|
||||
Ok(staging)
|
||||
};
|
||||
staging = staging
|
||||
.script(script_version.to_script_kind(), script_cbor.to_vec())
|
||||
.add_spend_redeemer(
|
||||
locked_input.clone(),
|
||||
redeemer_cbor.to_vec(),
|
||||
Some(ex_units.into()),
|
||||
)
|
||||
.fee(fee)
|
||||
.network_id(network_id);
|
||||
if let Some(d) = witness_datum_cbor {
|
||||
staging = staging.datum(d.to_vec());
|
||||
}
|
||||
// PLUTUS-4 audit fix: pallas-txbuilder only computes
|
||||
// script_data_hash if language_view is set. Without it, the
|
||||
// body's hash is None and the chain rejects with
|
||||
// PPViewHashesDontMatch. PlutusV3 path requires a V3 cost
|
||||
// model — caller-supplied via ProtocolParams.
|
||||
if let Some(cost_model) = params.plutus_v3_cost_model.as_deref() {
|
||||
if matches!(script_version, PlutusVersion::V3) {
|
||||
staging =
|
||||
staging.language_view(script_version.to_script_kind(), cost_model.to_vec());
|
||||
}
|
||||
}
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
// Pass 1 — placeholder fee, measure unsigned size.
|
||||
let change_pass1 = total_in
|
||||
|
|
@ -316,7 +311,10 @@ pub fn looks_like_script_address(addr_bech32: &str) -> bool {
|
|||
// is a script-payment + key-delegation address; types 2,
|
||||
// 3, 5, 7 also have script payment parts. Matches header
|
||||
// bits where bit 4 = 1 for script payment.
|
||||
bytes.first().map(|b| (b >> 4) & 0b0001 != 0).unwrap_or(false)
|
||||
bytes
|
||||
.first()
|
||||
.map(|b| (b >> 4) & 0b0001 != 0)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,66 +13,34 @@
|
|||
// Naming kept as `_PREPROD` for git churn reasons; treat as
|
||||
// "current Plutus V3 protocol parameters."
|
||||
pub const PLUTUS_V3_COST_MODEL_PREPROD: [i64; 297] = [
|
||||
100788, 420, 1, 1, 1000, 173, 0, 1,
|
||||
1000, 59957, 4, 1, 11183, 32, 201305, 8356,
|
||||
4, 16000, 100, 16000, 100, 16000, 100, 16000,
|
||||
100, 16000, 100, 16000, 100, 100, 100, 16000,
|
||||
100, 94375, 32, 132994, 32, 61462, 4, 72010,
|
||||
178, 0, 1, 22151, 32, 91189, 769, 4,
|
||||
2, 85848, 123203, 7305, -900, 1716, 549, 57,
|
||||
85848, 0, 1, 1, 1000, 42921, 4, 2,
|
||||
24548, 29498, 38, 1, 898148, 27279, 1, 51775,
|
||||
558, 1, 39184, 1000, 60594, 1, 141895, 32,
|
||||
83150, 32, 15299, 32, 76049, 1, 13169, 4,
|
||||
22100, 10, 28999, 74, 1, 28999, 74, 1,
|
||||
43285, 552, 1, 44749, 541, 1, 33852, 32,
|
||||
68246, 32, 72362, 32, 7243, 32, 7391, 32,
|
||||
11546, 32, 85848, 123203, 7305, -900, 1716, 549,
|
||||
57, 85848, 0, 1, 90434, 519, 0, 1,
|
||||
74433, 32, 85848, 123203, 7305, -900, 1716, 549,
|
||||
57, 85848, 0, 1, 1, 85848, 123203, 7305,
|
||||
-900, 1716, 549, 57, 85848, 0, 1, 955506,
|
||||
213312, 0, 2, 270652, 22588, 4, 1457325, 64566,
|
||||
4, 20467, 1, 4, 0, 141992, 32, 100788,
|
||||
420, 1, 1, 81663, 32, 59498, 32, 20142,
|
||||
32, 24588, 32, 20744, 32, 25933, 32, 24623,
|
||||
32, 43053543, 10, 53384111, 14333, 10, 43574283, 26308,
|
||||
10, 16000, 100, 16000, 100, 962335, 18, 2780678,
|
||||
6, 442008, 1, 52538055, 3756, 18, 267929, 18,
|
||||
76433006, 8868, 18, 52948122, 18, 1995836, 36, 3227919,
|
||||
12, 901022, 1, 166917843, 4307, 36, 284546, 36,
|
||||
158221314, 26549, 36, 74698472, 36, 333849714, 1, 254006273,
|
||||
72, 2174038, 72, 2261318, 64571, 4, 207616, 8310,
|
||||
4, 1293828, 28716, 63, 0, 1, 1006041, 43623,
|
||||
251, 0, 1, 100181, 726, 719, 0, 1,
|
||||
100181, 726, 719, 0, 1, 100181, 726, 719,
|
||||
0, 1, 107878, 680, 0, 1, 95336, 1,
|
||||
281145, 18848, 0, 1, 180194, 159, 1, 1,
|
||||
158519, 8942, 0, 1, 159378, 8813, 0, 1,
|
||||
107490, 3298, 1, 106057, 655, 1, 1964219, 24520,
|
||||
3,
|
||||
100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100,
|
||||
16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, 100, 16000, 100, 94375, 32,
|
||||
132994, 32, 61462, 4, 72010, 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, 123203, 7305, -900,
|
||||
1716, 549, 57, 85848, 0, 1, 1, 1000, 42921, 4, 2, 24548, 29498, 38, 1, 898148, 27279, 1, 51775,
|
||||
558, 1, 39184, 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10,
|
||||
28999, 74, 1, 28999, 74, 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, 32, 72362, 32,
|
||||
7243, 32, 7391, 32, 11546, 32, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 90434,
|
||||
519, 0, 1, 74433, 32, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, 85848, 123203,
|
||||
7305, -900, 1716, 549, 57, 85848, 0, 1, 955506, 213312, 0, 2, 270652, 22588, 4, 1457325, 64566,
|
||||
4, 20467, 1, 4, 0, 141992, 32, 100788, 420, 1, 1, 81663, 32, 59498, 32, 20142, 32, 24588, 32,
|
||||
20744, 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, 14333, 10, 43574283, 26308, 10, 16000,
|
||||
100, 16000, 100, 962335, 18, 2780678, 6, 442008, 1, 52538055, 3756, 18, 267929, 18, 76433006,
|
||||
8868, 18, 52948122, 18, 1995836, 36, 3227919, 12, 901022, 1, 166917843, 4307, 36, 284546, 36,
|
||||
158221314, 26549, 36, 74698472, 36, 333849714, 1, 254006273, 72, 2174038, 72, 2261318, 64571,
|
||||
4, 207616, 8310, 4, 1293828, 28716, 63, 0, 1, 1006041, 43623, 251, 0, 1, 100181, 726, 719, 0,
|
||||
1, 100181, 726, 719, 0, 1, 100181, 726, 719, 0, 1, 107878, 680, 0, 1, 95336, 1, 281145, 18848,
|
||||
0, 1, 180194, 159, 1, 1, 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, 1, 106057, 655,
|
||||
1, 1964219, 24520, 3,
|
||||
];
|
||||
pub const PLUTUS_V2_COST_MODEL_PREPROD: [i64; 175] = [
|
||||
100788, 420, 1, 1, 1000, 173, 0, 1,
|
||||
1000, 59957, 4, 1, 11183, 32, 201305, 8356,
|
||||
4, 16000, 100, 16000, 100, 16000, 100, 16000,
|
||||
100, 16000, 100, 16000, 100, 100, 100, 16000,
|
||||
100, 94375, 32, 132994, 32, 61462, 4, 72010,
|
||||
178, 0, 1, 22151, 32, 91189, 769, 4,
|
||||
2, 85848, 228465, 122, 0, 1, 1, 1000,
|
||||
42921, 4, 2, 24548, 29498, 38, 1, 898148,
|
||||
27279, 1, 51775, 558, 1, 39184, 1000, 60594,
|
||||
1, 141895, 32, 83150, 32, 15299, 32, 76049,
|
||||
1, 13169, 4, 22100, 10, 28999, 74, 1,
|
||||
28999, 74, 1, 43285, 552, 1, 44749, 541,
|
||||
1, 33852, 32, 68246, 32, 72362, 32, 7243,
|
||||
32, 7391, 32, 11546, 32, 85848, 228465, 122,
|
||||
0, 1, 1, 90434, 519, 0, 1, 74433,
|
||||
32, 85848, 228465, 122, 0, 1, 1, 85848,
|
||||
228465, 122, 0, 1, 1, 955506, 213312, 0,
|
||||
2, 270652, 22588, 4, 1457325, 64566, 4, 20467,
|
||||
1, 4, 0, 141992, 32, 100788, 420, 1,
|
||||
1, 81663, 32, 59498, 32, 20142, 32, 24588,
|
||||
32, 20744, 32, 25933, 32, 24623, 32, 43053543,
|
||||
10, 53384111, 14333, 10, 43574283, 26308, 10,
|
||||
100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100,
|
||||
16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, 100, 16000, 100, 94375, 32,
|
||||
132994, 32, 61462, 4, 72010, 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, 228465, 122, 0, 1,
|
||||
1, 1000, 42921, 4, 2, 24548, 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, 1000, 60594,
|
||||
1, 141895, 32, 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, 1,
|
||||
43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32,
|
||||
85848, 228465, 122, 0, 1, 1, 90434, 519, 0, 1, 74433, 32, 85848, 228465, 122, 0, 1, 1, 85848,
|
||||
228465, 122, 0, 1, 1, 955506, 213312, 0, 2, 270652, 22588, 4, 1457325, 64566, 4, 20467, 1, 4,
|
||||
0, 141992, 32, 100788, 420, 1, 1, 81663, 32, 59498, 32, 20142, 32, 24588, 32, 20744, 32, 25933,
|
||||
32, 24623, 32, 43053543, 10, 53384111, 14333, 10, 43574283, 26308, 10,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -250,14 +250,13 @@ pub fn build_unsigned_plutus_mint(
|
|||
params,
|
||||
)?;
|
||||
Ok(crate::tx::UnsignedPayment {
|
||||
cbor_hex: built
|
||||
.tx_bytes
|
||||
.0
|
||||
.iter()
|
||||
.fold(String::with_capacity(built.tx_bytes.0.len() * 2), |mut s, b| {
|
||||
cbor_hex: built.tx_bytes.0.iter().fold(
|
||||
String::with_capacity(built.tx_bytes.0.len() * 2),
|
||||
|mut s, b| {
|
||||
s.push_str(&format!("{:02x}", b));
|
||||
s
|
||||
}),
|
||||
},
|
||||
),
|
||||
summary,
|
||||
})
|
||||
}
|
||||
|
|
@ -380,11 +379,8 @@ fn prepare_plutus_mint(
|
|||
let mut held: std::collections::BTreeMap<String, u64> = Default::default();
|
||||
for u in &funding {
|
||||
for (k, v) in &u.assets {
|
||||
*held.entry(k.clone()).or_insert(0) = held
|
||||
.get(k)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(*v);
|
||||
*held.entry(k.clone()).or_insert(0) =
|
||||
held.get(k).copied().unwrap_or(0).saturating_add(*v);
|
||||
}
|
||||
}
|
||||
// Pull in UTxOs that contribute the needed assets.
|
||||
|
|
@ -406,11 +402,8 @@ fn prepare_plutus_mint(
|
|||
}
|
||||
if helps {
|
||||
for (k, v) in &u.assets {
|
||||
*held.entry(k.clone()).or_insert(0) = held
|
||||
.get(k)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(*v);
|
||||
*held.entry(k.clone()).or_insert(0) =
|
||||
held.get(k).copied().unwrap_or(0).saturating_add(*v);
|
||||
}
|
||||
funding.push(u.clone());
|
||||
}
|
||||
|
|
@ -465,11 +458,8 @@ fn prepare_plutus_mint(
|
|||
let mut input_assets: std::collections::BTreeMap<String, u64> = Default::default();
|
||||
for u in &funding {
|
||||
for (k, v) in &u.assets {
|
||||
*input_assets.entry(k.clone()).or_insert(0) = input_assets
|
||||
.get(k)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(*v);
|
||||
*input_assets.entry(k.clone()).or_insert(0) =
|
||||
input_assets.get(k).copied().unwrap_or(0).saturating_add(*v);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -502,11 +492,8 @@ fn prepare_plutus_mint(
|
|||
}
|
||||
}
|
||||
for (k, q) in &needed_extras {
|
||||
*dest_assets.entry(k.clone()).or_insert(0) = dest_assets
|
||||
.get(k)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.saturating_add(*q);
|
||||
*dest_assets.entry(k.clone()).or_insert(0) =
|
||||
dest_assets.get(k).copied().unwrap_or(0).saturating_add(*q);
|
||||
}
|
||||
|
||||
// Change assets = input_assets minus dest extras (mint doesn't
|
||||
|
|
@ -529,116 +516,122 @@ fn prepare_plutus_mint(
|
|||
let funding_inputs: Vec<Input> = funding
|
||||
.iter()
|
||||
.map(|u| -> Result<_, WalletError> {
|
||||
Ok(Input::new(parse_tx_hash(&u.tx_hash_hex)?, u.output_index as u64))
|
||||
Ok(Input::new(
|
||||
parse_tx_hash(&u.tx_hash_hex)?,
|
||||
u.output_index as u64,
|
||||
))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let build_with_fee = |fee: u64,
|
||||
change_lovelace: u64|
|
||||
-> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for inp in &funding_inputs {
|
||||
staging = staging.input(inp.clone());
|
||||
}
|
||||
staging = staging.collateral_input(collateral_input.clone());
|
||||
|
||||
// Dest output.
|
||||
let mut dest_out = Output::new(dest_addr.clone(), args.dest_lovelace);
|
||||
for (k, q) in &dest_assets {
|
||||
if *q == 0 {
|
||||
continue;
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for inp in &funding_inputs {
|
||||
staging = staging.input(inp.clone());
|
||||
}
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let p = parse_policy_id(pol_hex)?;
|
||||
let n = parse_asset_name(name_hex)?;
|
||||
dest_out = dest_out
|
||||
.add_asset(p, n, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("dest add_asset: {e}")))?;
|
||||
}
|
||||
if let Some(d) = args.dest_inline_datum_cbor {
|
||||
dest_out = dest_out.set_inline_datum(d.to_vec());
|
||||
}
|
||||
staging = staging.output(dest_out);
|
||||
staging = staging.collateral_input(collateral_input.clone());
|
||||
|
||||
// Change output (only if needed).
|
||||
let nonzero_change: std::collections::BTreeMap<String, u64> = change_assets
|
||||
.iter()
|
||||
.filter(|(_, q)| **q > 0)
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
if change_lovelace > 0 || !nonzero_change.is_empty() {
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &nonzero_change {
|
||||
// Dest output.
|
||||
let mut dest_out = Output::new(dest_addr.clone(), args.dest_lovelace);
|
||||
for (k, q) in &dest_assets {
|
||||
if *q == 0 {
|
||||
continue;
|
||||
}
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let p = parse_policy_id(pol_hex)?;
|
||||
let n = parse_asset_name(name_hex)?;
|
||||
change_out = change_out
|
||||
dest_out = dest_out
|
||||
.add_asset(p, n, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
.map_err(|e| WalletError::Derivation(format!("dest add_asset: {e}")))?;
|
||||
}
|
||||
staging = staging.output(change_out);
|
||||
}
|
||||
if let Some(d) = args.dest_inline_datum_cbor {
|
||||
dest_out = dest_out.set_inline_datum(d.to_vec());
|
||||
}
|
||||
staging = staging.output(dest_out);
|
||||
|
||||
// Mint each asset.
|
||||
for (name_bytes, qty) in &parsed_mint_assets {
|
||||
// Change output (only if needed).
|
||||
let nonzero_change: std::collections::BTreeMap<String, u64> = change_assets
|
||||
.iter()
|
||||
.filter(|(_, q)| **q > 0)
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
if change_lovelace > 0 || !nonzero_change.is_empty() {
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &nonzero_change {
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let p = parse_policy_id(pol_hex)?;
|
||||
let n = parse_asset_name(name_hex)?;
|
||||
change_out = change_out
|
||||
.add_asset(p, n, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
}
|
||||
staging = staging.output(change_out);
|
||||
}
|
||||
|
||||
// Mint each asset.
|
||||
for (name_bytes, qty) in &parsed_mint_assets {
|
||||
staging = staging
|
||||
.mint_asset(policy_hash, name_bytes.clone(), *qty)
|
||||
.map_err(|e| WalletError::Derivation(format!("mint_asset: {e}")))?;
|
||||
}
|
||||
|
||||
// Inline policy script witness + redeemer.
|
||||
let kind: ScriptKind = match args.policy_version {
|
||||
PlutusVersion::V1 => ScriptKind::PlutusV1,
|
||||
PlutusVersion::V2 => ScriptKind::PlutusV2,
|
||||
PlutusVersion::V3 => ScriptKind::PlutusV3,
|
||||
};
|
||||
staging = staging
|
||||
.mint_asset(policy_hash, name_bytes.clone(), *qty)
|
||||
.map_err(|e| WalletError::Derivation(format!("mint_asset: {e}")))?;
|
||||
}
|
||||
.script(kind, args.policy_cbor.to_vec())
|
||||
.add_mint_redeemer(
|
||||
policy_hash,
|
||||
args.redeemer_cbor.to_vec(),
|
||||
Some(args.ex_units.into()),
|
||||
)
|
||||
.fee(fee)
|
||||
.network_id(network_id);
|
||||
|
||||
// Inline policy script witness + redeemer.
|
||||
let kind: ScriptKind = match args.policy_version {
|
||||
PlutusVersion::V1 => ScriptKind::PlutusV1,
|
||||
PlutusVersion::V2 => ScriptKind::PlutusV2,
|
||||
PlutusVersion::V3 => ScriptKind::PlutusV3,
|
||||
};
|
||||
staging = staging
|
||||
.script(kind, args.policy_cbor.to_vec())
|
||||
.add_mint_redeemer(
|
||||
policy_hash,
|
||||
args.redeemer_cbor.to_vec(),
|
||||
Some(args.ex_units.into()),
|
||||
)
|
||||
.fee(fee)
|
||||
.network_id(network_id);
|
||||
|
||||
for pkh in args.additional_signers {
|
||||
staging = staging.disclosed_signer(*pkh);
|
||||
}
|
||||
|
||||
// Plutus V1/V2/V3 each need their cost-model wired via
|
||||
// language_view so pallas computes script_data_hash on the tx
|
||||
// body. Without it, chain rejects with PPViewHashesDontMatch.
|
||||
// Caught 2026-05-07 attempting Agora's V2 GST-policy bootstrap
|
||||
// mint on preprod — earlier code only set language_view for
|
||||
// V3 and every V2 mint hit the chain rejection.
|
||||
match args.policy_version {
|
||||
PlutusVersion::V2 => {
|
||||
staging = staging.language_view(
|
||||
kind,
|
||||
crate::plutus_cost_models::PLUTUS_V2_COST_MODEL_PREPROD.to_vec(),
|
||||
);
|
||||
for pkh in args.additional_signers {
|
||||
staging = staging.disclosed_signer(*pkh);
|
||||
}
|
||||
PlutusVersion::V3 => {
|
||||
if let Some(cost_model) = params.plutus_v3_cost_model.as_deref() {
|
||||
staging = staging.language_view(kind, cost_model.to_vec());
|
||||
|
||||
// Plutus V1/V2/V3 each need their cost-model wired via
|
||||
// language_view so pallas computes script_data_hash on the tx
|
||||
// body. Without it, chain rejects with PPViewHashesDontMatch.
|
||||
// Caught 2026-05-07 attempting Agora's V2 GST-policy bootstrap
|
||||
// mint on preprod — earlier code only set language_view for
|
||||
// V3 and every V2 mint hit the chain rejection.
|
||||
match args.policy_version {
|
||||
PlutusVersion::V2 => {
|
||||
staging = staging.language_view(
|
||||
kind,
|
||||
crate::plutus_cost_models::PLUTUS_V2_COST_MODEL_PREPROD.to_vec(),
|
||||
);
|
||||
}
|
||||
PlutusVersion::V3 => {
|
||||
if let Some(cost_model) = params.plutus_v3_cost_model.as_deref() {
|
||||
staging = staging.language_view(kind, cost_model.to_vec());
|
||||
}
|
||||
}
|
||||
PlutusVersion::V1 => {
|
||||
// V1 cost model not yet provided in aldabra-core. If a
|
||||
// V1 mint is ever needed, append PLUTUS_V1_COST_MODEL_PREPROD
|
||||
// to plutus_cost_models.rs and add the matching arm here.
|
||||
}
|
||||
}
|
||||
PlutusVersion::V1 => {
|
||||
// V1 cost model not yet provided in aldabra-core. If a
|
||||
// V1 mint is ever needed, append PLUTUS_V1_COST_MODEL_PREPROD
|
||||
// to plutus_cost_models.rs and add the matching arm here.
|
||||
}
|
||||
}
|
||||
|
||||
Ok(staging)
|
||||
};
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
// Pass 1.
|
||||
let token_change = !change_assets.values().all(|v| *v == 0);
|
||||
let need_change_min = if token_change { params.min_utxo_lovelace } else { 0 };
|
||||
let need_change_min = if token_change {
|
||||
params.min_utxo_lovelace
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let change_pass1 = total_in
|
||||
.checked_sub(args.dest_lovelace.saturating_add(fee_pass1))
|
||||
.filter(|c| *c >= need_change_min)
|
||||
|
|
|
|||
|
|
@ -34,10 +34,7 @@ use crate::{PaymentKey, WalletError};
|
|||
|
||||
/// Append a vkeywitness from the given payment key to an existing
|
||||
/// (unsigned or partially-signed) Conway tx. Returns the new CBOR.
|
||||
pub fn add_witness(
|
||||
payment_key: &PaymentKey,
|
||||
cbor_bytes: &[u8],
|
||||
) -> Result<Vec<u8>, WalletError> {
|
||||
pub fn add_witness(payment_key: &PaymentKey, cbor_bytes: &[u8]) -> Result<Vec<u8>, WalletError> {
|
||||
let mut tx = Tx::decode_fragment(cbor_bytes)
|
||||
.map_err(|e| WalletError::Derivation(format!("decode tx: {e}")))?;
|
||||
|
||||
|
|
@ -79,8 +76,8 @@ pub fn add_witness(
|
|||
witnesses.push(new_witness);
|
||||
tx.transaction_witness_set.vkeywitness = NonEmptySet::from_vec(witnesses);
|
||||
|
||||
let encoded = minicbor::to_vec(&tx)
|
||||
.map_err(|e| WalletError::Derivation(format!("encode tx: {e}")))?;
|
||||
let encoded =
|
||||
minicbor::to_vec(&tx).map_err(|e| WalletError::Derivation(format!("encode tx: {e}")))?;
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ pub fn parse_pool_id(bech32_str: &str) -> Result<Hash<28>, WalletError> {
|
|||
}
|
||||
|
||||
fn parse_address(bech32: &str) -> Result<pallas_addresses::Address, WalletError> {
|
||||
pallas_addresses::Address::from_bech32(bech32)
|
||||
.map_err(|e| WalletError::Address(e.to_string()))
|
||||
pallas_addresses::Address::from_bech32(bech32).map_err(|e| WalletError::Address(e.to_string()))
|
||||
}
|
||||
|
||||
fn parse_tx_hash(hex_str: &str) -> Result<Hash<32>, WalletError> {
|
||||
|
|
@ -156,50 +155,51 @@ pub fn build_signed_stake_delegation(
|
|||
}
|
||||
}
|
||||
|
||||
let build_with_fee = |fee: u64,
|
||||
change_lovelace: u64|
|
||||
-> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &input_assets {
|
||||
if *q == 0 {
|
||||
continue;
|
||||
let build_with_fee =
|
||||
|fee: u64, change_lovelace: u64| -> Result<StagingTransaction, WalletError> {
|
||||
let mut staging = StagingTransaction::new();
|
||||
for u in &selected {
|
||||
let h = parse_tx_hash(&u.tx_hash_hex)?;
|
||||
staging = staging.input(Input::new(h, u.output_index as u64));
|
||||
}
|
||||
if k.len() < 56 {
|
||||
return Err(WalletError::Derivation(
|
||||
"asset key shorter than 56 chars".into(),
|
||||
));
|
||||
let mut change_out = Output::new(change_addr.clone(), change_lovelace);
|
||||
for (k, q) in &input_assets {
|
||||
if *q == 0 {
|
||||
continue;
|
||||
}
|
||||
if k.len() < 56 {
|
||||
return Err(WalletError::Derivation(
|
||||
"asset key shorter than 56 chars".into(),
|
||||
));
|
||||
}
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let mut policy_bytes = [0u8; 28];
|
||||
for i in 0..28 {
|
||||
policy_bytes[i] =
|
||||
u8::from_str_radix(&pol_hex[i * 2..i * 2 + 2], 16).map_err(|_| {
|
||||
WalletError::Derivation("invalid policy hex in asset key".into())
|
||||
})?;
|
||||
}
|
||||
let policy = Hash::<28>::new(policy_bytes);
|
||||
let mut name_bytes = Vec::with_capacity(name_hex.len() / 2);
|
||||
for i in (0..name_hex.len()).step_by(2) {
|
||||
name_bytes.push(
|
||||
u8::from_str_radix(&name_hex[i..i + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid name hex".into()))?,
|
||||
);
|
||||
}
|
||||
change_out = change_out
|
||||
.add_asset(policy, name_bytes, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
}
|
||||
let pol_hex = &k[..56];
|
||||
let name_hex = &k[56..];
|
||||
let mut policy_bytes = [0u8; 28];
|
||||
for i in 0..28 {
|
||||
policy_bytes[i] = u8::from_str_radix(&pol_hex[i * 2..i * 2 + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid policy hex in asset key".into()))?;
|
||||
staging = staging.output(change_out);
|
||||
for cb in &cert_bytes_list {
|
||||
staging = staging.add_certificate(cb.clone());
|
||||
}
|
||||
let policy = Hash::<28>::new(policy_bytes);
|
||||
let mut name_bytes = Vec::with_capacity(name_hex.len() / 2);
|
||||
for i in (0..name_hex.len()).step_by(2) {
|
||||
name_bytes.push(
|
||||
u8::from_str_radix(&name_hex[i..i + 2], 16)
|
||||
.map_err(|_| WalletError::Derivation("invalid name hex".into()))?,
|
||||
);
|
||||
}
|
||||
change_out = change_out
|
||||
.add_asset(policy, name_bytes, *q)
|
||||
.map_err(|e| WalletError::Derivation(format!("change add_asset: {e}")))?;
|
||||
}
|
||||
staging = staging.output(change_out);
|
||||
for cb in &cert_bytes_list {
|
||||
staging = staging.add_certificate(cb.clone());
|
||||
}
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
staging = staging.fee(fee).network_id(network_id);
|
||||
Ok(staging)
|
||||
};
|
||||
|
||||
// Pass 1 — measure unsigned size.
|
||||
let change_pass1 = total_in
|
||||
|
|
@ -217,11 +217,11 @@ pub fn build_signed_stake_delegation(
|
|||
let real_fee = params.min_fee_for_size(est_signed);
|
||||
|
||||
let token_change = !input_assets.is_empty();
|
||||
let final_change = total_in
|
||||
.checked_sub(deposit + real_fee)
|
||||
.ok_or_else(|| WalletError::Derivation(format!(
|
||||
let final_change = total_in.checked_sub(deposit + real_fee).ok_or_else(|| {
|
||||
WalletError::Derivation(format!(
|
||||
"insufficient funds for fee: total_in={total_in} deposit={deposit} fee={real_fee}"
|
||||
)))?;
|
||||
))
|
||||
})?;
|
||||
if final_change < params.min_utxo_lovelace && token_change {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"insufficient ADA for token-bearing change: change={final_change}, min={}",
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@
|
|||
use ed25519_bip32::XPrv;
|
||||
use pallas_addresses::Address as PallasAddress;
|
||||
use pallas_crypto::key::ed25519::SecretKeyExtended;
|
||||
use pallas_txbuilder::{BuildConway, BuiltTransaction, Input, Output, ScriptKind, StagingTransaction};
|
||||
use pallas_txbuilder::{
|
||||
BuildConway, BuiltTransaction, Input, Output, ScriptKind, StagingTransaction,
|
||||
};
|
||||
|
||||
/// Reference-script attached to a tx output. Used to deploy Plutus
|
||||
/// validators / minting policies as reusable on-chain references so
|
||||
|
|
@ -488,16 +490,13 @@ pub struct UnsignedPayment {
|
|||
pub summary: PaymentSummary,
|
||||
}
|
||||
|
||||
fn build_unsigned_bytes(
|
||||
staging: StagingTransaction,
|
||||
) -> Result<Vec<u8>, WalletError> {
|
||||
fn build_unsigned_bytes(staging: StagingTransaction) -> Result<Vec<u8>, WalletError> {
|
||||
let built = staging
|
||||
.build_conway_raw()
|
||||
.map_err(|e| WalletError::Derivation(format!("conway build: {e}")))?;
|
||||
Ok(built.tx_bytes.0)
|
||||
}
|
||||
|
||||
|
||||
/// Internal helper — runs the two-pass fee refinement and returns
|
||||
/// the final `BuiltTransaction` plus a `PaymentSummary` describing
|
||||
/// the body. Handles both ADA-only and multi-asset payments; pass
|
||||
|
|
@ -1247,7 +1246,11 @@ mod tests {
|
|||
)
|
||||
.expect("multi-asset payment builds + signs");
|
||||
// Multi-asset tx is meaningfully larger than ADA-only.
|
||||
assert!(cbor.len() > 200, "cbor unexpectedly short: {} bytes", cbor.len());
|
||||
assert!(
|
||||
cbor.len() > 200,
|
||||
"cbor unexpectedly short: {} bytes",
|
||||
cbor.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue