diff --git a/cw_core/lib/format_fixed.dart b/cw_core/lib/format_fixed.dart new file mode 100644 index 000000000..b7045815e --- /dev/null +++ b/cw_core/lib/format_fixed.dart @@ -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); +} diff --git a/cw_core/test/format_fixed_test.dart b/cw_core/test/format_fixed_test.dart new file mode 100644 index 000000000..b2d494e94 --- /dev/null +++ b/cw_core/test/format_fixed_test.dart @@ -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', + ), + ); + }); + }); +} diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index bed214940..921a4a849 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -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 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)}'; } diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index 61889cc2f..0fc47b6dc 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -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); + } +} diff --git a/test/utils/payment_request_test.dart b/test/utils/payment_request_test.dart new file mode 100644 index 000000000..f7ebbafca --- /dev/null +++ b/test/utils/payment_request_test.dart @@ -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, ""); + }); + }); + }); +}