Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Handoff Protocol #140

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7e09e72
Initial handoff implementation, according to v14 specification.
koczadly May 19, 2021
d557f18
Update translations messages and generate arb.
koczadly May 19, 2021
b507dae
Encode state block link field as hex instead of account.
koczadly May 19, 2021
842100c
Update localization to add payment failed.
koczadly May 19, 2021
20e3040
Validate handoff amount to be one or greater.
koczadly May 20, 2021
023eeb8
Add local database for storing and re-using handoff payment specifica…
koczadly May 23, 2021
480472b
Add support for pasting block handoff URI into send sheet, and fix bu…
koczadly May 23, 2021
2c48e66
Rename SendTransaction database key and class names.
koczadly May 23, 2021
7420a5e
Fix bug where quicksend amount doesn't populate amount field in send …
koczadly May 23, 2021
7c97c12
Rename payments database table and classes.
koczadly May 24, 2021
188f593
Improve handoff localization messages.
koczadly May 24, 2021
8256192
Update localization and warning dialog.
koczadly May 24, 2021
680aced
Cleanup code.
koczadly May 26, 2021
b6e4ac3
Rename various handoff classes and methods, and improve code comments.
koczadly May 31, 2021
709738d
Update to revision 15 of the handoff specification, rename and cleanu…
koczadly Jun 18, 2021
6d81391
Allow variable duration of snackbar message, change default from 2.5s…
koczadly Jun 21, 2021
febde7a
Rename handoff invalid localization name.
koczadly Jun 21, 2021
2c941f6
Update to V16 of protocol specification, cleanup code and fix unit test.
koczadly Jun 21, 2021
94b0670
Rename variables, improve comments and cleanup code.
koczadly Jun 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@

<data android:scheme="manta" />
</intent-filter>
<!-- handoff uri scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="nanohandoff" />
</intent-filter>
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
1 change: 1 addition & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<string>xrb</string>
<string>nano</string>
<string>manta</string>
<string>nanohandoff</string>
</array>
</dict>
</array>
Expand Down
1 change: 1 addition & 0 deletions lib/appstate_container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ class StateContainerState extends State<StateContainer> {
encryptedSecret = null;
});
sl.get<DBHelper>().dropAccounts();
sl.get<DBHelper>().dropPayments();
sl.get<AccountService>().clearQueue();
}

Expand Down
44 changes: 43 additions & 1 deletion lib/l10n/intl_messages.arb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"@@last_modified": "2021-03-24T20:45:02.422993",
"@@last_modified": "2021-06-21T16:53:07.653251",
"cancel": "Cancel",
"@cancel": {
"description": "dialog_cancel",
Expand Down Expand Up @@ -192,6 +192,12 @@
"type": "text",
"placeholders": {}
},
"handoffInvalid": "The entered payment request isn't supported by Natrium, or contains invalid data.",
"@handoffInvalid": {
"description": "Handoff payment request error (unsupported/invalid)",
"type": "text",
"placeholders": {}
},
"removeContact": "Remove Contact",
"@removeContact": {
"description": "contact_remove_btn",
Expand Down Expand Up @@ -372,6 +378,30 @@
"type": "text",
"placeholders": {}
},
"handoffPaymentFailed": "Payment failed or rejected by the service",
"@handoffPaymentFailed": {
"description": "Payment failed or was rejected (sometimes followed by colon with details)",
"type": "text",
"placeholders": {}
},
"handoffPaymentAlreadyComplete": "You have already sent this payment.",
"@handoffPaymentAlreadyComplete": {
"description": "One-time handoff payment has already been made",
"type": "text",
"placeholders": {}
},
"handoffExpired": "This payment has expired",
"@handoffExpired": {
"description": "Handoff payment has expired (sometimes followed by colon with details)",
"type": "text",
"placeholders": {}
},
"usingHandoff": "Using block handoff",
"@usingHandoff": {
"description": "Payment will be processed \"using block handoff\" protocol",
"type": "text",
"placeholders": {}
},
"enterAmount": "Enter Amount",
"@enterAmount": {
"description": "send_amount_hint",
Expand Down Expand Up @@ -420,6 +450,18 @@
"type": "text",
"placeholders": {}
},
"paymentCannotReplay": "This payment cannot be replayed directly from the app. Return to the website or store to make another payment.",
"@paymentCannotReplay": {
"description": "Payment cannot be replayed (was a one-time payment)",
"type": "text",
"placeholders": {}
},
"sendDestinationWarning": "The protocol used to make this payment could not be detected. If you are sending funds to a website, exchange or business, you should verify that the destination address is correct before sending.",
"@sendDestinationWarning": {
"description": "Hint user that destination address should be checked first.",
"type": "text",
"placeholders": {}
},
"pinCreateTitle": "Create a 6-digit pin",
"@pinCreateTitle": {
"description": "pin_create_title",
Expand Down
44 changes: 44 additions & 0 deletions lib/localization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ class AppLocalization {
name: "qrUnknownError");
}

String get handoffInvalid {
return Intl.message(
"The entered payment request isn't supported by Natrium, or contains invalid data.",
desc: "Handoff payment request error (unsupported/invalid)",
name: 'handoffInvalid');
}

/// -- END GENERIC ITEMS

/// -- CONTACT ITEMS
Expand Down Expand Up @@ -344,6 +351,30 @@ class AppLocalization {
name: 'mantaError');
}

String get handoffPaymentFailed {
return Intl.message("Payment failed or rejected by the service",
desc: 'Payment failed or was rejected (sometimes followed by colon with details)',
name: 'handoffPaymentFailed');
}

String get handoffPaymentAlreadyComplete {
return Intl.message("You have already sent this payment.",
desc: 'One-time handoff payment has already been made',
name: 'handoffPaymentAlreadyComplete');
}

String get handoffExpired {
return Intl.message("This payment has expired",
desc: 'Handoff payment has expired (sometimes followed by colon with details)',
name: 'handoffExpired');
}

String get usingHandoff {
return Intl.message("Using block handoff",
desc: 'Payment will be processed "using block handoff" protocol',
name: 'usingHandoff');
}

String get enterAmount {
return Intl.message("Enter Amount",
desc: 'send_amount_hint', name: 'enterAmount');
Expand Down Expand Up @@ -383,6 +414,19 @@ class AppLocalization {
return Intl.message("Send From", desc: 'send_title', name: 'sendFrom');
}

String get paymentCannotReplay {
return Intl.message("This payment cannot be replayed directly from the app. Return to the website or store to make another payment.",
desc: 'Payment cannot be replayed (was a one-time payment)',
name: 'paymentCannotReplay');
}

String get sendDestinationWarning {
return Intl.message(
"The protocol used to make this payment could not be detected. If you are sending funds to a website, exchange or business, you should verify that the destination address is correct before sending.",
desc: 'Hint user that destination address should be checked first.',
name: 'sendDestinationWarning');
}

/// -- END SEND ITEMS

/// -- PIN SCREEN
Expand Down
34 changes: 27 additions & 7 deletions lib/model/address.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import 'dart:core';

import 'package:flutter_nano_ffi/flutter_nano_ffi.dart';
import 'package:logger/logger.dart';
import 'package:natrium_wallet_flutter/model/handoff/handoff_payment_req.dart';

import '../service_locator.dart';

// Object to represent an account address or address URI, and provide useful utilities
class Address {
String _address;
String _amount;
String _handoffData;

Address(String value) {
_parseAddressString(value);
}

String get address => _address;

String get amount => _amount;

String getShortString() {
Expand All @@ -28,6 +33,19 @@ class Address {
_address.substring(_address.length - 4);
}

/// Returns the handoff payment request encoded in the URI, or null if not
/// provided, unsupported or invalid.
HOPaymentRequest getHandoffPaymentReq() {
if (_handoffData == null)
return null;
try {
return HOPaymentRequest.fromBase64(_handoffData);
} catch (e) {
sl.get<Logger>().w("Invalid or unsupported handoff request in nano URI", e);
return null;
}
}

bool isValid() {
return _address == null
? false
Expand All @@ -36,17 +54,19 @@ class Address {

void _parseAddressString(String value) {
if (value != null) {
value = value.toLowerCase();
_address = NanoAccounts.findAccountInString(
NanoAccountType.NANO, value.replaceAll("\n", ""));
NanoAccountType.NANO, value.toLowerCase().replaceAll("\n", ""));
var split = value.split(':');
if (split.length > 1) {
Uri uri = Uri.tryParse(value);
if (uri != null && uri.queryParameters['amount'] != null) {
BigInt amount = BigInt.tryParse(uri.queryParameters['amount']);
if (amount != null) {
_amount = amount.toString();
if (uri != null) {
if (uri.queryParameters['amount'] != null) {
BigInt amount = BigInt.tryParse(uri.queryParameters['amount']);
if (amount != null) {
_amount = amount.toString();
}
}
_handoffData = uri.queryParameters['handoff'];
}
}
}
Expand Down
73 changes: 64 additions & 9 deletions lib/model/db/appdb.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import 'dart:async';
import 'dart:io' as io;
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';

import 'package:natrium_wallet_flutter/model/db/account.dart';
import 'package:natrium_wallet_flutter/model/db/contact.dart';
import 'package:natrium_wallet_flutter/model/db/payment.dart';
import 'package:natrium_wallet_flutter/util/nanoutil.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';

class DBHelper {
static const int DB_VERSION = 3;
static const int DB_VERSION = 4;

static const String CONTACTS_SQL = """CREATE TABLE Contacts(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
Expand All @@ -23,9 +25,14 @@ class DBHelper {
last_accessed INTEGER,
private_key TEXT,
balance TEXT)""";
static const String ACCOUNTS_ADD_ACCOUNT_COLUMN_SQL = """
ALTER TABLE Accounts ADD address TEXT
""";
static const String ACCOUNTS_ADD_ACCOUNT_COLUMN_SQL =
"ALTER TABLE Accounts ADD address TEXT";
static const String PAYMENTS_SQL = """CREATE TABLE Payments(
block_hash TEXT PRIMARY KEY,
reference TEXT,
protocol INTEGER,
protocol_data TEXT)""";

static Database _db;

NanoUtil _nanoUtil;
Expand Down Expand Up @@ -53,6 +60,7 @@ class DBHelper {
await db.execute(CONTACTS_SQL);
await db.execute(ACCOUNTS_SQL);
await db.execute(ACCOUNTS_ADD_ACCOUNT_COLUMN_SQL);
await db.execute(PAYMENTS_SQL);
}

void _onUpgrade(Database db, int oldVersion, int newVersion) async {
Expand All @@ -62,9 +70,12 @@ class DBHelper {
await db.execute(ACCOUNTS_ADD_ACCOUNT_COLUMN_SQL);
} else if (oldVersion == 2) {
await db.execute(ACCOUNTS_ADD_ACCOUNT_COLUMN_SQL);
} else if (oldVersion == 3) {
await db.execute(PAYMENTS_SQL);
}
}


// Contacts
Future<List<Contact>> getContacts() async {
var dbClient = await db;
Expand Down Expand Up @@ -337,6 +348,50 @@ class DBHelper {

Future<void> dropAccounts() async {
var dbClient = await db;
return await dbClient.rawDelete('DELETE FROM ACCOUNTS');
await dbClient.rawDelete('DELETE FROM ACCOUNTS');
}


// Payments
Future<PaymentInfo> getPayment(String hash) async {
var dbClient = await db;
List<Map> list = await dbClient.rawQuery(
"SELECT * FROM Payments WHERE block_hash=UPPER(?)",
[hash]);

if (list.length == 1) {
return PaymentInfo(
list[0]["reference"],
PaymentProtocolExt.fromInt(list[0]["protocol"]),
protocolData: list[0]["protocol_data"]);
}
return null;
}

Future<int> savePayment(String blockHash, PaymentInfo payment) async {
var dbClient = await db;
return await dbClient.rawInsert(
'REPLACE INTO Payments values(UPPER(?), ?, ?, ?)',
[
blockHash,
(payment.reference != null && payment.reference.length > 100)
? payment.reference.substring(0, 100)
: payment.reference,
payment.protocol.intVal,
payment.protocolData
]);
}

Future<void> dropPayments() async {
var dbClient = await db;
await dbClient.rawDelete('DELETE FROM Payments');
}



// block_hash TEXT PRIMARY KEY
// reference TEXT
// method INTEGER
// method_data BLOB

}
34 changes: 34 additions & 0 deletions lib/model/db/payment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

// Represents a SEND transaction
class PaymentInfo {
final String reference; // Reference text, tag or label
final PaymentProtocol protocol; // Protocol used to process the transaction
final String protocolData; // Optional data associated with the protocol

PaymentInfo(this.reference, this.protocol, {this.protocolData});
}


enum PaymentProtocol {
NONE, MANTA, HANDOFF
}

extension PaymentProtocolExt on PaymentProtocol {
static PaymentProtocol fromInt(int ordinal) {
switch (ordinal) {
case 1: return PaymentProtocol.NONE;
case 2: return PaymentProtocol.MANTA;
case 3: return PaymentProtocol.HANDOFF;
default: return null;
}
}

int get intVal {
switch (this) {
case PaymentProtocol.NONE: return 1;
case PaymentProtocol.MANTA: return 2;
case PaymentProtocol.HANDOFF: return 3;
default: return 0; // Null or unknown
}
}
}
Loading