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(
101 deposit_raw: &[u8],
102 key: &PrivateKey,
103 lock_time: u32,
104) -> Result<ClaimPlan, String> {
105 let dep = parse_transaction(deposit_raw).map_err(|e| format!("parse deposit tx: {e}"))?;
106 let cov_out = dep
107 .outputs
108 .get(COVENANT_VOUT)
109 .ok_or("deposit has no vout 0")?;
110 let fee_out = dep
111 .outputs
112 .get(FEE_VOUT)
113 .ok_or("deposit has no vout 1 (recipient fee utxo)")?;
114
115 let params = parse_locking_script(&cov_out.script)?;
116 if cov_out.satoshis != params.amount {
117 return Err(format!(
118 "covenant output value {} != pinned amount {}",
119 cov_out.satoshis, params.amount
120 ));
121 }
122 let our_pub = key.public_key().to_compressed();
123 if our_pub.as_slice() != params.recipient.as_slice() {
124 return Err("this gift is not locked to your key".into());
125 }
126
127 let pay = p2pkh_script(&hash160(¶ms.recipient));
128 let dep_txid_internal = sha256d(deposit_raw);
129
130 let mut inputs = vec![
131 TxInput {
132 txid: dep_txid_internal,
133 output_index: COVENANT_VOUT as u32,
134 script: vec![],
135 sequence: NON_FINAL_SEQUENCE,
136 },
137 TxInput {
138 txid: dep_txid_internal,
139 output_index: FEE_VOUT as u32,
140 script: vec![],
141 sequence: NON_FINAL_SEQUENCE,
142 },
143 ];
144 let mut outputs = vec![
145 TxOutput {
147 satoshis: params.amount,
148 script: pay.clone(),
149 },
150 TxOutput {
152 satoshis: fee_out.satoshis,
153 script: pay.clone(),
154 },
155 ];
156
157 let cov_scope = SIGHASH_ANYONECANPAY | SIGHASH_SINGLE | SIGHASH_FORKID; let (preimage, cov_sighash) = {
160 let p = SighashParams {
161 version: TX_VERSION as i32,
162 inputs: &inputs,
163 outputs: &outputs,
164 locktime: lock_time,
165 input_index: COVENANT_VOUT,
166 subscript: &cov_out.script,
167 satoshis: params.amount,
168 scope: cov_scope,
169 };
170 (build_sighash_preimage(&p), compute_sighash_for_signing(&p))
171 };
172 let cov_sig = key
173 .sign(&cov_sighash)
174 .map_err(|e| format!("covenant sign: {e}"))?;
175 let cov_txsig = TransactionSignature::new(cov_sig, cov_scope).to_low_s();
176 let mut cov_unlock = push_bytes(&cov_txsig.to_checksig_format());
177 cov_unlock.extend_from_slice(&push_bytes(&preimage));
178 inputs[0].script = cov_unlock;
179
180 inputs[1].script = vec![0u8; 108];
182 let est = serialize_tx(TX_VERSION, &inputs, &outputs, lock_time).len() as u64;
183 inputs[1].script = vec![];
184 let fee = (est * FEE_RATE_SAT_PER_KB).div_ceil(1000) + 1;
188 if fee_out.satoshis <= fee {
189 return Err(format!(
190 "fee utxo {} is too small for the ~{fee}-sat claim fee",
191 fee_out.satoshis
192 ));
193 }
194 let change = fee_out.satoshis - fee;
195 if change < 1 {
196 outputs.pop(); } else {
198 outputs[1].satoshis = change;
199 }
200
201 let fee_scope = SIGHASH_ALL | SIGHASH_FORKID; let fee_sighash = {
204 let p = SighashParams {
205 version: TX_VERSION as i32,
206 inputs: &inputs,
207 outputs: &outputs,
208 locktime: lock_time,
209 input_index: FEE_VOUT,
210 subscript: &fee_out.script,
211 satoshis: fee_out.satoshis,
212 scope: fee_scope,
213 };
214 compute_sighash_for_signing(&p)
215 };
216 let fee_sig = key
217 .sign(&fee_sighash)
218 .map_err(|e| format!("fee sign: {e}"))?;
219 let fee_txsig = TransactionSignature::new(fee_sig, fee_scope).to_low_s();
220 let mut fee_unlock = push_bytes(&fee_txsig.to_checksig_format());
221 fee_unlock.extend_from_slice(&push_bytes(&our_pub));
222 inputs[1].script = fee_unlock;
223
224 let raw = serialize_tx(TX_VERSION, &inputs, &outputs, lock_time);
225 let claim_txid = txid_display(&raw);
226
227 Ok(ClaimPlan {
228 deposit_txid: txid_display(deposit_raw),
229 fee_utxo_sats: fee_out.satoshis,
230 fee,
231 change: outputs.get(1).map(|o| o.satoshis).unwrap_or(0),
232 claim_raw_hex: to_hex(&raw),
233 claim_txid,
234 covenant: params,
235 })
236}