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