That fuck shit the fascists are using
1package org.tm.archive.database;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6
7import androidx.annotation.AnyThread;
8import androidx.annotation.NonNull;
9import androidx.annotation.Nullable;
10import androidx.annotation.WorkerThread;
11import androidx.lifecycle.LiveData;
12import androidx.lifecycle.MutableLiveData;
13
14import com.mobilecoin.lib.exceptions.SerializationException;
15
16import org.signal.core.util.CursorExtensionsKt;
17import org.signal.core.util.CursorUtil;
18import org.signal.core.util.SQLiteDatabaseExtensionsKt;
19import org.signal.core.util.SqlUtil;
20import org.signal.core.util.logging.Log;
21import org.tm.archive.database.model.MmsMessageRecord;
22import org.tm.archive.database.model.MessageId;
23import org.tm.archive.database.model.MessageRecord;
24import org.tm.archive.database.model.databaseprotos.CryptoValue;
25import org.tm.archive.dependencies.ApplicationDependencies;
26import org.tm.archive.payments.CryptoValueUtil;
27import org.tm.archive.payments.Direction;
28import org.tm.archive.payments.FailureReason;
29import org.tm.archive.payments.MobileCoinPublicAddress;
30import org.tm.archive.payments.Payee;
31import org.tm.archive.payments.Payment;
32import org.tm.archive.payments.State;
33import org.tm.archive.payments.proto.PaymentMetaData;
34import org.tm.archive.recipients.RecipientId;
35import org.signal.core.util.Base64;
36import org.tm.archive.util.livedata.LiveDataUtil;
37import org.whispersystems.signalservice.api.payments.Money;
38import org.whispersystems.signalservice.api.util.UuidUtil;
39
40import java.io.IOException;
41import java.util.Arrays;
42import java.util.Collection;
43import java.util.Collections;
44import java.util.LinkedList;
45import java.util.List;
46import java.util.UUID;
47
48public final class PaymentTable extends DatabaseTable implements RecipientIdDatabaseReference {
49
50 private static final String TAG = Log.tag(PaymentTable.class);
51
52 public static final String TABLE_NAME = "payments";
53
54 private static final String ID = "_id";
55 private static final String PAYMENT_UUID = "uuid";
56 private static final String RECIPIENT_ID = "recipient";
57 private static final String ADDRESS = "recipient_address";
58 private static final String TIMESTAMP = "timestamp";
59 private static final String DIRECTION = "direction";
60 private static final String STATE = "state";
61 private static final String NOTE = "note";
62 private static final String AMOUNT = "amount";
63 private static final String FEE = "fee";
64 private static final String TRANSACTION = "transaction_record";
65 private static final String RECEIPT = "receipt";
66 private static final String PUBLIC_KEY = "receipt_public_key";
67 private static final String META_DATA = "payment_metadata";
68 private static final String FAILURE = "failure_reason";
69 private static final String BLOCK_INDEX = "block_index";
70 private static final String BLOCK_TIME = "block_timestamp";
71 private static final String SEEN = "seen";
72
73 public static final String CREATE_TABLE =
74 "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY, " +
75 PAYMENT_UUID + " TEXT DEFAULT NULL, " +
76 RECIPIENT_ID + " INTEGER DEFAULT 0, " +
77 ADDRESS + " TEXT DEFAULT NULL, " +
78 TIMESTAMP + " INTEGER, " +
79 NOTE + " TEXT DEFAULT NULL, " +
80 DIRECTION + " INTEGER, " +
81 STATE + " INTEGER, " +
82 FAILURE + " INTEGER, " +
83 AMOUNT + " BLOB NOT NULL, " +
84 FEE + " BLOB NOT NULL, " +
85 TRANSACTION + " BLOB DEFAULT NULL, " +
86 RECEIPT + " BLOB DEFAULT NULL, " +
87 META_DATA + " BLOB DEFAULT NULL, " +
88 PUBLIC_KEY + " TEXT DEFAULT NULL, " +
89 BLOCK_INDEX + " INTEGER DEFAULT 0, " +
90 BLOCK_TIME + " INTEGER DEFAULT 0, " +
91 SEEN + " INTEGER, " +
92 "UNIQUE(" + PAYMENT_UUID + ") ON CONFLICT ABORT)";
93
94 public static final String[] CREATE_INDEXES = {
95 "CREATE INDEX IF NOT EXISTS timestamp_direction_index ON " + TABLE_NAME + " (" + TIMESTAMP + ", " + DIRECTION + ");",
96 "CREATE INDEX IF NOT EXISTS timestamp_index ON " + TABLE_NAME + " (" + TIMESTAMP + ");",
97 "CREATE UNIQUE INDEX IF NOT EXISTS receipt_public_key_index ON " + TABLE_NAME + " (" + PUBLIC_KEY + ");"
98 };
99
100 private final MutableLiveData<Object> changeSignal;
101
102 PaymentTable(@NonNull Context context, @NonNull SignalDatabase databaseHelper) {
103 super(context, databaseHelper);
104
105 this.changeSignal = new MutableLiveData<>(new Object());
106 }
107
108 @WorkerThread
109 public void createIncomingPayment(@NonNull UUID uuid,
110 @Nullable RecipientId fromRecipient,
111 long timestamp,
112 @NonNull String note,
113 @NonNull Money amount,
114 @NonNull Money fee,
115 @NonNull byte[] receipt,
116 boolean seen)
117 throws PublicKeyConflictException, SerializationException
118 {
119 create(uuid, fromRecipient, null, timestamp, 0, note, Direction.RECEIVED, State.SUBMITTED, amount, fee, null, receipt, null, seen);
120 }
121
122 @WorkerThread
123 public void createOutgoingPayment(@NonNull UUID uuid,
124 @Nullable RecipientId toRecipient,
125 @NonNull MobileCoinPublicAddress publicAddress,
126 long timestamp,
127 @NonNull String note,
128 @NonNull Money amount)
129 {
130 try {
131 create(uuid, toRecipient, publicAddress, timestamp, 0, note, Direction.SENT, State.INITIAL, amount, amount.toZero(), null, null, null, true);
132 } catch (PublicKeyConflictException e) {
133 Log.w(TAG, "Tried to create payment but the public key appears already in the database", e);
134 throw new IllegalArgumentException(e);
135 } catch (SerializationException e) {
136 throw new IllegalArgumentException(e);
137 }
138 }
139
140 /**
141 * Inserts a payment in its final successful state.
142 * <p>
143 * This is for when a linked device has told us about the payment only.
144 */
145 @WorkerThread
146 public void createSuccessfulPayment(@NonNull UUID uuid,
147 @Nullable RecipientId toRecipient,
148 @NonNull MobileCoinPublicAddress publicAddress,
149 long timestamp,
150 long blockIndex,
151 @NonNull String note,
152 @NonNull Money amount,
153 @NonNull Money fee,
154 @NonNull byte[] receipt,
155 @NonNull PaymentMetaData metaData)
156 throws SerializationException
157 {
158 try {
159 create(uuid, toRecipient, publicAddress, timestamp, blockIndex, note, Direction.SENT, State.SUCCESSFUL, amount, fee, null, receipt, metaData, true);
160 } catch (PublicKeyConflictException e) {
161 Log.w(TAG, "Tried to create payment but the public key appears already in the database", e);
162 throw new AssertionError(e);
163 }
164 }
165
166 @WorkerThread
167 public void createDefrag(@NonNull UUID uuid,
168 @Nullable RecipientId self,
169 @NonNull MobileCoinPublicAddress selfPublicAddress,
170 long timestamp,
171 @NonNull Money fee,
172 @NonNull byte[] transaction,
173 @NonNull byte[] receipt)
174 {
175 try {
176 create(uuid, self, selfPublicAddress, timestamp, 0, "", Direction.SENT, State.SUBMITTED, fee.toZero(), fee, transaction, receipt, null, true);
177 } catch (PublicKeyConflictException e) {
178 Log.w(TAG, "Tried to create payment but the public key appears already in the database", e);
179 throw new AssertionError(e);
180 } catch (SerializationException e) {
181 throw new IllegalArgumentException(e);
182 }
183 }
184
185 @WorkerThread
186 private void create(@NonNull UUID uuid,
187 @Nullable RecipientId recipientId,
188 @Nullable MobileCoinPublicAddress publicAddress,
189 long timestamp,
190 long blockIndex,
191 @NonNull String note,
192 @NonNull Direction direction,
193 @NonNull State state,
194 @NonNull Money amount,
195 @NonNull Money fee,
196 @Nullable byte[] transaction,
197 @Nullable byte[] receipt,
198 @Nullable PaymentMetaData metaData,
199 boolean seen)
200 throws PublicKeyConflictException, SerializationException
201 {
202 if (recipientId == null && publicAddress == null) {
203 throw new AssertionError();
204 }
205
206 if (amount.isNegative()) {
207 throw new AssertionError();
208 }
209
210 if (fee.isNegative()) {
211 throw new AssertionError();
212 }
213
214 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
215 ContentValues values = new ContentValues(15);
216
217 values.put(PAYMENT_UUID, uuid.toString());
218 if (recipientId == null || recipientId.isUnknown()) {
219 values.put(RECIPIENT_ID, 0);
220 } else {
221 values.put(RECIPIENT_ID, recipientId.serialize());
222 }
223 if (publicAddress == null) {
224 values.putNull(ADDRESS);
225 } else {
226 values.put(ADDRESS, publicAddress.getPaymentAddressBase58());
227 }
228 values.put(TIMESTAMP, timestamp);
229 values.put(BLOCK_INDEX, blockIndex);
230 values.put(NOTE, note);
231 values.put(DIRECTION, direction.serialize());
232 values.put(STATE, state.serialize());
233 values.put(AMOUNT, CryptoValueUtil.moneyToCryptoValue(amount).encode());
234 values.put(FEE, CryptoValueUtil.moneyToCryptoValue(fee).encode());
235 if (transaction != null) {
236 values.put(TRANSACTION, transaction);
237 } else {
238 values.putNull(TRANSACTION);
239 }
240 if (receipt != null) {
241 values.put(RECEIPT, receipt);
242 values.put(PUBLIC_KEY, Base64.encodeWithPadding(PaymentMetaDataUtil.receiptPublic(PaymentMetaDataUtil.fromReceipt(receipt))));
243 } else {
244 values.putNull(RECEIPT);
245 values.putNull(PUBLIC_KEY);
246 }
247 if (metaData != null) {
248 values.put(META_DATA, metaData.encode());
249 } else {
250 values.put(META_DATA, PaymentMetaDataUtil.fromReceiptAndTransaction(receipt, transaction).encode());
251 }
252 values.put(SEEN, seen ? 1 : 0);
253
254 long inserted = database.insert(TABLE_NAME, null, values);
255
256 if (inserted == -1) {
257 throw new PublicKeyConflictException();
258 }
259
260 notifyChanged(uuid);
261 }
262
263 public void deleteAll() {
264 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
265 database.delete(TABLE_NAME, null, null);
266 Log.i(TAG, "Deleted all records");
267 }
268
269 @WorkerThread
270 public boolean delete(@NonNull UUID uuid) {
271 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
272 String where = PAYMENT_UUID + " = ?";
273 String[] args = {uuid.toString()};
274 int deleted;
275
276 database.beginTransaction();
277 try {
278 deleted = database.delete(TABLE_NAME, where, args);
279
280 if (deleted > 1) {
281 Log.w(TAG, "More than one row matches criteria");
282 throw new AssertionError();
283 }
284 database.setTransactionSuccessful();
285 } finally {
286 database.endTransaction();
287 }
288
289 if (deleted > 0) {
290 notifyChanged(uuid);
291 }
292
293 return deleted > 0;
294 }
295
296 @WorkerThread
297 public @NonNull List<PaymentTransaction> getAll() {
298 SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
299 List<PaymentTransaction> result = new LinkedList<>();
300
301 try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, TIMESTAMP + " DESC")) {
302 while (cursor.moveToNext()) {
303 result.add(readPayment(cursor));
304 }
305 }
306
307 return result;
308 }
309
310 @WorkerThread
311 public void markAllSeen() {
312 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
313 ContentValues values = new ContentValues(1);
314 List<UUID> unseenIds = new LinkedList<>();
315 String[] unseenProjection = SqlUtil.buildArgs(PAYMENT_UUID);
316 String unseenWhile = SEEN + " != ?";
317 String[] unseenArgs = SqlUtil.buildArgs("1");
318 int updated = -1;
319
320 values.put(SEEN, 1);
321
322 try {
323 database.beginTransaction();
324
325 try (Cursor cursor = database.query(TABLE_NAME, unseenProjection, unseenWhile, unseenArgs, null, null, null)) {
326 while (cursor != null && cursor.moveToNext()) {
327 unseenIds.add(UUID.fromString(CursorUtil.requireString(cursor, PAYMENT_UUID)));
328 }
329 }
330
331 if (!unseenIds.isEmpty()) {
332 updated = database.update(TABLE_NAME, values, null, null);
333 }
334
335 database.setTransactionSuccessful();
336 } finally {
337 database.endTransaction();
338 }
339
340 if (updated > 0) {
341 for (final UUID unseenId : unseenIds) {
342 notifyUuidChanged(unseenId);
343 }
344
345 notifyChanged();
346 }
347 }
348
349 @WorkerThread
350 public void markPaymentSeen(@NonNull UUID uuid) {
351 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
352 ContentValues values = new ContentValues(1);
353 String where = PAYMENT_UUID + " = ?";
354 String[] args = {uuid.toString()};
355
356 values.put(SEEN, 1);
357 int updated = database.update(TABLE_NAME, values, where, args);
358
359 if (updated > 0) {
360 notifyChanged(uuid);
361 }
362 }
363
364 @WorkerThread
365 public @NonNull List<PaymentTransaction> getUnseenPayments() {
366 SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
367 String query = SEEN + " = 0 AND " + STATE + " = " + State.SUCCESSFUL.serialize();
368 List<PaymentTransaction> results = new LinkedList<>();
369
370 try (Cursor cursor = db.query(TABLE_NAME, null, query, null, null, null, null)) {
371 while (cursor.moveToNext()) {
372 results.add(readPayment(cursor));
373 }
374 }
375
376 return results;
377 }
378
379 @WorkerThread
380 public @Nullable PaymentTransaction getPayment(@NonNull UUID uuid) {
381 SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
382 String select = PAYMENT_UUID + " = ?";
383 String[] args = {uuid.toString()};
384
385 try (Cursor cursor = database.query(TABLE_NAME, null, select, args, null, null, null)) {
386 if (cursor.moveToNext()) {
387 PaymentTransaction payment = readPayment(cursor);
388
389 if (cursor.moveToNext()) {
390 throw new AssertionError("Multiple records for one UUID");
391 }
392
393 return payment;
394 } else {
395 return null;
396 }
397 }
398 }
399
400 public @NonNull List<Payment> getPayments(@Nullable Collection<UUID> paymentUuids) {
401 if (paymentUuids == null || paymentUuids.isEmpty()) {
402 return Collections.emptyList();
403 }
404
405 List<SqlUtil.Query> queries = SqlUtil.buildCollectionQuery(PAYMENT_UUID, paymentUuids);
406 List<Payment> payments = new LinkedList<>();
407
408 for (SqlUtil.Query query : queries) {
409 Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase())
410 .from(TABLE_NAME)
411 .where(query.getWhere(), (Object[]) query.getWhereArgs())
412 .run();
413
414 payments.addAll(CursorExtensionsKt.readToList(cursor, PaymentTable::readPayment));
415 }
416
417 return payments;
418 }
419
420 public @NonNull List<UUID> getSubmittedIncomingPayments() {
421 return CursorExtensionsKt.readToList(
422 SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), PAYMENT_UUID)
423 .from(TABLE_NAME)
424 .where(DIRECTION + " = ? AND " + STATE + " = ?", Direction.RECEIVED.serialize(), State.SUBMITTED.serialize())
425 .run(),
426 c -> UuidUtil.parseOrNull(CursorUtil.requireString(c, PAYMENT_UUID))
427 );
428 }
429
430 @AnyThread
431 public @NonNull LiveData<List<PaymentTransaction>> getAllLive() {
432 return LiveDataUtil.mapAsync(changeSignal, change -> getAll());
433 }
434
435 @WorkerThread
436 public @NonNull MessageRecord updateMessageWithPayment(@NonNull MessageRecord record) {
437 if (record.isPaymentNotification()) {
438 Payment payment = getPayment(UuidUtil.parseOrThrow(record.getBody()));
439 if (payment != null && record instanceof MmsMessageRecord) {
440 return ((MmsMessageRecord) record).withPayment(payment);
441 } else {
442 throw new AssertionError("Payment not found for message");
443 }
444 }
445 return record;
446 }
447
448 @Override
449 public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) {
450 ContentValues values = new ContentValues();
451 values.put(RECIPIENT_ID, toId.serialize());
452 getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(fromId));
453 }
454
455 public boolean markPaymentSubmitted(@NonNull UUID uuid,
456 @NonNull byte[] transaction,
457 @NonNull byte[] receipt,
458 @NonNull Money fee)
459 throws PublicKeyConflictException
460 {
461 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
462 String where = PAYMENT_UUID + " = ?";
463 String[] whereArgs = {uuid.toString()};
464 int updated;
465 ContentValues values = new ContentValues(6);
466
467 values.put(STATE, State.SUBMITTED.serialize());
468 values.put(TRANSACTION, transaction);
469 values.put(RECEIPT, receipt);
470 try {
471 values.put(PUBLIC_KEY, Base64.encodeWithPadding(PaymentMetaDataUtil.receiptPublic(PaymentMetaDataUtil.fromReceipt(receipt))));
472 values.put(META_DATA, PaymentMetaDataUtil.fromReceiptAndTransaction(receipt, transaction).encode());
473 } catch (SerializationException e) {
474 throw new IllegalArgumentException(e);
475 }
476 values.put(FEE, CryptoValueUtil.moneyToCryptoValue(fee).encode());
477
478 database.beginTransaction();
479 try {
480 updated = database.update(TABLE_NAME, values, where, whereArgs);
481
482 if (updated == -1) {
483 throw new PublicKeyConflictException();
484 }
485
486 if (updated > 1) {
487 Log.w(TAG, "More than one row matches criteria");
488 throw new AssertionError();
489 }
490 database.setTransactionSuccessful();
491 } finally {
492 database.endTransaction();
493 }
494
495 if (updated > 0) {
496 notifyChanged(uuid);
497 }
498
499 return updated > 0;
500 }
501
502 public boolean markPaymentSuccessful(@NonNull UUID uuid, long blockIndex) {
503 return markPayment(uuid, State.SUCCESSFUL, null, null, blockIndex);
504 }
505
506 public boolean markReceivedPaymentSuccessful(@NonNull UUID uuid, @NonNull Money amount, long blockIndex) {
507 return markPayment(uuid, State.SUCCESSFUL, amount, null, blockIndex);
508 }
509
510 public boolean markPaymentFailed(@NonNull UUID uuid, @NonNull FailureReason failureReason) {
511 return markPayment(uuid, State.FAILED, null, failureReason, null);
512 }
513
514 private boolean markPayment(@NonNull UUID uuid,
515 @NonNull State state,
516 @Nullable Money amount,
517 @Nullable FailureReason failureReason,
518 @Nullable Long blockIndex)
519 {
520 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
521 String where = PAYMENT_UUID + " = ?";
522 String[] whereArgs = {uuid.toString()};
523 int updated;
524 ContentValues values = new ContentValues(3);
525
526 values.put(STATE, state.serialize());
527
528 if (amount != null) {
529 values.put(AMOUNT, CryptoValueUtil.moneyToCryptoValue(amount).encode());
530 }
531
532 if (state == State.FAILED) {
533 values.put(FAILURE, failureReason != null ? failureReason.serialize()
534 : FailureReason.UNKNOWN.serialize());
535 } else {
536 if (failureReason != null) {
537 throw new AssertionError();
538 }
539 values.putNull(FAILURE);
540 }
541
542 if (blockIndex != null) {
543 values.put(BLOCK_INDEX, blockIndex);
544 }
545
546 database.beginTransaction();
547 try {
548 updated = database.update(TABLE_NAME, values, where, whereArgs);
549
550 if (updated > 1) {
551 Log.w(TAG, "More than one row matches criteria");
552 throw new AssertionError();
553 }
554 database.setTransactionSuccessful();
555 } finally {
556 database.endTransaction();
557 }
558
559 if (updated > 0) {
560 notifyChanged(uuid);
561 }
562
563 return updated > 0;
564 }
565
566 public boolean updateBlockDetails(@NonNull UUID uuid,
567 long blockIndex,
568 long blockTimestamp)
569 {
570 SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
571 String where = PAYMENT_UUID + " = ?";
572 String[] whereArgs = {uuid.toString()};
573 int updated;
574 ContentValues values = new ContentValues(2);
575
576 values.put(BLOCK_INDEX, blockIndex);
577 values.put(BLOCK_TIME, blockTimestamp);
578
579 database.beginTransaction();
580 try {
581 updated = database.update(TABLE_NAME, values, where, whereArgs);
582
583 if (updated > 1) {
584 Log.w(TAG, "More than one row matches criteria");
585 throw new AssertionError();
586 }
587 database.setTransactionSuccessful();
588 } finally {
589 database.endTransaction();
590 }
591
592 if (updated > 0) {
593 notifyChanged(uuid);
594 }
595
596 return updated > 0;
597 }
598
599 private static @NonNull PaymentTransaction readPayment(@NonNull Cursor cursor) {
600 return new PaymentTransaction(UUID.fromString(CursorUtil.requireString(cursor, PAYMENT_UUID)),
601 getRecipientId(cursor),
602 MobileCoinPublicAddress.fromBase58NullableOrThrow(CursorUtil.requireString(cursor, ADDRESS)),
603 CursorUtil.requireLong(cursor, TIMESTAMP),
604 Direction.deserialize(CursorUtil.requireInt(cursor, DIRECTION)),
605 State.deserialize(CursorUtil.requireInt(cursor, STATE)),
606 FailureReason.deserialize(CursorUtil.requireInt(cursor, FAILURE)),
607 CursorUtil.requireString(cursor, NOTE),
608 getMoneyValue(CursorUtil.requireBlob(cursor, AMOUNT)),
609 getMoneyValue(CursorUtil.requireBlob(cursor, FEE)),
610 CursorUtil.requireBlob(cursor, TRANSACTION),
611 CursorUtil.requireBlob(cursor, RECEIPT),
612 PaymentMetaDataUtil.parseOrThrow(CursorUtil.requireBlob(cursor, META_DATA)),
613 CursorUtil.requireLong(cursor, BLOCK_INDEX),
614 CursorUtil.requireLong(cursor, BLOCK_TIME),
615 CursorUtil.requireBoolean(cursor, SEEN));
616 }
617
618 private static @Nullable RecipientId getRecipientId(@NonNull Cursor cursor) {
619 long id = CursorUtil.requireLong(cursor, RECIPIENT_ID);
620 if (id == 0) return null;
621 return RecipientId.from(id);
622 }
623
624 private static @NonNull Money getMoneyValue(@NonNull byte[] blob) {
625 try {
626 CryptoValue cryptoValue = CryptoValue.ADAPTER.decode(blob);
627 return CryptoValueUtil.cryptoValueToMoney(cryptoValue);
628 } catch (IOException e) {
629 throw new AssertionError(e);
630 }
631 }
632
633 /**
634 * notifyChanged will alert the database observer for two events:
635 *
636 * 1. It will alert the global payments observer that something changed
637 * 2. It will alert the uuid specific observer that something will change.
638 *
639 * You should not call this in a tight loop, opting to call notifyUuidChanged instead.
640 */
641 private void notifyChanged(@Nullable UUID uuid) {
642 notifyChanged();
643 notifyUuidChanged(uuid);
644 }
645
646 /**
647 * Notifies the global payments observer that something changed.
648 */
649 private void notifyChanged() {
650 changeSignal.postValue(new Object());
651 ApplicationDependencies.getDatabaseObserver().notifyAllPaymentsListeners();
652 }
653
654 /**
655 * Notify the database observer of a change for a specific uuid. Does not trigger
656 * the global payments observer.
657 */
658 private void notifyUuidChanged(@Nullable UUID uuid) {
659 if (uuid != null) {
660 ApplicationDependencies.getDatabaseObserver().notifyPaymentListeners(uuid);
661 MessageId messageId = SignalDatabase.messages().getPaymentMessage(uuid);
662 if (messageId != null) {
663 ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId);
664 }
665 }
666 }
667
668 public static final class PaymentTransaction implements Payment {
669 private final UUID uuid;
670 private final Payee payee;
671 private final long timestamp;
672 private final Direction direction;
673 private final State state;
674 private final FailureReason failureReason;
675 private final String note;
676 private final Money amount;
677 private final Money fee;
678 private final byte[] transaction;
679 private final byte[] receipt;
680 private final PaymentMetaData paymentMetaData;
681 private final Long blockIndex;
682 private final long blockTimestamp;
683 private final boolean seen;
684
685 PaymentTransaction(@NonNull UUID uuid,
686 @Nullable RecipientId recipientId,
687 @Nullable MobileCoinPublicAddress publicAddress,
688 long timestamp,
689 @NonNull Direction direction,
690 @NonNull State state,
691 @Nullable FailureReason failureReason,
692 @NonNull String note,
693 @NonNull Money amount,
694 @NonNull Money fee,
695 @Nullable byte[] transaction,
696 @Nullable byte[] receipt,
697 @NonNull PaymentMetaData paymentMetaData,
698 @Nullable Long blockIndex,
699 long blockTimestamp,
700 boolean seen)
701 {
702 this.uuid = uuid;
703 this.paymentMetaData = paymentMetaData;
704 this.payee = fromPaymentTransaction(recipientId, publicAddress);
705 this.timestamp = timestamp;
706 this.direction = direction;
707 this.state = state;
708 this.failureReason = failureReason;
709 this.note = note;
710 this.amount = amount;
711 this.fee = fee;
712 this.transaction = transaction;
713 this.receipt = receipt;
714 this.blockIndex = blockIndex;
715 this.blockTimestamp = blockTimestamp;
716 this.seen = seen;
717
718 if (amount.isNegative()) {
719 throw new AssertionError();
720 }
721 }
722
723 @Override
724 public @NonNull UUID getUuid() {
725 return uuid;
726 }
727
728 @Override
729 public @NonNull Payee getPayee() {
730 return payee;
731 }
732
733 @Override
734 public long getBlockIndex() {
735 return blockIndex;
736 }
737
738 @Override
739 public long getBlockTimestamp() {
740 return blockTimestamp;
741 }
742
743 @Override
744 public long getTimestamp() {
745 return timestamp;
746 }
747
748 @Override
749 public @NonNull Direction getDirection() {
750 return direction;
751 }
752
753 @Override
754 public @NonNull State getState() {
755 return state;
756 }
757
758 @Override
759 public @Nullable FailureReason getFailureReason() {
760 return failureReason;
761 }
762
763 @Override
764 public @NonNull String getNote() {
765 return note;
766 }
767
768 @Override
769 public @NonNull Money getAmount() {
770 return amount;
771 }
772
773 @Override
774 public @NonNull Money getFee() {
775 return fee;
776 }
777
778 @Override
779 public @NonNull PaymentMetaData getPaymentMetaData() {
780 return paymentMetaData;
781 }
782
783 @Override
784 public boolean isSeen() {
785 return seen;
786 }
787
788 public @Nullable byte[] getTransaction() {
789 return transaction;
790 }
791
792 public @Nullable byte[] getReceipt() {
793 return receipt;
794 }
795
796 @Override
797 public boolean equals(@Nullable Object o) {
798 if (this == o) return true;
799 if (!(o instanceof PaymentTransaction)) return false;
800
801 final PaymentTransaction other = (PaymentTransaction) o;
802
803 return timestamp == other.timestamp &&
804 uuid.equals(other.uuid) &&
805 payee.equals(other.payee) &&
806 direction == other.direction &&
807 state == other.state &&
808 note.equals(other.note) &&
809 amount.equals(other.amount) &&
810 Arrays.equals(transaction, other.transaction) &&
811 Arrays.equals(receipt, other.receipt) &&
812 paymentMetaData.equals(other.paymentMetaData);
813 }
814
815 @Override
816 public int hashCode() {
817 int result = uuid.hashCode();
818 result = 31 * result + payee.hashCode();
819 result = 31 * result + (int) (timestamp ^ (timestamp >>> 32));
820 result = 31 * result + direction.hashCode();
821 result = 31 * result + state.hashCode();
822 result = 31 * result + note.hashCode();
823 result = 31 * result + amount.hashCode();
824 result = 31 * result + Arrays.hashCode(transaction);
825 result = 31 * result + Arrays.hashCode(receipt);
826 result = 31 * result + paymentMetaData.hashCode();
827 return result;
828 }
829 }
830
831 private static @NonNull Payee fromPaymentTransaction(@Nullable RecipientId recipientId, @Nullable MobileCoinPublicAddress publicAddress) {
832 if (recipientId == null && publicAddress == null) {
833 throw new AssertionError();
834 }
835
836 if (recipientId != null) {
837 return Payee.fromRecipientAndAddress(recipientId, publicAddress);
838 } else {
839 return new Payee(publicAddress);
840 }
841 }
842
843 public final class PublicKeyConflictException extends Exception {
844 private PublicKeyConflictException() {}
845 }
846}