mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user