That fuck shit the fascists are using
at master 846 lines 31 kB view raw
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}