RPC
This commit is contained in:
parent
f23cd2376f
commit
4cafff16a4
|
@ -19,11 +19,6 @@ name = "wallet"
|
|||
path = "src/main/wallet.rs"
|
||||
required-features = ["dotenv"]
|
||||
|
||||
[[bin]]
|
||||
name = "tests"
|
||||
path = "src/main/tests.rs"
|
||||
required-features = ["dotenv"]
|
||||
|
||||
#[[bin]]
|
||||
#name = "ledger"
|
||||
#path = "src/main/ledger.rs"
|
||||
|
|
|
@ -39,6 +39,7 @@ Example
|
|||
$ zcashd -datadir=$PWD --daemon
|
||||
$ zcash-cli -datadir=$PWD getinfo
|
||||
$ zcash-cli -datadir=$PWD z_getnewaccount
|
||||
$ zcash-cli -datadir=$PWD z_getnewaddress
|
||||
$ zcash-cli -datadir=$PWD listaddresses
|
||||
$ zcash-cli -datadir=$PWD generate 200
|
||||
$ zcash-cli -datadir=$PWD getbalance
|
||||
|
@ -48,6 +49,11 @@ $ zcash-cli -datadir=$PWD generate 10
|
|||
$ zcash-cli -datadir=$PWD z_gettotalbalance
|
||||
```
|
||||
|
||||
zcash-cli -datadir=$PWD z_sendmany "ANY_TADDR" '[{"address": "zregtestsapling12qlzvqkla5ysscxx9l4dn69m7zwjggplap4gp3x9r0mnk868whgpxc03atkj83zh3xqgz0rguq0", "amount": 62.49999}]'
|
||||
|
||||
zcash-cli -datadir=$PWD z_sendmany "ANY_TADDR" '[{"address": "zregtestsapling1zdrds45f09kxhzq3ak2p6j6qj9a094tjp955f9nmk44ke5qm8xsrpncauxrx3efh76euq78nhyt", "amount": 624.99999}]'
|
||||
|
||||
|
||||
## Lightwalletd
|
||||
|
||||
- Start lightwalletd
|
||||
|
@ -84,6 +90,15 @@ $ curl -X GET http://localhost:8000/latest_height
|
|||
$ curl -X GET http://localhost:8000/unified_address?t=1\&s=1\&o=1
|
||||
```
|
||||
|
||||
zcash-cli -datadir=$PWD z_sendmany "zregtestsapling1zdrds45f09kxhzq3ak2p6j6qj9a094tjp955f9nmk44ke5qm8xsrpncauxrx3efh76euq78nhyt" '[
|
||||
{"address": "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", "amount": 100},
|
||||
{"address": "zregtestsapling1qzy9wafd2axnenul6t6wav76dys6s8uatsq778mpmdvmx4k9myqxsd9m73aqdgc7gwnv53wga4j", "amount": 100},
|
||||
{"address": "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", "amount": 100}
|
||||
]' 1 0.00001 "AllowRevealedRecipients"
|
||||
|
||||
|
||||
|
||||
|
||||
zcash-cli -datadir=$PWD z_sendmany "ANY_TADDR" '[{"address": "zregtestsapling1rlf8jpvk6qymgsn6pclkpnee0u77pajpz5g7955uzrxsefc837h326rkjag7rwuhn2cyympd8jh", "amount": 6.24999}]'
|
||||
zcash-cli -datadir=$PWD z_sendmany "zregtestsapling1rlf8jpvk6qymgsn6pclkpnee0u77pajpz5g7955uzrxsefc837h326rkjag7rwuhn2cyympd8jh" '[{"address": "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", "amount": 6.24997}]'
|
||||
zcash-cli -datadir=$PWD z_sendmany "zregtestsapling1rlf8jpvk6qymgsn6pclkpnee0u77pajpz5g7955uzrxsefc837h326rkjag7rwuhn2cyympd8jh" '[{"address": "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", "amount": 6.24997}]' 1 0.00001 "AllowRevealedAmounts"
|
||||
|
|
|
@ -5,8 +5,8 @@ pub mod historical_prices;
|
|||
pub mod mempool;
|
||||
pub mod message;
|
||||
pub mod payment;
|
||||
pub mod payment_v2;
|
||||
pub mod payment_uri;
|
||||
pub mod payment_v2;
|
||||
pub mod sync;
|
||||
|
||||
#[cfg(feature = "dart_ffi")]
|
||||
|
|
|
@ -7,6 +7,8 @@ use crate::db::AccountData;
|
|||
use crate::key2::decode_key;
|
||||
use crate::taddr::{derive_taddr, derive_tkeys};
|
||||
use crate::transaction::retrieve_tx_info;
|
||||
use crate::unified::UnifiedAddressType;
|
||||
use crate::zip32::derive_zip32;
|
||||
use crate::{connect_lightwalletd, AccountInfo, KeyPack};
|
||||
use anyhow::anyhow;
|
||||
use bip39::{Language, Mnemonic};
|
||||
|
@ -16,8 +18,6 @@ use std::fs::File;
|
|||
use std::io::BufReader;
|
||||
use zcash_client_backend::encoding::{decode_extended_full_viewing_key, encode_payment_address};
|
||||
use zcash_primitives::consensus::Parameters;
|
||||
use crate::unified::UnifiedAddressType;
|
||||
use crate::zip32::derive_zip32;
|
||||
|
||||
/// Create a new account
|
||||
/// # Arguments
|
||||
|
@ -89,8 +89,7 @@ fn new_account_with_key(coin: u8, name: &str, key: &str, index: u32) -> anyhow::
|
|||
db.create_orchard(account)?;
|
||||
}
|
||||
db.store_ua_settings(account, true, true, true)?;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
db.store_ua_settings(account, false, true, false)?;
|
||||
}
|
||||
Ok(account)
|
||||
|
@ -136,7 +135,8 @@ pub fn new_diversified_address() -> anyhow::Result<String> {
|
|||
let fvk = decode_extended_full_viewing_key(
|
||||
c.chain.network().hrp_sapling_extended_full_viewing_key(),
|
||||
&fvk,
|
||||
).map_err(|_| anyhow!("Bech32 Decode Error"))?;
|
||||
)
|
||||
.map_err(|_| anyhow!("Bech32 Decode Error"))?;
|
||||
let mut diversifier_index = db.get_diversifier(c.id_account)?;
|
||||
diversifier_index.increment().unwrap();
|
||||
let (new_diversifier_index, pa) = fvk
|
||||
|
@ -301,13 +301,19 @@ pub async fn import_sync_data(coin: u8, file: &str) -> anyhow::Result<()> {
|
|||
/// * t, s, o: include transparent, sapling, orchard receivers?
|
||||
///
|
||||
/// The address depends on the UA settings and may include transparent, sapling & orchard receivers
|
||||
pub fn get_unified_address(coin: u8, id_account: u32, t: bool, s: bool, o: bool) -> anyhow::Result<String> {
|
||||
pub fn get_unified_address(
|
||||
coin: u8,
|
||||
id_account: u32,
|
||||
t: bool,
|
||||
s: bool,
|
||||
o: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
let c = CoinConfig::get(coin);
|
||||
let db = c.db()?;
|
||||
let tpe = UnifiedAddressType {
|
||||
transparent: t,
|
||||
sapling: s,
|
||||
orchard: o
|
||||
orchard: o,
|
||||
};
|
||||
let address = crate::get_unified_address(c.chain.network(), &db, id_account, Some(tpe))?; // use ua settings from db
|
||||
Ok(address)
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::db::AccountBackup;
|
|||
use bech32::{FromBase32, ToBase32, Variant};
|
||||
use chacha20poly1305::aead::{Aead, NewAead};
|
||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
|
||||
const NONCE: &[u8; 12] = b"unique nonce";
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//! Access to server mempool
|
||||
|
||||
use crate::api::sync::get_latest_height;
|
||||
use anyhow::anyhow;
|
||||
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
|
||||
use zcash_primitives::consensus::Parameters;
|
||||
use crate::api::sync::get_latest_height;
|
||||
|
||||
use crate::coinconfig::CoinConfig;
|
||||
use crate::db::AccountData;
|
||||
|
@ -22,7 +22,8 @@ pub async fn scan() -> anyhow::Result<i64> {
|
|||
let fvk = decode_extended_full_viewing_key(
|
||||
c.chain.network().hrp_sapling_extended_full_viewing_key(),
|
||||
&fvk,
|
||||
).map_err(|_| anyhow!("Decode error"))?;
|
||||
)
|
||||
.map_err(|_| anyhow!("Decode error"))?;
|
||||
let mut client = c.connect_lwd().await?;
|
||||
mempool
|
||||
.update(&mut client, height, &fvk.fvk.vk.ivk())
|
||||
|
|
|
@ -208,7 +208,7 @@ pub struct Recipient {
|
|||
pub max_amount_per_note: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct RecipientShort {
|
||||
pub address: String,
|
||||
pub amount: u64,
|
||||
|
@ -243,7 +243,7 @@ impl From<RecipientShort> for RecipientMemo {
|
|||
address: r.address,
|
||||
amount: r.amount,
|
||||
memo: Memo::Empty,
|
||||
max_amount_per_note: 0
|
||||
max_amount_per_note: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,57 @@
|
|||
use crate::api::payment::RecipientShort;
|
||||
use std::cmp::min;
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use crate::api::payment::{RecipientMemo, RecipientShort};
|
||||
use crate::{AccountData, CoinConfig, fetch_utxos, TransactionBuilderConfig, TransactionPlan};
|
||||
use crate::note_selection::{FeeZIP327, Order};
|
||||
|
||||
async fn prepare_payment_v2(recipients: &[RecipientShort]) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn build_tx_plan(
|
||||
coin: u8,
|
||||
account: u32,
|
||||
last_height: u32,
|
||||
recipients: &[RecipientMemo],
|
||||
config: &TransactionBuilderConfig,
|
||||
confirmations: u32,
|
||||
) -> anyhow::Result<TransactionPlan> {
|
||||
let c = CoinConfig::get(coin);
|
||||
let fvk = {
|
||||
let db = c.db()?;
|
||||
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
||||
fvk
|
||||
};
|
||||
|
||||
let mut orders = vec![];
|
||||
let mut id_order = 0;
|
||||
for r in recipients {
|
||||
let mut amount = r.amount;
|
||||
let max_amount_per_note = if r.max_amount_per_note == 0 {
|
||||
u64::MAX
|
||||
} else {
|
||||
r.max_amount_per_note
|
||||
};
|
||||
while amount > 0 {
|
||||
let a = min(amount, max_amount_per_note);
|
||||
let memo_bytes: MemoBytes = r.memo.clone().into();
|
||||
let order = Order::new(id_order, &r.address, a, memo_bytes);
|
||||
orders.push(order);
|
||||
amount -= a;
|
||||
id_order += 1;
|
||||
}
|
||||
}
|
||||
let utxos = fetch_utxos(
|
||||
coin,
|
||||
account,
|
||||
last_height,
|
||||
true,
|
||||
confirmations,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::info!("UTXO: {:?}", utxos);
|
||||
|
||||
let tx_plan = crate::note_selection::build_tx_plan::<FeeZIP327>(&fvk, last_height, &utxos, &orders, config)?;
|
||||
Ok(tx_plan)
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
use crate::coinconfig::CoinConfig;
|
||||
use crate::scan::{AMProgressCallback, Progress};
|
||||
use crate::sync::CTree;
|
||||
use crate::{AccountData, BlockId, CompactTxStreamerClient, DbAdapter};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::transport::Channel;
|
||||
use tonic::Request;
|
||||
use zcash_primitives::sapling::Note;
|
||||
use crate::sync::CTree;
|
||||
|
||||
const DEFAULT_CHUNK_SIZE: u32 = 100_000;
|
||||
|
||||
|
|
|
@ -355,7 +355,10 @@ fn decrypt_notes<'a, N: Parameters>(
|
|||
let mut count_outputs = 0u32;
|
||||
let mut spends: Vec<Nf> = vec![];
|
||||
let mut notes: Vec<DecryptedNote> = vec![];
|
||||
let vvks: Vec<_> = vks.iter().map(|vk| PreparedIncomingViewingKey::new(&vk.1.ivk)).collect();
|
||||
let vvks: Vec<_> = vks
|
||||
.iter()
|
||||
.map(|vk| PreparedIncomingViewingKey::new(&vk.1.ivk))
|
||||
.collect();
|
||||
let mut outputs: Vec<(SaplingDomain<N>, AccountOutput<N>)> = vec![];
|
||||
for (tx_index, vtx) in block.vtx.iter().enumerate() {
|
||||
for cs in vtx.spends.iter() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{connect_lightwalletd, CompactTxStreamerClient, DbAdapter};
|
||||
use crate::fountain::FountainCodes;
|
||||
use crate::mempool::MemPool;
|
||||
use crate::{connect_lightwalletd, CompactTxStreamerClient, DbAdapter};
|
||||
use anyhow::anyhow;
|
||||
use lazy_static::lazy_static;
|
||||
use lazycell::AtomicLazyCell;
|
||||
|
|
218
src/db.rs
218
src/db.rs
|
@ -1,27 +1,27 @@
|
|||
use crate::chain::{Nf, NfRef};
|
||||
use crate::chain::Nf;
|
||||
use crate::contact::Contact;
|
||||
use crate::orchard::{derive_orchard_keys, OrchardKeyBytes, OrchardViewKey};
|
||||
use crate::prices::Quote;
|
||||
use crate::sapling::SaplingViewKey;
|
||||
use crate::sync;
|
||||
use crate::sync::tree::{CTree, TreeCheckpoint};
|
||||
use crate::taddr::{derive_tkeys, TBalance};
|
||||
use crate::transaction::{GetTransactionDetailRequest, TransactionDetails};
|
||||
use crate::sync::tree::{CTree, TreeCheckpoint, Witness};
|
||||
use crate::unified::UnifiedAddressType;
|
||||
use crate::note_selection::{Source, UTXO};
|
||||
use orchard::keys::FullViewingKey;
|
||||
use rusqlite::Error::QueryReturnedNoRows;
|
||||
use rusqlite::{params, Connection, OptionalExtension, Transaction};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use orchard::keys::FullViewingKey;
|
||||
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
|
||||
use zcash_params::coin::{CoinType, get_coin_chain, get_coin_id};
|
||||
use zcash_params::coin::{get_coin_chain, get_coin_id, CoinType};
|
||||
use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters};
|
||||
use zcash_primitives::merkle_tree::IncrementalWitness;
|
||||
use zcash_primitives::sapling::{Diversifier, Node, Note, Rseed, SaplingIvk};
|
||||
use zcash_primitives::sapling::{Diversifier, Node, Note, SaplingIvk};
|
||||
use zcash_primitives::zip32::{DiversifierIndex, ExtendedFullViewingKey};
|
||||
use crate::note_selection::{Source, UTXO};
|
||||
use crate::orchard::{derive_orchard_keys, OrchardKeyBytes, OrchardViewKey};
|
||||
use crate::sapling::SaplingViewKey;
|
||||
use crate::sync;
|
||||
use crate::unified::UnifiedAddressType;
|
||||
|
||||
mod migration;
|
||||
|
||||
|
@ -88,11 +88,6 @@ pub struct AccountBackup {
|
|||
pub t_addr: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AccountSeed {
|
||||
pub id_account: u32,
|
||||
pub seed: String,
|
||||
}
|
||||
|
||||
pub fn wrap_query_no_rows(name: &'static str) -> impl Fn(rusqlite::Error) -> anyhow::Error {
|
||||
move |err: rusqlite::Error| match err {
|
||||
QueryReturnedNoRows => anyhow::anyhow!("Query {} returned no rows", name),
|
||||
|
@ -102,10 +97,7 @@ pub fn wrap_query_no_rows(name: &'static str) -> impl Fn(rusqlite::Error) -> any
|
|||
|
||||
impl DbAdapterBuilder {
|
||||
pub fn build(&self) -> anyhow::Result<DbAdapter> {
|
||||
DbAdapter::new(
|
||||
self.coin_type,
|
||||
&self.db_path,
|
||||
)
|
||||
DbAdapter::new(self.coin_type, &self.db_path)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,11 +201,7 @@ impl DbAdapter {
|
|||
)
|
||||
.unwrap();
|
||||
let ivk = fvk.fvk.vk.ivk();
|
||||
Ok(SaplingViewKey {
|
||||
account,
|
||||
fvk,
|
||||
ivk
|
||||
})
|
||||
Ok(SaplingViewKey { account, fvk, ivk })
|
||||
})?;
|
||||
let mut fvks = vec![];
|
||||
for r in rows {
|
||||
|
@ -224,17 +212,15 @@ impl DbAdapter {
|
|||
}
|
||||
|
||||
pub fn get_orchard_fvks(&self) -> anyhow::Result<Vec<OrchardViewKey>> {
|
||||
let mut statement = self.connection.prepare("SELECT account, fvk FROM orchard_addrs")?;
|
||||
let mut statement = self
|
||||
.connection
|
||||
.prepare("SELECT account, fvk FROM orchard_addrs")?;
|
||||
let rows = statement.query_map([], |row| {
|
||||
let account: u32 = row.get(0)?;
|
||||
let fvk: Vec<u8> = row.get(1)?;
|
||||
let fvk: [u8; 96] = fvk.try_into().unwrap();
|
||||
let fvk = FullViewingKey::from_bytes(&fvk).unwrap();
|
||||
let vk =
|
||||
OrchardViewKey {
|
||||
account,
|
||||
fvk,
|
||||
};
|
||||
let vk = OrchardViewKey { account, fvk };
|
||||
Ok(vk)
|
||||
})?;
|
||||
let mut fvks = vec![];
|
||||
|
@ -260,8 +246,14 @@ impl DbAdapter {
|
|||
|
||||
let tx = self.connection.transaction()?;
|
||||
tx.execute("DELETE FROM blocks WHERE height > ?1", params![height])?;
|
||||
tx.execute("DELETE FROM sapling_tree WHERE height > ?1", params![height])?;
|
||||
tx.execute("DELETE FROM orchard_tree WHERE height > ?1", params![height])?;
|
||||
tx.execute(
|
||||
"DELETE FROM sapling_tree WHERE height > ?1",
|
||||
params![height],
|
||||
)?;
|
||||
tx.execute(
|
||||
"DELETE FROM orchard_tree WHERE height > ?1",
|
||||
params![height],
|
||||
)?;
|
||||
tx.execute(
|
||||
"DELETE FROM sapling_witnesses WHERE height > ?1",
|
||||
params![height],
|
||||
|
@ -324,7 +316,7 @@ impl DbAdapter {
|
|||
log::debug!("+transaction");
|
||||
db_tx.execute(
|
||||
"INSERT INTO transactions(account, txid, height, timestamp, tx_index, value)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 0)",
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 0) ON CONFLICT DO NOTHING", // ignore conflict when same tx has sapling + orchard outputs
|
||||
params![account, txid, height, timestamp, tx_index],
|
||||
)?;
|
||||
let id_tx: u32 = db_tx
|
||||
|
@ -351,8 +343,8 @@ impl DbAdapter {
|
|||
note.diversifier, note.value as i64, note.rcm, note.rho, note.nf, orchard, note.spent])?;
|
||||
let id_note: u32 = db_tx
|
||||
.query_row(
|
||||
"SELECT id_note FROM received_notes WHERE tx = ?1 AND output_index = ?2",
|
||||
params![id_tx, note.output_index],
|
||||
"SELECT id_note FROM received_notes WHERE tx = ?1 AND output_index = ?2 AND orchard = ?3",
|
||||
params![id_tx, note.output_index, orchard],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(wrap_query_no_rows("store_received_note/id_note"))?;
|
||||
|
@ -365,33 +357,58 @@ impl DbAdapter {
|
|||
height: u32,
|
||||
id_note: u32,
|
||||
connection: &Connection,
|
||||
shielded_pool: &str
|
||||
shielded_pool: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
log::debug!("+store_witness");
|
||||
let mut bb: Vec<u8> = vec![];
|
||||
witness.write(&mut bb)?;
|
||||
connection.execute(
|
||||
&format!("INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)", shielded_pool),
|
||||
&format!(
|
||||
"INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)",
|
||||
shielded_pool
|
||||
),
|
||||
params![id_note, height, bb],
|
||||
)?;
|
||||
log::debug!("-store_witness");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store_block_timestamp(&self, height: u32, hash: &[u8], timestamp: u32) -> anyhow::Result<()> {
|
||||
self.connection.execute("INSERT INTO blocks(height, hash, timestamp) VALUES (?1,?2,?3)", params![height, hash, timestamp])?;
|
||||
pub fn store_block_timestamp(
|
||||
&self,
|
||||
height: u32,
|
||||
hash: &[u8],
|
||||
timestamp: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
self.connection.execute(
|
||||
"INSERT INTO blocks(height, hash, timestamp) VALUES (?1,?2,?3)",
|
||||
params![height, hash, timestamp],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store_tree(height: u32, tree: &CTree, db_tx: &Connection, shielded_pool: &str) -> anyhow::Result<()> {
|
||||
pub fn store_tree(
|
||||
height: u32,
|
||||
tree: &CTree,
|
||||
db_tx: &Connection,
|
||||
shielded_pool: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut bb: Vec<u8> = vec![];
|
||||
tree.write(&mut bb)?;
|
||||
db_tx.execute(&format!("INSERT INTO {}_tree(height, tree) VALUES (?1,?2)", shielded_pool), params![height, &bb])?;
|
||||
db_tx.execute(
|
||||
&format!(
|
||||
"INSERT INTO {}_tree(height, tree) VALUES (?1,?2)",
|
||||
shielded_pool
|
||||
),
|
||||
params![height, &bb],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_transaction_with_memo(&self, details: &TransactionDetails) -> anyhow::Result<()> {
|
||||
self.connection.execute("UPDATE transactions SET address = ?1, memo = ?2 WHERE id_tx = ?3", params![details.address, details.memo, details.id_tx])?;
|
||||
self.connection.execute(
|
||||
"UPDATE transactions SET address = ?1, memo = ?2 WHERE id_tx = ?3",
|
||||
params![details.address, details.memo, details.id_tx],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -447,13 +464,22 @@ impl DbAdapter {
|
|||
}))
|
||||
}
|
||||
|
||||
pub fn get_tree_by_name(&self, height: u32, shielded_pool: &str) -> anyhow::Result<TreeCheckpoint> {
|
||||
let tree = self.connection.query_row(
|
||||
&format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool),
|
||||
[height], |row| {
|
||||
let tree: Vec<u8> = row.get(0)?;
|
||||
Ok(tree)
|
||||
}).optional()?;
|
||||
pub fn get_tree_by_name(
|
||||
&self,
|
||||
height: u32,
|
||||
shielded_pool: &str,
|
||||
) -> anyhow::Result<TreeCheckpoint> {
|
||||
let tree = self
|
||||
.connection
|
||||
.query_row(
|
||||
&format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool),
|
||||
[height],
|
||||
|row| {
|
||||
let tree: Vec<u8> = row.get(0)?;
|
||||
Ok(tree)
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
|
||||
match tree {
|
||||
Some(tree) => {
|
||||
|
@ -474,7 +500,7 @@ impl DbAdapter {
|
|||
None => Ok(TreeCheckpoint {
|
||||
tree: CTree::new(),
|
||||
witnesses: vec![],
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -502,9 +528,7 @@ impl DbAdapter {
|
|||
Ok(nfs)
|
||||
}
|
||||
|
||||
pub fn get_unspent_nullifiers(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<ReceivedNoteShort>> {
|
||||
pub fn get_unspent_nullifiers(&self) -> anyhow::Result<Vec<ReceivedNoteShort>> {
|
||||
let sql = "SELECT id_note, account, nf, value FROM received_notes WHERE spent IS NULL OR spent = 0";
|
||||
let mut statement = self.connection.prepare(sql)?;
|
||||
let nfs_res = statement.query_map(params![], |row| {
|
||||
|
@ -529,7 +553,11 @@ impl DbAdapter {
|
|||
Ok(nfs)
|
||||
}
|
||||
|
||||
pub fn get_unspent_received_notes(&self, account: u32, anchor_height: u32) -> anyhow::Result<Vec<UTXO>> {
|
||||
pub fn get_unspent_received_notes(
|
||||
&self,
|
||||
account: u32,
|
||||
anchor_height: u32,
|
||||
) -> anyhow::Result<Vec<UTXO>> {
|
||||
let mut notes = vec![];
|
||||
let mut statement = self.connection.prepare(
|
||||
"SELECT id_note, diversifier, value, rcm, witness FROM received_notes r, sapling_witnesses w WHERE spent IS NULL AND account = ?2 AND rho IS NULL
|
||||
|
@ -546,7 +574,7 @@ impl DbAdapter {
|
|||
id_note,
|
||||
diversifier: diversifier.try_into().unwrap(),
|
||||
rseed: rcm.try_into().unwrap(),
|
||||
witness
|
||||
witness,
|
||||
};
|
||||
Ok(UTXO {
|
||||
source,
|
||||
|
@ -575,7 +603,7 @@ impl DbAdapter {
|
|||
diversifier: diversifier.try_into().unwrap(),
|
||||
rseed: rcm.try_into().unwrap(),
|
||||
rho: rho.try_into().unwrap(),
|
||||
witness
|
||||
witness,
|
||||
};
|
||||
Ok(UTXO {
|
||||
source,
|
||||
|
@ -787,19 +815,47 @@ impl DbAdapter {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_account_by_fvk(&self, fvk: &str) -> anyhow::Result<Option<u32>> {
|
||||
let account = self
|
||||
.connection
|
||||
.query_row(
|
||||
"SELECT id_account FROM accounts WHERE fvk = ?1",
|
||||
params![fvk],
|
||||
|row| {
|
||||
let account: u32 = row.get(0)?;
|
||||
Ok(account)
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
pub fn get_orchard(&self, account: u32) -> anyhow::Result<Option<OrchardKeyBytes>> {
|
||||
let key = self.connection.query_row("SELECT sk, fvk FROM orchard_addrs WHERE account = ?1", params![account], |row| {
|
||||
let sk: Vec<u8> = row.get(0)?;
|
||||
let fvk: Vec<u8> = row.get(1)?;
|
||||
Ok(OrchardKeyBytes {
|
||||
sk: sk.try_into().unwrap(),
|
||||
fvk: fvk.try_into().unwrap(),
|
||||
})
|
||||
}).optional()?;
|
||||
let key = self
|
||||
.connection
|
||||
.query_row(
|
||||
"SELECT sk, fvk FROM orchard_addrs WHERE account = ?1",
|
||||
params![account],
|
||||
|row| {
|
||||
let sk: Vec<u8> = row.get(0)?;
|
||||
let fvk: Vec<u8> = row.get(1)?;
|
||||
Ok(OrchardKeyBytes {
|
||||
sk: sk.try_into().unwrap(),
|
||||
fvk: fvk.try_into().unwrap(),
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
pub fn store_ua_settings(&self, account: u32, transparent: bool, sapling: bool, orchard: bool) -> anyhow::Result<()> {
|
||||
pub fn store_ua_settings(
|
||||
&self,
|
||||
account: u32,
|
||||
transparent: bool,
|
||||
sapling: bool,
|
||||
orchard: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
self.connection.execute(
|
||||
"INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![account, transparent, sapling, orchard],
|
||||
|
@ -808,16 +864,20 @@ impl DbAdapter {
|
|||
}
|
||||
|
||||
pub fn get_ua_settings(&self, account: u32) -> anyhow::Result<UnifiedAddressType> {
|
||||
let tpe = self.connection.query_row("SELECT transparent, sapling, orchard FROM ua_settings WHERE account = ?1", params![account], |row| {
|
||||
let transparent: bool = row.get(0)?;
|
||||
let sapling: bool = row.get(1)?;
|
||||
let orchard: bool = row.get(2)?;
|
||||
Ok(UnifiedAddressType {
|
||||
transparent,
|
||||
sapling,
|
||||
orchard
|
||||
})
|
||||
})?;
|
||||
let tpe = self.connection.query_row(
|
||||
"SELECT transparent, sapling, orchard FROM ua_settings WHERE account = ?1",
|
||||
params![account],
|
||||
|row| {
|
||||
let transparent: bool = row.get(0)?;
|
||||
let sapling: bool = row.get(1)?;
|
||||
let orchard: bool = row.get(2)?;
|
||||
Ok(UnifiedAddressType {
|
||||
transparent,
|
||||
sapling,
|
||||
orchard,
|
||||
})
|
||||
},
|
||||
)?;
|
||||
Ok(tpe)
|
||||
}
|
||||
|
||||
|
@ -1051,7 +1111,9 @@ impl DbAdapter {
|
|||
}
|
||||
|
||||
pub fn get_txid_without_memo(&self) -> anyhow::Result<Vec<GetTransactionDetailRequest>> {
|
||||
let mut stmt = self.connection.prepare("SELECT account, id_tx, height, txid FROM transactions WHERE memo IS NULL")?;
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT account, id_tx, height, txid FROM transactions WHERE memo IS NULL")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let account: u32 = row.get(0)?;
|
||||
let id_tx: u32 = row.get(1)?;
|
||||
|
@ -1223,9 +1285,9 @@ pub struct AccountData {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::{DbAdapter, DEFAULT_DB_PATH, ReceivedNote};
|
||||
use zcash_params::coin::CoinType;
|
||||
use crate::db::{DbAdapter, ReceivedNote, DEFAULT_DB_PATH};
|
||||
use crate::sync::{CTree, Witness};
|
||||
use zcash_params::coin::CoinType;
|
||||
|
||||
#[test]
|
||||
fn test_balance() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::orchard::derive_orchard_keys;
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use zcash_primitives::consensus::{Network, Parameters};
|
||||
use crate::orchard::derive_orchard_keys;
|
||||
|
||||
pub fn get_schema_version(connection: &Connection) -> anyhow::Result<u32> {
|
||||
let version: Option<u32> = connection
|
||||
|
@ -180,23 +180,38 @@ pub fn init_db(connection: &Connection, network: &Network) -> anyhow::Result<()>
|
|||
}
|
||||
|
||||
if version < 5 {
|
||||
connection.execute("CREATE TABLE orchard_addrs(
|
||||
connection.execute(
|
||||
"CREATE TABLE orchard_addrs(
|
||||
account INTEGER PRIMARY KEY,
|
||||
sk BLOB,
|
||||
fvk BLOB NOT NULL)", [])?;
|
||||
connection.execute("CREATE TABLE ua_settings(
|
||||
fvk BLOB NOT NULL)",
|
||||
[],
|
||||
)?;
|
||||
connection.execute(
|
||||
"CREATE TABLE ua_settings(
|
||||
account INTEGER PRIMARY KEY,
|
||||
transparent BOOL NOT NULL,
|
||||
sapling BOOL NOT NULL,
|
||||
orchard BOOL NOT NULL)", [])?;
|
||||
orchard BOOL NOT NULL)",
|
||||
[],
|
||||
)?;
|
||||
upgrade_accounts(&connection, network)?;
|
||||
connection.execute("CREATE TABLE sapling_tree(
|
||||
connection.execute(
|
||||
"CREATE TABLE sapling_tree(
|
||||
height INTEGER PRIMARY KEY,
|
||||
tree BLOB NOT NULL)", [])?;
|
||||
connection.execute("CREATE TABLE orchard_tree(
|
||||
tree BLOB NOT NULL)",
|
||||
[],
|
||||
)?;
|
||||
connection.execute(
|
||||
"CREATE TABLE orchard_tree(
|
||||
height INTEGER PRIMARY KEY,
|
||||
tree BLOB NOT NULL)", [])?;
|
||||
connection.execute("INSERT INTO sapling_tree SELECT height, sapling_tree FROM blocks", [])?;
|
||||
tree BLOB NOT NULL)",
|
||||
[],
|
||||
)?;
|
||||
connection.execute(
|
||||
"INSERT INTO sapling_tree SELECT height, sapling_tree FROM blocks",
|
||||
[],
|
||||
)?;
|
||||
connection.execute("ALTER TABLE blocks DROP sapling_tree", [])?;
|
||||
connection.execute(
|
||||
"CREATE TABLE IF NOT EXISTS new_received_notes (
|
||||
|
@ -217,12 +232,18 @@ pub fn init_db(connection: &Connection, network: &Network) -> anyhow::Result<()>
|
|||
CONSTRAINT tx_output UNIQUE (tx, orchard, output_index))",
|
||||
[],
|
||||
)?;
|
||||
connection.execute("INSERT INTO new_received_notes(
|
||||
connection.execute(
|
||||
"INSERT INTO new_received_notes(
|
||||
id_note, account, position, tx, height, output_index, diversifier, value,
|
||||
rcm, nf, spent, excluded
|
||||
) SELECT * FROM received_notes", [])?;
|
||||
) SELECT * FROM received_notes",
|
||||
[],
|
||||
)?;
|
||||
connection.execute("DROP TABLE received_notes", [])?;
|
||||
connection.execute("ALTER TABLE new_received_notes RENAME TO received_notes", [])?;
|
||||
connection.execute(
|
||||
"ALTER TABLE new_received_notes RENAME TO received_notes",
|
||||
[],
|
||||
)?;
|
||||
connection.execute(
|
||||
"CREATE TABLE IF NOT EXISTS orchard_witnesses (
|
||||
id_witness INTEGER PRIMARY KEY,
|
||||
|
@ -282,10 +303,15 @@ fn upgrade_accounts(connection: &Connection, network: &Network) -> anyhow::Resul
|
|||
let has_orchard = seed.is_some();
|
||||
if let Some(seed) = seed {
|
||||
let orchard_keys = derive_orchard_keys(network.coin_type(), &seed, aindex);
|
||||
connection.execute("INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1,?2,?3)", params![id_account, &orchard_keys.sk, &orchard_keys.fvk])?;
|
||||
connection.execute(
|
||||
"INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1,?2,?3)",
|
||||
params![id_account, &orchard_keys.sk, &orchard_keys.fvk],
|
||||
)?;
|
||||
}
|
||||
connection.execute("INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1,?2,?3,?4)",
|
||||
params![id_account, has_transparent, true, has_orchard])?;
|
||||
connection.execute(
|
||||
"INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1,?2,?3,?4)",
|
||||
params![id_account, has_transparent, true, has_orchard],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::chain::DecryptedBlock;
|
||||
use crate::gpu::{collect_nf, GPUProcessor};
|
||||
use crate::lw_rpc::CompactBlock;
|
||||
use crate::{Hash, hash::GENERATORS_EXP};
|
||||
use crate::{hash::GENERATORS_EXP, Hash};
|
||||
use anyhow::Result;
|
||||
use ff::BatchInverter;
|
||||
use jubjub::Fq;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::Hash;
|
||||
use ff::PrimeField;
|
||||
use group::{Curve, GroupEncoding};
|
||||
use jubjub::{ExtendedNielsPoint, ExtendedPoint, Fr, SubgroupPoint};
|
||||
|
@ -6,7 +7,6 @@ use std::io::Read;
|
|||
use std::ops::AddAssign;
|
||||
use zcash_params::GENERATORS;
|
||||
use zcash_primitives::constants::PEDERSEN_HASH_CHUNKS_PER_GENERATOR;
|
||||
use crate::Hash;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref GENERATORS_EXP: Vec<ExtendedNielsPoint> = read_generators_bin();
|
||||
|
|
|
@ -41,11 +41,11 @@ pub fn is_valid_key(coin: u8, key: &str) -> i8 {
|
|||
if Mnemonic::from_phrase(key, Language::English).is_ok() {
|
||||
return 0;
|
||||
}
|
||||
if decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), key).is_ok()
|
||||
{
|
||||
if decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), key).is_ok() {
|
||||
return 1;
|
||||
}
|
||||
if decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), key).is_ok()
|
||||
if decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), key)
|
||||
.is_ok()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
@ -61,7 +61,6 @@ pub fn is_valid_address(coin: u8, address: &str) -> bool {
|
|||
recipient.is_some()
|
||||
}
|
||||
|
||||
|
||||
fn derive_secret_key(
|
||||
network: &Network,
|
||||
mnemonic: &Mnemonic,
|
||||
|
|
25
src/lib.rs
25
src/lib.rs
|
@ -80,9 +80,9 @@ mod contact;
|
|||
mod db;
|
||||
mod fountain;
|
||||
mod hash;
|
||||
mod sync;
|
||||
mod sapling;
|
||||
mod orchard;
|
||||
mod sapling;
|
||||
mod sync;
|
||||
// mod key;
|
||||
mod key2;
|
||||
mod mempool;
|
||||
|
@ -90,9 +90,9 @@ mod misc;
|
|||
mod pay;
|
||||
mod prices;
|
||||
// mod print;
|
||||
mod note_selection;
|
||||
mod scan;
|
||||
mod taddr;
|
||||
mod note_selection;
|
||||
mod transaction;
|
||||
mod unified;
|
||||
// mod ua;
|
||||
|
@ -115,15 +115,11 @@ mod ledger {
|
|||
}
|
||||
}
|
||||
|
||||
pub use crate::chain::{
|
||||
connect_lightwalletd, get_best_server,
|
||||
ChainError,
|
||||
};
|
||||
pub use crate::chain::{connect_lightwalletd, get_best_server, ChainError};
|
||||
pub use crate::coinconfig::{
|
||||
CoinConfig, init_coin, set_active, set_active_account, set_coin_lwd_url,
|
||||
COIN_CONFIG,
|
||||
init_coin, set_active, set_active_account, set_coin_lwd_url, CoinConfig, COIN_CONFIG,
|
||||
};
|
||||
pub use crate::db::{AccountData, AccountInfo, AccountRec, DbAdapter, TxRec, DbAdapterBuilder};
|
||||
pub use crate::db::{AccountData, AccountInfo, AccountRec, DbAdapter, DbAdapterBuilder, TxRec};
|
||||
pub use crate::fountain::{FountainCodes, RaptorQDrops};
|
||||
// pub use crate::key::KeyHelpers;
|
||||
pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient;
|
||||
|
@ -132,7 +128,13 @@ pub use crate::pay::{broadcast_tx, Tx, TxIn, TxOut};
|
|||
pub use zip32::KeyPack;
|
||||
// pub use crate::wallet::{decrypt_backup, encrypt_backup, RecipientMemo, Wallet, WalletBalance};
|
||||
|
||||
pub use unified::{get_unified_address, decode_unified_address};
|
||||
pub use note_selection::{
|
||||
build_tx_plan, build_tx, get_secret_keys,
|
||||
TransactionPlan, TransactionBuilderConfig,
|
||||
TxBuilderContext,
|
||||
fetch_utxos,
|
||||
};
|
||||
pub use unified::{decode_unified_address, get_unified_address};
|
||||
|
||||
#[cfg(feature = "ledger_sapling")]
|
||||
pub use crate::ledger::sapling::build_tx_ledger;
|
||||
|
@ -150,3 +152,4 @@ pub fn init_test() {
|
|||
init_coin(0, "./zec.db").unwrap();
|
||||
set_coin_lwd_url(0, "http://127.0.0.1:9067");
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
use thiserror::Error;
|
||||
use warp_api_ffi::{fetch_utxos, init_test, note_select_with_fee_v2, TransactionBuilderConfig, TransactionPlan};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
init_test();
|
||||
|
||||
let config = TransactionBuilderConfig::new();
|
||||
|
||||
let utxos = fetch_utxos(0, 1, 220, true, 0).await.unwrap();
|
||||
let mut orders = vec![];
|
||||
|
||||
note_select_with_fee_v2("", 0, &utxos, &mut orders, &config).unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ extern crate rocket;
|
|||
|
||||
use anyhow::anyhow;
|
||||
use lazy_static::lazy_static;
|
||||
use rand::rngs::OsRng;
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::http::Status;
|
||||
use rocket::response::Responder;
|
||||
|
@ -13,9 +14,9 @@ use std::fs::File;
|
|||
use std::io::{BufReader, Read};
|
||||
use std::sync::Mutex;
|
||||
use thiserror::Error;
|
||||
use warp_api_ffi::api::payment::{Recipient, RecipientMemo};
|
||||
use warp_api_ffi::api::payment::{Recipient, RecipientMemo, RecipientShort};
|
||||
use warp_api_ffi::api::payment_uri::PaymentURI;
|
||||
use warp_api_ffi::{get_best_server, AccountData, AccountInfo, AccountRec, CoinConfig, KeyPack, Tx, TxRec, RaptorQDrops};
|
||||
use warp_api_ffi::{build_tx, get_best_server, get_secret_keys, AccountData, AccountInfo, AccountRec, CoinConfig, KeyPack, RaptorQDrops, TransactionPlan, Tx, TxRec, TransactionBuilderConfig, TxBuilderContext};
|
||||
|
||||
lazy_static! {
|
||||
static ref SYNC_CANCELED: Mutex<bool> = Mutex::new(false);
|
||||
|
@ -101,6 +102,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
merge_data,
|
||||
derive_keys,
|
||||
instant_sync,
|
||||
get_tx_plan,
|
||||
build_from_plan,
|
||||
],
|
||||
)
|
||||
.attach(AdHoc::config::<Config>())
|
||||
|
@ -182,7 +185,13 @@ pub fn get_address() -> Result<String, Error> {
|
|||
#[get("/unified_address?<t>&<s>&<o>")]
|
||||
pub fn get_unified_address(t: u8, s: u8, o: u8) -> Result<String, Error> {
|
||||
let c = CoinConfig::get_active();
|
||||
let address = warp_api_ffi::api::account::get_unified_address(c.coin, c.id_account, t != 0, s != 0, o != 0)?;
|
||||
let address = warp_api_ffi::api::account::get_unified_address(
|
||||
c.coin,
|
||||
c.id_account,
|
||||
t != 0,
|
||||
s != 0,
|
||||
o != 0,
|
||||
)?;
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
|
@ -292,6 +301,60 @@ pub async fn broadcast_tx(tx_hex: String) -> Result<String, Error> {
|
|||
Ok(tx_id)
|
||||
}
|
||||
|
||||
#[post(
|
||||
"/get_tx_plan?<confirmations>",
|
||||
data = "<recipients>"
|
||||
)]
|
||||
pub async fn get_tx_plan(
|
||||
confirmations: u32,
|
||||
recipients: Json<Vec<RecipientShort>>,
|
||||
) -> Result<Json<TransactionPlan>, Error> {
|
||||
let c = CoinConfig::get_active();
|
||||
let coin = c.coin;
|
||||
let account = c.id_account;
|
||||
let last_height = warp_api_ffi::api::sync::get_latest_height().await?;
|
||||
let change_address =
|
||||
warp_api_ffi::api::account::get_unified_address(coin, account, true, true, true)?;
|
||||
let recipients: Vec<_> = recipients
|
||||
.iter()
|
||||
.map(|r| RecipientMemo::from(r.clone()))
|
||||
.collect();
|
||||
let config = TransactionBuilderConfig::new(&change_address);
|
||||
|
||||
let plan = warp_api_ffi::api::payment_v2::build_tx_plan(
|
||||
coin,
|
||||
account,
|
||||
last_height,
|
||||
&recipients,
|
||||
&config,
|
||||
confirmations,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(plan))
|
||||
}
|
||||
|
||||
#[post("/build_from_plan", data = "<tx_plan>")]
|
||||
pub async fn build_from_plan(tx_plan: Json<TransactionPlan>) -> Result<String, Error> {
|
||||
let c = CoinConfig::get_active();
|
||||
let fvk = {
|
||||
let db = c.db()?;
|
||||
let AccountData { fvk, .. } = db.get_account_info(c.id_account)?;
|
||||
fvk
|
||||
};
|
||||
|
||||
if fvk != tx_plan.fvk {
|
||||
return Err(Error::Other(anyhow::anyhow!(
|
||||
"Account does not match transaction"
|
||||
)));
|
||||
}
|
||||
|
||||
let keys = get_secret_keys(c.coin, c.id_account)?;
|
||||
let context = TxBuilderContext::from_height(c.coin, tx_plan.height)?;
|
||||
let tx = build_tx(c.chain.network(), &keys, &tx_plan, context, OsRng).unwrap();
|
||||
let tx = hex::encode(&tx);
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
#[get("/new_diversified_address")]
|
||||
pub fn new_diversified_address() -> Result<String, Error> {
|
||||
let address = warp_api_ffi::api::account::new_diversified_address()?;
|
||||
|
|
|
@ -6,7 +6,9 @@ use tonic::Request;
|
|||
|
||||
use crate::coinconfig::CoinConfig;
|
||||
use zcash_primitives::consensus::BlockHeight;
|
||||
use zcash_primitives::sapling::note_encryption::{PreparedIncomingViewingKey, try_sapling_compact_note_decryption};
|
||||
use zcash_primitives::sapling::note_encryption::{
|
||||
try_sapling_compact_note_decryption, PreparedIncomingViewingKey,
|
||||
};
|
||||
use zcash_primitives::sapling::SaplingIvk;
|
||||
|
||||
const DEFAULT_EXCLUDE_LEN: u8 = 1;
|
||||
|
|
|
@ -1,48 +1,53 @@
|
|||
use std::str::FromStr;
|
||||
pub use crate::note_selection::TransactionBuilderError::TxTooComplex;
|
||||
pub use crate::note_selection::types::{
|
||||
UTXO, Order, RecipientShort, TransactionBuilderConfig, TransactionPlan,
|
||||
Source, Destination };
|
||||
pub use utxo::fetch_utxos;
|
||||
pub use builder::{TxBuilderContext, get_secret_keys, build_tx};
|
||||
pub use optimize::build_tx_plan;
|
||||
pub use fee::{FeeCalculator, FeeZIP327};
|
||||
|
||||
use thiserror::Error;
|
||||
use zcash_primitives::memo::Memo;
|
||||
use ua::decode;
|
||||
use optimize::{allocate_funds, fill, group_orders, outputs_for_change, select_inputs, sum_utxos};
|
||||
use crate::api::payment::Recipient;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TransactionBuilderError {
|
||||
#[error("Not enough funds")]
|
||||
NotEnoughFunds,
|
||||
#[error("Tx too complex")]
|
||||
TxTooComplex,
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TransactionBuilderError>;
|
||||
|
||||
mod types;
|
||||
mod fee;
|
||||
mod ser;
|
||||
mod ua;
|
||||
mod utxo;
|
||||
mod fill;
|
||||
mod select;
|
||||
mod optimize;
|
||||
mod fee;
|
||||
mod builder;
|
||||
|
||||
const MAX_ATTEMPTS: usize = 10;
|
||||
|
||||
pub fn recipients_to_orders(recipients: &[Recipient]) -> Result<Vec<Order>> {
|
||||
let orders: Result<Vec<_>> = recipients.iter().enumerate().map(|(i, r)| {
|
||||
let destinations = decode(&r.address)?;
|
||||
Ok::<_, TransactionBuilderError>(Order {
|
||||
id: i as u32,
|
||||
destinations,
|
||||
amount: r.amount,
|
||||
memo: Memo::from_str(&r.memo).unwrap().into(),
|
||||
})
|
||||
}).collect();
|
||||
Ok(orders?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::cmp::min;
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
pub use types::{Source, Destination, PrivacyPolicy, Pool, UTXO, NoteSelectConfig};
|
||||
pub use utxo::fetch_utxos;
|
||||
pub use fill::decode;
|
||||
pub use select::note_select_with_fee;
|
||||
pub use fee::{FeeZIP327, FeeFlat};
|
||||
use crate::api::payment::RecipientMemo;
|
||||
use crate::note_selection::types::TransactionPlan;
|
||||
|
||||
async fn prepare_multi_payment(
|
||||
coin: u8,
|
||||
account: u32,
|
||||
last_height: u32,
|
||||
recipients: &[RecipientMemo],
|
||||
config: &NoteSelectConfig,
|
||||
anchor_offset: u32) -> anyhow::Result<TransactionPlan>
|
||||
{
|
||||
let mut orders = vec![];
|
||||
let mut id_order = 0;
|
||||
for r in recipients {
|
||||
let mut amount = r.amount;
|
||||
let max_amount_per_note = if r.max_amount_per_note == 0 { u64::MAX } else { r.max_amount_per_note };
|
||||
while amount > 0 {
|
||||
let a = min(amount, max_amount_per_note);
|
||||
let memo_bytes: MemoBytes = r.memo.clone().into();
|
||||
let order = decode(id_order, &r.address, a, memo_bytes)?;
|
||||
orders.push(order);
|
||||
amount -= a;
|
||||
id_order += 1;
|
||||
}
|
||||
}
|
||||
let utxos = fetch_utxos(coin, account, last_height, config.use_transparent, anchor_offset).await?;
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, config)?;
|
||||
|
||||
Ok(tx_plan)
|
||||
}
|
||||
|
|
|
@ -1,39 +1,41 @@
|
|||
use std::str::FromStr;
|
||||
use crate::coinconfig::get_prover;
|
||||
use super::types::*;
|
||||
use super::{decode};
|
||||
use crate::orchard::{get_proving_key, OrchardHasher, ORCHARD_ROOTS};
|
||||
use crate::sapling::{SaplingHasher, SAPLING_ROOTS};
|
||||
use crate::sync::tree::TreeCheckpoint;
|
||||
use crate::sync::Witness;
|
||||
use crate::{broadcast_tx, init_coin, set_active, set_coin_lwd_url, AccountData, CoinConfig};
|
||||
use anyhow::anyhow;
|
||||
use jubjub::Fr;
|
||||
use orchard::{Address, Anchor, Bundle};
|
||||
use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey};
|
||||
use secp256k1::{All, PublicKey, Secp256k1, SecretKey};
|
||||
use zcash_primitives::consensus::{BlockHeight, BranchId, Network, Parameters};
|
||||
use zcash_primitives::transaction::builder::Builder;
|
||||
use orchard::builder::Builder as OrchardBuilder;
|
||||
use orchard::bundle::Flags;
|
||||
use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey};
|
||||
use orchard::note::Nullifier;
|
||||
use orchard::value::NoteValue;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use orchard::{Address, Anchor, Bundle};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use ripemd::{Digest, Ripemd160};
|
||||
use secp256k1::{All, PublicKey, Secp256k1, SecretKey};
|
||||
use sha2::Sha256;
|
||||
use std::str::FromStr;
|
||||
use zcash_client_backend::encoding::decode_extended_spending_key;
|
||||
use zcash_primitives::consensus::{BlockHeight, BranchId, Network, Parameters};
|
||||
use zcash_primitives::legacy::TransparentAddress;
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use zcash_primitives::merkle_tree::IncrementalWitness;
|
||||
use zcash_primitives::sapling::{Diversifier, Node, PaymentAddress, Rseed};
|
||||
use zcash_primitives::sapling::prover::TxProver;
|
||||
use zcash_primitives::sapling::{Diversifier, Node, PaymentAddress, Rseed};
|
||||
use zcash_primitives::transaction::builder::Builder;
|
||||
use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut};
|
||||
use zcash_primitives::transaction::{Transaction, TransactionData, TxVersion};
|
||||
use zcash_primitives::transaction::sighash::SignableInput;
|
||||
use zcash_primitives::transaction::sighash_v5::v5_signature_hash;
|
||||
use zcash_primitives::transaction::txid::TxIdDigester;
|
||||
use zcash_primitives::transaction::{Transaction, TransactionData, TxVersion};
|
||||
use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
|
||||
use crate::{AccountData, broadcast_tx, CoinConfig, init_coin, set_active, set_coin_lwd_url};
|
||||
use crate::coinconfig::get_prover;
|
||||
use crate::note_selection::types::TransactionPlan;
|
||||
use crate::note_selection::{decode, Destination, FeeFlat, fetch_utxos, note_select_with_fee, NoteSelectConfig, PrivacyPolicy, Source};
|
||||
use crate::orchard::{get_proving_key, ORCHARD_ROOTS, OrchardHasher};
|
||||
use crate::sapling::{SAPLING_ROOTS, SaplingHasher};
|
||||
use crate::sync::tree::TreeCheckpoint;
|
||||
use crate::sync::Witness;
|
||||
use crate::note_selection::fee::FeeFlat;
|
||||
use crate::note_selection::{build_tx_plan, fetch_utxos};
|
||||
|
||||
pub struct SecretKeys {
|
||||
pub transparent: Option<SecretKey>,
|
||||
|
@ -41,24 +43,24 @@ pub struct SecretKeys {
|
|||
pub orchard: Option<SpendingKey>,
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
pub struct TxBuilderContext {
|
||||
pub height: u32,
|
||||
pub sapling_anchor: [u8; 32],
|
||||
pub orchard_anchor: [u8; 32],
|
||||
}
|
||||
|
||||
impl Context {
|
||||
impl TxBuilderContext {
|
||||
pub fn from_height(coin: u8, height: u32) -> anyhow::Result<Self> {
|
||||
let c = CoinConfig::get(coin);
|
||||
let db = c.db.as_ref().unwrap();
|
||||
let db = db.lock().unwrap();
|
||||
let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "sapling")?;
|
||||
let hasher = SaplingHasher {};
|
||||
let sapling_anchor= tree.root(32, &SAPLING_ROOTS, &hasher);
|
||||
let sapling_anchor = tree.root(32, &SAPLING_ROOTS, &hasher);
|
||||
let TreeCheckpoint { tree, .. } = db.get_tree_by_name(height, "orchard")?;
|
||||
let hasher = OrchardHasher::new();
|
||||
let orchard_anchor= tree.root(32, &ORCHARD_ROOTS, &hasher);
|
||||
let context = Context {
|
||||
let orchard_anchor = tree.root(32, &ORCHARD_ROOTS, &hasher);
|
||||
let context = TxBuilderContext {
|
||||
height,
|
||||
sapling_anchor,
|
||||
orchard_anchor,
|
||||
|
@ -69,8 +71,13 @@ impl Context {
|
|||
|
||||
const EXPIRY_HEIGHT: u32 = 50;
|
||||
|
||||
// TODO: Remove unwrap()
|
||||
pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, context: Context, mut rng: impl RngCore + CryptoRng + Clone) -> anyhow::Result<Vec<u8>> {
|
||||
pub fn build_tx(
|
||||
network: &Network,
|
||||
skeys: &SecretKeys,
|
||||
plan: &TransactionPlan,
|
||||
context: TxBuilderContext,
|
||||
mut rng: impl RngCore + CryptoRng + Clone,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let secp = Secp256k1::<All>::new();
|
||||
let transparent_address = skeys.transparent.map(|tkey| {
|
||||
let pub_key = PublicKey::from_secret_key(&secp, &tkey);
|
||||
|
@ -94,7 +101,9 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
|
||||
let mut has_orchard = false;
|
||||
let mut builder = Builder::new(*network, BlockHeight::from_u32(context.height));
|
||||
let anchor: Anchor = orchard::tree::MerkleHashOrchard::from_bytes(&context.orchard_anchor).unwrap().into();
|
||||
let anchor: Anchor = orchard::tree::MerkleHashOrchard::from_bytes(&context.orchard_anchor)
|
||||
.unwrap()
|
||||
.into();
|
||||
let mut orchard_builder = OrchardBuilder::new(Flags::from_parts(true, true), anchor);
|
||||
for spend in plan.spends.iter() {
|
||||
match &spend.source {
|
||||
|
@ -102,11 +111,18 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
let utxo = OutPoint::new(*txid, *index);
|
||||
let coin = TxOut {
|
||||
value: Amount::from_u64(spend.amount).unwrap(),
|
||||
script_pubkey: transparent_address.ok_or(anyhow!("No transparent key")).map(|ta| ta.script())?,
|
||||
script_pubkey: transparent_address
|
||||
.ok_or(anyhow!("No transparent key"))
|
||||
.map(|ta| ta.script())?,
|
||||
};
|
||||
builder.add_transparent_input(skeys.transparent.unwrap(), utxo, coin)?;
|
||||
}
|
||||
Source::Sapling { diversifier, rseed, witness, .. } => {
|
||||
Source::Sapling {
|
||||
diversifier,
|
||||
rseed,
|
||||
witness,
|
||||
..
|
||||
} => {
|
||||
let diversifier = Diversifier(*diversifier);
|
||||
let sapling_address = sapling_fvk.fvk.vk.to_payment_address(diversifier).unwrap();
|
||||
let rseed = Rseed::BeforeZip212(Fr::from_bytes(rseed).unwrap());
|
||||
|
@ -115,21 +131,36 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
let merkle_path = witness.path().unwrap();
|
||||
builder.add_sapling_spend(skeys.sapling.clone(), diversifier, note, merkle_path)?;
|
||||
}
|
||||
Source::Orchard { id_note, diversifier, rho, rseed, witness} => {
|
||||
Source::Orchard {
|
||||
id_note,
|
||||
diversifier,
|
||||
rho,
|
||||
rseed,
|
||||
witness,
|
||||
} => {
|
||||
has_orchard = true;
|
||||
let diversifier = orchard::keys::Diversifier::from_bytes(*diversifier);
|
||||
let sender_address = orchard_fvk.as_ref().ok_or(anyhow!("No Orchard key")).map(|fvk| fvk.address(diversifier, Scope::External))?;
|
||||
let sender_address = orchard_fvk
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("No Orchard key"))
|
||||
.map(|fvk| fvk.address(diversifier, Scope::External))?;
|
||||
let value = NoteValue::from_raw(spend.amount);
|
||||
let rho = Nullifier::from_bytes(&rho).unwrap();
|
||||
let rseed = orchard::note::RandomSeed::from_bytes(*rseed, &rho).unwrap();
|
||||
let note = orchard::Note::from_parts(sender_address, value, rho, rseed).unwrap();
|
||||
let witness = Witness::from_bytes(*id_note, &witness)?;
|
||||
let auth_path: Vec<_> = witness.auth_path(32, &ORCHARD_ROOTS, &OrchardHasher::new()).iter()
|
||||
.map(|n| {
|
||||
orchard::tree::MerkleHashOrchard::from_bytes(n).unwrap()
|
||||
}).collect();
|
||||
let merkle_path = orchard::tree::MerklePath::from_parts(witness.position as u32, auth_path.try_into().unwrap());
|
||||
orchard_builder.add_spend(orchard_fvk.clone().unwrap(), note, merkle_path).map_err(|e| anyhow!(e.to_string()))?;
|
||||
let auth_path: Vec<_> = witness
|
||||
.auth_path(32, &ORCHARD_ROOTS, &OrchardHasher::new())
|
||||
.iter()
|
||||
.map(|n| orchard::tree::MerkleHashOrchard::from_bytes(n).unwrap())
|
||||
.collect();
|
||||
let merkle_path = orchard::tree::MerklePath::from_parts(
|
||||
witness.position as u32,
|
||||
auth_path.try_into().unwrap(),
|
||||
);
|
||||
orchard_builder
|
||||
.add_spend(orchard_fvk.clone().unwrap(), note, merkle_path)
|
||||
.map_err(|e| anyhow!(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,26 +174,40 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
}
|
||||
Destination::Sapling(addr) => {
|
||||
let sapling_address = PaymentAddress::from_bytes(addr).unwrap();
|
||||
builder.add_sapling_output(Some(sapling_ovk), sapling_address, value, output.memo.clone())?;
|
||||
builder.add_sapling_output(
|
||||
Some(sapling_ovk),
|
||||
sapling_address,
|
||||
value,
|
||||
output.memo.clone(),
|
||||
)?;
|
||||
}
|
||||
Destination::Orchard(addr) => {
|
||||
has_orchard = true;
|
||||
let orchard_address = Address::from_raw_address_bytes(addr).unwrap();
|
||||
orchard_builder.add_recipient(orchard_ovk.clone(), orchard_address,
|
||||
NoteValue::from_raw(output.amount), Some(*output.memo.as_array())).map_err(|_| anyhow!("Orchard::add_recipient"))?;
|
||||
orchard_builder
|
||||
.add_recipient(
|
||||
orchard_ovk.clone(),
|
||||
orchard_address,
|
||||
NoteValue::from_raw(output.amount),
|
||||
Some(*output.memo.as_array()),
|
||||
)
|
||||
.map_err(|_| anyhow!("Orchard::add_recipient"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let transparent_bundle = builder.transparent_builder.build();
|
||||
let mut ctx = get_prover().new_sapling_proving_context();
|
||||
let sapling_bundle = builder.sapling_builder
|
||||
let sapling_bundle = builder
|
||||
.sapling_builder
|
||||
.build(
|
||||
get_prover(),
|
||||
&mut ctx,
|
||||
&mut rng,
|
||||
BlockHeight::from_u32(context.height), None
|
||||
).unwrap();
|
||||
BlockHeight::from_u32(context.height),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut orchard_bundle: Option<Bundle<_, Amount>> = None;
|
||||
if has_orchard {
|
||||
|
@ -185,12 +230,15 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
let sig_hash = v5_signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
|
||||
let sig_hash: [u8; 32] = sig_hash.as_bytes().try_into().unwrap();
|
||||
|
||||
let transparent_bundle = unauthed_tx.transparent_bundle().map(|tb| {
|
||||
tb.clone().apply_signatures(&unauthed_tx, &txid_parts)
|
||||
});
|
||||
let transparent_bundle = unauthed_tx
|
||||
.transparent_bundle()
|
||||
.map(|tb| tb.clone().apply_signatures(&unauthed_tx, &txid_parts));
|
||||
|
||||
let sapling_bundle = unauthed_tx.sapling_bundle().map(|sb| {
|
||||
sb.clone().apply_signatures(get_prover(), &mut ctx, &mut rng, &sig_hash).unwrap().0
|
||||
sb.clone()
|
||||
.apply_signatures(get_prover(), &mut ctx, &mut rng, &sig_hash)
|
||||
.unwrap()
|
||||
.0
|
||||
});
|
||||
|
||||
let mut orchard_signing_keys = vec![];
|
||||
|
@ -198,11 +246,15 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
orchard_signing_keys.push(SpendAuthorizingKey::from(&sk));
|
||||
}
|
||||
|
||||
let orchard_bundle = unauthed_tx.orchard_bundle()
|
||||
.map(|ob| {
|
||||
let proven = ob.clone().create_proof(get_proving_key(), rng.clone()).unwrap();
|
||||
proven.apply_signatures(rng.clone(), sig_hash, &orchard_signing_keys).unwrap()
|
||||
});
|
||||
let orchard_bundle = unauthed_tx.orchard_bundle().map(|ob| {
|
||||
let proven = ob
|
||||
.clone()
|
||||
.create_proof(get_proving_key(), rng.clone())
|
||||
.unwrap();
|
||||
proven
|
||||
.apply_signatures(rng.clone(), sig_hash, &orchard_signing_keys)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let tx_data: TransactionData<zcash_primitives::transaction::Authorized> =
|
||||
TransactionData::from_parts(
|
||||
|
@ -225,20 +277,23 @@ pub fn build_tx(network: &Network, skeys: &SecretKeys, plan: &TransactionPlan, c
|
|||
|
||||
pub fn get_secret_keys(coin: u8, account: u32) -> anyhow::Result<SecretKeys> {
|
||||
let c = CoinConfig::get(coin);
|
||||
let db = c.db.as_ref().unwrap();
|
||||
let db = db.lock().unwrap();
|
||||
let db = c.db()?;
|
||||
|
||||
let transparent_sk = db.get_tsk(account)?.map(|tsk| {
|
||||
SecretKey::from_str(&tsk).unwrap()
|
||||
});
|
||||
let transparent_sk = db
|
||||
.get_tsk(account)?
|
||||
.map(|tsk| SecretKey::from_str(&tsk).unwrap());
|
||||
|
||||
let AccountData { sk, .. } = db.get_account_info(account)?;
|
||||
let sapling_sk = sk.ok_or(anyhow!("No secret key"))?;
|
||||
let sapling_sk = decode_extended_spending_key(c.chain.network().hrp_sapling_extended_spending_key(), &sapling_sk).unwrap();
|
||||
let sapling_sk = decode_extended_spending_key(
|
||||
c.chain.network().hrp_sapling_extended_spending_key(),
|
||||
&sapling_sk,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let orchard_sk = db.get_orchard(account)?.map(|ob| {
|
||||
SpendingKey::from_bytes(ob.sk).unwrap()
|
||||
});
|
||||
let orchard_sk = db
|
||||
.get_orchard(account)?
|
||||
.map(|ob| SpendingKey::from_bytes(ob.sk).unwrap());
|
||||
|
||||
let sk = SecretKeys {
|
||||
transparent: transparent_sk,
|
||||
|
@ -264,15 +319,13 @@ async fn dummy_test() {
|
|||
log::info!("Height {}", height);
|
||||
|
||||
const REGTEST_CHANGE: &str = "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8";
|
||||
let mut config = NoteSelectConfig::new(REGTEST_CHANGE);
|
||||
config.use_transparent = true;
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
let mut config = TransactionBuilderConfig::new(REGTEST_CHANGE);
|
||||
|
||||
log::info!("Getting signing keys");
|
||||
let keys = get_secret_keys(0, 1).unwrap();
|
||||
|
||||
log::info!("Building signing context");
|
||||
let context = Context::from_height(0, height).unwrap();
|
||||
let context = TxBuilderContext::from_height(0, height).unwrap();
|
||||
|
||||
log::info!("Getting available notes");
|
||||
let utxos = fetch_utxos(0, 1, height, true, 0).await.unwrap();
|
||||
|
@ -280,16 +333,24 @@ async fn dummy_test() {
|
|||
|
||||
log::info!("Preparing outputs");
|
||||
let mut orders = vec![];
|
||||
orders.push(decode(1, "tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq", 10000, MemoBytes::empty()).unwrap());
|
||||
orders.push(decode(2, "zregtestsapling1qzy9wafd2axnenul6t6wav76dys6s8uatsq778mpmdvmx4k9myqxsd9m73aqdgc7gwnv53wga4j", 20000, MemoBytes::empty()).unwrap());
|
||||
orders.push(decode(3, "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", 30000, MemoBytes::empty()).unwrap());
|
||||
orders.push(decode(4, "uregtest1yvucqfqnmq5ldc6fkvuudlsjhxg56hxph9ymmcnmpzpywd752ym8sr5l5d24wqn4enz3gakk6alf5hlpw2cjs3jjrcdae3nksrefyum5x400f9gs3ak9yllcr8czhrlnjufuuy7n5mh", 40000, MemoBytes::empty()).unwrap());
|
||||
orders.push(decode(5, "uregtest1wqgc0cm50a7a647qrdglgj62fl40q8njsrcfkt2mzlsmj979rdmsdwuysypc6ewxjxz0zc48kmm35jwx4q6c4fgqwkmmqyhwlep4n2hc0229vf6cahcnesr38y7gyzfx6pa8zg9jvv9", 50000, MemoBytes::empty()).unwrap());
|
||||
orders.push(decode(6, "uregtest1usu9eyxgqu48sa8lqug6ccjc7vcam3mt3a5t7jvyxj7pq5dgdtkjgkqzsyh9pfeav9970xddp2c9h5x44drwnz4f0zwc894k3vt380g6kfsg9j9fmnpljye9r56d94njsv40uaam392xvmky2v38dh3yhayz44z6xv402slujuhwy3mg", 60000, MemoBytes::empty()).unwrap());
|
||||
orders.push(decode(7, "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8", 70000, MemoBytes::empty()).unwrap());
|
||||
orders.push(
|
||||
Order::new(
|
||||
1,
|
||||
"tmWXoSBwPoCjJCNZjw4P7heoVMcT2Ronrqq",
|
||||
10000,
|
||||
MemoBytes::empty(),
|
||||
)
|
||||
);
|
||||
orders.push(Order::new(2, "zregtestsapling1qzy9wafd2axnenul6t6wav76dys6s8uatsq778mpmdvmx4k9myqxsd9m73aqdgc7gwnv53wga4j", 20000, MemoBytes::empty()));
|
||||
orders.push(Order::new(3, "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8", 30000, MemoBytes::empty()));
|
||||
orders.push(Order::new(4, "uregtest1yvucqfqnmq5ldc6fkvuudlsjhxg56hxph9ymmcnmpzpywd752ym8sr5l5d24wqn4enz3gakk6alf5hlpw2cjs3jjrcdae3nksrefyum5x400f9gs3ak9yllcr8czhrlnjufuuy7n5mh", 40000, MemoBytes::empty()));
|
||||
orders.push(Order::new(5, "uregtest1wqgc0cm50a7a647qrdglgj62fl40q8njsrcfkt2mzlsmj979rdmsdwuysypc6ewxjxz0zc48kmm35jwx4q6c4fgqwkmmqyhwlep4n2hc0229vf6cahcnesr38y7gyzfx6pa8zg9jvv9", 50000, MemoBytes::empty()));
|
||||
orders.push(Order::new(6, "uregtest1usu9eyxgqu48sa8lqug6ccjc7vcam3mt3a5t7jvyxj7pq5dgdtkjgkqzsyh9pfeav9970xddp2c9h5x44drwnz4f0zwc894k3vt380g6kfsg9j9fmnpljye9r56d94njsv40uaam392xvmky2v38dh3yhayz44z6xv402slujuhwy3mg", 60000, MemoBytes::empty()));
|
||||
orders.push(Order::new(7, "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8", 70000, MemoBytes::empty()));
|
||||
|
||||
log::info!("Building tx plan");
|
||||
let tx_plan = note_select_with_fee::<FeeFlat>(&utxos, &mut orders, &config).unwrap();
|
||||
let tx_plan =
|
||||
build_tx_plan::<FeeFlat>("", 0, &utxos, &mut orders, &config).unwrap();
|
||||
log::info!("Plan: {}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
log::info!("Building tx");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::types::*;
|
||||
use std::cmp::max;
|
||||
use crate::note_selection::types::*;
|
||||
|
||||
const MARGINAL_FEE: u64 = 5000;
|
||||
const GRACE_ACTIONS: u64 = 2;
|
||||
|
@ -20,17 +20,23 @@ impl FeeCalculator for FeeZIP327 {
|
|||
n_in[pool] += 1;
|
||||
}
|
||||
for o in outputs {
|
||||
if !o.is_fee {
|
||||
let pool = o.destination.pool() as usize;
|
||||
n_out[pool] += 1;
|
||||
}
|
||||
let pool = o.destination.pool() as usize;
|
||||
n_out[pool] += 1;
|
||||
}
|
||||
|
||||
let n_logical_actions = max(n_in[0], n_out[0]) +
|
||||
max(n_in[1], n_out[1]) +
|
||||
max(n_in[2], n_out[2]);
|
||||
let n_logical_actions =
|
||||
max(n_in[0], n_out[0]) + max(n_in[1], n_out[1]) + max(n_in[2], n_out[2]);
|
||||
|
||||
log::info!("fee: {}/{} {}/{} {}/{} = {}", n_in[0], n_out[0], n_in[1], n_out[1], n_in[2], n_out[2], n_logical_actions);
|
||||
log::info!(
|
||||
"fee: {}/{} {}/{} {}/{} = {}",
|
||||
n_in[0],
|
||||
n_out[0],
|
||||
n_in[1],
|
||||
n_out[1],
|
||||
n_in[2],
|
||||
n_out[2],
|
||||
n_logical_actions
|
||||
);
|
||||
let fee = MARGINAL_FEE * max(n_logical_actions, GRACE_ACTIONS);
|
||||
fee
|
||||
}
|
||||
|
@ -43,4 +49,3 @@ impl FeeCalculator for FeeFlat {
|
|||
1000
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
use std::cmp::min;
|
||||
use zcash_address::{AddressKind, ZcashAddress};
|
||||
use zcash_address::unified::{Container, Receiver};
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use crate::note_selection::types::{PrivacyPolicy, Fill, Execution, Order, Pool, PoolAllocation, Destination, PoolPrecedence};
|
||||
|
||||
/// Decode address and return it as an order
|
||||
///
|
||||
pub fn decode(id: u32, address: &str, amount: u64, memo: MemoBytes) -> anyhow::Result<Order> {
|
||||
let address = ZcashAddress::try_from_encoded(address)?;
|
||||
let mut order = Order::default();
|
||||
order.id = id;
|
||||
match address.kind {
|
||||
AddressKind::Sprout(_) => {}
|
||||
AddressKind::Sapling(data) => {
|
||||
let destination = Destination::Sapling(data);
|
||||
order.destinations[Pool::Sapling as usize] = Some(destination);
|
||||
}
|
||||
AddressKind::Unified(unified_address) => {
|
||||
for address in unified_address.items() {
|
||||
match address {
|
||||
Receiver::Orchard(data) => {
|
||||
let destination = Destination::Orchard(data);
|
||||
order.destinations[Pool::Orchard as usize] = Some(destination);
|
||||
}
|
||||
Receiver::Sapling(data) => {
|
||||
let destination = Destination::Sapling(data);
|
||||
order.destinations[Pool::Sapling as usize] = Some(destination);
|
||||
}
|
||||
Receiver::P2pkh(data) => {
|
||||
let destination = Destination::Transparent(data);
|
||||
order.destinations[Pool::Transparent as usize] = Some(destination);
|
||||
}
|
||||
Receiver::P2sh(_) => {}
|
||||
Receiver::Unknown { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
AddressKind::P2pkh(data) => {
|
||||
let destination = Destination::Transparent(data);
|
||||
order.destinations[Pool::Transparent as usize] = Some(destination);
|
||||
}
|
||||
AddressKind::P2sh(_) => {}
|
||||
}
|
||||
order.amount = amount;
|
||||
order.memo = memo;
|
||||
|
||||
Ok(order)
|
||||
}
|
||||
|
||||
pub fn execute_orders(orders: &mut [Order], initial_pool: &PoolAllocation, use_transparent: bool, use_shielded: bool,
|
||||
privacy_policy: PrivacyPolicy, precedence: &PoolPrecedence) -> anyhow::Result<Execution> {
|
||||
let mut allocation: PoolAllocation = PoolAllocation::default();
|
||||
let mut fills = vec![];
|
||||
|
||||
for order in orders.iter_mut() {
|
||||
let order_precedence = if order.is_fee { precedence } else { order.priority.to_pool_precedence() };
|
||||
// log::info!("Order {:?}", order);
|
||||
// Direct Shielded Fill - s2s, o2o
|
||||
// t2t only for fees
|
||||
if use_shielded {
|
||||
for &pool in order_precedence {
|
||||
if pool == Pool::Transparent && !order.is_fee { continue }
|
||||
if order.destinations[pool as usize].is_some() {
|
||||
fill_order(pool, pool, order, initial_pool, &mut allocation, &mut fills);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if privacy_policy != PrivacyPolicy::SamePoolOnly {
|
||||
// Indirect Shielded - z2z: s2o, o2s
|
||||
for &pool in order_precedence {
|
||||
if order.destinations[pool as usize].is_none() { continue }
|
||||
if !use_shielded { continue }
|
||||
if let Some(from_pool) = pool.other_shielded() {
|
||||
fill_order(from_pool, pool, order, initial_pool, &mut allocation, &mut fills);
|
||||
}
|
||||
}
|
||||
|
||||
if privacy_policy == PrivacyPolicy::AnyPool {
|
||||
// Other - s2t, o2t, t2s, t2o
|
||||
for &pool in order_precedence {
|
||||
if order.destinations[pool as usize].is_none() { continue }
|
||||
match pool {
|
||||
Pool::Transparent if use_shielded => {
|
||||
for &from_pool in precedence {
|
||||
if from_pool != Pool::Transparent {
|
||||
fill_order(from_pool, pool, order, initial_pool, &mut allocation, &mut fills);
|
||||
}
|
||||
}
|
||||
}
|
||||
Pool::Sapling | Pool::Orchard if use_transparent => {
|
||||
fill_order(Pool::Transparent, pool, order, initial_pool, &mut allocation, &mut fills);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
// t2t
|
||||
if use_transparent && order.destinations[Pool::Transparent as usize].is_some() {
|
||||
fill_order(Pool::Transparent, Pool::Transparent, order, initial_pool, &mut allocation, &mut fills);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let execution = Execution {
|
||||
allocation,
|
||||
fills,
|
||||
};
|
||||
|
||||
Ok(execution)
|
||||
}
|
||||
|
||||
fn fill_order(from: Pool, to: Pool, order: &mut Order, initial_pool: &PoolAllocation,
|
||||
fills: &mut PoolAllocation, executions: &mut Vec<Fill>) {
|
||||
let from = from as usize;
|
||||
let to = to as usize;
|
||||
let destination = order.destinations[to].as_ref().unwrap(); // Checked by caller
|
||||
let order_remaining = order.amount - order.filled;
|
||||
let pool_remaining = initial_pool.0[from] - fills.0[from];
|
||||
let amount = min(pool_remaining, order_remaining);
|
||||
order.filled += amount;
|
||||
fills.0[from] += amount;
|
||||
if amount > 0 {
|
||||
let execution = Fill {
|
||||
id_order: order.id,
|
||||
destination: destination.clone(),
|
||||
amount,
|
||||
memo: order.memo.clone(),
|
||||
is_fee: order.is_fee,
|
||||
};
|
||||
log::debug!("{:?}", execution);
|
||||
executions.push(execution);
|
||||
}
|
||||
assert!(order.amount == order.filled || initial_pool.0[from] == fills.0[from]); // fill must be to the max
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
fn other_shielded(&self) -> Option<Self> {
|
||||
match self {
|
||||
Pool::Transparent => None,
|
||||
Pool::Sapling => Some(Pool::Orchard),
|
||||
Pool::Orchard => Some(Pool::Sapling),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
use std::str::FromStr;
|
||||
use anyhow::anyhow;
|
||||
use zcash_primitives::memo::{Memo, MemoBytes};
|
||||
use crate::note_selection::fee::FeeCalculator;
|
||||
use crate::note_selection::{MAX_ATTEMPTS, TransactionBuilderError};
|
||||
use crate::note_selection::TransactionBuilderError::TxTooComplex;
|
||||
use crate::note_selection::ua::decode;
|
||||
use super::{Result, types::*};
|
||||
|
||||
pub fn sum_utxos(utxos: &[UTXO]) -> Result<PoolAllocation> {
|
||||
let mut pool = PoolAllocation::default();
|
||||
for utxo in utxos {
|
||||
match utxo.source {
|
||||
Source::Transparent { .. } => {
|
||||
pool.0[0] += utxo.amount;
|
||||
}
|
||||
Source::Sapling { .. } => {
|
||||
pool.0[1] += utxo.amount;
|
||||
}
|
||||
Source::Orchard { .. } => {
|
||||
pool.0[2] += utxo.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub fn group_orders(orders: &[Order], fee: u64) -> Result<(Vec<OrderInfo>, OrderGroupAmounts)> {
|
||||
let mut order_info = vec![];
|
||||
for order in orders {
|
||||
let mut group_type = 0;
|
||||
for i in 0..3 {
|
||||
if order.destinations[i].is_some() {
|
||||
group_type |= 1 << i;
|
||||
}
|
||||
}
|
||||
order_info.push(OrderInfo {
|
||||
group_type,
|
||||
amount: order.amount,
|
||||
});
|
||||
}
|
||||
|
||||
let mut t0 = 0u64;
|
||||
let mut s0 = 0u64;
|
||||
let mut o0 = 0u64;
|
||||
let mut x = 0u64;
|
||||
for info in order_info.iter_mut() {
|
||||
if info.group_type != 1 {
|
||||
info.group_type &= 6; // unselect transparent outputs except for t-addr
|
||||
}
|
||||
match info.group_type {
|
||||
1 => {
|
||||
t0 += info.amount;
|
||||
}
|
||||
2 => {
|
||||
s0 += info.amount;
|
||||
}
|
||||
4 => {
|
||||
o0 += info.amount;
|
||||
}
|
||||
6 => {
|
||||
x += info.amount;
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
log::debug!("{} {} {} {}", t0, s0, o0, x);
|
||||
let amounts = OrderGroupAmounts {
|
||||
t0,
|
||||
s0,
|
||||
o0,
|
||||
x,
|
||||
fee,
|
||||
};
|
||||
Ok((order_info, amounts))
|
||||
}
|
||||
|
||||
pub fn allocate_funds(amounts: &OrderGroupAmounts, initial: &PoolAllocation) -> Result<FundAllocation> {
|
||||
log::debug!("{:?}", initial);
|
||||
|
||||
let OrderGroupAmounts { t0, s0, o0, x , fee} = *amounts;
|
||||
let (t0, s0, o0, x, fee) = (t0 as i64, s0 as i64, o0 as i64, x as i64, fee as i64);
|
||||
|
||||
let sum = t0 + s0 + o0 + x + fee;
|
||||
let tmax = initial.0[0] as i64;
|
||||
let smax = initial.0[1] as i64;
|
||||
let omax = initial.0[2] as i64;
|
||||
|
||||
let mut s1;
|
||||
let mut o1;
|
||||
let mut s2;
|
||||
let mut o2;
|
||||
let mut t2 = sum - smax - omax;
|
||||
if t2 > 0 {
|
||||
if t2 > tmax {
|
||||
return Err(TransactionBuilderError::NotEnoughFunds)
|
||||
}
|
||||
// Not enough shielded notes. Use them all before using transparent notes
|
||||
s2 = smax;
|
||||
o2 = omax;
|
||||
let d_s = (t0 + fee - t2) / 2;
|
||||
let d_o = t0 + fee - t2 - d_s; // handle case when t0+fee-t2 is odd
|
||||
s1 = s2 - d_s - s0;
|
||||
o1 = o2 - d_o - o0;
|
||||
|
||||
if s1 < 0 {
|
||||
s1 = 0;
|
||||
o1 = x;
|
||||
}
|
||||
else if o1 < 0 {
|
||||
o1 = 0;
|
||||
s1 = x;
|
||||
}
|
||||
}
|
||||
else {
|
||||
t2 = 0;
|
||||
let d_s = (t0 + fee) / 2;
|
||||
let d_o = t0 + fee - d_s;
|
||||
|
||||
// Solve relaxed problem
|
||||
let inp = sum / 2;
|
||||
s2 = inp;
|
||||
o2 = sum - inp;
|
||||
s1 = s2 - d_s - s0;
|
||||
o1 = o2 - d_o - o0;
|
||||
|
||||
// Check solution validity
|
||||
if s1 < 0 {
|
||||
s1 = 0;
|
||||
o1 = x;
|
||||
s2 = s0 + d_s;
|
||||
o2 = o0 + d_o + x;
|
||||
} else if o1 < 0 {
|
||||
o1 = 0;
|
||||
s1 = x;
|
||||
o2 = o0 + d_o;
|
||||
s2 = s0 + d_s + x;
|
||||
}
|
||||
|
||||
assert!(s2 >= 0);
|
||||
assert!(o2 >= 0);
|
||||
|
||||
// Check account balances
|
||||
|
||||
if s2 > smax {
|
||||
s2 = smax;
|
||||
o2 = sum - s2;
|
||||
}
|
||||
if o2 > omax {
|
||||
o2 = omax;
|
||||
s2 = sum - o2;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(s1 >= 0);
|
||||
assert!(o1 >= 0);
|
||||
assert!(t2 >= 0);
|
||||
assert!(s2 >= 0);
|
||||
assert!(o2 >= 0);
|
||||
assert!(t2 <= tmax);
|
||||
assert!(s2 <= smax);
|
||||
assert!(o2 <= omax);
|
||||
|
||||
assert_eq!(sum, t2 + s2 + o2);
|
||||
assert_eq!(x, s1 + o1);
|
||||
|
||||
log::debug!("{} {}", s1, o1);
|
||||
log::debug!("{} {} {}", t2, s2, o2);
|
||||
|
||||
let fund_allocation = FundAllocation {
|
||||
s1: s1 as u64,
|
||||
o1: o1 as u64,
|
||||
t2: t2 as u64,
|
||||
s2: s2 as u64,
|
||||
o2: o2 as u64,
|
||||
};
|
||||
Ok(fund_allocation)
|
||||
}
|
||||
|
||||
pub fn fill(orders: &[Order], order_infos: &[OrderInfo], amounts: &OrderGroupAmounts, allocation: &FundAllocation) -> Result<Vec<Fill>> {
|
||||
assert_eq!(orders.len(), order_infos.len());
|
||||
let mut fills = vec![];
|
||||
let mut f = 0f64;
|
||||
if amounts.x != 0 {
|
||||
f = allocation.s1 as f64 / amounts.x as f64;
|
||||
}
|
||||
for (order, info) in orders.iter().zip(order_infos) {
|
||||
match info.group_type {
|
||||
1 | 2 | 4 => {
|
||||
let fill = Fill {
|
||||
id_order: Some(order.id),
|
||||
destination: order.destinations[ilog2(info.group_type)].unwrap(),
|
||||
amount: order.amount,
|
||||
memo: order.memo.clone(),
|
||||
};
|
||||
fills.push(fill);
|
||||
}
|
||||
6 => {
|
||||
let fill1 = Fill {
|
||||
id_order: Some(order.id),
|
||||
destination: order.destinations[1].unwrap(),
|
||||
amount: (order.amount as f64 * f).round() as u64,
|
||||
memo: order.memo.clone(),
|
||||
};
|
||||
let fill2 = Fill {
|
||||
id_order: Some(order.id),
|
||||
destination: order.destinations[2].unwrap(),
|
||||
amount: order.amount - fill1.amount,
|
||||
memo: order.memo.clone(),
|
||||
};
|
||||
if fill1.amount != 0 { fills.push(fill1); }
|
||||
if fill2.amount != 0 { fills.push(fill2); }
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
Ok(fills)
|
||||
}
|
||||
|
||||
pub fn select_inputs(utxos: &[UTXO], allocation: &FundAllocation) -> Result<(Vec<UTXO>, PoolAllocation)> {
|
||||
let mut needed = [allocation.t2, allocation.s2, allocation.o2];
|
||||
let mut change = [0u64; 3];
|
||||
let mut inputs = vec![];
|
||||
for utxo in utxos {
|
||||
let idx = match utxo.source {
|
||||
Source::Transparent { .. } => 0,
|
||||
Source::Sapling { .. } => 1,
|
||||
Source::Orchard { .. } => 2,
|
||||
};
|
||||
if needed[idx] > 0 {
|
||||
let available = utxo.amount;
|
||||
let a = available.min(needed[idx]);
|
||||
inputs.push(utxo.clone());
|
||||
needed[idx] -= a;
|
||||
change[idx] += available - a;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((inputs, PoolAllocation(change)))
|
||||
}
|
||||
|
||||
pub fn outputs_for_change(change_destinations: &[Option<Destination>; 3], change: &PoolAllocation) -> Result<Vec<Fill>> {
|
||||
let mut change_fills = vec![];
|
||||
for i in 0..3 {
|
||||
let destination = change_destinations[i];
|
||||
if let Some(destination) = destination {
|
||||
let change_fill = Fill {
|
||||
id_order: None,
|
||||
destination,
|
||||
amount: change.0[i],
|
||||
memo: MemoBytes::empty(),
|
||||
};
|
||||
if change_fill.amount != 0 { change_fills.push(change_fill); }
|
||||
} else {
|
||||
return Err(anyhow!("No change address").into())
|
||||
}
|
||||
}
|
||||
Ok(change_fills)
|
||||
}
|
||||
|
||||
pub fn build_tx_plan<F: FeeCalculator>(fvk: &str, height: u32, utxos: &[UTXO], orders: &[Order], config: &TransactionBuilderConfig) -> Result<TransactionPlan> {
|
||||
let mut fee = 0;
|
||||
|
||||
for _ in 0..MAX_ATTEMPTS {
|
||||
let balances = sum_utxos(utxos)?;
|
||||
let (groups, amounts) = group_orders(&orders, fee)?;
|
||||
let allocation = allocate_funds(&amounts, &balances)?;
|
||||
|
||||
let OrderGroupAmounts { s0, o0, .. } = amounts;
|
||||
let FundAllocation { s1, o1, s2, o2, .. } = allocation;
|
||||
let (s0, o0, s1, o1, s2, o2) = (s0 as i64, o0 as i64, s1 as i64, o1 as i64, s2 as i64, o2 as i64);
|
||||
let net_chg = [s0 + s1 - s2, o0 + o1 - o2];
|
||||
|
||||
let mut fills = fill(&orders, &groups, &amounts, &allocation)?;
|
||||
|
||||
let (notes, change) = select_inputs(&utxos, &allocation)?;
|
||||
let change_destinations = decode(&config.change_address)?;
|
||||
let change_outputs = outputs_for_change(&change_destinations, &change).unwrap();
|
||||
fills.extend(change_outputs);
|
||||
|
||||
let updated_fee = F::calculate_fee(¬es, &fills);
|
||||
if updated_fee == fee {
|
||||
let tx_plan = TransactionPlan {
|
||||
fvk: fvk.to_string(),
|
||||
height,
|
||||
spends: notes,
|
||||
outputs: fills,
|
||||
net_chg,
|
||||
fee
|
||||
};
|
||||
return Ok(tx_plan)
|
||||
}
|
||||
fee = updated_fee;
|
||||
}
|
||||
Err(TxTooComplex)
|
||||
}
|
||||
|
||||
fn ilog2(u: usize) -> usize {
|
||||
match u {
|
||||
1 => 0,
|
||||
2 => 1,
|
||||
4 => 2,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
use std::cmp::min;
|
||||
use std::slice;
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use crate::note_selection::decode;
|
||||
use crate::note_selection::fee::FeeCalculator;
|
||||
use crate::note_selection::fill::execute_orders;
|
||||
use crate::note_selection::types::{NoteSelectConfig, Order, PoolAllocation, UTXO, Destination, TransactionPlan, Fill, PoolPrecedence, PoolPriority};
|
||||
|
||||
pub fn select_notes(allocation: &PoolAllocation, utxos: &[UTXO]) -> anyhow::Result<Vec<UTXO>> {
|
||||
let mut allocation = allocation.clone();
|
||||
|
||||
let mut selected = vec![];
|
||||
for utxo in utxos {
|
||||
let pool = utxo.source.pool() as usize;
|
||||
if allocation.0[pool] > 0 {
|
||||
let amount = min(allocation.0[pool], utxo.amount);
|
||||
selected.push(utxo.clone());
|
||||
allocation.0[pool] -= amount;
|
||||
}
|
||||
}
|
||||
Ok(selected)
|
||||
}
|
||||
|
||||
struct OrderExecutor {
|
||||
pub pool_available: PoolAllocation,
|
||||
pub pool_used: PoolAllocation,
|
||||
pub config: NoteSelectConfig,
|
||||
pub fills: Vec<Fill>,
|
||||
}
|
||||
|
||||
impl OrderExecutor {
|
||||
pub fn new(initial_pool: PoolAllocation, config: NoteSelectConfig) -> Self {
|
||||
OrderExecutor {
|
||||
pool_available: initial_pool,
|
||||
pool_used: PoolAllocation::default(),
|
||||
config,
|
||||
fills: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, orders: &mut [Order], precedence: &PoolPrecedence) -> anyhow::Result<bool> {
|
||||
let order_execution = execute_orders(orders, &self.pool_available,
|
||||
self.config.use_transparent, self.config.use_shielded,
|
||||
self.config.privacy_policy.clone(), precedence)?; // calculate an execution plan without considering the fee
|
||||
self.fills.extend(order_execution.fills);
|
||||
self.pool_available = self.pool_available - order_execution.allocation;
|
||||
self.pool_used = self.pool_used + order_execution.allocation;
|
||||
let fully_filled = orders.iter().all(|o| o.amount == o.filled);
|
||||
Ok(fully_filled)
|
||||
}
|
||||
|
||||
pub fn add(&mut self, fill: Fill) {
|
||||
self.fills.push(fill);
|
||||
}
|
||||
|
||||
pub fn select_notes(&self, utxos: &[UTXO]) -> anyhow::Result<Vec<UTXO>> {
|
||||
select_notes(&self.pool_used, &utxos)
|
||||
}
|
||||
}
|
||||
|
||||
const ANY_DESTINATION: [Option<Destination>; 3] = [Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43]))];
|
||||
const MAX_ATTEMPTS: usize = 10;
|
||||
/// Select notes from the `utxos` that can pay for the `orders`
|
||||
///
|
||||
pub fn note_select_with_fee<F: FeeCalculator>(utxos: &[UTXO], orders: &mut [Order], config: &NoteSelectConfig) -> anyhow::Result<TransactionPlan> {
|
||||
let initial_pool = PoolAllocation::from(&*utxos); // amount of funds available in each pool
|
||||
let mut fee = 0;
|
||||
let mut n_attempts = 0;
|
||||
let change_order = decode(u32::MAX, &config.change_address, 0, MemoBytes::empty())?;
|
||||
let change_destinations = change_order.destinations;
|
||||
|
||||
let mut plan = loop {
|
||||
for o in orders.iter_mut() {
|
||||
o.filled = 0;
|
||||
}
|
||||
let mut executor = OrderExecutor::new(initial_pool, config.clone());
|
||||
if !executor.execute(orders, &config.precedence)? {
|
||||
anyhow::bail!("Unsufficient Funds")
|
||||
}
|
||||
if fee == 0 {
|
||||
let notes = executor.select_notes(utxos)?;
|
||||
fee = F::calculate_fee(¬es, &executor.fills);
|
||||
log::debug!("base fee: {}", fee);
|
||||
}
|
||||
|
||||
// Favor the pools that are already used, because it may cause lower fees
|
||||
let pool_needed = executor.pool_used;
|
||||
let prec_1 = config.precedence.iter().filter(|&&p| pool_needed.0[p as usize] != 0);
|
||||
let prec_2 = config.precedence.iter().filter(|&&p| pool_needed.0[p as usize] == 0);
|
||||
let fee_precedence: PoolPrecedence = prec_1.chain(prec_2).cloned().collect::<Vec<_>>().try_into().unwrap();
|
||||
log::debug!("Fee precedence: {:?}", fee_precedence);
|
||||
let mut fee_order = Order {
|
||||
id: u32::MAX,
|
||||
destinations: ANY_DESTINATION,
|
||||
priority: PoolPriority::OS, // ignored for fees
|
||||
amount: fee,
|
||||
memo: MemoBytes::empty(),
|
||||
is_fee: true, // do not include in fee calculation
|
||||
filled: 0,
|
||||
};
|
||||
|
||||
if !executor.execute(slice::from_mut(&mut fee_order), &fee_precedence)? {
|
||||
anyhow::bail!("Unsufficient Funds [fees]")
|
||||
}
|
||||
|
||||
// Let's figure out the change
|
||||
let pool_needed = executor.pool_used;
|
||||
let total_needed = pool_needed.total();
|
||||
|
||||
let notes = executor.select_notes(utxos)?;
|
||||
let pool_spent = PoolAllocation::from(&*notes);
|
||||
let total_spent = pool_spent.total();
|
||||
let change = pool_spent - pool_needed; // must be >= 0 because the note selection covers the fills
|
||||
|
||||
log::debug!("pool_needed: {:?} {}", pool_needed, total_needed);
|
||||
log::debug!("pool_spent: {:?} {}", pool_spent, total_spent);
|
||||
log::debug!("change: {:?}", change);
|
||||
|
||||
for pool in 0..3 {
|
||||
if change.0[pool] != 0 {
|
||||
executor.add(Fill {
|
||||
id_order: u32::MAX,
|
||||
destination: change_destinations[pool].unwrap(),
|
||||
amount: change.0[pool],
|
||||
memo: MemoBytes::empty(),
|
||||
is_fee: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let notes = executor.select_notes(utxos)?;
|
||||
let new_fee = F::calculate_fee(¬es, &executor.fills);
|
||||
log::debug!("new fee: {}", new_fee);
|
||||
|
||||
if new_fee == fee || n_attempts == MAX_ATTEMPTS {
|
||||
let plan = TransactionPlan {
|
||||
spends: notes.clone(),
|
||||
outputs: executor.fills.clone(),
|
||||
fee: 0,
|
||||
};
|
||||
break plan;
|
||||
}
|
||||
fee = new_fee; // retry with the new fee
|
||||
n_attempts += 1;
|
||||
};
|
||||
|
||||
plan.fee = plan.outputs.iter().filter_map(|f| if f.is_fee { Some(f.amount) } else { None }).sum();
|
||||
plan.outputs = plan.outputs.into_iter().filter(|f| !f.is_fee).collect();
|
||||
|
||||
Ok(plan)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use serde_with::serde_as;
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde_as]
|
||||
#[serde(remote = "MemoBytes")]
|
||||
pub struct MemoBytesProxy(
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
#[serde(getter = "get_memo_bytes")]
|
||||
pub Vec<u8>,
|
||||
);
|
||||
|
||||
fn get_memo_bytes(memo: &MemoBytes) -> Vec<u8> {
|
||||
memo.as_slice().to_vec()
|
||||
}
|
||||
|
||||
impl From<MemoBytesProxy> for MemoBytes {
|
||||
fn from(p: MemoBytesProxy) -> MemoBytes {
|
||||
MemoBytes::from_bytes(&p.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +1,23 @@
|
|||
use serde::Serialize;
|
||||
use assert_matches::assert_matches;
|
||||
use serde_json::Value;
|
||||
use zcash_primitives::memo::Memo;
|
||||
use crate::{CoinConfig, init_test};
|
||||
use crate::api::payment::RecipientMemo;
|
||||
use crate::unified::UnifiedAddressType;
|
||||
use super::{*, types::*};
|
||||
|
||||
// must have T+S+O receivers
|
||||
const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e";
|
||||
const UA_TSO: &str = "uregtest1mxy5wq2n0xw57nuxa4lqpl358zw4vzyfgadsn5jungttmqcv6nx6cpx465dtpzjzw0vprjle4j4nqqzxtkuzm93regvgg4xce0un5ec6tedquc469zjhtdpkxz04kunqqyasv4rwvcweh3ue0ku0payn29stl2pwcrghyzscrrju9ar57rn36wgz74nmynwcyw27rjd8yk477l97ez8";
|
||||
const UA_O: &str = "uregtest1mzt5lx5s5u8kczlfr82av97kjckmfjfuq8y9849h6cl9chhdekxsm6r9dklracflqwplrnfzm5rucp5txfdm04z5myrde8y3y5rayev8";
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_utxo() {
|
||||
init_test();
|
||||
let utxos = fetch_utxos(0, 1, 235, true, 0).await.unwrap();
|
||||
|
||||
for utxo in utxos.iter() {
|
||||
log::info!("{:?}", utxo);
|
||||
}
|
||||
|
||||
assert_eq!(utxos[0].amount, 624999000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ua() {
|
||||
init_test();
|
||||
let c = CoinConfig::get(0);
|
||||
let db = c.db().unwrap();
|
||||
let address = crate::get_unified_address(c.chain.network(), &db, 1,
|
||||
Some(UnifiedAddressType { transparent: true, sapling: true, orchard: false })).unwrap(); // use ua settings from db
|
||||
println!("{}", address);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_payment() {
|
||||
init_test();
|
||||
let config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
|
||||
let recipients = vec![
|
||||
RecipientMemo {
|
||||
address: UA_O.to_string(),
|
||||
amount: 89000,
|
||||
memo: Memo::Empty.into(),
|
||||
max_amount_per_note: 0,
|
||||
}
|
||||
];
|
||||
let tx_plan = prepare_multi_payment(0, 1, 205,
|
||||
&recipients, &config, 3,
|
||||
).await.unwrap();
|
||||
|
||||
let tx_json = serde_json::to_string(&tx_plan).unwrap();
|
||||
println!("{}", tx_json);
|
||||
|
||||
// expected: s2o because the recipient ua has only an orchard receiver
|
||||
assert_eq!(tx_plan.outputs[0].destination.pool(), Pool::Orchard);
|
||||
assert_eq!(tx_plan.outputs[0].amount, 89000);
|
||||
assert_eq!(tx_plan.outputs[1].destination.pool(), Pool::Sapling); // change goes back to sapling
|
||||
assert_eq!(tx_plan.outputs[1].amount, 624900000);
|
||||
// fee = 10000 per zip-317
|
||||
assert_eq!(tx_plan.fee, 10000);
|
||||
|
||||
assert_eq!(tx_plan.spends[0].amount, tx_plan.outputs[0].amount + tx_plan.outputs[1].amount + tx_plan.fee);
|
||||
}
|
||||
|
||||
macro_rules! order {
|
||||
($id:expr, $q:expr, $destinations:expr) => {
|
||||
Order {
|
||||
id: $id,
|
||||
amount: $q * 1000,
|
||||
destinations: $destinations,
|
||||
priority: PoolPriority::OS,
|
||||
filled: 0,
|
||||
is_fee: false,
|
||||
memo: MemoBytes::empty(),
|
||||
}
|
||||
};
|
||||
}
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use crate::note_selection::build_tx_plan;
|
||||
use crate::note_selection::fee::{FeeCalculator, FeeZIP327};
|
||||
use crate::note_selection::optimize::{outputs_for_change, select_inputs};
|
||||
use crate::note_selection::ua::decode;
|
||||
use super::types::*;
|
||||
use super::optimize::{allocate_funds, fill, group_orders};
|
||||
use super::TransactionBuilderError::NotEnoughFunds;
|
||||
|
||||
macro_rules! utxo {
|
||||
($id:expr, $q:expr) => {
|
||||
UTXO {
|
||||
amount: $q * 1000,
|
||||
source: Source::Transparent { txid: [0u8; 32], index: $id },
|
||||
source: Source::Transparent {
|
||||
txid: [0u8; 32],
|
||||
index: $id,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -115,9 +51,24 @@ macro_rules! orchard {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! order {
|
||||
($id:expr, $q:expr, $destinations:expr) => {
|
||||
Order {
|
||||
id: $id,
|
||||
amount: $q * 1000,
|
||||
destinations: $destinations,
|
||||
memo: MemoBytes::empty(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! t {
|
||||
($id: expr, $q:expr) => {
|
||||
order!($id, $q, [Some(Destination::Transparent([0u8; 20])), None, None])
|
||||
order!(
|
||||
$id,
|
||||
$q,
|
||||
[Some(Destination::Transparent([0u8; 20])), None, None]
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -135,216 +86,650 @@ macro_rules! o {
|
|||
|
||||
macro_rules! ts {
|
||||
($id: expr, $q:expr) => {
|
||||
order!($id, $q, [Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), None])
|
||||
order!(
|
||||
$id,
|
||||
$q,
|
||||
[
|
||||
Some(Destination::Transparent([0u8; 20])),
|
||||
Some(Destination::Sapling([0u8; 43])),
|
||||
None
|
||||
]
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! to {
|
||||
($id: expr, $q:expr) => {
|
||||
order!($id, $q, [Some(Destination::Transparent([0u8; 20])), None, Some(Destination::Orchard([0u8; 43]))])
|
||||
order!(
|
||||
$id,
|
||||
$q,
|
||||
[
|
||||
Some(Destination::Transparent([0u8; 20])),
|
||||
None,
|
||||
Some(Destination::Orchard([0u8; 43]))
|
||||
]
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! so {
|
||||
($id: expr, $q:expr) => {
|
||||
order!($id, $q, [None, Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43]))])
|
||||
order!(
|
||||
$id,
|
||||
$q,
|
||||
[
|
||||
None,
|
||||
Some(Destination::Sapling([0u8; 43])),
|
||||
Some(Destination::Orchard([0u8; 43]))
|
||||
]
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! tso {
|
||||
($id: expr, $q:expr) => {
|
||||
order!($id, $q, [Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43]))])
|
||||
order!(
|
||||
$id,
|
||||
$q,
|
||||
[
|
||||
Some(Destination::Transparent([0u8; 20])),
|
||||
Some(Destination::Sapling([0u8; 43])),
|
||||
Some(Destination::Orchard([0u8; 43]))
|
||||
]
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_example1() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.use_transparent = true;
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
#[ignore]
|
||||
fn test_select() {
|
||||
env_logger::init();
|
||||
|
||||
let utxos = [utxo!(1, 5), utxo!(2, 7), sapling!(3, 12), orchard!(4, 10)];
|
||||
let mut orders = [t!(1, 10)];
|
||||
// Exhaustive test of every combination of T/S/O/S+O recipients
|
||||
// with every combination of assets in sender's account
|
||||
let mut c = 0usize;
|
||||
for t in 0..=10 {
|
||||
for s in 0..=10 {
|
||||
for o in 0..=10 {
|
||||
for so in 0..=10 {
|
||||
for fee in 0..=10 {
|
||||
let amounts = OrderGroupAmounts {
|
||||
t0: t * 10_000,
|
||||
s0: s * 10_000,
|
||||
o0: o * 10_000,
|
||||
x: so * 10_000,
|
||||
fee: fee * 1000,
|
||||
};
|
||||
for t in 0..=10 {
|
||||
for s in 0..=10 {
|
||||
for o in 0..=10 {
|
||||
let _ = allocate_funds(&amounts, &&PoolAllocation([t * 20_000, s * 20_000, o * 20_000]));
|
||||
c += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":5000},{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":2}},"amount":7000},{"source":{"Sapling":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":12000},{"source":{"Orchard":{"id_note":4,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":10000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":4000,"memo":[246]}],"fee":20000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
println!("{} tests", c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_example2() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
|
||||
let utxos = [utxo!(1, 5), utxo!(2, 7), sapling!(3, 12), orchard!(4, 10), orchard!(5, 10)];
|
||||
let mut orders = [t!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":5000},{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":2}},"amount":7000},{"source":{"Sapling":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":12000},{"source":{"Orchard":{"id_note":4,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":10000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":4000,"memo":[246]}],"fee":20000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
fn test_t2t() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 100,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([150, 0, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 110,
|
||||
s2: 0,
|
||||
o2: 0
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_example3() {
|
||||
fn test_t2zs() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 100,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([150, 0, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 110,
|
||||
s2: 0,
|
||||
o2: 0
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_t2zo() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 100,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([150, 0, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 110,
|
||||
s2: 0,
|
||||
o2: 0
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_t2ua() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 100,
|
||||
fee: 10
|
||||
}, &PoolAllocation([150, 0, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 50,
|
||||
o1: 50,
|
||||
t2: 110,
|
||||
s2: 0,
|
||||
o2: 0
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zs2zs() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 100,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 110,
|
||||
o2: 0
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zo2zo() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 100,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 0, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 0,
|
||||
o2: 110
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ua2zs() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 100,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 105,
|
||||
o2: 5,
|
||||
}) // net change is (-5, -5) which is better than (-10, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ua2zo() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 100,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 5,
|
||||
o2: 105,
|
||||
}) // net change is (-5, -5) which is better than (-10, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ua2t() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 100,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 55,
|
||||
o2: 55,
|
||||
}) // split equally between sapling & orchard
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zs2t() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 100,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 110,
|
||||
o2: 0,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zo2t() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 100,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 0, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 0,
|
||||
o2: 110,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zo2zs() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 100,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 0, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 0,
|
||||
o2: 110,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zs2zo() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 100,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 0])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 0,
|
||||
s2: 110,
|
||||
o2: 0,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ua2ua() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 100,
|
||||
fee: 10
|
||||
}, &PoolAllocation([0, 150, 150])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 50,
|
||||
o1: 50,
|
||||
t2: 0,
|
||||
s2: 55,
|
||||
o2: 55,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tzs2zs() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 100,
|
||||
o0: 0,
|
||||
x: 0,
|
||||
fee: 10
|
||||
}, &PoolAllocation([150, 10, 10])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 0,
|
||||
t2: 90, // must use t because not enough zs & zo
|
||||
s2: 10,
|
||||
o2: 10,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tzs2ua() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 100,
|
||||
fee: 10
|
||||
}, &PoolAllocation([150, 10, 10])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 50,
|
||||
o1: 50, // split equally to minimize net change
|
||||
t2: 90, // must use t because not enough zs & zo
|
||||
s2: 10,
|
||||
o2: 10,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neg_ua2ua() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 0,
|
||||
s0: 0,
|
||||
o0: 0,
|
||||
x: 100,
|
||||
fee: 10
|
||||
}, &PoolAllocation([10, 10, 10]));
|
||||
assert_matches!(r, Err(NotEnoughFunds))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_odd_ua2ua() {
|
||||
let r = allocate_funds(&OrderGroupAmounts {
|
||||
t0: 1,
|
||||
s0: 1,
|
||||
o0: 1,
|
||||
x: 1,
|
||||
fee: 1
|
||||
}, &PoolAllocation([10, 10, 10])).unwrap();
|
||||
assert_eq!(r, FundAllocation {
|
||||
s1: 0,
|
||||
o1: 1,
|
||||
t2: 0,
|
||||
s2: 2,
|
||||
o2: 3,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fill() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.use_transparent = true;
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
config.precedence = [ Pool::Sapling, Pool::Orchard, Pool::Transparent ];
|
||||
let orders = vec![
|
||||
t!(1, 10),
|
||||
s!(2, 20),
|
||||
o!(3, 30),
|
||||
ts!(4, 40),
|
||||
to!(5, 50),
|
||||
so!(6, 60),
|
||||
tso!(7, 70),
|
||||
];
|
||||
let (groups, amounts) = group_orders(&orders, 0).unwrap();
|
||||
assert_eq!(amounts, OrderGroupAmounts {
|
||||
t0: 10_000,
|
||||
s0: 60_000,
|
||||
o0: 80_000,
|
||||
x: 130_000,
|
||||
fee: 0
|
||||
});
|
||||
let allocation = allocate_funds(&amounts, &PoolAllocation([200_000, 200_000, 200_000])).unwrap();
|
||||
|
||||
let utxos = [utxo!(1, 100), sapling!(2, 160), orchard!(3, 70), orchard!(4, 50)];
|
||||
let mut orders = [t!(1, 10), s!(2, 20), o!(3, 30), ts!(4, 40), to!(5, 50), so!(6, 60), tso!(7, 70)];
|
||||
let fills = fill(&orders, &groups, &amounts, &allocation).unwrap();
|
||||
log::info!("{:?}", allocation);
|
||||
log::info!("{:?}", fills);
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
assert_eq!(fills[5].amount + fills[6].amount, 60_000);
|
||||
assert_eq!(fills[7].amount + fills[8].amount, 70_000);
|
||||
assert_eq!(fills[1].amount + fills[3].amount + fills[5].amount + fills[7].amount, fills[2].amount + fills[4].amount + fills[6].amount + fills[8].amount);
|
||||
}
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":100000},{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":160000},{"source":{"Orchard":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":70000},{"source":{"Orchard":{"id_note":4,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":2,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":20000,"memo":[246]},{"id_order":3,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":30000,"memo":[246]},{"id_order":4,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":40000,"memo":[246]},{"id_order":5,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":50000,"memo":[246]},{"id_order":6,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":40000,"memo":[246]},{"id_order":6,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":20000,"memo":[246]},{"id_order":7,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":70000,"memo":[246]},{"id_order":4294967295,"destination":{"Transparent":"c7b7b3d299bd173ea278d792b1bd5fbdd11afe34"},"amount":55000,"memo":[246]}],"fee":45000}"#).unwrap();
|
||||
#[test]
|
||||
fn test_select_utxo() {
|
||||
let _ = env_logger::try_init();
|
||||
let allocation = FundAllocation { s1: 75000, o1: 55000, t2: 0, s2: 140000, o2: 140000 };
|
||||
let mut utxos = vec![];
|
||||
for i in 0..30 {
|
||||
if i < 10 {
|
||||
utxos.push(utxo!(i, 25));
|
||||
}
|
||||
else if i < 20 {
|
||||
utxos.push(sapling!(i, 25));
|
||||
}
|
||||
else {
|
||||
utxos.push(orchard!(i, 25));
|
||||
}
|
||||
}
|
||||
let (_inputs, change) = select_inputs(&utxos, &allocation).unwrap();
|
||||
|
||||
assert_eq!(change.0, [0, 10000, 10000]);
|
||||
}
|
||||
|
||||
const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e";
|
||||
|
||||
#[test]
|
||||
fn test_change_fills() {
|
||||
let _ = env_logger::try_init();
|
||||
let destinations = decode(CHANGE_ADDRESS).unwrap();
|
||||
let outputs = outputs_for_change(&destinations, &&PoolAllocation([0, 10000, 10000])).unwrap();
|
||||
log::info!("{:?}", outputs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fees() {
|
||||
let _ = env_logger::try_init();
|
||||
let utxos = utxos();
|
||||
let orders = vec![
|
||||
t!(1, 10),
|
||||
s!(2, 20),
|
||||
o!(3, 30),
|
||||
ts!(4, 40),
|
||||
to!(5, 50),
|
||||
so!(6, 60),
|
||||
tso!(7, 70),
|
||||
];
|
||||
let (groups, amounts) = group_orders(&orders, 0).unwrap();
|
||||
let allocation = allocate_funds(&amounts, &PoolAllocation([200_000, 200_000, 200_000])).unwrap();
|
||||
let fills = fill(&orders, &groups, &amounts, &allocation).unwrap();
|
||||
|
||||
let fees = FeeZIP327::calculate_fee(
|
||||
&utxos,
|
||||
&fills);
|
||||
assert_eq!(fees, 150_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tx_plan() {
|
||||
let _ = env_logger::try_init();
|
||||
let utxos = utxos();
|
||||
let orders = vec![
|
||||
t!(1, 10),
|
||||
s!(2, 20),
|
||||
o!(3, 30),
|
||||
ts!(4, 40),
|
||||
to!(5, 50),
|
||||
so!(6, 60),
|
||||
tso!(7, 70),
|
||||
];
|
||||
let tx_plan = build_tx_plan::<FeeZIP327>("", 0, &utxos, &orders,
|
||||
&TransactionBuilderConfig { change_address: CHANGE_ADDRESS.to_string() }).unwrap();
|
||||
let simple_plan: SimpleTxPlan = tx_plan.into();
|
||||
let plan = serde_json::to_string(&simple_plan).unwrap();
|
||||
log::info!("{}", plan);
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&simple_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{
|
||||
"inputs": [{
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25000
|
||||
}],
|
||||
"outputs": [{
|
||||
"pool": 0,
|
||||
"amount": 10000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 20000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 30000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 40000
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 50000
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 34615
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 25385
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 40385
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 29615
|
||||
}, {
|
||||
"pool": 1,
|
||||
"amount": 17500
|
||||
}, {
|
||||
"pool": 2,
|
||||
"amount": 17500
|
||||
}],
|
||||
"fee": 85000
|
||||
}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
}
|
||||
|
||||
/// A simple t2t
|
||||
///
|
||||
#[test]
|
||||
fn test_example4() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.use_transparent = true;
|
||||
config.use_shielded = false;
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)];
|
||||
let mut orders = [t!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Transparent":"c7b7b3d299bd173ea278d792b1bd5fbdd11afe34"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
#[derive(Serialize)]
|
||||
struct SimpleTxPlan {
|
||||
inputs: Vec<SimpleTxIO>,
|
||||
outputs: Vec<SimpleTxIO>,
|
||||
fee: u64,
|
||||
}
|
||||
|
||||
/// A simple z2z
|
||||
///
|
||||
#[test]
|
||||
fn test_example5() {
|
||||
let _ = env_logger::try_init();
|
||||
let config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
|
||||
// z2z are preferred over t2z, so we can keep the t-notes
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)];
|
||||
let mut orders = [s!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Sapling":"9fae6f28c245e095abf8c6730098e110bb67ae3e73302406b2b9c6d6b672ca9e64e14ef0560062a91dd429"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
#[derive(Serialize)]
|
||||
struct SimpleTxIO {
|
||||
pool: u8,
|
||||
amount: u64,
|
||||
}
|
||||
|
||||
/// A simple z2z
|
||||
///
|
||||
#[test]
|
||||
fn test_example5b() {
|
||||
let _ = env_logger::try_init();
|
||||
let config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
|
||||
// z2z are preferred over t2z, so we can keep the t-notes
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)];
|
||||
let mut orders = [o!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Orchard":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
}
|
||||
/// A simple z2t sapling
|
||||
///
|
||||
#[test]
|
||||
fn test_example6() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)];
|
||||
// Change the destination to t
|
||||
let mut orders = [t!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Sapling":"9fae6f28c245e095abf8c6730098e110bb67ae3e73302406b2b9c6d6b672ca9e64e14ef0560062a91dd429"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
}
|
||||
|
||||
/// A simple o2t
|
||||
///
|
||||
#[test]
|
||||
fn test_example7() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.precedence = [ Pool::Orchard, Pool::Sapling, Pool::Transparent ];
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)];
|
||||
// Change the destination to t
|
||||
let mut orders = [t!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Orchard":{"id_note":3,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","rho":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Transparent":"0000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Orchard":"2b6dca785c846b3752d13150e1c8f197ba9c8ead0a8bee1b3a52df0ad866362941e32d1b69d438b257cf82"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
impl From<TransactionPlan> for SimpleTxPlan {
|
||||
fn from(p: TransactionPlan) -> Self {
|
||||
SimpleTxPlan {
|
||||
inputs: p.spends.iter().map(|utxo| SimpleTxIO {
|
||||
pool: utxo.source.pool() as u8,
|
||||
amount: utxo.amount,
|
||||
}).collect(),
|
||||
outputs: p.outputs.iter().map(|utxo| SimpleTxIO {
|
||||
pool: utxo.destination.pool() as u8,
|
||||
amount: utxo.amount,
|
||||
}).collect(),
|
||||
fee: p.fee,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple t2z
|
||||
///
|
||||
#[test]
|
||||
fn test_example8() {
|
||||
let _ = env_logger::try_init();
|
||||
let mut config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
config.privacy_policy = PrivacyPolicy::AnyPool;
|
||||
config.use_transparent = true;
|
||||
config.use_shielded = false;
|
||||
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50), orchard!(3, 50)];
|
||||
let mut orders = [s!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Transparent":{"txid":"0000000000000000000000000000000000000000000000000000000000000000","index":1}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Sapling":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Transparent":"c7b7b3d299bd173ea278d792b1bd5fbdd11afe34"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
}
|
||||
|
||||
/// A simple z2z (Sapling/Orchard)
|
||||
///
|
||||
#[test]
|
||||
fn test_example9() {
|
||||
let _ = env_logger::try_init();
|
||||
let config = NoteSelectConfig::new(CHANGE_ADDRESS);
|
||||
|
||||
let utxos = [utxo!(1, 50), sapling!(2, 50)];
|
||||
let mut orders = [o!(1, 10)];
|
||||
|
||||
let tx_plan = note_select_with_fee::<FeeZIP327>(&utxos, &mut orders, &config).unwrap();
|
||||
println!("{}", serde_json::to_string(&tx_plan).unwrap());
|
||||
|
||||
let tx_plan_json = serde_json::to_value(&tx_plan).unwrap();
|
||||
let expected: Value = serde_json::from_str(r#"{"spends":[{"source":{"Sapling":{"id_note":2,"diversifier":"0000000000000000000000","rseed":"0000000000000000000000000000000000000000000000000000000000000000","witness":""}},"amount":50000}],"outputs":[{"id_order":1,"destination":{"Orchard":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},"amount":10000,"memo":[246]},{"id_order":4294967295,"destination":{"Sapling":"9fae6f28c245e095abf8c6730098e110bb67ae3e73302406b2b9c6d6b672ca9e64e14ef0560062a91dd429"},"amount":30000,"memo":[246]}],"fee":10000}"#).unwrap();
|
||||
assert_eq!(tx_plan_json, expected);
|
||||
fn utxos() -> Vec<UTXO> {
|
||||
let mut utxos = vec![];
|
||||
for i in 0..30 {
|
||||
if i < 10 {
|
||||
utxos.push(utxo!(i, 25));
|
||||
}
|
||||
else if i < 20 {
|
||||
utxos.push(sapling!(i, 25));
|
||||
}
|
||||
else {
|
||||
utxos.push(orchard!(i, 25));
|
||||
}
|
||||
}
|
||||
utxos
|
||||
}
|
||||
|
|
|
@ -1,44 +1,58 @@
|
|||
use std::ops::{Add, Sub};
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use serde::Serialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_with::serde_as;
|
||||
use serde_hex::{SerHex,Strict};
|
||||
use serde_hex::{SerHex, Strict};
|
||||
use zcash_primitives::memo::MemoBytes;
|
||||
use crate::note_selection::ua::decode;
|
||||
use super::ser::MemoBytesProxy;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PrivacyPolicy {
|
||||
SamePoolOnly,
|
||||
SamePoolTypeOnly,
|
||||
AnyPool,
|
||||
pub struct TransactionBuilderConfig {
|
||||
pub change_address: String,
|
||||
}
|
||||
|
||||
impl TransactionBuilderConfig {
|
||||
pub fn new(change_address: &str) -> Self {
|
||||
TransactionBuilderConfig {
|
||||
change_address: change_address.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Serialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum Source {
|
||||
Transparent {
|
||||
#[serde(with = "SerHex::<Strict>")] txid: [u8; 32],
|
||||
#[serde(with = "SerHex::<Strict>")]
|
||||
txid: [u8; 32],
|
||||
index: u32,
|
||||
},
|
||||
Sapling {
|
||||
id_note: u32,
|
||||
#[serde(with = "SerHex::<Strict>")] diversifier: [u8; 11],
|
||||
#[serde(with = "SerHex::<Strict>")] rseed: [u8; 32],
|
||||
#[serde_as(as = "serde_with::hex::Hex")] witness: Vec<u8>,
|
||||
#[serde(with = "SerHex::<Strict>")]
|
||||
diversifier: [u8; 11],
|
||||
#[serde(with = "SerHex::<Strict>")]
|
||||
rseed: [u8; 32],
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
witness: Vec<u8>,
|
||||
},
|
||||
Orchard {
|
||||
id_note: u32,
|
||||
#[serde(with = "SerHex::<Strict>")] diversifier: [u8; 11],
|
||||
#[serde(with = "SerHex::<Strict>")] rseed: [u8; 32],
|
||||
#[serde(with = "SerHex::<Strict>")] rho: [u8; 32],
|
||||
#[serde_as(as = "serde_with::hex::Hex")] witness: Vec<u8>,
|
||||
#[serde(with = "SerHex::<Strict>")]
|
||||
diversifier: [u8; 11],
|
||||
#[serde(with = "SerHex::<Strict>")]
|
||||
rseed: [u8; 32],
|
||||
#[serde(with = "SerHex::<Strict>")]
|
||||
rho: [u8; 32],
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
witness: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Debug)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||
#[serde_as]
|
||||
pub enum Destination {
|
||||
Transparent(#[serde(with = "SerHex::<Strict>")] [u8; 20]), // MD5
|
||||
Sapling(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diversifier + Jubjub Point
|
||||
Orchard(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diviersifer + Pallas Point
|
||||
Sapling(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diversifier + Jubjub Point
|
||||
Orchard(#[serde(with = "SerHex::<Strict>")] [u8; 43]), // Diviersifer + Pallas Point
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
|
@ -48,178 +62,98 @@ pub enum Pool {
|
|||
Orchard = 2,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Order {
|
||||
pub id: u32,
|
||||
pub destinations: [Option<Destination>; 3],
|
||||
pub priority: PoolPriority,
|
||||
pub amount: u64,
|
||||
#[serde(with = "MemoBytesProxy")] pub memo: MemoBytes,
|
||||
pub is_fee: bool,
|
||||
|
||||
pub filled: u64, // mutable
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde_as]
|
||||
#[serde(remote = "MemoBytes")]
|
||||
struct MemoBytesProxy(
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
#[serde(getter = "get_memo_bytes")]
|
||||
pub Vec<u8>
|
||||
);
|
||||
|
||||
fn get_memo_bytes(memo: &MemoBytes) -> Vec<u8> {
|
||||
memo.as_slice().to_vec()
|
||||
}
|
||||
|
||||
impl From<MemoBytesProxy> for MemoBytes {
|
||||
fn from(p: MemoBytesProxy) -> MemoBytes {
|
||||
MemoBytes::from_bytes(&p.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Order {
|
||||
fn default() -> Self {
|
||||
Order {
|
||||
id: 0,
|
||||
destinations: [None; 3],
|
||||
priority: PoolPriority::OS,
|
||||
amount: 0,
|
||||
memo: MemoBytes::empty(),
|
||||
is_fee: false,
|
||||
filled: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Fill {
|
||||
pub id_order: u32,
|
||||
pub destination: Destination,
|
||||
pub amount: u64,
|
||||
#[serde(with = "MemoBytesProxy")] pub memo: MemoBytes,
|
||||
#[serde(skip)]
|
||||
pub is_fee: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Execution {
|
||||
pub allocation: PoolAllocation,
|
||||
pub fills: Vec<Fill>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct TransactionPlan {
|
||||
pub spends: Vec<UTXO>,
|
||||
pub outputs: Vec<Fill>,
|
||||
pub fee: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct PoolAllocation(pub [u64; 3]);
|
||||
|
||||
pub type PoolPrecedence = [Pool; 3];
|
||||
|
||||
#[derive(Clone, Serialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct UTXO {
|
||||
pub source: Source,
|
||||
pub amount: u64,
|
||||
}
|
||||
|
||||
impl PoolAllocation {
|
||||
pub fn total(&self) -> u64 {
|
||||
self.0.iter().sum()
|
||||
}
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Order {
|
||||
pub id: u32,
|
||||
pub destinations: [Option<Destination>; 3],
|
||||
pub amount: u64,
|
||||
#[serde(with = "MemoBytesProxy")] pub memo: MemoBytes,
|
||||
}
|
||||
|
||||
impl From<&[UTXO]> for PoolAllocation {
|
||||
fn from(utxos: &[UTXO]) -> Self {
|
||||
let mut allocation = PoolAllocation::default();
|
||||
for utxo in utxos {
|
||||
let pool = utxo.source.pool() as usize;
|
||||
allocation.0[pool] += utxo.amount;
|
||||
}
|
||||
allocation
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Fill {
|
||||
pub id_order: Option<u32>,
|
||||
pub destination: Destination,
|
||||
pub amount: u64,
|
||||
#[serde(with = "MemoBytesProxy")] pub memo: MemoBytes,
|
||||
}
|
||||
|
||||
impl Add for PoolAllocation {
|
||||
type Output = PoolAllocation;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
let mut res = PoolAllocation::default();
|
||||
for i in 0..3 {
|
||||
res.0[i] = self.0[i] + rhs.0[i];
|
||||
}
|
||||
res
|
||||
}
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct RecipientShort {
|
||||
pub address: String,
|
||||
pub amount: u64,
|
||||
}
|
||||
|
||||
impl Sub for PoolAllocation {
|
||||
type Output = PoolAllocation;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
let mut res = PoolAllocation::default();
|
||||
for i in 0..3 {
|
||||
res.0[i] = self.0[i] - rhs.0[i];
|
||||
}
|
||||
res
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct TransactionPlan {
|
||||
pub fvk: String,
|
||||
pub height: u32,
|
||||
pub spends: Vec<UTXO>,
|
||||
pub outputs: Vec<Fill>,
|
||||
pub fee: u64,
|
||||
pub net_chg: [i64; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NoteSelectConfig {
|
||||
pub privacy_policy: PrivacyPolicy,
|
||||
pub use_transparent: bool,
|
||||
pub use_shielded: bool,
|
||||
pub precedence: PoolPrecedence,
|
||||
pub change_address: String,
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct OrderGroupAmounts {
|
||||
pub t0: u64,
|
||||
pub s0: u64,
|
||||
pub o0: u64,
|
||||
pub x: u64,
|
||||
pub fee: u64,
|
||||
}
|
||||
|
||||
impl NoteSelectConfig {
|
||||
pub fn new(change_address: &str) -> Self {
|
||||
NoteSelectConfig {
|
||||
privacy_policy: PrivacyPolicy::SamePoolTypeOnly,
|
||||
use_transparent: false,
|
||||
use_shielded: true,
|
||||
precedence: [ Pool::Transparent, Pool::Sapling, Pool::Orchard ], // We prefer to keep our orchard notes
|
||||
change_address: change_address.to_string()
|
||||
}
|
||||
}
|
||||
pub struct OrderInfo {
|
||||
pub group_type: usize,
|
||||
pub amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct FundAllocation {
|
||||
pub s1: u64,
|
||||
pub o1: u64,
|
||||
pub t2: u64,
|
||||
pub s2: u64,
|
||||
pub o2: u64,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn pool(&self) -> Pool {
|
||||
pub fn pool(&self) -> usize {
|
||||
match self {
|
||||
Source::Transparent { .. } => Pool::Transparent,
|
||||
Source::Sapling { .. } => Pool::Sapling,
|
||||
Source::Orchard { .. } => Pool::Orchard,
|
||||
Source::Transparent { .. } => 0,
|
||||
Source::Sapling { .. } => 1,
|
||||
Source::Orchard { .. } => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Destination {
|
||||
pub fn pool(&self) -> Pool {
|
||||
pub fn pool(&self) -> usize {
|
||||
match self {
|
||||
Destination::Transparent { .. } => Pool::Transparent,
|
||||
Destination::Sapling { .. } => Pool::Sapling,
|
||||
Destination::Orchard { .. } => Pool::Orchard,
|
||||
Destination::Transparent { .. } => 0,
|
||||
Destination::Sapling { .. } => 1,
|
||||
Destination::Orchard { .. } => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Debug)]
|
||||
pub enum PoolPriority {
|
||||
SO = 1,
|
||||
OS = 2,
|
||||
}
|
||||
|
||||
impl PoolPriority {
|
||||
pub fn to_pool_precedence(&self) -> &'static PoolPrecedence {
|
||||
match self {
|
||||
PoolPriority::SO => &[ Pool::Sapling, Pool::Orchard, Pool::Transparent ],
|
||||
PoolPriority::OS => &[ Pool::Orchard, Pool::Sapling, Pool::Transparent ],
|
||||
impl Order {
|
||||
pub fn new(id: u32, address: &str, amount: u64, memo: MemoBytes) -> Self {
|
||||
let destinations = decode(address).unwrap();
|
||||
Order {
|
||||
id,
|
||||
destinations,
|
||||
amount,
|
||||
memo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
use zcash_address::{AddressKind, ZcashAddress};
|
||||
use zcash_address::unified::{Container, Receiver};
|
||||
use super::types::*;
|
||||
|
||||
pub fn decode(address: &str) -> anyhow::Result<[Option<Destination>; 3]> {
|
||||
let mut destinations: [Option<Destination>; 3] = [None; 3];
|
||||
let address = ZcashAddress::try_from_encoded(address)?;
|
||||
match address.kind {
|
||||
AddressKind::Sprout(_) => {}
|
||||
AddressKind::Sapling(data) => {
|
||||
let destination = Destination::Sapling(data);
|
||||
destinations[Pool::Sapling as usize] = Some(destination);
|
||||
}
|
||||
AddressKind::Unified(unified_address) => {
|
||||
for address in unified_address.items() {
|
||||
match address {
|
||||
Receiver::Orchard(data) => {
|
||||
let destination = Destination::Orchard(data);
|
||||
destinations[Pool::Orchard as usize] = Some(destination);
|
||||
}
|
||||
Receiver::Sapling(data) => {
|
||||
let destination = Destination::Sapling(data);
|
||||
destinations[Pool::Sapling as usize] = Some(destination);
|
||||
}
|
||||
Receiver::P2pkh(data) => {
|
||||
let destination = Destination::Transparent(data);
|
||||
destinations[Pool::Transparent as usize] = Some(destination);
|
||||
}
|
||||
Receiver::P2sh(_) => {}
|
||||
Receiver::Unknown { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
AddressKind::P2pkh(data) => {
|
||||
let destination = Destination::Transparent(data);
|
||||
destinations[Pool::Transparent as usize] = Some(destination);
|
||||
}
|
||||
AddressKind::P2sh(_) => {}
|
||||
}
|
||||
|
||||
Ok(destinations)
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
use crate::CoinConfig;
|
||||
use crate::note_selection::types::Source;
|
||||
use crate::note_selection::UTXO;
|
||||
use crate::CoinConfig;
|
||||
|
||||
pub async fn fetch_utxos(coin: u8, account: u32, last_height: u32, use_transparent_inputs: bool, anchor_offset: u32) -> anyhow::Result<Vec<UTXO>> {
|
||||
pub async fn fetch_utxos(
|
||||
coin: u8,
|
||||
account: u32,
|
||||
last_height: u32,
|
||||
use_transparent_inputs: bool,
|
||||
anchor_offset: u32,
|
||||
) -> anyhow::Result<Vec<UTXO>> {
|
||||
let mut utxos = vec![];
|
||||
if use_transparent_inputs {
|
||||
utxos.extend(get_transparent_utxos(coin, account).await?);
|
||||
|
@ -17,25 +23,29 @@ pub async fn fetch_utxos(coin: u8, account: u32, last_height: u32, use_transpare
|
|||
|
||||
async fn get_transparent_utxos(coin: u8, account: u32) -> anyhow::Result<Vec<UTXO>> {
|
||||
let coin = CoinConfig::get(coin);
|
||||
let db = coin.db.as_ref().unwrap();
|
||||
let db = db.lock().unwrap();
|
||||
let taddr = db.get_taddr(account)?;
|
||||
let taddr = {
|
||||
let db = coin.db.as_ref().unwrap();
|
||||
let db = db.lock().unwrap();
|
||||
db.get_taddr(account)?
|
||||
};
|
||||
if let Some(taddr) = taddr {
|
||||
let mut client = coin.connect_lwd().await?;
|
||||
let utxos = crate::taddr::get_utxos(&mut client, &taddr, account).await?;
|
||||
let utxos: Vec<_> = utxos.iter().map(|utxo| {
|
||||
let source = Source::Transparent {
|
||||
txid: utxo.txid.clone().try_into().unwrap(),
|
||||
index: utxo.index as u32,
|
||||
};
|
||||
UTXO {
|
||||
source,
|
||||
amount: utxo.value_zat as u64,
|
||||
}
|
||||
}).collect();
|
||||
let utxos: Vec<_> = utxos
|
||||
.iter()
|
||||
.map(|utxo| {
|
||||
let source = Source::Transparent {
|
||||
txid: utxo.txid.clone().try_into().unwrap(),
|
||||
index: utxo.index as u32,
|
||||
};
|
||||
UTXO {
|
||||
source,
|
||||
amount: utxo.value_zat as u64,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(utxos)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,12 @@ lazy_static! {
|
|||
}
|
||||
|
||||
mod hash;
|
||||
mod note;
|
||||
mod key;
|
||||
mod note;
|
||||
|
||||
pub use note::{OrchardDecrypter, OrchardViewKey, DecryptedOrchardNote};
|
||||
pub use hash::{ORCHARD_ROOTS, OrchardHasher};
|
||||
pub use hash::{OrchardHasher, ORCHARD_ROOTS};
|
||||
pub use key::{derive_orchard_keys, OrchardKeyBytes};
|
||||
pub use note::{DecryptedOrchardNote, OrchardDecrypter, OrchardViewKey};
|
||||
|
||||
pub fn get_proving_key() -> &'static ProvingKey {
|
||||
if !PROVING_KEY.filled() {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::sync::{Hasher, Node};
|
||||
use crate::Hash;
|
||||
use group::cofactor::CofactorCurveAffine;
|
||||
use halo2_gadgets::sinsemilla::primitives::SINSEMILLA_S;
|
||||
use halo2_proofs::arithmetic::{CurveAffine, CurveExt};
|
||||
use halo2_proofs::pasta::EpAffine;
|
||||
use halo2_proofs::pasta::group::ff::PrimeField;
|
||||
use halo2_proofs::pasta::group::Curve;
|
||||
use halo2_proofs::pasta::pallas::{self, Affine, Point};
|
||||
use halo2_proofs::pasta::EpAffine;
|
||||
use lazy_static::lazy_static;
|
||||
use crate::Hash;
|
||||
use crate::sync::{Hasher, Node};
|
||||
|
||||
pub const Q_PERSONALIZATION: &str = "z.cash:SinsemillaQ";
|
||||
pub const MERKLE_CRH_PERSONALIZATION: &str = "z.cash:Orchard-MerkleCRH";
|
||||
|
@ -100,8 +100,12 @@ impl Hasher for OrchardHasher {
|
|||
Point::batch_normalize(extended, &mut hash_affine);
|
||||
hash_affine
|
||||
.iter()
|
||||
.map(|p|
|
||||
p.coordinates().map(|c| *c.x()).unwrap_or_else(pallas::Base::zero).to_repr())
|
||||
.map(|p| {
|
||||
p.coordinates()
|
||||
.map(|c| *c.x())
|
||||
.unwrap_or_else(pallas::Base::zero)
|
||||
.to_repr()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use bip39::{Language, Mnemonic, Seed};
|
||||
use orchard::Address;
|
||||
use orchard::keys::{FullViewingKey, Scope, SpendingKey};
|
||||
use orchard::Address;
|
||||
|
||||
pub struct OrchardKeyBytes {
|
||||
pub sk: [u8; 32],
|
||||
|
@ -18,14 +18,10 @@ impl OrchardKeyBytes {
|
|||
pub fn derive_orchard_keys(coin_type: u32, seed: &str, account_index: u32) -> OrchardKeyBytes {
|
||||
let mnemonic = Mnemonic::from_phrase(seed, Language::English).unwrap();
|
||||
let seed = Seed::new(&mnemonic, "");
|
||||
let sk = SpendingKey::from_zip32_seed(
|
||||
seed.as_bytes(),
|
||||
coin_type,
|
||||
account_index,
|
||||
).unwrap();
|
||||
let sk = SpendingKey::from_zip32_seed(seed.as_bytes(), coin_type, account_index).unwrap();
|
||||
let fvk = FullViewingKey::from(&sk);
|
||||
OrchardKeyBytes {
|
||||
sk: sk.to_bytes().clone(),
|
||||
fvk: fvk.to_bytes()
|
||||
fvk: fvk.to_bytes(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use crate::chain::Nf;
|
||||
use crate::db::ReceivedNote;
|
||||
use crate::sync::{
|
||||
CompactOutputBytes, DecryptedNote, Node, OutputPosition, TrialDecrypter, ViewKey,
|
||||
};
|
||||
use crate::{CompactTx, DbAdapterBuilder};
|
||||
use orchard::keys::Scope;
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use zcash_primitives::consensus::{BlockHeight, Parameters};
|
||||
use crate::chain::Nf;
|
||||
use crate::{CompactTx, DbAdapterBuilder};
|
||||
use crate::db::ReceivedNote;
|
||||
use crate::sync::{CompactOutputBytes, DecryptedNote, Node, OutputPosition, TrialDecrypter, ViewKey};
|
||||
use zcash_note_encryption;
|
||||
use zcash_params::coin::CoinType;
|
||||
use zcash_primitives::consensus::{BlockHeight, Parameters};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OrchardViewKey {
|
||||
|
@ -33,13 +35,19 @@ pub struct DecryptedOrchardNote {
|
|||
}
|
||||
|
||||
impl DecryptedNote<OrchardDomain, OrchardViewKey> for DecryptedOrchardNote {
|
||||
fn from_parts(vk: OrchardViewKey, note: orchard::Note, pa: orchard::Address, output_position: OutputPosition, cmx: Node) -> Self {
|
||||
fn from_parts(
|
||||
vk: OrchardViewKey,
|
||||
note: orchard::Note,
|
||||
pa: orchard::Address,
|
||||
output_position: OutputPosition,
|
||||
cmx: Node,
|
||||
) -> Self {
|
||||
DecryptedOrchardNote {
|
||||
vk,
|
||||
note,
|
||||
pa,
|
||||
output_position,
|
||||
cmx
|
||||
cmx,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +70,7 @@ impl DecryptedNote<OrchardDomain, OrchardViewKey> for DecryptedOrchardNote {
|
|||
rcm: self.note.rseed().as_bytes().to_vec(),
|
||||
nf: self.note.nullifier(&self.vk.fvk).to_bytes().to_vec(),
|
||||
rho: Some(self.note.rho().to_bytes().to_vec()),
|
||||
spent: None
|
||||
spent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,24 +80,27 @@ pub struct OrchardDecrypter<N> {
|
|||
pub network: N,
|
||||
}
|
||||
|
||||
impl <N> OrchardDecrypter<N> {
|
||||
impl<N> OrchardDecrypter<N> {
|
||||
pub fn new(network: N) -> Self {
|
||||
OrchardDecrypter {
|
||||
network,
|
||||
}
|
||||
OrchardDecrypter { network }
|
||||
}
|
||||
}
|
||||
|
||||
impl <N: Parameters> TrialDecrypter<N, OrchardDomain, OrchardViewKey, DecryptedOrchardNote> for OrchardDecrypter<N> {
|
||||
impl<N: Parameters> TrialDecrypter<N, OrchardDomain, OrchardViewKey, DecryptedOrchardNote>
|
||||
for OrchardDecrypter<N>
|
||||
{
|
||||
fn domain(&self, _height: BlockHeight, cob: &CompactOutputBytes) -> OrchardDomain {
|
||||
OrchardDomain::for_nullifier(orchard::note::Nullifier::from_bytes(&cob.nullifier).unwrap())
|
||||
}
|
||||
|
||||
fn spends(&self, vtx: &CompactTx) -> Vec<Nf> {
|
||||
vtx.actions.iter().map(|co| {
|
||||
let nf: [u8; 32] = co.nullifier.clone().try_into().unwrap();
|
||||
Nf(nf)
|
||||
}).collect()
|
||||
vtx.actions
|
||||
.iter()
|
||||
.map(|co| {
|
||||
let nf: [u8; 32] = co.nullifier.clone().try_into().unwrap();
|
||||
Nf(nf)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn outputs(&self, vtx: &CompactTx) -> Vec<CompactOutputBytes> {
|
||||
|
@ -104,14 +115,17 @@ pub fn test_decrypt() -> anyhow::Result<()> {
|
|||
// let mut cmx = hex::decode("df45e00eb39e4c281e2804a366d3010b7f663724472d12637e0a749e6ce22719").unwrap();
|
||||
// let ciphertext = hex::decode("d9bc6ee09b0afde5dd69bfdf4b667a38da3e1084e84eb6752d54800b9f5110203b60496ab5313dba3f2acb9ef30bcaf68fbfcc59").unwrap();
|
||||
|
||||
let nullifier = hex::decode("ea1b97cc83d326db4130433022f68dd32a0bc707448b19b0980e4e6404412b29").unwrap();
|
||||
let epk = hex::decode("e2f666e905666f29bb678c694602b2768bea655c0f2b18f9c342ad8b64b18c0c").unwrap();
|
||||
let cmx = hex::decode("4a95dbf0d1d0cac1376a0b8fb0fc2ed2843d0e2670dd976a63386b293f30de25").unwrap();
|
||||
let nullifier =
|
||||
hex::decode("ea1b97cc83d326db4130433022f68dd32a0bc707448b19b0980e4e6404412b29").unwrap();
|
||||
let epk =
|
||||
hex::decode("e2f666e905666f29bb678c694602b2768bea655c0f2b18f9c342ad8b64b18c0c").unwrap();
|
||||
let cmx =
|
||||
hex::decode("4a95dbf0d1d0cac1376a0b8fb0fc2ed2843d0e2670dd976a63386b293f30de25").unwrap();
|
||||
let ciphertext = hex::decode("73640095a90bb03d14f687d6acf4822618a3def1da3b71a588da1c68e25042f7c9aa759778e73aa2bb39d1061e51c1e8cf5e0bce").unwrap();
|
||||
|
||||
let db_builder = DbAdapterBuilder {
|
||||
coin_type: CoinType::Zcash,
|
||||
db_path: "./zec.db".to_string()
|
||||
db_path: "./zec.db".to_string(),
|
||||
};
|
||||
let db = db_builder.build()?;
|
||||
let keys = db.get_orchard_fvks()?.first().unwrap().clone();
|
||||
|
@ -121,11 +135,16 @@ pub fn test_decrypt() -> anyhow::Result<()> {
|
|||
nullifier: nullifier.clone().try_into().unwrap(),
|
||||
epk: epk.try_into().unwrap(),
|
||||
cmx: cmx.try_into().unwrap(),
|
||||
ciphertext: ciphertext.try_into().unwrap()
|
||||
ciphertext: ciphertext.try_into().unwrap(),
|
||||
};
|
||||
let domain = OrchardDomain::for_nullifier(orchard::note::Nullifier::from_bytes(&nullifier.try_into().unwrap()).unwrap());
|
||||
let r = zcash_note_encryption::try_compact_note_decryption(&domain, &fvk.to_ivk(Scope::External), &output);
|
||||
let domain = OrchardDomain::for_nullifier(
|
||||
orchard::note::Nullifier::from_bytes(&nullifier.try_into().unwrap()).unwrap(),
|
||||
);
|
||||
let r = zcash_note_encryption::try_compact_note_decryption(
|
||||
&domain,
|
||||
&fvk.to_ivk(Scope::External),
|
||||
&output,
|
||||
);
|
||||
println!("{:?}", r);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
11
src/pay.rs
11
src/pay.rs
|
@ -1,6 +1,7 @@
|
|||
use crate::db::SpendableNote;
|
||||
// use crate::wallet::RecipientMemo;
|
||||
use crate::api::payment::RecipientMemo;
|
||||
use crate::chain::get_latest_height;
|
||||
use crate::coinconfig::CoinConfig;
|
||||
use crate::{GetAddressUtxosReply, Hash, RawTransaction};
|
||||
use anyhow::anyhow;
|
||||
|
@ -12,7 +13,10 @@ use serde::{Deserialize, Serialize};
|
|||
use std::sync::mpsc;
|
||||
use tonic::Request;
|
||||
use zcash_client_backend::address::RecipientAddress;
|
||||
use zcash_client_backend::encoding::{decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, encode_payment_address};
|
||||
use zcash_client_backend::encoding::{
|
||||
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
||||
encode_payment_address,
|
||||
};
|
||||
use zcash_params::coin::{get_coin_chain, CoinChain, CoinType};
|
||||
use zcash_primitives::consensus::{BlockHeight, Parameters};
|
||||
use zcash_primitives::keys::OutgoingViewingKey;
|
||||
|
@ -25,7 +29,6 @@ use zcash_primitives::transaction::builder::{Builder, Progress};
|
|||
use zcash_primitives::transaction::components::amount::{DEFAULT_FEE, MAX_MONEY};
|
||||
use zcash_primitives::transaction::components::{Amount, OutPoint, TxOut as ZTxOut};
|
||||
use zcash_primitives::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
|
||||
use crate::chain::get_latest_height;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Tx {
|
||||
|
@ -350,7 +353,8 @@ impl Tx {
|
|||
let fvk = decode_extended_full_viewing_key(
|
||||
chain.network().hrp_sapling_extended_full_viewing_key(),
|
||||
&txin.fvk,
|
||||
).map_err(|_| anyhow!("Bech32 Decode Error"))?;
|
||||
)
|
||||
.map_err(|_| anyhow!("Bech32 Decode Error"))?;
|
||||
if fvk != efvk {
|
||||
anyhow::bail!("Incorrect account - Secret key mismatch")
|
||||
}
|
||||
|
@ -439,4 +443,3 @@ pub fn get_tx_summary(tx: &Tx) -> anyhow::Result<TxSummary> {
|
|||
}
|
||||
Ok(TxSummary { recipients })
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::io::Read;
|
||||
use group::GroupEncoding;
|
||||
use jubjub::{ExtendedNielsPoint, ExtendedPoint, SubgroupPoint};
|
||||
use lazy_static::lazy_static;
|
||||
use std::io::Read;
|
||||
use zcash_params::GENERATORS;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -11,8 +11,8 @@ lazy_static! {
|
|||
mod hash;
|
||||
mod note;
|
||||
|
||||
pub use note::{SaplingDecrypter, SaplingViewKey, DecryptedSaplingNote};
|
||||
pub use hash::{SaplingHasher, SAPLING_ROOTS};
|
||||
pub use note::{DecryptedSaplingNote, SaplingDecrypter, SaplingViewKey};
|
||||
|
||||
fn read_generators_bin() -> Vec<ExtendedNielsPoint> {
|
||||
let mut generators_bin = GENERATORS;
|
||||
|
@ -31,4 +31,3 @@ fn read_generators_bin() -> Vec<ExtendedNielsPoint> {
|
|||
}
|
||||
gens
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use super::GENERATORS_EXP;
|
||||
use crate::sync::{Hasher, Node};
|
||||
use crate::Hash;
|
||||
use ff::PrimeField;
|
||||
use group::Curve;
|
||||
use jubjub::{AffinePoint, ExtendedPoint, Fr};
|
||||
use lazy_static::lazy_static;
|
||||
use zcash_primitives::constants::PEDERSEN_HASH_CHUNKS_PER_GENERATOR;
|
||||
use crate::sync::{Hasher, Node};
|
||||
use crate::Hash;
|
||||
use super::GENERATORS_EXP;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SAPLING_ROOTS: Vec<Hash> = {
|
||||
|
@ -45,10 +45,7 @@ fn accumulate_generator(acc: &Fr, idx_generator: u32) -> ExtendedPoint {
|
|||
|
||||
pub fn hash_combine(depth: u8, left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
|
||||
let p = hash_combine_inner(depth, left, right);
|
||||
p
|
||||
.to_affine()
|
||||
.get_u()
|
||||
.to_repr()
|
||||
p.to_affine().get_u().to_repr()
|
||||
}
|
||||
|
||||
pub fn hash_combine_inner(depth: u8, left: &[u8; 32], right: &[u8; 32]) -> ExtendedPoint {
|
||||
|
@ -95,13 +92,12 @@ pub fn hash_combine_inner(depth: u8, left: &[u8; 32], right: &[u8; 32]) -> Exten
|
|||
v = v >> bit_offset & 0x07; // keep 3 bits
|
||||
accumulate_scalar(&mut acc, &mut cur, v as u8);
|
||||
|
||||
if (i+3) % PEDERSEN_HASH_CHUNKS_PER_GENERATOR as u32 == 0 {
|
||||
if (i + 3) % PEDERSEN_HASH_CHUNKS_PER_GENERATOR as u32 == 0 {
|
||||
hash += accumulate_generator(&acc, idx_generator);
|
||||
idx_generator += 1;
|
||||
acc = Fr::zero();
|
||||
cur = Fr::one();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
cur = cur.double().double().double(); // 2^4 * cur
|
||||
}
|
||||
bit_offset += 3;
|
||||
|
@ -136,20 +132,17 @@ impl Hasher for SaplingHasher {
|
|||
fn normalize(&self, extended: &[Self::Extended]) -> Vec<Node> {
|
||||
let mut hash_affine = vec![AffinePoint::identity(); extended.len()];
|
||||
ExtendedPoint::batch_normalize(extended, &mut hash_affine);
|
||||
hash_affine
|
||||
.iter()
|
||||
.map(|p| p.get_u().to_repr())
|
||||
.collect()
|
||||
hash_affine.iter().map(|p| p.get_u().to_repr()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::TryInto;
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use crate::hash::pedersen_hash;
|
||||
use crate::sapling::hash::hash_combine;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[test]
|
||||
fn test_hash1() {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use std::convert::TryInto;
|
||||
use crate::chain::Nf;
|
||||
use crate::db::ReceivedNote;
|
||||
use crate::sync::Node;
|
||||
use crate::sync::{CompactOutputBytes, DecryptedNote, OutputPosition, TrialDecrypter, ViewKey};
|
||||
use crate::CompactTx;
|
||||
use ff::PrimeField;
|
||||
use std::convert::TryInto;
|
||||
use zcash_note_encryption::Domain;
|
||||
use zcash_primitives::consensus::{BlockHeight, Parameters};
|
||||
use zcash_primitives::sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain};
|
||||
use zcash_primitives::sapling::{PaymentAddress, SaplingIvk};
|
||||
use zcash_primitives::zip32::ExtendedFullViewingKey;
|
||||
use crate::chain::Nf;
|
||||
use crate::CompactTx;
|
||||
use crate::db::ReceivedNote;
|
||||
use crate::sync::Node;
|
||||
use crate::sync::{CompactOutputBytes, DecryptedNote, OutputPosition, TrialDecrypter, ViewKey};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SaplingViewKey {
|
||||
|
@ -18,8 +18,10 @@ pub struct SaplingViewKey {
|
|||
pub ivk: SaplingIvk,
|
||||
}
|
||||
|
||||
impl <P: Parameters> ViewKey<SaplingDomain<P>> for SaplingViewKey {
|
||||
fn account(&self) -> u32 { self.account }
|
||||
impl<P: Parameters> ViewKey<SaplingDomain<P>> for SaplingViewKey {
|
||||
fn account(&self) -> u32 {
|
||||
self.account
|
||||
}
|
||||
fn ivk(&self) -> <SaplingDomain<P> as Domain>::IncomingViewingKey {
|
||||
PreparedIncomingViewingKey::new(&self.ivk)
|
||||
}
|
||||
|
@ -33,8 +35,14 @@ pub struct DecryptedSaplingNote {
|
|||
pub cmx: Node,
|
||||
}
|
||||
|
||||
impl <P: Parameters> DecryptedNote<SaplingDomain<P>, SaplingViewKey> for DecryptedSaplingNote {
|
||||
fn from_parts(vk: SaplingViewKey, note: zcash_primitives::sapling::Note, pa: PaymentAddress, output_position: OutputPosition, cmx: Node) -> Self {
|
||||
impl<P: Parameters> DecryptedNote<SaplingDomain<P>, SaplingViewKey> for DecryptedSaplingNote {
|
||||
fn from_parts(
|
||||
vk: SaplingViewKey,
|
||||
note: zcash_primitives::sapling::Note,
|
||||
pa: PaymentAddress,
|
||||
output_position: OutputPosition,
|
||||
cmx: Node,
|
||||
) -> Self {
|
||||
DecryptedSaplingNote {
|
||||
vk,
|
||||
note,
|
||||
|
@ -63,34 +71,37 @@ impl <P: Parameters> DecryptedNote<SaplingDomain<P>, SaplingViewKey> for Decrypt
|
|||
rcm: self.note.rcm().to_repr().to_vec(),
|
||||
nf: self.note.nf(&viewing_key.nk, position).to_vec(),
|
||||
rho: None,
|
||||
spent: None
|
||||
spent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SaplingDecrypter<N> {
|
||||
pub struct SaplingDecrypter<N> {
|
||||
pub network: N,
|
||||
}
|
||||
|
||||
impl <N> SaplingDecrypter<N> {
|
||||
impl<N> SaplingDecrypter<N> {
|
||||
pub fn new(network: N) -> Self {
|
||||
SaplingDecrypter {
|
||||
network,
|
||||
}
|
||||
SaplingDecrypter { network }
|
||||
}
|
||||
}
|
||||
|
||||
impl <N: Parameters> TrialDecrypter<N, SaplingDomain<N>, SaplingViewKey, DecryptedSaplingNote> for SaplingDecrypter<N> {
|
||||
impl<N: Parameters> TrialDecrypter<N, SaplingDomain<N>, SaplingViewKey, DecryptedSaplingNote>
|
||||
for SaplingDecrypter<N>
|
||||
{
|
||||
fn domain(&self, height: BlockHeight, _cob: &CompactOutputBytes) -> SaplingDomain<N> {
|
||||
SaplingDomain::<N>::for_height(self.network.clone(), height)
|
||||
}
|
||||
|
||||
fn spends(&self, vtx: &CompactTx) -> Vec<Nf> {
|
||||
vtx.spends.iter().map(|co| {
|
||||
let nf: [u8; 32] = co.nf.clone().try_into().unwrap();
|
||||
Nf(nf)
|
||||
}).collect()
|
||||
vtx.spends
|
||||
.iter()
|
||||
.map(|co| {
|
||||
let nf: [u8; 32] = co.nf.clone().try_into().unwrap();
|
||||
Nf(nf)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn outputs(&self, vtx: &CompactTx) -> Vec<CompactOutputBytes> {
|
||||
|
|
51
src/scan.rs
51
src/scan.rs
|
@ -2,26 +2,29 @@ use crate::chain::get_latest_height;
|
|||
use crate::db::AccountViewKey;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::transaction::{get_transaction_details, GetTransactionDetailRequest, retrieve_tx_info};
|
||||
use crate::{connect_lightwalletd, CompactBlock, CompactSaplingOutput, CompactTx, DbAdapterBuilder, CoinConfig};
|
||||
use crate::chain::{DecryptNode, download_chain};
|
||||
use crate::chain::{download_chain, DecryptNode};
|
||||
use crate::transaction::{get_transaction_details, retrieve_tx_info, GetTransactionDetailRequest};
|
||||
use crate::{
|
||||
connect_lightwalletd, CoinConfig, CompactBlock, CompactSaplingOutput, CompactTx,
|
||||
DbAdapterBuilder,
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use lazy_static::lazy_static;
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex;
|
||||
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
|
||||
use zcash_primitives::consensus::{Network, Parameters};
|
||||
|
||||
use zcash_primitives::sapling::Note;
|
||||
use zcash_primitives::sapling::note_encryption::SaplingDomain;
|
||||
use crate::orchard::{DecryptedOrchardNote, OrchardDecrypter, OrchardHasher, OrchardViewKey};
|
||||
use crate::sapling::{DecryptedSaplingNote, SaplingDecrypter, SaplingHasher, SaplingViewKey};
|
||||
use crate::sync::{Synchronizer, WarpProcessor};
|
||||
use zcash_primitives::sapling::note_encryption::SaplingDomain;
|
||||
use zcash_primitives::sapling::Note;
|
||||
|
||||
pub struct Blocks(pub Vec<CompactBlock>, pub usize);
|
||||
|
||||
|
@ -55,11 +58,23 @@ pub struct TxIdHeight {
|
|||
index: u32,
|
||||
}
|
||||
|
||||
type SaplingSynchronizer = Synchronizer<Network, SaplingDomain<Network>, SaplingViewKey, DecryptedSaplingNote,
|
||||
SaplingDecrypter<Network>, SaplingHasher>;
|
||||
type SaplingSynchronizer = Synchronizer<
|
||||
Network,
|
||||
SaplingDomain<Network>,
|
||||
SaplingViewKey,
|
||||
DecryptedSaplingNote,
|
||||
SaplingDecrypter<Network>,
|
||||
SaplingHasher,
|
||||
>;
|
||||
|
||||
type OrchardSynchronizer = Synchronizer<Network, OrchardDomain, OrchardViewKey, DecryptedOrchardNote,
|
||||
OrchardDecrypter<Network>, OrchardHasher>;
|
||||
type OrchardSynchronizer = Synchronizer<
|
||||
Network,
|
||||
OrchardDomain,
|
||||
OrchardViewKey,
|
||||
DecryptedOrchardNote,
|
||||
OrchardDecrypter<Network>,
|
||||
OrchardHasher,
|
||||
>;
|
||||
|
||||
pub async fn sync_async<'a>(
|
||||
coin: u8,
|
||||
|
@ -95,11 +110,23 @@ pub async fn sync_async<'a>(
|
|||
let mut height = start_height;
|
||||
let (blocks_tx, mut blocks_rx) = mpsc::channel::<Blocks>(1);
|
||||
tokio::spawn(async move {
|
||||
download_chain(&mut client, start_height, end_height, prev_hash, max_cost, cancel, blocks_tx).await?;
|
||||
download_chain(
|
||||
&mut client,
|
||||
start_height,
|
||||
end_height,
|
||||
prev_hash,
|
||||
max_cost,
|
||||
cancel,
|
||||
blocks_tx,
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
|
||||
let db_builder = DbAdapterBuilder { coin_type: c.coin_type, db_path: db_path.clone() };
|
||||
let db_builder = DbAdapterBuilder {
|
||||
coin_type: c.coin_type,
|
||||
db_path: db_path.clone(),
|
||||
};
|
||||
while let Some(blocks) = blocks_rx.recv().await {
|
||||
let first_block = blocks.0.first().unwrap(); // cannot be empty because blocks are not
|
||||
log::info!("Height: {}", first_block.height);
|
||||
|
|
124
src/sync.rs
124
src/sync.rs
|
@ -1,23 +1,31 @@
|
|||
use crate::chain::Nf;
|
||||
use crate::db::{DbAdapterBuilder, ReceivedNote, ReceivedNoteShort};
|
||||
use crate::{CompactBlock, DbAdapter};
|
||||
use anyhow::Result;
|
||||
use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::marker::PhantomData;
|
||||
use anyhow::Result;
|
||||
use rayon::prelude::*;
|
||||
use zcash_note_encryption::BatchDomain;
|
||||
use zcash_primitives::consensus::Parameters;
|
||||
use crate::{CompactBlock, DbAdapter};
|
||||
use crate::chain::Nf;
|
||||
use crate::db::{DbAdapterBuilder, ReceivedNote, ReceivedNoteShort};
|
||||
|
||||
pub mod tree;
|
||||
pub mod trial_decrypt;
|
||||
|
||||
pub use trial_decrypt::{ViewKey, DecryptedNote, TrialDecrypter, CompactOutputBytes, OutputPosition};
|
||||
pub use tree::{Hasher, Node, WarpProcessor, Witness, CTree};
|
||||
use crate::sync::tree::TreeCheckpoint;
|
||||
pub use tree::{CTree, Hasher, Node, WarpProcessor, Witness};
|
||||
pub use trial_decrypt::{
|
||||
CompactOutputBytes, DecryptedNote, OutputPosition, TrialDecrypter, ViewKey,
|
||||
};
|
||||
|
||||
pub struct Synchronizer<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>, VK: ViewKey<D>, DN: DecryptedNote<D, VK>,
|
||||
TD: TrialDecrypter<N, D, VK, DN>, H: Hasher> {
|
||||
pub struct Synchronizer<
|
||||
N: Parameters,
|
||||
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>,
|
||||
VK: ViewKey<D>,
|
||||
DN: DecryptedNote<D, VK>,
|
||||
TD: TrialDecrypter<N, D, VK, DN>,
|
||||
H: Hasher,
|
||||
> {
|
||||
pub decrypter: TD,
|
||||
pub warper: WarpProcessor<H>,
|
||||
pub vks: Vec<VK>,
|
||||
|
@ -31,13 +39,22 @@ pub struct Synchronizer<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes =
|
|||
pub _phantom: PhantomData<(N, D, DN)>,
|
||||
}
|
||||
|
||||
impl <N: Parameters + Sync,
|
||||
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]> + Sync + Send,
|
||||
VK: ViewKey<D> + Sync + Send,
|
||||
DN: DecryptedNote<D, VK> + Sync,
|
||||
TD: TrialDecrypter<N, D, VK, DN> + Sync,
|
||||
H: Hasher> Synchronizer<N, D, VK, DN, TD, H> {
|
||||
pub fn new(decrypter: TD, warper: WarpProcessor<H>, vks: Vec<VK>, db: DbAdapterBuilder, shielded_pool: String) -> Self {
|
||||
impl<
|
||||
N: Parameters + Sync,
|
||||
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]> + Sync + Send,
|
||||
VK: ViewKey<D> + Sync + Send,
|
||||
DN: DecryptedNote<D, VK> + Sync,
|
||||
TD: TrialDecrypter<N, D, VK, DN> + Sync,
|
||||
H: Hasher,
|
||||
> Synchronizer<N, D, VK, DN, TD, H>
|
||||
{
|
||||
pub fn new(
|
||||
decrypter: TD,
|
||||
warper: WarpProcessor<H>,
|
||||
vks: Vec<VK>,
|
||||
db: DbAdapterBuilder,
|
||||
shielded_pool: String,
|
||||
) -> Self {
|
||||
Synchronizer {
|
||||
decrypter,
|
||||
warper,
|
||||
|
@ -49,13 +66,14 @@ impl <N: Parameters + Sync,
|
|||
nullifiers: HashMap::default(),
|
||||
tree: CTree::new(),
|
||||
witnesses: vec![],
|
||||
_phantom: Default::default()
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, height: u32) -> Result<()> {
|
||||
let db = self.db.build()?;
|
||||
let TreeCheckpoint { tree, witnesses } = db.get_tree_by_name(height, &self.shielded_pool)?;
|
||||
let TreeCheckpoint { tree, witnesses } =
|
||||
db.get_tree_by_name(height, &self.shielded_pool)?;
|
||||
self.tree = tree;
|
||||
self.witnesses = witnesses;
|
||||
self.note_position = self.tree.get_position();
|
||||
|
@ -67,7 +85,9 @@ impl <N: Parameters + Sync,
|
|||
}
|
||||
|
||||
pub fn process(&mut self, blocks: &[CompactBlock]) -> Result<()> {
|
||||
if blocks.is_empty() { return Ok(()) }
|
||||
if blocks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let decrypter = self.decrypter.clone();
|
||||
let decrypted_blocks: Vec<_> = blocks
|
||||
.par_iter()
|
||||
|
@ -81,21 +101,36 @@ impl <N: Parameters + Sync,
|
|||
let mut new_witnesses = vec![];
|
||||
for decb in decrypted_blocks.iter() {
|
||||
for dectx in decb.txs.iter() {
|
||||
let id_tx = DbAdapter::store_transaction(&dectx.tx_id, dectx.account, dectx.height, dectx.timestamp, dectx.tx_index as u32, &db_tx)?;
|
||||
let id_tx = DbAdapter::store_transaction(
|
||||
&dectx.tx_id,
|
||||
dectx.account,
|
||||
dectx.height,
|
||||
dectx.timestamp,
|
||||
dectx.tx_index as u32,
|
||||
&db_tx,
|
||||
)?;
|
||||
let mut balance: i64 = 0;
|
||||
for decn in dectx.notes.iter() {
|
||||
let position = decn.position(self.note_position);
|
||||
let rn: ReceivedNote = decn.to_received_note(position as u64);
|
||||
let id_note = DbAdapter::store_received_note(&rn, id_tx, position, &db_tx)?;
|
||||
let nf = Nf(rn.nf.try_into().unwrap());
|
||||
self.nullifiers.insert(nf, ReceivedNoteShort {
|
||||
id: id_note,
|
||||
account: rn.account,
|
||||
self.nullifiers.insert(
|
||||
nf,
|
||||
value: rn.value
|
||||
});
|
||||
ReceivedNoteShort {
|
||||
id: id_note,
|
||||
account: rn.account,
|
||||
nf,
|
||||
value: rn.value,
|
||||
},
|
||||
);
|
||||
let witness = Witness::new(position, id_note, &decn.cmx());
|
||||
log::info!("Witness {} {} {}", witness.position, witness.id_note, hex::encode(witness.cmx));
|
||||
log::info!(
|
||||
"Witness {} {} {}",
|
||||
witness.position,
|
||||
witness.id_note,
|
||||
hex::encode(witness.cmx)
|
||||
);
|
||||
new_witnesses.push(witness);
|
||||
balance += rn.value as i64;
|
||||
}
|
||||
|
@ -112,7 +147,14 @@ impl <N: Parameters + Sync,
|
|||
for (tx_index, tx) in b.vtx.iter().enumerate() {
|
||||
for sp in self.decrypter.spends(tx).iter() {
|
||||
if let Some(rn) = self.nullifiers.get(sp) {
|
||||
let id_tx = DbAdapter::store_transaction(&tx.hash, rn.account, b.height as u32, b.time, tx_index as u32, &db_tx)?;
|
||||
let id_tx = DbAdapter::store_transaction(
|
||||
&tx.hash,
|
||||
rn.account,
|
||||
b.height as u32,
|
||||
b.time,
|
||||
tx_index as u32,
|
||||
&db_tx,
|
||||
)?;
|
||||
DbAdapter::add_value(id_tx, -(rn.value as i64), &db_tx)?;
|
||||
DbAdapter::mark_spent(rn.id, b.height as u32, &db_tx)?;
|
||||
self.nullifiers.remove(sp);
|
||||
|
@ -143,19 +185,25 @@ impl <N: Parameters + Sync,
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use zcash_primitives::consensus::Network;
|
||||
use zcash_primitives::sapling::note_encryption::SaplingDomain;
|
||||
use super::Synchronizer;
|
||||
use crate::coinconfig::COIN_CONFIG;
|
||||
use crate::db::DbAdapterBuilder;
|
||||
use crate::init_coin;
|
||||
use crate::sapling::{DecryptedSaplingNote, SaplingDecrypter, SaplingHasher, SaplingViewKey};
|
||||
use crate::sync::CTree;
|
||||
use crate::sync::tree::WarpProcessor;
|
||||
use super::Synchronizer;
|
||||
use crate::sync::CTree;
|
||||
use std::collections::HashMap;
|
||||
use zcash_primitives::consensus::Network;
|
||||
use zcash_primitives::sapling::note_encryption::SaplingDomain;
|
||||
|
||||
type SaplingSynchronizer = Synchronizer<Network, SaplingDomain<Network>, SaplingViewKey, DecryptedSaplingNote,
|
||||
SaplingDecrypter<Network>, SaplingHasher>;
|
||||
type SaplingSynchronizer = Synchronizer<
|
||||
Network,
|
||||
SaplingDomain<Network>,
|
||||
SaplingViewKey,
|
||||
DecryptedSaplingNote,
|
||||
SaplingDecrypter<Network>,
|
||||
SaplingHasher,
|
||||
>;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
|
@ -166,18 +214,20 @@ mod tests {
|
|||
decrypter: SaplingDecrypter::new(*network),
|
||||
warper: WarpProcessor::new(SaplingHasher::default()),
|
||||
vks: vec![],
|
||||
db: DbAdapterBuilder { coin_type: coin.coin_type, db_path: coin.db_path.as_ref().unwrap().to_owned() },
|
||||
db: DbAdapterBuilder {
|
||||
coin_type: coin.coin_type,
|
||||
db_path: coin.db_path.as_ref().unwrap().to_owned(),
|
||||
},
|
||||
shielded_pool: "sapling".to_string(),
|
||||
tree: CTree::new(),
|
||||
witnesses: vec![],
|
||||
|
||||
note_position: 0,
|
||||
nullifiers: HashMap::new(),
|
||||
_phantom: Default::default()
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
|
||||
synchronizer.initialize(1000).unwrap();
|
||||
synchronizer.process(&vec![]).unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use crate::Hash;
|
||||
use byteorder::WriteBytesExt;
|
||||
use group::Curve;
|
||||
use rayon::prelude::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::marker::PhantomData;
|
||||
use byteorder::WriteBytesExt;
|
||||
use group::Curve;
|
||||
use zcash_encoding::{Optional, Vector};
|
||||
use crate::Hash;
|
||||
|
||||
pub type Node = [u8; 32];
|
||||
|
||||
|
@ -157,7 +157,12 @@ impl Witness {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn auth_path<H: Hasher>(&self, height: usize, empty_roots: &[Node], hasher: &H) -> Vec<Node> {
|
||||
pub fn auth_path<H: Hasher>(
|
||||
&self,
|
||||
height: usize,
|
||||
empty_roots: &[Node],
|
||||
hasher: &H,
|
||||
) -> Vec<Node> {
|
||||
let mut filled_iter = self.filled.iter();
|
||||
let mut cursor_used = false;
|
||||
let mut next_filler = move |depth: usize| {
|
||||
|
@ -261,7 +266,7 @@ struct CTreeBuilder<H: Hasher> {
|
|||
hasher: H,
|
||||
}
|
||||
|
||||
impl <H: Hasher> Builder for CTreeBuilder<H> {
|
||||
impl<H: Hasher> Builder for CTreeBuilder<H> {
|
||||
type Context = ();
|
||||
type Output = CTree;
|
||||
|
||||
|
@ -309,11 +314,10 @@ impl <H: Hasher> Builder for CTreeBuilder<H> {
|
|||
|
||||
fn up(&mut self) {
|
||||
let h = if self.left.is_some() && self.right.is_some() {
|
||||
Some(self.hasher.node_combine(
|
||||
self.depth,
|
||||
&self.left.unwrap(),
|
||||
&self.right.unwrap(),
|
||||
))
|
||||
Some(
|
||||
self.hasher
|
||||
.node_combine(self.depth, &self.left.unwrap(), &self.right.unwrap()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -333,7 +337,9 @@ impl <H: Hasher> Builder for CTreeBuilder<H> {
|
|||
}
|
||||
|
||||
fn finished(&self) -> bool {
|
||||
self.depth as usize >= self.prev_tree.parents.len() && self.left.is_none() && self.right.is_none()
|
||||
self.depth as usize >= self.prev_tree.parents.len()
|
||||
&& self.left.is_none()
|
||||
&& self.right.is_none()
|
||||
}
|
||||
|
||||
fn finalize(self, _context: &()) -> CTree {
|
||||
|
@ -345,7 +351,7 @@ impl <H: Hasher> Builder for CTreeBuilder<H> {
|
|||
}
|
||||
}
|
||||
|
||||
impl <H: Hasher> CTreeBuilder<H> {
|
||||
impl<H: Hasher> CTreeBuilder<H> {
|
||||
fn new(prev_tree: &CTree, len: usize, first_block: bool, hasher: H) -> Self {
|
||||
let start = prev_tree.get_position();
|
||||
CTreeBuilder {
|
||||
|
@ -380,11 +386,7 @@ impl <H: Hasher> CTreeBuilder<H> {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get<'a>(
|
||||
commitments: &'a [Node],
|
||||
index: usize,
|
||||
offset: &'a Option<Node>,
|
||||
) -> &'a Node {
|
||||
fn get<'a>(commitments: &'a [Node], index: usize, offset: &'a Option<Node>) -> &'a Node {
|
||||
Self::get_opt(commitments, index, offset).unwrap()
|
||||
}
|
||||
|
||||
|
@ -409,8 +411,7 @@ fn combine_level<H: Hasher>(
|
|||
let nn = n / 2;
|
||||
let next_level = if nn > 100 {
|
||||
batch_level_combine(commitments, offset, nn, depth, hasher)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
single_level_combine(commitments, offset, nn, depth, hasher)
|
||||
};
|
||||
|
||||
|
@ -418,7 +419,13 @@ fn combine_level<H: Hasher>(
|
|||
nn
|
||||
}
|
||||
|
||||
fn batch_level_combine<H: Hasher>(commitments: &mut [Node], offset: Option<Node>, nn: usize, depth: u8, hasher: &H) -> Vec<Node> {
|
||||
fn batch_level_combine<H: Hasher>(
|
||||
commitments: &mut [Node],
|
||||
offset: Option<Node>,
|
||||
nn: usize,
|
||||
depth: u8,
|
||||
hasher: &H,
|
||||
) -> Vec<Node> {
|
||||
let hash_extended: Vec<_> = (0..nn)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
|
@ -432,7 +439,13 @@ fn batch_level_combine<H: Hasher>(commitments: &mut [Node], offset: Option<Node>
|
|||
hasher.normalize(&hash_extended)
|
||||
}
|
||||
|
||||
fn single_level_combine<H: Hasher>(commitments: &mut [Node], offset: Option<Node>, nn: usize, depth: u8, hasher: &H) -> Vec<Node> {
|
||||
fn single_level_combine<H: Hasher>(
|
||||
commitments: &mut [Node],
|
||||
offset: Option<Node>,
|
||||
nn: usize,
|
||||
depth: u8,
|
||||
hasher: &H,
|
||||
) -> Vec<Node> {
|
||||
(0..nn)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
|
@ -449,10 +462,10 @@ struct WitnessBuilder<H: Hasher> {
|
|||
witness: Witness,
|
||||
p: usize,
|
||||
inside: bool,
|
||||
_phantom: PhantomData<H>
|
||||
_phantom: PhantomData<H>,
|
||||
}
|
||||
|
||||
impl <H: Hasher> WitnessBuilder<H> {
|
||||
impl<H: Hasher> WitnessBuilder<H> {
|
||||
fn new(tree_builder: &CTreeBuilder<H>, prev_witness: &Witness, count: usize) -> Self {
|
||||
let position = prev_witness.position;
|
||||
// log::info!("Witness::new - {} {},{}", position, tree_builder.start, tree_builder.start + count);
|
||||
|
@ -466,7 +479,7 @@ impl <H: Hasher> WitnessBuilder<H> {
|
|||
}
|
||||
}
|
||||
|
||||
impl <H: Hasher> Builder for WitnessBuilder<H> {
|
||||
impl<H: Hasher> Builder for WitnessBuilder<H> {
|
||||
type Context = CTreeBuilder<H>;
|
||||
type Output = Witness;
|
||||
|
||||
|
@ -514,7 +527,9 @@ impl <H: Hasher> Builder for WitnessBuilder<H> {
|
|||
if tree.right.is_none() {
|
||||
self.witness.filled.push(*p1);
|
||||
}
|
||||
} else if depth as usize > tree.parents.len() || tree.parents[depth as usize - 1].is_none() {
|
||||
} else if depth as usize > tree.parents.len()
|
||||
|| tree.parents[depth as usize - 1].is_none()
|
||||
{
|
||||
self.witness.filled.push(*p1);
|
||||
}
|
||||
}
|
||||
|
@ -577,7 +592,7 @@ pub struct WarpProcessor<H> {
|
|||
hasher: H,
|
||||
}
|
||||
|
||||
impl <H: Hasher> WarpProcessor<H> {
|
||||
impl<H: Hasher> WarpProcessor<H> {
|
||||
pub fn new(hasher: H) -> WarpProcessor<H> {
|
||||
WarpProcessor {
|
||||
prev_tree: CTree::new(),
|
||||
|
@ -599,9 +614,7 @@ impl <H: Hasher> WarpProcessor<H> {
|
|||
return;
|
||||
}
|
||||
self.prev_witnesses.extend_from_slice(new_witnesses);
|
||||
let (t, ws) = self.advance_tree(
|
||||
nodes,
|
||||
);
|
||||
let (t, ws) = self.advance_tree(nodes);
|
||||
self.first_block = false;
|
||||
self.prev_tree = t;
|
||||
self.prev_witnesses = ws;
|
||||
|
@ -616,12 +629,15 @@ impl <H: Hasher> WarpProcessor<H> {
|
|||
}
|
||||
}
|
||||
|
||||
fn advance_tree(
|
||||
&self,
|
||||
mut commitments: &mut [Node],
|
||||
) -> (CTree, Vec<Witness>) {
|
||||
let mut builder = CTreeBuilder::<H>::new(&self.prev_tree, commitments.len(), self.first_block, self.hasher.clone());
|
||||
let mut witness_builders: Vec<_> = self.prev_witnesses
|
||||
fn advance_tree(&self, mut commitments: &mut [Node]) -> (CTree, Vec<Witness>) {
|
||||
let mut builder = CTreeBuilder::<H>::new(
|
||||
&self.prev_tree,
|
||||
commitments.len(),
|
||||
self.first_block,
|
||||
self.hasher.clone(),
|
||||
);
|
||||
let mut witness_builders: Vec<_> = self
|
||||
.prev_witnesses
|
||||
.iter()
|
||||
.map(|witness| WitnessBuilder::new(&builder, witness, commitments.len()))
|
||||
.collect();
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use std::collections::HashMap;
|
||||
use crate::chain::Nf;
|
||||
use crate::db::ReceivedNote;
|
||||
use crate::sync::tree::Node;
|
||||
use crate::{CompactBlock, CompactOrchardAction, CompactSaplingOutput, CompactTx};
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Instant;
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use zcash_note_encryption::batch::try_compact_note_decryption;
|
||||
use zcash_note_encryption::{BatchDomain, COMPACT_NOTE_SIZE, EphemeralKeyBytes, ShieldedOutput};
|
||||
use zcash_note_encryption::{BatchDomain, EphemeralKeyBytes, ShieldedOutput, COMPACT_NOTE_SIZE};
|
||||
use zcash_primitives::consensus::{BlockHeight, Parameters};
|
||||
use crate::{CompactBlock, CompactOrchardAction, CompactSaplingOutput, CompactTx};
|
||||
use crate::db::ReceivedNote;
|
||||
use crate::sync::tree::Node;
|
||||
|
||||
pub struct DecryptedBlock<D: BatchDomain, VK, DN: DecryptedNote<D, VK>> {
|
||||
pub height: u32,
|
||||
|
@ -44,7 +44,13 @@ pub struct OutputPosition {
|
|||
}
|
||||
|
||||
pub trait DecryptedNote<D: BatchDomain, VK>: Send + Sync {
|
||||
fn from_parts(vk: VK, note: D::Note, pa: D::Recipient, output_position: OutputPosition, cmx: Node) -> Self;
|
||||
fn from_parts(
|
||||
vk: VK,
|
||||
note: D::Note,
|
||||
pa: D::Recipient,
|
||||
output_position: OutputPosition,
|
||||
cmx: Node,
|
||||
) -> Self;
|
||||
fn position(&self, block_offset: usize) -> usize;
|
||||
fn cmx(&self) -> Node;
|
||||
fn to_received_note(&self, position: u64) -> ReceivedNote;
|
||||
|
@ -85,9 +91,17 @@ impl From<&CompactSaplingOutput> for CompactOutputBytes {
|
|||
fn from(co: &CompactSaplingOutput) -> Self {
|
||||
CompactOutputBytes {
|
||||
nullifier: [0u8; 32],
|
||||
epk: if co.epk.is_empty() { [0u8; 32] } else { co.epk.clone().try_into().unwrap() } ,
|
||||
epk: if co.epk.is_empty() {
|
||||
[0u8; 32]
|
||||
} else {
|
||||
co.epk.clone().try_into().unwrap()
|
||||
},
|
||||
cmx: co.cmu.clone().try_into().unwrap(), // cannot be filtered out
|
||||
ciphertext: if co.ciphertext.is_empty() { [0u8; 52] } else { co.ciphertext.clone().try_into().unwrap() },
|
||||
ciphertext: if co.ciphertext.is_empty() {
|
||||
[0u8; 52]
|
||||
} else {
|
||||
co.ciphertext.clone().try_into().unwrap()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,18 +110,25 @@ impl From<&CompactOrchardAction> for CompactOutputBytes {
|
|||
fn from(co: &CompactOrchardAction) -> Self {
|
||||
CompactOutputBytes {
|
||||
nullifier: co.nullifier.clone().try_into().unwrap(),
|
||||
epk: if co.ephemeral_key.is_empty() { [0u8; 32] } else { co.ephemeral_key.clone().try_into().unwrap() } ,
|
||||
epk: if co.ephemeral_key.is_empty() {
|
||||
[0u8; 32]
|
||||
} else {
|
||||
co.ephemeral_key.clone().try_into().unwrap()
|
||||
},
|
||||
cmx: co.cmx.clone().try_into().unwrap(), // cannot be filtered out
|
||||
ciphertext: if co.ciphertext.is_empty() { [0u8; 52] } else { co.ciphertext.clone().try_into().unwrap() },
|
||||
ciphertext: if co.ciphertext.is_empty() {
|
||||
[0u8; 52]
|
||||
} else {
|
||||
co.ciphertext.clone().try_into().unwrap()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct CompactShieldedOutput(CompactOutputBytes, OutputPosition);
|
||||
|
||||
impl<D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>> ShieldedOutput<D, COMPACT_NOTE_SIZE>
|
||||
for CompactShieldedOutput
|
||||
for CompactShieldedOutput
|
||||
{
|
||||
fn ephemeral_key(&self) -> EphemeralKeyBytes {
|
||||
EphemeralKeyBytes(self.0.epk)
|
||||
|
@ -120,12 +141,14 @@ for CompactShieldedOutput
|
|||
}
|
||||
}
|
||||
|
||||
pub trait TrialDecrypter<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>, VK: ViewKey<D>, DN: DecryptedNote<D, VK>>: Clone {
|
||||
fn decrypt_notes(
|
||||
&self,
|
||||
block: &CompactBlock,
|
||||
vks: &[VK],
|
||||
) -> DecryptedBlock<D, VK, DN> {
|
||||
pub trait TrialDecrypter<
|
||||
N: Parameters,
|
||||
D: BatchDomain<ExtractedCommitmentBytes = [u8; 32]>,
|
||||
VK: ViewKey<D>,
|
||||
DN: DecryptedNote<D, VK>,
|
||||
>: Clone
|
||||
{
|
||||
fn decrypt_notes(&self, block: &CompactBlock, vks: &[VK]) -> DecryptedBlock<D, VK, DN> {
|
||||
let height = BlockHeight::from_u32(block.height as u32);
|
||||
let mut count_outputs = 0u32;
|
||||
let mut spends: Vec<Nf> = vec![];
|
||||
|
@ -161,33 +184,31 @@ pub trait TrialDecrypter<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes
|
|||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let notes_decrypted =
|
||||
try_compact_note_decryption(&vvks, &outputs);
|
||||
let notes_decrypted = try_compact_note_decryption(&vvks, &outputs);
|
||||
let elapsed = start.elapsed().as_millis() as usize;
|
||||
|
||||
for (pos, opt_note) in notes_decrypted.iter().enumerate() {
|
||||
if let Some(((note, pa), _)) = opt_note {
|
||||
let vk = &vks[pos / outputs.len()];
|
||||
if let Some(((note, pa), vk_index)) = opt_note {
|
||||
let vk = &vks[*vk_index];
|
||||
let account = vk.account();
|
||||
let output = &outputs[pos % outputs.len()];
|
||||
let tx_index = output.1.1.tx_index;
|
||||
let tx_index = output.1 .1.tx_index;
|
||||
let tx_key = (account, tx_index);
|
||||
let tx = txs.entry(tx_key).or_insert_with(||
|
||||
DecryptedTx {
|
||||
account,
|
||||
height: block.height as u32,
|
||||
timestamp: block.time,
|
||||
tx_index,
|
||||
tx_id: block.vtx[tx_index].hash.clone(),
|
||||
notes: vec![],
|
||||
_phantom: PhantomData::default(),
|
||||
let tx = txs.entry(tx_key).or_insert_with(|| DecryptedTx {
|
||||
account,
|
||||
height: block.height as u32,
|
||||
timestamp: block.time,
|
||||
tx_index,
|
||||
tx_id: block.vtx[tx_index].hash.clone(),
|
||||
notes: vec![],
|
||||
_phantom: PhantomData::default(),
|
||||
});
|
||||
tx.notes.push(DN::from_parts(
|
||||
vk.clone(),
|
||||
note.clone(),
|
||||
pa.clone(),
|
||||
output.1.1.clone(),
|
||||
output.1.0.cmx,
|
||||
output.1 .1.clone(),
|
||||
output.1 .0.cmx,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -206,4 +227,3 @@ pub trait TrialDecrypter<N: Parameters, D: BatchDomain<ExtractedCommitmentBytes
|
|||
fn spends(&self, vtx: &CompactTx) -> Vec<Nf>;
|
||||
fn outputs(&self, vtx: &CompactTx) -> Vec<CompactOutputBytes>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use std::collections::HashMap;
|
||||
use crate::contact::{Contact, ContactDecoder};
|
||||
use crate::unified::orchard_as_unified;
|
||||
use crate::{AccountData, CoinConfig, CompactTxStreamerClient, DbAdapter, Hash, TxFilter};
|
||||
use std::convert::TryFrom;
|
||||
use serde::Serialize;
|
||||
use orchard::keys::{FullViewingKey, IncomingViewingKey, OutgoingViewingKey, Scope};
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use orchard::value::ValueCommitment;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use tonic::transport::Channel;
|
||||
use tonic::Request;
|
||||
use zcash_address::{ToAddress, ZcashAddress};
|
||||
|
@ -16,10 +17,11 @@ use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk};
|
|||
use zcash_params::coin::get_branch;
|
||||
use zcash_primitives::consensus::{BlockHeight, Network, Parameters};
|
||||
use zcash_primitives::memo::{Memo, MemoBytes};
|
||||
use zcash_primitives::sapling::note_encryption::{PreparedIncomingViewingKey, try_sapling_note_decryption, try_sapling_output_recovery};
|
||||
use zcash_primitives::sapling::note_encryption::{
|
||||
try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey,
|
||||
};
|
||||
use zcash_primitives::sapling::SaplingIvk;
|
||||
use zcash_primitives::transaction::Transaction;
|
||||
use crate::unified::orchard_as_unified;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ContactRef {
|
||||
|
@ -48,7 +50,15 @@ pub async fn get_transaction_details(coin: u8) -> anyhow::Result<()> {
|
|||
|
||||
let mut details = vec![];
|
||||
for req in reqs.iter() {
|
||||
let tx_details = retrieve_tx_info(network, &mut client, req.height, req.id_tx, &req.txid, &keys[&req.account]).await?;
|
||||
let tx_details = retrieve_tx_info(
|
||||
network,
|
||||
&mut client,
|
||||
req.height,
|
||||
req.id_tx,
|
||||
&req.txid,
|
||||
&keys[&req.account],
|
||||
)
|
||||
.await?;
|
||||
log::info!("{:?}", tx_details);
|
||||
details.push(tx_details);
|
||||
}
|
||||
|
@ -65,7 +75,12 @@ pub async fn get_transaction_details(coin: u8) -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_raw_transaction(network: &Network, client: &mut CompactTxStreamerClient<Channel>, height: u32, txid: &Hash) -> anyhow::Result<Transaction> {
|
||||
async fn fetch_raw_transaction(
|
||||
network: &Network,
|
||||
client: &mut CompactTxStreamerClient<Channel>,
|
||||
height: u32,
|
||||
txid: &Hash,
|
||||
) -> anyhow::Result<Transaction> {
|
||||
let consensus_branch_id = get_branch(network, height);
|
||||
let tx_filter = TxFilter {
|
||||
block: None,
|
||||
|
@ -83,7 +98,7 @@ async fn fetch_raw_transaction(network: &Network, client: &mut CompactTxStreamer
|
|||
#[derive(Clone)]
|
||||
pub struct DecryptionKeys {
|
||||
sapling_keys: (SaplingIvk, zcash_primitives::keys::OutgoingViewingKey),
|
||||
orchard_keys: Option<(IncomingViewingKey, OutgoingViewingKey)>
|
||||
orchard_keys: Option<(IncomingViewingKey, OutgoingViewingKey)>,
|
||||
}
|
||||
|
||||
pub fn decode_transaction(
|
||||
|
@ -122,18 +137,28 @@ pub fn decode_transaction(
|
|||
let mut contact_decoder = ContactDecoder::new(sapling_bundle.shielded_outputs.len());
|
||||
for output in sapling_bundle.shielded_outputs.iter() {
|
||||
let pivk = PreparedIncomingViewingKey::new(&sapling_ivk);
|
||||
if let Some((_note, pa, memo)) = try_sapling_note_decryption(network, height, &pivk, output) {
|
||||
if let Some((_note, pa, memo)) =
|
||||
try_sapling_note_decryption(network, height, &pivk, output)
|
||||
{
|
||||
let memo = Memo::try_from(memo)?;
|
||||
if zaddress.is_none() {
|
||||
zaddress = Some(encode_payment_address(network.hrp_sapling_payment_address(), &pa));
|
||||
zaddress = Some(encode_payment_address(
|
||||
network.hrp_sapling_payment_address(),
|
||||
&pa,
|
||||
));
|
||||
}
|
||||
if memo != Memo::Empty {
|
||||
tx_memo = memo;
|
||||
}
|
||||
}
|
||||
if let Some((_note, pa, memo, ..)) = try_sapling_output_recovery(network, height, &sapling_ovk, output) {
|
||||
if let Some((_note, pa, memo, ..)) =
|
||||
try_sapling_output_recovery(network, height, &sapling_ovk, output)
|
||||
{
|
||||
let _ = contact_decoder.add_memo(&memo); // ignore memo that is not for contacts, if we cannot decode it with ovk, we didn't make create this memo
|
||||
zaddress = Some(encode_payment_address(network.hrp_sapling_payment_address(), &pa));
|
||||
zaddress = Some(encode_payment_address(
|
||||
network.hrp_sapling_payment_address(),
|
||||
&pa,
|
||||
));
|
||||
let memo = Memo::try_from(memo)?;
|
||||
if memo != Memo::Empty {
|
||||
tx_memo = memo;
|
||||
|
@ -147,7 +172,8 @@ pub fn decode_transaction(
|
|||
if let Some((orchard_ivk, orchard_ovk)) = decryption_keys.orchard_keys.clone() {
|
||||
for action in orchard_bundle.actions().iter() {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
if let Some((_note, pa, memo)) = try_note_decryption(&domain, &orchard_ivk, action) {
|
||||
if let Some((_note, pa, memo)) = try_note_decryption(&domain, &orchard_ivk, action)
|
||||
{
|
||||
let memo = Memo::try_from(MemoBytes::from_bytes(&memo)?)?;
|
||||
if oaddress.is_none() {
|
||||
oaddress = Some(orchard_as_unified(network, &pa).encode());
|
||||
|
@ -156,8 +182,13 @@ pub fn decode_transaction(
|
|||
tx_memo = memo;
|
||||
}
|
||||
}
|
||||
if let Some((_note, pa, memo, ..)) = try_output_recovery_with_ovk(&domain, &orchard_ovk, action,
|
||||
action.cv_net(), &action.encrypted_note().out_ciphertext) {
|
||||
if let Some((_note, pa, memo, ..)) = try_output_recovery_with_ovk(
|
||||
&domain,
|
||||
&orchard_ovk,
|
||||
action,
|
||||
action.cv_net(),
|
||||
&action.encrypted_note().out_ciphertext,
|
||||
) {
|
||||
let memo = Memo::try_from(MemoBytes::from_bytes(&memo)?)?;
|
||||
oaddress = Some(orchard_as_unified(network, &pa).encode());
|
||||
if memo != Memo::Empty {
|
||||
|
@ -186,9 +217,15 @@ pub fn decode_transaction(
|
|||
Ok(tx_details)
|
||||
}
|
||||
|
||||
fn get_decryption_keys(network: &Network, account: u32, db: &DbAdapter) -> anyhow::Result<DecryptionKeys> {
|
||||
fn get_decryption_keys(
|
||||
network: &Network,
|
||||
account: u32,
|
||||
db: &DbAdapter,
|
||||
) -> anyhow::Result<DecryptionKeys> {
|
||||
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
||||
let fvk = decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk).unwrap();
|
||||
let fvk =
|
||||
decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk)
|
||||
.unwrap();
|
||||
let (sapling_ivk, sapling_ovk) = (fvk.fvk.vk.ivk(), fvk.fvk.ovk);
|
||||
|
||||
let okey = db.get_orchard(account)?;
|
||||
|
@ -209,7 +246,7 @@ pub async fn retrieve_tx_info(
|
|||
height: u32,
|
||||
id_tx: u32,
|
||||
txid: &Hash,
|
||||
decryption_keys: &DecryptionKeys
|
||||
decryption_keys: &DecryptionKeys,
|
||||
) -> anyhow::Result<TransactionDetails> {
|
||||
let transaction = fetch_raw_transaction(network, client, height, txid).await?;
|
||||
let tx_details = decode_transaction(network, height, id_tx, transaction, &decryption_keys)?;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use crate::{AccountData, DbAdapter};
|
||||
use anyhow::anyhow;
|
||||
use orchard::Address;
|
||||
use orchard::keys::{FullViewingKey, Scope};
|
||||
use zcash_address::{ToAddress, unified, ZcashAddress};
|
||||
use orchard::Address;
|
||||
use zcash_address::unified::{Container, Encoding, Receiver};
|
||||
use zcash_client_backend::encoding::{AddressCodec, decode_payment_address, encode_payment_address};
|
||||
use zcash_address::{unified, ToAddress, ZcashAddress};
|
||||
use zcash_client_backend::encoding::{
|
||||
decode_payment_address, encode_payment_address, AddressCodec,
|
||||
};
|
||||
use zcash_primitives::consensus::{Network, Parameters};
|
||||
use zcash_primitives::legacy::TransparentAddress;
|
||||
use zcash_primitives::sapling::PaymentAddress;
|
||||
use crate::{AccountData, DbAdapter};
|
||||
|
||||
pub struct UnifiedAddressType {
|
||||
pub transparent: bool,
|
||||
|
@ -24,9 +26,13 @@ pub struct DecodedUA {
|
|||
|
||||
impl std::fmt::Display for DecodedUA {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "DecodedUA: {:?} {:?} {:?}",
|
||||
write!(
|
||||
f,
|
||||
"DecodedUA: {:?} {:?} {:?}",
|
||||
self.transparent.as_ref().map(|a| a.encode(&self.network)),
|
||||
self.sapling.as_ref().map(|a| encode_payment_address(self.network.hrp_sapling_payment_address(), a)),
|
||||
self.sapling
|
||||
.as_ref()
|
||||
.map(|a| encode_payment_address(self.network.hrp_sapling_payment_address(), a)),
|
||||
self.orchard.as_ref().map(|a| {
|
||||
let ua = unified::Address(vec![Receiver::Orchard(a.to_raw_address_bytes())]);
|
||||
ua.encode(&self.network.address_network().unwrap())
|
||||
|
@ -35,8 +41,15 @@ impl std::fmt::Display for DecodedUA {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_unified_address(network: &Network, db: &DbAdapter, account: u32, tpe: Option<UnifiedAddressType>) -> anyhow::Result<String> {
|
||||
let tpe = tpe.ok_or(anyhow!("")).or_else(|_| db.get_ua_settings(account))?;
|
||||
pub fn get_unified_address(
|
||||
network: &Network,
|
||||
db: &DbAdapter,
|
||||
account: u32,
|
||||
tpe: Option<UnifiedAddressType>,
|
||||
) -> anyhow::Result<String> {
|
||||
let tpe = tpe
|
||||
.ok_or(anyhow!(""))
|
||||
.or_else(|_| db.get_ua_settings(account))?;
|
||||
|
||||
let mut rcvs = vec![];
|
||||
if tpe.transparent {
|
||||
|
@ -50,7 +63,7 @@ pub fn get_unified_address(network: &Network, db: &DbAdapter, account: u32, tpe:
|
|||
}
|
||||
}
|
||||
if tpe.sapling {
|
||||
let AccountData { address , .. } = db.get_account_info(account)?;
|
||||
let AccountData { address, .. } = db.get_account_info(account)?;
|
||||
let pa = decode_payment_address(network.hrp_sapling_payment_address(), &address).unwrap();
|
||||
let rcv = Receiver::Sapling(pa.to_bytes());
|
||||
rcvs.push(rcv);
|
||||
|
@ -75,7 +88,7 @@ pub fn decode_unified_address(network: &Network, ua: &str) -> anyhow::Result<Dec
|
|||
network: network.clone(),
|
||||
transparent: None,
|
||||
sapling: None,
|
||||
orchard: None
|
||||
orchard: None,
|
||||
};
|
||||
let network = network.address_network().unwrap();
|
||||
let (a_network, ua) = unified::Address::decode(ua)?;
|
||||
|
|
|
@ -7,7 +7,7 @@ curl -X GET http://localhost:8000/balance
|
|||
curl -X GET http://localhost:8000/tx_history
|
||||
curl -X GET http://localhost:8000/new_diversified_address
|
||||
curl -X POST 'http://localhost:8000/sync?offset=0'
|
||||
curl -X POST 'http://localhost:8000/rewind?height=1600000'
|
||||
curl -X POST 'http://localhost:8000/rewind?height=1800000'
|
||||
# Sync to latest height - 20 to calculate the witnesses for later
|
||||
curl -X POST 'http://localhost:8000/sync?offset=20'
|
||||
curl -X POST http://localhost:8000/mark_synced
|
||||
|
@ -16,3 +16,8 @@ curl -X GET http://localhost:8000/parse_payment_uri?uri=zcash%3Azs1hn7qwpjz6p5n2
|
|||
curl -X POST -H 'Content-Type: application/json' -d '{"recipients": [{"address": "zs1hn7qwpjz6p5n24hjhks73y6vn0tpk3c2cfu8wzgtgl4j9ht8ycjgjr47c94scce3uahaje9jkxn", "amount": 100000, "memo": "Hello", "reply_to": false, "subject": "hello", "max_amount_per_note": 0}], "confirmations": 10}' http://localhost:8000/create_offline_tx
|
||||
|
||||
// offline signing and pay need an account with a secret key
|
||||
|
||||
curl -X POST -H 'Content-Type: application/json' -d '{"coin": 0, "name": "test", "key": "bleak regret excuse hold divide novel rain clutch once used another visual forward small tumble artefact jewel bundle kid wolf universe focus weekend melt"}' http://localhost:8000/new_account
|
||||
curl -X POST 'http://localhost:8000/rewind?height=1800000'
|
||||
curl -X POST 'http://localhost:8000/sync?offset=0'
|
||||
curl 'http://localhost:8000/unified_address?t=0&s=0&o=1'
|
||||
|
|
Loading…
Reference in New Issue