kopia lustrzana https://github.com/ryukoposting/Signal-Android
791 wiersze
28 KiB
Java
791 wiersze
28 KiB
Java
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 com.mobilecoin.lib.exceptions.SerializationException;
|
|
|
|
import org.signal.core.util.logging.Log;
|
|
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.signal.core.util.CursorUtil;
|
|
import org.signal.core.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 implements RecipientIdDatabaseReference {
|
|
|
|
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 SignalDatabase 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, SerializationException
|
|
{
|
|
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 IllegalArgumentException(e);
|
|
} catch (SerializationException e) {
|
|
throw new IllegalArgumentException(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)
|
|
throws SerializationException
|
|
{
|
|
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);
|
|
} catch (SerializationException e) {
|
|
throw new IllegalArgumentException(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, SerializationException
|
|
{
|
|
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());
|
|
}
|
|
|
|
@Override
|
|
public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) {
|
|
ContentValues values = new ContentValues();
|
|
values.put(RECIPIENT_ID, toId.serialize());
|
|
getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(fromId));
|
|
}
|
|
|
|
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);
|
|
try {
|
|
values.put(PUBLIC_KEY, Base64.encodeBytes(PaymentMetaDataUtil.receiptPublic(PaymentMetaDataUtil.fromReceipt(receipt))));
|
|
values.put(META_DATA, PaymentMetaDataUtil.fromReceiptAndTransaction(receipt, transaction).toByteArray());
|
|
} catch (SerializationException e) {
|
|
throw new IllegalArgumentException(e);
|
|
}
|
|
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;
|
|
|
|
if (amount.isNegative()) {
|
|
throw new AssertionError();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public @NonNull UUID getUuid() {
|
|
return uuid;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Payee getPayee() {
|
|
return payee;
|
|
}
|
|
|
|
@Override
|
|
public long getBlockIndex() {
|
|
return blockIndex;
|
|
}
|
|
|
|
@Override
|
|
public long getBlockTimestamp() {
|
|
return blockTimestamp;
|
|
}
|
|
|
|
@Override
|
|
public long getTimestamp() {
|
|
return timestamp;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Direction getDirection() {
|
|
return direction;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull State getState() {
|
|
return state;
|
|
}
|
|
|
|
@Override
|
|
public @Nullable FailureReason getFailureReason() {
|
|
return failureReason;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull String getNote() {
|
|
return note;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Money getAmount() {
|
|
return amount;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Money getFee() {
|
|
return fee;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull PaymentMetaData getPaymentMetaData() {
|
|
return paymentMetaData;
|
|
}
|
|
|
|
@Override
|
|
public boolean isSeen() {
|
|
return seen;
|
|
}
|
|
|
|
public @Nullable byte[] getTransaction() {
|
|
return transaction;
|
|
}
|
|
|
|
public @Nullable byte[] getReceipt() {
|
|
return receipt;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(@Nullable Object o) {
|
|
if (this == o) return true;
|
|
if (!(o instanceof PaymentTransaction)) return false;
|
|
|
|
final PaymentTransaction other = (PaymentTransaction) o;
|
|
|
|
return timestamp == other.timestamp &&
|
|
uuid.equals(other.uuid) &&
|
|
payee.equals(other.payee) &&
|
|
direction == other.direction &&
|
|
state == other.state &&
|
|
note.equals(other.note) &&
|
|
amount.equals(other.amount) &&
|
|
Arrays.equals(transaction, other.transaction) &&
|
|
Arrays.equals(receipt, other.receipt) &&
|
|
paymentMetaData.equals(other.paymentMetaData);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
int result = uuid.hashCode();
|
|
result = 31 * result + payee.hashCode();
|
|
result = 31 * result + (int) (timestamp ^ (timestamp >>> 32));
|
|
result = 31 * result + direction.hashCode();
|
|
result = 31 * result + state.hashCode();
|
|
result = 31 * result + note.hashCode();
|
|
result = 31 * result + amount.hashCode();
|
|
result = 31 * result + Arrays.hashCode(transaction);
|
|
result = 31 * result + Arrays.hashCode(receipt);
|
|
result = 31 * result + paymentMetaData.hashCode();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private static @NonNull Payee fromPaymentTransaction(@Nullable RecipientId recipientId, @Nullable MobileCoinPublicAddress publicAddress) {
|
|
if (recipientId == null && publicAddress == null) {
|
|
throw new AssertionError();
|
|
}
|
|
|
|
if (recipientId != null) {
|
|
return Payee.fromRecipientAndAddress(recipientId, publicAddress);
|
|
} else {
|
|
return new Payee(publicAddress);
|
|
}
|
|
}
|
|
|
|
public final class PublicKeyConflictException extends Exception {
|
|
private PublicKeyConflictException() {}
|
|
}
|
|
}
|