1use serde::{Deserialize, Serialize};
6
7use crate::encode::{put_bytes, put_fixed, put_u32, put_u64};
8use crate::hash::{blake2b_256, Hash32};
9use crate::keys::Address;
10
11#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
13pub struct OutPoint {
14 pub txid: Hash32,
15 pub vout: u32,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
19pub struct TxInput {
20 pub prev: OutPoint,
21 pub pubkey: Vec<u8>,
23 pub signature: Vec<u8>,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
28pub struct TxOutput {
29 pub recipient: Address,
30 pub amount: u64,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
40pub struct ShieldedSpend {
41 pub anchor: Hash32,
43 pub nullifier: Hash32,
44 pub value: u64,
45 pub proof: Vec<u8>,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
52pub struct ShieldedOutput {
53 pub commitment: Hash32,
54 pub value: u64,
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
62pub struct ShieldedTransfer {
63 pub anchor: Hash32,
64 pub nullifier: Hash32,
65 pub commitment1: Hash32,
66 pub commitment2: Hash32,
67 pub fee: u64,
68 pub memo: Vec<u8>,
69 pub proof: Vec<u8>,
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
73pub struct ShieldedBundle {
74 pub spends: Vec<ShieldedSpend>,
75 pub outputs: Vec<ShieldedOutput>,
76 #[serde(default)]
78 pub transfers: Vec<ShieldedTransfer>,
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
85pub enum ContractAction {
86 Deploy { code: Vec<u8>, gas_limit: u64 },
89 Call { contract: Hash32, input: Vec<u8>, value: u64, gas_limit: u64 },
93 PoolCreate { token: Hash32, block_amt: u64, token_amt: u64, gas_limit: u64 },
97 PoolAdd { token: Hash32, block_amt: u64, token_max: u64, gas_limit: u64 },
100 PoolRemove { token: Hash32, shares: u64, gas_limit: u64 },
103 PoolSwapBuy { token: Hash32, block_in: u64, min_token_out: u64, gas_limit: u64 },
106 PoolSwapSell { token: Hash32, token_in: u64, min_block_out: u64, gas_limit: u64 },
109 LaunchToken { code: Vec<u8>, block_amt: u64, token_amt: u64, gas_limit: u64 },
116 LaunchNft { code: Vec<u8>, gas_limit: u64 },
121}
122
123impl ContractAction {
124 pub fn gas_limit(&self) -> u64 {
125 match self {
126 ContractAction::Deploy { gas_limit, .. } => *gas_limit,
127 ContractAction::Call { gas_limit, .. } => *gas_limit,
128 ContractAction::PoolCreate { gas_limit, .. } => *gas_limit,
129 ContractAction::PoolAdd { gas_limit, .. } => *gas_limit,
130 ContractAction::PoolRemove { gas_limit, .. } => *gas_limit,
131 ContractAction::PoolSwapBuy { gas_limit, .. } => *gas_limit,
132 ContractAction::PoolSwapSell { gas_limit, .. } => *gas_limit,
133 ContractAction::LaunchToken { gas_limit, .. } => *gas_limit,
134 ContractAction::LaunchNft { gas_limit, .. } => *gas_limit,
135 }
136 }
137
138 pub fn value(&self) -> u64 {
140 match self {
141 ContractAction::Deploy { .. } => 0,
142 ContractAction::Call { value, .. } => *value,
143 ContractAction::PoolCreate { block_amt, .. } => *block_amt,
144 ContractAction::PoolAdd { block_amt, .. } => *block_amt,
145 ContractAction::PoolRemove { .. } => 0,
146 ContractAction::PoolSwapBuy { block_in, .. } => *block_in,
147 ContractAction::PoolSwapSell { .. } => 0,
148 ContractAction::LaunchToken { block_amt, .. } => *block_amt,
149 ContractAction::LaunchNft { .. } => 0,
150 }
151 }
152}
153
154#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
155pub struct Transaction {
156 pub version: u32,
157 pub inputs: Vec<TxInput>,
159 pub outputs: Vec<TxOutput>,
160 pub coinbase_data: Vec<u8>,
163 pub shielded: Option<ShieldedBundle>,
164 #[serde(default)]
166 pub contract: Option<ContractAction>,
167}
168
169impl Transaction {
170 pub fn is_coinbase(&self) -> bool {
173 self.inputs.is_empty() && self.shielded.is_none()
174 }
175
176 pub fn shielded_in(&self) -> Option<u64> {
178 self.shielded.as_ref().map_or(Some(0), |b| {
179 b.spends.iter().try_fold(0u64, |acc, s| acc.checked_add(s.value))
180 })
181 }
182
183 pub fn shielded_out(&self) -> Option<u64> {
185 self.shielded.as_ref().map_or(Some(0), |b| {
186 b.outputs.iter().try_fold(0u64, |acc, o| acc.checked_add(o.value))
187 })
188 }
189
190 pub fn transfer_fees(&self) -> Option<u64> {
192 self.shielded.as_ref().map_or(Some(0), |b| {
193 b.transfers.iter().try_fold(0u64, |acc, t| acc.checked_add(t.fee))
194 })
195 }
196
197 pub fn nullifiers(&self) -> Vec<Hash32> {
199 self.shielded
200 .as_ref()
201 .map(|b| {
202 b.spends
203 .iter()
204 .map(|s| s.nullifier)
205 .chain(b.transfers.iter().map(|t| t.nullifier))
206 .collect()
207 })
208 .unwrap_or_default()
209 }
210
211 pub fn encode(&self, for_sighash: bool) -> Vec<u8> {
214 let mut out = Vec::new();
215 put_u32(&mut out, self.version);
216 put_u32(&mut out, self.inputs.len() as u32);
217 for input in &self.inputs {
218 put_fixed(&mut out, &input.prev.txid);
219 put_u32(&mut out, input.prev.vout);
220 put_bytes(&mut out, &input.pubkey);
221 if for_sighash {
222 put_bytes(&mut out, &[]);
223 } else {
224 put_bytes(&mut out, &input.signature);
225 }
226 }
227 put_u32(&mut out, self.outputs.len() as u32);
228 for output in &self.outputs {
229 put_fixed(&mut out, &output.recipient);
230 put_u64(&mut out, output.amount);
231 }
232 put_bytes(&mut out, &self.coinbase_data);
233 match &self.shielded {
234 None => put_u32(&mut out, 0),
235 Some(s) => {
236 put_u32(&mut out, 1);
237 put_u32(&mut out, s.spends.len() as u32);
238 for spend in &s.spends {
239 put_fixed(&mut out, &spend.anchor);
240 put_fixed(&mut out, &spend.nullifier);
241 put_u64(&mut out, spend.value);
242 if for_sighash {
245 put_bytes(&mut out, &[]);
246 } else {
247 put_bytes(&mut out, &spend.proof);
248 }
249 }
250 put_u32(&mut out, s.outputs.len() as u32);
251 for output in &s.outputs {
252 put_fixed(&mut out, &output.commitment);
253 put_u64(&mut out, output.value);
254 }
255 put_u32(&mut out, s.transfers.len() as u32);
256 for t in &s.transfers {
257 put_fixed(&mut out, &t.anchor);
258 put_fixed(&mut out, &t.nullifier);
259 put_fixed(&mut out, &t.commitment1);
260 put_fixed(&mut out, &t.commitment2);
261 put_u64(&mut out, t.fee);
262 put_bytes(&mut out, &t.memo);
263 if for_sighash {
264 put_bytes(&mut out, &[]);
265 } else {
266 put_bytes(&mut out, &t.proof);
267 }
268 }
269 }
270 }
271 match &self.contract {
272 None => put_u32(&mut out, 0),
273 Some(ContractAction::Deploy { code, gas_limit }) => {
274 put_u32(&mut out, 1);
275 put_bytes(&mut out, code);
276 put_u64(&mut out, *gas_limit);
277 }
278 Some(ContractAction::Call { contract, input, value, gas_limit }) => {
279 put_u32(&mut out, 2);
280 put_fixed(&mut out, contract);
281 put_bytes(&mut out, input);
282 put_u64(&mut out, *value);
283 put_u64(&mut out, *gas_limit);
284 }
285 Some(ContractAction::PoolCreate { token, block_amt, token_amt, gas_limit }) => {
286 put_u32(&mut out, 3);
287 put_fixed(&mut out, token);
288 put_u64(&mut out, *block_amt);
289 put_u64(&mut out, *token_amt);
290 put_u64(&mut out, *gas_limit);
291 }
292 Some(ContractAction::PoolAdd { token, block_amt, token_max, gas_limit }) => {
293 put_u32(&mut out, 4);
294 put_fixed(&mut out, token);
295 put_u64(&mut out, *block_amt);
296 put_u64(&mut out, *token_max);
297 put_u64(&mut out, *gas_limit);
298 }
299 Some(ContractAction::PoolRemove { token, shares, gas_limit }) => {
300 put_u32(&mut out, 5);
301 put_fixed(&mut out, token);
302 put_u64(&mut out, *shares);
303 put_u64(&mut out, *gas_limit);
304 }
305 Some(ContractAction::PoolSwapBuy { token, block_in, min_token_out, gas_limit }) => {
306 put_u32(&mut out, 6);
307 put_fixed(&mut out, token);
308 put_u64(&mut out, *block_in);
309 put_u64(&mut out, *min_token_out);
310 put_u64(&mut out, *gas_limit);
311 }
312 Some(ContractAction::PoolSwapSell { token, token_in, min_block_out, gas_limit }) => {
313 put_u32(&mut out, 7);
314 put_fixed(&mut out, token);
315 put_u64(&mut out, *token_in);
316 put_u64(&mut out, *min_block_out);
317 put_u64(&mut out, *gas_limit);
318 }
319 Some(ContractAction::LaunchToken { code, block_amt, token_amt, gas_limit }) => {
320 put_u32(&mut out, 8);
321 put_bytes(&mut out, code);
322 put_u64(&mut out, *block_amt);
323 put_u64(&mut out, *token_amt);
324 put_u64(&mut out, *gas_limit);
325 }
326 Some(ContractAction::LaunchNft { code, gas_limit }) => {
327 put_u32(&mut out, 9);
328 put_bytes(&mut out, code);
329 put_u64(&mut out, *gas_limit);
330 }
331 }
332 out
333 }
334
335 pub fn txid(&self) -> Hash32 {
336 blake2b_256(&self.encode(false))
337 }
338
339 pub fn sighash(&self) -> Hash32 {
341 blake2b_256(&self.encode(true))
342 }
343
344 pub fn total_output(&self) -> Option<u64> {
345 self.outputs
346 .iter()
347 .try_fold(0u64, |acc, o| acc.checked_add(o.amount))
348 }
349
350 pub fn serialized_size(&self) -> usize {
351 self.encode(false).len()
352 }
353
354 pub fn coinbase(height: u64, recipient: Address, amount: u64, tag: &[u8]) -> Self {
356 let mut coinbase_data = height.to_le_bytes().to_vec();
357 coinbase_data.extend_from_slice(tag);
358 Transaction {
359 version: 1,
360 inputs: vec![],
361 outputs: vec![TxOutput { recipient, amount }],
362 coinbase_data,
363 shielded: None,
364 contract: None,
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::keys::Keypair;
373
374 #[test]
375 fn txid_changes_with_content_sighash_ignores_sigs() {
376 let kp = Keypair::generate();
377 let mut tx = Transaction {
378 version: 1,
379 inputs: vec![TxInput {
380 prev: OutPoint { txid: [1; 32], vout: 0 },
381 pubkey: kp.public_bytes(),
382 signature: vec![],
383 }],
384 outputs: vec![TxOutput { recipient: kp.address(), amount: 42 }],
385 coinbase_data: vec![],
386 shielded: None,
387 contract: None,
388 };
389 let sighash_before = tx.sighash();
390 let txid_before = tx.txid();
391 tx.inputs[0].signature = kp.sign(&sighash_before).to_vec();
392 assert_eq!(tx.sighash(), sighash_before, "sighash must ignore signatures");
393 assert_ne!(tx.txid(), txid_before, "txid commits to signatures");
394
395 tx.outputs[0].amount = 43;
396 assert_ne!(tx.sighash(), sighash_before);
397 }
398
399 #[test]
400 fn coinbase_txids_unique_per_height() {
401 let addr = [9u8; 32];
402 let a = Transaction::coinbase(1, addr, 50, b"blockle");
403 let b = Transaction::coinbase(2, addr, 50, b"blockle");
404 assert!(a.is_coinbase());
405 assert_ne!(a.txid(), b.txid());
406 }
407}