1use bitcoin::opcodes::all::{OP_CHECKMULTISIG, OP_PUSHNUM_1, OP_PUSHNUM_16};
2use bitcoin::script::Instruction;
3use bitcoin::{Opcode, PublicKey, Script, WitnessVersion};
4
5#[derive(Debug, PartialEq, Eq, Clone)]
7pub enum TxoutType {
8 NonStandard,
9 Anchor,
11 PubKey(PublicKey),
12 PubKeyHash([u8; 20]),
13 ScriptHash([u8; 20]),
14 Multisig {
15 required_sigs: u8,
16 keys_count: u8,
17 keys: Vec<Vec<u8>>,
18 },
19 NullData,
21 WitnessV0ScriptHash([u8; 32]),
22 WitnessV0KeyHash([u8; 20]),
23 WitnessV1Taproot([u8; 32]),
24 WitnessUnknown(Vec<Vec<u8>>),
26}
27
28impl TxoutType {
29 pub fn script_type(&self) -> &'static str {
31 match self {
32 Self::NonStandard => "nonstandard",
33 Self::Anchor => "anchor",
34 Self::PubKey(_) => "pubkey",
35 Self::PubKeyHash(_) => "pubkeyhash",
36 Self::ScriptHash(_) => "scripthash",
37 Self::Multisig { .. } => "multisig",
38 Self::NullData => "nulldata",
39 Self::WitnessV0ScriptHash(_) => "witness_v0_scripthash",
40 Self::WitnessV0KeyHash(_) => "witness_v0_keyhash",
41 Self::WitnessV1Taproot(_) => "witness_v1_taproot",
42 Self::WitnessUnknown(_) => "witness_unknown",
43 }
44 }
45}
46
47pub fn solve(script_pubkey: &Script) -> TxoutType {
48 if script_pubkey.is_p2sh() {
49 let hash: [u8; 20] = script_pubkey.as_bytes()[2..22].try_into().unwrap();
50 return TxoutType::ScriptHash(hash);
51 }
52
53 if let Some(version) = script_pubkey.witness_version() {
54 let program = &script_pubkey.as_bytes()[2..];
55
56 match (version, program.len()) {
57 (WitnessVersion::V0, 20) => {
58 return TxoutType::WitnessV0KeyHash(program.try_into().unwrap());
59 }
60 (WitnessVersion::V0, 32) => {
61 return TxoutType::WitnessV0ScriptHash(program.try_into().unwrap());
62 }
63 (WitnessVersion::V1, 32) => {
64 return TxoutType::WitnessV1Taproot(program.try_into().unwrap());
65 }
66 _ => {}
67 }
68
69 if is_pay_to_anchor(script_pubkey) {
70 return TxoutType::Anchor;
71 }
72
73 if version != WitnessVersion::V0 {
74 return TxoutType::WitnessUnknown(vec![vec![version as u8], program.to_vec()]);
75 }
76
77 return TxoutType::NonStandard;
78 }
79
80 if script_pubkey.is_op_return() {
82 return TxoutType::NullData;
83 }
84
85 if let Some(pubkey) = script_pubkey.p2pk_public_key() {
86 return TxoutType::PubKey(pubkey);
87 }
88
89 if script_pubkey.is_p2pkh() {
90 return TxoutType::PubKeyHash(script_pubkey.as_bytes()[3..23].try_into().unwrap());
91 }
92
93 if let Some((required_sigs, keys_count, keys)) = match_multisig(script_pubkey) {
94 return TxoutType::Multisig {
95 required_sigs,
96 keys_count,
97 keys,
98 };
99 }
100
101 TxoutType::NonStandard
102}
103
104fn is_pay_to_anchor(script: &Script) -> bool {
105 let script_bytes = script.as_bytes();
106 script.len() == 4
107 && script_bytes[0] == OP_PUSHNUM_1.to_u8()
108 && script_bytes[1] == 0x02
109 && script_bytes[2] == 0x4e
110 && script_bytes[3] == 0x73
111}
112
113#[inline]
120fn match_multisig(script_pubkey: &Script) -> Option<(u8, u8, Vec<Vec<u8>>)> {
121 let mut instructions = script_pubkey.instructions();
122
123 let required_sigs = if let Ok(Instruction::Op(op)) = instructions.next()? {
124 decode_pushnum(op)?
125 } else {
126 return None;
127 };
128
129 let mut keys = Vec::new();
130 while let Some(Ok(instruction)) = instructions.next() {
131 match instruction {
132 Instruction::PushBytes(key) => {
133 keys.push(key.as_bytes().to_vec());
134 }
135 Instruction::Op(op) => {
136 if let Some(pushnum) = decode_pushnum(op) {
137 if pushnum as usize != keys.len() {
138 return None;
139 }
140 }
141 break;
142 }
143 }
144 }
145
146 let num_pubkeys: u8 = keys
147 .len()
148 .try_into()
149 .expect("Must fit into u8 due to the MAX_PUBKEYS_PER_MULTISIG limit; qed");
150
151 if required_sigs > num_pubkeys {
152 return None;
153 }
154
155 if let Ok(Instruction::Op(op)) = instructions.next()? {
156 if op != OP_CHECKMULTISIG {
157 return None;
158 }
159 }
160
161 if instructions.next().is_none() {
162 Some((required_sigs, num_pubkeys, keys))
163 } else {
164 None
165 }
166}
167
168fn decode_pushnum(opcode: Opcode) -> Option<u8> {
169 if (OP_PUSHNUM_1.to_u8()..=OP_PUSHNUM_16.to_u8()).contains(&opcode.to_u8()) {
170 Some(opcode.to_u8() - 0x50)
171 } else {
172 None
173 }
174}