Line data Source code
1 : part of '../../../../domain/entities/generic_transaction.dart';
2 :
3 : const String ADDRESS_NOT_SUPPORTED = "Address not supported";
4 :
5 : final class UTXOTransaction extends GenericTransaction {
6 : final String id;
7 : final List<ElectrumInput> inputs;
8 : final List<ElectrumOutput> outputs;
9 : final int version;
10 :
11 9 : const UTXOTransaction({
12 : required super.block,
13 : required super.fee,
14 : required super.hash,
15 : required super.timeMilli,
16 : required super.amount,
17 : required super.sender,
18 : required super.recipient,
19 : required super.token,
20 : required super.transferMethod,
21 : required super.confirmations,
22 : required super.status,
23 : required this.inputs,
24 : required this.outputs,
25 : required this.id,
26 : required this.version,
27 : });
28 :
29 4 : UTXOTransaction copyWith({
30 : int? block,
31 : Amount? fee,
32 : String? hash,
33 : int? timeMilli,
34 : Amount? amount,
35 : String? sender,
36 : String? recipient,
37 : CoinEntity? token,
38 : TransactionTransferMethod? transferMethod,
39 : int? confirmations,
40 : List<ElectrumInput>? inputs,
41 : List<ElectrumOutput>? outputs,
42 : String? id,
43 : int? version,
44 : ConfirmationStatus? status,
45 : }) {
46 4 : return UTXOTransaction(
47 4 : block: block ?? this.block,
48 4 : fee: fee ?? this.fee,
49 4 : hash: hash ?? this.hash,
50 4 : timeMilli: timeMilli ?? this.timeMilli,
51 4 : amount: amount ?? this.amount,
52 0 : sender: sender ?? this.sender,
53 4 : recipient: recipient ?? this.recipient,
54 4 : token: token ?? this.token,
55 4 : transferMethod: transferMethod ?? this.transferMethod,
56 4 : confirmations: confirmations ?? this.confirmations,
57 4 : inputs: inputs ?? this.inputs,
58 4 : outputs: outputs ?? this.outputs,
59 4 : id: id ?? this.id,
60 4 : version: version ?? this.version,
61 4 : status: status ?? this.status,
62 : );
63 : }
64 :
65 8 : factory UTXOTransaction.create({
66 : required Map<String, dynamic> json,
67 : required UTXONetworkType type,
68 : required Iterable<AddressType> addressTypes,
69 : required Iterable<NodeWithAddress> nodes,
70 : required Iterable<ElectrumOutput> spentOutputs,
71 : }) {
72 8 : final coin = type.coin;
73 8 : final id = json['txid'] as String;
74 : //final hash = json['hash'] as String;
75 :
76 8 : final inputs = (json['vin'] as List<dynamic>)
77 24 : .map((e) => ElectrumInput.fromJson(e as Map<String, dynamic>))
78 8 : .toList();
79 :
80 8 : final rawOutputs = (json['vout'] as List<dynamic>)
81 24 : .map((e) => ElectrumOutput.fromJson(e as Map<String, dynamic>))
82 8 : .toList();
83 :
84 8 : final outputs = findOurOwnCoins(rawOutputs, nodes, addressTypes, type);
85 :
86 16 : final sender = inputs.first.getAddress(
87 : type,
88 8 : addressType: addressTypes.first,
89 : );
90 :
91 8 : final transferMethod = determineSendDirection(
92 : inputs: inputs,
93 : outputs: outputs,
94 : nodes: nodes,
95 : type: type,
96 : addressTypes: addressTypes,
97 : );
98 :
99 8 : final (value, totalOutputValue) = determineTransactionValue(
100 : outputs,
101 : transferMethod,
102 : nodes,
103 : type,
104 : );
105 :
106 8 : final totalInputValue = spentOutputs.fold(
107 8 : BigInt.zero,
108 9 : (prev, spentOutput) => prev + spentOutput.value,
109 : );
110 :
111 8 : final fee_int = json['fee_int'] as int?;
112 2 : var fee = fee_int != null ? BigInt.from(fee_int) : null;
113 9 : fee ??= spentOutputs.isEmpty ? null : totalInputValue - totalOutputValue;
114 :
115 8 : final recipient = determineTransactionTarget(
116 : outputs,
117 : transferMethod,
118 : type,
119 8 : addressTypes.first,
120 : ) ??
121 : ADDRESS_NOT_SUPPORTED;
122 :
123 : //final blockHash = json['blockhash'] as String;
124 :
125 16 : final timestamp = (json['time'] ?? -1) * 1000;
126 32 : final height = int.tryParse(json['height'].toString()) ?? -1;
127 24 : final confirmations = int.tryParse(json['confirmations'].toString()) ?? -1;
128 8 : final version = json['version'] as int;
129 :
130 8 : return UTXOTransaction(
131 : block: height,
132 8 : fee: fee != null ? Amount(value: fee, decimals: coin.decimals) : null,
133 : hash: id,
134 : timeMilli: timestamp,
135 16 : amount: Amount(value: value, decimals: coin.decimals),
136 : sender: sender,
137 : recipient: recipient,
138 : token: coin,
139 : transferMethod: transferMethod,
140 : confirmations: confirmations,
141 : inputs: inputs,
142 : outputs: outputs,
143 : id: id,
144 : version: version,
145 8 : status: ConfirmationStatus.fromConfirmations(confirmations),
146 : );
147 : }
148 :
149 1 : factory UTXOTransaction.fromJson(Map<dynamic, dynamic> json) {
150 : if (json
151 : case {
152 2 : 'hash': String hash,
153 2 : 'block': int block,
154 2 : 'confirmations': int confirmations,
155 2 : 'timeMilli': int timeMilli,
156 2 : 'amount': Map amount,
157 2 : 'fee': Map? fee,
158 2 : 'sender': String sender,
159 2 : 'recipient': String recipient,
160 2 : 'transferMethod': int transferMethod,
161 2 : 'status': int status,
162 2 : 'token': Map token,
163 2 : 'id': String id,
164 2 : 'version': int version,
165 2 : 'inputs': JsonList inputs,
166 2 : 'outputs': JsonList outputs,
167 : }) {
168 1 : return UTXOTransaction(
169 : hash: hash,
170 : block: block,
171 : confirmations: confirmations,
172 : timeMilli: timeMilli,
173 1 : amount: Amount.fromJson(amount),
174 0 : fee: fee != null ? Amount.fromJson(fee) : null,
175 : sender: sender,
176 : recipient: recipient,
177 1 : transferMethod: TransactionTransferMethod.fromJson(transferMethod),
178 1 : status: ConfirmationStatus.fromJson(status),
179 : id: id,
180 : version: version,
181 4 : inputs: inputs.map((e) => ElectrumInput.fromJson(e)).toList(),
182 4 : outputs: outputs.map((e) => ElectrumOutput.fromJson(e)).toList(),
183 1 : token: CoinEntity.fromJson(token),
184 : );
185 : }
186 :
187 0 : throw Exception("Could not parse UTXOTransaction from $json");
188 : }
189 :
190 1 : @override
191 : Map<String, dynamic> toJson() {
192 1 : return {
193 1 : 'hash': hash,
194 1 : 'block': block,
195 1 : 'confirmations': confirmations,
196 1 : 'timeMilli': timeMilli,
197 2 : 'amount': amount.toJson(),
198 1 : 'fee': fee?.toJson(),
199 1 : 'sender': sender,
200 1 : 'recipient': recipient,
201 2 : 'transferMethod': transferMethod.index,
202 2 : 'status': status.index,
203 1 : 'id': id,
204 1 : 'version': version,
205 2 : 'token': token.toJson(),
206 5 : 'inputs': inputs.map((e) => e.toJson()).toList(),
207 5 : 'outputs': outputs.map((e) => e.toJson()).toList(),
208 : };
209 : }
210 : }
211 :
212 : class ElectrumInput {
213 : final String? scriptSig;
214 : final int? sequence;
215 : final String? txid;
216 : final int? vout;
217 : final List<String>? txinwitness;
218 : final String? coinbase;
219 :
220 14 : bool get isCoinbase => coinbase != null;
221 :
222 9 : const ElectrumInput({
223 : this.scriptSig,
224 : this.sequence,
225 : this.txid,
226 : this.vout,
227 : this.txinwitness,
228 : this.coinbase,
229 : });
230 :
231 8 : String getAddress(UTXONetworkType type, {AddressType? addressType}) {
232 : try {
233 8 : return getAddressFromInput(type, this, addressType: addressType);
234 : } catch (e) {
235 : return ADDRESS_NOT_SUPPORTED;
236 : }
237 : }
238 :
239 8 : Uint8List? get publicKey {
240 : try {
241 8 : return getPubKeyFromInput(this).$1;
242 : } catch (e) {
243 : return null;
244 : }
245 : }
246 :
247 8 : List<String> getAddresses({
248 : required Iterable<AddressType> addressTypes,
249 : required UTXONetworkType networkType,
250 : }) {
251 8 : final pubKey = publicKey;
252 :
253 : if (pubKey == null) {
254 4 : return throw Exception("Cannot get public key from input");
255 : }
256 8 : return [
257 8 : for (final addressType in addressTypes)
258 8 : pubKeyToAddress(pubKey, addressType, networkType)
259 : ];
260 : }
261 :
262 9 : factory ElectrumInput.fromJson(Map json) {
263 : return switch (json) {
264 : {
265 22 : "txinwitness": [String sig, String pubKey],
266 13 : "scriptSig": {
267 8 : "asm": _,
268 12 : "hex": String hex,
269 : },
270 13 : "sequence": int sequence,
271 13 : "txid": String txid,
272 13 : "vout": int vout,
273 : } =>
274 4 : ElectrumInput(
275 4 : txinwitness: [sig, pubKey],
276 : scriptSig: hex,
277 : sequence: sequence,
278 : txid: txid,
279 : vout: vout,
280 : ),
281 : {
282 9 : "scriptSig": {
283 : "asm": _,
284 8 : "hex": String hex,
285 : },
286 7 : "sequence": int sequence,
287 7 : "txid": String txid,
288 7 : "vout": int vout,
289 : } =>
290 7 : ElectrumInput(
291 : scriptSig: hex,
292 : sequence: sequence,
293 : txid: txid,
294 : vout: vout,
295 : ),
296 : {
297 6 : "coinbase": String coinbase,
298 1 : "sequence": int sequence,
299 : } =>
300 1 : ElectrumInput(
301 : coinbase: coinbase,
302 : sequence: sequence,
303 : ),
304 : {
305 3 : "scriptSig": {
306 : "asm": _,
307 2 : "hex": String hex,
308 : },
309 2 : "txid": String txid,
310 2 : "vout": int vout,
311 4 : "value_int": int _,
312 4 : "weight": int weight,
313 : } =>
314 2 : ElectrumInput(
315 : scriptSig: hex,
316 : txid: txid,
317 : vout: vout,
318 : sequence: weight,
319 : ),
320 : {
321 1 : 'scriptSig': String? scriptSig,
322 1 : 'sequence': int? sequence,
323 1 : 'txid': String? txid,
324 1 : 'vout': int? vout,
325 1 : 'txinwitness': List<String>? txinwitness,
326 1 : 'coinbase': String? coinbase,
327 : } =>
328 1 : ElectrumInput(
329 : scriptSig: scriptSig,
330 : sequence: sequence,
331 : txid: txid,
332 : vout: vout,
333 : txinwitness: txinwitness,
334 : coinbase: coinbase,
335 : ),
336 0 : _ => throw Exception("Could not parse ElectrumInput from $json"),
337 : };
338 : }
339 :
340 1 : Json toJson() {
341 1 : return {
342 1 : 'scriptSig': scriptSig,
343 1 : 'sequence': sequence,
344 1 : 'txid': txid,
345 1 : 'vout': vout,
346 1 : 'txinwitness': txinwitness,
347 1 : 'coinbase': coinbase,
348 : };
349 : }
350 : }
351 :
352 : class ElectrumOutput {
353 : final ElectrumScriptPubKey scriptPubKey;
354 : final bool belongsToUs;
355 : final bool spent;
356 : final BigInt value;
357 : final int n;
358 :
359 : ///
360 : /// Only available if [belongsToUs] is true
361 : ///
362 : final NodeWithAddress node;
363 :
364 10 : const ElectrumOutput({
365 : required this.scriptPubKey,
366 : required this.value,
367 : required this.n,
368 : this.belongsToUs = false,
369 : this.spent = false,
370 : required this.node,
371 : });
372 :
373 : /// Zeniq: { value_coin || value_satoshi: int, ... }
374 : /// Bitcoin: { value: float, ... }
375 :
376 9 : factory ElectrumOutput.fromJson(Map json) {
377 : if (json
378 : case {
379 18 : 'value': int value,
380 12 : 'n': int n,
381 7 : 'spent': bool spent,
382 2 : 'belongsToUs': bool belongsToUs,
383 2 : 'scriptPubKey': Map scriptPubKey,
384 2 : 'node': Map node,
385 : }) {
386 1 : return ElectrumOutput(
387 1 : value: value.toBigInt,
388 : n: n,
389 : spent: spent,
390 : belongsToUs: belongsToUs,
391 1 : scriptPubKey: ElectrumScriptPubKey.fromJson(scriptPubKey),
392 1 : node: NodeWithAddress.fromJson(node),
393 : );
394 : }
395 :
396 : final valIsSatoshi =
397 16 : json.containsKey('value_satoshi') || json.containsKey('value_int');
398 :
399 : var value =
400 15 : json['value_int'] ?? json['value'] ?? json['value_satoshi'] ?? 0;
401 :
402 : value =
403 16 : valIsSatoshi ? BigInt.from(value) : BigInt.from(toSatoshiValue(value));
404 :
405 8 : final n = json['n'] ?? -1;
406 :
407 8 : return ElectrumOutput(
408 : value: value,
409 : n: n,
410 8 : scriptPubKey: ElectrumScriptPubKey.fromJson(
411 8 : json['scriptPubKey'],
412 : ),
413 8 : node: EmptyNode(),
414 : );
415 : }
416 :
417 8 : String getAddress(UTXONetworkType type, {AddressType? addressType}) {
418 : try {
419 16 : return getAddressFromLockingScript(scriptPubKey, type,
420 : addressType: addressType);
421 : } catch (e) {
422 : return ADDRESS_NOT_SUPPORTED;
423 : }
424 : }
425 :
426 8 : Iterable<String> getAddresses({
427 : required UTXONetworkType networkType,
428 : required Iterable<AddressType> addressTypes,
429 : }) {
430 : try {
431 : final (pubKey, _) =
432 16 : getPublicKeyFromLockingScript(scriptPubKey, networkType);
433 :
434 8 : return [
435 8 : for (final addressType in addressTypes)
436 8 : pubKeyHashToAddress(pubKey, addressType, networkType)
437 : ];
438 : } catch (e) {
439 1 : return [];
440 : }
441 : }
442 :
443 8 : ElectrumOutput copyWith({
444 : bool? belongsToUs,
445 : bool? spent,
446 : NodeWithAddress? node,
447 : }) {
448 8 : return ElectrumOutput(
449 8 : scriptPubKey: scriptPubKey,
450 8 : value: value,
451 8 : n: n,
452 8 : spent: spent ?? this.spent,
453 7 : belongsToUs: belongsToUs ?? this.belongsToUs,
454 8 : node: node ?? this.node,
455 : );
456 : }
457 :
458 1 : Json toJson() {
459 1 : return {
460 2 : 'value': value.toInt(),
461 1 : 'n': n,
462 1 : 'spent': spent,
463 1 : 'belongsToUs': belongsToUs,
464 2 : 'scriptPubKey': scriptPubKey.toJson(),
465 2 : 'node': node.toJson(),
466 : };
467 : }
468 :
469 0 : @override
470 : String toString() {
471 0 : return 'ElectrumOutput{scriptPubKey: $scriptPubKey, belongsToUs: $belongsToUs, spent: $spent, value: $value, n: $n, node: $node}';
472 : }
473 : }
474 :
475 7 : int toSatoshiValue(num val) {
476 7 : final value_s = val.toString();
477 7 : final splits = value_s.split('.');
478 :
479 14 : if (splits.length == 2) {
480 7 : final intPart = splits[0];
481 7 : var decPart = splits[1];
482 7 : decPart = decPart.padRight(8, '0');
483 14 : return int.parse(intPart + decPart);
484 : }
485 8 : return (val * 1E8).toInt();
486 : }
487 :
488 : class ElectrumScriptPubKey {
489 : final String hexString;
490 : final String type;
491 :
492 10 : const ElectrumScriptPubKey({
493 : required this.hexString,
494 : required this.type,
495 : });
496 :
497 0 : bool get isP2SH => type == 'scripthash';
498 0 : bool get isP2PKH => type == 'pubkeyhash';
499 0 : bool get isP2WSH => type == 'witness_v0_scripthash';
500 9 : bool get isSegwit => type == 'witness_v0_keyhash';
501 :
502 9 : factory ElectrumScriptPubKey.fromJson(Map json) {
503 9 : return ElectrumScriptPubKey(
504 9 : hexString: json['hex'] as String,
505 9 : type: json['type'] as String,
506 : );
507 : }
508 :
509 4 : Uint8List get lockingScript {
510 12 : return Uint8List.fromList(hex.decode(hexString));
511 : }
512 :
513 1 : Json toJson() {
514 1 : return {
515 1 : 'hex': hexString,
516 1 : 'type': type,
517 : };
518 : }
519 : }
520 :
521 : final class NotAvaialableUTXOTransaction extends UTXOTransaction {
522 0 : NotAvaialableUTXOTransaction(String hash, int block, CoinEntity token)
523 0 : : super(
524 : block: block,
525 : hash: hash,
526 : id: hash,
527 0 : version: -1,
528 0 : confirmations: -1,
529 0 : amount: Amount.zero,
530 0 : fee: Amount.zero,
531 : inputs: const [],
532 : outputs: const [],
533 : recipient: "",
534 : sender: "",
535 : status: ConfirmationStatus.notSubmitted,
536 0 : timeMilli: -1,
537 : token: token,
538 : transferMethod: TransactionTransferMethod.unknown,
539 : );
540 : }
|