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 16 : for (final addressType in addressTypes) pubKeyToAddress(pubKey, addressType, networkType)
258 : ];
259 : }
260 :
261 9 : factory ElectrumInput.fromJson(Map json) {
262 : return switch (json) {
263 : {
264 22 : "txinwitness": [String sig, String pubKey],
265 13 : "scriptSig": {
266 8 : "asm": _,
267 12 : "hex": String hex,
268 : },
269 13 : "sequence": int sequence,
270 13 : "txid": String txid,
271 13 : "vout": int vout,
272 : } =>
273 4 : ElectrumInput(
274 4 : txinwitness: [sig, pubKey],
275 : scriptSig: hex,
276 : sequence: sequence,
277 : txid: txid,
278 : vout: vout,
279 : ),
280 : {
281 9 : "scriptSig": {
282 : "asm": _,
283 8 : "hex": String hex,
284 : },
285 7 : "sequence": int sequence,
286 7 : "txid": String txid,
287 7 : "vout": int vout,
288 : } =>
289 7 : ElectrumInput(
290 : scriptSig: hex,
291 : sequence: sequence,
292 : txid: txid,
293 : vout: vout,
294 : ),
295 : {
296 6 : "coinbase": String coinbase,
297 1 : "sequence": int sequence,
298 : } =>
299 1 : ElectrumInput(
300 : coinbase: coinbase,
301 : sequence: sequence,
302 : ),
303 : {
304 3 : "scriptSig": {
305 : "asm": _,
306 2 : "hex": String hex,
307 : },
308 2 : "txid": String txid,
309 2 : "vout": int vout,
310 4 : "value_int": int _,
311 4 : "weight": int weight,
312 : } =>
313 2 : ElectrumInput(
314 : scriptSig: hex,
315 : txid: txid,
316 : vout: vout,
317 : sequence: weight,
318 : ),
319 : {
320 1 : 'scriptSig': String? scriptSig,
321 1 : 'sequence': int? sequence,
322 1 : 'txid': String? txid,
323 1 : 'vout': int? vout,
324 1 : 'txinwitness': List<String>? txinwitness,
325 1 : 'coinbase': String? coinbase,
326 : } =>
327 1 : ElectrumInput(
328 : scriptSig: scriptSig,
329 : sequence: sequence,
330 : txid: txid,
331 : vout: vout,
332 : txinwitness: txinwitness,
333 : coinbase: coinbase,
334 : ),
335 0 : _ => throw Exception("Could not parse ElectrumInput from $json"),
336 : };
337 : }
338 :
339 1 : Json toJson() {
340 1 : return {
341 1 : 'scriptSig': scriptSig,
342 1 : 'sequence': sequence,
343 1 : 'txid': txid,
344 1 : 'vout': vout,
345 1 : 'txinwitness': txinwitness,
346 1 : 'coinbase': coinbase,
347 : };
348 : }
349 : }
350 :
351 : class ElectrumOutput {
352 : final ElectrumScriptPubKey scriptPubKey;
353 : final bool belongsToUs;
354 : final bool spent;
355 : final BigInt value;
356 : final int n;
357 :
358 : ///
359 : /// Only available if [belongsToUs] is true
360 : ///
361 : final NodeWithAddress node;
362 :
363 10 : const ElectrumOutput({
364 : required this.scriptPubKey,
365 : required this.value,
366 : required this.n,
367 : this.belongsToUs = false,
368 : this.spent = false,
369 : required this.node,
370 : });
371 :
372 : /// Zeniq: { value_coin || value_satoshi: int, ... }
373 : /// Bitcoin: { value: float, ... }
374 :
375 9 : factory ElectrumOutput.fromJson(Map json) {
376 : if (json
377 : case {
378 18 : 'value': int value,
379 12 : 'n': int n,
380 7 : 'spent': bool spent,
381 2 : 'belongsToUs': bool belongsToUs,
382 2 : 'scriptPubKey': Map scriptPubKey,
383 2 : 'node': Map node,
384 : }) {
385 1 : return ElectrumOutput(
386 1 : value: value.toBigInt,
387 : n: n,
388 : spent: spent,
389 : belongsToUs: belongsToUs,
390 1 : scriptPubKey: ElectrumScriptPubKey.fromJson(scriptPubKey),
391 1 : node: NodeWithAddress.fromJson(node),
392 : );
393 : }
394 :
395 16 : final valIsSatoshi = json.containsKey('value_satoshi') || json.containsKey('value_int');
396 :
397 15 : var value = json['value_int'] ?? json['value'] ?? json['value_satoshi'] ?? 0;
398 :
399 16 : value = valIsSatoshi ? BigInt.from(value) : BigInt.from(toSatoshiValue(value));
400 :
401 8 : final n = json['n'] ?? -1;
402 :
403 8 : return ElectrumOutput(
404 : value: value,
405 : n: n,
406 8 : scriptPubKey: ElectrumScriptPubKey.fromJson(
407 8 : json['scriptPubKey'],
408 : ),
409 8 : node: EmptyNode(),
410 : );
411 : }
412 :
413 8 : String getAddress(UTXONetworkType type, {AddressType? addressType}) {
414 : try {
415 16 : return getAddressFromLockingScript(scriptPubKey, type, addressType: addressType);
416 : } catch (e) {
417 : return ADDRESS_NOT_SUPPORTED;
418 : }
419 : }
420 :
421 8 : Iterable<String> getAddresses({
422 : required UTXONetworkType networkType,
423 : required Iterable<AddressType> addressTypes,
424 : }) {
425 : try {
426 16 : final (pubKey, _) = getPublicKeyFromLockingScript(scriptPubKey, networkType);
427 :
428 8 : return [
429 8 : for (final addressType in addressTypes)
430 8 : pubKeyHashToAddress(pubKey, addressType, networkType)
431 : ];
432 : } catch (e) {
433 1 : return [];
434 : }
435 : }
436 :
437 8 : ElectrumOutput copyWith({
438 : bool? belongsToUs,
439 : bool? spent,
440 : NodeWithAddress? node,
441 : }) {
442 8 : return ElectrumOutput(
443 8 : scriptPubKey: scriptPubKey,
444 8 : value: value,
445 8 : n: n,
446 8 : spent: spent ?? this.spent,
447 7 : belongsToUs: belongsToUs ?? this.belongsToUs,
448 8 : node: node ?? this.node,
449 : );
450 : }
451 :
452 1 : Json toJson() {
453 1 : return {
454 2 : 'value': value.toInt(),
455 1 : 'n': n,
456 1 : 'spent': spent,
457 1 : 'belongsToUs': belongsToUs,
458 2 : 'scriptPubKey': scriptPubKey.toJson(),
459 2 : 'node': node.toJson(),
460 : };
461 : }
462 :
463 0 : @override
464 : String toString() {
465 0 : return 'ElectrumOutput{scriptPubKey: $scriptPubKey, belongsToUs: $belongsToUs, spent: $spent, value: $value, n: $n, node: $node}';
466 : }
467 : }
468 :
469 7 : int toSatoshiValue(num val) {
470 7 : final value_s = val.toString();
471 7 : final splits = value_s.split('.');
472 :
473 14 : if (splits.length == 2) {
474 7 : final intPart = splits[0];
475 7 : var decPart = splits[1];
476 7 : decPart = decPart.padRight(8, '0');
477 14 : return int.parse(intPart + decPart);
478 : }
479 8 : return (val * 1E8).toInt();
480 : }
481 :
482 : class ElectrumScriptPubKey {
483 : final String hexString;
484 : final String type;
485 :
486 10 : const ElectrumScriptPubKey({
487 : required this.hexString,
488 : required this.type,
489 : });
490 :
491 0 : bool get isP2SH => type == 'scripthash';
492 0 : bool get isP2PKH => type == 'pubkeyhash';
493 0 : bool get isP2WSH => type == 'witness_v0_scripthash';
494 9 : bool get isSegwit => type == 'witness_v0_keyhash';
495 :
496 9 : factory ElectrumScriptPubKey.fromJson(Map json) {
497 9 : return ElectrumScriptPubKey(
498 9 : hexString: json['hex'] as String,
499 9 : type: json['type'] as String,
500 : );
501 : }
502 :
503 4 : Uint8List get lockingScript {
504 12 : return Uint8List.fromList(hex.decode(hexString));
505 : }
506 :
507 1 : Json toJson() {
508 1 : return {
509 1 : 'hex': hexString,
510 1 : 'type': type,
511 : };
512 : }
513 : }
514 :
515 : final class NotAvaialableUTXOTransaction extends UTXOTransaction {
516 0 : NotAvaialableUTXOTransaction(String hash, int block, CoinEntity token)
517 0 : : super(
518 : block: block,
519 : hash: hash,
520 : id: hash,
521 0 : version: -1,
522 0 : confirmations: -1,
523 0 : amount: Amount.zero,
524 0 : fee: Amount.zero,
525 : inputs: const [],
526 : outputs: const [],
527 : recipient: "",
528 : sender: "",
529 : status: ConfirmationStatus.notSubmitted,
530 0 : timeMilli: -1,
531 : token: token,
532 : transferMethod: TransactionTransferMethod.unknown,
533 : );
534 : }
|