gt:       if (amount.isNegative()) {
context: package org.thoughtcrime.securesms.database;
  
  import android.content.ContentValues;
  import android.content.Context;
  import android.database.Cursor;
  
  import androidx.annotation.AnyThread;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.WorkerThread;
  import androidx.lifecycle.LiveData;
  import androidx.lifecycle.MutableLiveData;
  
  import com.google.protobuf.InvalidProtocolBufferException;
  
  import org.signal.core.util.logging.Log;
  import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
  import org.thoughtcrime.securesms.database.model.databaseprotos.CryptoValue;
  import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
  import org.thoughtcrime.securesms.payments.CryptoValueUtil;
  import org.thoughtcrime.securesms.payments.Direction;
  import org.thoughtcrime.securesms.payments.FailureReason;
  import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
  import org.thoughtcrime.securesms.payments.Payee;
  import org.thoughtcrime.securesms.payments.Payment;
  import org.thoughtcrime.securesms.payments.State;
  import org.thoughtcrime.securesms.payments.proto.PaymentMetaData;
  import org.thoughtcrime.securesms.recipients.RecipientId;
  import org.thoughtcrime.securesms.util.Base64;
  import org.thoughtcrime.securesms.util.CursorUtil;
  import org.thoughtcrime.securesms.util.SqlUtil;
  import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
  import org.whispersystems.signalservice.api.payments.Money;
  
  import java.util.Arrays;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.UUID;
  
  public final class PaymentDatabase extends Database {
  
    private static final String TAG = Log.tag(PaymentDatabase.class);
  
    public static final String TABLE_NAME = "payments";
  
    private static final String ID           = "_id";
    private static final String PAYMENT_UUID = "uuid";
    private static final String RECIPIENT_ID = "recipient";
    private static final String ADDRESS      = "recipient_address";
    private static final String TIMESTAMP    = "timestamp";
    private static final String DIRECTION    = "direction";
    private static final String STATE        = "state";
    private static final String NOTE         = "note";
    private static final String AMOUNT       = "amount";
    private static final String FEE          = "fee";
    private static final String TRANSACTION  = "transaction_record";
    private static final String RECEIPT      = "receipt";
    private static final String PUBLIC_KEY   = "receipt_public_key";
    private static final String META_DATA    = "payment_metadata";
    private static final String FAILURE      = "failure_reason";
    private static final String BLOCK_INDEX  = "block_index";
    private static final String BLOCK_TIME   = "block_timestamp";
    private static final String SEEN         = "seen";
  
    public static final String CREATE_TABLE =
      "CREATE TABLE " + TABLE_NAME + "(" + ID           + " INTEGER PRIMARY KEY, " +
                                           PAYMENT_UUID + " TEXT DEFAULT NULL, " +
                                           RECIPIENT_ID + " INTEGER DEFAULT 0, " +
                                           ADDRESS      + " TEXT DEFAULT NULL, " +
                                           TIMESTAMP    + " INTEGER, " +
                                           NOTE         + " TEXT DEFAULT NULL, " +
                                           DIRECTION    + " INTEGER, " +
                                           STATE        + " INTEGER, " +
                                           FAILURE      + " INTEGER, " +
                                           AMOUNT       + " BLOB NOT NULL, " +
                                           FEE          + " BLOB NOT NULL, " +
                                           TRANSACTION  + " BLOB DEFAULT NULL, " +
                                           RECEIPT      + " BLOB DEFAULT NULL, " +
                                           META_DATA    + " BLOB DEFAULT NULL, " +
                                           PUBLIC_KEY   + " TEXT DEFAULT NULL, " +
                                           BLOCK_INDEX  + " INTEGER DEFAULT 0, " +
                                           BLOCK_TIME   + " INTEGER DEFAULT 0, " +
                                           SEEN         + " INTEGER, " +
                                           "UNIQUE(" + PAYMENT_UUID + ") ON CONFLICT ABORT)";
  
    public static final String[] CREATE_INDEXES = {
      "CREATE INDEX IF NOT EXISTS timestamp_direction_index ON " + TABLE_NAME + " (" + TIMESTAMP + ", " + DIRECTION + ");",
      "CREATE INDEX IF NOT EXISTS timestamp_index ON " + TABLE_NAME + " (" + TIMESTAMP + ");",
      "CREATE UNIQUE INDEX IF NOT EXISTS receipt_public_key_index ON " + TABLE_NAME + " (" + PUBLIC_KEY + ");"
    };
  
    private final MutableLiveData<Object> changeSignal;
  
    PaymentDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
      super(context, databaseHelper);
  
      this.changeSignal = new MutableLiveData<>(new Object());
    }
  
    @WorkerThread
    public void createIncomingPayment(@NonNull UUID uuid,
                                      @Nullable RecipientId fromRecipient,
                                      long timestamp,
                                      @NonNull String note,
                                      @NonNull Money amount,
                                      @NonNull Money fee,
                                      @NonNull byte[] receipt)
        throws PublicKeyConflictException
    {
      create(uuid, fromRecipient, null, timestamp, 0, note, Direction.RECEIVED, State.SUBMITTED, amount, fee, null, receipt, null, false);
    }
  
    @WorkerThread
    public void createOutgoingPayment(@NonNull UUID uuid,
                                      @Nullable RecipientId toRecipient,
                                      @NonNull MobileCoinPublicAddress publicAddress,
                                      long timestamp,
                                      @NonNull String note,
                                      @NonNull Money amount)
    {
      try {
        create(uuid, toRecipient, publicAddress, timestamp, 0, note, Direction.SENT, State.INITIAL, amount, amount.toZero(), null, null, null, true);
      } catch (PublicKeyConflictException e) {
        Log.w(TAG, "Tried to create payment but the public key appears already in the database", e);
        throw new AssertionError(e);
      }
    }
  
    /**
     * Inserts a payment in its final successful state.
     * <p>
     * This is for when a linked device has told us about the payment only.
     */
    @WorkerThread
    public void createSuccessfulPayment(@NonNull UUID uuid,
                                        @Nullable RecipientId toRecipient,
                                        @NonNull MobileCoinPublicAddress publicAddress,
                                        long timestamp,
                                        long blockIndex,
                                        @NonNull String note,
                                        @NonNull Money amount,
                                        @NonNull Money fee,
                                        @NonNull byte[] receipt,
                                        @NonNull PaymentMetaData metaData)
    {
      try {
        create(uuid, toRecipient, publicAddress, timestamp, blockIndex, note, Direction.SENT, State.SUCCESSFUL, amount, fee, null, receipt, metaData, true);
      } catch (PublicKeyConflictException e) {
        Log.w(TAG, "Tried to create payment but the public key appears already in the database", e);
        throw new AssertionError(e);
      }
    }
  
    @WorkerThread
    public void createDefrag(@NonNull UUID uuid,
                             @Nullable RecipientId self,
                             @NonNull MobileCoinPublicAddress selfPublicAddress,
                             long timestamp,
                             @NonNull Money fee,
                             @NonNull byte[] transaction,
                             @NonNull byte[] receipt)
    {
      try {
        create(uuid, self, selfPublicAddress, timestamp, 0, "", Direction.SENT, State.SUBMITTED, fee.toZero(), fee, transaction, receipt, null, true);
      } catch (PublicKeyConflictException e) {
        Log.w(TAG, "Tried to create payment but the public key appears already in the database", e);
        throw new AssertionError(e);
      }
    }
  
    @WorkerThread
    private void create(@NonNull UUID uuid,
                        @Nullable RecipientId recipientId,
                        @Nullable MobileCoinPublicAddress publicAddress,
                        long timestamp,
                        long blockIndex,
                        @NonNull String note,
                        @NonNull Direction direction,
                        @NonNull State state,
                        @NonNull Money amount,
                        @NonNull Money fee,
                        @Nullable byte[] transaction,
                        @Nullable byte[] receipt,
                        @Nullable PaymentMetaData metaData,
                        boolean seen)
        throws PublicKeyConflictException
    {
      if (recipientId == null && publicAddress == null) {
        throw new AssertionError();
      }
  
      if (amount.isNegative()) {
        throw new AssertionError();
      }
  
      if (fee.isNegative()) {
        throw new AssertionError();
      }
  
      SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
      ContentValues  values   = new ContentValues(15);
  
      values.put(PAYMENT_UUID, uuid.toString());
      if (recipientId == null || recipientId.isUnknown()) {
        values.put(RECIPIENT_ID, 0);
      } else {
        values.put(RECIPIENT_ID, recipientId.serialize());
      }
      if (publicAddress == null) {
        values.putNull(ADDRESS);
      } else {
        values.put(ADDRESS, publicAddress.getPaymentAddressBase58());
      }
      values.put(TIMESTAMP, timestamp);
      values.put(BLOCK_INDEX, blockIndex);
      values.put(NOTE, note);
      values.put(DIRECTION, direction.serialize());
      values.put(STATE, state.serialize());
      values.put(AMOUNT, CryptoValueUtil.moneyToCryptoValue(amount).toByteArray());
      values.put(FEE, CryptoValueUtil.moneyToCryptoValue(fee).toByteArray());
      if (transaction != null) {
        values.put(TRANSACTION, transaction);
      } else {
        values.putNull(TRANSACTION);
      }
      if (receipt != null) {
        values.put(RECEIPT, receipt);
        values.put(PUBLIC_KEY, Base64.encodeBytes(PaymentMetaDataUtil.receiptPublic(PaymentMetaDataUtil.fromReceipt(receipt))));
      } else {
        values.putNull(RECEIPT);
        values.putNull(PUBLIC_KEY);
      }
      if (metaData != null) {
        values.put(META_DATA, metaData.toByteArray());
      } else {
        values.put(META_DATA, PaymentMetaDataUtil.fromReceiptAndTransaction(receipt, transaction).toByteArray());
      }
      values.put(SEEN, seen ? 1 : 0);
  
      long inserted = database.insert(TABLE_NAME, null, values);
  
      if (inserted == -1) {
        throw new PublicKeyConflictException();
      }
  
      notifyChanged(uuid);
    }
  
    public void deleteAll() {
      SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
      database.delete(TABLE_NAME, null, null);
      Log.i(TAG, "Deleted all records");
    }
  
    @WorkerThread
    public boolean delete(@NonNull UUID uuid) {
      SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
      String         where    = PAYMENT_UUID + " = ?";
      String[]       args     = {uuid.toString()};
      int            deleted;
  
      database.beginTransaction();
      try {
        deleted = database.delete(TABLE_NAME, where, args);
  
        if (deleted > 1) {
          Log.w(TAG, "More than one row matches criteria");
          throw new AssertionError();
        }
        database.setTransactionSuccessful();
      } finally {
        database.endTransaction();
      }
  
      if (deleted > 0) {
        notifyChanged(uuid);
      }
  
      return deleted > 0;
    }
  
    @WorkerThread
    public @NonNull List<PaymentTransaction> getAll() {
      SQLiteDatabase           database = databaseHelper.getSignalReadableDatabase();
      List<PaymentTransaction> result   = new LinkedList<>();
  
      try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, TIMESTAMP + " DESC")) {
        while (cursor.moveToNext()) {
          result.add(readPayment(cursor));
        }
      }
  
      return result;
    }
  
    @WorkerThread
    public void markAllSeen() {
      SQLiteDatabase database         = databaseHelper.getSignalWritableDatabase();
      ContentValues  values           = new ContentValues(1);
      List<UUID>     unseenIds        = new LinkedList<>();
      String[]       unseenProjection = SqlUtil.buildArgs(PAYMENT_UUID);
      String         unseenWhile      = SEEN + " != ?";
      String[]       unseenArgs       = SqlUtil.buildArgs("1");
      int            updated          = -1;
  
      values.put(SEEN, 1);
  
      try {
        database.beginTransaction();
  
        try (Cursor cursor = database.query(TABLE_NAME, unseenProjection, unseenWhile, unseenArgs, null, null, null)) {
          while (cursor != null && cursor.moveToNext()) {
            unseenIds.add(UUID.fromString(CursorUtil.requireString(cursor, PAYMENT_UUID)));
          }
        }
  
        if (!unseenIds.isEmpty()) {
          updated = database.update(TABLE_NAME, values, null, null);
        }
  
        database.setTransactionSuccessful();
      } finally {
        database.endTransaction();
      }
  
      if (updated > 0) {
        for (final UUID unseenId : unseenIds) {
          notifyUuidChanged(unseenId);
        }
  
        notifyChanged();
      }
    }
  
    @WorkerThread
    public void markPaymentSeen(@NonNull UUID uuid) {
      SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
      ContentValues  values   = new ContentValues(1);
      String         where    = PAYMENT_UUID + " = ?";
      String[]       args     = {uuid.toString()};
  
      values.put(SEEN, 1);
      int updated = database.update(TABLE_NAME, values, where, args);
  
      if (updated > 0) {
        notifyChanged(uuid);
      }
    }
  
    @WorkerThread
    public @NonNull List<PaymentTransaction> getUnseenPayments() {
      SQLiteDatabase           db      = databaseHelper.getSignalReadableDatabase();
      String                   query   = SEEN + " = 0 AND " + STATE + " = " + State.SUCCESSFUL.serialize();
      List<PaymentTransaction> results = new LinkedList<>();
  
      try (Cursor cursor = db.query(TABLE_NAME, null, query, null, null, null, null)) {
        while (cursor.moveToNext()) {
          results.add(readPayment(cursor));
        }
      }
  
      return results;
    }
  
    @WorkerThread
    public @Nullable PaymentTransaction getPayment(@NonNull UUID uuid) {
      SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
      String         select   = PAYMENT_UUID + " = ?";
      String[]       args     = {uuid.toString()};
  
      try (Cursor cursor = database.query(TABLE_NAME, null, select, args, null, null, null)) {
        if (cursor.moveToNext()) {
          PaymentTransaction payment = readPayment(cursor);
  
          if (cursor.moveToNext()) {
            throw new AssertionError("Multiple records for one UUID");
          }
  
          return payment;
        } else {
          return null;
        }
      }
    }
  
    @AnyThread
    public @NonNull LiveData<List<PaymentTransaction>> getAllLive() {
      return LiveDataUtil.mapAsync(changeSignal, change -> getAll());
    }
  
    public boolean markPaymentSubmitted(@NonNull UUID uuid,
                                        @NonNull byte[] transaction,
                                        @NonNull byte[] receipt,
                                        @NonNull Money fee)
        throws PublicKeyConflictException
    {
      SQLiteDatabase database  = databaseHelper.getSignalWritableDatabase();
      String         where     = PAYMENT_UUID + " = ?";
      String[]       whereArgs = {uuid.toString()};
      int            updated;
      ContentValues  values    = new ContentValues(6);
  
      values.put(STATE, State.SUBMITTED.serialize());
      values.put(TRANSACTION, transaction);
      values.put(RECEIPT, receipt);
      values.put(PUBLIC_KEY, Base64.encodeBytes(PaymentMetaDataUtil.receiptPublic(PaymentMetaDataUtil.fromReceipt(receipt))));
      values.put(META_DATA, PaymentMetaDataUtil.fromReceiptAndTransaction(receipt, transaction).toByteArray());
      values.put(FEE, CryptoValueUtil.moneyToCryptoValue(fee).toByteArray());
  
      database.beginTransaction();
      try {
        updated = database.update(TABLE_NAME, values, where, whereArgs);
  
        if (updated == -1) {
          throw new PublicKeyConflictException();
        }
  
        if (updated > 1) {
          Log.w(TAG, "More than one row matches criteria");
          throw new AssertionError();
        }
        database.setTransactionSuccessful();
      } finally {
        database.endTransaction();
      }
  
      if (updated > 0) {
        notifyChanged(uuid);
      }
  
      return updated > 0;
    }
  
    public boolean markPaymentSuccessful(@NonNull UUID uuid, long blockIndex) {
      return markPayment(uuid, State.SUCCESSFUL, null, null, blockIndex);
    }
  
    public boolean markReceivedPaymentSuccessful(@NonNull UUID uuid, @NonNull Money amount, long blockIndex) {
      return markPayment(uuid, State.SUCCESSFUL, amount, null, blockIndex);
    }
  
    public boolean markPaymentFailed(@NonNull UUID uuid, @NonNull FailureReason failureReason) {
      return markPayment(uuid, State.FAILED, null, failureReason, null);
    }
  
    private boolean markPayment(@NonNull UUID uuid,
                                @NonNull State state,
                                @Nullable Money amount,
                                @Nullable FailureReason failureReason,
                                @Nullable Long blockIndex)
    {
      SQLiteDatabase database  = databaseHelper.getSignalWritableDatabase();
      String         where     = PAYMENT_UUID + " = ?";
      String[]       whereArgs = {uuid.toString()};
      int            updated;
      ContentValues  values    = new ContentValues(3);
  
      values.put(STATE, state.serialize());
  
      if (amount != null) {
        values.put(AMOUNT, CryptoValueUtil.moneyToCryptoValue(amount).toByteArray());
      }
  
      if (state == State.FAILED) {
        values.put(FAILURE, failureReason != null ? failureReason.serialize()
                                                  : FailureReason.UNKNOWN.serialize());
      } else {
        if (failureReason != null) {
          throw new AssertionError();
        }
        values.putNull(FAILURE);
      }
  
      if (blockIndex != null) {
        values.put(BLOCK_INDEX, blockIndex);
      }
  
      database.beginTransaction();
      try {
        updated = database.update(TABLE_NAME, values, where, whereArgs);
  
        if (updated > 1) {
          Log.w(TAG, "More than one row matches criteria");
          throw new AssertionError();
        }
        database.setTransactionSuccessful();
      } finally {
        database.endTransaction();
      }
  
      if (updated > 0) {
        notifyChanged(uuid);
      }
  
      return updated > 0;
    }
  
    public boolean updateBlockDetails(@NonNull UUID uuid,
                                      long blockIndex,
                                      long blockTimestamp)
    {
      SQLiteDatabase database  = databaseHelper.getSignalWritableDatabase();
      String         where     = PAYMENT_UUID + " = ?";
      String[]       whereArgs = {uuid.toString()};
      int            updated;
      ContentValues  values    = new ContentValues(2);
  
      values.put(BLOCK_INDEX, blockIndex);
      values.put(BLOCK_TIME, blockTimestamp);
  
      database.beginTransaction();
      try {
        updated = database.update(TABLE_NAME, values, where, whereArgs);
  
        if (updated > 1) {
          Log.w(TAG, "More than one row matches criteria");
          throw new AssertionError();
        }
        database.setTransactionSuccessful();
      } finally {
        database.endTransaction();
      }
  
      if (updated > 0) {
        notifyChanged(uuid);
      }
  
      return updated > 0;
    }
  
    private static @NonNull PaymentTransaction readPayment(@NonNull Cursor cursor) {
      return new PaymentTransaction(UUID.fromString(CursorUtil.requireString(cursor, PAYMENT_UUID)),
                                    getRecipientId(cursor),
                                    MobileCoinPublicAddress.fromBase58NullableOrThrow(CursorUtil.requireString(cursor, ADDRESS)),
                                    CursorUtil.requireLong(cursor, TIMESTAMP),
                                    Direction.deserialize(CursorUtil.requireInt(cursor, DIRECTION)),
                                    State.deserialize(CursorUtil.requireInt(cursor, STATE)),
                                    FailureReason.deserialize(CursorUtil.requireInt(cursor, FAILURE)),
                                    CursorUtil.requireString(cursor, NOTE),
                                    getMoneyValue(CursorUtil.requireBlob(cursor, AMOUNT)),
                                    getMoneyValue(CursorUtil.requireBlob(cursor, FEE)),
                                    CursorUtil.requireBlob(cursor, TRANSACTION),
                                    CursorUtil.requireBlob(cursor, RECEIPT),
                                    PaymentMetaDataUtil.parseOrThrow(CursorUtil.requireBlob(cursor, META_DATA)),
                                    CursorUtil.requireLong(cursor, BLOCK_INDEX),
                                    CursorUtil.requireLong(cursor, BLOCK_TIME),
                                    CursorUtil.requireBoolean(cursor, SEEN));
    }
  
    private static @Nullable RecipientId getRecipientId(@NonNull Cursor cursor) {
      long id = CursorUtil.requireLong(cursor, RECIPIENT_ID);
      if (id == 0) return null;
      return RecipientId.from(id);
    }
  
    private static @NonNull Money getMoneyValue(@NonNull byte[] blob) {
      try {
        CryptoValue cryptoValue = CryptoValue.parseFrom(blob);
        return CryptoValueUtil.cryptoValueToMoney(cryptoValue);
      } catch (InvalidProtocolBufferException e) {
        throw new AssertionError(e);
      }
    }
  
    /**
     * notifyChanged will alert the database observer for two events:
     *
     * 1. It will alert the global payments observer that something changed
     * 2. It will alert the uuid specific observer that something will change.
     *
     * You should not call this in a tight loop, opting to call notifyUuidChanged instead.
     */
    private void notifyChanged(@Nullable UUID uuid) {
      notifyChanged();
      notifyUuidChanged(uuid);
    }
  
    /**
     * Notifies the global payments observer that something changed.
     */
    private void notifyChanged() {
      changeSignal.postValue(new Object());
      ApplicationDependencies.getDatabaseObserver().notifyAllPaymentsListeners();
    }
  
    /**
     * Notify the database observer of a change for a specific uuid. Does not trigger
     * the global payments observer.
     */
    private void notifyUuidChanged(@Nullable UUID uuid) {
      if (uuid != null) {
        ApplicationDependencies.getDatabaseObserver().notifyPaymentListeners(uuid);
      }
    }
  
    public static final class PaymentTransaction implements Payment {
      private final UUID            uuid;
      private final Payee           payee;
      private final long            timestamp;
      private final Direction       direction;
      private final State           state;
      private final FailureReason   failureReason;
      private final String          note;
      private final Money           amount;
      private final Money           fee;
      private final byte[]          transaction;
      private final byte[]          receipt;
      private final PaymentMetaData paymentMetaData;
      private final Long            blockIndex;
      private final long            blockTimestamp;
      private final boolean         seen;
  
      PaymentTransaction(@NonNull UUID uuid,
                         @Nullable RecipientId recipientId,
                         @Nullable MobileCoinPublicAddress publicAddress,
                         long timestamp,
                         @NonNull Direction direction,
                         @NonNull State state,
                         @Nullable FailureReason failureReason,
                         @NonNull String note,
                         @NonNull Money amount,
                         @NonNull Money fee,
                         @Nullable byte[] transaction,
                         @Nullable byte[] receipt,
                         @NonNull PaymentMetaData paymentMetaData,
                         @Nullable Long blockIndex,
                         long blockTimestamp,
                         boolean seen)
      {
        this.uuid            = uuid;
        this.paymentMetaData = paymentMetaData;
        this.payee           = fromPaymentTransaction(recipientId, publicAddress);
        this.timestamp       = timestamp;
        this.direction       = direction;
        this.state           = state;
        this.failureReason   = failureReason;
        this.note            = note;
        this.amount          = amount;
        this.fee             = fee;
        this.transaction     = transaction;
        this.receipt         = receipt;
        this.blockIndex      = blockIndex;
        this.blockTimestamp  = blockTimestamp;
        this.seen            = seen;
  
  