Line data Source code
1 : import 'dart:typed_data';
2 : import 'package:bs58check/bs58check.dart' as bs58check;
3 : import 'package:convert/convert.dart';
4 : import 'package:dart_bech32/dart_bech32.dart';
5 : import 'package:walletkit_dart/src/crypto/utxo/entities/op_codes.dart';
6 : import 'package:walletkit_dart/src/crypto/utxo/utils/pubkey_to_address.dart';
7 : import 'package:walletkit_dart/src/domain/exceptions.dart';
8 : import 'package:walletkit_dart/src/utils/base32.dart';
9 : import 'package:walletkit_dart/walletkit_dart.dart';
10 :
11 : class P2Hash {
12 : final String address;
13 :
14 11 : const P2Hash(this.address);
15 :
16 45 : String get publicKeyScriptHash => sha256Hash(publicKeyScript).rev.toHex;
17 :
18 11 : Uint8List get publicKeyScript {
19 22 : if (address.startsWith(P2PKH_PREFIX) ||
20 22 : address.startsWith(P2PKH_PREFIX_LTC) ||
21 22 : address.startsWith(P2PKH_PREFIX_ZENIQ) ||
22 18 : address.startsWith(P2PKH_PREFIX_EC8)) {
23 10 : return p2pkhScript;
24 : }
25 :
26 16 : if (address.startsWith(P2SH_PREFIX) ||
27 16 : address.startsWith(P2SH_PREFIX_LTC)) {
28 2 : return p2shScript;
29 : }
30 :
31 16 : if (address.startsWith(P2WPKH_PREFIX_BTC) ||
32 14 : address.startsWith(P2WPKH_PREFIX_LTC)) {
33 7 : return p2wpkhScript;
34 : }
35 :
36 : /// Remove the prefix
37 8 : if (address.startsWith("bitcoincash:")) {
38 4 : return p2pkhScriptBCH;
39 : }
40 :
41 3 : throw UnsupportedError("Address type not supported: $address");
42 : }
43 :
44 : ///
45 : /// P2PKH
46 : ///
47 :
48 10 : Uint8List get p2pkhScript {
49 30 : final decodedHex = bs58check.decode(address).toHex;
50 10 : final pubKeyHash = decodedHex.substring(2);
51 :
52 20 : if (pubKeyHash.length != 40) {
53 0 : throw Failure("wrong pubKeyHash length");
54 : }
55 : // see https://en.bitcoinwiki.org/wiki/Pay-to-Pubkey_Hash for P2PKH
56 : final scriptP2PKH =
57 10 : "${OPCODE.OP_DUP}${OPCODE.OP_HASH160}$PUBKEY_SCRIPT_HASH_LENGTH_HEX$pubKeyHash${OPCODE.OP_EQUALVERIFY}${OPCODE.OP_CHECKSIG}";
58 :
59 10 : return scriptP2PKH.hexToBytes;
60 : }
61 :
62 : ///
63 : /// P2WPKH
64 : ///
65 :
66 7 : Uint8List get p2wpkhScript {
67 14 : final decoded = bech32.decode(address);
68 7 : var words = decoded.words;
69 : // Remove the witness version
70 7 : words = words.sublist(1);
71 : // Convert 5-bit words to 8-bit
72 : Uint8List convertedWords;
73 : try {
74 7 : convertedWords = bech32.fromWords(words);
75 : } catch (e) {
76 : convertedWords = words;
77 : }
78 :
79 : final scriptPubKeyHash =
80 14 : "${OPCODE.OP_0}$PUBKEY_SCRIPT_HASH_LENGTH_HEX${hex.encode(convertedWords)}";
81 :
82 7 : return scriptPubKeyHash.hexToBytes;
83 : }
84 :
85 : ///
86 : /// P2WPKH BCH
87 : ///
88 4 : Uint8List get p2pkhScriptBCH {
89 8 : final _address = address.substring(12); // remove "bitcoincash:"
90 :
91 8 : final payload = Base32().decode(_address);
92 :
93 : final payloadData =
94 16 : bech32.fromWords(payload.sublist(0, payload.length - 8));
95 :
96 4 : final version = payloadData[0];
97 4 : final pubKeyHash = payloadData.sublist(1);
98 :
99 8 : assert(version == 0);
100 12 : assert(pubKeyHash.length == 20);
101 :
102 4 : return [
103 4 : OPCODE.OP_DUP.hex,
104 4 : OPCODE.OP_HASH160.hex,
105 4 : pubKeyHash.length,
106 4 : ...pubKeyHash,
107 4 : OPCODE.OP_EQUALVERIFY.hex,
108 4 : OPCODE.OP_CHECKSIG.hex,
109 4 : ].toUint8List;
110 : }
111 :
112 : ///
113 : /// P2SH
114 : ///
115 :
116 2 : Uint8List get p2shScript {
117 : // Decode the Base58Check encoded P2SH address and extract the script hash
118 6 : final scriptHash = bs58check.decode(address).sublist(1);
119 :
120 2 : return [
121 2 : OPCODE.OP_HASH160.hex,
122 2 : scriptHash.length,
123 2 : ...scriptHash,
124 2 : OPCODE.OP_EQUAL.hex,
125 2 : ].toUint8List;
126 : }
127 :
128 : ///
129 : /// Utility functions
130 : ///
131 0 : static Uint8List toP2PKHScript(Uint8List segWitScript) {
132 0 : final pubkeyhash = segWitScript.sublist(2);
133 :
134 0 : return [
135 0 : OPCODE.OP_DUP.hex,
136 0 : OPCODE.OP_HASH160.hex,
137 0 : pubkeyhash.length,
138 0 : ...pubkeyhash,
139 0 : OPCODE.OP_EQUALVERIFY.hex,
140 0 : OPCODE.OP_CHECKSIG.hex,
141 0 : ].toUint8List;
142 : }
143 : }
144 :
145 : ///
146 : /// Returns the address from the locking script of a transaction output.
147 : ///
148 8 : String getAddressFromLockingScript(
149 : ElectrumScriptPubKey scriptPubKey,
150 : UTXONetworkType type, {
151 : AddressType? addressType,
152 : }) {
153 8 : final (pubKeyHash, walletType) = getPublicKeyFromLockingScript(
154 : scriptPubKey,
155 : type,
156 : );
157 :
158 : return switch (walletType) {
159 16 : HDWalletPurpose.BIP44 when addressType == AddressType.cashaddr =>
160 1 : bchAddrEncode(
161 1 : hrp: type.bech32,
162 : data: pubKeyHash,
163 1 : witnessVersion: type.pubKeyHashPrefix,
164 : ),
165 : HDWalletPurpose.BIP44 =>
166 14 : pubKeyHashToLegacyAddress(pubKeyHash, type.pubKeyHashPrefix),
167 4 : HDWalletPurpose.BIP49 =>
168 8 : pubKeyHashToP2SHAddress(pubKeyHash, type.scriptHashPrefix),
169 4 : HDWalletPurpose.BIP84 =>
170 12 : pubKeyHashToSegwitAddress(pubKeyHash, type.bech32, type.pubKeyHashPrefix),
171 0 : _ => throw UnsupportedError("Address type not supported: $pubKeyHash")
172 : };
173 : }
174 :
175 8 : (Uint8List, HDWalletPurpose) getPublicKeyFromLockingScript(
176 : ElectrumScriptPubKey scriptPubKey,
177 : UTXONetworkType type,
178 : ) {
179 8 : final hexKey = scriptPubKey.hexString;
180 :
181 : ///
182 : /// P2PKH
183 : ///
184 32 : if (hexKey.startsWith(p2pkhPrefix) && hexKey.endsWith(p2pkhPostfix)) {
185 8 : final pubKeyHashHex = hexKey.substring(
186 16 : p2pkhPrefix.length,
187 32 : hexKey.length - p2pkhPostfix.length,
188 : );
189 16 : final pubKeyHash = Uint8List.fromList(hex.decode(pubKeyHashHex));
190 : return (pubKeyHash, HDWalletPurpose.BIP44);
191 : }
192 :
193 : ///
194 : /// P2SH
195 : ///
196 16 : if (hexKey.startsWith(p2shPrefix) && hexKey.endsWith(p2shPostfix)) {
197 4 : final pubKeyHashHex = hexKey.substring(
198 8 : p2shPrefix.length,
199 16 : hexKey.length - p2shPostfix.length,
200 : );
201 8 : final pubKeyHash = Uint8List.fromList(hex.decode(pubKeyHashHex));
202 : return (pubKeyHash, HDWalletPurpose.BIP49);
203 : }
204 :
205 : ///
206 : /// P2WPKH
207 : ///
208 8 : if (hexKey.startsWith(p2wpkhPrefix)) {
209 4 : final pubKeyHashHex = hexKey.substring(
210 8 : p2wpkhPrefix.length,
211 4 : hexKey.length,
212 : );
213 8 : final pubKeyHash = Uint8List.fromList(hex.decode(pubKeyHashHex));
214 : return (pubKeyHash, HDWalletPurpose.BIP84);
215 : }
216 :
217 2 : throw UnsupportedError("Address type not supported: $hexKey");
218 : }
219 :
220 : ///
221 : /// Returns the address from the unlocking script of a transaction input. This only works for P2PKH and P2WPKH inputs.
222 : ///
223 8 : String getAddressFromInput(
224 : UTXONetworkType type,
225 : ElectrumInput input, {
226 : AddressType? addressType,
227 : }) {
228 8 : final (publicKey, pubKeyAddressType) = getPubKeyFromInput(input);
229 :
230 : ///
231 : /// Allow for overriding the address type (used for BCH)
232 : ///
233 : final _addressType = switch (addressType) {
234 9 : AddressType.cashaddr when pubKeyAddressType == AddressType.legacy =>
235 : AddressType.cashaddr,
236 : _ => pubKeyAddressType
237 : };
238 :
239 8 : return pubKeyToAddress(publicKey, _addressType, type);
240 : }
241 :
242 : ///
243 : /// Returns the PublicKey from the unlocking script of a transaction input. This only works for P2PKH and P2WPKH inputs.
244 : ///
245 8 : (Uint8List, AddressType) getPubKeyFromInput(
246 : ElectrumInput input,
247 : ) {
248 8 : final hexSig = input.scriptSig;
249 :
250 : ///
251 : /// Use ScriptSig (P2PKH)
252 : ///
253 8 : if (hexSig != null && hexSig.isNotEmpty) {
254 16 : if (hexSig.length < 68) {
255 8 : throw UnsupportedError("Address type not supported: $hexSig");
256 : }
257 24 : final redeemScriptHex = hexSig.substring(hexSig.length - 68);
258 8 : if (!redeemScriptHex.startsWith("21")) {
259 : // expect varInt 0x21 -> 33 byte pubkey
260 2 : throw UnsupportedError("Address type not supported: $hexSig");
261 : }
262 8 : final pubKeyHex = redeemScriptHex.substring(2);
263 16 : if (pubKeyHex.length != 66) {
264 0 : throw UnsupportedError("Address type not supported: $hexSig");
265 : }
266 8 : return (pubKeyHex.hexToBytes, AddressType.legacy);
267 : }
268 :
269 : ///
270 : /// Use ScriptWitness (P2WPKH)
271 : ///
272 4 : final witnessData = input.txinwitness;
273 4 : if (witnessData != null && witnessData.isNotEmpty) {
274 : // In P2WPKH witness data, the second item is the pubkey. It should start with either "02" or "03" (compressed pubkey).
275 4 : final pubKeyHex = witnessData[1];
276 :
277 8 : if (pubKeyHex.length != 66) {
278 0 : throw UnsupportedError("Address type not supported: $witnessData");
279 : }
280 :
281 4 : return (pubKeyHex.hexToBytes, AddressType.segwit);
282 : }
283 :
284 2 : throw UnsupportedError("Address type not supported: $hexSig");
285 : }
|