Clean up some dead code
This commit is contained in:
parent
b61772b639
commit
f23cd2376f
|
@ -29,51 +29,53 @@ async fn prepare_multi_payment(
|
||||||
use_transparent: bool,
|
use_transparent: bool,
|
||||||
anchor_offset: u32,
|
anchor_offset: u32,
|
||||||
) -> anyhow::Result<(Tx, Vec<u32>)> {
|
) -> anyhow::Result<(Tx, Vec<u32>)> {
|
||||||
let c = CoinConfig::get_active();
|
// let c = CoinConfig::get_active();
|
||||||
let mut tx_builder = TxBuilder::new(c.coin_type, last_height);
|
// let mut tx_builder = TxBuilder::new(c.coin_type, last_height);
|
||||||
|
//
|
||||||
let AccountData { fvk, .. } = c.db()?.get_account_info(c.id_account)?;
|
// let AccountData { fvk, .. } = c.db()?.get_account_info(c.id_account)?;
|
||||||
let fvk = decode_extended_full_viewing_key(
|
// let fvk = decode_extended_full_viewing_key(
|
||||||
c.chain.network().hrp_sapling_extended_full_viewing_key(),
|
// c.chain.network().hrp_sapling_extended_full_viewing_key(),
|
||||||
&fvk,
|
// &fvk,
|
||||||
)
|
// )
|
||||||
.unwrap();
|
// .unwrap();
|
||||||
let utxos = if use_transparent {
|
// let utxos = if use_transparent {
|
||||||
let mut client = c.connect_lwd().await?;
|
// let mut client = c.connect_lwd().await?;
|
||||||
let t_address = c.db()?.get_taddr(c.id_account)?;
|
// let t_address = c.db()?.get_taddr(c.id_account)?;
|
||||||
if let Some(t_address) = t_address {
|
// if let Some(t_address) = t_address {
|
||||||
get_utxos(&mut client, &t_address, c.id_account).await?
|
// get_utxos(&mut client, &t_address, c.id_account).await?
|
||||||
} else {
|
// } else {
|
||||||
vec![]
|
// vec![]
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
vec![]
|
// vec![]
|
||||||
};
|
// };
|
||||||
|
//
|
||||||
let target_amount: u64 = recipients.iter().map(|r| r.amount).sum();
|
// let target_amount: u64 = recipients.iter().map(|r| r.amount).sum();
|
||||||
let anchor_height = last_height.saturating_sub(anchor_offset);
|
// let anchor_height = last_height.saturating_sub(anchor_offset);
|
||||||
let spendable_notes = c
|
// let spendable_notes = c
|
||||||
.db()?
|
// .db()?
|
||||||
.get_spendable_notes(c.id_account, anchor_height, &fvk)?;
|
// .get_spendable_notes(c.id_account, anchor_height, &fvk)?;
|
||||||
let note_ids = tx_builder.select_inputs(&fvk, &spendable_notes, &utxos, target_amount)?;
|
// let note_ids = tx_builder.select_inputs(&fvk, &spendable_notes, &utxos, target_amount)?;
|
||||||
tx_builder.select_outputs(&fvk, recipients)?;
|
// tx_builder.select_outputs(&fvk, recipients)?;
|
||||||
Ok((tx_builder.tx, note_ids))
|
// Ok((tx_builder.tx, note_ids))
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign(tx: &Tx, progress_callback: PaymentProgressCallback) -> anyhow::Result<Vec<u8>> {
|
fn sign(tx: &Tx, progress_callback: PaymentProgressCallback) -> anyhow::Result<Vec<u8>> {
|
||||||
let c = CoinConfig::get_active();
|
// let c = CoinConfig::get_active();
|
||||||
let prover = get_prover();
|
// let prover = get_prover();
|
||||||
let db = c.db()?;
|
// let db = c.db()?;
|
||||||
let AccountData { sk: zsk, .. } = db.get_account_info(c.id_account)?;
|
// let AccountData { sk: zsk, .. } = db.get_account_info(c.id_account)?;
|
||||||
let zsk = zsk.ok_or(anyhow!("Cannot sign without secret key"))?;
|
// let zsk = zsk.ok_or(anyhow!("Cannot sign without secret key"))?;
|
||||||
let tsk = db
|
// let tsk = db
|
||||||
.get_tsk(c.id_account)?
|
// .get_tsk(c.id_account)?
|
||||||
.map(|tsk| SecretKey::from_str(&tsk).unwrap());
|
// .map(|tsk| SecretKey::from_str(&tsk).unwrap());
|
||||||
let extsk =
|
// let extsk =
|
||||||
decode_extended_spending_key(c.chain.network().hrp_sapling_extended_spending_key(), &zsk)
|
// decode_extended_spending_key(c.chain.network().hrp_sapling_extended_spending_key(), &zsk)
|
||||||
.unwrap();
|
// .unwrap();
|
||||||
let raw_tx = tx.sign(tsk, &extsk, prover, progress_callback)?;
|
// let raw_tx = tx.sign(tsk, &extsk, prover, progress_callback)?;
|
||||||
Ok(raw_tx)
|
// Ok(raw_tx)
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a multi payment for offline signing
|
/// Build a multi payment for offline signing
|
||||||
|
|
273
src/db.rs
273
src/db.rs
|
@ -157,8 +157,7 @@ impl DbAdapter {
|
||||||
.prepare("SELECT id_account FROM accounts WHERE ivk = ?1")?;
|
.prepare("SELECT id_account FROM accounts WHERE ivk = ?1")?;
|
||||||
let exists = statement.exists(params![ivk])?;
|
let exists = statement.exists(params![ivk])?;
|
||||||
self.connection.execute(
|
self.connection.execute(
|
||||||
"INSERT INTO accounts(name, seed, aindex, sk, ivk, address) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
"INSERT INTO accounts(name, seed, aindex, sk, ivk, address) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
ON CONFLICT DO NOTHING",
|
|
||||||
params![name, seed, index, sk, ivk, address],
|
params![name, seed, index, sk, ivk, address],
|
||||||
)?;
|
)?;
|
||||||
let id_account: u32 = self
|
let id_account: u32 = self
|
||||||
|
@ -246,24 +245,6 @@ impl DbAdapter {
|
||||||
Ok(fvks)
|
Ok(fvks)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_seeds(&self) -> anyhow::Result<Vec<AccountSeed>> {
|
|
||||||
let mut statement = self
|
|
||||||
.connection
|
|
||||||
.prepare("SELECT id_account, seed FROM accounts WHERE seed IS NOT NULL")?;
|
|
||||||
let rows = statement.query_map([], |row| {
|
|
||||||
let id_account: u32 = row.get(0)?;
|
|
||||||
let seed: String = row.get(1)?;
|
|
||||||
Ok(AccountSeed {
|
|
||||||
id_account,
|
|
||||||
seed})
|
|
||||||
})?;
|
|
||||||
let mut accounts = vec![];
|
|
||||||
for row in rows {
|
|
||||||
accounts.push(row?);
|
|
||||||
}
|
|
||||||
Ok(accounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trim_to_height(&mut self, height: u32) -> anyhow::Result<u32> {
|
pub fn trim_to_height(&mut self, height: u32) -> anyhow::Result<u32> {
|
||||||
// snap height to an existing checkpoint
|
// snap height to an existing checkpoint
|
||||||
let height = self.connection.query_row(
|
let height = self.connection.query_row(
|
||||||
|
@ -307,22 +288,6 @@ impl DbAdapter {
|
||||||
Ok(height)
|
Ok(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_txhash(&self, id_tx: u32) -> anyhow::Result<(u32, u32, u32, Vec<u8>, String)> {
|
|
||||||
let (account, height, timestamp, tx_hash, ivk) = self.connection.query_row(
|
|
||||||
"SELECT account, height, timestamp, txid, ivk FROM transactions t, accounts a WHERE id_tx = ?1 AND t.account = a.id_account",
|
|
||||||
params![id_tx],
|
|
||||||
|row| {
|
|
||||||
let account: u32 = row.get(0)?;
|
|
||||||
let height: u32 = row.get(1)?;
|
|
||||||
let timestamp: u32 = row.get(2)?;
|
|
||||||
let tx_hash: Vec<u8> = row.get(3)?;
|
|
||||||
let ivk: String = row.get(4)?;
|
|
||||||
Ok((account, height, timestamp, tx_hash, ivk))
|
|
||||||
},
|
|
||||||
).map_err(wrap_query_no_rows("get_txhash"))?;
|
|
||||||
Ok((account, height, timestamp, tx_hash, ivk))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn store_block(
|
pub fn store_block(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
@ -341,34 +306,7 @@ impl DbAdapter {
|
||||||
});
|
});
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"INSERT INTO blocks(height, hash, timestamp, sapling_tree, orchard_tree)
|
"INSERT INTO blocks(height, hash, timestamp, sapling_tree, orchard_tree)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
ON CONFLICT DO NOTHING",
|
|
||||||
params![height, hash, timestamp, &sapling_bb, orchard_bb],
|
|
||||||
)?;
|
|
||||||
log::debug!("-block");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn store_block2(
|
|
||||||
height: u32,
|
|
||||||
hash: &[u8],
|
|
||||||
timestamp: u32,
|
|
||||||
sapling_tree: &sync::CTree,
|
|
||||||
orchard_tree: Option<&sync::CTree>,
|
|
||||||
connection: &Connection,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::debug!("+block");
|
|
||||||
let mut sapling_bb: Vec<u8> = vec![];
|
|
||||||
sapling_tree.write(&mut sapling_bb)?;
|
|
||||||
let orchard_bb = orchard_tree.map(|tree| {
|
|
||||||
let mut bb: Vec<u8> = vec![];
|
|
||||||
tree.write(&mut bb).unwrap();
|
|
||||||
bb
|
|
||||||
});
|
|
||||||
connection.execute(
|
|
||||||
"INSERT INTO blocks(height, hash, timestamp, sapling_tree, orchard_tree)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
|
||||||
ON CONFLICT DO NOTHING",
|
|
||||||
params![height, hash, timestamp, &sapling_bb, orchard_bb],
|
params![height, hash, timestamp, &sapling_bb, orchard_bb],
|
||||||
)?;
|
)?;
|
||||||
log::debug!("-block");
|
log::debug!("-block");
|
||||||
|
@ -386,8 +324,7 @@ impl DbAdapter {
|
||||||
log::debug!("+transaction");
|
log::debug!("+transaction");
|
||||||
db_tx.execute(
|
db_tx.execute(
|
||||||
"INSERT INTO transactions(account, txid, height, timestamp, tx_index, value)
|
"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",
|
|
||||||
params![account, txid, height, timestamp, tx_index],
|
params![account, txid, height, timestamp, tx_index],
|
||||||
)?;
|
)?;
|
||||||
let id_tx: u32 = db_tx
|
let id_tx: u32 = db_tx
|
||||||
|
@ -423,25 +360,6 @@ impl DbAdapter {
|
||||||
Ok(id_note)
|
Ok(id_note)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Depends on the type of witness
|
|
||||||
pub fn store_witnesses(
|
|
||||||
connection: &Connection,
|
|
||||||
witness: &Witness,
|
|
||||||
height: u32,
|
|
||||||
id_note: u32,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::debug!("+witnesses");
|
|
||||||
let mut bb: Vec<u8> = vec![];
|
|
||||||
witness.write(&mut bb)?;
|
|
||||||
connection.execute(
|
|
||||||
"INSERT INTO sapling_witnesses(note, height, witness) VALUES (?1, ?2, ?3)
|
|
||||||
ON CONFLICT DO NOTHING",
|
|
||||||
params![id_note, height, bb],
|
|
||||||
)?;
|
|
||||||
log::debug!("-witnesses");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn store_witness(
|
pub fn store_witness(
|
||||||
witness: &sync::Witness,
|
witness: &sync::Witness,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
@ -453,8 +371,7 @@ impl DbAdapter {
|
||||||
let mut bb: Vec<u8> = vec![];
|
let mut bb: Vec<u8> = vec![];
|
||||||
witness.write(&mut bb)?;
|
witness.write(&mut bb)?;
|
||||||
connection.execute(
|
connection.execute(
|
||||||
&format!("INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)
|
&format!("INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)", shielded_pool),
|
||||||
ON CONFLICT DO NOTHING", shielded_pool),
|
|
||||||
params![id_note, height, bb],
|
params![id_note, height, bb],
|
||||||
)?;
|
)?;
|
||||||
log::debug!("-store_witness");
|
log::debug!("-store_witness");
|
||||||
|
@ -473,8 +390,8 @@ impl DbAdapter {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_transaction_with_memo(&self, id_tx: u32, details: &TransactionDetails) -> anyhow::Result<()> {
|
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, 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -486,21 +403,7 @@ impl DbAdapter {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_received_note_value(nf: &Nf, db_tx: &Transaction) -> anyhow::Result<(u32, i64)> {
|
#[allow(dead_code)]
|
||||||
let (account, value) = db_tx
|
|
||||||
.query_row(
|
|
||||||
"SELECT account, value FROM received_notes WHERE nf = ?1",
|
|
||||||
params![nf.0.to_vec()],
|
|
||||||
|row| {
|
|
||||||
let account: u32 = row.get(0)?;
|
|
||||||
let value: i64 = row.get(1)?;
|
|
||||||
Ok((account, value))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(wrap_query_no_rows("get_received_note_value"))?;
|
|
||||||
Ok((account, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_balance(&self, account: u32) -> anyhow::Result<u64> {
|
pub fn get_balance(&self, account: u32) -> anyhow::Result<u64> {
|
||||||
let balance: Option<i64> = self.connection.query_row(
|
let balance: Option<i64> = self.connection.query_row(
|
||||||
"SELECT SUM(value) FROM received_notes WHERE (spent IS NULL OR spent = 0) AND account = ?1",
|
"SELECT SUM(value) FROM received_notes WHERE (spent IS NULL OR spent = 0) AND account = ?1",
|
||||||
|
@ -575,28 +478,6 @@ impl DbAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nullifiers(&self) -> anyhow::Result<HashMap<Nf, NfRef>> {
|
|
||||||
let mut statement = self.connection.prepare(
|
|
||||||
"SELECT id_note, account, nf FROM received_notes WHERE spent IS NULL OR spent = 0",
|
|
||||||
)?;
|
|
||||||
let nfs_res = statement.query_map([], |row| {
|
|
||||||
let id_note: u32 = row.get(0)?;
|
|
||||||
let account: u32 = row.get(1)?;
|
|
||||||
let nf_vec: Vec<u8> = row.get(2)?;
|
|
||||||
let mut nf = [0u8; 32];
|
|
||||||
nf.clone_from_slice(&nf_vec);
|
|
||||||
let nf_ref = NfRef { id_note, account };
|
|
||||||
Ok((nf_ref, nf))
|
|
||||||
})?;
|
|
||||||
let mut nfs: HashMap<Nf, NfRef> = HashMap::new();
|
|
||||||
for n in nfs_res {
|
|
||||||
let n = n?;
|
|
||||||
nfs.insert(Nf(n.1), n.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(nfs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_nullifier_amounts(
|
pub fn get_nullifier_amounts(
|
||||||
&self,
|
&self,
|
||||||
account: u32,
|
account: u32,
|
||||||
|
@ -648,69 +529,6 @@ impl DbAdapter {
|
||||||
Ok(nfs)
|
Ok(nfs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nullifiers_raw(&self) -> anyhow::Result<Vec<(u32, u64, Vec<u8>)>> {
|
|
||||||
let mut statement = self
|
|
||||||
.connection
|
|
||||||
.prepare("SELECT account, value, nf FROM received_notes")?;
|
|
||||||
let res = statement.query_map([], |row| {
|
|
||||||
let account: u32 = row.get(0)?;
|
|
||||||
let amount: i64 = row.get(1)?;
|
|
||||||
let nf: Vec<u8> = row.get(2)?;
|
|
||||||
Ok((account, amount as u64, nf))
|
|
||||||
})?;
|
|
||||||
let mut v: Vec<(u32, u64, Vec<u8>)> = vec![];
|
|
||||||
for r in res {
|
|
||||||
v.push(r?);
|
|
||||||
}
|
|
||||||
Ok(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Depends on the type of witness - Should it returned any spendable note? sapling or orchard
|
|
||||||
pub fn get_spendable_notes(
|
|
||||||
&self,
|
|
||||||
account: u32,
|
|
||||||
anchor_height: u32,
|
|
||||||
fvk: &ExtendedFullViewingKey,
|
|
||||||
) -> anyhow::Result<Vec<SpendableNote>> {
|
|
||||||
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 (r.excluded IS NULL OR NOT r.excluded) AND w.height = (
|
|
||||||
SELECT MAX(height) FROM sapling_witnesses WHERE height <= ?1
|
|
||||||
) AND r.id_note = w.note")?;
|
|
||||||
let notes = statement.query_map(params![anchor_height, account], |row| {
|
|
||||||
let id_note: u32 = row.get(0)?;
|
|
||||||
|
|
||||||
let diversifier: Vec<u8> = row.get(1)?;
|
|
||||||
let value: i64 = row.get(2)?;
|
|
||||||
let rcm: Vec<u8> = row.get(3)?;
|
|
||||||
let witness: Vec<u8> = row.get(4)?;
|
|
||||||
|
|
||||||
let mut diversifer_bytes = [0u8; 11];
|
|
||||||
diversifer_bytes.copy_from_slice(&diversifier);
|
|
||||||
let diversifier = Diversifier(diversifer_bytes);
|
|
||||||
let mut rcm_bytes = [0u8; 32];
|
|
||||||
rcm_bytes.copy_from_slice(&rcm);
|
|
||||||
let rcm = jubjub::Fr::from_bytes(&rcm_bytes).unwrap();
|
|
||||||
let rseed = Rseed::BeforeZip212(rcm);
|
|
||||||
let witness = IncrementalWitness::<Node>::read(&*witness).unwrap();
|
|
||||||
|
|
||||||
let pa = fvk.fvk.vk.to_payment_address(diversifier).unwrap();
|
|
||||||
let note = pa.create_note(value as u64, rseed).unwrap();
|
|
||||||
Ok(SpendableNote {
|
|
||||||
id: id_note,
|
|
||||||
note,
|
|
||||||
diversifier,
|
|
||||||
witness,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
let mut spendable_notes: Vec<SpendableNote> = vec![];
|
|
||||||
for n in notes {
|
|
||||||
spendable_notes.push(n?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(spendable_notes)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 notes = vec![];
|
||||||
let mut statement = self.connection.prepare(
|
let mut statement = self.connection.prepare(
|
||||||
|
@ -855,7 +673,6 @@ impl DbAdapter {
|
||||||
Ok(contacts)
|
Ok(contacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Orchard diversifiers have a different space
|
|
||||||
pub fn get_diversifier(&self, account: u32) -> anyhow::Result<DiversifierIndex> {
|
pub fn get_diversifier(&self, account: u32) -> anyhow::Result<DiversifierIndex> {
|
||||||
let diversifier_index = self
|
let diversifier_index = self
|
||||||
.connection
|
.connection
|
||||||
|
@ -874,7 +691,6 @@ impl DbAdapter {
|
||||||
Ok(DiversifierIndex(diversifier_index))
|
Ok(DiversifierIndex(diversifier_index))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: See get_diversifier
|
|
||||||
pub fn store_diversifier(
|
pub fn store_diversifier(
|
||||||
&self,
|
&self,
|
||||||
account: u32,
|
account: u32,
|
||||||
|
@ -952,7 +768,7 @@ impl DbAdapter {
|
||||||
let bip44_path = format!("m/44'/{}'/0'/0/{}", self.network().coin_type(), aindex);
|
let bip44_path = format!("m/44'/{}'/0'/0/{}", self.network().coin_type(), aindex);
|
||||||
let (sk, address) = derive_tkeys(self.network(), &seed, &bip44_path)?;
|
let (sk, address) = derive_tkeys(self.network(), &seed, &bip44_path)?;
|
||||||
self.connection.execute(
|
self.connection.execute(
|
||||||
"INSERT INTO taddrs(account, sk, address) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING",
|
"INSERT INTO taddrs(account, sk, address) VALUES (?1, ?2, ?3)",
|
||||||
params![account, &sk, &address],
|
params![account, &sk, &address],
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
@ -964,7 +780,7 @@ impl DbAdapter {
|
||||||
if let Some(seed) = seed {
|
if let Some(seed) = seed {
|
||||||
let keys = derive_orchard_keys(self.network().coin_type(), &seed, aindex);
|
let keys = derive_orchard_keys(self.network().coin_type(), &seed, aindex);
|
||||||
self.connection.execute(
|
self.connection.execute(
|
||||||
"INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING",
|
"INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1, ?2, ?3)",
|
||||||
params![account, &keys.sk, &keys.fvk],
|
params![account, &keys.sk, &keys.fvk],
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
@ -985,7 +801,7 @@ impl DbAdapter {
|
||||||
|
|
||||||
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(
|
self.connection.execute(
|
||||||
"INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1, ?2, ?3, ?4) ON CONFLICT DO NOTHING",
|
"INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1, ?2, ?3, ?4)",
|
||||||
params![account, transparent, sapling, orchard],
|
params![account, transparent, sapling, orchard],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1035,37 +851,6 @@ impl DbAdapter {
|
||||||
Ok(quote)
|
Ok(quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_share_secret(
|
|
||||||
&self,
|
|
||||||
account: u32,
|
|
||||||
secret: &str,
|
|
||||||
index: usize,
|
|
||||||
threshold: usize,
|
|
||||||
participants: usize,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
self.connection.execute(
|
|
||||||
"INSERT INTO secret_shares(account, secret, idx, threshold, participants) VALUES (?1, ?2, ?3, ?4, ?5) \
|
|
||||||
ON CONFLICT (account) DO UPDATE SET secret = excluded.secret, threshold = excluded.threshold, participants = excluded.participants",
|
|
||||||
params![account, &secret, index as u32, threshold as u32, participants as u32],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_share_secret(&self, account: u32) -> anyhow::Result<String> {
|
|
||||||
let secret = self
|
|
||||||
.connection
|
|
||||||
.query_row(
|
|
||||||
"SELECT secret FROM secret_shares WHERE account = ?1",
|
|
||||||
params![account],
|
|
||||||
|row| {
|
|
||||||
let secret: String = row.get(0)?;
|
|
||||||
Ok(secret)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.optional()?;
|
|
||||||
Ok(secret.unwrap_or("".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn truncate_data(&self) -> anyhow::Result<()> {
|
pub fn truncate_data(&self) -> anyhow::Result<()> {
|
||||||
self.truncate_sync_data()?;
|
self.truncate_sync_data()?;
|
||||||
self.connection.execute("DELETE FROM diversifiers", [])?;
|
self.connection.execute("DELETE FROM diversifiers", [])?;
|
||||||
|
@ -1442,44 +1227,6 @@ mod tests {
|
||||||
use zcash_params::coin::CoinType;
|
use zcash_params::coin::CoinType;
|
||||||
use crate::sync::{CTree, Witness};
|
use crate::sync::{CTree, Witness};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_db() {
|
|
||||||
let mut db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
|
|
||||||
db.init_db().unwrap();
|
|
||||||
db.trim_to_height(0).unwrap();
|
|
||||||
|
|
||||||
let db_tx = db.begin_transaction().unwrap();
|
|
||||||
DbAdapter::store_block(&db_tx, 1, &[0u8; 32], 0, &CTree::new(), None).unwrap();
|
|
||||||
let id_tx = DbAdapter::store_transaction(&[0; 32], 1, 1, 0, 20, &db_tx).unwrap();
|
|
||||||
DbAdapter::store_received_note(
|
|
||||||
&ReceivedNote {
|
|
||||||
account: 1,
|
|
||||||
height: 1,
|
|
||||||
output_index: 0,
|
|
||||||
diversifier: vec![],
|
|
||||||
value: 0,
|
|
||||||
rcm: vec![],
|
|
||||||
nf: vec![],
|
|
||||||
rho: None,
|
|
||||||
spent: None,
|
|
||||||
},
|
|
||||||
id_tx,
|
|
||||||
5,
|
|
||||||
&db_tx,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let witness = Witness {
|
|
||||||
position: 10,
|
|
||||||
id_note: 0,
|
|
||||||
tree: CTree::new(),
|
|
||||||
filled: vec![],
|
|
||||||
cursor: CTree::new(),
|
|
||||||
cmx: [0u8; 32]
|
|
||||||
};
|
|
||||||
DbAdapter::store_witnesses(&db_tx, &witness, 1000, 1).unwrap();
|
|
||||||
db_tx.commit().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_balance() {
|
fn test_balance() {
|
||||||
let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
|
let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH).unwrap();
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{BufReader, BufWriter, Read, Write};
|
|
||||||
use byteorder::{LE, ReadBytesExt, WriteBytesExt};
|
|
||||||
use tonic::Request;
|
|
||||||
use prost::Message;
|
|
||||||
use zcash_client_backend::encoding::{decode_extended_full_viewing_key, decode_extended_spending_key, encode_extended_full_viewing_key, encode_payment_address};
|
|
||||||
use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters};
|
|
||||||
use zcash_primitives::sapling::note_encryption::SaplingDomain;
|
|
||||||
use zcash_primitives::zip32::ExtendedFullViewingKey;
|
|
||||||
use warp_api_ffi::{BlockId, BlockRange, ChainSpec, COIN_CONFIG, CoinConfig, CompactBlock, connect_lightwalletd, DbAdapter, DbAdapterBuilder, derive_zip32, init_coin};
|
|
||||||
use warp_api_ffi::sapling::{DecryptedSaplingNote, SaplingDecrypter, SaplingHasher, SaplingViewKey};
|
|
||||||
use warp_api_ffi::sync::{WarpProcessor, Synchronizer, CTree};
|
|
||||||
|
|
||||||
type SaplingSynchronizer = Synchronizer<Network, SaplingDomain<Network>, SaplingViewKey, DecryptedSaplingNote,
|
|
||||||
SaplingDecrypter<Network>, SaplingHasher>;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
async fn write_block_file() {
|
|
||||||
init_coin(1, "yec-new.db").unwrap();
|
|
||||||
let coin = COIN_CONFIG[1].lock().unwrap();
|
|
||||||
let mut client = connect_lightwalletd("https://lite.ycash.xyz:9067").await.unwrap();
|
|
||||||
let network = coin.chain.network();
|
|
||||||
let start = u32::from(network.activation_height(NetworkUpgrade::Sapling).unwrap()) + 1;
|
|
||||||
let end = client.get_latest_block(Request::new(ChainSpec {})).await.unwrap().into_inner();
|
|
||||||
let end = end.height as u32;
|
|
||||||
|
|
||||||
let mut blocks = client.get_block_range(Request::new(BlockRange {
|
|
||||||
start: Some(BlockId { height: start as u64, hash: vec![] }),
|
|
||||||
end: Some(BlockId { height: end as u64, hash: vec![] }),
|
|
||||||
spam_filter_threshold: 0
|
|
||||||
})).await.unwrap().into_inner();
|
|
||||||
|
|
||||||
let file = File::create("ycash.bin").unwrap();
|
|
||||||
let mut writer = BufWriter::new(file);
|
|
||||||
while let Some(block) = blocks.message().await.unwrap() {
|
|
||||||
println!("{}", block.height);
|
|
||||||
let mut buf = prost::bytes::BytesMut::new();
|
|
||||||
block.encode(&mut buf).unwrap();
|
|
||||||
writer.write_u32::<LE>(buf.len() as u32).unwrap();
|
|
||||||
writer.write_all(&buf).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_block_file(coin: &CoinConfig, fvk: ExtendedFullViewingKey) {
|
|
||||||
let network = coin.chain.network();
|
|
||||||
let file = File::open("/home/hanh/ycash.bin").unwrap();
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let db_builder = DbAdapterBuilder { coin_type: coin.coin_type, db_path: coin.db_path.as_ref().unwrap().to_owned() };
|
|
||||||
let mut synchronizer = SaplingSynchronizer {
|
|
||||||
decrypter: SaplingDecrypter::new(*network),
|
|
||||||
warper: WarpProcessor::new(SaplingHasher::default()),
|
|
||||||
vks: vec![SaplingViewKey {
|
|
||||||
account: 1,
|
|
||||||
fvk: fvk.clone(),
|
|
||||||
ivk: fvk.fvk.vk.ivk()
|
|
||||||
}],
|
|
||||||
tree: CTree::new(),
|
|
||||||
witnesses: vec![],
|
|
||||||
|
|
||||||
db: db_builder.clone(),
|
|
||||||
shielded_pool: "sapling".to_string(),
|
|
||||||
|
|
||||||
note_position: 0,
|
|
||||||
nullifiers: HashMap::new(),
|
|
||||||
_phantom: Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
synchronizer.initialize().unwrap();
|
|
||||||
|
|
||||||
let mut blocks = vec![];
|
|
||||||
let mut height = 0;
|
|
||||||
let mut hash = [0u8; 32];
|
|
||||||
let mut time = 0;
|
|
||||||
while let Ok(len) = reader.read_u32::<LE>() {
|
|
||||||
let mut buf = vec![0u8; len as usize];
|
|
||||||
reader.read_exact(&mut buf).unwrap();
|
|
||||||
let cb: CompactBlock = CompactBlock::decode(&*buf).unwrap();
|
|
||||||
height = cb.height;
|
|
||||||
hash.copy_from_slice(&cb.hash);
|
|
||||||
time = cb.time;
|
|
||||||
blocks.push(cb);
|
|
||||||
if height % 100_000 == 0 {
|
|
||||||
synchronizer.process(blocks).unwrap();
|
|
||||||
blocks = vec![];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
synchronizer.process(blocks).unwrap();
|
|
||||||
let db = db_builder.build().unwrap();
|
|
||||||
DbAdapter::store_block2(height as u32, &hash, time, &synchronizer.tree, None, &db.connection).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
env_logger::init();
|
|
||||||
init_coin(1, "yec-new.db").unwrap();
|
|
||||||
let coin = COIN_CONFIG[1].lock().unwrap();
|
|
||||||
let network = coin.chain.network();
|
|
||||||
let _ = dotenv::dotenv();
|
|
||||||
let seed_str = dotenv::var("SEED").unwrap();
|
|
||||||
let kp = derive_zip32(&network, &seed_str, 0, 0, None).unwrap();
|
|
||||||
let zk = kp.z_key.clone();
|
|
||||||
let sk = decode_extended_spending_key(network.hrp_sapling_extended_spending_key(), &zk).unwrap().unwrap();
|
|
||||||
|
|
||||||
let fvk = ExtendedFullViewingKey::from(&sk);
|
|
||||||
let fvk_str = encode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk);
|
|
||||||
let (_, pa) = fvk.default_address();
|
|
||||||
let address = encode_payment_address(network.hrp_sapling_payment_address(), &pa);
|
|
||||||
let db_builder = DbAdapterBuilder { coin_type: coin.coin_type, db_path: coin.db_path.as_ref().unwrap().to_owned() };
|
|
||||||
let db = db_builder.build().unwrap();
|
|
||||||
db.store_account("test", Some(&seed_str), 0, Some(&zk), &fvk_str, &address).unwrap();
|
|
||||||
|
|
||||||
// write_block_file().await;
|
|
||||||
read_block_file(&coin, fvk);
|
|
||||||
}
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use crate::contact::{Contact, ContactDecoder};
|
use crate::contact::{Contact, ContactDecoder};
|
||||||
use crate::{AccountData, CoinConfig, CompactTxStreamerClient, DbAdapter, Hash, TxFilter};
|
use crate::{AccountData, CoinConfig, CompactTxStreamerClient, DbAdapter, Hash, TxFilter};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use orchard::keys::{FullViewingKey, Scope};
|
use orchard::keys::{FullViewingKey, IncomingViewingKey, OutgoingViewingKey, Scope};
|
||||||
use orchard::note_encryption::OrchardDomain;
|
use orchard::note_encryption::OrchardDomain;
|
||||||
use orchard::value::ValueCommitment;
|
use orchard::value::ValueCommitment;
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
|
@ -16,6 +17,7 @@ use zcash_params::coin::get_branch;
|
||||||
use zcash_primitives::consensus::{BlockHeight, Network, Parameters};
|
use zcash_primitives::consensus::{BlockHeight, Network, Parameters};
|
||||||
use zcash_primitives::memo::{Memo, MemoBytes};
|
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::{PreparedIncomingViewingKey, try_sapling_note_decryption, try_sapling_output_recovery};
|
||||||
|
use zcash_primitives::sapling::SaplingIvk;
|
||||||
use zcash_primitives::transaction::Transaction;
|
use zcash_primitives::transaction::Transaction;
|
||||||
use crate::unified::orchard_as_unified;
|
use crate::unified::orchard_as_unified;
|
||||||
|
|
||||||
|
@ -29,18 +31,37 @@ pub struct ContactRef {
|
||||||
pub async fn get_transaction_details(coin: u8) -> anyhow::Result<()> {
|
pub async fn get_transaction_details(coin: u8) -> anyhow::Result<()> {
|
||||||
let c = CoinConfig::get(coin);
|
let c = CoinConfig::get(coin);
|
||||||
let network = c.chain.network();
|
let network = c.chain.network();
|
||||||
|
let mut client = c.connect_lwd().await?;
|
||||||
|
let mut keys = HashMap::new();
|
||||||
|
|
||||||
|
let reqs = {
|
||||||
|
let db = c.db.as_ref().unwrap();
|
||||||
|
let db = db.lock().unwrap();
|
||||||
|
let reqs = db.get_txid_without_memo()?;
|
||||||
|
for req in reqs.iter() {
|
||||||
|
let decryption_keys = get_decryption_keys(network, req.account, &db)?;
|
||||||
|
keys.insert(req.account, decryption_keys);
|
||||||
|
}
|
||||||
|
reqs
|
||||||
|
// Make sure we don't hold a mutex across await
|
||||||
|
};
|
||||||
|
|
||||||
|
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?;
|
||||||
|
log::info!("{:?}", tx_details);
|
||||||
|
details.push(tx_details);
|
||||||
|
}
|
||||||
|
|
||||||
let db = c.db.as_ref().unwrap();
|
let db = c.db.as_ref().unwrap();
|
||||||
let db = db.lock().unwrap();
|
let db = db.lock().unwrap();
|
||||||
let mut client = c.connect_lwd().await?;
|
for tx_details in details.iter() {
|
||||||
let reqs = db.get_txid_without_memo()?;
|
db.update_transaction_with_memo(tx_details)?;
|
||||||
for req in reqs {
|
|
||||||
let tx_details = retrieve_tx_info(network, &mut client, &db, req.account, req.height, &req.txid).await?;
|
|
||||||
log::info!("{:?}", tx_details);
|
|
||||||
db.update_transaction_with_memo(req.id_tx, &tx_details)?;
|
|
||||||
for c in tx_details.contacts.iter() {
|
for c in tx_details.contacts.iter() {
|
||||||
db.store_contact(c, false)?;
|
db.store_contact(c, false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,23 +80,20 @@ async fn fetch_raw_transaction(network: &Network, client: &mut CompactTxStreamer
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DecryptionKeys {
|
||||||
|
sapling_keys: (SaplingIvk, zcash_primitives::keys::OutgoingViewingKey),
|
||||||
|
orchard_keys: Option<(IncomingViewingKey, OutgoingViewingKey)>
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decode_transaction(
|
pub fn decode_transaction(
|
||||||
network: &Network,
|
network: &Network,
|
||||||
account: u32,
|
|
||||||
height: u32,
|
height: u32,
|
||||||
|
id_tx: u32,
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
db: &DbAdapter,
|
decryption_keys: &DecryptionKeys,
|
||||||
) -> anyhow::Result<TransactionDetails> {
|
) -> anyhow::Result<TransactionDetails> {
|
||||||
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
let (sapling_ivk, sapling_ovk) = decryption_keys.sapling_keys.clone();
|
||||||
let fvk = decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk).unwrap();
|
|
||||||
let sapling_ivk = fvk.fvk.vk.ivk();
|
|
||||||
let sapling_ovk = fvk.fvk.ovk;
|
|
||||||
|
|
||||||
let okey = db.get_orchard(account)?;
|
|
||||||
let okey = okey.map(|okey| {
|
|
||||||
let fvk = FullViewingKey::from_bytes(&okey.fvk).unwrap();
|
|
||||||
(fvk.to_ivk(Scope::External), fvk.to_ovk(Scope::External))
|
|
||||||
});
|
|
||||||
|
|
||||||
let height = BlockHeight::from_u32(height);
|
let height = BlockHeight::from_u32(height);
|
||||||
let mut taddress: Option<String> = None;
|
let mut taddress: Option<String> = None;
|
||||||
|
@ -126,7 +144,7 @@ pub fn decode_transaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(orchard_bundle) = tx.orchard_bundle() {
|
if let Some(orchard_bundle) = tx.orchard_bundle() {
|
||||||
if let Some((orchard_ivk, orchard_ovk)) = okey {
|
if let Some((orchard_ivk, orchard_ovk)) = decryption_keys.orchard_keys.clone() {
|
||||||
for action in orchard_bundle.actions().iter() {
|
for action in orchard_bundle.actions().iter() {
|
||||||
let domain = OrchardDomain::for_action(action);
|
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) {
|
||||||
|
@ -159,6 +177,7 @@ pub fn decode_transaction(
|
||||||
Memo::Arbitrary(_) => "Unrecognized".to_string(),
|
Memo::Arbitrary(_) => "Unrecognized".to_string(),
|
||||||
};
|
};
|
||||||
let tx_details = TransactionDetails {
|
let tx_details = TransactionDetails {
|
||||||
|
id_tx,
|
||||||
address,
|
address,
|
||||||
memo,
|
memo,
|
||||||
contacts,
|
contacts,
|
||||||
|
@ -167,16 +186,33 @@ pub fn decode_transaction(
|
||||||
Ok(tx_details)
|
Ok(tx_details)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (sapling_ivk, sapling_ovk) = (fvk.fvk.vk.ivk(), fvk.fvk.ovk);
|
||||||
|
|
||||||
|
let okey = db.get_orchard(account)?;
|
||||||
|
let okey = okey.map(|okey| {
|
||||||
|
let fvk = FullViewingKey::from_bytes(&okey.fvk).unwrap();
|
||||||
|
(fvk.to_ivk(Scope::External), fvk.to_ovk(Scope::External))
|
||||||
|
});
|
||||||
|
let decryption_keys = DecryptionKeys {
|
||||||
|
sapling_keys: (sapling_ivk, sapling_ovk),
|
||||||
|
orchard_keys: okey,
|
||||||
|
};
|
||||||
|
Ok(decryption_keys)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn retrieve_tx_info(
|
pub async fn retrieve_tx_info(
|
||||||
network: &Network,
|
network: &Network,
|
||||||
client: &mut CompactTxStreamerClient<Channel>,
|
client: &mut CompactTxStreamerClient<Channel>,
|
||||||
db: &DbAdapter,
|
|
||||||
account: u32,
|
|
||||||
height: u32,
|
height: u32,
|
||||||
|
id_tx: u32,
|
||||||
txid: &Hash,
|
txid: &Hash,
|
||||||
|
decryption_keys: &DecryptionKeys
|
||||||
) -> anyhow::Result<TransactionDetails> {
|
) -> anyhow::Result<TransactionDetails> {
|
||||||
let transaction = fetch_raw_transaction(network, client, height, txid).await?;
|
let transaction = fetch_raw_transaction(network, client, height, txid).await?;
|
||||||
let tx_details = decode_transaction(network, account, height, transaction, db)?;
|
let tx_details = decode_transaction(network, height, id_tx, transaction, &decryption_keys)?;
|
||||||
|
|
||||||
Ok(tx_details)
|
Ok(tx_details)
|
||||||
}
|
}
|
||||||
|
@ -190,6 +226,7 @@ pub struct GetTransactionDetailRequest {
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct TransactionDetails {
|
pub struct TransactionDetails {
|
||||||
|
pub id_tx: u32,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub memo: String,
|
pub memo: String,
|
||||||
pub contacts: Vec<Contact>,
|
pub contacts: Vec<Contact>,
|
||||||
|
|
Loading…
Reference in New Issue