mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
* feat(zcash): initial commit - initial work on cw_zcash - use single .db for everything zcash to speed up sync and scanning - implement silent autoshielding of non-orchard funds - use FFI for everything to fetch the data - add support for passphrase by adding extra word to seed phrase - enforce lints on CI for cw_zcash directory - use correct zcash name, and call transparent zcash tZEC - minor cleanups outsite of cw_zcash - gitmodules, gitignore - db debug - fix broken addressPageType in WalletInfo - enable debug options in kDebugMode and kProfileMode * feat: T address rotation (and cache) wip: minor fixes * wip: shield tx list * fix fiat amounts fix derrivation of rotation account addresses screen for zcash block height fix T address shields improvements Amount in disposable T shields UI improvements show pending outgoing tx in tx history * fix null check on passphrase page fix tx showing as pending rename auto shield txs fixed send all to include fee use rotating address in addressForExchange wrap all calls of WarpApi.getLatestHeight in try catch to prevent leaking errors other minor fixes from notion * wallet group debug cache sent tx address wallet T address rotation fixes automatically pick the fee for auto-shield * fix T address cache deserialization fix autoshield icon fix autoshield text getCachedDestinationAddress fix send address cache * address comments from review * wip: iOS fix * fix swap * Allow cakepay payment from zcash wallet * fix: ios (I'm stupid) fix: reorder addresses to make orchard default fix: send all balance * 5.6.6 bump fix: ios apple review add transport error to ignored exceptions * fix: leaving dust behind when sweeping all * Change wallets order * fix: reflect balance of pending out txs fix: restore height support * linux support (disabled) * fix: date picker for zcash on restore screen fix: build script for linux match WarpApi.warpSync args with upstream * fix: linter script * ensure all exchange providers support ZEC (#2802) * ensure all exchange providers support ZEC * remove zaddr and tzec currencies and refactor providers * Update lib/exchange/provider/near_Intents_exchange_provider.dart * add back old zcash currencies for backward compatibility, thanks Czarek * minor fix [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update android/app/src/main/AndroidManifestBase.xml [skip ci] * Update ios/Runner/InfoBase.plist [skip ci] * fix: remove unused code fix: macos fix: disable on linux and macos * update build numbers [skip ci] * Add zcash to birdpay --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> Co-authored-by: Serhii <borodenko.sv@gmail.com>
456 lines
11 KiB
Dart
456 lines
11 KiB
Dart
import 'package:cw_core/format_fixed.dart';
|
|
|
|
abstract class PaymentURI {
|
|
PaymentURI({required this.amount, required this.address});
|
|
|
|
final String amount;
|
|
final String address;
|
|
}
|
|
|
|
class MoneroURI extends PaymentURI {
|
|
MoneroURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'monero:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class HavenURI extends PaymentURI {
|
|
HavenURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'haven:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class BitcoinURI extends PaymentURI {
|
|
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
|
|
|
|
final String pjUri;
|
|
|
|
@override
|
|
String toString() {
|
|
final qp = <String, String>{};
|
|
|
|
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
|
|
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
|
|
qp['pjos'] = '0';
|
|
qp['pj'] = pjUri;
|
|
}
|
|
|
|
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
|
|
}
|
|
}
|
|
|
|
class LitecoinURI extends PaymentURI {
|
|
LitecoinURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'litecoin:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class EthereumURI extends PaymentURI {
|
|
EthereumURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'ethereum:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class BaseURI extends PaymentURI {
|
|
BaseURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'base:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class ArbitrumURI extends PaymentURI {
|
|
ArbitrumURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'arbitrum:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class BitcoinCashURI extends PaymentURI {
|
|
BitcoinCashURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = address;
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class NanoURI extends PaymentURI {
|
|
NanoURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'nano:$address';
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class PolygonURI extends PaymentURI {
|
|
PolygonURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'polygon:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class SolanaURI extends PaymentURI {
|
|
SolanaURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'solana:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class TronURI extends PaymentURI {
|
|
TronURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'tron:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class WowneroURI extends PaymentURI {
|
|
WowneroURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'wownero:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class ZanoURI extends PaymentURI {
|
|
ZanoURI({required String amount, required String address})
|
|
: super(amount: amount, address: address);
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'zano:' + address;
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class DecredURI extends PaymentURI {
|
|
DecredURI({required String amount, required String address})
|
|
: super(amount: amount, address: address);
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'decred:' + address;
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class DogeURI extends PaymentURI {
|
|
DogeURI({required String amount, required String address})
|
|
: super(amount: amount, address: address);
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'doge:' + address;
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class ZcashURI extends PaymentURI {
|
|
ZcashURI({required super.amount, required super.address});
|
|
|
|
@override
|
|
String toString() {
|
|
var base = 'zcash:$address';
|
|
|
|
if (amount.isNotEmpty) {
|
|
base += '?amount=${amount.replaceAll(',', '.')}';
|
|
}
|
|
|
|
return base;
|
|
}
|
|
}
|
|
|
|
class ERC681URI extends PaymentURI {
|
|
final int chainId;
|
|
final String? contractAddress;
|
|
|
|
ERC681URI({
|
|
required this.chainId,
|
|
required super.address,
|
|
required super.amount,
|
|
required this.contractAddress,
|
|
});
|
|
|
|
@override
|
|
String toString() {
|
|
var uri = 'ethereum:';
|
|
|
|
final targetAddress = contractAddress ?? address;
|
|
uri += targetAddress;
|
|
|
|
if (chainId != 1) {
|
|
uri += '@$chainId';
|
|
}
|
|
|
|
if (contractAddress != null) {
|
|
uri += '/transfer';
|
|
}
|
|
|
|
final params = <String, String>{};
|
|
|
|
if (contractAddress != null) {
|
|
params['address'] = address;
|
|
if (amount.isNotEmpty) {
|
|
params['uint256'] = _formatAmountForERC20(amount);
|
|
}
|
|
} else {
|
|
if (amount.isNotEmpty) {
|
|
params['value'] = _formatAmountForNative(amount);
|
|
}
|
|
}
|
|
|
|
if (params.isNotEmpty) {
|
|
uri += '?';
|
|
uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
|
|
}
|
|
|
|
return uri;
|
|
}
|
|
|
|
/// Formats amount for ERC-20 token transfers (in atomic units)
|
|
String _formatAmountForERC20(String amount) {
|
|
try {
|
|
// Convert decimal amount to BigInt (assuming 18 decimals)
|
|
final amountDouble = double.parse(amount.replaceAll(',', '.'));
|
|
final amountBigInt = BigInt.from(amountDouble * 1e18);
|
|
return amountBigInt.toString();
|
|
} catch (e) {
|
|
// Fallback to original amount if parsing fails
|
|
return amount.replaceAll(',', '.');
|
|
}
|
|
}
|
|
|
|
/// Formats amount for native ETH payments (in wei using scientific notation)
|
|
String _formatAmountForNative(String amount) {
|
|
try {
|
|
// Convert decimal amount to double for scientific notation
|
|
final amountDouble = double.parse(amount.replaceAll(',', '.'));
|
|
|
|
// Use scientific notation as recommended by ERC-681
|
|
return '${amountDouble}e18';
|
|
} catch (e) {
|
|
// Fallback to original amount if parsing fails
|
|
return amount.replaceAll(',', '.');
|
|
}
|
|
}
|
|
|
|
factory ERC681URI.fromUri(Uri uri) {
|
|
final (isContract, targetAddress) = _getTargetAddress(uri.path);
|
|
final chainId = _getChainID(uri.path);
|
|
|
|
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
|
|
final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
|
|
|
|
var formatedAmount = "";
|
|
|
|
if (amountParam != null) {
|
|
final normalized = _normalizeToIntegerWei(amountParam);
|
|
formatedAmount = formatFixed(BigInt.parse(normalized), 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);
|
|
}
|
|
|
|
/// Normalize an input amount into an integer wei string.
|
|
///
|
|
/// Accepts the following forms:
|
|
/// - Integer string: "123000000000000000" → unchanged
|
|
/// - Scientific notation: "0.123e18", "1e6" → expanded to integer
|
|
/// - Decimal ETH: "0.123456" → shifted by 18 decimals
|
|
static String _normalizeToIntegerWei(String input) {
|
|
final raw = input.replaceAll(',', '.').trim();
|
|
|
|
// First we check if it's already a plain integer (basically just a number with no dot, no exponent)
|
|
try {
|
|
final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
|
|
!raw.contains('.') &&
|
|
!raw.toLowerCase().contains('e');
|
|
if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
|
|
|
|
// Then we check if it's a scientific notation
|
|
final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
|
|
if (sci.hasMatch(raw)) {
|
|
final mantissaStr = raw.toLowerCase().split('e')[0];
|
|
final exp = int.parse(raw.toLowerCase().split('e')[1]);
|
|
return _expandDecimal(mantissaStr, exp);
|
|
}
|
|
|
|
// Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
|
|
if (raw.contains('.')) {
|
|
return _expandDecimal(raw, 18);
|
|
}
|
|
return raw;
|
|
} catch (e) {
|
|
return raw;
|
|
}
|
|
|
|
// If none of these checks work, we return the raw input
|
|
}
|
|
|
|
/// Expands a decimal string by shifting the decimal point `expShift` places
|
|
/// to the right and returns an integer string (digits only, optional leading minus).
|
|
/// Examples:
|
|
/// _expandDecimal('0.123456', 18) -> '123456000000000000'
|
|
/// _expandDecimal('1.2', 3) -> '1200'
|
|
static String _expandDecimal(String decimalStr, int expShift) {
|
|
var s = decimalStr.trim();
|
|
var sign = '';
|
|
if (s.startsWith('-') || s.startsWith('+')) {
|
|
sign = s[0] == '-' ? '-' : '';
|
|
s = s.substring(1);
|
|
}
|
|
|
|
// First we split the integer and fractional parts
|
|
final parts = s.split('.');
|
|
final intPart = parts[0].isEmpty ? '0' : parts[0];
|
|
final fracPart = parts.length > 1 ? parts[1] : '';
|
|
final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
|
|
final fracLen = fracPart.length;
|
|
|
|
// Then we calculate the effective shift = desired shift minus existing fractional digits
|
|
final shift = expShift - fracLen;
|
|
if (shift >= 0) {
|
|
final head = digits.isEmpty ? '0' : digits;
|
|
final zeros = List.filled(shift, '0').join();
|
|
final res = head + zeros;
|
|
return sign + (res.isEmpty ? '0' : res);
|
|
} else {
|
|
// Need to insert a decimal point within digits; return integer by truncating
|
|
final cut = digits.length + shift;
|
|
if (cut <= 0) {
|
|
return '0';
|
|
}
|
|
final res = digits.substring(0, cut);
|
|
return sign + (res.isEmpty ? '0' : res);
|
|
}
|
|
}
|
|
}
|