zcash_client_sqlite: Fix balance in expired unmined transaction history.
Fixes #1292 Fixes #1299
This commit is contained in:
parent
9c82b87d1a
commit
9c9bd40549
|
@ -1015,9 +1015,6 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update now-expired transactions that didn't get mined.
|
|
||||||
wallet::update_expired_notes(wdb.conn.0, last_scanned_height)?;
|
|
||||||
|
|
||||||
wallet::scanning::scan_complete(
|
wallet::scanning::scan_complete(
|
||||||
wdb.conn.0,
|
wdb.conn.0,
|
||||||
&wdb.params,
|
&wdb.params,
|
||||||
|
|
|
@ -730,7 +730,7 @@ pub(crate) fn spend_fails_on_locked_notes<T: ShieldedPoolTester>() {
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
st.scan_cached_blocks(h1 + 1, 41);
|
st.scan_cached_blocks(h1 + 1, 40);
|
||||||
|
|
||||||
// Second proposal still fails
|
// Second proposal still fails
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
|
@ -1927,20 +1927,20 @@ pub(crate) fn data_db_truncation<T: ShieldedPoolTester>() {
|
||||||
// Scan the cache
|
// Scan the cache
|
||||||
st.scan_cached_blocks(h, 2);
|
st.scan_cached_blocks(h, 2);
|
||||||
|
|
||||||
// Account balance should reflect both received notes
|
// Spendable balance should reflect both received notes
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.get_total_balance(account.account_id()),
|
st.get_spendable_balance(account.account_id(), 1),
|
||||||
(value + value2).unwrap()
|
(value + value2).unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
// "Rewind" to height of last scanned block
|
// "Rewind" to height of last scanned block (this is a no-op)
|
||||||
st.wallet_mut()
|
st.wallet_mut()
|
||||||
.transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h + 1))
|
.transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h + 1))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Account balance should be unaltered
|
// Spendable balance should be unaltered
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.get_total_balance(account.account_id()),
|
st.get_spendable_balance(account.account_id(), 1),
|
||||||
(value + value2).unwrap()
|
(value + value2).unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1949,15 +1949,20 @@ pub(crate) fn data_db_truncation<T: ShieldedPoolTester>() {
|
||||||
.transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h))
|
.transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Account balance should only contain the first received note
|
// Spendable balance should only contain the first received note;
|
||||||
assert_eq!(st.get_total_balance(account.account_id()), value);
|
// the rest should be pending.
|
||||||
|
assert_eq!(st.get_spendable_balance(account.account_id(), 1), value);
|
||||||
|
assert_eq!(
|
||||||
|
st.get_pending_shielded_balance(account.account_id(), 1),
|
||||||
|
value2
|
||||||
|
);
|
||||||
|
|
||||||
// Scan the cache again
|
// Scan the cache again
|
||||||
st.scan_cached_blocks(h, 2);
|
st.scan_cached_blocks(h, 2);
|
||||||
|
|
||||||
// Account balance should again reflect both received notes
|
// Account balance should again reflect both received notes
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.get_total_balance(account.account_id()),
|
st.get_spendable_balance(account.account_id(), 1),
|
||||||
(value + value2).unwrap()
|
(value + value2).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1128,12 +1128,20 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
LEFT OUTER JOIN v_{table_prefix}_shards_scan_state scan_state
|
LEFT OUTER JOIN v_{table_prefix}_shards_scan_state scan_state
|
||||||
ON n.commitment_tree_position >= scan_state.start_position
|
ON n.commitment_tree_position >= scan_state.start_position
|
||||||
AND n.commitment_tree_position < scan_state.end_position_exclusive
|
AND n.commitment_tree_position < scan_state.end_position_exclusive
|
||||||
WHERE n.spent IS NULL
|
WHERE (
|
||||||
AND (
|
t.block IS NOT NULL -- the receiving tx is mined
|
||||||
t.expiry_height IS NULL
|
OR t.expiry_height IS NULL -- the receiving tx will not expire
|
||||||
OR t.block IS NOT NULL
|
OR t.expiry_height >= :summary_height -- the receiving tx is unexpired
|
||||||
OR t.expiry_height >= :summary_height
|
)
|
||||||
)",
|
-- and the received note is unspent
|
||||||
|
AND n.id NOT IN (
|
||||||
|
SELECT {table_prefix}_received_note_id
|
||||||
|
FROM {table_prefix}_received_note_spends
|
||||||
|
JOIN transactions t ON t.id_tx = transaction_id
|
||||||
|
WHERE t.block IS NOT NULL -- the spending transaction is mined
|
||||||
|
OR t.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
OR t.expiry_height > :summary_height -- the spending tx is unexpired
|
||||||
|
)"
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
let mut rows =
|
let mut rows =
|
||||||
|
@ -1245,10 +1253,17 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
let mut stmt_transparent_balances = tx.prepare(
|
let mut stmt_transparent_balances = tx.prepare(
|
||||||
"SELECT u.received_by_account_id, SUM(u.value_zat)
|
"SELECT u.received_by_account_id, SUM(u.value_zat)
|
||||||
FROM utxos u
|
FROM utxos u
|
||||||
LEFT OUTER JOIN transactions tx
|
|
||||||
ON tx.id_tx = u.spent_in_tx
|
|
||||||
WHERE u.height <= :max_height
|
WHERE u.height <= :max_height
|
||||||
AND (u.spent_in_tx IS NULL OR (tx.block IS NULL AND tx.expiry_height <= :stable_height))
|
-- and the received txo is unspent
|
||||||
|
AND u.id NOT IN (
|
||||||
|
SELECT transparent_received_output_id
|
||||||
|
FROM transparent_received_output_spends txo_spends
|
||||||
|
JOIN transactions tx
|
||||||
|
ON tx.id_tx = txo_spends.transaction_id
|
||||||
|
WHERE tx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR tx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
OR tx.expiry_height > :stable_height -- the spending tx is unexpired
|
||||||
|
)
|
||||||
GROUP BY u.received_by_account_id",
|
GROUP BY u.received_by_account_id",
|
||||||
)?;
|
)?;
|
||||||
let mut rows = stmt_transparent_balances.query(named_params![
|
let mut rows = stmt_transparent_balances.query(named_params![
|
||||||
|
@ -1840,7 +1855,12 @@ pub(crate) fn get_min_unspent_height(
|
||||||
"SELECT MIN(tx.block)
|
"SELECT MIN(tx.block)
|
||||||
FROM sapling_received_notes n
|
FROM sapling_received_notes n
|
||||||
JOIN transactions tx ON tx.id_tx = n.tx
|
JOIN transactions tx ON tx.id_tx = n.tx
|
||||||
WHERE n.spent IS NULL",
|
WHERE n.id NOT IN (
|
||||||
|
SELECT sapling_received_note_id
|
||||||
|
FROM sapling_received_note_spends
|
||||||
|
JOIN transactions tx ON tx.id_tx = transaction_id
|
||||||
|
WHERE tx.block IS NOT NULL
|
||||||
|
)",
|
||||||
[],
|
[],
|
||||||
|row| {
|
|row| {
|
||||||
row.get(0)
|
row.get(0)
|
||||||
|
@ -1852,7 +1872,12 @@ pub(crate) fn get_min_unspent_height(
|
||||||
"SELECT MIN(tx.block)
|
"SELECT MIN(tx.block)
|
||||||
FROM orchard_received_notes n
|
FROM orchard_received_notes n
|
||||||
JOIN transactions tx ON tx.id_tx = n.tx
|
JOIN transactions tx ON tx.id_tx = n.tx
|
||||||
WHERE n.spent IS NULL",
|
WHERE n.id NOT IN (
|
||||||
|
SELECT orchard_received_note_id
|
||||||
|
FROM orchard_received_note_spends
|
||||||
|
JOIN transactions tx ON tx.id_tx = transaction_id
|
||||||
|
WHERE tx.block IS NOT NULL
|
||||||
|
)",
|
||||||
[],
|
[],
|
||||||
|row| {
|
|row| {
|
||||||
row.get(0)
|
row.get(0)
|
||||||
|
@ -1898,7 +1923,25 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing to do if we're deleting back down to the max height
|
// Delete from the scanning queue any range with a start height greater than the
|
||||||
|
// truncation height, and then truncate any remaining range by setting the end
|
||||||
|
// equal to the truncation height + 1. This sets our view of the chain tip back
|
||||||
|
// to the retained height.
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM scan_queue
|
||||||
|
WHERE block_range_start >= :new_end_height",
|
||||||
|
named_params![":new_end_height": u32::from(block_height + 1)],
|
||||||
|
)?;
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE scan_queue
|
||||||
|
SET block_range_end = :new_end_height
|
||||||
|
WHERE block_range_end > :new_end_height",
|
||||||
|
named_params![":new_end_height": u32::from(block_height + 1)],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// If we're removing scanned blocks, we need to truncate the note commitment tree, un-mine
|
||||||
|
// transactions, and remove received transparent outputs and affected block records from the
|
||||||
|
// database.
|
||||||
if block_height < last_scanned_height {
|
if block_height < last_scanned_height {
|
||||||
// Truncate the note commitment trees
|
// Truncate the note commitment trees
|
||||||
let mut wdb = WalletDb {
|
let mut wdb = WalletDb {
|
||||||
|
@ -1913,36 +1956,15 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
||||||
tree.truncate_removing_checkpoint(&block_height).map(|_| ())
|
tree.truncate_removing_checkpoint(&block_height).map(|_| ())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Rewind received notes
|
|
||||||
conn.execute(
|
|
||||||
"DELETE FROM sapling_received_notes
|
|
||||||
WHERE id IN (
|
|
||||||
SELECT rn.id
|
|
||||||
FROM sapling_received_notes rn
|
|
||||||
LEFT OUTER JOIN transactions tx
|
|
||||||
ON tx.id_tx = rn.tx
|
|
||||||
WHERE tx.block IS NOT NULL AND tx.block > ?
|
|
||||||
);",
|
|
||||||
[u32::from(block_height)],
|
|
||||||
)?;
|
|
||||||
#[cfg(feature = "orchard")]
|
|
||||||
conn.execute(
|
|
||||||
"DELETE FROM orchard_received_notes
|
|
||||||
WHERE id IN (
|
|
||||||
SELECT rn.id
|
|
||||||
FROM orchard_received_notes rn
|
|
||||||
LEFT OUTER JOIN transactions tx
|
|
||||||
ON tx.id_tx = rn.tx
|
|
||||||
WHERE tx.block IS NOT NULL AND tx.block > ?
|
|
||||||
);",
|
|
||||||
[u32::from(block_height)],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Do not delete sent notes; this can contain data that is not recoverable
|
// Do not delete sent notes; this can contain data that is not recoverable
|
||||||
// from the chain. Wallets must continue to operate correctly in the
|
// from the chain. Wallets must continue to operate correctly in the
|
||||||
// presence of stale sent notes that link to unmined transactions.
|
// presence of stale sent notes that link to unmined transactions.
|
||||||
|
// Also, do not delete received notes; they may contain memo data that is
|
||||||
|
// not recoverable; balance APIs must ensure that un-mined received notes
|
||||||
|
// do not count towards spendability or transaction balalnce.
|
||||||
|
|
||||||
// Rewind utxos
|
// Rewind utxos. It is currently necessary to delete these because we do
|
||||||
|
// not have the full transaction data for the received output.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"DELETE FROM utxos WHERE height > ?",
|
"DELETE FROM utxos WHERE height > ?",
|
||||||
[u32::from(block_height)],
|
[u32::from(block_height)],
|
||||||
|
@ -1955,7 +1977,7 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
||||||
[u32::from(block_height)],
|
[u32::from(block_height)],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Now that they aren't depended on, delete scanned blocks.
|
// Now that they aren't depended on, delete un-mined blocks.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"DELETE FROM blocks WHERE height > ?",
|
"DELETE FROM blocks WHERE height > ?",
|
||||||
[u32::from(block_height)],
|
[u32::from(block_height)],
|
||||||
|
@ -1968,32 +1990,6 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
||||||
WHERE block_height > :block_height",
|
WHERE block_height > :block_height",
|
||||||
named_params![":block_height": u32::from(block_height)],
|
named_params![":block_height": u32::from(block_height)],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Delete from the scanning queue any range with a start height greater than the
|
|
||||||
// truncation height, and then truncate any remaining range by setting the end
|
|
||||||
// equal to the truncation height + 1.
|
|
||||||
conn.execute(
|
|
||||||
"DELETE FROM scan_queue
|
|
||||||
WHERE block_range_start > :block_height",
|
|
||||||
named_params![":block_height": u32::from(block_height)],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE scan_queue
|
|
||||||
SET block_range_end = :end_height
|
|
||||||
WHERE block_range_end > :end_height",
|
|
||||||
named_params![":end_height": u32::from(block_height + 1)],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Prioritize the height we just rewound to for verification.
|
|
||||||
let query_range = block_height..(block_height + 1);
|
|
||||||
let scan_range = ScanRange::from_parts(query_range.clone(), ScanPriority::Verify);
|
|
||||||
replace_queue_entries::<SqliteClientError>(
|
|
||||||
conn,
|
|
||||||
&query_range,
|
|
||||||
Some(scan_range).into_iter(),
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -2037,11 +2033,15 @@ pub(crate) fn get_unspent_transparent_output(
|
||||||
let mut stmt_select_utxo = conn.prepare_cached(
|
let mut stmt_select_utxo = conn.prepare_cached(
|
||||||
"SELECT u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height
|
"SELECT u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height
|
||||||
FROM utxos u
|
FROM utxos u
|
||||||
LEFT OUTER JOIN transactions tx
|
|
||||||
ON tx.id_tx = u.spent_in_tx
|
|
||||||
WHERE u.prevout_txid = :txid
|
WHERE u.prevout_txid = :txid
|
||||||
AND u.prevout_idx = :output_index
|
AND u.prevout_idx = :output_index
|
||||||
AND tx.block IS NULL",
|
AND u.id NOT IN (
|
||||||
|
SELECT txo_spends.id
|
||||||
|
FROM transparent_received_output_spends txo_spends
|
||||||
|
JOIN transactions tx ON tx.id_tx = txo_spends.transaction_id
|
||||||
|
WHERE tx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR tx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
)",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let result: Result<Option<WalletTransparentOutput>, SqliteClientError> = stmt_select_utxo
|
let result: Result<Option<WalletTransparentOutput>, SqliteClientError> = stmt_select_utxo
|
||||||
|
@ -2078,11 +2078,17 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
|
||||||
"SELECT u.prevout_txid, u.prevout_idx, u.script,
|
"SELECT u.prevout_txid, u.prevout_idx, u.script,
|
||||||
u.value_zat, u.height
|
u.value_zat, u.height
|
||||||
FROM utxos u
|
FROM utxos u
|
||||||
LEFT OUTER JOIN transactions tx
|
|
||||||
ON tx.id_tx = u.spent_in_tx
|
|
||||||
WHERE u.address = :address
|
WHERE u.address = :address
|
||||||
AND u.height <= :max_height
|
AND u.height <= :max_height
|
||||||
AND (u.spent_in_tx IS NULL OR (tx.block IS NULL AND tx.expiry_height <= :stable_height))",
|
AND u.id NOT IN (
|
||||||
|
SELECT txo_spends.transparent_received_output_id
|
||||||
|
FROM transparent_received_output_spends txo_spends
|
||||||
|
JOIN transactions tx ON tx.id_tx = txo_spends.transaction_id
|
||||||
|
WHERE
|
||||||
|
tx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR tx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
OR tx.expiry_height > :stable_height -- the spending tx is unexpired
|
||||||
|
)",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let addr_str = address.encode(params);
|
let addr_str = address.encode(params);
|
||||||
|
@ -2124,11 +2130,17 @@ pub(crate) fn get_transparent_balances<P: consensus::Parameters>(
|
||||||
let mut stmt_blocks = conn.prepare(
|
let mut stmt_blocks = conn.prepare(
|
||||||
"SELECT u.address, SUM(u.value_zat)
|
"SELECT u.address, SUM(u.value_zat)
|
||||||
FROM utxos u
|
FROM utxos u
|
||||||
LEFT OUTER JOIN transactions tx
|
|
||||||
ON tx.id_tx = u.spent_in_tx
|
|
||||||
WHERE u.received_by_account_id = :account_id
|
WHERE u.received_by_account_id = :account_id
|
||||||
AND u.height <= :max_height
|
AND u.height <= :max_height
|
||||||
AND (u.spent_in_tx IS NULL OR (tx.block IS NULL AND tx.expiry_height <= :stable_height))
|
AND u.id NOT IN (
|
||||||
|
SELECT txo_spends.transparent_received_output_id
|
||||||
|
FROM transparent_received_output_spends txo_spends
|
||||||
|
JOIN transactions tx ON tx.id_tx = txo_spends.transaction_id
|
||||||
|
WHERE
|
||||||
|
tx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR tx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
OR tx.expiry_height > :stable_height -- the spending tx is unexpired
|
||||||
|
)
|
||||||
GROUP BY u.address",
|
GROUP BY u.address",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -2316,9 +2328,12 @@ pub(crate) fn mark_transparent_utxo_spent(
|
||||||
outpoint: &OutPoint,
|
outpoint: &OutPoint,
|
||||||
) -> Result<(), SqliteClientError> {
|
) -> Result<(), SqliteClientError> {
|
||||||
let mut stmt_mark_transparent_utxo_spent = conn.prepare_cached(
|
let mut stmt_mark_transparent_utxo_spent = conn.prepare_cached(
|
||||||
"UPDATE utxos SET spent_in_tx = :spent_in_tx
|
"INSERT INTO transparent_received_output_spends (transparent_received_output_id, transaction_id)
|
||||||
WHERE prevout_txid = :prevout_txid
|
SELECT txo.id, :spent_in_tx
|
||||||
AND prevout_idx = :prevout_idx",
|
FROM utxos txo
|
||||||
|
WHERE txo.prevout_txid = :prevout_txid
|
||||||
|
AND txo.prevout_idx = :prevout_idx
|
||||||
|
ON CONFLICT (transparent_received_output_id, transaction_id) DO NOTHING",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let sql_args = named_params![
|
let sql_args = named_params![
|
||||||
|
@ -2418,29 +2433,6 @@ pub(crate) fn put_legacy_transparent_utxo<P: consensus::Parameters>(
|
||||||
stmt_upsert_legacy_transparent_utxo.query_row(sql_args, |row| row.get::<_, i64>(0).map(UtxoId))
|
stmt_upsert_legacy_transparent_utxo.query_row(sql_args, |row| row.get::<_, i64>(0).map(UtxoId))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks notes that have not been mined in transactions
|
|
||||||
/// as expired, up to the given block height.
|
|
||||||
pub(crate) fn update_expired_notes(
|
|
||||||
conn: &rusqlite::Connection,
|
|
||||||
expiry_height: BlockHeight,
|
|
||||||
) -> Result<(), SqliteClientError> {
|
|
||||||
let mut stmt_update_sapling_expired = conn.prepare_cached(
|
|
||||||
"UPDATE sapling_received_notes SET spent = NULL WHERE EXISTS (
|
|
||||||
SELECT id_tx FROM transactions
|
|
||||||
WHERE id_tx = sapling_received_notes.spent AND block IS NULL AND expiry_height < ?
|
|
||||||
)",
|
|
||||||
)?;
|
|
||||||
stmt_update_sapling_expired.execute([u32::from(expiry_height)])?;
|
|
||||||
let mut stmt_update_orchard_expired = conn.prepare_cached(
|
|
||||||
"UPDATE orchard_received_notes SET spent = NULL WHERE EXISTS (
|
|
||||||
SELECT id_tx FROM transactions
|
|
||||||
WHERE id_tx = orchard_received_notes.spent AND block IS NULL AND expiry_height < ?
|
|
||||||
)",
|
|
||||||
)?;
|
|
||||||
stmt_update_orchard_expired.execute([u32::from(expiry_height)])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// A utility function for creation of parameters for use in `insert_sent_output`
|
// A utility function for creation of parameters for use in `insert_sent_output`
|
||||||
// and `put_sent_output`
|
// and `put_sent_output`
|
||||||
fn recipient_params<P: consensus::Parameters>(
|
fn recipient_params<P: consensus::Parameters>(
|
||||||
|
|
|
@ -65,19 +65,26 @@ where
|
||||||
let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol);
|
let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol);
|
||||||
let result = conn.query_row_and_then(
|
let result = conn.query_row_and_then(
|
||||||
&format!(
|
&format!(
|
||||||
"SELECT {table_prefix}_received_notes.id, txid, {index_col},
|
"SELECT rn.id, txid, {index_col},
|
||||||
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
||||||
accounts.ufvk, recipient_key_scope
|
accounts.ufvk, recipient_key_scope
|
||||||
FROM {table_prefix}_received_notes
|
FROM {table_prefix}_received_notes rn
|
||||||
INNER JOIN accounts ON accounts.id = {table_prefix}_received_notes.account_id
|
INNER JOIN accounts ON accounts.id = rn.account_id
|
||||||
INNER JOIN transactions ON transactions.id_tx = {table_prefix}_received_notes.tx
|
INNER JOIN transactions ON transactions.id_tx = rn.tx
|
||||||
WHERE txid = :txid
|
WHERE txid = :txid
|
||||||
|
AND transactions.block IS NOT NULL
|
||||||
AND {index_col} = :output_index
|
AND {index_col} = :output_index
|
||||||
AND accounts.ufvk IS NOT NULL
|
AND accounts.ufvk IS NOT NULL
|
||||||
AND recipient_key_scope IS NOT NULL
|
AND recipient_key_scope IS NOT NULL
|
||||||
AND nf IS NOT NULL
|
AND nf IS NOT NULL
|
||||||
AND commitment_tree_position IS NOT NULL
|
AND commitment_tree_position IS NOT NULL
|
||||||
AND spent IS NULL"
|
AND rn.id NOT IN (
|
||||||
|
SELECT {table_prefix}_received_note_id
|
||||||
|
FROM {table_prefix}_received_note_spends
|
||||||
|
JOIN transactions stx ON stx.id_tx = transaction_id
|
||||||
|
WHERE stx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR stx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
)"
|
||||||
),
|
),
|
||||||
named_params![
|
named_params![
|
||||||
":txid": txid.as_ref(),
|
":txid": txid.as_ref(),
|
||||||
|
@ -143,10 +150,7 @@ where
|
||||||
SELECT
|
SELECT
|
||||||
{table_prefix}_received_notes.id AS id, txid, {index_col},
|
{table_prefix}_received_notes.id AS id, txid, {index_col},
|
||||||
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
||||||
SUM(value) OVER (
|
SUM(value) OVER (ROWS UNBOUNDED PRECEDING) AS so_far,
|
||||||
PARTITION BY {table_prefix}_received_notes.account_id, spent
|
|
||||||
ORDER BY {table_prefix}_received_notes.id
|
|
||||||
) AS so_far,
|
|
||||||
accounts.ufvk as ufvk, recipient_key_scope
|
accounts.ufvk as ufvk, recipient_key_scope
|
||||||
FROM {table_prefix}_received_notes
|
FROM {table_prefix}_received_notes
|
||||||
INNER JOIN accounts
|
INNER JOIN accounts
|
||||||
|
@ -158,9 +162,16 @@ where
|
||||||
AND recipient_key_scope IS NOT NULL
|
AND recipient_key_scope IS NOT NULL
|
||||||
AND nf IS NOT NULL
|
AND nf IS NOT NULL
|
||||||
AND commitment_tree_position IS NOT NULL
|
AND commitment_tree_position IS NOT NULL
|
||||||
AND spent IS NULL
|
|
||||||
AND transactions.block <= :anchor_height
|
AND transactions.block <= :anchor_height
|
||||||
AND {table_prefix}_received_notes.id NOT IN rarray(:exclude)
|
AND {table_prefix}_received_notes.id NOT IN rarray(:exclude)
|
||||||
|
AND {table_prefix}_received_notes.id NOT IN (
|
||||||
|
SELECT {table_prefix}_received_note_id
|
||||||
|
FROM {table_prefix}_received_note_spends
|
||||||
|
JOIN transactions stx ON stx.id_tx = transaction_id
|
||||||
|
WHERE stx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR stx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
OR stx.expiry_height > :anchor_height -- the spending tx is unexpired
|
||||||
|
)
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges unscanned
|
SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges unscanned
|
||||||
-- select all the unscanned ranges involving the shard containing this note
|
-- select all the unscanned ranges involving the shard containing this note
|
||||||
|
|
|
@ -402,6 +402,17 @@ mod tests {
|
||||||
ON UPDATE RESTRICT,
|
ON UPDATE RESTRICT,
|
||||||
CONSTRAINT nf_uniq UNIQUE (spend_pool, nf)
|
CONSTRAINT nf_uniq UNIQUE (spend_pool, nf)
|
||||||
)",
|
)",
|
||||||
|
"CREATE TABLE orchard_received_note_spends (
|
||||||
|
orchard_received_note_id INTEGER NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (orchard_received_note_id)
|
||||||
|
REFERENCES orchard_received_notes(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (transaction_id)
|
||||||
|
-- We do not delete transactions, so this does not cascade
|
||||||
|
REFERENCES transactions(id_tx),
|
||||||
|
UNIQUE (orchard_received_note_id, transaction_id)
|
||||||
|
)",
|
||||||
"CREATE TABLE orchard_received_notes (
|
"CREATE TABLE orchard_received_notes (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
tx INTEGER NOT NULL,
|
tx INTEGER NOT NULL,
|
||||||
|
@ -414,12 +425,10 @@ mod tests {
|
||||||
nf BLOB UNIQUE,
|
nf BLOB UNIQUE,
|
||||||
is_change INTEGER NOT NULL,
|
is_change INTEGER NOT NULL,
|
||||||
memo BLOB,
|
memo BLOB,
|
||||||
spent INTEGER,
|
|
||||||
commitment_tree_position INTEGER,
|
commitment_tree_position INTEGER,
|
||||||
recipient_key_scope INTEGER,
|
recipient_key_scope INTEGER,
|
||||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_output UNIQUE (tx, action_index)
|
CONSTRAINT tx_output UNIQUE (tx, action_index)
|
||||||
)",
|
)",
|
||||||
"CREATE TABLE orchard_tree_cap (
|
"CREATE TABLE orchard_tree_cap (
|
||||||
|
@ -447,6 +456,17 @@ mod tests {
|
||||||
contains_marked INTEGER,
|
contains_marked INTEGER,
|
||||||
CONSTRAINT root_unique UNIQUE (root_hash)
|
CONSTRAINT root_unique UNIQUE (root_hash)
|
||||||
)",
|
)",
|
||||||
|
"CREATE TABLE sapling_received_note_spends (
|
||||||
|
sapling_received_note_id INTEGER NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (sapling_received_note_id)
|
||||||
|
REFERENCES sapling_received_notes(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (transaction_id)
|
||||||
|
-- We do not delete transactions, so this does not cascade
|
||||||
|
REFERENCES transactions(id_tx),
|
||||||
|
UNIQUE (sapling_received_note_id, transaction_id)
|
||||||
|
)",
|
||||||
r#"CREATE TABLE "sapling_received_notes" (
|
r#"CREATE TABLE "sapling_received_notes" (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
tx INTEGER NOT NULL,
|
tx INTEGER NOT NULL,
|
||||||
|
@ -458,12 +478,10 @@ mod tests {
|
||||||
nf BLOB UNIQUE,
|
nf BLOB UNIQUE,
|
||||||
is_change INTEGER NOT NULL,
|
is_change INTEGER NOT NULL,
|
||||||
memo BLOB,
|
memo BLOB,
|
||||||
spent INTEGER,
|
|
||||||
commitment_tree_position INTEGER,
|
commitment_tree_position INTEGER,
|
||||||
recipient_key_scope INTEGER,
|
recipient_key_scope INTEGER,
|
||||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
||||||
)"#,
|
)"#,
|
||||||
"CREATE TABLE sapling_tree_cap (
|
"CREATE TABLE sapling_tree_cap (
|
||||||
|
@ -535,6 +553,17 @@ mod tests {
|
||||||
fee INTEGER,
|
fee INTEGER,
|
||||||
FOREIGN KEY (block) REFERENCES blocks(height)
|
FOREIGN KEY (block) REFERENCES blocks(height)
|
||||||
)",
|
)",
|
||||||
|
"CREATE TABLE transparent_received_output_spends (
|
||||||
|
transparent_received_output_id INTEGER NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (transparent_received_output_id)
|
||||||
|
REFERENCES utxos(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (transaction_id)
|
||||||
|
-- We do not delete transactions, so this does not cascade
|
||||||
|
REFERENCES transactions(id_tx),
|
||||||
|
UNIQUE (transparent_received_output_id, transaction_id)
|
||||||
|
)",
|
||||||
"CREATE TABLE tx_locator_map (
|
"CREATE TABLE tx_locator_map (
|
||||||
block_height INTEGER NOT NULL,
|
block_height INTEGER NOT NULL,
|
||||||
tx_index INTEGER NOT NULL,
|
tx_index INTEGER NOT NULL,
|
||||||
|
@ -550,9 +579,7 @@ mod tests {
|
||||||
script BLOB NOT NULL,
|
script BLOB NOT NULL,
|
||||||
value_zat INTEGER NOT NULL,
|
value_zat INTEGER NOT NULL,
|
||||||
height INTEGER NOT NULL,
|
height INTEGER NOT NULL,
|
||||||
spent_in_tx INTEGER,
|
|
||||||
FOREIGN KEY (received_by_account_id) REFERENCES accounts(id),
|
FOREIGN KEY (received_by_account_id) REFERENCES accounts(id),
|
||||||
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
||||||
)"#,
|
)"#,
|
||||||
];
|
];
|
||||||
|
@ -584,18 +611,12 @@ mod tests {
|
||||||
r#"CREATE INDEX orchard_received_notes_account ON orchard_received_notes (
|
r#"CREATE INDEX orchard_received_notes_account ON orchard_received_notes (
|
||||||
account_id ASC
|
account_id ASC
|
||||||
)"#,
|
)"#,
|
||||||
r#"CREATE INDEX orchard_received_notes_spent ON orchard_received_notes (
|
|
||||||
spent ASC
|
|
||||||
)"#,
|
|
||||||
r#"CREATE INDEX orchard_received_notes_tx ON orchard_received_notes (
|
r#"CREATE INDEX orchard_received_notes_tx ON orchard_received_notes (
|
||||||
tx ASC
|
tx ASC
|
||||||
)"#,
|
)"#,
|
||||||
r#"CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes" (
|
r#"CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes" (
|
||||||
"account_id" ASC
|
"account_id" ASC
|
||||||
)"#,
|
)"#,
|
||||||
r#"CREATE INDEX "sapling_received_notes_spent" ON "sapling_received_notes" (
|
|
||||||
"spent" ASC
|
|
||||||
)"#,
|
|
||||||
r#"CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes" (
|
r#"CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes" (
|
||||||
"tx" ASC
|
"tx" ASC
|
||||||
)"#,
|
)"#,
|
||||||
|
@ -603,7 +624,6 @@ mod tests {
|
||||||
r#"CREATE INDEX sent_notes_to_account ON "sent_notes" (to_account_id)"#,
|
r#"CREATE INDEX sent_notes_to_account ON "sent_notes" (to_account_id)"#,
|
||||||
r#"CREATE INDEX sent_notes_tx ON "sent_notes" (tx)"#,
|
r#"CREATE INDEX sent_notes_tx ON "sent_notes" (tx)"#,
|
||||||
r#"CREATE INDEX utxos_received_by_account ON "utxos" (received_by_account_id)"#,
|
r#"CREATE INDEX utxos_received_by_account ON "utxos" (received_by_account_id)"#,
|
||||||
r#"CREATE INDEX utxos_spent_in_tx ON "utxos" (spent_in_tx)"#,
|
|
||||||
];
|
];
|
||||||
let mut indices_query = st
|
let mut indices_query = st
|
||||||
.wallet()
|
.wallet()
|
||||||
|
@ -686,6 +706,19 @@ mod tests {
|
||||||
subtree_start_height,
|
subtree_start_height,
|
||||||
subtree_end_height,
|
subtree_end_height,
|
||||||
contains_marked".to_owned(),
|
contains_marked".to_owned(),
|
||||||
|
// v_received_note_spends
|
||||||
|
"CREATE VIEW v_received_note_spends AS
|
||||||
|
SELECT
|
||||||
|
2 AS pool,
|
||||||
|
sapling_received_note_id AS received_note_id,
|
||||||
|
transaction_id
|
||||||
|
FROM sapling_received_note_spends
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
3 AS pool,
|
||||||
|
orchard_received_note_id AS received_note_id,
|
||||||
|
transaction_id
|
||||||
|
FROM orchard_received_note_spends".to_owned(),
|
||||||
// v_received_notes
|
// v_received_notes
|
||||||
"CREATE VIEW v_received_notes AS
|
"CREATE VIEW v_received_notes AS
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -697,7 +730,6 @@ mod tests {
|
||||||
sapling_received_notes.value,
|
sapling_received_notes.value,
|
||||||
is_change,
|
is_change,
|
||||||
sapling_received_notes.memo,
|
sapling_received_notes.memo,
|
||||||
spent,
|
|
||||||
sent_notes.id AS sent_note_id
|
sent_notes.id AS sent_note_id
|
||||||
FROM sapling_received_notes
|
FROM sapling_received_notes
|
||||||
LEFT JOIN sent_notes
|
LEFT JOIN sent_notes
|
||||||
|
@ -713,7 +745,6 @@ mod tests {
|
||||||
orchard_received_notes.value,
|
orchard_received_notes.value,
|
||||||
is_change,
|
is_change,
|
||||||
orchard_received_notes.memo,
|
orchard_received_notes.memo,
|
||||||
spent,
|
|
||||||
sent_notes.id AS sent_note_id
|
sent_notes.id AS sent_note_id
|
||||||
FROM orchard_received_notes
|
FROM orchard_received_notes
|
||||||
LEFT JOIN sent_notes
|
LEFT JOIN sent_notes
|
||||||
|
@ -834,8 +865,11 @@ mod tests {
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM v_received_notes
|
FROM v_received_notes
|
||||||
|
JOIN v_received_note_spends rns
|
||||||
|
ON rns.pool = v_received_notes.pool
|
||||||
|
AND rns.received_note_id = v_received_notes.id_within_pool_table
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = v_received_notes.spent
|
ON transactions.id_tx = rns.transaction_id
|
||||||
UNION
|
UNION
|
||||||
-- Transparent TXOs spent in this transaction
|
-- Transparent TXOs spent in this transaction
|
||||||
SELECT utxos.received_by_account_id AS account_id,
|
SELECT utxos.received_by_account_id AS account_id,
|
||||||
|
@ -848,8 +882,10 @@ mod tests {
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM utxos
|
FROM utxos
|
||||||
|
JOIN transparent_received_output_spends tros
|
||||||
|
ON tros.transparent_received_output_id = utxos.id
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = utxos.spent_in_tx
|
ON transactions.id_tx = tros.transaction_id
|
||||||
),
|
),
|
||||||
-- Obtain a count of the notes that the wallet created in each transaction,
|
-- Obtain a count of the notes that the wallet created in each transaction,
|
||||||
-- not counting change notes.
|
-- not counting change notes.
|
||||||
|
|
|
@ -229,12 +229,10 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
nf BLOB UNIQUE,
|
nf BLOB UNIQUE,
|
||||||
is_change INTEGER NOT NULL,
|
is_change INTEGER NOT NULL,
|
||||||
memo BLOB,
|
memo BLOB,
|
||||||
spent INTEGER,
|
|
||||||
commitment_tree_position INTEGER,
|
commitment_tree_position INTEGER,
|
||||||
recipient_key_scope INTEGER,
|
recipient_key_scope INTEGER,
|
||||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
CONSTRAINT tx_output UNIQUE (tx, output_index)
|
||||||
);
|
);
|
||||||
CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes_new" (
|
CREATE INDEX "sapling_received_notes_account" ON "sapling_received_notes_new" (
|
||||||
|
@ -243,11 +241,36 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes_new" (
|
CREATE INDEX "sapling_received_notes_tx" ON "sapling_received_notes_new" (
|
||||||
"tx" ASC
|
"tx" ASC
|
||||||
);
|
);
|
||||||
CREATE INDEX "sapling_received_notes_spent" ON "sapling_received_notes_new" (
|
|
||||||
"spent" ASC
|
-- Replace the `spent` column in `sapling_received_notes` with a junction table between
|
||||||
|
-- received notes and the transactions that spend them. This is necessary as otherwise
|
||||||
|
-- we cannot compute the correct value of transactions that expire unmined.
|
||||||
|
CREATE TABLE sapling_received_note_spends (
|
||||||
|
sapling_received_note_id INTEGER NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (sapling_received_note_id)
|
||||||
|
REFERENCES sapling_received_notes(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (transaction_id)
|
||||||
|
-- We do not delete transactions, so this does not cascade
|
||||||
|
REFERENCES transactions(id_tx),
|
||||||
|
UNIQUE (sapling_received_note_id, transaction_id)
|
||||||
);
|
);
|
||||||
INSERT INTO sapling_received_notes_new (id, tx, output_index, account_id, diversifier, value, rcm, nf, is_change, memo, spent, commitment_tree_position, recipient_key_scope)
|
|
||||||
SELECT id_note, tx, output_index, account, diversifier, value, rcm, nf, is_change, memo, spent, commitment_tree_position, recipient_key_scope
|
INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id)
|
||||||
|
SELECT id_note, spent
|
||||||
|
FROM sapling_received_notes
|
||||||
|
WHERE spent IS NOT NULL;
|
||||||
|
|
||||||
|
INSERT INTO sapling_received_notes_new (
|
||||||
|
id, tx, output_index, account_id,
|
||||||
|
diversifier, value, rcm, nf, is_change, memo, commitment_tree_position,
|
||||||
|
recipient_key_scope
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
id_note, tx, output_index, account,
|
||||||
|
diversifier, value, rcm, nf, is_change, memo, commitment_tree_position,
|
||||||
|
recipient_key_scope
|
||||||
FROM sapling_received_notes;
|
FROM sapling_received_notes;
|
||||||
|
|
||||||
DROP TABLE sapling_received_notes;
|
DROP TABLE sapling_received_notes;
|
||||||
|
@ -295,17 +318,35 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
script BLOB NOT NULL,
|
script BLOB NOT NULL,
|
||||||
value_zat INTEGER NOT NULL,
|
value_zat INTEGER NOT NULL,
|
||||||
height INTEGER NOT NULL,
|
height INTEGER NOT NULL,
|
||||||
spent_in_tx INTEGER,
|
|
||||||
FOREIGN KEY (received_by_account_id) REFERENCES accounts(id),
|
FOREIGN KEY (received_by_account_id) REFERENCES accounts(id),
|
||||||
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
||||||
);
|
);
|
||||||
CREATE INDEX utxos_received_by_account ON utxos_new (received_by_account_id);
|
CREATE INDEX utxos_received_by_account ON utxos_new (received_by_account_id);
|
||||||
CREATE INDEX utxos_spent_in_tx ON utxos_new (spent_in_tx);
|
|
||||||
INSERT INTO utxos_new (id, received_by_account_id, address, prevout_txid, prevout_idx, script, value_zat, height, spent_in_tx)
|
INSERT INTO utxos_new (id, received_by_account_id, address, prevout_txid, prevout_idx, script, value_zat, height)
|
||||||
SELECT id_utxo, received_by_account, address, prevout_txid, prevout_idx, script, value_zat, height, spent_in_tx
|
SELECT id_utxo, received_by_account, address, prevout_txid, prevout_idx, script, value_zat, height
|
||||||
FROM utxos;
|
FROM utxos;
|
||||||
|
|
||||||
|
-- Replace the `spent_in_tx` column in `utxos` with a junction table between received
|
||||||
|
-- outputs and the transactions that spend them. This is necessary as otherwise we
|
||||||
|
-- cannot compute the correct value of transactions that expire unmined.
|
||||||
|
CREATE TABLE transparent_received_output_spends (
|
||||||
|
transparent_received_output_id INTEGER NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (transparent_received_output_id)
|
||||||
|
REFERENCES utxos(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (transaction_id)
|
||||||
|
-- We do not delete transactions, so this does not cascade
|
||||||
|
REFERENCES transactions(id_tx),
|
||||||
|
UNIQUE (transparent_received_output_id, transaction_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO transparent_received_output_spends (transparent_received_output_id, transaction_id)
|
||||||
|
SELECT id_utxo, spent_in_tx
|
||||||
|
FROM utxos
|
||||||
|
WHERE spent_in_tx IS NOT NULL;
|
||||||
|
|
||||||
DROP TABLE utxos;
|
DROP TABLE utxos;
|
||||||
ALTER TABLE utxos_new RENAME TO utxos;
|
ALTER TABLE utxos_new RENAME TO utxos;
|
||||||
"#,
|
"#,
|
||||||
|
@ -361,8 +402,10 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM sapling_received_notes
|
FROM sapling_received_notes
|
||||||
|
JOIN sapling_received_note_spends
|
||||||
|
ON sapling_received_note_id = sapling_received_notes.id
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = sapling_received_notes.spent
|
ON transactions.id_tx = sapling_received_note_spends.transaction_id
|
||||||
UNION
|
UNION
|
||||||
SELECT utxos.id AS id,
|
SELECT utxos.id AS id,
|
||||||
utxos.received_by_account_id AS account_id,
|
utxos.received_by_account_id AS account_id,
|
||||||
|
@ -374,8 +417,10 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM utxos
|
FROM utxos
|
||||||
|
JOIN transparent_received_output_spends txo_spends
|
||||||
|
ON txo_spends.transparent_received_output_id = txos.id
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = utxos.spent_in_tx
|
ON transactions.id_tx = txo_spends.transaction_id
|
||||||
),
|
),
|
||||||
sent_note_counts AS (
|
sent_note_counts AS (
|
||||||
SELECT sent_notes.from_account_id AS account_id,
|
SELECT sent_notes.from_account_id AS account_id,
|
||||||
|
|
|
@ -45,12 +45,10 @@ impl RusqliteMigration for Migration {
|
||||||
nf BLOB UNIQUE,
|
nf BLOB UNIQUE,
|
||||||
is_change INTEGER NOT NULL,
|
is_change INTEGER NOT NULL,
|
||||||
memo BLOB,
|
memo BLOB,
|
||||||
spent INTEGER,
|
|
||||||
commitment_tree_position INTEGER,
|
commitment_tree_position INTEGER,
|
||||||
recipient_key_scope INTEGER,
|
recipient_key_scope INTEGER,
|
||||||
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
|
||||||
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
FOREIGN KEY (account_id) REFERENCES accounts(id),
|
||||||
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_output UNIQUE (tx, action_index)
|
CONSTRAINT tx_output UNIQUE (tx, action_index)
|
||||||
);
|
);
|
||||||
CREATE INDEX orchard_received_notes_account ON orchard_received_notes (
|
CREATE INDEX orchard_received_notes_account ON orchard_received_notes (
|
||||||
|
@ -59,8 +57,17 @@ impl RusqliteMigration for Migration {
|
||||||
CREATE INDEX orchard_received_notes_tx ON orchard_received_notes (
|
CREATE INDEX orchard_received_notes_tx ON orchard_received_notes (
|
||||||
tx ASC
|
tx ASC
|
||||||
);
|
);
|
||||||
CREATE INDEX orchard_received_notes_spent ON orchard_received_notes (
|
|
||||||
spent ASC
|
CREATE TABLE orchard_received_note_spends (
|
||||||
|
orchard_received_note_id INTEGER NOT NULL,
|
||||||
|
transaction_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (orchard_received_note_id)
|
||||||
|
REFERENCES orchard_received_notes(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (transaction_id)
|
||||||
|
-- We do not delete transactions, so this does not cascade
|
||||||
|
REFERENCES transactions(id_tx),
|
||||||
|
UNIQUE (orchard_received_note_id, transaction_id)
|
||||||
);",
|
);",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -78,7 +85,6 @@ impl RusqliteMigration for Migration {
|
||||||
sapling_received_notes.value,
|
sapling_received_notes.value,
|
||||||
is_change,
|
is_change,
|
||||||
sapling_received_notes.memo,
|
sapling_received_notes.memo,
|
||||||
spent,
|
|
||||||
sent_notes.id AS sent_note_id
|
sent_notes.id AS sent_note_id
|
||||||
FROM sapling_received_notes
|
FROM sapling_received_notes
|
||||||
LEFT JOIN sent_notes
|
LEFT JOIN sent_notes
|
||||||
|
@ -94,7 +100,6 @@ impl RusqliteMigration for Migration {
|
||||||
orchard_received_notes.value,
|
orchard_received_notes.value,
|
||||||
is_change,
|
is_change,
|
||||||
orchard_received_notes.memo,
|
orchard_received_notes.memo,
|
||||||
spent,
|
|
||||||
sent_notes.id AS sent_note_id
|
sent_notes.id AS sent_note_id
|
||||||
FROM orchard_received_notes
|
FROM orchard_received_notes
|
||||||
LEFT JOIN sent_notes
|
LEFT JOIN sent_notes
|
||||||
|
@ -103,6 +108,25 @@ impl RusqliteMigration for Migration {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
transaction.execute_batch({
|
||||||
|
let sapling_pool_code = pool_code(PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||||
|
let orchard_pool_code = pool_code(PoolType::Shielded(ShieldedProtocol::Orchard));
|
||||||
|
&format!(
|
||||||
|
"CREATE VIEW v_received_note_spends AS
|
||||||
|
SELECT
|
||||||
|
{sapling_pool_code} AS pool,
|
||||||
|
sapling_received_note_id AS received_note_id,
|
||||||
|
transaction_id
|
||||||
|
FROM sapling_received_note_spends
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
{orchard_pool_code} AS pool,
|
||||||
|
orchard_received_note_id AS received_note_id,
|
||||||
|
transaction_id
|
||||||
|
FROM orchard_received_note_spends;"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
transaction.execute_batch({
|
transaction.execute_batch({
|
||||||
let transparent_pool_code = pool_code(PoolType::Transparent);
|
let transparent_pool_code = pool_code(PoolType::Transparent);
|
||||||
&format!(
|
&format!(
|
||||||
|
@ -157,8 +181,11 @@ impl RusqliteMigration for Migration {
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM v_received_notes
|
FROM v_received_notes
|
||||||
|
JOIN v_received_note_spends rns
|
||||||
|
ON rns.pool = v_received_notes.pool
|
||||||
|
AND rns.received_note_id = v_received_notes.id_within_pool_table
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = v_received_notes.spent
|
ON transactions.id_tx = rns.transaction_id
|
||||||
UNION
|
UNION
|
||||||
-- Transparent TXOs spent in this transaction
|
-- Transparent TXOs spent in this transaction
|
||||||
SELECT utxos.received_by_account_id AS account_id,
|
SELECT utxos.received_by_account_id AS account_id,
|
||||||
|
@ -171,8 +198,10 @@ impl RusqliteMigration for Migration {
|
||||||
0 AS received_count,
|
0 AS received_count,
|
||||||
0 AS memo_present
|
0 AS memo_present
|
||||||
FROM utxos
|
FROM utxos
|
||||||
|
JOIN transparent_received_output_spends tros
|
||||||
|
ON tros.transparent_received_output_id = utxos.id
|
||||||
JOIN transactions
|
JOIN transactions
|
||||||
ON transactions.id_tx = utxos.spent_in_tx
|
ON transactions.id_tx = tros.transaction_id
|
||||||
),
|
),
|
||||||
-- Obtain a count of the notes that the wallet created in each transaction,
|
-- Obtain a count of the notes that the wallet created in each transaction,
|
||||||
-- not counting change notes.
|
-- not counting change notes.
|
||||||
|
@ -223,9 +252,14 @@ impl RusqliteMigration for Migration {
|
||||||
LEFT JOIN sent_note_counts
|
LEFT JOIN sent_note_counts
|
||||||
ON sent_note_counts.account_id = notes.account_id
|
ON sent_note_counts.account_id = notes.account_id
|
||||||
AND sent_note_counts.txid = notes.txid
|
AND sent_note_counts.txid = notes.txid
|
||||||
GROUP BY notes.account_id, notes.txid;
|
GROUP BY notes.account_id, notes.txid;"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
DROP VIEW v_tx_outputs;
|
transaction.execute_batch({
|
||||||
|
let transparent_pool_code = pool_code(PoolType::Transparent);
|
||||||
|
&format!(
|
||||||
|
"DROP VIEW v_tx_outputs;
|
||||||
CREATE VIEW v_tx_outputs AS
|
CREATE VIEW v_tx_outputs AS
|
||||||
SELECT transactions.txid AS txid,
|
SELECT transactions.txid AS txid,
|
||||||
v_received_notes.pool AS output_pool,
|
v_received_notes.pool AS output_pool,
|
||||||
|
@ -267,7 +301,8 @@ impl RusqliteMigration for Migration {
|
||||||
ON transactions.id_tx = sent_notes.tx
|
ON transactions.id_tx = sent_notes.tx
|
||||||
LEFT JOIN v_received_notes
|
LEFT JOIN v_received_notes
|
||||||
ON sent_notes.id = v_received_notes.sent_note_id
|
ON sent_notes.id = v_received_notes.sent_note_id
|
||||||
WHERE COALESCE(v_received_notes.is_change, 0) = 0;")
|
WHERE COALESCE(v_received_notes.is_change, 0) = 0;"
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -3,7 +3,7 @@ use orchard::{
|
||||||
keys::Diversifier,
|
keys::Diversifier,
|
||||||
note::{Note, Nullifier, RandomSeed, Rho},
|
note::{Note, Nullifier, RandomSeed, Rho},
|
||||||
};
|
};
|
||||||
use rusqlite::{named_params, params, Connection, Row};
|
use rusqlite::{named_params, Connection, Row, Transaction};
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
data_api::NullifierQuery,
|
data_api::NullifierQuery,
|
||||||
|
@ -222,7 +222,7 @@ pub(crate) fn select_spendable_orchard_notes<P: consensus::Parameters>(
|
||||||
/// - A transaction will not contain more than 2^63 shielded outputs.
|
/// - A transaction will not contain more than 2^63 shielded outputs.
|
||||||
/// - A note value will never exceed 2^63 zatoshis.
|
/// - A note value will never exceed 2^63 zatoshis.
|
||||||
pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
conn: &Connection,
|
conn: &Transaction,
|
||||||
output: &T,
|
output: &T,
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
spent_in: Option<i64>,
|
spent_in: Option<i64>,
|
||||||
|
@ -232,13 +232,13 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
(
|
(
|
||||||
tx, action_index, account_id,
|
tx, action_index, account_id,
|
||||||
diversifier, value, rho, rseed, memo, nf,
|
diversifier, value, rho, rseed, memo, nf,
|
||||||
is_change, spent, commitment_tree_position,
|
is_change, commitment_tree_position,
|
||||||
recipient_key_scope
|
recipient_key_scope
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
:tx, :action_index, :account_id,
|
:tx, :action_index, :account_id,
|
||||||
:diversifier, :value, :rho, :rseed, :memo, :nf,
|
:diversifier, :value, :rho, :rseed, :memo, :nf,
|
||||||
:is_change, :spent, :commitment_tree_position,
|
:is_change, :commitment_tree_position,
|
||||||
:recipient_key_scope
|
:recipient_key_scope
|
||||||
)
|
)
|
||||||
ON CONFLICT (tx, action_index) DO UPDATE
|
ON CONFLICT (tx, action_index) DO UPDATE
|
||||||
|
@ -250,9 +250,9 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
nf = IFNULL(:nf, nf),
|
nf = IFNULL(:nf, nf),
|
||||||
memo = IFNULL(:memo, memo),
|
memo = IFNULL(:memo, memo),
|
||||||
is_change = IFNULL(:is_change, is_change),
|
is_change = IFNULL(:is_change, is_change),
|
||||||
spent = IFNULL(:spent, spent),
|
|
||||||
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position),
|
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position),
|
||||||
recipient_key_scope = :recipient_key_scope",
|
recipient_key_scope = :recipient_key_scope
|
||||||
|
RETURNING orchard_received_notes.id",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let rseed = output.note().rseed();
|
let rseed = output.note().rseed();
|
||||||
|
@ -270,15 +270,25 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
":nf": output.nullifier().map(|nf| nf.to_bytes()),
|
":nf": output.nullifier().map(|nf| nf.to_bytes()),
|
||||||
":memo": memo_repr(output.memo()),
|
":memo": memo_repr(output.memo()),
|
||||||
":is_change": output.is_change(),
|
":is_change": output.is_change(),
|
||||||
":spent": spent_in,
|
|
||||||
":commitment_tree_position": output.note_commitment_tree_position().map(u64::from),
|
":commitment_tree_position": output.note_commitment_tree_position().map(u64::from),
|
||||||
":recipient_key_scope": output.recipient_key_scope().map(scope_code),
|
":recipient_key_scope": output.recipient_key_scope().map(scope_code),
|
||||||
];
|
];
|
||||||
|
|
||||||
stmt_upsert_received_note
|
let received_note_id = stmt_upsert_received_note
|
||||||
.execute(sql_args)
|
.query_row(sql_args, |row| row.get::<_, i64>(0))
|
||||||
.map_err(SqliteClientError::from)?;
|
.map_err(SqliteClientError::from)?;
|
||||||
|
|
||||||
|
if let Some(spent_in) = spent_in {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO orchard_received_note_spends (orchard_received_note_id, transaction_id)
|
||||||
|
VALUES (:orchard_received_note_id, :transaction_id)
|
||||||
|
ON CONFLICT (orchard_received_note_id, transaction_id) DO NOTHING",
|
||||||
|
named_params![
|
||||||
|
":orchard_received_note_id": received_note_id,
|
||||||
|
":transaction_id": spent_in
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,10 +307,16 @@ pub(crate) fn get_orchard_nullifiers(
|
||||||
NullifierQuery::Unspent => conn.prepare(
|
NullifierQuery::Unspent => conn.prepare(
|
||||||
"SELECT rn.account_id, rn.nf
|
"SELECT rn.account_id, rn.nf
|
||||||
FROM orchard_received_notes rn
|
FROM orchard_received_notes rn
|
||||||
LEFT OUTER JOIN transactions tx
|
JOIN transactions tx ON tx.id_tx = rn.tx
|
||||||
ON tx.id_tx = rn.spent
|
WHERE rn.nf IS NOT NULL
|
||||||
WHERE tx.block IS NULL
|
AND tx.block IS NOT NULL
|
||||||
AND nf IS NOT NULL",
|
AND rn.id NOT IN (
|
||||||
|
SELECT spends.orchard_received_note_id
|
||||||
|
FROM orchard_received_note_spends spends
|
||||||
|
JOIN transactions stx ON stx.id_tx = spends.transaction_id
|
||||||
|
WHERE stx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR stx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
)",
|
||||||
)?,
|
)?,
|
||||||
NullifierQuery::All => conn.prepare(
|
NullifierQuery::All => conn.prepare(
|
||||||
"SELECT rn.account_id, rn.nf
|
"SELECT rn.account_id, rn.nf
|
||||||
|
@ -329,10 +345,16 @@ pub(crate) fn mark_orchard_note_spent(
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
nf: &Nullifier,
|
nf: &Nullifier,
|
||||||
) -> Result<bool, SqliteClientError> {
|
) -> Result<bool, SqliteClientError> {
|
||||||
let mut stmt_mark_orchard_note_spent =
|
let mut stmt_mark_orchard_note_spent = conn.prepare_cached(
|
||||||
conn.prepare_cached("UPDATE orchard_received_notes SET spent = ? WHERE nf = ?")?;
|
"INSERT INTO orchard_received_note_spends (orchard_received_note_id, transaction_id)
|
||||||
|
SELECT id, :transaction_id FROM orchard_received_notes WHERE nf = :nf
|
||||||
|
ON CONFLICT (orchard_received_note_id, transaction_id) DO NOTHING",
|
||||||
|
)?;
|
||||||
|
|
||||||
match stmt_mark_orchard_note_spent.execute(params![tx_ref, nf.to_bytes()])? {
|
match stmt_mark_orchard_note_spent.execute(named_params![
|
||||||
|
":nf": nf.to_bytes(),
|
||||||
|
":transaction_id": tx_ref
|
||||||
|
])? {
|
||||||
0 => Ok(false),
|
0 => Ok(false),
|
||||||
1 => Ok(true),
|
1 => Ok(true),
|
||||||
_ => unreachable!("nf column is marked as UNIQUE"),
|
_ => unreachable!("nf column is marked as UNIQUE"),
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use group::ff::PrimeField;
|
use group::ff::PrimeField;
|
||||||
use incrementalmerkletree::Position;
|
use incrementalmerkletree::Position;
|
||||||
use rusqlite::{named_params, params, Connection, Row};
|
use rusqlite::{named_params, Connection, Row, Transaction};
|
||||||
|
|
||||||
use sapling::{self, Diversifier, Nullifier, Rseed};
|
use sapling::{self, Diversifier, Nullifier, Rseed};
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
|
@ -241,10 +241,16 @@ pub(crate) fn get_sapling_nullifiers(
|
||||||
NullifierQuery::Unspent => conn.prepare(
|
NullifierQuery::Unspent => conn.prepare(
|
||||||
"SELECT rn.account_id, rn.nf
|
"SELECT rn.account_id, rn.nf
|
||||||
FROM sapling_received_notes rn
|
FROM sapling_received_notes rn
|
||||||
LEFT OUTER JOIN transactions tx
|
JOIN transactions tx ON tx.id_tx = rn.tx
|
||||||
ON tx.id_tx = rn.spent
|
WHERE rn.nf IS NOT NULL
|
||||||
WHERE tx.block IS NULL
|
AND tx.block IS NOT NULL
|
||||||
AND nf IS NOT NULL",
|
AND rn.id NOT IN (
|
||||||
|
SELECT spends.sapling_received_note_id
|
||||||
|
FROM sapling_received_note_spends spends
|
||||||
|
JOIN transactions stx ON stx.id_tx = spends.transaction_id
|
||||||
|
WHERE stx.block IS NOT NULL -- the spending tx is mined
|
||||||
|
OR stx.expiry_height IS NULL -- the spending tx will not expire
|
||||||
|
)",
|
||||||
),
|
),
|
||||||
NullifierQuery::All => conn.prepare(
|
NullifierQuery::All => conn.prepare(
|
||||||
"SELECT rn.account_id, rn.nf
|
"SELECT rn.account_id, rn.nf
|
||||||
|
@ -273,10 +279,16 @@ pub(crate) fn mark_sapling_note_spent(
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
nf: &sapling::Nullifier,
|
nf: &sapling::Nullifier,
|
||||||
) -> Result<bool, SqliteClientError> {
|
) -> Result<bool, SqliteClientError> {
|
||||||
let mut stmt_mark_sapling_note_spent =
|
let mut stmt_mark_sapling_note_spent = conn.prepare_cached(
|
||||||
conn.prepare_cached("UPDATE sapling_received_notes SET spent = ? WHERE nf = ?")?;
|
"INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id)
|
||||||
|
SELECT id, :transaction_id FROM sapling_received_notes WHERE nf = :nf
|
||||||
|
ON CONFLICT (sapling_received_note_id, transaction_id) DO NOTHING",
|
||||||
|
)?;
|
||||||
|
|
||||||
match stmt_mark_sapling_note_spent.execute(params![tx_ref, &nf.0[..]])? {
|
match stmt_mark_sapling_note_spent.execute(named_params![
|
||||||
|
":nf": &nf.0[..],
|
||||||
|
":transaction_id": tx_ref
|
||||||
|
])? {
|
||||||
0 => Ok(false),
|
0 => Ok(false),
|
||||||
1 => Ok(true),
|
1 => Ok(true),
|
||||||
_ => unreachable!("nf column is marked as UNIQUE"),
|
_ => unreachable!("nf column is marked as UNIQUE"),
|
||||||
|
@ -289,7 +301,7 @@ pub(crate) fn mark_sapling_note_spent(
|
||||||
/// - A transaction will not contain more than 2^63 shielded outputs.
|
/// - A transaction will not contain more than 2^63 shielded outputs.
|
||||||
/// - A note value will never exceed 2^63 zatoshis.
|
/// - A note value will never exceed 2^63 zatoshis.
|
||||||
pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
||||||
conn: &Connection,
|
conn: &Transaction,
|
||||||
output: &T,
|
output: &T,
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
spent_in: Option<i64>,
|
spent_in: Option<i64>,
|
||||||
|
@ -297,7 +309,7 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
||||||
let mut stmt_upsert_received_note = conn.prepare_cached(
|
let mut stmt_upsert_received_note = conn.prepare_cached(
|
||||||
"INSERT INTO sapling_received_notes
|
"INSERT INTO sapling_received_notes
|
||||||
(tx, output_index, account_id, diversifier, value, rcm, memo, nf,
|
(tx, output_index, account_id, diversifier, value, rcm, memo, nf,
|
||||||
is_change, spent, commitment_tree_position,
|
is_change, commitment_tree_position,
|
||||||
recipient_key_scope)
|
recipient_key_scope)
|
||||||
VALUES (
|
VALUES (
|
||||||
:tx,
|
:tx,
|
||||||
|
@ -309,7 +321,6 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
||||||
:memo,
|
:memo,
|
||||||
:nf,
|
:nf,
|
||||||
:is_change,
|
:is_change,
|
||||||
:spent,
|
|
||||||
:commitment_tree_position,
|
:commitment_tree_position,
|
||||||
:recipient_key_scope
|
:recipient_key_scope
|
||||||
)
|
)
|
||||||
|
@ -321,9 +332,9 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
||||||
nf = IFNULL(:nf, nf),
|
nf = IFNULL(:nf, nf),
|
||||||
memo = IFNULL(:memo, memo),
|
memo = IFNULL(:memo, memo),
|
||||||
is_change = IFNULL(:is_change, is_change),
|
is_change = IFNULL(:is_change, is_change),
|
||||||
spent = IFNULL(:spent, spent),
|
|
||||||
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position),
|
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position),
|
||||||
recipient_key_scope = :recipient_key_scope",
|
recipient_key_scope = :recipient_key_scope
|
||||||
|
RETURNING sapling_received_notes.id",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let rcm = output.note().rcm().to_repr();
|
let rcm = output.note().rcm().to_repr();
|
||||||
|
@ -340,15 +351,26 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
|
||||||
":nf": output.nullifier().map(|nf| nf.0.as_ref()),
|
":nf": output.nullifier().map(|nf| nf.0.as_ref()),
|
||||||
":memo": memo_repr(output.memo()),
|
":memo": memo_repr(output.memo()),
|
||||||
":is_change": output.is_change(),
|
":is_change": output.is_change(),
|
||||||
":spent": spent_in,
|
|
||||||
":commitment_tree_position": output.note_commitment_tree_position().map(u64::from),
|
":commitment_tree_position": output.note_commitment_tree_position().map(u64::from),
|
||||||
":recipient_key_scope": output.recipient_key_scope().map(scope_code)
|
":recipient_key_scope": output.recipient_key_scope().map(scope_code)
|
||||||
];
|
];
|
||||||
|
|
||||||
stmt_upsert_received_note
|
let received_note_id = stmt_upsert_received_note
|
||||||
.execute(sql_args)
|
.query_row(sql_args, |row| row.get::<_, i64>(0))
|
||||||
.map_err(SqliteClientError::from)?;
|
.map_err(SqliteClientError::from)?;
|
||||||
|
|
||||||
|
if let Some(spent_in) = spent_in {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO sapling_received_note_spends (sapling_received_note_id, transaction_id)
|
||||||
|
VALUES (:sapling_received_note_id, :transaction_id)
|
||||||
|
ON CONFLICT (sapling_received_note_id, transaction_id) DO NOTHING",
|
||||||
|
named_params![
|
||||||
|
":sapling_received_note_id": received_note_id,
|
||||||
|
":transaction_id": spent_in
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue