dev
dart 343 lines 10.4 KB
Raw
1 import 'dart:convert';
2 import 'dart:math';
3 import 'dart:typed_data';
4
5 import 'package:cw_core/get_height_by_date_zec.dart';
6 import 'package:cw_core/transaction_direction.dart';
7 import 'package:cw_zcash/src/util/hex.dart';
8 import 'package:zkool/src/rust/api/account.dart' as zkool_account;
9
10 enum TxType {
11 unknown,
12 selfTransfer, // 0
13 receive, // 1
14 sent, // 2
15 unshield, // 4
16 shield, // 8
17 transparentSelfTransfer, // 12
18 }
19
20 enum NotePool { transparent, sapling, orchard, ironwood }
21
22 class ZkoolTx {
23 ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount)
24 : this._tx = tx,
25 this._txAccount = txAccount;
26 final zkool_account.Tx _tx;
27 final zkool_account.TxAccount _txAccount;
28
29 int get height => max(
30 _tx.height,
31 _txAccount.height,
32 ); // sometimes returns 0 - higher number would be correct in this case
33
34 DateTime get time {
35 final ts = max(_tx.time, _txAccount.time) * 1000;
36 if (ts != 0) {
37 return DateTime.fromMillisecondsSinceEpoch(ts);
38 }
39 if (height == 0) {
40 return DateTime.now();
41 }
42 return ZcashHeight.getTimeByBlockHeight(height);
43 }
44
45 String? get to => _txAccount.outputs.firstOrNull?.address;
46 Iterable<String> get outputAddresses =>
47 _txAccount.outputs.map((final o) => o.address).whereType<String>();
48 Iterable<({String address, int pool, BigInt value})> get outputsWithAddress => _txAccount.outputs
49 .where((final o) => o.address.isNotEmpty)
50 .map((final o) => (address: o.address, pool: o.pool, value: o.value));
51 Iterable<int> get spendPools => _txAccount.spends.map((final s) => s.pool);
52 Iterable<int> get notePools => _txAccount.notes.map((final n) => n.pool);
53 String? get memo => _txAccount.memos.firstOrNull?.memo;
54
55 BigInt get transparentOrSaplingSpent {
56 if (_txAccount.spends.isEmpty) {
57 return BigInt.zero;
58 }
59 return _txAccount.spends
60 .where(
61 (final s) => s.pool == NotePool.transparent.index || s.pool == NotePool.sapling.index,
62 )
63 .fold(BigInt.zero, (final a, final s) => a + s.value);
64 }
65
66 BigInt get orchardReceived {
67 final fromNotes = _txAccount.notes
68 .where((final n) => n.pool == NotePool.orchard.index)
69 .fold(BigInt.zero, (final a, final n) => a + n.value);
70 if (fromNotes > BigInt.zero) {
71 return fromNotes;
72 }
73 return _txAccount.outputs
74 .where((final o) => o.pool == NotePool.orchard.index)
75 .fold(BigInt.zero, (final a, final o) => a + o.value);
76 }
77
78 BigInt get ironwoodReceived {
79 final fromNotes = _txAccount.notes
80 .where((final n) => n.pool == NotePool.ironwood.index)
81 .fold(BigInt.zero, (final a, final n) => a + n.value);
82 if (fromNotes > BigInt.zero) {
83 return fromNotes;
84 }
85 return _txAccount.outputs
86 .where((final o) => o.pool == NotePool.ironwood.index)
87 .fold(BigInt.zero, (final a, final o) => a + o.value);
88 }
89
90 BigInt get orchardSpent => _txAccount.spends
91 .where((final s) => s.pool == NotePool.orchard.index)
92 .fold(BigInt.zero, (final a, final s) => a + s.value);
93
94 String get txHash {
95 final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList());
96 final txId = uint8ListToHex(reversed);
97 return txId;
98 }
99
100 BigInt get value => _value.abs();
101
102 BigInt get fee => BigInt.from(_tx.fee);
103
104
105 BigInt get _value {
106 if (_tx.value != 0) {
107 return BigInt.from(_tx.value);
108 }
109
110 return _calcValue;
111 }
112
113 /////////// calc is short for calculator (calculated in this case)
114 BigInt get _calcValue {
115 final noteSum = _txAccount.notes.isEmpty
116 ? BigInt.from(0)
117 : _txAccount.notes.map((final note) => note.value).reduce((final a, final b) => a + b);
118 final outputSum = _txAccount.outputs.isEmpty
119 ? BigInt.from(0)
120 : _txAccount.outputs
121 .map((final output) => output.value)
122 .reduce((final a, final b) => a + b);
123 final spentSum = _txAccount.spends.isEmpty
124 ? BigInt.from(0)
125 : _txAccount.spends.map((final spent) => spent.value).reduce((final a, final b) => a + b);
126
127 return noteSum - outputSum - spentSum;
128 }
129
130 TransactionDirection get direction {
131 switch (type) {
132 case TxType.selfTransfer:
133 case TxType.receive:
134 case TxType.unshield:
135 case TxType.shield:
136 case TxType.transparentSelfTransfer:
137 return TransactionDirection.incoming;
138 case TxType.sent:
139 return TransactionDirection.outgoing;
140 case TxType.unknown:
141 return _value > BigInt.from(0)
142 ? TransactionDirection.incoming
143 : TransactionDirection.outgoing;
144 }
145 }
146
147 TxType get type {
148 switch (_tx.tpe) {
149 case 0:
150 return TxType.selfTransfer;
151 case 1:
152 return TxType.receive;
153 case 2:
154 return TxType.sent;
155 case 4:
156 return TxType.unshield;
157 case 8:
158 return TxType.shield;
159 case 12:
160 return TxType.transparentSelfTransfer;
161 default:
162 return TxType.unknown;
163 }
164 }
165
166 Map<String, dynamic> toJson() => {
167 "tx": {
168 "id": _tx.id,
169 "txid": base64.encode(_tx.txid),
170 "height": _tx.height,
171 "time": _tx.time,
172 "value": value.toInt(),
173
174 "tpe": _tx.tpe,
175 "zsaValue": _tx.zsaValue,
176 "assetDisplay": _tx.assetDisplay,
177 },
178 "txaccount": {
179 "id": _txAccount.id,
180 "account": _txAccount.account,
181 "txid": base64.encode(_txAccount.txid),
182 "height": _txAccount.height,
183 "time": _txAccount.time,
184 "notes": _txAccount.notes
185 .map(
186 (final note) => {
187 "id": note.id,
188 "pool": note.pool,
189 "height": note.height,
190 "tx": note.tx,
191 "scope": note.scope,
192 "value": note.value.toString(),
193 "locked": note.locked,
194 },
195 )
196 .toList(),
197 "spends": _txAccount.spends
198 .map(
199 (final spend) => {
200 "id": spend.id,
201 "pool": spend.pool,
202 "height": spend.height,
203 "value": spend.value.toString(),
204 "assetDisplay": spend.assetDisplay,
205 },
206 )
207 .toList(),
208 "outputs": _txAccount.outputs
209 .map(
210 (final output) => {
211 "id": output.id,
212 "pool": output.pool,
213 "height": output.height,
214 "value": output.value.toString(),
215 "address": output.address,
216 },
217 )
218 .toList(),
219 "memos": _txAccount.memos
220 .map(
221 (final memo) => {
222 "note": memo.note,
223 "pool": memo.pool,
224 "output": memo.output,
225 "memo": memo.memo,
226 },
227 )
228 .toList(),
229 },
230 };
231
232 static int _asInt(final dynamic value, {final int fallback = 0}) {
233 if (value == null) {
234 return fallback;
235 }
236 if (value is int) {
237 return value;
238 }
239 if (value is num) {
240 return value.toInt();
241 }
242 return int.parse(value.toString());
243 }
244
245 static Uint8List _txidFromJson(final dynamic raw) {
246 return _bytesFromJson(raw, fieldName: "txid");
247 }
248
249 static Uint8List _bytesFromJson(final dynamic raw, {required final String fieldName}) {
250 if (raw == null) {
251 return Uint8List(0);
252 }
253 if (raw is Uint8List) {
254 return raw;
255 }
256 if (raw is List) {
257 return Uint8List.fromList(List<int>.from(raw));
258 }
259 if (raw is String) {
260 return base64.decode(raw);
261 }
262 throw FormatException("Invalid $fieldName in ZkoolTx json: $raw");
263 }
264
265 static List<T> _listFromJson<T>(final dynamic raw, final T Function(Map<String, dynamic>) parse) {
266 if (raw is! List) {
267 return const [];
268 }
269 return raw.map((final entry) => parse(Map<String, dynamic>.from(entry as Map))).toList();
270 }
271
272 static ZkoolTx fromJson(final Map<String, dynamic> json) {
273 final txJson = Map<String, dynamic>.from((json["tx"] as Map?)?.cast<String, dynamic>() ?? json);
274 final txAccountJson = Map<String, dynamic>.from(
275 (json["txaccount"] as Map?)?.cast<String, dynamic>() ?? json,
276 );
277
278 return ZkoolTx(
279 zkool_account.Tx(
280 id: _asInt(txJson["id"]),
281 txid: _txidFromJson(txJson["txid"]),
282 height: _asInt(txJson["height"]),
283 time: _asInt(txJson["time"]),
284 value: _asInt(txJson["value"]),
285 fee: _asInt(txJson["fee"]),
286 tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]),
287 zsaValue: _asInt(txJson["zsaValue"]),
288 assetDisplay: txJson["assetDisplay"] as String? ?? "",
289 isUserMemo: txJson["isUserMemo"] as bool? ?? false,
290 ),
291 zkool_account.TxAccount(
292 id: _asInt(txAccountJson["id"]),
293 account: _asInt(txAccountJson["account"]),
294 txid: _txidFromJson(txAccountJson["txid"]),
295 height: _asInt(txAccountJson["height"]),
296 time: _asInt(txAccountJson["time"]),
297 notes: _listFromJson(
298 txAccountJson["notes"],
299 (final a) => zkool_account.TxNote(
300 id: _asInt(a["id"]),
301 pool: _asInt(a["pool"]),
302 height: _asInt(a["height"]),
303 tx: _asInt(a["tx"]),
304 value: BigInt.parse(a["value"].toString()),
305 scope: _asInt(a["scope"]),
306 locked: a["locked"] as bool? ?? false,
307 assetDisplay: a["assetDisplay"] as String? ?? "",
308 ),
309 ),
310 spends: _listFromJson(
311 txAccountJson["spends"],
312 (final a) => zkool_account.TxSpend(
313 id: _asInt(a["id"]),
314 pool: _asInt(a["pool"]),
315 height: _asInt(a["height"]),
316 value: BigInt.parse(a["value"].toString()),
317 assetDisplay: a["assetDisplay"] as String? ?? "",
318 ),
319 ),
320 outputs: _listFromJson(
321 txAccountJson["outputs"],
322 (final a) => zkool_account.TxOutput(
323 id: _asInt(a["id"]),
324 pool: _asInt(a["pool"]),
325 height: _asInt(a["height"]),
326 value: BigInt.parse(a["value"].toString()),
327 address: a["address"] as String? ?? "",
328 ),
329 ),
330 memos: _listFromJson(
331 txAccountJson["memos"],
332 (final a) => zkool_account.TxMemo(
333 note: a["note"] == null ? null : _asInt(a["note"]),
334 pool: _asInt(a["pool"]),
335 output: a["output"] == null ? null : _asInt(a["output"]),
336 memo: a["memo"] as String?,
337 memoBytes: _bytesFromJson(a["memoBytes"], fieldName: "memoBytes"),
338 ),
339 ),
340 ),
341 );
342 }
343 }