Payments.

Co-authored-by: Alan Evans <alan@signal.org>
Co-authored-by: Alex Hart <alex@signal.org>
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Android Team
2021-04-06 13:03:33 -03:00
committed by Alan Evans
parent c42023855b
commit fddba2906a
311 changed files with 18956 additions and 235 deletions

View File

@@ -63,6 +63,7 @@ public class DatabaseFactory {
private final StorageKeyDatabase storageKeyDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
private final PaymentDatabase paymentDatabase;
public static DatabaseFactory getInstance(Context context) {
if (instance == null) {
@@ -165,6 +166,10 @@ public class DatabaseFactory {
return getInstance(context).mentionDatabase;
}
public static PaymentDatabase getPaymentDatabase(Context context) {
return getInstance(context).paymentDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase().getSqlCipherDatabase();
}
@@ -217,6 +222,7 @@ public class DatabaseFactory {
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@@ -1,8 +1,6 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.database.ContentObserver;
import android.database.Cursor;
import androidx.annotation.NonNull;
@@ -13,6 +11,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executor;
/**
@@ -28,6 +27,8 @@ public final class DatabaseObserver {
private final Set<Observer> conversationListObservers;
private final Map<Long, Set<Observer>> conversationObservers;
private final Map<Long, Set<Observer>> verboseConversationObservers;
private final Map<UUID, Set<Observer>> paymentObservers;
private final Set<Observer> allPaymentsObservers;
public DatabaseObserver(Application application) {
this.application = application;
@@ -35,6 +36,8 @@ public final class DatabaseObserver {
this.conversationListObservers = new HashSet<>();
this.conversationObservers = new HashMap<>();
this.verboseConversationObservers = new HashMap<>();
this.paymentObservers = new HashMap<>();
this.allPaymentsObservers = new HashSet<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
@@ -55,11 +58,24 @@ public final class DatabaseObserver {
});
}
public void registerPaymentObserver(@NonNull UUID paymentId, @NonNull Observer listener) {
executor.execute(() -> {
registerMapped(paymentObservers, paymentId, listener);
});
}
public void registerAllPaymentsObserver(@NonNull Observer listener) {
executor.execute(() -> {
allPaymentsObservers.add(listener);
});
}
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
unregisterMapped(conversationObservers, listener);
unregisterMapped(verboseConversationObservers, listener);
unregisterMapped(paymentObservers, listener);
});
}
@@ -105,6 +121,18 @@ public final class DatabaseObserver {
application.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
public void notifyPaymentListeners(@NonNull UUID paymentId) {
executor.execute(() -> {
notifyMapped(paymentObservers, paymentId);
});
}
public void notifyAllPaymentsListeners() {
executor.execute(() -> {
notifySet(allPaymentsObservers);
});
}
private <K> void registerMapped(@NonNull Map<K, Set<Observer>> map, @NonNull K key, @NonNull Observer listener) {
Set<Observer> listeners = map.get(key);
@@ -132,6 +160,12 @@ public final class DatabaseObserver {
}
}
public static void notifySet(@NonNull Set<Observer> set) {
for (final Observer observer : set) {
observer.onChanged();
}
}
public interface Observer {
/**
* Called when the relevant data changes. Executed on a serial executor, so don't do any

View File

@@ -0,0 +1,774 @@
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.getWritableDatabase();
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.getWritableDatabase();
database.delete(TABLE_NAME, null, null);
Log.i(TAG, "Deleted all records");
}
@WorkerThread
public boolean delete(@NonNull UUID uuid) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
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.getReadableDatabase();
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.getWritableDatabase();
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.getWritableDatabase();
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.getReadableDatabase();
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.getReadableDatabase();
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.getWritableDatabase();
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.getWritableDatabase();
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.getWritableDatabase();
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() {}
}
}

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.database;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mobilecoin.lib.KeyImage;
import com.mobilecoin.lib.Receipt;
import com.mobilecoin.lib.RistrettoPublic;
import com.mobilecoin.lib.Transaction;
import com.mobilecoin.lib.exceptions.SerializationException;
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData;
import java.util.List;
import java.util.Set;
public final class PaymentMetaDataUtil {
public static PaymentMetaData parseOrThrow(byte[] requireBlob) {
try {
return PaymentMetaData.parseFrom(requireBlob);
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
}
public static @NonNull PaymentMetaData fromReceipt(@Nullable byte[] receipt) {
PaymentMetaData.MobileCoinTxoIdentification.Builder builder = PaymentMetaData.MobileCoinTxoIdentification.newBuilder();
if (receipt != null) {
addReceiptData(receipt, builder);
}
return PaymentMetaData.newBuilder().setMobileCoinTxoIdentification(builder).build();
}
public static @NonNull PaymentMetaData fromKeysAndImages(@NonNull List<ByteString> publicKeys, @NonNull List<ByteString> keyImages) {
PaymentMetaData.MobileCoinTxoIdentification.Builder builder = PaymentMetaData.MobileCoinTxoIdentification.newBuilder();
builder.addAllKeyImages(keyImages);
builder.addAllPublicKey(publicKeys);
return PaymentMetaData.newBuilder().setMobileCoinTxoIdentification(builder).build();
}
public static @NonNull PaymentMetaData fromReceiptAndTransaction(@Nullable byte[] receipt, @Nullable byte[] transaction) {
PaymentMetaData.MobileCoinTxoIdentification.Builder builder = PaymentMetaData.MobileCoinTxoIdentification.newBuilder();
if (transaction != null) {
addTransactionData(transaction, builder);
} else if (receipt != null) {
addReceiptData(receipt, builder);
}
return PaymentMetaData.newBuilder().setMobileCoinTxoIdentification(builder).build();
}
private static void addReceiptData(@NonNull byte[] receipt, PaymentMetaData.MobileCoinTxoIdentification.Builder builder) {
try {
RistrettoPublic publicKey = Receipt.fromBytes(receipt).getPublicKey();
addPublicKey(builder, publicKey);
} catch (SerializationException e) {
throw new AssertionError(e);
}
}
private static void addTransactionData(@NonNull byte[] transactionBytes, PaymentMetaData.MobileCoinTxoIdentification.Builder builder) {
try {
Transaction transaction = Transaction.fromBytes(transactionBytes);
Set<KeyImage> keyImages = transaction.getKeyImages();
for (KeyImage keyImage : keyImages) {
builder.addKeyImages(ByteString.copyFrom(keyImage.getData()));
}
for (RistrettoPublic publicKey : transaction.getOutputPublicKeys()) {
addPublicKey(builder, publicKey);
}
} catch (SerializationException e) {
throw new AssertionError(e);
}
}
private static void addPublicKey(@NonNull PaymentMetaData.MobileCoinTxoIdentification.Builder builder, @NonNull RistrettoPublic publicKey) {
builder.addPublicKey(ByteString.copyFrom(publicKey.getKeyBytes()));
}
public static byte[] receiptPublic(@NonNull PaymentMetaData paymentMetaData) {
return Stream.of(paymentMetaData.getMobileCoinTxoIdentification().getPublicKeyList()).single().toByteArray();
}
}

View File

@@ -18,7 +18,6 @@ import com.annimon.stream.Stream;
import com.bumptech.glide.Glide;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.signal.core.util.logging.Log;
@@ -30,12 +29,10 @@ import org.thoughtcrime.securesms.database.DraftDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
import org.thoughtcrime.securesms.database.MentionDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PaymentDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
@@ -172,8 +169,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int WALLPAPER = 88;
private static final int ABOUT = 89;
private static final int SPLIT_SYSTEM_NAMES = 90;
private static final int PAYMENTS = 91;
private static final int DATABASE_VERSION = 90;
private static final int DATABASE_VERSION = 91;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -204,6 +202,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL(StickerDatabase.CREATE_TABLE);
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
db.execSQL(MentionDatabase.CREATE_TABLE);
db.execSQL(PaymentDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
@@ -218,6 +217,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
executeStatements(db, StickerDatabase.CREATE_INDEXES);
executeStatements(db, StorageKeyDatabase.CREATE_INDEXES);
executeStatements(db, MentionDatabase.CREATE_INDEXES);
executeStatements(db, PaymentDatabase.CREATE_INDEXES);
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
@@ -1265,6 +1265,32 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL("UPDATE recipient SET system_given_name = system_display_name");
}
if (oldVersion < PAYMENTS) {
db.execSQL("CREATE TABLE payments(_id INTEGER PRIMARY KEY, " +
"uuid TEXT DEFAULT NULL, " +
"recipient INTEGER DEFAULT 0, " +
"recipient_address TEXT DEFAULT NULL, " +
"timestamp INTEGER, " +
"note TEXT DEFAULT NULL, " +
"direction INTEGER, " +
"state INTEGER, " +
"failure_reason INTEGER, " +
"amount BLOB NOT NULL, " +
"fee BLOB NOT NULL, " +
"transaction_record BLOB DEFAULT NULL, " +
"receipt BLOB DEFAULT NULL, " +
"payment_metadata BLOB DEFAULT NULL, " +
"receipt_public_key TEXT DEFAULT NULL, " +
"block_index INTEGER DEFAULT 0, " +
"block_timestamp INTEGER DEFAULT 0, " +
"seen INTEGER, " +
"UNIQUE(uuid) ON CONFLICT ABORT)");
db.execSQL("CREATE INDEX IF NOT EXISTS timestamp_direction_index ON payments (timestamp, direction);");
db.execSQL("CREATE INDEX IF NOT EXISTS timestamp_index ON payments (timestamp);");
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS receipt_public_key_index ON payments (receipt_public_key);");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();