mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
feat: add support for Ethereum EIP-681 URIs in PaymentRequest and implement test coverage (#2375)
* feat: add support for Ethereum EIP-681 URIs in PaymentRequest and implement test coverage * test: add unit test for EIP-681 URI handling with contract and no chainId [skip-ci] * fix(CW-1114): rounding error for really large numbers * fix: wrong decimals for eth fee display * fix: limit fractional digits to a maximum of `decimals` in `format_fixed` function [skip-ci] * test: add unit tests for `formatFixed` function
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import 'package:cw_core/parse_fixed.dart';
|
||||
|
||||
String formatFixed(BigInt value, int? decimals,
|
||||
{int? fractionalDigits, bool trimZeros = true}) {
|
||||
decimals ??= 0;
|
||||
fractionalDigits ??= decimals;
|
||||
|
||||
var multiplier = getMultiplier(decimals);
|
||||
// Make sure wei is a big number (convert as necessary)
|
||||
var negative = value.isNegative;
|
||||
if (negative) value = value * BigInt.from(-1);
|
||||
|
||||
var fraction = value
|
||||
.modPow(BigInt.one, BigInt.parse(multiplier))
|
||||
.toString()
|
||||
.padLeft(decimals, "0");
|
||||
|
||||
if (fractionalDigits < 0) fractionalDigits = 0;
|
||||
if (fractionalDigits > decimals) fractionalDigits = decimals;
|
||||
fraction = fraction.substring(0, fractionalDigits);
|
||||
|
||||
if (trimZeros) {
|
||||
fraction = removeTrailing("0", fraction);
|
||||
}
|
||||
|
||||
final whole = (value ~/ BigInt.parse(multiplier));
|
||||
|
||||
final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction";
|
||||
|
||||
if (negative) return "-$valString";
|
||||
|
||||
return valString;
|
||||
}
|
||||
|
||||
String removeTrailing(String pattern, String from) {
|
||||
if (pattern.isEmpty) return from;
|
||||
var i = from.length;
|
||||
while (i > 0 && from.startsWith(pattern, i - pattern.length)) {
|
||||
i -= pattern.length;
|
||||
}
|
||||
return from.substring(0, i);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('formatFixed', () {
|
||||
group('formatFixed, no fractional digits and trimming zeros', () {
|
||||
test('should format 1000000 into 1',
|
||||
() => expect(formatFixed(BigInt.parse("1000000"), 6), '1'));
|
||||
|
||||
test('should format 1000001 into 1.000001',
|
||||
() => expect(formatFixed(BigInt.parse("1000001"), 6), '1.000001'));
|
||||
});
|
||||
|
||||
group('formatFixed, different fractional digits and trimming zeros', () {
|
||||
test(
|
||||
'should format 1000001 into 1',
|
||||
() => expect(
|
||||
formatFixed(BigInt.parse("1000001"), 6, fractionalDigits: 5), '1'),
|
||||
);
|
||||
|
||||
test(
|
||||
'should format 1000000 into 1, fractionalDigits > decimals',
|
||||
() => expect(
|
||||
formatFixed(BigInt.parse("1000000"), 6, fractionalDigits: 12), '1'),
|
||||
);
|
||||
|
||||
test(
|
||||
'should format 1000001 into 1.000001, fractionalDigits > decimals',
|
||||
() => expect(
|
||||
formatFixed(BigInt.parse("1000001"), 6, fractionalDigits: 12),
|
||||
'1.000001',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('formatFixed, less fractional digits and not trimming zeros', () {
|
||||
test(
|
||||
'should format 1000000 into 1.000000',
|
||||
() => expect(
|
||||
formatFixed(BigInt.parse("1000000"), 6, trimZeros: false),
|
||||
'1.000000',
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
'should format 1000001 into 1.00000',
|
||||
() => expect(
|
||||
formatFixed(BigInt.parse("1000001"), 6,
|
||||
fractionalDigits: 5, trimZeros: false),
|
||||
'1.00000',
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
'should format 1000000 into 1.000000',
|
||||
() => expect(
|
||||
formatFixed(BigInt.parse("1000000"), 6,
|
||||
fractionalDigits: 12, trimZeros: false),
|
||||
'1.000000',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
import 'package:hex/hex.dart' as Hex;
|
||||
@@ -27,23 +27,17 @@ class PendingEVMChainTransaction with PendingTransaction {
|
||||
@override
|
||||
String get amountFormatted {
|
||||
if (isInfiniteApproval) return "∞";
|
||||
final _amount = (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString();
|
||||
return _amount.substring(0, min(10, _amount.length));
|
||||
return formatFixed(BigInt.parse(amount), exponent);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commit() async => await sendTransaction();
|
||||
|
||||
@override
|
||||
String get feeFormatted {
|
||||
return "$feeFormattedValue $feeCurrency";
|
||||
}
|
||||
String get feeFormatted => "$feeFormattedValue $feeCurrency";
|
||||
|
||||
@override
|
||||
String get feeFormattedValue {
|
||||
final _fee = (fee / BigInt.from(pow(10, 18))).toString();
|
||||
return _fee.substring(0, min(10, _fee.length));
|
||||
}
|
||||
String get feeFormattedValue => formatFixed(fee, 18, fractionalDigits: 10);
|
||||
|
||||
@override
|
||||
String get hex => bytesToHex(signedTransaction, include0x: true);
|
||||
@@ -53,7 +47,7 @@ class PendingEVMChainTransaction with PendingTransaction {
|
||||
final String eip1559Hex = '0x02${hex.substring(2)}';
|
||||
final Uint8List bytes = Uint8List.fromList(Hex.HEX.decode(eip1559Hex.substring(2)));
|
||||
|
||||
var txid = keccak256(bytes);
|
||||
final txid = keccak256(bytes);
|
||||
|
||||
return '0x${Hex.HEX.encode(txid)}';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:cake_wallet/nano/nano.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
|
||||
class PaymentRequest {
|
||||
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
|
||||
@@ -26,12 +28,21 @@ class PaymentRequest {
|
||||
callbackUrl = uri.queryParameters['callback'];
|
||||
callbackMessage = uri.queryParameters['callbackMessage'];
|
||||
walletType = uri.queryParameters['type'];
|
||||
|
||||
if (scheme == "ethereum") {
|
||||
final paymentUri = ERC681URI.fromUri(uri);
|
||||
|
||||
address = paymentUri.address;
|
||||
amount = paymentUri.amount;
|
||||
}
|
||||
}
|
||||
|
||||
if (scheme == "nano-gpt") {
|
||||
scheme = walletType ?? "nano";
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (nano != null) {
|
||||
if (amount.isNotEmpty) {
|
||||
if (address.contains("nano")) {
|
||||
@@ -61,3 +72,54 @@ class PaymentRequest {
|
||||
final String? callbackUrl;
|
||||
final String? callbackMessage;
|
||||
}
|
||||
|
||||
class ERC681URI extends PaymentURI {
|
||||
final int chainId;
|
||||
final String? contractAddress;
|
||||
|
||||
ERC681URI({
|
||||
required this.chainId,
|
||||
required super.address,
|
||||
required super.amount,
|
||||
required this.contractAddress,
|
||||
});
|
||||
|
||||
factory ERC681URI.fromUri(Uri uri) {
|
||||
final (isContract, targetAddress) = _getTargetAddress(uri.path);
|
||||
final chainId = _getChainID(uri.path);
|
||||
|
||||
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
|
||||
final amount = isContract
|
||||
? uri.queryParameters["uint256"]
|
||||
: uri.queryParameters["value"];
|
||||
|
||||
var formatedAmount = "";
|
||||
|
||||
if (amount != null) {
|
||||
formatedAmount = formatFixed(BigInt.parse(amount), 18);
|
||||
} else {
|
||||
formatedAmount = uri.queryParameters["amount"] ?? "";
|
||||
}
|
||||
|
||||
return ERC681URI(
|
||||
chainId: chainId,
|
||||
address: address,
|
||||
amount: formatedAmount,
|
||||
contractAddress: isContract ? targetAddress : null,
|
||||
);
|
||||
}
|
||||
|
||||
static int _getChainID(String path) {
|
||||
return int.parse(RegExp(
|
||||
r'@\d*',
|
||||
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
|
||||
"1");
|
||||
}
|
||||
|
||||
static (bool, String) _getTargetAddress(String path) {
|
||||
final targetAddress = RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false)
|
||||
.firstMatch(path)!
|
||||
.group(0)!;
|
||||
return (path.contains("/"), targetAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('PaymentRequest', () {
|
||||
group('Ethereum URIs', () {
|
||||
test("extract address and amount from EIP681 Uri with contract", () {
|
||||
final uri = Uri.parse(
|
||||
"ethereum:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41&uint256=2000000000000000000");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
expect(paymentRequest.amount, "2");
|
||||
});
|
||||
|
||||
test("extract address and amount from EIP681 Uri", () {
|
||||
final uri = Uri.parse(
|
||||
"ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?value=2000000000000000000");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
expect(paymentRequest.amount, "2");
|
||||
});
|
||||
|
||||
test("extract address and amount from Cake Style Uri", () {
|
||||
final uri =
|
||||
Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
expect(paymentRequest.amount, "2.00");
|
||||
});
|
||||
|
||||
test("extract address from EIP681 Uri with contract", () {
|
||||
final uri = Uri.parse(
|
||||
"ethereum:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174@1/transfer?address=0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
expect(paymentRequest.amount, "");
|
||||
});
|
||||
|
||||
test("extract address and amount from EIP681 Uri with contract and no chainId", () {
|
||||
final uri = Uri.parse(
|
||||
"ethereum:0x1234567890abcdef1234567890abcdef12345678/transfer?address=0xabcdef1234567890abcdef1234567890abcdef12&uint256=1000000000000000000");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xabcdef1234567890abcdef1234567890abcdef12");
|
||||
expect(paymentRequest.amount, "1");
|
||||
});
|
||||
|
||||
test("extract address from minimal EIP681 Uri", () {
|
||||
final uri = Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
expect(paymentRequest.amount, "");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user