use crate::wallets::common::{sign_coin_spends, DerivationRecord};
use async_trait::async_trait;
use blst::min_pk::SecretKey;
use dashmap::mapref::one::Ref;
use dashmap::DashMap;
use dg_xch_core::blockchain::announcement::Announcement;
use dg_xch_core::blockchain::coin::Coin;
use dg_xch_core::blockchain::coin_record::{CatCoinRecord, CoinRecord};
use dg_xch_core::blockchain::coin_spend::CoinSpend;
use dg_xch_core::blockchain::condition_opcode::ConditionOpcode;
use dg_xch_core::blockchain::sized_bytes::{Bytes32, Bytes48};
use dg_xch_core::blockchain::spend_bundle::SpendBundle;
use dg_xch_core::blockchain::transaction_record::{TransactionRecord, TransactionType};
use dg_xch_core::blockchain::wallet_type::{AmountWithPuzzleHash, WalletType};
use dg_xch_core::clvm::program::{Program, SerializedProgram};
use dg_xch_core::clvm::utils::INFINITE_COST;
use dg_xch_core::consensus::constants::ConsensusConstants;
use dg_xch_core::traits::SizedBytes;
use dg_xch_core::utils::hash_256;
use dg_xch_keys::{master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened};
use dg_xch_puzzles::p2_delegated_puzzle_or_hidden_puzzle::{
puzzle_for_pk, puzzle_hash_for_pk, solution_for_conditions,
};
use dg_xch_puzzles::utils::{
make_assert_absolute_seconds_exceeds_condition, make_assert_coin_announcement,
make_assert_puzzle_announcement, make_create_coin_announcement, make_create_coin_condition,
make_create_puzzle_announcement, make_reserve_fee_condition,
};
use dg_xch_serialize::{ChiaProtocolVersion, ChiaSerialize};
use log::{debug, info};
use num_traits::ToPrimitive;
use rand::prelude::StdRng;
use rand::{Rng, SeedableRng};
use std::cmp::max;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex;
pub mod common;
pub mod memory_wallet;
pub mod plotnft_utils;
#[derive(Default)]
pub struct SecretKeyStore {
keys: DashMap<Bytes48, Bytes32>,
}
impl SecretKeyStore {
#[must_use]
pub fn save_secret_key(&self, secret_key: &SecretKey) -> Option<Bytes32> {
self.keys.insert(
Bytes48::from(secret_key.sk_to_pk()),
Bytes32::from(secret_key.to_bytes()),
)
}
#[must_use]
pub fn secret_key_for_public_key(&self, pub_key: &Bytes48) -> Option<Ref<Bytes48, Bytes32>> {
self.keys.get(pub_key)
}
}
#[derive(Eq, PartialEq, Hash, Clone, Copy)]
struct Primary {
puzzle_hash: Bytes32,
amount: u64,
}
pub struct WalletInfo<T: WalletStore> {
pub id: u32,
pub name: String,
pub wallet_type: WalletType,
pub constants: Arc<ConsensusConstants>,
pub master_sk: SecretKey,
pub wallet_store: Arc<Mutex<T>>,
pub data: String, }
#[async_trait]
pub trait WalletStore {
fn get_master_sk(&self) -> &SecretKey;
fn standard_coins(&self) -> Arc<Mutex<Vec<CoinRecord>>>;
fn cat_coins(&self) -> Arc<Mutex<Vec<CatCoinRecord>>>;
fn secret_key_store(&self) -> &SecretKeyStore;
fn current_index(&self) -> u32;
fn next_index(&self) -> u32;
async fn get_confirmed_balance(&self) -> u128;
async fn get_unconfirmed_balance(&self) -> u128;
async fn get_pending_change_balance(&self) -> u128;
async fn populate_secret_key_for_puzzle_hash(
&self,
puz_hash: &Bytes32,
) -> Result<Bytes48, Error>;
async fn add_puzzle_hash_and_keys(
&self,
puzzle_hash: Bytes32,
keys: (Bytes32, Bytes48),
) -> Option<(Bytes32, Bytes48)>;
async fn get_max_send_amount(&self) -> u128 {
let unspent: Vec<CoinRecord> = self
.standard_coins()
.lock()
.await
.iter()
.filter(|v| !v.spent)
.copied()
.collect();
if unspent.is_empty() {
0
} else {
unspent.iter().map(|v| v.coin.amount as u128).sum()
}
}
async fn get_spendable_balance(&self) -> u128 {
self.get_max_send_amount().await
}
async fn get_puzzle_hashes(
&self,
start: u32,
count: u32,
hardened: bool,
) -> Result<Vec<Bytes32>, Error> {
let mut puz_hashes = vec![];
for i in start..start + count {
puz_hashes.push(
self.get_derivation_record_at_index(i, hardened)
.await?
.puzzle_hash,
);
}
Ok(puz_hashes)
}
fn wallet_sk(&self, index: u32, hardened: bool) -> Result<SecretKey, Error> {
if hardened {
master_sk_to_wallet_sk(self.get_master_sk(), index)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}")))
} else {
master_sk_to_wallet_sk_unhardened(self.get_master_sk(), index)
.map_err(|e| Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}")))
}
}
async fn get_derivation_record_at_index(
&self,
index: u32,
hardened: bool,
) -> Result<DerivationRecord, Error> {
let wallet_sk = self.wallet_sk(index, hardened)?;
let _ = self.secret_key_store().save_secret_key(&wallet_sk);
let pubkey = Bytes48::from(wallet_sk.sk_to_pk().to_bytes());
let puzzle_hash = puzzle_hash_for_pk(pubkey)?;
self.add_puzzle_hash_and_keys(puzzle_hash, (Bytes32::from(wallet_sk), pubkey))
.await;
Ok(DerivationRecord {
index,
puzzle_hash,
pubkey,
wallet_type: WalletType::StandardWallet,
wallet_id: 1,
hardened,
})
}
async fn get_unused_derivation_record(
&self,
hardened: bool,
) -> Result<DerivationRecord, Error> {
self.get_derivation_record_at_index(self.next_index(), hardened)
.await
}
async fn get_derivation_record(&self, hardened: bool) -> Result<DerivationRecord, Error> {
self.get_derivation_record_at_index(self.current_index(), hardened)
.await
}
#[allow(clippy::too_many_lines)]
async fn select_coins(
&self,
amount: u64,
exclude: Option<&[Coin]>,
min_coin_amount: Option<u64>,
max_coin_amount: u64,
exclude_coin_amounts: Option<&[u64]>,
) -> Result<HashSet<Coin>, Error> {
let spendable_amount = self.get_spendable_balance().await;
let exclude = exclude.unwrap_or_default();
let min_coin_amount = min_coin_amount.unwrap_or(0);
let exclude_coin_amounts = exclude_coin_amounts.unwrap_or_default();
if amount as u128 > spendable_amount {
Err(Error::new(ErrorKind::InvalidInput, format!("Can't select amount higher than our spendable balance. Amount: {amount}, spendable: {spendable_amount}")))
} else {
debug!("About to select coins for amount {amount}");
let max_num_coins = 500;
let mut sum_spendable_coins = 0;
let mut valid_spendable_coins: Vec<Coin> = vec![];
for coin_record in self
.standard_coins()
.lock()
.await
.iter()
.filter(|v| !v.spent)
{
if exclude.contains(&coin_record.coin) {
continue;
}
if coin_record.coin.amount < min_coin_amount
|| coin_record.coin.amount > max_coin_amount
{
continue;
}
if exclude_coin_amounts.contains(&coin_record.coin.amount) {
continue;
}
sum_spendable_coins += coin_record.coin.amount;
valid_spendable_coins.push(coin_record.coin);
}
if sum_spendable_coins < amount {
return Err(Error::new(ErrorKind::InvalidInput, format!("Transaction for {amount} is greater than spendable balance of {sum_spendable_coins}. There may be other transactions pending or our minimum coin amount is too high.")));
}
if amount == 0 && sum_spendable_coins == 0 {
return Err(Error::new(ErrorKind::InvalidInput, "No coins available to spend, you can not create a coin with an amount of 0, without already having coins."));
}
valid_spendable_coins.sort_by(|f, s| f.amount.cmp(&s.amount));
if let Some(c) = check_for_exact_match(&valid_spendable_coins, amount) {
info!("Selected coin with an exact match: {c:?}");
Ok(HashSet::from([c]))
} else {
let mut smaller_coin_sum = 0; let mut all_sum = 0; let mut smaller_coins = vec![];
for coin in &valid_spendable_coins {
if coin.amount < amount {
smaller_coin_sum += coin.amount;
smaller_coins.push(*coin);
}
all_sum += coin.amount;
}
if smaller_coin_sum == amount && smaller_coins.len() < max_num_coins && amount != 0
{
debug!("Selected all smaller coins because they equate to an exact match of the target: {smaller_coins:?}");
Ok(smaller_coins.iter().copied().collect())
} else if smaller_coin_sum < amount {
let smallest_coin =
select_smallest_coin_over_target(amount, &valid_spendable_coins);
if let Some(smallest_coin) = smallest_coin {
debug!("Selected closest greater coin: {}", smallest_coin.name());
Ok(HashSet::from([smallest_coin]))
} else {
return Err(Error::new(ErrorKind::InvalidInput, format!("Transaction of {amount} mojo is greater than available sum {all_sum} mojos.")));
}
} else if smaller_coin_sum > amount {
let mut coin_set = knapsack_coin_algorithm(
&smaller_coins,
amount,
max_coin_amount,
max_num_coins,
None,
);
debug!("Selected coins from knapsack algorithm: {coin_set:?}");
if coin_set.is_none() {
coin_set = sum_largest_coins(amount as u128, &smaller_coins);
if coin_set.is_none()
|| coin_set.as_ref().map(HashSet::len).unwrap_or_default()
> max_num_coins
{
let greater_coin =
select_smallest_coin_over_target(amount, &valid_spendable_coins);
if let Some(greater_coin) = greater_coin {
coin_set = Some(HashSet::from([greater_coin]));
} else {
return Err(Error::new(ErrorKind::InvalidInput, format!("Transaction of {amount} mojo would use more than {max_num_coins} coins. Try sending a smaller amount")));
}
}
}
coin_set.ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
"Failed to select coins for transaction",
)
})
} else {
match select_smallest_coin_over_target(amount, &valid_spendable_coins) {
Some(coin) => {
debug!("Resorted to selecting smallest coin over target due to dust.: {coin:?}");
Ok(HashSet::from([coin]))
}
None => Err(Error::new(
ErrorKind::InvalidInput,
"Too many coins are required to make this transaction",
)),
}
}
}
}
}
async fn populate_secret_keys_for_coin_spends(
&self,
coin_spends: &[CoinSpend],
) -> Result<(), Error> {
for coin_spend in coin_spends {
self.populate_secret_key_for_puzzle_hash(&coin_spend.coin.puzzle_hash)
.await?;
}
Ok(())
}
async fn secret_key_for_public_key(&self, public_key: &Bytes48) -> Result<SecretKey, Error>;
fn mapping_function<'a, F>(
&'a self,
public_key: &'a Bytes48,
) -> Box<dyn Future<Output = Result<SecretKey, Error>> + Send + 'a> {
Box::new(self.secret_key_for_public_key(public_key))
}
}
fn check_for_exact_match(coin_list: &[Coin], target: u64) -> Option<Coin> {
for coin in coin_list {
if coin.amount == target {
return Some(*coin);
}
}
None
}
fn select_smallest_coin_over_target(target: u64, sorted_coin_list: &[Coin]) -> Option<Coin> {
for coin in sorted_coin_list {
if coin.amount >= target {
return Some(*coin);
}
}
None
}
fn sum_largest_coins(target: u128, sorted_coins: &[Coin]) -> Option<HashSet<Coin>> {
let mut total_value = 0u128;
let mut selected_coins = HashSet::default();
for coin in sorted_coins {
total_value += coin.amount as u128;
selected_coins.insert(*coin);
if total_value >= target {
return Some(selected_coins);
}
}
None
}
fn knapsack_coin_algorithm(
smaller_coins: &[Coin],
target: u64,
max_coin_amount: u64,
max_num_coins: usize,
seed: Option<[u8; 32]>,
) -> Option<HashSet<Coin>> {
let mut best_set_sum = max_coin_amount;
let mut best_set_of_coins: Option<HashSet<Coin>> = None;
let seed = Bytes32::new(seed.unwrap_or(*b"---------knapsack seed----------"));
let mut rand = StdRng::from_seed(seed.bytes());
for _ in 0..1000 {
let mut selected_coins = HashSet::default();
let mut selected_coins_sum = 0;
let mut n_pass = 0;
let mut target_reached = false;
while n_pass < 2 && !target_reached {
for coin in smaller_coins {
if (n_pass == 0 && rand.gen::<bool>())
|| (n_pass == 1 && !selected_coins.contains(coin))
{
if selected_coins.len() > max_num_coins {
break;
}
selected_coins_sum += coin.amount;
selected_coins.insert(*coin);
match selected_coins_sum.cmp(&target) {
std::cmp::Ordering::Greater => {
target_reached = true;
if selected_coins_sum < best_set_sum {
best_set_of_coins = Some(selected_coins.clone());
best_set_sum = selected_coins_sum;
selected_coins_sum -= coin.amount;
selected_coins.remove(coin);
}
}
std::cmp::Ordering::Less => {}
std::cmp::Ordering::Equal => return Some(selected_coins),
}
}
}
n_pass += 1;
}
}
best_set_of_coins
}
#[async_trait]
pub trait Wallet<T: WalletStore + Send + Sync, C> {
fn create(info: WalletInfo<T>, config: C) -> Result<Self, Error>
where
Self: Sized;
fn create_simulator(info: WalletInfo<T>, config: C) -> Result<Self, Error>
where
Self: Sized;
fn name(&self) -> &str;
async fn sync(&self) -> Result<bool, Error>;
fn is_synced(&self) -> bool;
fn wallet_info(&self) -> &WalletInfo<T>;
fn wallet_store(&self) -> Arc<Mutex<T>>;
fn require_derivation_paths(&self) -> bool {
true
}
#[allow(clippy::cast_possible_truncation)]
async fn puzzle_hashes(
&self,
start_index: usize,
count: usize,
hardened: bool,
) -> Result<Vec<Bytes32>, Error> {
let mut hashes = vec![];
for i in start_index..start_index + count {
hashes.push(
self.wallet_store()
.lock()
.await
.get_derivation_record_at_index(i as u32, hardened)
.await?
.puzzle_hash,
);
}
Ok(hashes)
}
fn puzzle_for_pk(&self, public_key: Bytes48) -> Result<Program, Error> {
puzzle_for_pk(public_key)
}
fn puzzle_hash_for_pk(&self, public_key: Bytes48) -> Result<Bytes32, Error> {
puzzle_hash_for_pk(public_key)
}
#[allow(clippy::too_many_arguments)]
async fn create_spend_bundle(
&self,
payments: Vec<AmountWithPuzzleHash>,
input_coins: &[CoinRecord],
change_puzzle_hash: Option<Bytes32>,
allow_excess: bool,
fee: i64,
origin_id: Option<Bytes32>,
solution_transformer: Option<Box<dyn Fn(Program) -> Program + 'static + Send + Sync>>,
) -> Result<SpendBundle, Error>;
#[allow(clippy::too_many_arguments)]
fn make_solution(
&self,
primaries: &[AmountWithPuzzleHash],
min_time: u64,
coin_announcements: Option<HashSet<Bytes32>>,
coin_announcements_to_assert: Option<HashSet<Bytes32>>,
puzzle_announcements: Option<HashSet<Vec<u8>>>,
puzzle_announcements_to_assert: Option<HashSet<Bytes32>>,
fee: u64,
) -> Result<Program, Error> {
let mut condition_list = vec![];
for primary in primaries {
condition_list.push(make_create_coin_condition(
primary.puzzle_hash,
primary.amount,
&primary.memos,
));
}
if min_time > 0 {
condition_list.push(make_assert_absolute_seconds_exceeds_condition(min_time));
}
if fee > 0 {
condition_list.push(make_reserve_fee_condition(fee));
}
if let Some(coin_announcements) = coin_announcements {
for announcement in coin_announcements {
condition_list.push(make_create_coin_announcement(&announcement.bytes()));
}
}
if let Some(coin_announcements_to_assert) = coin_announcements_to_assert {
for announcement_hash in coin_announcements_to_assert {
condition_list.push(make_assert_coin_announcement(&announcement_hash));
}
}
if let Some(puzzle_announcements) = puzzle_announcements {
for announcement in puzzle_announcements {
condition_list.push(make_create_puzzle_announcement(&announcement));
}
}
if let Some(puzzle_announcements_to_assert) = puzzle_announcements_to_assert {
for announcement_hash in puzzle_announcements_to_assert {
condition_list.push(make_assert_puzzle_announcement(&announcement_hash));
}
}
solution_for_conditions(condition_list)
}
fn compute_memos(
&self,
spend_bundle: &SpendBundle,
) -> Result<HashMap<Bytes32, Vec<Vec<u8>>>, Error> {
let mut memos: HashMap<Bytes32, Vec<Vec<u8>>> = HashMap::default();
for coin_spend in &spend_bundle.coin_spends {
for (coin_name, coin_memos) in compute_memos_for_spend(coin_spend)? {
match memos.remove(&coin_name) {
Some(mut existing_memos) => {
existing_memos.extend(coin_memos);
memos.insert(coin_name, existing_memos);
}
None => {
memos.insert(coin_name, coin_memos);
}
}
}
}
Ok(memos)
}
async fn puzzle_for_puzzle_hash(&self, puz_hash: &Bytes32) -> Result<Program, Error> {
let public_key = self
.wallet_store()
.lock()
.await
.populate_secret_key_for_puzzle_hash(puz_hash)
.await?;
puzzle_for_pk(public_key)
}
async fn get_new_puzzle(&self) -> Result<Program, Error> {
let dr = self
.wallet_store()
.lock()
.await
.get_unused_derivation_record(false)
.await?;
let puzzle = puzzle_for_pk(dr.pubkey)?;
self.wallet_store()
.lock()
.await
.populate_secret_key_for_puzzle_hash(&puzzle.tree_hash())
.await?;
Ok(puzzle)
}
async fn get_puzzle_hash(&self, new: bool) -> Result<Bytes32, Error> {
Ok(if new {
self.get_new_puzzlehash().await?
} else {
let dr = self
.wallet_store()
.lock()
.await
.get_derivation_record(false)
.await?;
dr.puzzle_hash
})
}
async fn get_new_puzzlehash(&self) -> Result<Bytes32, Error> {
let dr = self
.wallet_store()
.lock()
.await
.get_unused_derivation_record(false)
.await?;
self.wallet_store()
.lock()
.await
.populate_secret_key_for_puzzle_hash(&dr.puzzle_hash)
.await?;
Ok(dr.puzzle_hash)
}
async fn convert_puzzle_hash(&self, puzzle_hash: Bytes32) -> Bytes32 {
puzzle_hash
}
async fn generate_simple_signed_transaction(
&self,
mojos: u64,
fee_mojos: u64,
to_address: Bytes32,
) -> Result<TransactionRecord, Error> {
self.generate_signed_transaction(
mojos,
&to_address,
fee_mojos,
None,
None,
None,
false,
None,
None,
None,
false,
None,
None,
None,
None,
None,
)
.await
}
async fn generate_simple_unsigned_transaction(
&self,
mojos: u64,
fee_mojos: u64,
to_address: Bytes32,
) -> Result<Vec<CoinSpend>, Error> {
self.generate_unsigned_transaction(
mojos,
&to_address,
fee_mojos,
None,
None,
None,
false,
None,
None,
None,
false,
None,
None,
None,
None,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn generate_signed_transaction(
&self,
amount: u64,
puzzle_hash: &Bytes32,
fee: u64,
origin_id: Option<Bytes32>,
coins: Option<Vec<Coin>>,
primaries: Option<&[AmountWithPuzzleHash]>,
ignore_max_send_amount: bool,
coin_announcements_to_consume: Option<&[Announcement]>,
puzzle_announcements_to_consume: Option<&[Announcement]>,
memos: Option<Vec<Vec<u8>>>,
negative_change_allowed: bool,
min_coin_amount: Option<u64>,
max_coin_amount: Option<u64>,
exclude_coin_amounts: Option<&[u64]>,
exclude_coins: Option<&[Coin]>,
reuse_puzhash: Option<bool>,
) -> Result<TransactionRecord, Error> {
let non_change_amount = if let Some(primaries) = primaries {
amount + primaries.iter().map(|a| a.amount).sum::<u64>()
} else {
amount
};
debug!(
"Generating transaction for: {} {} {:?}",
puzzle_hash, amount, coins
);
let transaction = self
.generate_unsigned_transaction(
amount,
puzzle_hash,
fee,
origin_id,
coins,
primaries,
ignore_max_send_amount,
coin_announcements_to_consume,
puzzle_announcements_to_consume,
memos,
negative_change_allowed,
min_coin_amount,
max_coin_amount,
exclude_coin_amounts,
exclude_coins,
reuse_puzhash,
)
.await?;
assert!(!transaction.is_empty());
info!("About to sign a transaction: {:?}", transaction);
let wallet_store = self.wallet_store().clone();
let spend_bundle = sign_coin_spends(
transaction,
|pub_key| {
let pub_key = *pub_key;
let wallet_store = wallet_store.clone();
async move {
wallet_store
.lock()
.await
.secret_key_for_public_key(&pub_key)
.await
}
},
HashMap::with_capacity(0),
&self.wallet_info().constants.agg_sig_me_additional_data,
self.wallet_info()
.constants
.max_block_cost_clvm
.to_u64()
.unwrap(),
)
.await?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let add_list = spend_bundle.additions()?;
let rem_list = spend_bundle.removals();
let output_amount: u64 = add_list.iter().map(|a| a.amount).sum::<u64>() + fee;
let input_amount: u64 = rem_list.iter().map(|a| a.amount).sum::<u64>();
if negative_change_allowed {
assert!(output_amount >= input_amount);
} else {
assert_eq!(output_amount, input_amount);
}
let memos = self.compute_memos(&spend_bundle)?;
let memos = memos
.into_iter()
.map(|v| (v.0, v.1))
.collect::<Vec<(Bytes32, Vec<Vec<u8>>)>>();
let name = spend_bundle.name()?;
Ok(TransactionRecord {
confirmed_at_height: 0,
created_at_time: now,
to_puzzle_hash: *puzzle_hash,
amount: non_change_amount,
fee_amount: fee,
confirmed: false,
sent: 0,
spend_bundle: Some(spend_bundle),
additions: add_list,
removals: rem_list,
wallet_id: 1,
sent_to: vec![],
trade_id: None,
transaction_type: TransactionType::OutgoingTx as u32,
name,
memos,
})
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_sign_loss)]
async fn generate_unsigned_transaction(
&self,
amount: u64,
puzzle_hash: &Bytes32,
fee: u64,
origin_id: Option<Bytes32>,
coins: Option<Vec<Coin>>,
primaries: Option<&[AmountWithPuzzleHash]>,
ignore_max_send_amount: bool,
coin_announcements_to_consume: Option<&[Announcement]>,
puzzle_announcements_to_consume: Option<&[Announcement]>,
memos: Option<Vec<Vec<u8>>>,
negative_change_allowed: bool,
min_coin_amount: Option<u64>,
max_coin_amount: Option<u64>,
exclude_coin_amounts: Option<&[u64]>,
exclude_coins: Option<&[Coin]>,
reuse_puzhash: Option<bool>,
) -> Result<Vec<CoinSpend>, Error> {
let mut primaries_amount = 0u64;
let total_amount: u128;
if let Some(primaries) = primaries {
for primary in primaries {
primaries_amount += primary.amount;
}
total_amount = amount as u128 + fee as u128 + primaries_amount as u128;
} else {
total_amount = amount as u128 + fee as u128;
}
let reuse_puzhash = reuse_puzhash.unwrap_or(true);
let total_balance = self
.wallet_store()
.lock()
.await
.get_spendable_balance()
.await;
if !ignore_max_send_amount {
let max_send = self.wallet_store().lock().await.get_max_send_amount().await;
if total_amount > max_send {
return Err(Error::new(ErrorKind::InvalidInput, format!("Can't send more than {max_send} mojos in a single transaction, got {total_amount}")));
}
debug!("Max send amount: {max_send}");
}
let coins_set: HashSet<Coin>;
if coins.is_none() {
if total_amount > total_balance {
return Err(Error::new(ErrorKind::InvalidInput, format!("Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {total_amount} mojos")));
}
coins_set = self
.wallet_store()
.lock()
.await
.select_coins(
total_amount as u64,
exclude_coins,
min_coin_amount,
max_coin_amount.unwrap_or(
self.wallet_info()
.constants
.max_coin_amount
.to_u64()
.unwrap_or_default(),
),
exclude_coin_amounts,
)
.await?;
} else if exclude_coins.is_some() {
return Err(Error::new(
ErrorKind::InvalidInput,
"Can't exclude coins when also specifically including coins",
));
} else {
coins_set = HashSet::from_iter(coins.unwrap_or_default());
}
assert!(!coins_set.is_empty());
info!("Found Coins to use: {:?}", coins_set);
let spend_value: i128 = coins_set.iter().map(|v| i128::from(v.amount)).sum::<i128>();
info!("spend_value is {spend_value} and total_amount is {total_amount}");
let mut change = spend_value - total_amount as i128;
if negative_change_allowed {
change = max(0, change);
}
assert!(change >= 0);
let coin_announcements_bytes = coin_announcements_to_consume
.unwrap_or_default()
.iter()
.map(Announcement::name)
.collect::<Vec<Bytes32>>();
let puzzle_announcements_bytes = puzzle_announcements_to_consume
.unwrap_or_default()
.iter()
.map(Announcement::name)
.collect::<Vec<Bytes32>>();
let mut spends: Vec<CoinSpend> = vec![];
let mut primary_announcement_hash = None;
if primaries.is_some() {
let mut all_primaries_list = primaries
.unwrap_or_default()
.iter()
.map(|a| Primary {
puzzle_hash: a.puzzle_hash,
amount: a.amount,
})
.collect::<Vec<Primary>>();
all_primaries_list.push(Primary {
puzzle_hash: *puzzle_hash,
amount,
});
let as_set: HashSet<Primary> = all_primaries_list.iter().copied().collect();
if all_primaries_list.len() != as_set.len() {
return Err(Error::new(
ErrorKind::InvalidInput,
"Cannot create two identical coins",
));
}
}
let memos = memos.unwrap_or_default();
let mut origin_id = origin_id;
for coin in &coins_set {
if [None, Some(coin.name())].contains(&origin_id) {
origin_id = Some(coin.name());
let mut primaries = if let Some(primaries) = primaries {
let mut primaries = primaries.to_vec();
primaries.push(AmountWithPuzzleHash {
amount,
puzzle_hash: *puzzle_hash,
memos: memos.clone(),
});
primaries
} else if amount > 0 {
vec![AmountWithPuzzleHash {
amount,
puzzle_hash: *puzzle_hash,
memos: memos.clone(),
}]
} else {
vec![]
};
if change > 0 {
let change_puzzle_hash = if reuse_puzhash {
let mut change_puzzle_hash = coin.puzzle_hash;
for primary in &primaries {
if change_puzzle_hash == primary.puzzle_hash
&& change == i128::from(primary.amount)
{
change_puzzle_hash = self.get_new_puzzlehash().await?;
break;
}
}
change_puzzle_hash
} else {
self.get_new_puzzlehash().await?
};
primaries.push(AmountWithPuzzleHash {
amount: change as u64,
puzzle_hash: change_puzzle_hash,
memos: vec![],
});
}
let mut message_list: Vec<Bytes32> = coins_set.iter().map(Coin::name).collect();
for primary in &primaries {
message_list.push(
Coin {
parent_coin_info: coin.name(),
puzzle_hash: primary.puzzle_hash,
amount: primary.amount,
}
.name(),
);
}
let message = hash_256(message_list.iter().fold(vec![], |mut v, e| {
v.extend(
e.to_bytes(ChiaProtocolVersion::default())
.expect("Bytes32 has Safe to_bytes"),
);
v
}));
let coin_announcements = HashSet::from([Bytes32::new(message)]);
let coin_announcements_to_assert = HashSet::from_iter(coin_announcements_bytes);
let puzzle_announcements_to_assert = HashSet::from_iter(puzzle_announcements_bytes);
info!("Primaries: {primaries:?}");
info!(
"coin_announcements: {:?}",
coin_announcements
.iter()
.map(|v| { hex::encode(v) })
.collect::<Vec<String>>()
);
info!("coin_announcements_to_assert: {coin_announcements_to_assert:?}");
info!("puzzle_announcements_to_assert: {puzzle_announcements_to_assert:?}");
let puzzle = self.puzzle_for_puzzle_hash(&coin.puzzle_hash).await?;
let solution = self.make_solution(
&primaries,
0,
if coin_announcements.is_empty() {
None
} else {
Some(coin_announcements)
},
if coin_announcements_to_assert.is_empty() {
None
} else {
Some(coin_announcements_to_assert)
},
None,
if puzzle_announcements_to_assert.is_empty() {
None
} else {
Some(puzzle_announcements_to_assert)
},
fee,
)?;
primary_announcement_hash = Some(
Announcement {
origin_info: coin.name(),
message: message.to_vec(),
morph_bytes: None,
}
.name(),
);
info!("Reveal: {} ", hex::encode(&puzzle.serialized));
info!("Solution: {} ", hex::encode(&solution.serialized));
spends.push(CoinSpend {
coin: *coin,
puzzle_reveal: SerializedProgram::from_bytes(&puzzle.serialized),
solution: SerializedProgram::from_bytes(&solution.serialized),
});
break;
}
}
for coin in coins_set {
if Some(coin.name()) == origin_id {
continue;
}
let puzzle = self.puzzle_for_puzzle_hash(&coin.puzzle_hash).await?;
let solution = self.make_solution(
&[],
0,
None,
Some(HashSet::from_iter(primary_announcement_hash)),
None,
None,
0,
)?;
info!("Reveal: {} ", hex::encode(&puzzle.serialized));
info!("Solution: {} ", hex::encode(&solution.serialized));
spends.push(CoinSpend {
coin,
puzzle_reveal: SerializedProgram::from_bytes(&puzzle.serialized),
solution: SerializedProgram::from_bytes(&solution.serialized),
});
}
info!("Spends is {:?}", spends);
Ok(spends)
}
}
pub fn compute_memos_for_spend(
coin_spend: &CoinSpend,
) -> Result<HashMap<Bytes32, Vec<Vec<u8>>>, Error> {
let (_, result) = coin_spend
.puzzle_reveal
.run_with_cost(INFINITE_COST, &coin_spend.solution.to_program())?;
let mut memos = HashMap::default();
let result_list = result.as_list();
for condition in result_list {
let mut conditions: Vec<Program> = condition.as_list();
if ConditionOpcode::from(&conditions[0]) == ConditionOpcode::CreateCoin
&& conditions.len() >= 4
{
let memo_list = conditions.remove(3);
let amount = conditions.remove(2);
let puzzle_hash = conditions.remove(1);
let coin_added = Coin {
parent_coin_info: coin_spend.coin.name(),
puzzle_hash: Bytes32::try_from(puzzle_hash)?,
amount: amount
.as_int()?
.to_u64()
.ok_or(Error::new(ErrorKind::InvalidInput, "invalid amount"))?,
};
let memo_list = memo_list
.as_list()
.into_iter()
.map(|v| v.as_vec().unwrap_or_default())
.collect::<Vec<Vec<u8>>>();
memos.insert(coin_added.name(), memo_list);
}
}
Ok(memos)
}