1use bitcoin::secp256k1::Secp256k1;
31use bitcoin::{Transaction, TxOut};
32
33use crate::detected::DetectedOutput;
34use crate::error::ReceiveError;
35use crate::label::LabelManager;
36use silent_payments_core::keys::{ScanSecretKey, SpendPublicKey};
37
38pub struct BlockScanner {
47 scan_secret: ScanSecretKey,
48 spend_pubkey: SpendPublicKey,
49 labels: LabelManager,
50}
51
52impl BlockScanner {
53 pub fn new(
55 scan_secret: ScanSecretKey,
56 spend_pubkey: SpendPublicKey,
57 labels: LabelManager,
58 ) -> Self {
59 Self {
60 scan_secret,
61 spend_pubkey,
62 labels,
63 }
64 }
65
66 pub fn scan_transaction(
84 &self,
85 tx: &Transaction,
86 prevouts: &[TxOut],
87 secp: &Secp256k1<bitcoin::secp256k1::All>,
88 ) -> Result<Vec<DetectedOutput>, ReceiveError> {
89 let has_p2tr = tx.output.iter().any(|out| out.script_pubkey.is_p2tr());
91 if !has_p2tr {
92 return Ok(Vec::new());
93 }
94
95 let tweak_data = match bdk_sp::receive::compute_tweak_data(tx, prevouts) {
98 Ok(td) => td,
99 Err(_) => return Ok(Vec::new()),
100 };
101
102 let ecdh_shared_secret =
104 bdk_sp::compute_shared_secret(self.scan_secret.as_inner(), &tweak_data);
105
106 let labels_btree = self.labels.to_btree();
108
109 let sp_outs = bdk_sp::receive::scan_txouts(
111 *self.spend_pubkey.as_inner(),
112 &labels_btree,
113 tx,
114 ecdh_shared_secret,
115 )
116 .map_err(|e| ReceiveError::Scanning(e.to_string()))?;
117
118 let detected = sp_outs
120 .into_iter()
121 .map(|sp_out| {
122 DetectedOutput::new(
123 sp_out.outpoint,
124 sp_out.amount,
125 sp_out.script_pubkey,
126 sp_out.tweak,
127 sp_out.label,
128 )
129 })
130 .collect();
131
132 let _ = secp; Ok(detected)
139 }
140
141 pub fn scan_secret(&self) -> &ScanSecretKey {
143 &self.scan_secret
144 }
145
146 pub fn spend_pubkey(&self) -> &SpendPublicKey {
148 &self.spend_pubkey
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use bitcoin::secp256k1::{Secp256k1, SecretKey};
156 use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
157 use silent_payments_core::keys::{ScanSecretKey, SpendSecretKey};
158
159 const SCAN_SK_BYTES: [u8; 32] = [
160 0xea, 0xdc, 0x78, 0x16, 0x5f, 0xf1, 0xf8, 0xea, 0x94, 0xad, 0x7c, 0xfd, 0xc5, 0x49, 0x90,
161 0x73, 0x8a, 0x4c, 0x53, 0xf6, 0xe0, 0x50, 0x7b, 0x42, 0x15, 0x42, 0x01, 0xb8, 0xe5, 0xdf,
162 0xf3, 0xb1,
163 ];
164
165 const SPEND_SK_BYTES: [u8; 32] = [
166 0x93, 0xf5, 0xed, 0x90, 0x7a, 0xd5, 0xb2, 0xbd, 0xbb, 0xdc, 0xb5, 0xd9, 0x11, 0x6e, 0xbc,
167 0x0a, 0x4e, 0x1f, 0x92, 0xf9, 0x10, 0xd5, 0x26, 0x02, 0x37, 0xfa, 0x45, 0xa9, 0x40, 0x8a,
168 0xad, 0x16,
169 ];
170
171 fn test_scanner() -> BlockScanner {
172 let secp = Secp256k1::new();
173 let scan_sk = ScanSecretKey::from_slice(&SCAN_SK_BYTES).unwrap();
174 let spend_sk = SpendSecretKey::from_slice(&SPEND_SK_BYTES).unwrap();
175 let spend_pk = spend_sk.public_key(&secp);
176 BlockScanner::new(scan_sk, spend_pk, LabelManager::new())
177 }
178
179 #[test]
180 fn test_no_p2tr_outputs_returns_empty() {
181 let secp = Secp256k1::new();
182 let scanner = test_scanner();
183
184 let tx = Transaction {
186 version: bitcoin::transaction::Version::TWO,
187 lock_time: bitcoin::absolute::LockTime::ZERO,
188 input: vec![TxIn {
189 previous_output: OutPoint::null(),
190 script_sig: ScriptBuf::new(),
191 sequence: Sequence::MAX,
192 witness: Witness::new(),
193 }],
194 output: vec![TxOut {
195 value: Amount::from_sat(50_000),
196 script_pubkey: ScriptBuf::from_hex("001453d9c40342ee880e766522c3e2b854d37f2b3cbf")
198 .unwrap(),
199 }],
200 };
201
202 let prevouts = vec![TxOut {
203 value: Amount::from_sat(100_000),
204 script_pubkey: ScriptBuf::from_hex("001453d9c40342ee880e766522c3e2b854d37f2b3cbf")
205 .unwrap(),
206 }];
207
208 let result = scanner.scan_transaction(&tx, &prevouts, &secp).unwrap();
209 assert!(result.is_empty(), "non-P2TR tx should return empty vec");
210 }
211
212 #[test]
213 fn test_no_eligible_inputs_returns_empty() {
214 let secp = Secp256k1::new();
215 let scanner = test_scanner();
216
217 let dummy_sk = SecretKey::from_slice(&[0x42; 32]).unwrap();
219 let (xonly, _) = dummy_sk.public_key(&secp).x_only_public_key();
220 let tweaked = bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(xonly);
221 let p2tr_script = ScriptBuf::new_p2tr_tweaked(tweaked);
222
223 let tx = Transaction {
224 version: bitcoin::transaction::Version::TWO,
225 lock_time: bitcoin::absolute::LockTime::ZERO,
226 input: vec![TxIn {
227 previous_output: OutPoint::null(),
228 script_sig: ScriptBuf::new(),
229 sequence: Sequence::MAX,
230 witness: Witness::new(),
231 }],
232 output: vec![TxOut {
233 value: Amount::from_sat(50_000),
234 script_pubkey: p2tr_script,
235 }],
236 };
237
238 let prevouts = vec![TxOut {
240 value: Amount::from_sat(100_000),
241 script_pubkey: ScriptBuf::from_hex(
243 "0020000000000000000000000000000000000000000000000000000000000000dead",
244 )
245 .unwrap(),
246 }];
247
248 let result = scanner.scan_transaction(&tx, &prevouts, &secp).unwrap();
249 assert!(
250 result.is_empty(),
251 "tx with no eligible inputs should return empty vec"
252 );
253 }
254
255 #[test]
256 fn accessors_return_correct_references() {
257 let scanner = test_scanner();
258 let _scan = scanner.scan_secret();
260 let _spend = scanner.spend_pubkey();
261 }
262}