#![allow(clippy::result_large_err)]
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::WalletError;
pub use crate::types::{coin_records_to_states, SuccessResponse, XchServerCoin};
use crate::types::{EveProof, LineageProof, Proof};
use crate::xch_server_coin::{urls_from_conditions, MirrorArgs, MirrorSolution, NewXchServerCoin};
use crate::{NetworkType, UnspentCoinStates};
use chia_bls::{sign, verify, PublicKey, SecretKey, Signature};
use chia_consensus::consensus_constants::ConsensusConstants;
use chia_consensus::flags::{DONT_VALIDATE_SIGNATURE, MEMPOOL_MODE};
use chia_consensus::owned_conditions::OwnedSpendBundleConditions;
use chia_consensus::run_block_generator::run_block_generator;
use chia_consensus::solution_generator::solution_generator;
use chia_protocol::{
Bytes, Bytes32, Coin, CoinSpend, CoinState, CoinStateFilters, RejectHeaderRequest,
RequestBlockHeader, RequestFeeEstimates, RespondBlockHeader, RespondFeeEstimates, SpendBundle,
TransactionAck,
};
use chia_puzzle_types::{
nft::NftMetadata,
standard::{StandardArgs, StandardSolution},
DeriveSynthetic,
};
use chia_puzzles::SINGLETON_LAUNCHER_HASH;
use chia_wallet_sdk::client::Peer;
use chia_wallet_sdk::driver::{
get_merkle_tree, Datastore, DatastoreMetadata, DelegatedPuzzle, Did, DidInfo, DriverError,
HashedPtr, IntermediateLauncher, Launcher, Layer, NftMint, OracleLayer, SpendContext,
SpendWithConditions, StandardLayer, WriterLayer,
};
use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature, SignerError};
use chia_wallet_sdk::types::{
announcement_id,
conditions::{CreateCoin, MeltSingleton, Memos, UpdateDatastoreMerkleRoot},
Condition, Conditions, MAINNET_CONSTANTS, TESTNET11_CONSTANTS,
};
use chia_wallet_sdk::utils::{self, CoinSelectionError};
use clvm_traits::{clvm_tuple, FromClvm, ToClvm};
use clvm_utils::tree_hash;
use clvmr::Allocator;
use hex_literal::hex;
pub const DATASTORE_LAUNCHER_HINT: Bytes32 = Bytes32::new(hex!(
"
aa7e5b234e1d55967bf0a316395a2eab6cb3370332c0f251f0e44a5afb84fc68
"
));
pub const DIG_ASSET_ID: Bytes32 = Bytes32::new(hex!(
"a406d3a9de984d03c9591c10d917593b434d5263cabe2b42f6b367df16832f81"
));
pub const MAX_CLVM_COST: u64 = 11_000_000_000;
pub async fn get_unspent_coin_states_by_hint(
peer: &Peer,
hint: Bytes32,
network_type: NetworkType,
) -> Result<UnspentCoinStates, WalletError> {
let header_hash = match network_type {
NetworkType::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
NetworkType::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
};
get_unspent_coin_states(peer, hint, None, header_hash, true).await
}
pub async fn get_unspent_coin_states(
peer: &Peer,
puzzle_hash: Bytes32,
previous_height: Option<u32>,
previous_header_hash: Bytes32,
allow_hints: bool,
) -> Result<UnspentCoinStates, WalletError> {
let mut coin_states = Vec::new();
let mut last_height = previous_height.unwrap_or_default();
let mut last_header_hash = previous_header_hash;
loop {
let response = peer
.request_puzzle_state(
vec![puzzle_hash],
if last_height == 0 {
None
} else {
Some(last_height)
},
last_header_hash,
CoinStateFilters {
include_spent: false,
include_unspent: true,
include_hinted: allow_hints,
min_amount: 1,
},
false,
)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectPuzzleState)?;
last_height = response.height;
last_header_hash = response.header_hash;
coin_states.extend(
response
.coin_states
.into_iter()
.filter(|cs| cs.spent_height.is_none()),
);
if response.is_finished {
break;
}
}
Ok(UnspentCoinStates {
coin_states,
last_height,
last_header_hash,
})
}
pub fn select_coins(coins: Vec<Coin>, total_amount: u64) -> Result<Vec<Coin>, CoinSelectionError> {
utils::select_coins(coins.into_iter().collect(), total_amount)
}
fn spend_coins_together(
ctx: &mut SpendContext,
synthetic_key: PublicKey,
coins: &[Coin],
extra_conditions: Conditions,
output: i64,
change_puzzle_hash: Bytes32,
) -> Result<(), WalletError> {
let p2 = StandardLayer::new(synthetic_key);
let change = i64::try_from(coins.iter().map(|coin| coin.amount).sum::<u64>()).unwrap() - output;
assert!(change >= 0);
let change = change as u64;
let first_coin_id = coins[0].coin_id();
for (i, &coin) in coins.iter().enumerate() {
if i == 0 {
let mut conditions = extra_conditions.clone();
if change > 0 {
conditions = conditions.create_coin(change_puzzle_hash, change, Memos::None);
}
p2.spend(ctx, coin, conditions)?;
} else {
p2.spend(
ctx,
coin,
Conditions::new().assert_concurrent_spend(first_coin_id),
)?;
}
}
Ok(())
}
pub fn send_xch(
synthetic_key: PublicKey,
coins: &[Coin],
outputs: &[(Bytes32, u64, Vec<Bytes>)],
fee: u64,
) -> Result<Vec<CoinSpend>, WalletError> {
let mut ctx = SpendContext::new();
let mut conditions = Conditions::new().reserve_fee(fee);
let mut total_amount = fee;
for output in outputs {
let memos = ctx.alloc(&output.2)?;
conditions = conditions.create_coin(output.0, output.1, Memos::Some(memos));
total_amount += output.1;
}
spend_coins_together(
&mut ctx,
synthetic_key,
coins,
conditions,
total_amount.try_into().unwrap(),
StandardArgs::curry_tree_hash(synthetic_key).into(),
)?;
Ok(ctx.take())
}
pub fn create_server_coin(
synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
hint: Bytes32,
uris: Vec<String>,
amount: u64,
fee: u64,
) -> Result<NewXchServerCoin, WalletError> {
let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
let mut memos = Vec::with_capacity(uris.len() + 1);
memos.push(hint.to_vec());
for url in &uris {
memos.push(url.as_bytes().to_vec());
}
let mut ctx = SpendContext::new();
let memos = ctx.alloc(&memos)?;
let conditions = Conditions::new()
.create_coin(
MirrorArgs::curry_tree_hash().into(),
amount,
Memos::Some(memos),
)
.reserve_fee(fee);
spend_coins_together(
&mut ctx,
synthetic_key,
&selected_coins,
conditions,
(amount + fee).try_into().unwrap(),
puzzle_hash,
)?;
let server_coin = XchServerCoin {
coin: Coin::new(
selected_coins[0].coin_id(),
MirrorArgs::curry_tree_hash().into(),
amount,
),
p2_puzzle_hash: puzzle_hash,
memo_urls: uris,
};
Ok(NewXchServerCoin {
coin_spends: ctx.take(),
server_coin,
})
}
pub async fn spend_xch_server_coins(
peer: &Peer,
synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
total_fee: u64,
network: TargetNetwork,
) -> Result<Vec<CoinSpend>, WalletError> {
let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
let mut fee_coins = Vec::new();
let mut server_coins = Vec::new();
for coin in selected_coins {
if coin.puzzle_hash == puzzle_hash {
fee_coins.push(coin);
} else {
server_coins.push(coin);
}
}
if server_coins.is_empty() {
return Ok(Vec::new());
}
assert!(!fee_coins.is_empty());
let parent_coins = peer
.request_coin_state(
server_coins.iter().map(|sc| sc.parent_coin_info).collect(),
None,
match network {
TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
},
false,
)
.await?
.map_err(|_| WalletError::RejectCoinState)?
.coin_states;
let mut ctx = SpendContext::new();
let puzzle_reveal = ctx.curry(MirrorArgs::default())?;
let mut conditions = Conditions::new().reserve_fee(total_fee);
let mut total_fee: i64 = total_fee.try_into().unwrap();
for server_coin in server_coins {
let parent_coin = parent_coins
.iter()
.find(|cs| cs.coin.coin_id() == server_coin.parent_coin_info)
.copied()
.ok_or(WalletError::UnknownCoin)?;
if parent_coin.coin.puzzle_hash != puzzle_hash {
return Err(WalletError::Permission);
}
let parent_inner_puzzle = ctx.curry(StandardArgs::new(synthetic_key))?;
let puzzle_reveal = ctx.serialize(&puzzle_reveal)?;
let solution = ctx.serialize(&MirrorSolution {
parent_parent_id: parent_coin.coin.parent_coin_info,
parent_inner_puzzle,
parent_amount: parent_coin.coin.amount,
parent_solution: StandardSolution {
original_public_key: None,
delegated_puzzle: (),
solution: (),
},
})?;
total_fee -= i64::try_from(server_coin.amount).unwrap();
ctx.insert(CoinSpend::new(server_coin, puzzle_reveal, solution));
conditions = conditions.assert_concurrent_spend(server_coin.coin_id());
}
spend_coins_together(
&mut ctx,
synthetic_key,
&fee_coins,
conditions,
total_fee,
puzzle_hash,
)?;
Ok(ctx.take())
}
pub async fn fetch_xch_server_coin(
peer: &Peer,
coin_state: CoinState,
max_cost: u64,
) -> Result<XchServerCoin, WalletError> {
let Some(created_height) = coin_state.created_height else {
return Err(WalletError::UnknownCoin);
};
let spend = peer
.request_puzzle_and_solution(coin_state.coin.parent_coin_info, created_height)
.await?
.map_err(|_| WalletError::RejectPuzzleSolution)?;
let mut allocator = Allocator::new();
let Ok(output) = spend
.puzzle
.run(&mut allocator, 0, max_cost, &spend.solution)
else {
return Err(WalletError::Clvm);
};
let Ok(conditions) = Vec::<Condition>::from_clvm(&allocator, output.1) else {
return Err(WalletError::Parse(
"Failed to get conditions from clvm allocator".to_string(),
));
};
let Some(urls) = urls_from_conditions(&allocator, &coin_state.coin, &conditions) else {
return Err(WalletError::Parse(
"Failed to get urls from conditions".to_string(),
));
};
let puzzle = spend
.puzzle
.to_clvm(&mut allocator)
.map_err(DriverError::ToClvm)?;
Ok(XchServerCoin {
coin: coin_state.coin,
p2_puzzle_hash: tree_hash(&allocator, puzzle).into(),
memo_urls: urls,
})
}
#[allow(clippy::too_many_arguments)]
pub fn mint_store(
minter_synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
root_hash: Bytes32,
label: Option<String>,
description: Option<String>,
bytes: Option<u64>,
size_proof: Option<String>,
owner_puzzle_hash: Bytes32,
delegated_puzzles: Vec<DelegatedPuzzle>,
fee: u64,
) -> Result<SuccessResponse, WalletError> {
let minter_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(minter_synthetic_key).into();
let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
let total_amount = fee + 1;
let mut ctx = SpendContext::new();
let p2 = StandardLayer::new(minter_synthetic_key);
let lead_coin = selected_coins[0];
let lead_coin_name = lead_coin.coin_id();
for coin in selected_coins.into_iter().skip(1) {
p2.spend(
&mut ctx,
coin,
Conditions::new().assert_concurrent_spend(lead_coin_name),
)?;
}
let (launch_singleton, datastore) = Launcher::new(lead_coin_name, 1).mint_datastore(
&mut ctx,
DatastoreMetadata {
root_hash,
label,
description,
bytes,
size_proof,
},
owner_puzzle_hash.into(),
delegated_puzzles,
)?;
let launch_singleton = Conditions::new().extend(
launch_singleton
.into_iter()
.map(|cond| {
if let Condition::CreateCoin(cc) = cond {
if cc.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
let hint = ctx.hint(DATASTORE_LAUNCHER_HINT)?;
return Ok(Condition::CreateCoin(CreateCoin {
puzzle_hash: cc.puzzle_hash,
amount: cc.amount,
memos: hint,
}));
}
return Ok(Condition::CreateCoin(cc));
}
Ok(cond)
})
.collect::<Result<Vec<_>, WalletError>>()?,
);
let lead_coin_conditions = if total_amount_from_coins > total_amount {
let hint = ctx.hint(minter_puzzle_hash)?;
launch_singleton.create_coin(
minter_puzzle_hash,
total_amount_from_coins - total_amount,
hint,
)
} else {
launch_singleton
};
p2.spend(&mut ctx, lead_coin, lead_coin_conditions)?;
Ok(SuccessResponse {
coin_spends: ctx.take(),
new_datastore: datastore,
})
}
pub struct SyncStoreResponse {
pub latest_store: Datastore,
pub latest_height: u32,
pub root_hash_history: Option<Vec<(Bytes32, u64)>>,
}
pub async fn sync_store(
peer: &Peer,
store: &Datastore,
last_height: Option<u32>,
last_header_hash: Bytes32,
with_history: bool,
) -> Result<SyncStoreResponse, WalletError> {
let mut latest_store = store.clone();
let mut history = vec![];
let response = peer
.request_coin_state(
vec![store.coin.coin_id()],
last_height,
last_header_hash,
false,
)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectCoinState)?;
let mut last_coin_record = response
.coin_states
.into_iter()
.next()
.ok_or(WalletError::UnknownCoin)?;
let mut ctx = SpendContext::new();
while last_coin_record.spent_height.is_some() {
let puzzle_and_solution_req = peer
.request_puzzle_and_solution(
last_coin_record.coin.coin_id(),
last_coin_record.spent_height.unwrap(),
)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectPuzzleSolution)?;
let cs = CoinSpend {
coin: last_coin_record.coin,
puzzle_reveal: puzzle_and_solution_req.puzzle,
solution: puzzle_and_solution_req.solution,
};
let new_store = Datastore::<DatastoreMetadata>::from_spend(
&mut ctx,
&cs,
&latest_store.info.delegated_puzzles,
)?
.ok_or(WalletError::Parse("Store from spend is None".to_string()))?;
if with_history {
let resp: Result<RespondBlockHeader, RejectHeaderRequest> = peer
.request_fallible(RequestBlockHeader {
height: last_coin_record.spent_height.unwrap(),
})
.await
.map_err(WalletError::Client)?;
let block_header = resp.map_err(|_| WalletError::RejectHeaderRequest)?;
history.push((
new_store.info.metadata.root_hash,
block_header
.header_block
.foliage_transaction_block
.unwrap()
.timestamp,
));
}
let response = peer
.request_coin_state(
vec![new_store.coin.coin_id()],
last_height,
last_header_hash,
false,
)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectCoinState)?;
last_coin_record = response
.coin_states
.into_iter()
.next()
.ok_or(WalletError::UnknownCoin)?;
latest_store = new_store;
}
Ok(SyncStoreResponse {
latest_store,
latest_height: last_coin_record
.created_height
.ok_or(WalletError::UnknownCoin)?,
root_hash_history: if with_history { Some(history) } else { None },
})
}
pub async fn sync_store_using_launcher_id(
peer: &Peer,
launcher_id: Bytes32,
last_height: Option<u32>,
last_header_hash: Bytes32,
with_history: bool,
) -> Result<SyncStoreResponse, WalletError> {
let response = peer
.request_coin_state(vec![launcher_id], last_height, last_header_hash, false)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectCoinState)?;
let last_coin_record = response
.coin_states
.into_iter()
.next()
.ok_or(WalletError::UnknownCoin)?;
let mut ctx = SpendContext::new();
let puzzle_and_solution_req = peer
.request_puzzle_and_solution(
last_coin_record.coin.coin_id(),
last_coin_record
.spent_height
.ok_or(WalletError::UnknownCoin)?,
)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectPuzzleSolution)?;
let cs = CoinSpend {
coin: last_coin_record.coin,
puzzle_reveal: puzzle_and_solution_req.puzzle,
solution: puzzle_and_solution_req.solution,
};
let first_store = Datastore::<DatastoreMetadata>::from_spend(&mut ctx, &cs, &[])?
.ok_or(WalletError::Parse("Store from spend is None".to_string()))?;
let res = sync_store(
peer,
&first_store,
last_height,
last_header_hash,
with_history,
)
.await?;
let root_hash_history = if let Some(mut res_root_hash_history) = res.root_hash_history {
let spent_timestamp = if let Some(spent_height) = last_coin_record.spent_height {
let resp: Result<RespondBlockHeader, RejectHeaderRequest> = peer
.request_fallible(RequestBlockHeader {
height: spent_height,
})
.await
.map_err(WalletError::Client)?;
let resp = resp.map_err(|_| WalletError::RejectHeaderRequest)?;
resp.header_block
.foliage_transaction_block
.unwrap()
.timestamp
} else {
0
};
res_root_hash_history.insert(0, (first_store.info.metadata.root_hash, spent_timestamp));
Some(res_root_hash_history)
} else {
None
};
Ok(SyncStoreResponse {
latest_store: res.latest_store,
latest_height: res.latest_height,
root_hash_history,
})
}
pub async fn get_store_creation_height(
peer: &Peer,
launcher_id: Bytes32,
last_height: Option<u32>,
last_header_hash: Bytes32,
) -> Result<u32, WalletError> {
let response = peer
.request_coin_state(vec![launcher_id], last_height, last_header_hash, false)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectCoinState)?;
let last_coin_record = response
.coin_states
.into_iter()
.next()
.ok_or(WalletError::UnknownCoin)?;
last_coin_record
.created_height
.ok_or(WalletError::UnknownCoin)
}
#[derive(Clone, Debug)]
pub enum DataStoreInnerSpend {
Owner(PublicKey),
Admin(PublicKey),
Writer(PublicKey),
}
fn update_store_with_conditions(
ctx: &mut SpendContext,
conditions: Conditions,
datastore: Datastore,
inner_spend_info: DataStoreInnerSpend,
allow_admin: bool,
allow_writer: bool,
) -> Result<SuccessResponse, WalletError> {
let inner_datastore_spend = match inner_spend_info {
DataStoreInnerSpend::Owner(pk) => {
StandardLayer::new(pk).spend_with_conditions(ctx, conditions)?
}
DataStoreInnerSpend::Admin(pk) => {
if !allow_admin {
return Err(WalletError::Permission);
}
StandardLayer::new(pk).spend_with_conditions(ctx, conditions)?
}
DataStoreInnerSpend::Writer(pk) => {
if !allow_writer {
return Err(WalletError::Permission);
}
WriterLayer::new(StandardLayer::new(pk)).spend(ctx, conditions)?
}
};
let parent_delegated_puzzles = datastore.info.delegated_puzzles.clone();
let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
let new_datastore =
Datastore::<DatastoreMetadata>::from_spend(ctx, &new_spend, &parent_delegated_puzzles)?
.ok_or(WalletError::Parse("Store from spend is None".to_string()))?;
Ok(SuccessResponse {
coin_spends: vec![new_spend],
new_datastore,
})
}
pub fn update_store_ownership(
datastore: Datastore,
new_owner_puzzle_hash: Bytes32,
new_delegated_puzzles: Vec<DelegatedPuzzle>,
inner_spend_info: DataStoreInnerSpend,
) -> Result<SuccessResponse, WalletError> {
let ctx = &mut SpendContext::new();
let update_condition: Condition = match inner_spend_info {
DataStoreInnerSpend::Owner(_) => {
Datastore::<DatastoreMetadata>::owner_create_coin_condition(
ctx,
datastore.info.launcher_id,
new_owner_puzzle_hash,
new_delegated_puzzles,
true,
)?
}
DataStoreInnerSpend::Admin(_) => {
let merkle_tree = get_merkle_tree(ctx, new_delegated_puzzles.clone())?;
let new_merkle_root_condition = UpdateDatastoreMerkleRoot {
new_merkle_root: merkle_tree.root(),
memos: Datastore::<DatastoreMetadata>::get_recreation_memos(
datastore.info.launcher_id,
new_owner_puzzle_hash.into(),
new_delegated_puzzles,
),
}
.to_clvm(&mut **ctx)
.map_err(DriverError::ToClvm)?;
Condition::Other(new_merkle_root_condition)
}
_ => return Err(WalletError::Permission),
};
let update_conditions = Conditions::new().with(update_condition);
update_store_with_conditions(
ctx,
update_conditions,
datastore,
inner_spend_info,
true,
false,
)
}
pub fn update_store_metadata(
datastore: Datastore,
new_root_hash: Bytes32,
new_label: Option<String>,
new_description: Option<String>,
new_bytes: Option<u64>,
new_size_proof: Option<String>,
inner_spend_info: DataStoreInnerSpend,
) -> Result<SuccessResponse, WalletError> {
let ctx = &mut SpendContext::new();
let new_metadata = DatastoreMetadata {
root_hash: new_root_hash,
label: new_label,
description: new_description,
bytes: new_bytes,
size_proof: new_size_proof,
};
let mut new_metadata_condition = Conditions::new().with(
Datastore::<DatastoreMetadata>::new_metadata_condition(ctx, new_metadata)?,
);
if let DataStoreInnerSpend::Owner(_) = inner_spend_info {
new_metadata_condition = new_metadata_condition.with(
Datastore::<DatastoreMetadata>::owner_create_coin_condition(
ctx,
datastore.info.launcher_id,
datastore.info.owner_puzzle_hash,
datastore.info.delegated_puzzles.clone(),
false,
)?,
);
}
update_store_with_conditions(
ctx,
new_metadata_condition,
datastore,
inner_spend_info,
true,
true,
)
}
pub fn melt_store(
datastore: Datastore,
owner_pk: PublicKey,
) -> Result<Vec<CoinSpend>, WalletError> {
let ctx = &mut SpendContext::new();
let melt_conditions = Conditions::new()
.with(Condition::reserve_fee(1))
.with(Condition::Other(
MeltSingleton {}
.to_clvm(&mut **ctx)
.map_err(DriverError::ToClvm)?,
));
let inner_datastore_spend =
StandardLayer::new(owner_pk).spend_with_conditions(ctx, melt_conditions)?;
let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
Ok(vec![new_spend])
}
pub fn oracle_spend(
spender_synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
datastore: Datastore,
fee: u64,
) -> Result<SuccessResponse, WalletError> {
let Some(DelegatedPuzzle::Oracle(oracle_ph, oracle_fee)) = datastore
.info
.delegated_puzzles
.iter()
.find(|dp| matches!(dp, DelegatedPuzzle::Oracle(_, _)))
else {
return Err(WalletError::Permission);
};
let spender_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(spender_synthetic_key).into();
let total_amount = oracle_fee + fee;
let ctx = &mut SpendContext::new();
let p2 = StandardLayer::new(spender_synthetic_key);
let lead_coin = selected_coins[0];
let lead_coin_name = lead_coin.coin_id();
let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
for coin in selected_coins.into_iter().skip(1) {
p2.spend(
ctx,
coin,
Conditions::new().assert_concurrent_spend(lead_coin_name),
)?;
}
let assert_oracle_conds = Conditions::new().assert_puzzle_announcement(announcement_id(
datastore.coin.puzzle_hash,
Bytes::new("$".into()),
));
let mut lead_coin_conditions = assert_oracle_conds;
if total_amount_from_coins > total_amount {
let hint = ctx.hint(spender_puzzle_hash)?;
lead_coin_conditions = lead_coin_conditions.create_coin(
spender_puzzle_hash,
total_amount_from_coins - total_amount,
hint,
);
}
if fee > 0 {
lead_coin_conditions = lead_coin_conditions.reserve_fee(fee);
}
p2.spend(ctx, lead_coin, lead_coin_conditions)?;
let inner_datastore_spend = OracleLayer::new(*oracle_ph, *oracle_fee)
.ok_or(DriverError::OddOracleFee)?
.construct_spend(ctx, ())?;
let parent_delegated_puzzles = datastore.info.delegated_puzzles.clone();
let new_spend = datastore.spend(ctx, inner_datastore_spend)?;
let new_datastore = Datastore::from_spend(ctx, &new_spend, &parent_delegated_puzzles)?
.ok_or(WalletError::Parse("Store from spend is None".to_string()))?;
ctx.insert(new_spend.clone());
Ok(SuccessResponse {
coin_spends: ctx.take(),
new_datastore,
})
}
pub fn add_fee(
spender_synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
coin_ids: Vec<Bytes32>,
fee: u64,
) -> Result<Vec<CoinSpend>, WalletError> {
let spender_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(spender_synthetic_key).into();
let total_amount_from_coins = selected_coins.iter().map(|c| c.amount).sum::<u64>();
let mut ctx = SpendContext::new();
let p2 = StandardLayer::new(spender_synthetic_key);
let lead_coin = selected_coins[0];
let lead_coin_name = lead_coin.coin_id();
for coin in selected_coins.into_iter().skip(1) {
p2.spend(
&mut ctx,
coin,
Conditions::new().assert_concurrent_spend(lead_coin_name),
)?;
}
let mut lead_coin_conditions = Conditions::new().reserve_fee(fee);
if total_amount_from_coins > fee {
let hint = ctx.hint(spender_puzzle_hash)?;
lead_coin_conditions = lead_coin_conditions.create_coin(
spender_puzzle_hash,
total_amount_from_coins - fee,
hint,
);
}
for coin_id in coin_ids {
lead_coin_conditions = lead_coin_conditions.assert_concurrent_spend(coin_id);
}
p2.spend(&mut ctx, lead_coin, lead_coin_conditions)?;
Ok(ctx.take())
}
pub fn public_key_to_synthetic_key(pk: PublicKey) -> PublicKey {
pk.derive_synthetic()
}
pub fn secret_key_to_synthetic_key(sk: SecretKey) -> SecretKey {
sk.derive_synthetic()
}
#[derive(Debug, Clone, Copy)]
pub enum TargetNetwork {
Mainnet,
Testnet11,
}
impl TargetNetwork {
fn get_constants(&self) -> &ConsensusConstants {
match self {
TargetNetwork::Mainnet => &MAINNET_CONSTANTS,
TargetNetwork::Testnet11 => &TESTNET11_CONSTANTS,
}
}
}
pub fn sign_coin_spends(
coin_spends: Vec<CoinSpend>,
private_keys: Vec<SecretKey>,
network: TargetNetwork,
) -> Result<Signature, SignerError> {
let mut allocator = Allocator::new();
let required_signatures = RequiredSignature::from_coin_spends(
&mut allocator,
&coin_spends,
&AggSigConstants::new(network.get_constants().agg_sig_me_additional_data),
)?;
let key_pairs = private_keys
.iter()
.map(|sk| {
(
sk.public_key(),
sk.clone(),
sk.public_key().derive_synthetic(),
sk.derive_synthetic(),
)
})
.flat_map(|(pk1, sk1, pk2, sk2)| vec![(pk1, sk1), (pk2, sk2)])
.collect::<HashMap<PublicKey, SecretKey>>();
let mut sig = Signature::default();
for required in required_signatures {
let RequiredSignature::Bls(required) = required else {
continue;
};
let sk = key_pairs.get(&required.public_key);
if let Some(sk) = sk {
sig += &sign(sk, required.message());
}
}
Ok(sig)
}
pub async fn broadcast_spend_bundle(
peer: &Peer,
spend_bundle: SpendBundle,
) -> Result<TransactionAck, WalletError> {
peer.send_transaction(spend_bundle)
.await
.map_err(WalletError::Client)
}
pub async fn get_header_hash(peer: &Peer, height: u32) -> Result<Bytes32, WalletError> {
let resp: Result<RespondBlockHeader, RejectHeaderRequest> = peer
.request_fallible(RequestBlockHeader { height })
.await
.map_err(WalletError::Client)?;
resp.map_err(|_| WalletError::RejectHeaderRequest)
.map(|resp| resp.header_block.header_hash())
}
pub async fn get_fee_estimate(peer: &Peer, target_time_seconds: u64) -> Result<u64, WalletError> {
let target_time_seconds = target_time_seconds
+ SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let resp: RespondFeeEstimates = peer
.request_infallible(RequestFeeEstimates {
time_targets: vec![target_time_seconds],
})
.await
.map_err(WalletError::Client)?;
let fee_estimate_group = resp.estimates;
if let Some(error_message) = fee_estimate_group.error {
return Err(WalletError::FeeEstimateRejection(error_message));
}
if let Some(first_estimate) = fee_estimate_group.estimates.first() {
if let Some(error_message) = &first_estimate.error {
return Err(WalletError::FeeEstimateRejection(error_message.clone()));
}
return Ok(first_estimate.estimated_fee_rate.mojos_per_clvm_cost);
}
Err(WalletError::FeeEstimateRejection(
"No fee estimates available".to_string(),
))
}
pub async fn is_coin_spent(
peer: &Peer,
coin_id: Bytes32,
last_height: Option<u32>,
last_header_hash: Bytes32,
) -> Result<bool, WalletError> {
let response = peer
.request_coin_state(vec![coin_id], last_height, last_header_hash, false)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectCoinState)?;
if let Some(coin_state) = response.coin_states.first() {
return Ok(coin_state.spent_height.is_some());
}
Ok(false)
}
pub fn make_message(msg: Bytes) -> Result<Bytes32, WalletError> {
let mut alloc = Allocator::new();
let thing_ptr = clvm_tuple!("Chia Signed Message", msg)
.to_clvm(&mut alloc)
.map_err(DriverError::ToClvm)?;
Ok(tree_hash(&alloc, thing_ptr).into())
}
pub fn sign_message(message: Bytes, sk: SecretKey) -> Result<Signature, WalletError> {
Ok(sign(&sk, make_message(message)?))
}
pub fn verify_signature(
message: Bytes,
pk: PublicKey,
sig: Signature,
) -> Result<bool, WalletError> {
Ok(verify(&sig, &pk, make_message(message)?))
}
pub fn get_cost(coin_spends: Vec<CoinSpend>) -> Result<u64, WalletError> {
let mut alloc = Allocator::new();
let generator = solution_generator(
coin_spends
.into_iter()
.map(|cs| (cs.coin, cs.puzzle_reveal, cs.solution)),
)?;
let conds = run_block_generator::<&[u8], _>(
&mut alloc,
&generator,
[],
u64::MAX,
MEMPOOL_MODE | DONT_VALIDATE_SIGNATURE,
&Signature::default(),
None,
TargetNetwork::Mainnet.get_constants(),
)?;
let conds = OwnedSpendBundleConditions::from(&alloc, conds);
Ok(conds.cost)
}
pub struct PossibleLaunchersResponse {
pub launcher_ids: Vec<Bytes32>,
pub last_height: u32,
pub last_header_hash: Bytes32,
}
pub async fn look_up_possible_launchers(
peer: &Peer,
previous_height: Option<u32>,
previous_header_hash: Bytes32,
) -> Result<PossibleLaunchersResponse, WalletError> {
let resp = get_unspent_coin_states(
peer,
DATASTORE_LAUNCHER_HINT,
previous_height,
previous_header_hash,
true,
)
.await?;
Ok(PossibleLaunchersResponse {
last_header_hash: resp.last_header_hash,
last_height: resp.last_height,
launcher_ids: resp
.coin_states
.into_iter()
.filter_map(|coin_state| {
if coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
Some(coin_state.coin.coin_id())
} else {
None
}
})
.collect(),
})
}
pub async fn subscribe_to_coin_states(
peer: &Peer,
coin_id: Bytes32,
previous_height: Option<u32>,
previous_header_hash: Bytes32,
) -> Result<Option<u32>, WalletError> {
let response = peer
.request_coin_state(vec![coin_id], previous_height, previous_header_hash, true)
.await
.map_err(WalletError::Client)?
.map_err(|_| WalletError::RejectCoinState)?;
if let Some(coin_state) = response.coin_states.first() {
return Ok(coin_state.spent_height);
}
Err(WalletError::UnknownCoin)
}
pub async fn unsubscribe_from_coin_states(
peer: &Peer,
coin_id: Bytes32,
) -> Result<(), WalletError> {
peer.remove_coin_subscriptions(Some(vec![coin_id]))
.await
.map_err(WalletError::Client)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn mint_nft(
peer: &Peer,
synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
did_string: &str,
recipient_puzzle_hash: Bytes32,
metadata: NftMetadata,
_royalty_puzzle_hash: Option<Bytes32>,
royalty_basis_points: u16,
fee: u64,
network: TargetNetwork,
) -> Result<Vec<CoinSpend>, WalletError> {
let (did_proof, did_coin) =
resolve_did_string_and_generate_proof(peer, did_string, network).await?;
let mut ctx = SpendContext::new();
let did_proof = match did_proof {
chia_puzzle_types::Proof::Eve(eve) => Proof::Eve(EveProof {
parent_parent_coin_info: eve.parent_parent_coin_info,
parent_amount: eve.parent_amount,
}),
chia_puzzle_types::Proof::Lineage(lineage) => Proof::Lineage(LineageProof {
parent_parent_coin_info: lineage.parent_parent_coin_info,
parent_inner_puzzle_hash: lineage.parent_inner_puzzle_hash,
parent_amount: lineage.parent_amount,
}),
};
let public_key_bytes = synthetic_key.derive_synthetic().to_bytes();
let mut public_key_hash = [0u8; 32];
public_key_hash.copy_from_slice(&public_key_bytes[..32]);
let mut meta_data_allocator = Allocator::new();
let node_metadata = metadata.to_clvm(&mut meta_data_allocator)?;
let metadata_hashed_ptr = HashedPtr::from_ptr(&meta_data_allocator, node_metadata);
let did_info: DidInfo = DidInfo::new(
did_coin.coin_id(),
None,
1,
metadata_hashed_ptr,
public_key_hash.into(),
);
let did = Did::new(did_coin, did_proof, did_info);
let p2 = StandardLayer::new(synthetic_key);
let nft_mint = NftMint::new(
metadata_hashed_ptr,
recipient_puzzle_hash,
royalty_basis_points,
None, );
let (mint_conditions, _nft) = IntermediateLauncher::new(did_coin.coin_id(), 0, 1)
.create(&mut ctx)?
.mint_nft(&mut ctx, &nft_mint)?;
let _updated_did = did.update(&mut ctx, &p2, mint_conditions)?;
let total_input = selected_coins.iter().map(|coin| coin.amount).sum::<u64>();
let total_needed = fee + 1;
if total_input < total_needed {
return Err(WalletError::InsufficientCoinAmount); }
let _change = total_input - total_needed;
let change_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
spend_coins_together(
&mut ctx,
synthetic_key,
&selected_coins,
Conditions::new().reserve_fee(fee),
total_needed as i64,
change_puzzle_hash,
)?;
Ok(ctx.take())
}
pub async fn generate_did_proof(
peer: &Peer,
did_coin: Coin,
network: TargetNetwork,
) -> Result<(chia_puzzle_types::Proof, Coin), WalletError> {
let proof = generate_did_proof_from_chain(peer, did_coin, network).await?;
Ok((proof, did_coin))
}
pub fn generate_did_proof_manual(
did_coin: Coin,
parent_coin: Option<Coin>,
parent_inner_puzzle_hash: Option<Bytes32>,
) -> Result<chia_puzzle_types::Proof, WalletError> {
match parent_coin {
None => {
Ok(chia_puzzle_types::Proof::Eve(chia_puzzle_types::EveProof {
parent_parent_coin_info: did_coin.parent_coin_info,
parent_amount: 1, }))
}
Some(parent) => {
let parent_inner_puzzle_hash = parent_inner_puzzle_hash.ok_or(WalletError::Parse(
"Parent inner puzzle hash is required".to_string(),
))?;
Ok(chia_puzzle_types::Proof::Lineage(
chia_puzzle_types::LineageProof {
parent_parent_coin_info: parent.parent_coin_info,
parent_inner_puzzle_hash,
parent_amount: parent.amount,
},
))
}
}
}
pub async fn generate_did_proof_from_chain(
peer: &Peer,
did_coin: Coin,
network: TargetNetwork,
) -> Result<chia_puzzle_types::Proof, WalletError> {
let parent_coin_states = peer
.request_coin_state(
vec![did_coin.parent_coin_info],
None,
match network {
TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
},
false,
)
.await?
.map_err(|_| WalletError::RejectCoinState)?
.coin_states;
let parent_coin_state = parent_coin_states.first().ok_or(WalletError::UnknownCoin)?;
if parent_coin_state.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
return Ok(chia_puzzle_types::Proof::Eve(chia_puzzle_types::EveProof {
parent_parent_coin_info: parent_coin_state.coin.parent_coin_info,
parent_amount: parent_coin_state.coin.amount,
}));
}
let parent_spend_height = parent_coin_state
.spent_height
.ok_or(WalletError::UnknownCoin)?;
let _parent_spend = peer
.request_puzzle_and_solution(parent_coin_state.coin.coin_id(), parent_spend_height)
.await?
.map_err(|_| WalletError::RejectPuzzleSolution)?;
let _allocator = Allocator::new();
Ok(chia_puzzle_types::Proof::Lineage(
chia_puzzle_types::LineageProof {
parent_parent_coin_info: parent_coin_state.coin.parent_coin_info,
parent_inner_puzzle_hash: Bytes32::default(), parent_amount: parent_coin_state.coin.amount,
},
))
}
pub fn create_simple_did(
synthetic_key: PublicKey,
selected_coins: Vec<Coin>,
fee: u64,
) -> Result<(Vec<CoinSpend>, Coin), WalletError> {
let mut ctx = SpendContext::new();
let p2 = StandardLayer::new(synthetic_key);
let puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into();
let total_input = selected_coins.iter().map(|coin| coin.amount).sum::<u64>();
let total_needed = fee + 1;
if total_input < total_needed {
return Err(WalletError::InsufficientCoinAmount); }
let change = total_input - total_needed;
let first_coin = selected_coins[0];
let launcher = Launcher::new(first_coin.coin_id(), 1);
let (create_did_conditions, did) = launcher.create_simple_did(&mut ctx, &p2)?;
let first_coin_id = first_coin.coin_id();
for (i, &coin) in selected_coins.iter().enumerate() {
if i == 0 {
let mut conditions = create_did_conditions.clone();
if change > 0 {
let hint = ctx.hint(puzzle_hash)?;
conditions = conditions.create_coin(puzzle_hash, change, hint);
}
if fee > 0 {
conditions = conditions.reserve_fee(fee);
}
p2.spend(&mut ctx, coin, conditions)?;
} else {
p2.spend(
&mut ctx,
coin,
Conditions::new().assert_concurrent_spend(first_coin_id),
)?;
}
}
Ok((ctx.take(), did.coin))
}
pub async fn resolve_did_string_and_generate_proof(
peer: &Peer,
did_string: &str,
network: TargetNetwork,
) -> Result<(chia_puzzle_types::Proof, Coin), WalletError> {
let parts: Vec<&str> = did_string.split(':').collect();
if parts.len() != 3 || parts[0] != "did" || parts[1] != "chia" {
return Err(WalletError::Parse("Invalid DID string".to_string()));
}
let bech32_part = parts[2];
use chia_wallet_sdk::utils::Address;
let address = Address::decode(bech32_part)
.map_err(|_| WalletError::Parse("Cannot decode address".to_string()))?;
let did_id = address.puzzle_hash;
let launcher_states = peer
.request_coin_state(
vec![did_id],
None,
match network {
TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
},
false,
)
.await?
.map_err(|_| WalletError::RejectCoinState)?
.coin_states;
let launcher_state = launcher_states.first().ok_or(WalletError::UnknownCoin)?;
if launcher_state.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH.into() {
return Err(WalletError::PuzzleHashMismatch(
"Coin puzzle hash does not match datastore singleton launcher hash".to_string(),
));
}
let launcher_spend_height = launcher_state
.spent_height
.ok_or(WalletError::UnknownCoin)?;
let launcher_spend = peer
.request_puzzle_and_solution(launcher_state.coin.coin_id(), launcher_spend_height)
.await?
.map_err(|_| WalletError::RejectPuzzleSolution)?;
let mut allocator = Allocator::new();
let launcher_puzzle = launcher_spend.puzzle.to_clvm(&mut allocator)?;
let launcher_solution = launcher_spend.solution.to_clvm(&mut allocator)?;
let output = clvmr::run_program(
&mut allocator,
&clvmr::ChiaDialect::new(0),
launcher_puzzle,
launcher_solution,
u64::MAX,
)
.map_err(|_| WalletError::Clvm)?;
let conditions =
Vec::<Condition>::from_clvm(&allocator, output.1).map_err(|_| WalletError::Clvm)?;
let mut first_did_coin: Option<Coin> = None;
for condition in conditions {
if let Some(create_coin) = condition.into_create_coin() {
if create_coin.amount % 2 == 1 {
first_did_coin = Some(Coin::new(
launcher_state.coin.coin_id(),
create_coin.puzzle_hash,
create_coin.amount,
));
break;
}
}
}
let first_did_coin = first_did_coin.ok_or(WalletError::UnknownCoin)?;
let mut current_did_coin = first_did_coin;
loop {
let coin_states = peer
.request_coin_state(
vec![current_did_coin.coin_id()],
None,
match network {
TargetNetwork::Mainnet => MAINNET_CONSTANTS.genesis_challenge,
TargetNetwork::Testnet11 => TESTNET11_CONSTANTS.genesis_challenge,
},
false,
)
.await?
.map_err(|_| WalletError::RejectCoinState)?
.coin_states;
let coin_state = coin_states.first().ok_or(WalletError::UnknownCoin)?;
if coin_state.spent_height.is_none() {
break;
}
let spend_height = coin_state.spent_height.unwrap();
let spend = peer
.request_puzzle_and_solution(current_did_coin.coin_id(), spend_height)
.await?
.map_err(|_| WalletError::RejectPuzzleSolution)?;
let spend_puzzle = spend.puzzle.to_clvm(&mut allocator)?;
let spend_solution = spend.solution.to_clvm(&mut allocator)?;
let spend_output = clvmr::run_program(
&mut allocator,
&clvmr::ChiaDialect::new(0),
spend_puzzle,
spend_solution,
u64::MAX,
)
.map_err(|_| WalletError::Clvm)?;
let spend_conditions = Vec::<Condition>::from_clvm(&allocator, spend_output.1)
.map_err(|_| WalletError::Clvm)?;
let mut child_did_coin: Option<Coin> = None;
for condition in spend_conditions {
if let Some(create_coin) = condition.into_create_coin() {
if create_coin.amount % 2 == 1 {
child_did_coin = Some(Coin::new(
current_did_coin.coin_id(),
create_coin.puzzle_hash,
create_coin.amount,
));
break;
}
}
}
current_did_coin = child_did_coin.ok_or(WalletError::UnknownCoin)?;
}
let proof = generate_did_proof_from_chain(peer, current_did_coin, network).await?;
Ok((proof, current_did_coin))
}
#[cfg(test)]
mod melt_kat {
use super::*;
use chia_wallet_sdk::test::{BlsPair, Simulator};
#[test]
fn from_spend_reports_owner_melt_as_missing_child() -> anyhow::Result<()> {
let mut sim = Simulator::new();
let owner = BlsPair::default();
let owner_puzzle_hash: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
let funding_coin = sim.new_coin(owner_puzzle_hash, 1);
let minted = mint_store(
owner.pk,
vec![funding_coin],
Bytes32::new([1; 32]),
None,
None,
None,
None,
owner_puzzle_hash,
vec![],
0,
)?;
let datastore = minted.new_datastore.clone();
sim.spend_coins(minted.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
let mut ctx = SpendContext::new();
let launcher_spend = minted
.coin_spends
.iter()
.find(|cs| cs.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into())
.expect("mint must contain the singleton launcher spend");
let launched = Datastore::<DatastoreMetadata>::from_spend(&mut ctx, launcher_spend, &[])?;
assert!(
launched.is_some(),
"from_spend must recognise the datastore-creating launcher spend as Ok(Some)"
);
let melt_spends = melt_store(datastore, owner.pk)?;
assert_eq!(melt_spends.len(), 1, "melt produces exactly one spend");
sim.spend_coins(melt_spends.clone(), std::slice::from_ref(&owner.sk))?;
let mut ctx = SpendContext::new();
let result = Datastore::<DatastoreMetadata>::from_spend(&mut ctx, &melt_spends[0], &[]);
assert!(
matches!(result, Err(DriverError::MissingChild)),
"0.36 must still surface an owner melt as Err(DriverError::MissingChild), got {result:?}"
);
Ok(())
}
}