bsv_wallet_cli/gift/
claim.rs1use bsv_sdk::primitives::bsv::sighash::{
16 build_sighash_preimage, compute_sighash_for_signing, parse_transaction, SighashParams, TxInput,
17 TxOutput, SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_FORKID, SIGHASH_SINGLE,
18};
19use bsv_sdk::primitives::bsv::TransactionSignature;
20use bsv_sdk::primitives::{hash160, sha256d, to_hex, PrivateKey, Writer};
21
22use super::covenant::{parse_locking_script, CovenantParams};
23
24const TX_VERSION: u32 = 1;
25const COVENANT_VOUT: usize = 0;
26const FEE_VOUT: usize = 1;
27const NON_FINAL_SEQUENCE: u32 = 0xffff_fffe;
28const FEE_RATE_SAT_PER_KB: u64 = 101;
32
33pub struct ClaimPlan {
34 pub deposit_txid: String,
35 pub covenant: CovenantParams,
36 pub fee_utxo_sats: u64,
37 pub fee: u64,
38 pub change: u64,
39 pub claim_raw_hex: String,
40 pub claim_txid: String,
41}
42
43fn p2pkh_script(pkh: &[u8]) -> Vec<u8> {
44 let mut s = Vec::with_capacity(25);
45 s.extend_from_slice(&[0x76, 0xa9, 0x14]);
46 s.extend_from_slice(pkh);
47 s.extend_from_slice(&[0x88, 0xac]);
48 s
49}
50
51fn push_bytes(data: &[u8]) -> Vec<u8> {
53 let n = data.len();
54 let mut v = Vec::with_capacity(n + 4);
55 if n < 0x4c {
56 v.push(n as u8);
57 } else if n <= 0xff {
58 v.push(0x4c);
59 v.push(n as u8);
60 } else if n <= 0xffff {
61 v.push(0x4d);
62 v.extend_from_slice(&(n as u16).to_le_bytes());
63 } else {
64 v.push(0x4e);
65 v.extend_from_slice(&(n as u32).to_le_bytes());
66 }
67 v.extend_from_slice(data);
68 v
69}
70
71fn serialize_tx(version: u32, inputs: &[TxInput], outputs: &[TxOutput], locktime: u32) -> Vec<u8> {
72 let mut w = Writer::new();
73 w.write_u32_le(version);
74 w.write_var_int(inputs.len() as u64);
75 for inp in inputs {
76 w.write_bytes(&inp.txid);
77 w.write_u32_le(inp.output_index);
78 w.write_var_int(inp.script.len() as u64);
79 w.write_bytes(&inp.script);
80 w.write_u32_le(inp.sequence);
81 }
82 w.write_var_int(outputs.len() as u64);
83 for out in outputs {
84 w.write_u64_le(out.satoshis);
85 w.write_var_int(out.script.len() as u64);
86 w.write_bytes(&out.script);
87 }
88 w.write_u32_le(locktime);
89 w.into_bytes()
90}
91
92fn txid_display(raw: &[u8]) -> String {
93 let mut h = sha256d(raw);
94 h.reverse();
95 to_hex(&h)
96}
97
98pub fn build_claim_tx(deposit_raw: &[u8], key: &PrivateKey, lock_time: u32) -> Result<ClaimPlan, String> {
101 let dep = parse_transaction(deposit_raw).map_err(|e| format!("parse deposit tx: {e}"))?;
102 let cov_out = dep.outputs.get(COVENANT_VOUT).ok_or("deposit has no vout 0")?;
103 let fee_out = dep
104 .outputs
105 .get(FEE_VOUT)
106 .ok_or("deposit has no vout 1 (recipient fee utxo)")?;
107
108 let params = parse_locking_script(&cov_out.script)?;
109 if cov_out.satoshis != params.amount {
110 return Err(format!(
111 "covenant output value {} != pinned amount {}",
112 cov_out.satoshis, params.amount
113 ));
114 }
115 let our_pub = key.public_key().to_compressed();
116 if our_pub.as_slice() != params.recipient.as_slice() {
117 return Err("this gift is not locked to your key".into());
118 }
119
120 let pay = p2pkh_script(&hash160(¶ms.recipient));
121 let dep_txid_internal = sha256d(deposit_raw);
122
123 let mut inputs = vec![
124 TxInput {
125 txid: dep_txid_internal,
126 output_index: COVENANT_VOUT as u32,
127 script: vec![],
128 sequence: NON_FINAL_SEQUENCE,
129 },
130 TxInput {
131 txid: dep_txid_internal,
132 output_index: FEE_VOUT as u32,
133 script: vec![],
134 sequence: NON_FINAL_SEQUENCE,
135 },
136 ];
137 let mut outputs = vec![
138 TxOutput {
140 satoshis: params.amount,
141 script: pay.clone(),
142 },
143 TxOutput {
145 satoshis: fee_out.satoshis,
146 script: pay.clone(),
147 },
148 ];
149
150 let cov_scope = SIGHASH_ANYONECANPAY | SIGHASH_SINGLE | SIGHASH_FORKID; let (preimage, cov_sighash) = {
153 let p = SighashParams {
154 version: TX_VERSION as i32,
155 inputs: &inputs,
156 outputs: &outputs,
157 locktime: lock_time,
158 input_index: COVENANT_VOUT,
159 subscript: &cov_out.script,
160 satoshis: params.amount,
161 scope: cov_scope,
162 };
163 (build_sighash_preimage(&p), compute_sighash_for_signing(&p))
164 };
165 let cov_sig = key.sign(&cov_sighash).map_err(|e| format!("covenant sign: {e}"))?;
166 let cov_txsig = TransactionSignature::new(cov_sig, cov_scope).to_low_s();
167 let mut cov_unlock = push_bytes(&cov_txsig.to_checksig_format());
168 cov_unlock.extend_from_slice(&push_bytes(&preimage));
169 inputs[0].script = cov_unlock;
170
171 inputs[1].script = vec![0u8; 108];
173 let est = serialize_tx(TX_VERSION, &inputs, &outputs, lock_time).len() as u64;
174 inputs[1].script = vec![];
175 let fee = (est * FEE_RATE_SAT_PER_KB).div_ceil(1000) + 1;
179 if fee_out.satoshis <= fee {
180 return Err(format!(
181 "fee utxo {} is too small for the ~{fee}-sat claim fee",
182 fee_out.satoshis
183 ));
184 }
185 let change = fee_out.satoshis - fee;
186 if change < 1 {
187 outputs.pop(); } else {
189 outputs[1].satoshis = change;
190 }
191
192 let fee_scope = SIGHASH_ALL | SIGHASH_FORKID; let fee_sighash = {
195 let p = SighashParams {
196 version: TX_VERSION as i32,
197 inputs: &inputs,
198 outputs: &outputs,
199 locktime: lock_time,
200 input_index: FEE_VOUT,
201 subscript: &fee_out.script,
202 satoshis: fee_out.satoshis,
203 scope: fee_scope,
204 };
205 compute_sighash_for_signing(&p)
206 };
207 let fee_sig = key.sign(&fee_sighash).map_err(|e| format!("fee sign: {e}"))?;
208 let fee_txsig = TransactionSignature::new(fee_sig, fee_scope).to_low_s();
209 let mut fee_unlock = push_bytes(&fee_txsig.to_checksig_format());
210 fee_unlock.extend_from_slice(&push_bytes(&our_pub));
211 inputs[1].script = fee_unlock;
212
213 let raw = serialize_tx(TX_VERSION, &inputs, &outputs, lock_time);
214 let claim_txid = txid_display(&raw);
215
216 Ok(ClaimPlan {
217 deposit_txid: txid_display(deposit_raw),
218 fee_utxo_sats: fee_out.satoshis,
219 fee,
220 change: outputs.get(1).map(|o| o.satoshis).unwrap_or(0),
221 claim_raw_hex: to_hex(&raw),
222 claim_txid,
223 covenant: params,
224 })
225}