mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
targets:
|
||||
$default:
|
||||
sources:
|
||||
exclude:
|
||||
- "**/.secrets.g.dart"
|
||||
@@ -1,12 +1,32 @@
|
||||
class Account {
|
||||
Account({required this.id, required this.label, this.balance});
|
||||
Account({
|
||||
required this.id,
|
||||
required this.label,
|
||||
String? balance,
|
||||
}) : balance = balance ?? "0.00";
|
||||
|
||||
Account.fromMap(Map<String, Object> map)
|
||||
: this.id = map['id'] == null ? 0 : int.parse(map['id'] as String),
|
||||
this.label = (map['label'] ?? '') as String,
|
||||
this.balance = (map['balance'] ?? '0.00') as String;
|
||||
Account.fromMap(Map<String, Object?> map)
|
||||
: id = map["id"] is int ? map["id"] as int : int.tryParse(map["id"]?.toString() ?? "") ?? 0,
|
||||
label = (map["label"] ?? "") as String,
|
||||
balance = (map["balance"] ?? "0.00") as String;
|
||||
|
||||
final int id;
|
||||
final String label;
|
||||
final String? balance;
|
||||
final String balance;
|
||||
|
||||
Account copyWith({
|
||||
int? id,
|
||||
String? label,
|
||||
String? balance,
|
||||
}) => Account(
|
||||
id: id ?? this.id,
|
||||
label: label ?? this.label,
|
||||
balance: balance ?? this.balance,
|
||||
);
|
||||
|
||||
Map<String, Object> toMap() => {
|
||||
"id": id.toString(),
|
||||
"label": label,
|
||||
"balance": balance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
abstract class AccountList<T> {
|
||||
ObservableList<T> get accounts;
|
||||
|
||||
void update();
|
||||
|
||||
List<T> getAll();
|
||||
|
||||
Future<void> addAccount({required String label});
|
||||
|
||||
Future<void> setLabelAccount({required int accountIndex, required String label});
|
||||
|
||||
void refresh();
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'address_info.g.dart';
|
||||
part 'address_info.part.dart';
|
||||
|
||||
@HiveType(typeId: ADDRESS_INFO_TYPE_ID)
|
||||
// @HiveType(typeId: ADDRESS_INFO_TYPE_ID)
|
||||
class AddressInfo extends HiveObject {
|
||||
AddressInfo({required this.address, this.accountIndex, required this.label});
|
||||
|
||||
static const typeId = ADDRESS_INFO_TYPE_ID;
|
||||
static const boxName = 'AddressInfo';
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
int? accountIndex;
|
||||
|
||||
@HiveField(1, defaultValue: '')
|
||||
// @HiveField(1, defaultValue: '')
|
||||
String address;
|
||||
|
||||
@HiveField(2, defaultValue: '')
|
||||
// @HiveField(2, defaultValue: '')
|
||||
String label;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'address_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AddressInfoAdapter extends TypeAdapter<AddressInfo> {
|
||||
@override
|
||||
final int typeId = 11;
|
||||
|
||||
@override
|
||||
AddressInfo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AddressInfo(
|
||||
address: fields[1] == null ? '' : fields[1] as String,
|
||||
accountIndex: fields[0] as int?,
|
||||
label: fields[2] == null ? '' : fields[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AddressInfo obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.accountIndex)
|
||||
..writeByte(1)
|
||||
..write(obj.address)
|
||||
..writeByte(2)
|
||||
..write(obj.label);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AddressInfoAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
String _smartAmountSanitizer(String amount) {
|
||||
if (amount.contains(RegExp(r'^(?=.*[.])(?=.*,).*$'))) {
|
||||
if (amount.indexOf(".") > amount.indexOf(",")) {
|
||||
amount = amount.replaceAll(",", "");
|
||||
} else {
|
||||
amount = amount.replaceAll(".", "");
|
||||
}
|
||||
}
|
||||
|
||||
return amount.replaceAll(",", ".");
|
||||
}
|
||||
|
||||
extension SanitizerUserAmounts on String {
|
||||
String sanitized() => _smartAmountSanitizer(this);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
|
||||
class ExchangeRate {
|
||||
const ExchangeRate({required this.base, required this.quote});
|
||||
|
||||
/// The currency being priced (e.g. BTC in a BTC/USD pair).
|
||||
final Currency base;
|
||||
|
||||
/// The price of one whole unit of [base], in the quote currency
|
||||
/// (e.g. 45000 USD in a BTC/USD pair).
|
||||
final Money quote;
|
||||
|
||||
/// Converts [amount] between the base and quote currencies.
|
||||
///
|
||||
/// Results are truncated toward zero.
|
||||
///
|
||||
/// Throws an [ArgumentError] if [amount]'s currency is not part of
|
||||
/// this pair.
|
||||
Money convert(Money amount) {
|
||||
if (base == quote.currency && base == amount.currency) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
if (amount.currency == base) {
|
||||
final scale = BigInt.from(10).pow(amount.decimals);
|
||||
|
||||
return quote.isZero
|
||||
? Money.zero(quote.currency)
|
||||
: Money(amount.amount * quote.amount ~/ scale, quote.currency, quote.decimals);
|
||||
}
|
||||
|
||||
if (amount.currency == quote.currency) {
|
||||
if (quote.isZero) {
|
||||
return Money.zero(base);
|
||||
}
|
||||
|
||||
final numerator = amount.amount * BigInt.from(10).pow(base.decimals + quote.decimals);
|
||||
final denominator = quote.amount * BigInt.from(10).pow(amount.decimals);
|
||||
|
||||
return Money(numerator ~/ denominator, base);
|
||||
}
|
||||
|
||||
throw ArgumentError(
|
||||
"Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import "dart:math";
|
||||
|
||||
import "package:cw_core/amount/utils.dart";
|
||||
import "package:cw_core/crypto_amount_format.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/format_fixed.dart";
|
||||
import "package:cw_core/parse_fixed.dart";
|
||||
|
||||
export "money_local.dart";
|
||||
|
||||
class Money implements Comparable<Money> {
|
||||
const Money(this.amount, this.currency, [int? overrideDecimals])
|
||||
: _overrideDecimals = overrideDecimals;
|
||||
|
||||
factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
|
||||
|
||||
factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency);
|
||||
|
||||
/// Parse the [source] and turn it into [Money] trimming trailing 0s
|
||||
///
|
||||
/// Throws a [FormatException] if the [source] is not a valid decimal or
|
||||
/// not in canonical representation or if it is a decimal when [isBaseUnit]
|
||||
factory Money.parse(
|
||||
String source,
|
||||
Currency currency, {
|
||||
bool isBaseUnit = false,
|
||||
bool strictParsing = true,
|
||||
}) {
|
||||
if (!isBaseUnit) {
|
||||
source = trimTrailingFractionZeros(source);
|
||||
}
|
||||
final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
|
||||
final amount = isBaseUnit ? BigInt.parse(source) : parseFixed(source, decimals);
|
||||
|
||||
return Money(amount, currency, decimals);
|
||||
}
|
||||
|
||||
/// Parse the [source] and turn it into [Money] if possible trimming trailing 0s
|
||||
///
|
||||
/// As [parse] except that this method returns `null` if the input is not
|
||||
/// valid or if it is a decimal when [isBaseUnit]
|
||||
static Money? tryParse(
|
||||
String source,
|
||||
Currency currency, {
|
||||
bool isBaseUnit = false,
|
||||
bool strictParsing = true,
|
||||
}) {
|
||||
try {
|
||||
if (!isBaseUnit) {
|
||||
source = trimTrailingFractionZeros(source);
|
||||
}
|
||||
final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
|
||||
final amount = isBaseUnit ? BigInt.tryParse(source) : tryParseFixed(source, decimals);
|
||||
|
||||
return amount != null ? Money(amount, currency, decimals) : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Money? trySafeParse(source, Currency currency, {bool isBaseUnit = false}) {
|
||||
final amount = isBaseUnit
|
||||
? BigInt.tryParse(source.toString())
|
||||
: tryParseFixed(source.toString().withDecimals(currency.decimals), currency.decimals);
|
||||
|
||||
return amount != null ? Money(amount, currency) : null;
|
||||
}
|
||||
|
||||
final BigInt amount;
|
||||
final Currency currency;
|
||||
|
||||
final int? _overrideDecimals;
|
||||
|
||||
/// Returns the amount of decimals of [currency]
|
||||
int get decimals => _overrideDecimals ?? currency.decimals;
|
||||
|
||||
/// Returns the sign of this [BigInt] amount.
|
||||
/// Returns 0 for zero, -1 for values less than zero and +1 for values
|
||||
/// greater than zero.
|
||||
int get sign => amount.sign;
|
||||
|
||||
/// Returns `true` when amount of this money is zero.
|
||||
bool get isZero => amount == BigInt.zero;
|
||||
|
||||
/// Returns `true` when amount of this money is negative.
|
||||
bool get isNegative => amount.isNegative;
|
||||
|
||||
/// Compares this to [other].
|
||||
///
|
||||
/// [other] has to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
@override
|
||||
int compareTo(Money other) {
|
||||
_assertSameCurrency(other);
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a.compareTo(aligned.b);
|
||||
}
|
||||
|
||||
/// Returns `true` if [other] is the same amount of money in
|
||||
/// the same currency.
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other is! Money || other.currency != currency) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a == aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is less than [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator <(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a < aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is less than or equal to [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator <=(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a <= aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is greater than [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator >(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a > aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is greater than or equal to [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator >=(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a >= aligned.b;
|
||||
}
|
||||
|
||||
/// Adds the amount of [other] to this amount.
|
||||
///
|
||||
/// Both operands must be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
Money operator +(Money other) {
|
||||
_assertSameCurrency(other);
|
||||
|
||||
final aligned = _align(other);
|
||||
return copyWith(amount: aligned.a + aligned.b, decimals: aligned.decimals);
|
||||
}
|
||||
|
||||
/// unary minus operator.
|
||||
Money operator -() => copyWith(amount: -amount);
|
||||
|
||||
/// Subtracts the amount of [other] from this amount.
|
||||
///
|
||||
/// Both operands must be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
Money operator -(Money other) {
|
||||
_assertSameCurrency(other);
|
||||
|
||||
final aligned = _align(other);
|
||||
return copyWith(amount: aligned.a - aligned.b, decimals: aligned.decimals);
|
||||
}
|
||||
|
||||
/// Returns [Money] multiplied by [other].
|
||||
///
|
||||
/// The result is again [Money].
|
||||
Money operator *(BigInt other) => copyWith(amount: amount * other);
|
||||
|
||||
/// Returns [Money] divided by [other].
|
||||
///
|
||||
/// The result is again [Money].
|
||||
Money operator /(BigInt other) {
|
||||
if (other == BigInt.zero) {
|
||||
throw Exception("Division by zero.");
|
||||
}
|
||||
|
||||
final neg = (amount.isNegative) ^ (other.isNegative);
|
||||
final A = amount.abs();
|
||||
final B = other.abs();
|
||||
|
||||
final q = A ~/ B;
|
||||
final r = A % B;
|
||||
|
||||
// Compare 2*r with B to detect half/up/down
|
||||
final twiceR = r << 1;
|
||||
final mag = (twiceR >= B) ? q + BigInt.one : q;
|
||||
|
||||
return copyWith(amount: neg ? -mag : mag);
|
||||
}
|
||||
|
||||
/// Creates a copy of this [Money] object with optional new values
|
||||
/// for [amount], [currency].
|
||||
///
|
||||
/// If just [currency] is provided and the decimals missmatch
|
||||
/// the amount will be transformed to keep its canonical representation
|
||||
Money copyWith({BigInt? amount, Currency? currency, int? decimals}) {
|
||||
if (currency != null && amount == null && decimals == null &&
|
||||
currency.decimals != this.decimals) {
|
||||
return Money(
|
||||
_transformAmount(this.amount, this.decimals, currency.decimals),
|
||||
currency,
|
||||
currency.decimals,
|
||||
);
|
||||
}
|
||||
|
||||
return Money(amount ?? this.amount, currency ?? this.currency, decimals ?? this.decimals);
|
||||
}
|
||||
|
||||
void _assertSameCurrency(Money other, [String? message]) {
|
||||
if (currency != other.currency) {
|
||||
throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
|
||||
}
|
||||
}
|
||||
|
||||
static BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
|
||||
final diff = targetDecimals - sourceDecimals;
|
||||
|
||||
return diff == 0
|
||||
? source
|
||||
: diff > 0
|
||||
? source * BigInt.from(10).pow(diff)
|
||||
: source ~/ BigInt.from(10).pow(-diff);
|
||||
}
|
||||
|
||||
({BigInt a, BigInt b, int decimals}) _align(Money other) {
|
||||
final decimals = max(this.decimals, other.decimals);
|
||||
return (
|
||||
a: _transformAmount(amount, this.decimals, decimals),
|
||||
b: _transformAmount(other.amount, other.decimals, decimals),
|
||||
decimals: decimals,
|
||||
);
|
||||
}
|
||||
|
||||
static int _getActualDecimals(String value, Currency currency) {
|
||||
final comps = value.split(".");
|
||||
if (comps.length > 2) {
|
||||
throw FormatException("Money._getActualDecimals: too many decimal points, value, $value");
|
||||
}
|
||||
|
||||
return max(comps.length == 2 ? comps[1].length : 0, currency.decimals);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
final value = formatFixed(amount, this.decimals, trimZeros: true);
|
||||
final decimals = _getActualDecimals(value, currency);
|
||||
|
||||
return Object.hash(value, decimals, currency);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => formatFixed(amount, decimals);
|
||||
|
||||
String toStringWithSymbol({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
bool withSymbolPrefix = false,
|
||||
}) {
|
||||
final amount = toStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit,
|
||||
);
|
||||
final symbol = getSymbol(useBaseUnit: useBaseUnit);
|
||||
|
||||
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
|
||||
}
|
||||
|
||||
String toStringWithPrecision({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
}) =>
|
||||
formatFixed(
|
||||
decimals != currency.decimals
|
||||
? _transformAmount(amount, decimals, currency.decimals)
|
||||
: amount,
|
||||
useBaseUnit ? 0 : currency.decimals,
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
);
|
||||
|
||||
// To Override the symbol with the ticker of the base unit
|
||||
String getSymbol({required bool useBaseUnit}) {
|
||||
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
|
||||
return "sats";
|
||||
}
|
||||
return currency.symbol;
|
||||
}
|
||||
|
||||
/// new [Money] with absolute value of this instance
|
||||
Money abs() => copyWith(amount: amount.abs());
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import "dart:math";
|
||||
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/amount/utils.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
|
||||
/// Turn a double representation of a currency amount to a proper Money representation
|
||||
/// truncating currencies with more than 20 decimals because double can not handle more
|
||||
extension ToMoney on double {
|
||||
Money? tryToMoney(Currency currency) => Money.tryParse(_toSafeString(currency), currency);
|
||||
|
||||
Money toMoney(Currency currency) => Money.parse(_toSafeString(currency), currency);
|
||||
|
||||
String _toSafeString(Currency currency) =>
|
||||
trimTrailingFractionZeros(toStringAsFixed(min(currency.decimals, 20)));
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_amount_format.dart";
|
||||
|
||||
extension WithLocalSeparator on Money {
|
||||
String toLocalStringWithSymbol({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
bool withSymbolPrefix = false,
|
||||
String? locale,
|
||||
}) {
|
||||
final amount = toLocalStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit,
|
||||
locale: locale,
|
||||
);
|
||||
final symbol = getSymbol(useBaseUnit: useBaseUnit);
|
||||
|
||||
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
|
||||
}
|
||||
|
||||
String toLocalStringWithPrecision({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
String? locale,
|
||||
}) =>
|
||||
_withLocalSeparator(
|
||||
toStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit,
|
||||
),
|
||||
locale: locale,
|
||||
);
|
||||
|
||||
String _withLocalSeparator(String amount, {String? locale}) {
|
||||
final isNegative = amount.startsWith("-");
|
||||
final formatted = (isNegative ? amount.substring(1) : amount).withLocalSeperator(locale);
|
||||
|
||||
return isNegative ? "-$formatted" : formatted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
String trimTrailingFractionZeros(String value) {
|
||||
if (!value.contains('.')) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var end = value.length;
|
||||
while (end > 0 && value[end - 1] == "0") {
|
||||
end--;
|
||||
}
|
||||
|
||||
final trimmed = end == value.length ? value : value.substring(0, end);
|
||||
return trimmed.endsWith('.') ? trimmed.substring(0, trimmed.length - 1) : trimmed;
|
||||
}
|
||||
@@ -1,77 +1,4 @@
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:decimal/intl.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class AmountConverter {
|
||||
static const _moneroAmountLength = 12;
|
||||
static const _moneroAmountDivider = 1000000000000;
|
||||
static const _wowneroAmountLength = 11;
|
||||
static const _wowneroAmountDivider = 100000000000;
|
||||
static const _bitcoinAmountDivider = 100000000;
|
||||
static const _bitcoinAmountLength = 8;
|
||||
static final _bitcoinAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = _bitcoinAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
static final _moneroAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = _moneroAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
static final _wowneroAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = _wowneroAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
|
||||
static String amountIntToString(CryptoCurrency cryptoCurrency, int amount) {
|
||||
switch (cryptoCurrency) {
|
||||
case CryptoCurrency.xmr:
|
||||
return _moneroAmountToString(amount);
|
||||
case CryptoCurrency.wow:
|
||||
return _wowneroAmountToString(amount);
|
||||
case CryptoCurrency.btc:
|
||||
case CryptoCurrency.bch:
|
||||
case CryptoCurrency.ltc:
|
||||
case CryptoCurrency.dcr:
|
||||
case CryptoCurrency.doge:
|
||||
return _bitcoinAmountToString(amount);
|
||||
case CryptoCurrency.xhv:
|
||||
case CryptoCurrency.xag:
|
||||
case CryptoCurrency.xau:
|
||||
case CryptoCurrency.xaud:
|
||||
case CryptoCurrency.xbtc:
|
||||
case CryptoCurrency.xcad:
|
||||
case CryptoCurrency.xchf:
|
||||
case CryptoCurrency.xcny:
|
||||
case CryptoCurrency.xeur:
|
||||
case CryptoCurrency.xgbp:
|
||||
case CryptoCurrency.xjpy:
|
||||
case CryptoCurrency.xnok:
|
||||
case CryptoCurrency.xnzd:
|
||||
case CryptoCurrency.xusd:
|
||||
return _moneroAmountToString(amount);
|
||||
case CryptoCurrency.zano:
|
||||
return _moneroAmountToStringUsingDecimals(amount);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static double cryptoAmountToDouble({required num amount, required num divider}) =>
|
||||
amount / divider;
|
||||
|
||||
static String _moneroAmountToString(int amount) => _moneroAmountFormat
|
||||
.format(cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider));
|
||||
|
||||
static String _bitcoinAmountToString(int amount) => _bitcoinAmountFormat
|
||||
.format(cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider));
|
||||
|
||||
static String _wowneroAmountToString(int amount) => _wowneroAmountFormat
|
||||
.format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider));
|
||||
|
||||
static Decimal cryptoAmountToDecimal({required int amount, required int divider}) =>
|
||||
(Decimal.fromInt(amount) / Decimal.fromInt(divider)).toDecimal();
|
||||
|
||||
static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat
|
||||
.format(DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider)));
|
||||
|
||||
static String toBaseUnits(String amount, int decimals) {
|
||||
amount = amount.trim();
|
||||
if (amount.isEmpty) return '0';
|
||||
|
||||
+14
-20
@@ -1,26 +1,20 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
/// Balance Data class with all amounts in the lowest possible currency (e.g. satoshis or wei)
|
||||
abstract class Balance {
|
||||
const Balance(this.available, this.additional,
|
||||
{this.secondAvailable, this.secondAdditional, this.frozen});
|
||||
const Balance(
|
||||
this.available,
|
||||
this.unavailable, {
|
||||
this.secondAvailable,
|
||||
this.secondUnavailable,
|
||||
this.frozen,
|
||||
});
|
||||
|
||||
Balance.fromInt(int available, int additional,
|
||||
{int? secondAvailable, int? secondAdditional, int? frozen})
|
||||
: available = BigInt.from(available),
|
||||
additional = BigInt.from(additional),
|
||||
secondAvailable = secondAvailable == null ? null : BigInt.from(secondAvailable),
|
||||
secondAdditional = secondAdditional == null ? null : BigInt.from(secondAdditional),
|
||||
frozen = frozen == null ? null : BigInt.from(frozen) {}
|
||||
final Money available;
|
||||
final Money unavailable;
|
||||
|
||||
final BigInt available;
|
||||
final BigInt additional;
|
||||
final Money? secondAvailable;
|
||||
final Money? secondUnavailable;
|
||||
|
||||
final BigInt? secondAvailable;
|
||||
final BigInt? secondAdditional;
|
||||
|
||||
final BigInt? frozen;
|
||||
|
||||
BigInt get fullAvailableBalance => available;
|
||||
|
||||
@deprecated
|
||||
String get formattedUnAvailableBalance => '';
|
||||
final Money? frozen;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
|
||||
class BalanceCardLayout {
|
||||
BalanceCardLayout._(
|
||||
this._settings, {
|
||||
required List<int> visible,
|
||||
required List<int> hidden,
|
||||
required this.needsRepair,
|
||||
}) : visible = List<int>.unmodifiable(visible),
|
||||
hidden = List<int>.unmodifiable(hidden);
|
||||
|
||||
factory BalanceCardLayout.resolve({
|
||||
required List<int> accountIndices,
|
||||
required List<BalanceCardStyleSettings> settings,
|
||||
}) {
|
||||
final List<int> accounts = [];
|
||||
for (final accountIndex in accountIndices) {
|
||||
if (!accounts.contains(accountIndex)) {
|
||||
accounts.add(accountIndex);
|
||||
}
|
||||
}
|
||||
|
||||
final settingsByAccount = <int, BalanceCardStyleSettings>{};
|
||||
for (final setting in settings) {
|
||||
if (accounts.contains(setting.accountIndex)) {
|
||||
settingsByAccount.putIfAbsent(setting.accountIndex, () => setting);
|
||||
}
|
||||
}
|
||||
|
||||
final hidden = [
|
||||
for (final accountIndex in accounts)
|
||||
if (settingsByAccount[accountIndex]?.hidden ?? false) accountIndex,
|
||||
]..sort();
|
||||
|
||||
final visible = [
|
||||
for (final accountIndex in accounts)
|
||||
if (!hidden.contains(accountIndex)) accountIndex,
|
||||
];
|
||||
|
||||
visible.sort((a, b) => _compareStackPosition(a, b, settingsByAccount));
|
||||
|
||||
var needsRepair = false;
|
||||
for (var position = 0; position < visible.length; position++) {
|
||||
final stored = settingsByAccount[visible[position]];
|
||||
if (stored != null && stored.cardOrder != position) {
|
||||
needsRepair = true;
|
||||
}
|
||||
}
|
||||
|
||||
return BalanceCardLayout._(
|
||||
settingsByAccount,
|
||||
visible: visible,
|
||||
hidden: hidden,
|
||||
needsRepair: needsRepair,
|
||||
);
|
||||
}
|
||||
|
||||
final Map<int, BalanceCardStyleSettings> _settings;
|
||||
|
||||
final List<int> visible;
|
||||
final List<int> hidden;
|
||||
|
||||
// flag when db order is broken
|
||||
final bool needsRepair;
|
||||
|
||||
List<int> get designOrder => designOrderOf(visible);
|
||||
|
||||
static List<int> stackFromOrder(Map<int, int> cardOrder, {bool forceSingleCard = false}) {
|
||||
final positions = cardOrder.keys.toList()..sort();
|
||||
final accountIndices = [for (final position in positions) cardOrder[position]!];
|
||||
|
||||
if (accountIndices.isEmpty || !forceSingleCard) {
|
||||
return accountIndices;
|
||||
}
|
||||
return [accountIndices.first];
|
||||
}
|
||||
|
||||
static List<int> designOrderOf(Iterable<int> accountIndices) =>
|
||||
List<int>.of(accountIndices)..sort();
|
||||
|
||||
Map<int, int> get orders =>
|
||||
{for (var position = 0; position < visible.length; position++) visible[position]: position};
|
||||
|
||||
BalanceCardStyleSettings? settingFor(int accountIndex) => _settings[accountIndex];
|
||||
|
||||
BalanceCardLayout unhiding(int accountIndex) {
|
||||
if (!hidden.contains(accountIndex)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return BalanceCardLayout._(
|
||||
_settings,
|
||||
visible: [...visible, accountIndex],
|
||||
hidden: [
|
||||
for (final hiddenAccount in hidden)
|
||||
if (hiddenAccount != accountIndex) hiddenAccount,
|
||||
],
|
||||
needsRepair: true,
|
||||
);
|
||||
}
|
||||
|
||||
static int _compareStackPosition(
|
||||
int a,
|
||||
int b,
|
||||
Map<int, BalanceCardStyleSettings> settingsByAccount,
|
||||
) {
|
||||
final orderA = _storedOrder(settingsByAccount[a]);
|
||||
final orderB = _storedOrder(settingsByAccount[b]);
|
||||
|
||||
if (orderA != orderB) {
|
||||
if (orderA == null) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB == null) {
|
||||
return -1;
|
||||
}
|
||||
return orderA.compareTo(orderB);
|
||||
}
|
||||
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
static int? _storedOrder(BalanceCardStyleSettings? setting) =>
|
||||
setting == null || setting.cardOrder < 0 ? null : setting.cardOrder;
|
||||
}
|
||||
@@ -7,7 +7,10 @@ class BalanceCardStyleSettings {
|
||||
final int accountIndex;
|
||||
final int gradientIndex;
|
||||
final bool useSpecialDesign;
|
||||
final bool hidden;
|
||||
final String backgroundImagePath;
|
||||
final int iconStyleIndex;
|
||||
final bool isGradientOnly;
|
||||
final int cardOrder;
|
||||
|
||||
BalanceCardStyleSettings(
|
||||
@@ -15,7 +18,10 @@ class BalanceCardStyleSettings {
|
||||
required this.accountIndex,
|
||||
required this.gradientIndex,
|
||||
required this.useSpecialDesign,
|
||||
required this.hidden,
|
||||
required this.backgroundImagePath,
|
||||
this.iconStyleIndex = 0,
|
||||
this.isGradientOnly = false,
|
||||
required this.cardOrder});
|
||||
|
||||
static const tableName = "BalanceCardStyleSettings";
|
||||
@@ -25,8 +31,11 @@ class BalanceCardStyleSettings {
|
||||
"walletInfoId": walletInfoId,
|
||||
"accountIndex": accountIndex,
|
||||
"gradientIndex": gradientIndex,
|
||||
"hidden": hidden ? 1 : 0,
|
||||
"useSpecialDesign": useSpecialDesign ? 1 : 0,
|
||||
"backgroundImagePath": backgroundImagePath,
|
||||
"iconStyleIndex": iconStyleIndex,
|
||||
"isGradientOnly": isGradientOnly ? 1 : 0,
|
||||
"cardOrder": cardOrder,
|
||||
};
|
||||
return ret;
|
||||
@@ -36,22 +45,61 @@ class BalanceCardStyleSettings {
|
||||
return BalanceCardStyleSettings(
|
||||
walletInfoId: json["walletInfoId"] as int,
|
||||
accountIndex: json["accountIndex"] as int,
|
||||
hidden: json["hidden"] == 1,
|
||||
gradientIndex: json["gradientIndex"] as int,
|
||||
useSpecialDesign: json["useSpecialDesign"] == 1,
|
||||
backgroundImagePath: json["backgroundImagePath"] as String? ?? "",
|
||||
iconStyleIndex: json["iconStyleIndex"] as int? ?? 0,
|
||||
isGradientOnly: json["isGradientOnly"] == 1,
|
||||
cardOrder: json["cardOrder"] as int? ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
static BalanceCardStyleSettings fromCardDesign(
|
||||
int walletInfoId, int accountIndex, int cardOrder, CardDesign design) {
|
||||
BalanceCardStyleSettings copyWith({
|
||||
int? walletInfoId,
|
||||
int? accountIndex,
|
||||
int? gradientIndex,
|
||||
int? iconStyleIndex,
|
||||
bool? isGradientOnly,
|
||||
bool? useSpecialDesign,
|
||||
bool? hidden,
|
||||
String? backgroundImagePath,
|
||||
int? cardOrder,
|
||||
}) {
|
||||
return BalanceCardStyleSettings(
|
||||
walletInfoId: walletInfoId ?? this.walletInfoId,
|
||||
accountIndex: accountIndex ?? this.accountIndex,
|
||||
gradientIndex: gradientIndex ?? this.gradientIndex,
|
||||
useSpecialDesign: useSpecialDesign ?? this.useSpecialDesign,
|
||||
hidden: hidden ?? this.hidden,
|
||||
backgroundImagePath: backgroundImagePath ?? this.backgroundImagePath,
|
||||
cardOrder: cardOrder ?? this.cardOrder,
|
||||
iconStyleIndex: iconStyleIndex ?? this.iconStyleIndex,
|
||||
isGradientOnly: isGradientOnly ?? this.isGradientOnly
|
||||
);
|
||||
}
|
||||
|
||||
static BalanceCardStyleSettings fromCardDesign({
|
||||
required int walletInfoId,
|
||||
required int accountIndex,
|
||||
required int cardOrder,
|
||||
required CardDesign design,
|
||||
int iconStyleIndex = 0,
|
||||
int? gradientIndexOverride,
|
||||
bool hidden = false
|
||||
}) {
|
||||
final int gradientIndex =
|
||||
gradientIndexOverride ?? CardDesign.allGradients.indexOf(design.gradient);
|
||||
return BalanceCardStyleSettings(
|
||||
walletInfoId: walletInfoId,
|
||||
accountIndex: accountIndex,
|
||||
gradientIndex: CardDesign.allGradients.indexOf(design.gradient),
|
||||
gradientIndex: gradientIndex,
|
||||
useSpecialDesign: design.backgroundType == CardDesignBackgroundTypes.svgFull,
|
||||
hidden: hidden,
|
||||
backgroundImagePath:
|
||||
design.backgroundType == CardDesignBackgroundTypes.image ? design.imagePath : "",
|
||||
iconStyleIndex: iconStyleIndex,
|
||||
isGradientOnly: design.backgroundType == CardDesignBackgroundTypes.gradientOnly,
|
||||
cardOrder: cardOrder,
|
||||
);
|
||||
}
|
||||
@@ -80,6 +128,58 @@ class BalanceCardStyleSettings {
|
||||
}
|
||||
|
||||
Future<void> insert() async {
|
||||
db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
static Future<void> setVisibleOrder(int walletInfoId, Map<int, int> orderByAccountIndex) =>
|
||||
_setOrder(walletInfoId, orderByAccountIndex, unhide: true);
|
||||
|
||||
static Future<void> setOrder(int walletInfoId, Map<int, int> orderByAccountIndex) =>
|
||||
_setOrder(walletInfoId, orderByAccountIndex, unhide: false);
|
||||
|
||||
static Future<void> _setOrder(
|
||||
int walletInfoId,
|
||||
Map<int, int> orderByAccountIndex, {
|
||||
required bool unhide,
|
||||
}) async {
|
||||
if (orderByAccountIndex.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final entries = orderByAccountIndex.entries.toList();
|
||||
final batch = db!.batch();
|
||||
|
||||
for (final entry in entries) {
|
||||
batch.update(
|
||||
tableName,
|
||||
{"cardOrder": entry.value, if (unhide) "hidden": 0},
|
||||
where: "walletInfoId = ? AND accountIndex = ?",
|
||||
whereArgs: [walletInfoId, entry.key],
|
||||
);
|
||||
}
|
||||
|
||||
final updatedRowCounts = await batch.commit();
|
||||
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
if (updatedRowCounts[i] == 0) {
|
||||
await db!.insert(
|
||||
tableName,
|
||||
positionOnly(walletInfoId, entries[i].key, entries[i].value).toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static BalanceCardStyleSettings positionOnly(
|
||||
int walletInfoId, int accountIndex, int cardOrder) =>
|
||||
BalanceCardStyleSettings(
|
||||
walletInfoId: walletInfoId,
|
||||
accountIndex: accountIndex,
|
||||
gradientIndex: -1,
|
||||
useSpecialDesign: true,
|
||||
hidden: false,
|
||||
backgroundImagePath: "",
|
||||
cardOrder: cardOrder,
|
||||
);
|
||||
}
|
||||
|
||||
+161
-20
@@ -3,7 +3,7 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CardDesignBackgroundTypes { image, svgIcon, svgFull }
|
||||
enum CardDesignBackgroundTypes { image, svgIcon, svgFull, gradientOnly }
|
||||
|
||||
class CardColorCombination {
|
||||
final Color textColor;
|
||||
@@ -33,18 +33,27 @@ class CardColorCombination {
|
||||
);
|
||||
}
|
||||
|
||||
class CardIconPath {
|
||||
final String path;
|
||||
final bool preColored;
|
||||
|
||||
const CardIconPath(this.path, {this.preColored = false});
|
||||
}
|
||||
|
||||
class CardDesign {
|
||||
final Gradient gradient;
|
||||
final String imagePath;
|
||||
final CardDesignBackgroundTypes backgroundType;
|
||||
final CardColorCombination colors;
|
||||
final bool preColoredIcon;
|
||||
|
||||
const CardDesign(
|
||||
{this.backgroundType = CardDesignBackgroundTypes.svgIcon,
|
||||
this.gradient = const LinearGradient(
|
||||
colors: [Colors.black], begin: Alignment.topCenter, end: Alignment.bottomCenter),
|
||||
this.imagePath = "assets/new-ui/blank.svg",
|
||||
this.colors = CardColorCombination.dark});
|
||||
this.colors = CardColorCombination.dark,
|
||||
this.preColoredIcon = false});
|
||||
|
||||
static const LinearGradient gradientOrange = LinearGradient(
|
||||
colors: <Color>[Color(0xFFFF7C02), Color(0xFFFF5602)],
|
||||
@@ -120,6 +129,9 @@ class CardDesign {
|
||||
|
||||
static const genericDefault = CardDesign(gradient: gradientBlue);
|
||||
|
||||
static const gradientOnlyDesign =
|
||||
CardDesign(backgroundType: CardDesignBackgroundTypes.gradientOnly, gradient: gradientBlue);
|
||||
|
||||
static const btc = CardDesign(imagePath: "assets/new-ui/balance_card_icons/bitcoin.svg");
|
||||
|
||||
static const eth = CardDesign(imagePath: "assets/new-ui/balance_card_icons/ethereum.svg");
|
||||
@@ -156,6 +168,8 @@ class CardDesign {
|
||||
|
||||
static const bnb = CardDesign(imagePath: "assets/new-ui/balance_card_icons/bnb.svg");
|
||||
|
||||
static const robinhood = CardDesign(imagePath: "assets/new-ui/balance_card_icons/robinhood.svg");
|
||||
|
||||
static const ethSpecial = CardDesign(
|
||||
gradient: const LinearGradient(
|
||||
colors: <Color>[Color(0xFF6259FF), Color(0xFF3B20E6)],
|
||||
@@ -311,11 +325,20 @@ class CardDesign {
|
||||
backgroundType: CardDesignBackgroundTypes.svgFull,
|
||||
imagePath: "assets/new-ui/balance_card_backgrounds/bnb.svg");
|
||||
|
||||
static const robinhoodSpecial = CardDesign(
|
||||
gradient: const LinearGradient(
|
||||
colors: <Color>[Color(0xFFBDFE00), Color(0xFF85BE00)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter),
|
||||
backgroundType: CardDesignBackgroundTypes.svgFull,
|
||||
imagePath: "assets/new-ui/balance_card_backgrounds/robinhood.svg");
|
||||
|
||||
CardDesign withGradient(Gradient gradient) => CardDesign(
|
||||
gradient: gradient,
|
||||
colors: preferredColorCombinations[gradient] ?? colors,
|
||||
imagePath: imagePath,
|
||||
backgroundType: backgroundType);
|
||||
backgroundType: backgroundType,
|
||||
preColoredIcon: preColoredIcon);
|
||||
|
||||
CardDesign withGradientAndColorCombination(
|
||||
Gradient gradient, CardColorCombination cardColorCombination) =>
|
||||
@@ -323,7 +346,61 @@ class CardDesign {
|
||||
gradient: gradient,
|
||||
colors: cardColorCombination,
|
||||
imagePath: imagePath,
|
||||
backgroundType: backgroundType);
|
||||
backgroundType: backgroundType,
|
||||
preColoredIcon: preColoredIcon);
|
||||
|
||||
CardDesign withIcon(CardIconPath icon) => CardDesign(
|
||||
gradient: gradient,
|
||||
colors: colors,
|
||||
imagePath: icon.path,
|
||||
backgroundType: backgroundType,
|
||||
preColoredIcon: icon.preColored);
|
||||
|
||||
static const String _balanceCardIconPrefix = "assets/new-ui/balance_card_icons";
|
||||
static const String _chainIconPrefix = "assets/new-ui/card_icons/chain_icons";
|
||||
static const String _ogIconPrefix = "assets/new-ui/card_icons/og_icons";
|
||||
static const String _outlineIconPrefix = "assets/new-ui/card_icons/outline_icons";
|
||||
static const String _symbolIconPrefix = "assets/new-ui/card_icons/symbol_icons";
|
||||
static const String _genericCakeIcon = "$_balanceCardIconPrefix/cake-card-icon.svg";
|
||||
|
||||
static const Map<CryptoCurrency, _CurrencyIconNames> _iconNames = {
|
||||
CryptoCurrency.arbEth: _CurrencyIconNames(ticker: 'arb', longName: 'arbitrum'),
|
||||
CryptoCurrency.baseEth:
|
||||
_CurrencyIconNames(ticker: 'base', longName: 'base', chainFile: 'base_icon'),
|
||||
CryptoCurrency.robEth: _CurrencyIconNames(ticker: "rob", longName: "robinhood"),
|
||||
CryptoCurrency.bch:
|
||||
_CurrencyIconNames(ticker: 'bch', longName: 'bitcoin_cash', chainFile: 'bitcoin-cash'),
|
||||
CryptoCurrency.btc: _CurrencyIconNames(ticker: 'btc', longName: 'bitcoin', outlineFile: 'BTC'),
|
||||
CryptoCurrency.bnb: _CurrencyIconNames(ticker: 'bnb', longName: 'bnb'),
|
||||
CryptoCurrency.dcr: _CurrencyIconNames(ticker: 'dcr', longName: 'decred'),
|
||||
CryptoCurrency.doge: _CurrencyIconNames(ticker: 'doge', longName: 'dogecoin'),
|
||||
CryptoCurrency.eth: _CurrencyIconNames(ticker: 'eth', longName: 'ethereum'),
|
||||
CryptoCurrency.btcln: _CurrencyIconNames(ticker: 'ln', longName: 'lightning'),
|
||||
CryptoCurrency.ltc: _CurrencyIconNames(ticker: 'ltc', longName: 'litecoin'),
|
||||
CryptoCurrency.xmr:
|
||||
_CurrencyIconNames(ticker: 'xmr', longName: 'monero', ogPath: 'assets/images/xmr-og.webp'),
|
||||
CryptoCurrency.nano: _CurrencyIconNames(ticker: 'xno', longName: 'nano'),
|
||||
CryptoCurrency.maticpoly: _CurrencyIconNames(ticker: 'pol', longName: 'polygon'),
|
||||
CryptoCurrency.sol: _CurrencyIconNames(ticker: 'sol', longName: 'solana'),
|
||||
CryptoCurrency.trx:
|
||||
_CurrencyIconNames(ticker: 'trx', longName: 'tron', ogPath: '$_ogIconPrefix/tron-og.svg'),
|
||||
CryptoCurrency.zano: _CurrencyIconNames(ticker: 'zano', longName: 'zano'),
|
||||
CryptoCurrency.zec: _CurrencyIconNames(ticker: 'zec', longName: 'zcash'),
|
||||
};
|
||||
|
||||
static List<CardIconPath> iconPathsForWalletType(CryptoCurrency currency) {
|
||||
final n = _iconNames[currency];
|
||||
if (n == null) return const [];
|
||||
|
||||
return [
|
||||
CardIconPath('$_symbolIconPrefix/${n.ticker}-symbol.svg'),
|
||||
CardIconPath('$_outlineIconPrefix/${n.outlineFile ?? n.ticker}-outline.svg'),
|
||||
CardIconPath('$_balanceCardIconPrefix/${n.longName}.svg'),
|
||||
CardIconPath('$_chainIconPrefix/${n.chainFile ?? n.longName}.svg', preColored: true),
|
||||
CardIconPath(n.ogPath ?? '$_ogIconPrefix/${n.ticker}-og.svg', preColored: true),
|
||||
const CardIconPath(_genericCakeIcon),
|
||||
];
|
||||
}
|
||||
|
||||
static const List<CardDesign> all = [
|
||||
genericDefault,
|
||||
@@ -343,6 +420,7 @@ class CardDesign {
|
||||
arbitrum,
|
||||
zec,
|
||||
bnb,
|
||||
robinhood,
|
||||
ethSpecial,
|
||||
btcSpecial,
|
||||
xmrSpecial,
|
||||
@@ -357,7 +435,8 @@ class CardDesign {
|
||||
zanoSpecial,
|
||||
arbitrumSpecial,
|
||||
zecSpecial,
|
||||
bnbSpecial
|
||||
bnbSpecial,
|
||||
robinhoodSpecial,
|
||||
];
|
||||
|
||||
static CardDesign forCurrencySpecial(CryptoCurrency currency) {
|
||||
@@ -387,6 +466,7 @@ class CardDesign {
|
||||
CryptoCurrency.zec: zec,
|
||||
CryptoCurrency.dcr: dcr,
|
||||
CryptoCurrency.bnb: bnb,
|
||||
CryptoCurrency.robEth: robinhood,
|
||||
};
|
||||
|
||||
static const Map<CryptoCurrency, CardDesign> specialDesignsForCurrencies = {
|
||||
@@ -408,6 +488,7 @@ class CardDesign {
|
||||
CryptoCurrency.arbEth: arbitrumSpecial,
|
||||
CryptoCurrency.zec: zecSpecial,
|
||||
CryptoCurrency.bnb: bnbSpecial,
|
||||
CryptoCurrency.robEth: robinhoodSpecial,
|
||||
};
|
||||
|
||||
static Map<Gradient, CardColorCombination> preferredColorCombinations = {
|
||||
@@ -423,29 +504,89 @@ class CardDesign {
|
||||
CardDesign.gradientBlack: CardColorCombination.black,
|
||||
};
|
||||
|
||||
CardDesign withIconStyleIndex(int iconStyleIndex, CryptoCurrency currency) {
|
||||
final paths = iconPathsForWalletType(currency);
|
||||
if (paths.isEmpty) return this;
|
||||
if (iconStyleIndex < 0 || iconStyleIndex >= paths.length) return this;
|
||||
return withIcon(paths[iconStyleIndex]);
|
||||
}
|
||||
|
||||
static Gradient gradientForStoredIndex(
|
||||
int gradientIndex,
|
||||
CryptoCurrency walletCurrency,
|
||||
) {
|
||||
final n = CardDesign.allGradients.length;
|
||||
final special = specialDesignsForCurrencies[walletCurrency];
|
||||
if (gradientIndex >= 0 && gradientIndex < n) {
|
||||
return CardDesign.allGradients[gradientIndex];
|
||||
}
|
||||
if (gradientIndex == n && special != null) {
|
||||
return special.gradient;
|
||||
}
|
||||
return gradientBlue;
|
||||
}
|
||||
|
||||
static CardDesign fromStyleSettings(
|
||||
BalanceCardStyleSettings? setting, CryptoCurrency walletCurrency) {
|
||||
if (setting == null) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
} else if (setting.backgroundImagePath.isNotEmpty) {
|
||||
return CardDesign(
|
||||
imagePath: setting.backgroundImagePath,
|
||||
}
|
||||
|
||||
if (setting.isGradientOnly) {
|
||||
final gradient = gradientForStoredIndex(setting.gradientIndex, walletCurrency);
|
||||
final textColors = preferredColorCombinations[gradient] ??
|
||||
specialDesignsForCurrencies[walletCurrency]?.colors ??
|
||||
gradientOnlyDesign.colors;
|
||||
return gradientOnlyDesign.withGradientAndColorCombination(gradient, textColors);
|
||||
}
|
||||
|
||||
if (setting.backgroundImagePath.isNotEmpty) {
|
||||
return CardDesign(imagePath: setting.backgroundImagePath);
|
||||
}
|
||||
|
||||
if (setting.useSpecialDesign && setting.gradientIndex != -1) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency).withGradient(
|
||||
gradientForStoredIndex(setting.gradientIndex, walletCurrency),
|
||||
);
|
||||
} else if (setting.useSpecialDesign && setting.gradientIndex != -1) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency)
|
||||
.withGradient(CardDesign.allGradients[setting.gradientIndex]);
|
||||
} else if (!setting.useSpecialDesign && setting.gradientIndex == -1) {
|
||||
}
|
||||
|
||||
if (!setting.useSpecialDesign && setting.gradientIndex == -1) {
|
||||
final specialColors = specialDesignsForCurrencies[walletCurrency] ?? genericDefault;
|
||||
return CardDesign.forCurrencyIcon(walletCurrency)
|
||||
final design = CardDesign.forCurrencyIcon(walletCurrency)
|
||||
.withGradientAndColorCombination(specialColors.gradient, specialColors.colors);
|
||||
} else if (setting.useSpecialDesign) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
} else if (setting.gradientIndex != -1) {
|
||||
return CardDesign.forCurrencyIcon(walletCurrency)
|
||||
.withGradient(CardDesign.allGradients[setting.gradientIndex]);
|
||||
} else {
|
||||
printV("somehow, the user saved the design settings with literally no customization?");
|
||||
return design.withIconStyleIndex(setting.iconStyleIndex, walletCurrency);
|
||||
}
|
||||
if (setting.useSpecialDesign) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
}
|
||||
if (setting.gradientIndex != -1) {
|
||||
final baseIcon = CardDesign.forCurrencyIcon(walletCurrency);
|
||||
final gradient = gradientForStoredIndex(setting.gradientIndex, walletCurrency);
|
||||
final textColors = preferredColorCombinations[gradient] ??
|
||||
specialDesignsForCurrencies[walletCurrency]?.colors ??
|
||||
baseIcon.colors;
|
||||
return baseIcon
|
||||
.withGradientAndColorCombination(gradient, textColors)
|
||||
.withIconStyleIndex(setting.iconStyleIndex, walletCurrency);
|
||||
}
|
||||
printV("somehow, the user saved the design settings with literally no "
|
||||
"customization?");
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurrencyIconNames {
|
||||
final String ticker;
|
||||
final String longName;
|
||||
final String? outlineFile;
|
||||
final String? chainFile;
|
||||
final String? ogPath;
|
||||
|
||||
const _CurrencyIconNames({
|
||||
required this.ticker,
|
||||
required this.longName,
|
||||
this.outlineFile,
|
||||
this.chainFile,
|
||||
this.ogPath,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,10 @@ extension MaxDecimals on String {
|
||||
return [parts.first.withLocalSeperator(locale), ...parts.sublist(1)].join(" ");
|
||||
}
|
||||
|
||||
final formater = NumberFormat("#,###", locale);
|
||||
final formater = NumberFormat(
|
||||
"#,###",
|
||||
Intl.verifiedLocale(locale, NumberFormat.localeExists, onFailure: (_) => "en"),
|
||||
);
|
||||
final parts = replaceAll(",", "").split(".");
|
||||
if (parts.first.contains("< 0")) parts.first = "0";
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/currency_groups.dart';
|
||||
import 'package:cw_core/enumerable_item.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
import 'package:cw_core/parse_fixed.dart';
|
||||
|
||||
class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implements Currency {
|
||||
const CryptoCurrency({
|
||||
@@ -17,18 +18,34 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
this.chainIconPath,
|
||||
this.enabled = false,
|
||||
this.isPotentialScam = false,
|
||||
this.groups = const {},
|
||||
}) : super(title: title, raw: raw);
|
||||
|
||||
final String name;
|
||||
final String? tag;
|
||||
final String? fullName;
|
||||
final String? iconPath;
|
||||
final String? flatIconPath;
|
||||
final String? chainIconPath;
|
||||
@override
|
||||
final int decimals;
|
||||
final bool enabled;
|
||||
final bool isPotentialScam;
|
||||
final Set<String> groups;
|
||||
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
final String? fullName;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
final int decimals;
|
||||
|
||||
@override
|
||||
String get symbol => title;
|
||||
@override
|
||||
String get apiString => "crypto.$title";
|
||||
|
||||
set enabled(bool value) => this.enabled = value;
|
||||
|
||||
@@ -126,6 +143,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
CryptoCurrency.arbEth,
|
||||
CryptoCurrency.usdcArb,
|
||||
CryptoCurrency.usdtArb,
|
||||
CryptoCurrency.robEth,
|
||||
];
|
||||
|
||||
static const havenCurrencies = [
|
||||
@@ -197,7 +215,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 5,
|
||||
name: 'dai',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/dai.svg',
|
||||
decimals: 18);
|
||||
decimals: 18,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const dash = CryptoCurrency(
|
||||
title: 'DASH',
|
||||
fullName: 'Dash',
|
||||
@@ -254,7 +273,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 12,
|
||||
name: 'usdt',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdterc20 = CryptoCurrency(
|
||||
title: 'USDT',
|
||||
tag: 'ETH',
|
||||
@@ -262,7 +282,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 13,
|
||||
name: 'usdterc20',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const xlm = CryptoCurrency(
|
||||
title: 'XLM',
|
||||
fullName: 'Stellar',
|
||||
@@ -359,7 +380,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 36,
|
||||
name: 'usdttrc20',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const hbar = CryptoCurrency(
|
||||
title: 'HBAR',
|
||||
fullName: 'Hedera',
|
||||
@@ -390,7 +412,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 40,
|
||||
name: 'usdc',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdcsol = CryptoCurrency(
|
||||
title: 'USDC',
|
||||
tag: 'SOL',
|
||||
@@ -398,7 +421,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 41,
|
||||
name: 'usdcsol',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const zaddr = CryptoCurrency(
|
||||
title: 'ZZEC',
|
||||
tag: 'ZEC',
|
||||
@@ -437,7 +461,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 46,
|
||||
name: 'usdcpoly',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const dcr = CryptoCurrency(
|
||||
title: 'DCR',
|
||||
fullName: 'Decred',
|
||||
@@ -758,7 +783,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 87,
|
||||
name: 'usdtpoly',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdcEPoly = CryptoCurrency(
|
||||
title: 'USDC.E',
|
||||
tag: 'POL',
|
||||
@@ -788,7 +814,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 91,
|
||||
name: 'usdtsol',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdcTrc20 = CryptoCurrency(
|
||||
title: 'USDC',
|
||||
tag: 'TRX',
|
||||
@@ -796,7 +823,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 92,
|
||||
name: 'usdctrc20',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const tbtc = CryptoCurrency(
|
||||
title: 'tBTC',
|
||||
fullName: 'Testnet Bitcoin',
|
||||
@@ -844,7 +872,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 98,
|
||||
name: 'deuro',
|
||||
iconPath: 'assets/images/deuro_icon.png',
|
||||
decimals: 18);
|
||||
decimals: 18,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdtbsc = CryptoCurrency(
|
||||
title: 'USDT',
|
||||
tag: 'BSC',
|
||||
@@ -852,7 +881,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 99,
|
||||
name: 'usdtbsc',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 18);
|
||||
decimals: 18,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const ndeps = CryptoCurrency(
|
||||
title: 'NDEPS',
|
||||
tag: 'ETH',
|
||||
@@ -891,7 +921,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
fullName: 'Ethereum',
|
||||
raw: 104,
|
||||
name: 'baseth',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/base.svg',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/ethereum.svg',
|
||||
decimals: 18,
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/base.svg",
|
||||
chainIconPath: "assets/new-ui/chain_badges/base.svg");
|
||||
@@ -911,7 +941,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
name: 'arbeth',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/ethereum.svg',
|
||||
decimals: 18,
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/arbitrum.svg");
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/arbitrum.svg",
|
||||
chainIconPath: "assets/new-ui/chain_badges/arbitrum.svg");
|
||||
static const zec = CryptoCurrency(
|
||||
title: 'ZEC',
|
||||
fullName: 'Zcash',
|
||||
@@ -927,7 +958,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 108,
|
||||
name: 'usdcarb',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdtArb = CryptoCurrency(
|
||||
title: 'USDT',
|
||||
tag: 'ARB',
|
||||
@@ -935,7 +967,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 109,
|
||||
name: 'usdtarb',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const ltcmweb = CryptoCurrency(
|
||||
title: 'LTC',
|
||||
fullName: 'Litecoin MWeb',
|
||||
@@ -943,6 +976,16 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
name: 'ltcmweb',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/litecoin.svg',
|
||||
decimals: 8);
|
||||
static const robEth = CryptoCurrency(
|
||||
title: "ETH",
|
||||
tag: "ROB",
|
||||
fullName: "Ethereum",
|
||||
raw: 111,
|
||||
name: "robeth",
|
||||
iconPath: "assets/new-ui/crypto_full_icons/ethereum.svg",
|
||||
decimals: 18,
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/robinhood.svg",
|
||||
chainIconPath: "assets/new-ui/chain_badges/robinhood.svg");
|
||||
|
||||
static final Map<int, CryptoCurrency> _rawCurrencyMap = [
|
||||
...all,
|
||||
@@ -968,7 +1011,14 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
...zcashCurrencies
|
||||
].fold<Map<String, CryptoCurrency>>(<String, CryptoCurrency>{}, (acc, item) {
|
||||
if (item.fullName != null) {
|
||||
acc.addAll({item.fullName!.toLowerCase(): item});
|
||||
final fullName = item.fullName!.toLowerCase();
|
||||
final existing = acc[fullName];
|
||||
|
||||
// Mainnet ETH and the L2 ETHs share the "Ethereum" full name,
|
||||
// so we make the untagged coin keeps the name
|
||||
if (existing == null || existing.tag != null) {
|
||||
acc[fullName] = item;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
@@ -978,6 +1028,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
'bitcoin': btc,
|
||||
'bitcoincash': bch,
|
||||
'polygon': maticpoly,
|
||||
'solana': sol,
|
||||
'nano-gpt': nano,
|
||||
'secret': scrt,
|
||||
'stellar': xlm,
|
||||
@@ -993,6 +1044,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
'base': baseEth,
|
||||
'arbitrum': arbEth,
|
||||
'bsc': bnb,
|
||||
"rob": robEth,
|
||||
"robinhood": robEth,
|
||||
};
|
||||
|
||||
static CryptoCurrency deserialize({required int raw}) {
|
||||
@@ -1094,6 +1147,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
int? decimals,
|
||||
bool? enabled,
|
||||
bool? isPotentialScam,
|
||||
Set<String>? groups,
|
||||
}) =>
|
||||
CryptoCurrency(
|
||||
title: title ?? this.title,
|
||||
@@ -1105,6 +1159,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
decimals: decimals ?? this.decimals,
|
||||
enabled: enabled ?? this.enabled,
|
||||
isPotentialScam: isPotentialScam ?? this.isPotentialScam,
|
||||
groups: groups ?? this.groups,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -1112,13 +1167,13 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
|
||||
bool titleAndTagEqual(CryptoCurrency other) => title == other.title && tag == other.tag;
|
||||
|
||||
@override
|
||||
Money parseAmount(String value) => Money.parse(value, this);
|
||||
|
||||
@override
|
||||
Money? tryParseAmount(String value) => Money.tryParse(value, this);
|
||||
|
||||
/// Format the raw amount into its decimal representation eg. turn Sats into Bitcoin
|
||||
String formatAmount(BigInt amount, {int? fractionalDigits, bool trimZeros = true}) =>
|
||||
formatFixed(amount, decimals, fractionalDigits: fractionalDigits, trimZeros: trimZeros);
|
||||
|
||||
/// Parse the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
|
||||
BigInt parseAmount(String value) => parseFixed(value, decimals);
|
||||
|
||||
/// Try parsing the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
|
||||
BigInt? tryParseAmount(String value) => tryParseFixed(value, decimals);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
enum MemoLabelType { destinationTag, memo }
|
||||
|
||||
const Map<CryptoCurrency, MemoLabelType> _currenciesRequiringMemo = {
|
||||
CryptoCurrency.xrp: MemoLabelType.destinationTag,
|
||||
CryptoCurrency.xlm: MemoLabelType.memo,
|
||||
CryptoCurrency.ton: MemoLabelType.memo,
|
||||
CryptoCurrency.eos: MemoLabelType.memo,
|
||||
CryptoCurrency.hbar: MemoLabelType.memo,
|
||||
};
|
||||
|
||||
MemoLabelType? memoLabelTypeFor(CryptoCurrency currency) => _currenciesRequiringMemo[currency];
|
||||
@@ -1,7 +1,24 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
abstract class Currency {
|
||||
String get symbol;
|
||||
String get name;
|
||||
String? get tag;
|
||||
String? get fullName;
|
||||
String? get iconPath;
|
||||
int get decimals;
|
||||
String? get tag;
|
||||
|
||||
/// Parse the [value] and turn it into [Money]
|
||||
Money parseAmount(String value);
|
||||
|
||||
/// Try parsing the [value] and turn it into [Money]
|
||||
Money? tryParseAmount(String value);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Currency && other.decimals == decimals && other.symbol == symbol && other.tag == tag;
|
||||
|
||||
@override
|
||||
int get hashCode => decimals.hashCode ^ symbol.hashCode ^ tag.hashCode;
|
||||
String get apiString;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ CryptoCurrency getCryptoCurrencyByChainId(int chainId) {
|
||||
return CryptoCurrency.arbEth;
|
||||
case 56:
|
||||
return CryptoCurrency.bnb;
|
||||
case 4663:
|
||||
return CryptoCurrency.robEth;
|
||||
default:
|
||||
return CryptoCurrency.eth;
|
||||
}
|
||||
@@ -73,6 +75,8 @@ int? getChainIdByCryptoCurrency(CryptoCurrency currency) {
|
||||
return 42161;
|
||||
case CryptoCurrency.bnb:
|
||||
return 56;
|
||||
case CryptoCurrency.robEth:
|
||||
return 4663;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -97,5 +101,40 @@ String getCryptoCurrencyIconForWalletListItem(WalletType type,
|
||||
return "assets/new-ui/crypto_full_icons/base.svg";
|
||||
}
|
||||
|
||||
|
||||
return walletTypeToCryptoCurrency(type, isTestnet: isTestnet, chainId: chainId).iconPath ?? "";
|
||||
}
|
||||
|
||||
String? symbolIconPathForWalletType(WalletType type) {
|
||||
const prefix = "assets/new-ui/card_icons/symbol_icons";
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return "$prefix/xmr-symbol.svg";
|
||||
case WalletType.bitcoin:
|
||||
return "$prefix/btc-symbol.svg";
|
||||
case WalletType.litecoin:
|
||||
return "$prefix/ltc-symbol.svg";
|
||||
case WalletType.ethereum:
|
||||
return "$prefix/eth-symbol.svg";
|
||||
case WalletType.base:
|
||||
return "$prefix/base-symbol.svg";
|
||||
case WalletType.arbitrum:
|
||||
return "$prefix/arb-symbol.svg";
|
||||
case WalletType.bsc:
|
||||
return "$prefix/bnb-symbol.svg";
|
||||
case WalletType.bitcoinCash:
|
||||
return "$prefix/bch-symbol.svg";
|
||||
case WalletType.polygon:
|
||||
return "$prefix/pol-symbol.svg";
|
||||
case WalletType.dogecoin:
|
||||
return "$prefix/doge-symbol.svg";
|
||||
case WalletType.nano:
|
||||
return "$prefix/xno-symbol.svg";
|
||||
case WalletType.wownero:
|
||||
case WalletType.banano:
|
||||
case WalletType.none:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool isMonochromeSymbolIcon(String path) => path.contains("/symbol_icons/");
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
abstract class CurrencyGroups {
|
||||
static const stablecoin = 'stablecoin';
|
||||
static const tokenizedStock = 'tokenized_stock';
|
||||
}
|
||||
+357
-72
@@ -1,28 +1,54 @@
|
||||
import 'dart:io';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import "package:cw_core/db/sqlite_debug.dart";
|
||||
import "package:cw_core/root_dir.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:path/path.dart" as p;
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
Database? db;
|
||||
|
||||
Future<void> _addColumnIfNotExists(
|
||||
Database db, {
|
||||
required String table,
|
||||
required String column,
|
||||
required String definition,
|
||||
}) async {
|
||||
Database db, {
|
||||
required String table,
|
||||
required String column,
|
||||
required String definition,
|
||||
}) async {
|
||||
final result = await db.rawQuery("PRAGMA table_info($table)");
|
||||
final columnExists = result.any((row) => row['name'] == column);
|
||||
final columnExists = result.any((row) => row["name"] == column);
|
||||
|
||||
if (!columnExists) {
|
||||
await db.execute(
|
||||
'ALTER TABLE $table ADD COLUMN $column $definition;',
|
||||
"ALTER TABLE $table ADD COLUMN $column $definition;",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> sqliteDebugMarkerFile() async {
|
||||
final appDir = await getAppDir();
|
||||
final dbDebugMarker = p.join(appDir.path, ".sqlite_db_debug");
|
||||
return File(dbDebugMarker);
|
||||
}
|
||||
|
||||
Future<void> initDb({String? pathOverride}) async {
|
||||
if (!kDebugMode && !kProfileMode) {
|
||||
await _initDb(pathOverride: pathOverride);
|
||||
return;
|
||||
}
|
||||
final dbDebugMarker = await sqliteDebugMarkerFile();
|
||||
try {
|
||||
if (dbDebugMarker.existsSync()) {
|
||||
throw Exception("Debug marker is present");
|
||||
}
|
||||
await _initDb(pathOverride: pathOverride);
|
||||
} catch (e, s) {
|
||||
await handleSqliteError(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initDb({String? pathOverride}) async {
|
||||
if (Platform.isLinux || Platform.isWindows) {
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
}
|
||||
@@ -38,11 +64,13 @@ Future<void> initDb({String? pathOverride}) async {
|
||||
}
|
||||
}
|
||||
await db?.close();
|
||||
db = await openDatabase(dbFile.path, version: 6,
|
||||
onUpgrade: (Database db, int oldVersion, int newVersion) async {
|
||||
printV("migrating: $oldVersion, $newVersion");
|
||||
if (oldVersion <= 1) {
|
||||
await db.execute('''
|
||||
db = await openDatabase(
|
||||
dbFile.path,
|
||||
version: 15,
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
printV("migrating: $oldVersion, $newVersion");
|
||||
if (oldVersion <= 1) {
|
||||
await db.execute("""
|
||||
DELETE FROM WalletInfo
|
||||
WHERE walletInfoId NOT IN (
|
||||
SELECT MIN(walletInfoId)
|
||||
@@ -52,10 +80,10 @@ WHERE walletInfoId NOT IN (
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique
|
||||
ON WalletInfo (id);
|
||||
''');
|
||||
}
|
||||
if (oldVersion <= 2) {
|
||||
await db.execute('''
|
||||
""");
|
||||
}
|
||||
if (oldVersion <= 2) {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
@@ -66,37 +94,116 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
|
||||
FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'WalletInfo',
|
||||
column: 'receiveInfoboxDismissed',
|
||||
definition: 'BOOLEAN DEFAULT FALSE',
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "receiveInfoboxDismissed",
|
||||
definition: "BOOLEAN DEFAULT FALSE",
|
||||
);
|
||||
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'cardOrder',
|
||||
definition: 'INTEGER DEFAULT 0',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 3) {
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE");
|
||||
// null - primary token (eth, sol etc)
|
||||
// not null - address of fav token
|
||||
// if address doesn't correspond to a valid token, fallback to primary token
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
|
||||
}
|
||||
if (oldVersion <= 4) {
|
||||
await _createBridgeTransferTable(db);
|
||||
}
|
||||
if (oldVersion <= 5) {
|
||||
await _createTradeTable(db);
|
||||
}
|
||||
}, onCreate: (Database db, int version) async {
|
||||
await db.execute('''
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'cardOrder',
|
||||
definition: 'INTEGER DEFAULT 0',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 3) {
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE");
|
||||
// null - primary token (eth, sol etc)
|
||||
// not null - address of fav token
|
||||
// if address doesn't correspond to a valid token, fallback to primary token
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
if (oldVersion <= 4) {
|
||||
await _createBridgeTransferTable(db);
|
||||
}
|
||||
|
||||
if (oldVersion <= 5) {
|
||||
await _createTradeTable(db);
|
||||
}
|
||||
if (oldVersion <= 6) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'Trade',
|
||||
column: 'toAddressExtraId',
|
||||
definition: 'TEXT',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 7) {
|
||||
await _createNodeTable(db);
|
||||
}
|
||||
if (oldVersion <= 8) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'iconStyleIndex',
|
||||
definition: 'INTEGER DEFAULT 0',
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'isGradientOnly',
|
||||
definition: 'BOOLEAN DEFAULT FALSE',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 9) {
|
||||
await _createErc20TokenTable(db);
|
||||
await _createSplTokenTable(db);
|
||||
await _createTronTokenTable(db);
|
||||
}
|
||||
if (oldVersion <= 10) {
|
||||
await _createImportedNFTTable(db);
|
||||
}
|
||||
if (oldVersion <= 11) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "showSeedBackupReminder",
|
||||
definition: "BOOLEAN DEFAULT FALSE",
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 12) {
|
||||
await _createChartsTables(db);
|
||||
}
|
||||
|
||||
if (oldVersion <= 13) {
|
||||
await _createWalletInfoAccountTable(db);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "accountDiscoveryLimit",
|
||||
definition: "INTEGER DEFAULT NULL",
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "isMultiAccountsEnabled",
|
||||
definition: "INTEGER DEFAULT NULL",
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "currentAccountIndex",
|
||||
definition: "INTEGER NOT NULL DEFAULT 0",
|
||||
);
|
||||
|
||||
await _migrateBitcoinCardStylesForAccounts(db);
|
||||
}
|
||||
if(oldVersion <= 14) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "BalanceCardStyleSettings",
|
||||
column: "hidden",
|
||||
definition: "BOOLEAN DEFAULT FALSE",
|
||||
);
|
||||
}
|
||||
},
|
||||
onCreate: (Database db, int version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE WalletInfo (
|
||||
walletInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
id TEXT NOT NULL,
|
||||
@@ -119,13 +226,17 @@ CREATE TABLE WalletInfo (
|
||||
hashedWalletIdentifier TEXT,
|
||||
isNonSeedWallet INTEGER DEFAULT (0) NOT NULL,
|
||||
sortOrder INTEGER DEFAULT (0) NOT NULL,
|
||||
currentAccountIndex INTEGER NOT NULL DEFAULT 0,
|
||||
receiveInfoboxDismissed BOOLEAN DEFAULT FALSE,
|
||||
showCombinedBalance BOOLEAN DEFAULT TRUE,
|
||||
favoriteTokenAddress TEXT DEFAULT NULL
|
||||
favoriteTokenAddress TEXT DEFAULT NULL,
|
||||
accountDiscoveryLimit INTEGER DEFAULT NULL,
|
||||
isMultiAccountsEnabled INTEGER DEFAULT NULL,
|
||||
showSeedBackupReminder BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE WalletInfoDerivationInfo (
|
||||
walletInfoDerivationInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
address TEXT NOT NULL,
|
||||
@@ -136,9 +247,9 @@ CREATE TABLE WalletInfoDerivationInfo (
|
||||
scriptType TEXT,
|
||||
description TEXT
|
||||
);
|
||||
''');
|
||||
""");
|
||||
|
||||
await db.execute('''
|
||||
await db.execute('''
|
||||
CREATE TABLE WalletInfoAddress (
|
||||
walletInfoAddressId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
walletInfoId INTEGER,
|
||||
@@ -148,7 +259,7 @@ CREATE TABLE WalletInfoAddress (
|
||||
);
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE WalletInfoAddressInfo (
|
||||
walletInfoAddressInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletInfoId INTEGER NOT NULL,
|
||||
@@ -158,9 +269,9 @@ CREATE TABLE WalletInfoAddressInfo (
|
||||
mapValueLabel TEXT NOT NULL,
|
||||
CONSTRAINT WalletInfoAddressInfo_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
""");
|
||||
|
||||
await db.execute('''
|
||||
await db.execute('''
|
||||
CREATE TABLE "WalletInfoAddressMap" (
|
||||
walletInfoAddressMapId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletInfoId INTEGER NOT NULL,
|
||||
@@ -169,29 +280,58 @@ CREATE TABLE "WalletInfoAddressMap" (
|
||||
CONSTRAINT WalletInfoAddress_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique
|
||||
ON WalletInfo (id);
|
||||
''');
|
||||
await db.execute('''
|
||||
""");
|
||||
await db.execute('''
|
||||
CREATE TABLE BalanceCardStyleSettings (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
PRIMARY KEY (walletInfoId, accountIndex),
|
||||
FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await _createBridgeTransferTable(db);
|
||||
await _createTradeTable(db);
|
||||
});
|
||||
await _createBridgeTransferTable(db);
|
||||
await _createNodeTable(db);
|
||||
await _createTradeTable(db);
|
||||
await _createChartsTables(db);
|
||||
await _createErc20TokenTable(db);
|
||||
await _createSplTokenTable(db);
|
||||
await _createTronTokenTable(db);
|
||||
await _createImportedNFTTable(db);
|
||||
await _createWalletInfoAccountTable(db);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createChartsTables(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE PriceData (
|
||||
fromCurrency TEXT NOT NULL,
|
||||
toCurrency TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
price TEXT NOT NULL,
|
||||
PRIMARY KEY (fromCurrency, toCurrency, timestamp)
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE TABLE ChartsAssets (
|
||||
asset TEXT PRIMARY KEY,
|
||||
isFavorite BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createTradeTable(Database db) async {
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS Trade (
|
||||
tradeId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
id TEXT NOT NULL,
|
||||
@@ -225,6 +365,7 @@ CREATE TABLE IF NOT EXISTS Trade (
|
||||
refundAddress TEXT,
|
||||
walletId TEXT,
|
||||
payoutAddress TEXT,
|
||||
toAddressExtraId TEXT,
|
||||
password TEXT,
|
||||
providerId TEXT,
|
||||
providerName TEXT,
|
||||
@@ -245,11 +386,48 @@ CREATE TABLE IF NOT EXISTS Trade (
|
||||
chainId INTEGER,
|
||||
fee REAL
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_trade_id_unique
|
||||
ON Trade (id);
|
||||
''');
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createWalletInfoAccountTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS WalletInfoAccount (
|
||||
walletInfoId INTEGER NOT NULL,
|
||||
accountIndex INTEGER NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
PRIMARY KEY (walletInfoId, accountIndex),
|
||||
CONSTRAINT WalletInfoAccount_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
""");
|
||||
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_walletinfoaccount_walletinfoid
|
||||
ON WalletInfoAccount(walletInfoId);
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
Future<void> _migrateBitcoinCardStylesForAccounts(Database db) async {
|
||||
const bitcoinWallets = 'SELECT walletInfoId FROM WalletInfo WHERE "type" = ?';
|
||||
final btc = WalletType.bitcoin.index;
|
||||
|
||||
// Lightning: 0 -> -2 (first, so accountIndex 0 is free for the Bitcoin card)
|
||||
await db.rawUpdate(
|
||||
"UPDATE OR REPLACE BalanceCardStyleSettings SET accountIndex = -2 "
|
||||
"WHERE accountIndex = 0 AND walletInfoId IN ($bitcoinWallets)",
|
||||
[btc],
|
||||
);
|
||||
|
||||
// Bitcoin: -1 -> 0 (primary account, first in order)
|
||||
await db.rawUpdate(
|
||||
"UPDATE OR REPLACE BalanceCardStyleSettings SET accountIndex = 0, cardOrder = 0 "
|
||||
"WHERE accountIndex = -1 AND walletInfoId IN ($bitcoinWallets)",
|
||||
[btc],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> dumpDb() async {
|
||||
@@ -265,7 +443,7 @@ Future<Map<String, dynamic>> dumpDb() async {
|
||||
|
||||
Future<List<String>> _getTableNames(Database db) async {
|
||||
final tableNames = await db.rawQuery('SELECT name FROM sqlite_master WHERE type = "table"');
|
||||
return tableNames.map((e) => (e["name"]).toString()).toList();
|
||||
return tableNames.map((e) => e["name"].toString()).toList();
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _dumpDb() async {
|
||||
@@ -288,7 +466,7 @@ Future<Map<String, dynamic>> dumpCustomDb(String path) async {
|
||||
}
|
||||
|
||||
Future<void> _createBridgeTransferTable(Database db) async {
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS BridgeTransfer (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
wallet_id TEXT NOT NULL,
|
||||
@@ -307,9 +485,116 @@ CREATE TABLE IF NOT EXISTS BridgeTransfer (
|
||||
error_message TEXT,
|
||||
status_message TEXT
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_bridgetransfer_wallet_id
|
||||
ON BridgeTransfer(wallet_id);
|
||||
''');
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createErc20TokenTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS Erc20Token (
|
||||
Erc20TokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
chainId INTEGER NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
symbol TEXT NOT NULL DEFAULT '',
|
||||
contractAddress TEXT NOT NULL,
|
||||
decimal INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
iconPath TEXT,
|
||||
tag TEXT,
|
||||
isPotentialScam INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_erc20token_wallet_chain_contract
|
||||
ON Erc20Token (walletName, chainId, contractAddress);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createSplTokenTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS SPLToken (
|
||||
SPLTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
symbol TEXT NOT NULL DEFAULT '',
|
||||
mintAddress TEXT NOT NULL,
|
||||
decimal INTEGER NOT NULL DEFAULT 0,
|
||||
mint TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
iconPath TEXT,
|
||||
tag TEXT,
|
||||
isPotentialScam INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_spltoken_wallet_mint
|
||||
ON SPLToken (walletName, mintAddress);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createImportedNFTTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ImportedNFT (
|
||||
ImportedNFTId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
identifier TEXT NOT NULL,
|
||||
name TEXT,
|
||||
symbol TEXT,
|
||||
description TEXT,
|
||||
imageUrl TEXT,
|
||||
isOwned INTEGER
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_importednft_wallet_chain_identifier
|
||||
ON ImportedNFT (walletName, chain, identifier);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createTronTokenTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS TronToken (
|
||||
TronTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
symbol TEXT NOT NULL DEFAULT '',
|
||||
contractAddress TEXT NOT NULL,
|
||||
decimal INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
iconPath TEXT,
|
||||
tag TEXT,
|
||||
isPotentialScam INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_trontoken_wallet_contract
|
||||
ON TronToken (walletName, contractAddress);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createNodeTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE Node (
|
||||
NodeId INTEGER PRIMARY KEY,
|
||||
uri TEXT NOT NULL,
|
||||
path TEXT,
|
||||
login TEXT,
|
||||
label TEXT,
|
||||
password TEXT,
|
||||
isPow INTEGER NOT NULL,
|
||||
useSSL INTEGER,
|
||||
typeRaw INTEGER NOT NULL,
|
||||
trusted INTEGER NOT NULL,
|
||||
socksProxyAddress TEXT,
|
||||
isEnabledForAutoSwitching BOOLEAN DEFAULT FALSE,
|
||||
isOfficial BOOLEAN DEFAULT FALSE,
|
||||
isBuiltin BOOLEAN DEFAULT FALSE,
|
||||
isDefault BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/db/sqlite.dart';
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> handleSqliteError(final dynamic error, final StackTrace stackTrace) async {
|
||||
try {
|
||||
final f = await sqliteDebugMarkerFile();
|
||||
f.createSync();
|
||||
} catch (_) {}
|
||||
runApp(
|
||||
SqliteErrorHandlerApp(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
),
|
||||
);
|
||||
await Future.delayed(Duration(hours: 24));
|
||||
}
|
||||
|
||||
class SqliteErrorHandlerApp extends StatelessWidget {
|
||||
const SqliteErrorHandlerApp({
|
||||
super.key,
|
||||
required this.error,
|
||||
required this.stackTrace,
|
||||
});
|
||||
|
||||
final dynamic error;
|
||||
final StackTrace? stackTrace;
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Cake Wallet Crash',
|
||||
themeMode: ThemeMode.light,
|
||||
home: SqliteErrorHandler(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteErrorHandler extends StatefulWidget {
|
||||
SqliteErrorHandler({
|
||||
super.key,
|
||||
required this.error,
|
||||
required this.stackTrace,
|
||||
});
|
||||
|
||||
final dynamic error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
@override
|
||||
State<SqliteErrorHandler> createState() => _SqliteErrorHandlerState();
|
||||
}
|
||||
|
||||
class _SqliteErrorHandlerState extends State<SqliteErrorHandler> {
|
||||
String log = "";
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Sqlite Recovery Screen"),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildNotice(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _text(final dynamic element) {
|
||||
return SelectableText(
|
||||
element.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
void _dbgRemoveDebugMarker() async {
|
||||
final f = await sqliteDebugMarkerFile();
|
||||
f.delete();
|
||||
}
|
||||
|
||||
void _corruptTheDb() async {
|
||||
final dbFile = File("${(await getAppDir()).path}/cake.db");
|
||||
await writeBytesToFile(dbFile);
|
||||
}
|
||||
|
||||
void _dumpDb() async {
|
||||
try {
|
||||
final dbFile = File("${(await getAppDir()).path}/cake.db");
|
||||
final jsonText = JsonEncoder.withIndent(" ").convert(await dumpCustomDb(dbFile.path));
|
||||
setState(() {
|
||||
log = jsonText;
|
||||
});
|
||||
} catch (e, s) {
|
||||
setState(() {
|
||||
log = """
|
||||
e: $e
|
||||
|
||||
s: $s
|
||||
""";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _removeDb() async {
|
||||
try {
|
||||
File("${(await getAppDir()).path}/cake.db.old").deleteSync();
|
||||
} catch (_) {}
|
||||
final dbFile = File("${(await getAppDir()).path}/cake.db");
|
||||
dbFile.renameSync("${(await getAppDir()).path}/cake.db.old");
|
||||
}
|
||||
|
||||
List<Widget> _debugOptions() {
|
||||
return [
|
||||
_debugButton("Remove debug marker", _dbgRemoveDebugMarker),
|
||||
if (kDebugMode || kProfileMode) _debugButton("[DO NOT PRESS] Corrupt the DB", _corruptTheDb),
|
||||
_debugButton("Remove DB", _removeDb),
|
||||
_debugButton("Dump DB", _dumpDb),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _debugButton(String text, VoidCallback callback) {
|
||||
return ElevatedButton(
|
||||
onPressed: callback,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildNotice() {
|
||||
return [
|
||||
_text("""Critical error occured in SQLite
|
||||
This is very rare, usually caused by the device killing the app before it finishes writing file.
|
||||
Here you can recover the database or recreate it to regain access to your wallets.
|
||||
During this process some of the metadata may get lost.
|
||||
If you need help, report it at https://hash.boats/report."""),
|
||||
Divider(),
|
||||
..._debugOptions(),
|
||||
if (log.isNotEmpty) ...[
|
||||
Divider(),
|
||||
SelectableText(
|
||||
log,
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
Divider(),
|
||||
_text(widget.error),
|
||||
Divider(),
|
||||
_text(widget.stackTrace),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> writeBytesToFile(File file) async {
|
||||
const int seek = 5;
|
||||
const int count = 50000;
|
||||
final raf = await file.open(mode: FileMode.writeOnlyAppend);
|
||||
try {
|
||||
await raf.setPosition(seek);
|
||||
final zeros = Uint8List.fromList(List.generate(count, (_) => 0x42));
|
||||
await raf.writeFrom(zeros);
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,34 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:cw_core/utils/file.dart' as file;
|
||||
import 'package:cake_backup/backup.dart' as cwb;
|
||||
|
||||
EncryptionFileUtils encryptionFileUtilsFor(bool direct) =>
|
||||
direct ? XChaCha20EncryptionFileUtils() : Salsa20EncryhptionFileUtils();
|
||||
EncryptionFileUtils encryptionFileUtilsFor(bool isDirect) =>
|
||||
XChaCha20MigratingEncryptionFileUtils(isDirect: isDirect);
|
||||
|
||||
abstract class EncryptionFileUtils {
|
||||
Future<void> write({required String path, required String password, required String data});
|
||||
Future<String> read({required String path, required String password});
|
||||
}
|
||||
|
||||
class Salsa20EncryhptionFileUtils extends EncryptionFileUtils {
|
||||
// Requires legacy complex key + iv as password
|
||||
@override
|
||||
Future<void> write(
|
||||
{required String path, required String password, required String data}) async =>
|
||||
await file.write(path: path, password: password, data: data);
|
||||
class XChaCha20MigratingEncryptionFileUtils extends EncryptionFileUtils {
|
||||
XChaCha20MigratingEncryptionFileUtils({required this.isDirect});
|
||||
|
||||
// Requires legacy complex key + iv as password
|
||||
@override
|
||||
Future<String> read({required String path, required String password}) async =>
|
||||
await file.read(path: path, password: password);
|
||||
}
|
||||
final bool isDirect;
|
||||
|
||||
class XChaCha20EncryptionFileUtils extends EncryptionFileUtils {
|
||||
@override
|
||||
Future<void> write({required String path, required String password, required String data}) async {
|
||||
final encrypted = await cwb.encrypt(password, Uint8List.fromList(data.codeUnits));
|
||||
await File(path).writeAsBytes(encrypted);
|
||||
Future<void> write({required String path, required String password, required String data}) {
|
||||
return file.write(
|
||||
path: path,
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: !isDirect,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> read({required String path, required String password}) async {
|
||||
final file = File(path);
|
||||
final encrypted = await file.readAsBytes();
|
||||
final bytes = await cwb.decrypt(password, encrypted);
|
||||
return String.fromCharCodes(bytes);
|
||||
Future<String> read({required String path, required String password}) {
|
||||
return file.read(
|
||||
path: path,
|
||||
password: password,
|
||||
highEntropyPassphrase: !isDirect,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
abstract class EnumerableItem<T> {
|
||||
const EnumerableItem({required this.title, required this.raw});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ abstract class Enumerate {
|
||||
String get value;
|
||||
|
||||
@override
|
||||
operator ==(other) {
|
||||
bool operator ==(other) {
|
||||
if (identical(other, this)) return true;
|
||||
if (other is! Enumerate) return false;
|
||||
return other.runtimeType == runtimeType && value == other.value;
|
||||
|
||||
+159
-45
@@ -1,32 +1,8 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'erc20_token.g.dart';
|
||||
|
||||
@HiveType(typeId: Erc20Token.typeId)
|
||||
class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
@HiveField(2)
|
||||
final String contractAddress;
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
@HiveField(5)
|
||||
String? iconPath;
|
||||
@HiveField(6)
|
||||
final String? tag;
|
||||
@HiveField(7, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class Erc20Token extends CryptoCurrency {
|
||||
Erc20Token({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
@@ -36,6 +12,10 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
this.iconPath,
|
||||
this.tag,
|
||||
this.isPotentialScam = false,
|
||||
this.id = 0,
|
||||
this.walletName,
|
||||
this.chainId,
|
||||
Set<String> groups = const {},
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: symbol.toLowerCase(),
|
||||
@@ -45,38 +25,172 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
iconPath: iconPath,
|
||||
decimals: decimal,
|
||||
isPotentialScam: isPotentialScam,
|
||||
groups: groups,
|
||||
);
|
||||
|
||||
Erc20Token.copyWith(Erc20Token other, {String? icon, String? tag, bool? enabled})
|
||||
: this.name = other.name,
|
||||
this.symbol = other.symbol,
|
||||
this.contractAddress = other.contractAddress,
|
||||
this.decimal = other.decimal,
|
||||
this._enabled = enabled ?? other.enabled,
|
||||
this.tag = tag ?? other.tag,
|
||||
this.iconPath = icon ?? other.iconPath,
|
||||
this.isPotentialScam = other.isPotentialScam,
|
||||
Erc20Token.copyWith(
|
||||
Erc20Token other, {
|
||||
String? icon,
|
||||
super.tag,
|
||||
bool? enabled,
|
||||
String? walletName,
|
||||
int? chainId,
|
||||
}) : name = other.name,
|
||||
symbol = other.symbol,
|
||||
contractAddress = other.contractAddress,
|
||||
decimal = other.decimal,
|
||||
_enabled = enabled ?? other.enabled,
|
||||
tag = tag ?? other.tag,
|
||||
iconPath = icon ?? other.iconPath,
|
||||
isPotentialScam = other.isPotentialScam,
|
||||
id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
chainId = chainId ?? other.chainId,
|
||||
super(
|
||||
name: other.name,
|
||||
title: other.symbol.toUpperCase(),
|
||||
fullName: other.name,
|
||||
tag: tag,
|
||||
iconPath: icon,
|
||||
decimals: other.decimal,
|
||||
isPotentialScam: other.isPotentialScam,
|
||||
groups: other.groups,
|
||||
);
|
||||
|
||||
static const typeId = ERC20_TOKEN_TYPE_ID;
|
||||
static const boxName = 'Erc20Tokens';
|
||||
static const ethereumBoxName = 'EthereumErc20Tokens';
|
||||
static const polygonBoxName = 'PolygonErc20Tokens';
|
||||
static const baseBoxName = 'BaseErc20Tokens';
|
||||
static const arbitrumBoxName = 'ArbitrumErc20Tokens';
|
||||
static const bscBoxName = 'BscErc20Tokens';
|
||||
Erc20Token.fromMap(Map<String, Object?> map)
|
||||
: this(
|
||||
name: map["name"] as String? ?? "",
|
||||
symbol: map["symbol"] as String? ?? "",
|
||||
contractAddress: map["contractAddress"] as String? ?? "",
|
||||
decimal: (map["decimal"] ?? 0) as int,
|
||||
enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
|
||||
iconPath: map["iconPath"] as String?,
|
||||
tag: map["tag"] as String?,
|
||||
isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
|
||||
id: (map[selfIdColumn] ?? 0) as int,
|
||||
walletName: map["walletName"] as String?,
|
||||
chainId: map["chainId"] as int?,
|
||||
);
|
||||
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
String? iconPath;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
bool isPotentialScam;
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
int id;
|
||||
int? chainId;
|
||||
bool _enabled;
|
||||
final int decimal;
|
||||
String? walletName;
|
||||
final String contractAddress;
|
||||
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"chainId": chainId,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"contractAddress": contractAddress.toLowerCase(),
|
||||
"decimal": decimal,
|
||||
"enabled": _enabled ? 1 : 0,
|
||||
"iconPath": iconPath,
|
||||
"tag": tag,
|
||||
"isPotentialScam": isPotentialScam ? 1 : 0,
|
||||
};
|
||||
|
||||
static String get tableName => "Erc20Token";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
Future<int> save() async {
|
||||
if (walletName == null || chainId == null) {
|
||||
throw StateError("Erc20Token.save() requires walletName and chainId to be set");
|
||||
}
|
||||
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<Erc20Token>> selectList(
|
||||
String where,
|
||||
List<dynamic> whereArgs, {
|
||||
String? orderBy,
|
||||
}) async {
|
||||
orderBy ??= selfIdColumn;
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => Erc20Token.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<List<Erc20Token>> getAllForWallet(String walletName, int chainId) =>
|
||||
selectList("walletName = ? AND chainId = ?", [walletName, chainId]);
|
||||
|
||||
static Future<Erc20Token?> getByContract(
|
||||
String walletName,
|
||||
int chainId,
|
||||
String contractAddress,
|
||||
) async {
|
||||
final list = await selectList(
|
||||
"walletName = ? AND chainId = ? AND contractAddress = ?",
|
||||
[walletName, chainId, contractAddress.toLowerCase()],
|
||||
);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
static Future<int> deleteForWallet(String walletName, int chainId, String contractAddress) =>
|
||||
db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND chainId = ? AND contractAddress = ?",
|
||||
whereArgs: [walletName, chainId, contractAddress.toLowerCase()],
|
||||
);
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName) =>
|
||||
db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
|
||||
|
||||
static Future<void> renameWallet(String oldName, String newName) async {
|
||||
await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
|
||||
await db!
|
||||
.update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is Erc20Token && other.contractAddress == contractAddress;
|
||||
|
||||
@override
|
||||
int get hashCode => contractAddress.hashCode;
|
||||
|
||||
@override
|
||||
String get apiString => "evm.$contractAddress";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import "package:cw_core/cake_hive.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/erc20_token.dart" as erc20_new;
|
||||
import "package:cw_core/hive_type_ids.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:hive/hive.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
part "erc20_token_legacy.part.dart";
|
||||
|
||||
Future<void> performErc20TokenHiveMigration() async {
|
||||
try {
|
||||
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
|
||||
CakeHive.registerAdapter(Erc20TokenAdapter());
|
||||
}
|
||||
|
||||
final wallets = await WalletInfo.getAll();
|
||||
await Erc20Token.migrateAllToSqlite(wallets);
|
||||
} catch (e) {
|
||||
printV("Error performing Erc20Token Hive migration: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
// @HiveType(typeId: Erc20Token.typeId)
|
||||
class Erc20Token extends HiveObject {
|
||||
Erc20Token({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.contractAddress,
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
this.tag,
|
||||
this.isPotentialScam = false,
|
||||
}) : _enabled = enabled;
|
||||
// @HiveField(0)
|
||||
final String name;
|
||||
// @HiveField(1)
|
||||
final String symbol;
|
||||
// @HiveField(2)
|
||||
final String contractAddress;
|
||||
// @HiveField(3)
|
||||
final int decimal;
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
// @HiveField(5)
|
||||
String? iconPath;
|
||||
// @HiveField(6)
|
||||
final String? tag;
|
||||
// @HiveField(7, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static const typeId = ERC20_TOKEN_TYPE_ID;
|
||||
static const boxName = "Erc20Tokens";
|
||||
static const ethereumBoxName = "EthereumErc20Tokens";
|
||||
static const polygonBoxName = "PolygonErc20Tokens";
|
||||
static const baseBoxName = "BaseErc20Tokens";
|
||||
static const arbitrumBoxName = "ArbitrumErc20Tokens";
|
||||
static const bscBoxName = "BscErc20Tokens";
|
||||
|
||||
static const chainIdToBoxSuffix = {
|
||||
1: ethereumBoxName,
|
||||
137: polygonBoxName,
|
||||
8453: baseBoxName,
|
||||
42161: arbitrumBoxName,
|
||||
56: bscBoxName,
|
||||
};
|
||||
|
||||
static const evmWalletTypes = [
|
||||
WalletType.ethereum,
|
||||
WalletType.polygon,
|
||||
WalletType.base,
|
||||
WalletType.arbitrum,
|
||||
WalletType.bsc,
|
||||
];
|
||||
|
||||
static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
|
||||
await _migrateLegacyGlobalBox(wallets);
|
||||
|
||||
final sanitizedToRawNames = <String, Set<String>>{};
|
||||
for (final wallet in wallets) {
|
||||
if (!evmWalletTypes.contains(wallet.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitizedToRawNames
|
||||
.putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
|
||||
.add(wallet.name);
|
||||
}
|
||||
|
||||
for (final entry in sanitizedToRawNames.entries) {
|
||||
for (final chainEntry in chainIdToBoxSuffix.entries) {
|
||||
final tokenBoxName = "${entry.key}_${chainEntry.value}";
|
||||
try {
|
||||
if (!await CakeHive.boxExists(tokenBoxName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<Erc20Token>(tokenBoxName);
|
||||
|
||||
for (final group in _mergeByLowercaseContract(box)) {
|
||||
for (final rawName in entry.value) {
|
||||
await group.token.migrateToSqlite(walletName: rawName, chainId: chainEntry.key);
|
||||
}
|
||||
await box.deleteAll(group.sourceKeys);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating erc20 token box $tokenBoxName: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _migrateLegacyGlobalBox(List<WalletInfo> wallets) async {
|
||||
try {
|
||||
if (!await CakeHive.boxExists(boxName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wallets.any((wallet) => wallet.type == WalletType.ethereum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<Erc20Token>(boxName);
|
||||
final ethereumWallets =
|
||||
wallets.where((wallet) => wallet.type == WalletType.ethereum).toList();
|
||||
|
||||
for (final group in _mergeByLowercaseContract(box)) {
|
||||
for (final wallet in ethereumWallets) {
|
||||
await group.token.migrateToSqlite(walletName: wallet.name, chainId: 1);
|
||||
}
|
||||
await box.deleteAll(group.sourceKeys);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating legacy global erc20 token box: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
static List<_MergedTokenGroup> _mergeByLowercaseContract(Box<Erc20Token> box) {
|
||||
final groups = <String, _MergedTokenGroup>{};
|
||||
|
||||
for (final key in box.keys) {
|
||||
final token = box.get(key);
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final lowerKey = token.contractAddress.toLowerCase();
|
||||
final existing = groups[lowerKey];
|
||||
|
||||
if (existing == null) {
|
||||
groups[lowerKey] = _MergedTokenGroup(token: token, sourceKeys: [key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
groups[lowerKey] = _MergedTokenGroup(
|
||||
token: Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
contractAddress: lowerKey,
|
||||
decimal: token.decimal,
|
||||
enabled: token.enabled || existing.token.enabled,
|
||||
iconPath:
|
||||
(token.iconPath?.isNotEmpty ?? false) ? token.iconPath : existing.token.iconPath,
|
||||
tag: token.tag ?? existing.token.tag,
|
||||
isPotentialScam: token.isPotentialScam || existing.token.isPotentialScam,
|
||||
),
|
||||
sourceKeys: [...existing.sourceKeys, key],
|
||||
);
|
||||
}
|
||||
|
||||
return groups.values.toList();
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite({required String walletName, required int chainId}) async {
|
||||
final row = erc20_new.Erc20Token(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal,
|
||||
enabled: _enabled,
|
||||
iconPath: iconPath,
|
||||
tag: tag,
|
||||
isPotentialScam: isPotentialScam,
|
||||
walletName: walletName,
|
||||
chainId: chainId,
|
||||
).toMap();
|
||||
row[erc20_new.Erc20Token.selfIdColumn] = null;
|
||||
|
||||
await db!.insert(
|
||||
erc20_new.Erc20Token.tableName,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MergedTokenGroup {
|
||||
_MergedTokenGroup({required this.token, required this.sourceKeys});
|
||||
|
||||
final Erc20Token token;
|
||||
final List<dynamic> sourceKeys;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of "erc20_token_legacy.dart";
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class Erc20TokenAdapter extends TypeAdapter<Erc20Token> {
|
||||
@override
|
||||
final int typeId = 12;
|
||||
|
||||
@override
|
||||
Erc20Token read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Erc20Token(
|
||||
name: fields[0] as String,
|
||||
symbol: fields[1] as String,
|
||||
contractAddress: fields[2] as String,
|
||||
decimal: fields[3] as int,
|
||||
iconPath: fields[5] as String?,
|
||||
tag: fields[6] as String?,
|
||||
isPotentialScam: fields[7] == null ? false : fields[7] as bool,
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Erc20Token obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.symbol)
|
||||
..writeByte(2)
|
||||
..write(obj.contractAddress)
|
||||
..writeByte(3)
|
||||
..write(obj.decimal)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(6)
|
||||
..write(obj.tag)
|
||||
..writeByte(7)
|
||||
..write(obj.isPotentialScam);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Erc20TokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -26,9 +26,7 @@ class TransactionCommitFailed implements Exception {
|
||||
TransactionCommitFailed({this.errorMessage});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return errorMessage ?? "unknown error";
|
||||
}
|
||||
String toString() => errorMessage ?? "unknown error";
|
||||
}
|
||||
|
||||
class TransactionCommitFailedDustChange implements Exception {}
|
||||
@@ -57,11 +55,35 @@ class SignSPLTokenTransactionRentException implements Exception {}
|
||||
|
||||
class NoAssociatedTokenAccountException implements Exception {}
|
||||
|
||||
/// ==============================================================================
|
||||
/// ==============================================================================
|
||||
class NotAnNFTException implements Exception {}
|
||||
|
||||
class AmbiguousTokenSymbolException implements Exception {
|
||||
AmbiguousTokenSymbolException(this.symbol);
|
||||
|
||||
final String symbol;
|
||||
}
|
||||
|
||||
class RestoreFromSeedException implements Exception {
|
||||
final String message;
|
||||
|
||||
RestoreFromSeedException(this.message);
|
||||
}
|
||||
|
||||
class WalletDeprecationException implements Exception {
|
||||
final String seed;
|
||||
final CryptoCurrency curr;
|
||||
|
||||
@override
|
||||
String toString() => "Wallet type no longer supported";
|
||||
|
||||
WalletDeprecationException({required this.seed, required this.curr});
|
||||
}
|
||||
|
||||
class WalletSwitchException implements Exception {
|
||||
WalletSwitchException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,44 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri
|
||||
decimals ??= 0;
|
||||
fractionalDigits ??= decimals;
|
||||
|
||||
var multiplier = getMultiplier(decimals);
|
||||
final multiplier = getMultiplier(decimals);
|
||||
// Make sure wei is a big number (convert as necessary)
|
||||
var negative = value.isNegative;
|
||||
if (negative) value = value * BigInt.from(-1);
|
||||
final negative = value.isNegative;
|
||||
if (negative) {
|
||||
value = -value;
|
||||
}
|
||||
|
||||
var fraction =
|
||||
value.modPow(BigInt.one, BigInt.parse(multiplier)).toString().padLeft(decimals, "0");
|
||||
|
||||
if (fractionalDigits < 0) fractionalDigits = 0;
|
||||
if (fractionalDigits > decimals) fractionalDigits = decimals;
|
||||
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 whole = value ~/ BigInt.parse(multiplier);
|
||||
|
||||
final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction";
|
||||
|
||||
if (negative) return "-$valString";
|
||||
if (negative) {
|
||||
return "-$valString";
|
||||
}
|
||||
|
||||
return valString;
|
||||
}
|
||||
|
||||
String removeTrailing(String pattern, String from) {
|
||||
if (pattern.isEmpty) return from;
|
||||
if (pattern.isEmpty) {
|
||||
return from;
|
||||
}
|
||||
|
||||
var i = from.length;
|
||||
while (i > 0 && from.startsWith(pattern, i - pattern.length)) {
|
||||
i -= pattern.length;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cw_core/get_height_by_date_xmr.dart';
|
||||
import 'dart:convert';
|
||||
@@ -7,7 +6,7 @@ import 'dart:convert';
|
||||
// FIXME: Hardcoded values; Works only for monero
|
||||
|
||||
int getMoneroHeigthByDate({required DateTime date}) =>
|
||||
MoneroHeight.getBlockHeightByTime(date..subtract(Duration(days: 1)));
|
||||
MoneroHeight.getBlockHeightByTime(date.subtract(const Duration(days: 1)));
|
||||
|
||||
const havenDates = {
|
||||
"2023-05": 1352995,
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
class ZcashHeight {
|
||||
// for i in $(seq 1 331); do echo "_Checkpoint($(date -j -f "%Y-%m-%d %H:%M:%S" "$(curl -s "https://mainnet.zcashexplorer.app/blocks/${i}0000" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')" "+%s"), ${i}0000),"; done > points.txt
|
||||
static final List<_Checkpoint> _points = [
|
||||
_Checkpoint(1479114667, 10000),
|
||||
_Checkpoint(1480618460, 20000),
|
||||
_Checkpoint(1482129143, 30000),
|
||||
_Checkpoint(1483635610, 40000),
|
||||
_Checkpoint(1485145514, 50000),
|
||||
_Checkpoint(1486655742, 60000),
|
||||
_Checkpoint(1488163642, 70000),
|
||||
_Checkpoint(1489668244, 80000),
|
||||
_Checkpoint(1491175107, 90000),
|
||||
_Checkpoint(1492681094, 100000),
|
||||
_Checkpoint(1494185626, 110000),
|
||||
_Checkpoint(1495691260, 120000),
|
||||
_Checkpoint(1497196465, 130000),
|
||||
_Checkpoint(1498702428, 140000),
|
||||
_Checkpoint(1500209116, 150000),
|
||||
_Checkpoint(1501716823, 160000),
|
||||
_Checkpoint(1503222893, 170000),
|
||||
_Checkpoint(1504730866, 180000),
|
||||
_Checkpoint(1506236963, 190000),
|
||||
_Checkpoint(1507744233, 200000),
|
||||
_Checkpoint(1509254578, 210000),
|
||||
_Checkpoint(1510768449, 220000),
|
||||
_Checkpoint(1512275502, 230000),
|
||||
_Checkpoint(1513783951, 240000),
|
||||
_Checkpoint(1515292290, 250000),
|
||||
_Checkpoint(1516799219, 260000),
|
||||
_Checkpoint(1518306180, 270000),
|
||||
_Checkpoint(1519813817, 280000),
|
||||
_Checkpoint(1521319150, 290000),
|
||||
_Checkpoint(1522828107, 300000),
|
||||
_Checkpoint(1524336223, 310000),
|
||||
_Checkpoint(1525844954, 320000),
|
||||
_Checkpoint(1527354342, 330000),
|
||||
_Checkpoint(1528860301, 340000),
|
||||
_Checkpoint(1530365206, 350000),
|
||||
_Checkpoint(1531873693, 360000),
|
||||
_Checkpoint(1533380392, 370000),
|
||||
_Checkpoint(1534888873, 380000),
|
||||
_Checkpoint(1536393381, 390000),
|
||||
_Checkpoint(1537899117, 400000),
|
||||
_Checkpoint(1539406330, 410000),
|
||||
_Checkpoint(1540912214, 420000),
|
||||
_Checkpoint(1542422891, 430000),
|
||||
_Checkpoint(1543931111, 440000),
|
||||
_Checkpoint(1545437758, 450000),
|
||||
_Checkpoint(1546945529, 460000),
|
||||
_Checkpoint(1548450107, 470000),
|
||||
_Checkpoint(1549957767, 480000),
|
||||
_Checkpoint(1551466960, 490000),
|
||||
_Checkpoint(1552969806, 500000),
|
||||
_Checkpoint(1554477648, 510000),
|
||||
_Checkpoint(1555981451, 520000),
|
||||
_Checkpoint(1557489241, 530000),
|
||||
_Checkpoint(1558997282, 540000),
|
||||
_Checkpoint(1560503840, 550000),
|
||||
_Checkpoint(1562011717, 560000),
|
||||
_Checkpoint(1563518091, 570000),
|
||||
_Checkpoint(1565027136, 580000),
|
||||
_Checkpoint(1566532760, 590000),
|
||||
_Checkpoint(1568039395, 600000),
|
||||
_Checkpoint(1569547911, 610000),
|
||||
_Checkpoint(1571051853, 620000),
|
||||
_Checkpoint(1572559831, 630000),
|
||||
_Checkpoint(1574069743, 640000),
|
||||
_Checkpoint(1575576895, 650000),
|
||||
_Checkpoint(1576603014, 660000),
|
||||
_Checkpoint(1577356346, 670000),
|
||||
_Checkpoint(1578109375, 680000),
|
||||
_Checkpoint(1578863180, 690000),
|
||||
_Checkpoint(1579616443, 700000),
|
||||
_Checkpoint(1580369806, 710000),
|
||||
_Checkpoint(1581122786, 720000),
|
||||
_Checkpoint(1581876634, 730000),
|
||||
_Checkpoint(1582630471, 740000),
|
||||
_Checkpoint(1583383678, 750000),
|
||||
_Checkpoint(1584133944, 760000),
|
||||
_Checkpoint(1584887940, 770000),
|
||||
_Checkpoint(1585641057, 780000),
|
||||
_Checkpoint(1586395543, 790000),
|
||||
_Checkpoint(1587148500, 800000),
|
||||
_Checkpoint(1587901493, 810000),
|
||||
_Checkpoint(1588655183, 820000),
|
||||
_Checkpoint(1589408476, 830000),
|
||||
_Checkpoint(1590163464, 840000),
|
||||
_Checkpoint(1590916379, 850000),
|
||||
_Checkpoint(1591669786, 860000),
|
||||
_Checkpoint(1592423887, 870000),
|
||||
_Checkpoint(1593176771, 880000),
|
||||
_Checkpoint(1593930930, 890000),
|
||||
_Checkpoint(1594684380, 900000),
|
||||
_Checkpoint(1595439209, 910000),
|
||||
_Checkpoint(1596192463, 920000),
|
||||
_Checkpoint(1596946009, 930000),
|
||||
_Checkpoint(1597700256, 940000),
|
||||
_Checkpoint(1598453538, 950000),
|
||||
_Checkpoint(1599207483, 960000),
|
||||
_Checkpoint(1599960598, 970000),
|
||||
_Checkpoint(1600714563, 980000),
|
||||
_Checkpoint(1601467884, 990000),
|
||||
_Checkpoint(1602220941, 1000000),
|
||||
_Checkpoint(1602974722, 1010000),
|
||||
_Checkpoint(1603728848, 1020000),
|
||||
_Checkpoint(1604485637, 1030000),
|
||||
_Checkpoint(1605238381, 1040000),
|
||||
_Checkpoint(1605992807, 1050000),
|
||||
_Checkpoint(1606747750, 1060000),
|
||||
_Checkpoint(1607500606, 1070000),
|
||||
_Checkpoint(1608255278, 1080000),
|
||||
_Checkpoint(1609008684, 1090000),
|
||||
_Checkpoint(1609763782, 1100000),
|
||||
_Checkpoint(1610517871, 1110000),
|
||||
_Checkpoint(1611271575, 1120000),
|
||||
_Checkpoint(1612025546, 1130000),
|
||||
_Checkpoint(1612779940, 1140000),
|
||||
_Checkpoint(1613533993, 1150000),
|
||||
_Checkpoint(1614287669, 1160000),
|
||||
_Checkpoint(1615042297, 1170000),
|
||||
_Checkpoint(1615792993, 1180000),
|
||||
_Checkpoint(1616546145, 1190000),
|
||||
_Checkpoint(1617299867, 1200000),
|
||||
_Checkpoint(1618054038, 1210000),
|
||||
_Checkpoint(1618808513, 1220000),
|
||||
_Checkpoint(1619563243, 1230000),
|
||||
_Checkpoint(1620318159, 1240000),
|
||||
_Checkpoint(1621071298, 1250000),
|
||||
_Checkpoint(1621826156, 1260000),
|
||||
_Checkpoint(1622580237, 1270000),
|
||||
_Checkpoint(1623335553, 1280000),
|
||||
_Checkpoint(1624090141, 1290000),
|
||||
_Checkpoint(1624844712, 1300000),
|
||||
_Checkpoint(1625597720, 1310000),
|
||||
_Checkpoint(1626351652, 1320000),
|
||||
_Checkpoint(1627106054, 1330000),
|
||||
_Checkpoint(1627860648, 1340000),
|
||||
_Checkpoint(1628614015, 1350000),
|
||||
_Checkpoint(1629368398, 1360000),
|
||||
_Checkpoint(1630122679, 1370000),
|
||||
_Checkpoint(1630877190, 1380000),
|
||||
_Checkpoint(1631631884, 1390000),
|
||||
_Checkpoint(1632385851, 1400000),
|
||||
_Checkpoint(1633141006, 1410000),
|
||||
_Checkpoint(1633895592, 1420000),
|
||||
_Checkpoint(1634649550, 1430000),
|
||||
_Checkpoint(1635403121, 1440000),
|
||||
_Checkpoint(1636157669, 1450000),
|
||||
_Checkpoint(1636915133, 1460000),
|
||||
_Checkpoint(1637670036, 1470000),
|
||||
_Checkpoint(1638424121, 1480000),
|
||||
_Checkpoint(1639177413, 1490000),
|
||||
_Checkpoint(1639931234, 1500000),
|
||||
_Checkpoint(1640684476, 1510000),
|
||||
_Checkpoint(1641438827, 1520000),
|
||||
_Checkpoint(1642192402, 1530000),
|
||||
_Checkpoint(1642946383, 1540000),
|
||||
_Checkpoint(1643700901, 1550000),
|
||||
_Checkpoint(1644455522, 1560000),
|
||||
_Checkpoint(1645208504, 1570000),
|
||||
_Checkpoint(1645962081, 1580000),
|
||||
_Checkpoint(1646715150, 1590000),
|
||||
_Checkpoint(1647465727, 1600000),
|
||||
_Checkpoint(1648219045, 1610000),
|
||||
_Checkpoint(1648973131, 1620000),
|
||||
_Checkpoint(1649727422, 1630000),
|
||||
_Checkpoint(1650481408, 1640000),
|
||||
_Checkpoint(1651234287, 1650000),
|
||||
_Checkpoint(1651989865, 1660000),
|
||||
_Checkpoint(1652743327, 1670000),
|
||||
_Checkpoint(1653497455, 1680000),
|
||||
_Checkpoint(1654252094, 1690000),
|
||||
_Checkpoint(1655005944, 1700000),
|
||||
_Checkpoint(1655759708, 1710000),
|
||||
_Checkpoint(1656513765, 1720000),
|
||||
_Checkpoint(1657268358, 1730000),
|
||||
_Checkpoint(1658022453, 1740000),
|
||||
_Checkpoint(1658777019, 1750000),
|
||||
_Checkpoint(1659531133, 1760000),
|
||||
_Checkpoint(1660283907, 1770000),
|
||||
_Checkpoint(1661037645, 1780000),
|
||||
_Checkpoint(1661791977, 1790000),
|
||||
_Checkpoint(1662544394, 1800000),
|
||||
_Checkpoint(1663298018, 1810000),
|
||||
_Checkpoint(1664051450, 1820000),
|
||||
_Checkpoint(1664805547, 1830000),
|
||||
_Checkpoint(1665558994, 1840000),
|
||||
_Checkpoint(1666312456, 1850000),
|
||||
_Checkpoint(1667066473, 1860000),
|
||||
_Checkpoint(1667823393, 1870000),
|
||||
_Checkpoint(1668576623, 1880000),
|
||||
_Checkpoint(1669330657, 1890000),
|
||||
_Checkpoint(1670084156, 1900000),
|
||||
_Checkpoint(1670837831, 1910000),
|
||||
_Checkpoint(1671591094, 1920000),
|
||||
_Checkpoint(1672343975, 1930000),
|
||||
_Checkpoint(1673097703, 1940000),
|
||||
_Checkpoint(1673852016, 1950000),
|
||||
_Checkpoint(1674605810, 1960000),
|
||||
_Checkpoint(1675360050, 1970000),
|
||||
_Checkpoint(1676113710, 1980000),
|
||||
_Checkpoint(1676867174, 1990000),
|
||||
_Checkpoint(1677620242, 2000000),
|
||||
_Checkpoint(1678374852, 2010000),
|
||||
_Checkpoint(1679125063, 2020000),
|
||||
_Checkpoint(1679878121, 2030000),
|
||||
_Checkpoint(1680632254, 2040000),
|
||||
_Checkpoint(1681386518, 2050000),
|
||||
_Checkpoint(1682140381, 2060000),
|
||||
_Checkpoint(1682894564, 2070000),
|
||||
_Checkpoint(1683648178, 2080000),
|
||||
_Checkpoint(1684401932, 2090000),
|
||||
_Checkpoint(1685156464, 2100000),
|
||||
_Checkpoint(1685910040, 2110000),
|
||||
_Checkpoint(1686663406, 2120000),
|
||||
_Checkpoint(1687417875, 2130000),
|
||||
_Checkpoint(1688172069, 2140000),
|
||||
_Checkpoint(1688925610, 2150000),
|
||||
_Checkpoint(1689679500, 2160000),
|
||||
_Checkpoint(1690432691, 2170000),
|
||||
_Checkpoint(1691186349, 2180000),
|
||||
_Checkpoint(1691940901, 2190000),
|
||||
_Checkpoint(1692694780, 2200000),
|
||||
_Checkpoint(1693449743, 2210000),
|
||||
_Checkpoint(1694203665, 2220000),
|
||||
_Checkpoint(1694957643, 2230000),
|
||||
_Checkpoint(1695710712, 2240000),
|
||||
_Checkpoint(1696465787, 2250000),
|
||||
_Checkpoint(1697218701, 2260000),
|
||||
_Checkpoint(1697973031, 2270000),
|
||||
_Checkpoint(1698726864, 2280000),
|
||||
_Checkpoint(1699483348, 2290000),
|
||||
_Checkpoint(1700237650, 2300000),
|
||||
_Checkpoint(1700991210, 2310000),
|
||||
_Checkpoint(1701745742, 2320000),
|
||||
_Checkpoint(1702500723, 2330000),
|
||||
_Checkpoint(1703253121, 2340000),
|
||||
_Checkpoint(1704007238, 2350000),
|
||||
_Checkpoint(1704760935, 2360000),
|
||||
_Checkpoint(1705514669, 2370000),
|
||||
_Checkpoint(1706268087, 2380000),
|
||||
_Checkpoint(1707021423, 2390000),
|
||||
_Checkpoint(1707776257, 2400000),
|
||||
_Checkpoint(1708529542, 2410000),
|
||||
_Checkpoint(1709283065, 2420000),
|
||||
_Checkpoint(1710037165, 2430000),
|
||||
_Checkpoint(1710787780, 2440000),
|
||||
_Checkpoint(1711541488, 2450000),
|
||||
_Checkpoint(1712294996, 2460000),
|
||||
_Checkpoint(1713048843, 2470000),
|
||||
_Checkpoint(1713803486, 2480000),
|
||||
_Checkpoint(1714557127, 2490000),
|
||||
_Checkpoint(1715311181, 2500000),
|
||||
_Checkpoint(1716064271, 2510000),
|
||||
_Checkpoint(1716818792, 2520000),
|
||||
_Checkpoint(1717572765, 2530000),
|
||||
_Checkpoint(1718326333, 2540000),
|
||||
_Checkpoint(1719080275, 2550000),
|
||||
_Checkpoint(1719834297, 2560000),
|
||||
_Checkpoint(1720588042, 2570000),
|
||||
_Checkpoint(1721341832, 2580000),
|
||||
_Checkpoint(1722095122, 2590000),
|
||||
_Checkpoint(1722848604, 2600000),
|
||||
_Checkpoint(1723602357, 2610000),
|
||||
_Checkpoint(1724355843, 2620000),
|
||||
_Checkpoint(1725110085, 2630000),
|
||||
_Checkpoint(1725863681, 2640000),
|
||||
_Checkpoint(1726616892, 2650000),
|
||||
_Checkpoint(1727371443, 2660000),
|
||||
_Checkpoint(1728124893, 2670000),
|
||||
_Checkpoint(1728878141, 2680000),
|
||||
_Checkpoint(1729631572, 2690000),
|
||||
_Checkpoint(1730385888, 2700000),
|
||||
_Checkpoint(1731142437, 2710000),
|
||||
_Checkpoint(1731896266, 2720000),
|
||||
_Checkpoint(1732650193, 2730000),
|
||||
_Checkpoint(1733403626, 2740000),
|
||||
_Checkpoint(1734157390, 2750000),
|
||||
_Checkpoint(1734911442, 2760000),
|
||||
_Checkpoint(1735665720, 2770000),
|
||||
_Checkpoint(1736419845, 2780000),
|
||||
_Checkpoint(1737173511, 2790000),
|
||||
_Checkpoint(1737926795, 2800000),
|
||||
_Checkpoint(1738681511, 2810000),
|
||||
_Checkpoint(1739434236, 2820000),
|
||||
_Checkpoint(1740188214, 2830000),
|
||||
_Checkpoint(1740941352, 2840000),
|
||||
_Checkpoint(1741692338, 2850000),
|
||||
_Checkpoint(1742445247, 2860000),
|
||||
_Checkpoint(1743198927, 2870000),
|
||||
_Checkpoint(1743952947, 2880000),
|
||||
_Checkpoint(1744706370, 2890000),
|
||||
_Checkpoint(1745461377, 2900000),
|
||||
_Checkpoint(1746214735, 2910000),
|
||||
_Checkpoint(1746967908, 2920000),
|
||||
_Checkpoint(1747722271, 2930000),
|
||||
_Checkpoint(1748474582, 2940000),
|
||||
_Checkpoint(1749228582, 2950000),
|
||||
_Checkpoint(1749983372, 2960000),
|
||||
_Checkpoint(1750737278, 2970000),
|
||||
_Checkpoint(1751490762, 2980000),
|
||||
_Checkpoint(1752243931, 2990000),
|
||||
_Checkpoint(1752997873, 3000000),
|
||||
_Checkpoint(1753751756, 3010000),
|
||||
_Checkpoint(1754504935, 3020000),
|
||||
_Checkpoint(1755259449, 3030000),
|
||||
_Checkpoint(1756012904, 3040000),
|
||||
_Checkpoint(1756765335, 3050000),
|
||||
_Checkpoint(1757519206, 3060000),
|
||||
_Checkpoint(1758272484, 3070000),
|
||||
_Checkpoint(1759025597, 3080000),
|
||||
_Checkpoint(1759778957, 3090000),
|
||||
_Checkpoint(1760532780, 3100000),
|
||||
_Checkpoint(1761286210, 3110000),
|
||||
_Checkpoint(1762039783, 3120000),
|
||||
_Checkpoint(1762796627, 3130000),
|
||||
_Checkpoint(1763550532, 3140000),
|
||||
_Checkpoint(1764303860, 3150000),
|
||||
_Checkpoint(1765057381, 3160000),
|
||||
_Checkpoint(1765811222, 3170000),
|
||||
_Checkpoint(1766564356, 3180000),
|
||||
_Checkpoint(1767318326, 3190000),
|
||||
_Checkpoint(1768071956, 3200000),
|
||||
_Checkpoint(1768824957, 3210000),
|
||||
_Checkpoint(1769578781, 3220000),
|
||||
_Checkpoint(1770332206, 3230000),
|
||||
_Checkpoint(1771086265, 3240000),
|
||||
_Checkpoint(1771840180, 3250000),
|
||||
_Checkpoint(1772592783, 3260000),
|
||||
_Checkpoint(1773343947, 3270000),
|
||||
_Checkpoint(1774097085, 3280000),
|
||||
_Checkpoint(1774850161, 3290000),
|
||||
_Checkpoint(1775603525, 3300000),
|
||||
_Checkpoint(1776358117, 3310000),
|
||||
];
|
||||
|
||||
static int getBlockHeightByTime(DateTime time) {
|
||||
final t = time.toUtc().millisecondsSinceEpoch;
|
||||
|
||||
if (_points.isEmpty) return 0;
|
||||
|
||||
if (t <= _points.first.timeMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _points.length - 1; i++) {
|
||||
final p0 = _points[i];
|
||||
final p1 = _points[i + 1];
|
||||
|
||||
if (t >= p0.timeMs && t < p1.timeMs) {
|
||||
return _interpolate(
|
||||
p0.height,
|
||||
p1.height,
|
||||
p0.timeMs,
|
||||
p1.timeMs,
|
||||
t,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final last = _points[_points.length - 1];
|
||||
final prev = _points[_points.length - 2];
|
||||
|
||||
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
|
||||
|
||||
final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor();
|
||||
|
||||
return last.height + blocksSinceLast;
|
||||
}
|
||||
|
||||
static DateTime getTimeByBlockHeight(int height) {
|
||||
if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
if (height <= 1) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _points.length - 1; i++) {
|
||||
final p0 = _points[i];
|
||||
final p1 = _points[i + 1];
|
||||
if (height >= p0.height && height < p1.height) {
|
||||
final t = _interpolate(
|
||||
p0.timeMs,
|
||||
p1.timeMs,
|
||||
p0.height,
|
||||
p1.height,
|
||||
height,
|
||||
);
|
||||
return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true);
|
||||
}
|
||||
}
|
||||
|
||||
final last = _points[_points.length - 1];
|
||||
final prev = _points[_points.length - 2];
|
||||
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
|
||||
final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor();
|
||||
return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true);
|
||||
}
|
||||
|
||||
static int _interpolate(
|
||||
int h0,
|
||||
int h1,
|
||||
int t0,
|
||||
int t1,
|
||||
int t,
|
||||
) {
|
||||
final ratio = (t - t0) / (t1 - t0);
|
||||
return h0 + ((h1 - h0) * ratio).floor();
|
||||
}
|
||||
}
|
||||
|
||||
class _Checkpoint {
|
||||
final int timeMs;
|
||||
final int height;
|
||||
|
||||
_Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ enum DeviceConnectionType {
|
||||
break;
|
||||
case HardwareWalletType.trezor:
|
||||
isSupported = [
|
||||
// WalletType.monero,
|
||||
WalletType.monero,
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.ethereum,
|
||||
|
||||
@@ -4,7 +4,8 @@ import 'dart:typed_data';
|
||||
import 'package:cw_core/hardware/hardware_account_data.dart';
|
||||
|
||||
abstract class HardwareWalletService {
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5});
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
Future<Uint8List> signTransaction({required String transaction}) => throw UnimplementedError();
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class ImportedNFT {
|
||||
ImportedNFT({
|
||||
required this.walletName,
|
||||
required this.chain,
|
||||
required this.identifier,
|
||||
this.name,
|
||||
this.symbol,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
this.isOwned,
|
||||
this.id = 0,
|
||||
});
|
||||
|
||||
ImportedNFT.copyWith(ImportedNFT other, {String? walletName})
|
||||
: id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
chain = other.chain,
|
||||
identifier = other.identifier,
|
||||
name = other.name,
|
||||
symbol = other.symbol,
|
||||
description = other.description,
|
||||
imageUrl = other.imageUrl,
|
||||
isOwned = other.isOwned;
|
||||
|
||||
factory ImportedNFT.fromMap(Map<String, Object?> map) => ImportedNFT(
|
||||
id: map[selfIdColumn] as int? ?? 0,
|
||||
walletName: map["walletName"]! as String,
|
||||
chain: map["chain"]! as String,
|
||||
identifier: map["identifier"]! as String,
|
||||
name: map["name"] as String?,
|
||||
symbol: map["symbol"] as String?,
|
||||
description: map["description"] as String?,
|
||||
imageUrl: map["imageUrl"] as String?,
|
||||
isOwned: map["isOwned"] == null ? null : map["isOwned"] == 1,
|
||||
);
|
||||
|
||||
int id;
|
||||
final String walletName;
|
||||
final String chain;
|
||||
final String identifier;
|
||||
final String? name;
|
||||
final String? symbol;
|
||||
final String? description;
|
||||
final String? imageUrl;
|
||||
final bool? isOwned;
|
||||
|
||||
static String get tableName => "ImportedNFT";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
static const solanaChain = "solana";
|
||||
|
||||
Map<String, Object?> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"chain": chain,
|
||||
"identifier": identifier,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"description": description,
|
||||
"imageUrl": imageUrl,
|
||||
"isOwned": isOwned == null ? null : (isOwned! ? 1 : 0),
|
||||
};
|
||||
|
||||
Future<int> save() async {
|
||||
final row = toMap();
|
||||
|
||||
if (row[selfIdColumn] == 0) {
|
||||
row[selfIdColumn] = null;
|
||||
}
|
||||
|
||||
id = await db!.insert(tableName, row, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<int> updateMetadata(ImportedNFT nft) => db!.update(
|
||||
tableName,
|
||||
nft.toMap()..remove(selfIdColumn),
|
||||
where: "walletName = ? AND chain = ? AND identifier = ?",
|
||||
whereArgs: [nft.walletName, nft.chain, nft.identifier],
|
||||
);
|
||||
|
||||
static Future<List<ImportedNFT>> getAllForWallet(String walletName, [String? chain]) async {
|
||||
final rows = await db!.query(
|
||||
tableName,
|
||||
where: chain == null ? "walletName = ?" : "walletName = ? AND chain = ?",
|
||||
whereArgs: chain == null ? [walletName] : [walletName, chain],
|
||||
orderBy: selfIdColumn,
|
||||
);
|
||||
|
||||
return rows.map(ImportedNFT.fromMap).toList();
|
||||
}
|
||||
|
||||
static Future<int> deleteOne(String walletName, String chain, String identifier) => db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND chain = ? AND identifier = ?",
|
||||
whereArgs: [walletName, chain, identifier],
|
||||
);
|
||||
|
||||
static String _scope(List<String>? chains) =>
|
||||
chains == null ? "" : " AND chain IN (${List.filled(chains.length, "?").join(", ")})";
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName, {List<String>? chains}) async {
|
||||
if (chains != null && chains.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ?${_scope(chains)}",
|
||||
whereArgs: [walletName, ...?chains],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> renameWallet(
|
||||
String oldName,
|
||||
String newName, {
|
||||
List<String>? chains,
|
||||
}) async {
|
||||
if (chains != null && chains.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ?${_scope(chains)}",
|
||||
whereArgs: [newName, ...?chains],
|
||||
);
|
||||
await db!.update(
|
||||
tableName,
|
||||
{"walletName": newName},
|
||||
where: "walletName = ?${_scope(chains)}",
|
||||
whereArgs: [oldName, ...?chains],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,8 @@
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
const ivEncodedStringLength = 12;
|
||||
|
||||
String generateKey() {
|
||||
final key = encrypt.Key.fromSecureRandom(512);
|
||||
final iv = encrypt.IV.fromSecureRandom(8);
|
||||
|
||||
return key.base64 + iv.base64;
|
||||
}
|
||||
|
||||
List<String> extractKeys(String key) {
|
||||
final _key = key.substring(0, key.length - ivEncodedStringLength);
|
||||
final iv = key.substring(key.length - ivEncodedStringLength);
|
||||
|
||||
return [_key, iv];
|
||||
}
|
||||
|
||||
Future<String> encode(
|
||||
{required encrypt.Key key, required encrypt.IV iv, required String data}) async {
|
||||
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
|
||||
final encrypted = encrypter.encrypt(data, iv: iv);
|
||||
|
||||
return encrypted.base64;
|
||||
}
|
||||
|
||||
Future<String> decode({required String password, required String data}) async {
|
||||
final keys = extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
|
||||
final encrypted = encrypter.decrypt64(data, iv: iv);
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
+150
-41
@@ -1,96 +1,180 @@
|
||||
import 'dart:convert';
|
||||
import "dart:convert";
|
||||
|
||||
import 'package:bech32/bech32.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import "package:bech32/bech32.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/utils/proxy_wrapper.dart";
|
||||
|
||||
const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"];
|
||||
const _LUD17_PREFIXES = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
|
||||
const _LUD17_PREFIXES = ["lnurlw", "lnurlc", "lnurlp", "keyauth"];
|
||||
|
||||
bool isBolt11ZeroInvoice(String invoice) {
|
||||
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
try {
|
||||
final request =
|
||||
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
|
||||
final prefix =
|
||||
_BOLT_PREFIXES.firstWhere((prefix) => request.hrp.startsWith(prefix), orElse: () => "");
|
||||
final prefix =
|
||||
_BOLT_PREFIXES.firstWhere(request.hrp.startsWith, orElse: () => "");
|
||||
|
||||
return request.hrp.length == prefix.length;
|
||||
return request.hrp.length == prefix.length;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the amount of a Bolt 11 Invoice in
|
||||
int? _getAmountBolt11Msat(String invoice) {
|
||||
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
final request =
|
||||
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
|
||||
final prefix = _BOLT_PREFIXES.firstWhere(
|
||||
(prefix) => request.hrp.startsWith(prefix),
|
||||
orElse: () => throw FormatException('Invalid BOLT11 invoice: unknown HRP prefix.'),
|
||||
request.hrp.startsWith,
|
||||
orElse: () => throw const FormatException("Invalid BOLT11 invoice: unknown HRP prefix."),
|
||||
);
|
||||
|
||||
final amountPart = request.hrp.substring(prefix.length);
|
||||
if (amountPart.isEmpty) return null; // zero-amount invoice
|
||||
if (amountPart.isEmpty) {
|
||||
return null; // zero-amount invoice
|
||||
}
|
||||
|
||||
final hasMultiplier = RegExp(r'[munp]$').hasMatch(amountPart);
|
||||
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : '';
|
||||
final hasMultiplier = RegExp(r"[munp]$").hasMatch(amountPart);
|
||||
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : "";
|
||||
final numberStr = hasMultiplier ? amountPart.substring(0, amountPart.length - 1) : amountPart;
|
||||
|
||||
if (numberStr.isEmpty || !RegExp(r'^\d+$').hasMatch(numberStr)) {
|
||||
throw FormatException('Invalid BOLT11 invoice: invalid amount number in HRP.');
|
||||
if (numberStr.isEmpty || !RegExp(r"^\d+$").hasMatch(numberStr)) {
|
||||
throw const FormatException("Invalid BOLT11 invoice: invalid amount number in HRP.");
|
||||
}
|
||||
|
||||
final amount = int.parse(numberStr);
|
||||
|
||||
switch (multiplier) {
|
||||
case '': // bitcoin
|
||||
case "": // bitcoin
|
||||
return amount * 100000000000;
|
||||
case 'm': // milli-bitcoin
|
||||
case "m": // milli-bitcoin
|
||||
return amount * 100000000;
|
||||
case 'u': // micro-bitcoin
|
||||
case "u": // micro-bitcoin
|
||||
return amount * 100000;
|
||||
case 'n': // nano-bitcoin
|
||||
case "n": // nano-bitcoin
|
||||
return amount * 100;
|
||||
case 'p':
|
||||
case "p":
|
||||
if (amount % 10 != 0) {
|
||||
throw FormatException('Invalid BOLT11 invoice: amount not representable in whole msat.');
|
||||
throw const FormatException(
|
||||
"Invalid BOLT11 invoice: amount not representable in whole msat.",
|
||||
);
|
||||
}
|
||||
return amount ~/ 10;
|
||||
default:
|
||||
throw FormatException('Invalid BOLT11 invoice: unknown amount multiplier.');
|
||||
throw const FormatException("Invalid BOLT11 invoice: unknown amount multiplier.");
|
||||
}
|
||||
}
|
||||
|
||||
int? getBolt11Amount(String invoice) {
|
||||
Money? getBolt11Amount(String invoice) {
|
||||
final msat = _getAmountBolt11Msat(invoice);
|
||||
if (msat == null || msat % 1000 != 0) return null;
|
||||
return msat ~/ 1000;
|
||||
if (msat == null || msat % 1000 != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
|
||||
}
|
||||
|
||||
class LNURL {
|
||||
static Future<int?> getPayRequestAmount(String lnurl) async {
|
||||
static Future<Money?> getPayRequestAmount(String lnurl) async {
|
||||
final url = decode(lnurl);
|
||||
final response = await ProxyWrapper().get(clearnetUri: url);
|
||||
|
||||
if (response.statusCode != 200) return null;
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final body = jsonDecode(response.body) as Map;
|
||||
final tag = body["tag"] as String?;
|
||||
|
||||
if (tag != "payRequest") return null;
|
||||
if (tag != "payRequest") {
|
||||
return null;
|
||||
}
|
||||
|
||||
final msat = body["minSendable"] as int?;
|
||||
final maxSendable = body["maxSendable"] as int?;
|
||||
|
||||
// if minSendable and maxSendable are the same we assume a specific payment request
|
||||
if (msat != maxSendable) return null;
|
||||
if (msat != maxSendable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (msat == null || msat % 1000 != 0) return null;
|
||||
return msat ~/ 1000;
|
||||
if (msat == null || msat % 1000 != 0) {
|
||||
return null;
|
||||
}
|
||||
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LNURLWithdrawRequest?> getWithdrawRequest(String lnurl) async {
|
||||
final url = decode(lnurl);
|
||||
final response = await ProxyWrapper().get(clearnetUri: url);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final body = jsonDecode(response.body) as Map;
|
||||
final tag = body["tag"] as String?;
|
||||
|
||||
if (tag != "withdrawRequest") {
|
||||
return null;
|
||||
}
|
||||
|
||||
final minWithdrawable = body["minWithdrawable"] as int?;
|
||||
final maxWithdrawable = body["maxWithdrawable"] as int?;
|
||||
|
||||
if ((minWithdrawable == null || minWithdrawable % 1000 != 0) ||
|
||||
(maxWithdrawable == null || maxWithdrawable % 1000 != 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return LNURLWithdrawRequest(
|
||||
callback: body["callback"] as String,
|
||||
k1: body["k1"] as String,
|
||||
description: body["defaultDescription"] as String,
|
||||
minAmount: Money.fromInt(minWithdrawable ~/ 1000, CryptoCurrency.btcln),
|
||||
maxAmount: Money.fromInt(maxWithdrawable ~/ 1000, CryptoCurrency.btcln),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> commitWithdrawRequest(LNURLWithdrawRequest request, String invoice) async {
|
||||
try {
|
||||
final url = Uri.parse(request.callback);
|
||||
url.queryParameters["k1"] = request.k1;
|
||||
url.queryParameters["pr"] = invoice;
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: url);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final body = jsonDecode(response.body) as Map;
|
||||
final status = body["status"] as String?;
|
||||
|
||||
if (status == "OK") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static String encode(String url) {
|
||||
final raw = _convert(utf8.encode(url), 8, 5, true);
|
||||
return const Bech32Codec().encode(Bech32('lnurl', raw), 999);
|
||||
return const Bech32Codec().encode(Bech32("lnurl", raw), 999);
|
||||
}
|
||||
|
||||
static Uri decode(String encodedUrl) {
|
||||
@@ -108,9 +192,10 @@ class LNURL {
|
||||
if (_LUD17_PREFIXES.contains(decodedUri.scheme)) {
|
||||
/// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.'
|
||||
decodedUri = decodedUri.replace(
|
||||
scheme: decodedUri.host.endsWith('onion') || decodedUri.host.endsWith('onion.')
|
||||
? 'http'
|
||||
: 'https');
|
||||
scheme: decodedUri.host.endsWith("onion") || decodedUri.host.endsWith("onion.")
|
||||
? "http"
|
||||
: "https",
|
||||
);
|
||||
} else {
|
||||
/// Try to parse the input as a lnUrl. Will throw an error if it fails.
|
||||
final lnUrl = _findLnUrl(encodedUrl);
|
||||
@@ -123,12 +208,30 @@ class LNURL {
|
||||
}
|
||||
}
|
||||
|
||||
class LNURLWithdrawRequest {
|
||||
const LNURLWithdrawRequest({
|
||||
required this.callback,
|
||||
required this.k1,
|
||||
required this.description,
|
||||
required this.minAmount,
|
||||
required this.maxAmount,
|
||||
});
|
||||
|
||||
final String callback;
|
||||
final String k1;
|
||||
final String description;
|
||||
final Money minAmount;
|
||||
final Money maxAmount;
|
||||
}
|
||||
|
||||
/// Parse and return a given lnurl string if it's valid. Will remove
|
||||
/// `lightning:` from the beginning of it if present.
|
||||
String _findLnUrl(String input) {
|
||||
final res = RegExp(r',*?((lnurl)([0-9]+[a-z0-9]+))').allMatches(input.toLowerCase());
|
||||
final res = RegExp(r",*?((lnurl)([0-9]+[a-z0-9]+))").allMatches(input.toLowerCase());
|
||||
|
||||
if (res.length != 1) throw ArgumentError('Not a valid lnurl string');
|
||||
if (res.length != 1) {
|
||||
throw ArgumentError("Not a valid lnurl string");
|
||||
}
|
||||
return res.first.group(0)!;
|
||||
}
|
||||
|
||||
@@ -150,10 +253,16 @@ List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits > 0) result.add((value << (outBits - bits)) & maxV);
|
||||
if (bits > 0) {
|
||||
result.add((value << (outBits - bits)) & maxV);
|
||||
}
|
||||
} else {
|
||||
if (bits >= inBits) throw Exception('[BECH32] Excess padding');
|
||||
if ((value << (outBits - bits)) & maxV > 0) throw Exception('[BECH32] Non-zero padding');
|
||||
if (bits >= inBits) {
|
||||
throw Exception("[BECH32] Excess padding");
|
||||
}
|
||||
if ((value << (outBits - bits)) & maxV > 0) {
|
||||
throw Exception("[BECH32] Non-zero padding");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/monero_amount_format.dart';
|
||||
|
||||
class MoneroBalance extends Balance {
|
||||
MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0})
|
||||
: formattedFrozenBalance = moneroAmountToString(amount: frozenBalance),
|
||||
super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance);
|
||||
|
||||
final int fullBalance;
|
||||
final int unlockedBalance;
|
||||
final int frozenBalance;
|
||||
final String formattedFrozenBalance;
|
||||
|
||||
@override
|
||||
String get formattedUnAvailableBalance =>
|
||||
formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'mweb_utxo.g.dart';
|
||||
part 'mweb_utxo.part.dart';
|
||||
|
||||
@HiveType(typeId: MWEB_UTXO_TYPE_ID)
|
||||
// @HiveType(typeId: MWEB_UTXO_TYPE_ID)
|
||||
class MwebUtxo extends HiveObject {
|
||||
MwebUtxo({
|
||||
required this.height,
|
||||
@@ -17,21 +17,21 @@ class MwebUtxo extends HiveObject {
|
||||
static const typeId = MWEB_UTXO_TYPE_ID;
|
||||
static const boxName = 'MwebUtxo';
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
int height;
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
int value;
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
String address;
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
String outputId;
|
||||
|
||||
@HiveField(4)
|
||||
// @HiveField(4)
|
||||
int blockTime;
|
||||
|
||||
@HiveField(5, defaultValue: false)
|
||||
// @HiveField(5, defaultValue: false)
|
||||
bool spent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'mweb_utxo.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class MwebUtxoAdapter extends TypeAdapter<MwebUtxo> {
|
||||
@override
|
||||
final int typeId = 20;
|
||||
|
||||
@override
|
||||
MwebUtxo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return MwebUtxo(
|
||||
height: fields[0] as int,
|
||||
value: fields[1] as int,
|
||||
address: fields[2] as String,
|
||||
outputId: fields[3] as String,
|
||||
blockTime: fields[4] as int,
|
||||
spent: fields[5] == null ? false : fields[5] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MwebUtxo obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.height)
|
||||
..writeByte(1)
|
||||
..write(obj.value)
|
||||
..writeByte(2)
|
||||
..write(obj.address)
|
||||
..writeByte(3)
|
||||
..write(obj.outputId)
|
||||
..writeByte(4)
|
||||
..write(obj.blockTime)
|
||||
..writeByte(5)
|
||||
..write(obj.spent);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MwebUtxoAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'nano_account.g.dart';
|
||||
part 'nano_account.part.dart';
|
||||
|
||||
@HiveType(typeId: NanoAccount.typeId)
|
||||
// @HiveType(typeId: NanoAccount.typeId)
|
||||
class NanoAccount extends HiveObject {
|
||||
NanoAccount({required this.label, required this.id, this.balance, this.isSelected = false});
|
||||
|
||||
static const typeId = NANO_ACCOUNT_TYPE_ID;
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
String label;
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
final int id;
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
bool isSelected;
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
String? balance;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'nano_account.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class NanoAccountAdapter extends TypeAdapter<NanoAccount> {
|
||||
@override
|
||||
final int typeId = 13;
|
||||
|
||||
@override
|
||||
NanoAccount read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return NanoAccount(
|
||||
label: fields[0] as String,
|
||||
id: fields[1] as int,
|
||||
balance: fields[3] as String?,
|
||||
isSelected: fields[2] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, NanoAccount obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.label)
|
||||
..writeByte(1)
|
||||
..write(obj.id)
|
||||
..writeByte(2)
|
||||
..write(obj.isSelected)
|
||||
..writeByte(3)
|
||||
..write(obj.balance);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is NanoAccountAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
+211
-57
@@ -1,33 +1,60 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:blockchain_utils/hex/hex.dart';
|
||||
import 'package:cw_core/node_list.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_socket/abstract.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
part 'node.g.dart';
|
||||
import 'db/sqlite.dart';
|
||||
|
||||
Uri createUriFromElectrumAddress(String address, String path) =>
|
||||
Uri.tryParse('tcp://$address$path')!;
|
||||
|
||||
@HiveType(typeId: Node.typeId)
|
||||
class Node extends HiveObject with Keyable {
|
||||
Future<void> validateBuiltinNodes() async {
|
||||
// ensures nodes stored as builtin in the db correspond to the nodes in the .yml files
|
||||
|
||||
final builtinFromDb = await Node.getAllBuiltin();
|
||||
final builtinFromList = await loadAllDefaultNodes();
|
||||
|
||||
for (final listNode in builtinFromList) {
|
||||
// preserve proxy settings from user
|
||||
try {
|
||||
final matchedDbNode = builtinFromDb.firstWhere((dbNode) => dbNode.uri == listNode.uri);
|
||||
listNode.socksProxyAddress = matchedDbNode.socksProxyAddress;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
final dbSet = builtinFromDb.toSet();
|
||||
final listSet = builtinFromList.toSet();
|
||||
|
||||
final nodesToDelete = dbSet.difference(listSet);
|
||||
final nodesToAdd = listSet.difference(dbSet);
|
||||
|
||||
for (final node in nodesToDelete) await node.delete();
|
||||
for (final node in nodesToAdd) await node.save();
|
||||
}
|
||||
|
||||
class Node {
|
||||
Node({
|
||||
this.id = 0,
|
||||
this.label,
|
||||
this.login,
|
||||
this.password,
|
||||
this.useSSL,
|
||||
this.isPow = false,
|
||||
this.trusted = false,
|
||||
this.socksProxyAddress,
|
||||
this.path = '',
|
||||
this.isEnabledForAutoSwitching = false,
|
||||
this.isOfficial = false,
|
||||
this.isBuiltin = false,
|
||||
this.isDefault = false,
|
||||
String? uri,
|
||||
WalletType? type,
|
||||
}) {
|
||||
@@ -50,67 +77,204 @@ class Node extends HiveObject with Keyable {
|
||||
trusted: $trusted,
|
||||
socksProxyAddress: $socksProxyAddress,
|
||||
isEnabledForAutoSwitching: $isEnabledForAutoSwitching,
|
||||
isOfficial: $isOfficial,
|
||||
isBuiltin: $isBuiltin,
|
||||
isDefault: $isDefault
|
||||
})""";
|
||||
}
|
||||
|
||||
Node.fromMap(Map<String, Object?> map)
|
||||
: uriRaw = map['uri'] as String? ?? '',
|
||||
: id = (map[selfIdColumn] ?? 0) as int,
|
||||
uriRaw = map['uri'] as String? ?? '',
|
||||
path = map['path'] as String? ?? '',
|
||||
login = map['login'] as String?,
|
||||
label = map['label'] as String?,
|
||||
password = map['password'] as String?,
|
||||
useSSL = map['useSSL'] as bool?,
|
||||
trusted = map['trusted'] as bool? ?? false,
|
||||
socksProxyAddress = map['socksProxyPort'] as String?,
|
||||
isEnabledForAutoSwitching = map['isEnabledForAutoSwitching'] as bool? ?? false;
|
||||
isPow = _getBoolFromDB(map['isPow']),
|
||||
useSSL = _getBoolFromDB(map['useSSL']),
|
||||
typeRaw = (map["typeRaw"] ?? 0) as int,
|
||||
trusted = _getBoolFromDB(map['trusted']),
|
||||
socksProxyAddress = map['socksProxyAddress'] as String?,
|
||||
isEnabledForAutoSwitching = _getBoolFromDB(map['isEnabledForAutoSwitching']),
|
||||
isOfficial = _getBoolFromDB(map['isOfficial']),
|
||||
isBuiltin = _getBoolFromDB(map['isBuiltin']),
|
||||
isDefault = _getBoolFromDB(map['isDefault']);
|
||||
|
||||
static const typeId = NODE_TYPE_ID;
|
||||
static const boxName = 'Nodes';
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@HiveField(0, defaultValue: '')
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
selfIdColumn: id,
|
||||
'uri': uriRaw,
|
||||
'path': path,
|
||||
'login': login,
|
||||
"label": label,
|
||||
'password': password,
|
||||
"isPow": isPow ? 1 : 0,
|
||||
'useSSL': (useSSL ?? false) ? 1 : 0,
|
||||
"typeRaw": typeRaw,
|
||||
'trusted': trusted ? 1 : 0,
|
||||
'socksProxyAddress': socksProxyAddress,
|
||||
'isEnabledForAutoSwitching': isEnabledForAutoSwitching ? 1 : 0,
|
||||
"isOfficial": isOfficial ? 1 : 0,
|
||||
"isBuiltin": isBuiltin ? 1 : 0,
|
||||
"isDefault": isDefault ? 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
Node.fromUri(Uri uri, WalletType type)
|
||||
: id = 0,
|
||||
uriRaw =
|
||||
"${uri.host}${uri.hasPort ? ':${uri.port}' : (uri.queryParameters['port']?.isNotEmpty == true ? ':${uri.queryParameters['port']}' : '')}",
|
||||
path = uri.path,
|
||||
login = uri.userInfo.contains(':')
|
||||
? uri.userInfo.split(':')[0]
|
||||
: uri.queryParameters['username'],
|
||||
password = uri.userInfo.contains(':')
|
||||
? uri.userInfo.split(':')[1]
|
||||
: uri.queryParameters['password'],
|
||||
useSSL = uri.queryParameters['protocol'] == 'https',
|
||||
trusted = uri.queryParameters['trusted'] == 'true',
|
||||
isPow = false,
|
||||
isEnabledForAutoSwitching = false,
|
||||
isOfficial = false,
|
||||
isBuiltin = false,
|
||||
isDefault = false,
|
||||
typeRaw = serializeToInt(type);
|
||||
|
||||
Future<int> delete() async {
|
||||
return await db!.delete(tableName, where: '${selfIdColumn} = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
static Future<int> deleteAll() async {
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [0]);
|
||||
}
|
||||
|
||||
static Future<int> deleteAllPow() async {
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [1]);
|
||||
}
|
||||
|
||||
Future<int> save() async {
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<Node>> selectList(String where, List<dynamic> whereArgs,
|
||||
{String? orderBy}) async {
|
||||
if (orderBy == null) {
|
||||
orderBy = selfIdColumn;
|
||||
}
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => Node.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<Node?> select(String where, List<dynamic> whereArgs, {String? orderBy}) async {
|
||||
if (orderBy == null) {
|
||||
orderBy = selfIdColumn;
|
||||
}
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return list.isEmpty ? null : Node.fromMap(list.first);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAll() async {
|
||||
return selectList("isPow = ?", [0]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllBuiltin() async {
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [0, 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllPowBuiltin() async {
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [1, 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllForWalletType(WalletType type) async {
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 0]);
|
||||
}
|
||||
|
||||
static Future<Node?> getDefaultForWalletType(WalletType type) async {
|
||||
return (await selectList(
|
||||
"typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), 0, 1]))
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
static Future<Node?> getDefaultPowForWalletType(WalletType type) async {
|
||||
return (await selectList(
|
||||
"typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), 1, 1]))
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllForWalletTypePow(WalletType type) async {
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllPow() async {
|
||||
return selectList("isPow = ?", [1]);
|
||||
}
|
||||
|
||||
static Future<Node?> get(int id) async {
|
||||
return select("${selfIdColumn} = ?", [id]);
|
||||
}
|
||||
|
||||
int id;
|
||||
late String uriRaw;
|
||||
|
||||
@HiveField(1)
|
||||
String? login;
|
||||
|
||||
@HiveField(2)
|
||||
String? password;
|
||||
|
||||
@HiveField(3, defaultValue: 0)
|
||||
late int typeRaw;
|
||||
|
||||
@HiveField(4)
|
||||
bool? useSSL;
|
||||
|
||||
@HiveField(5, defaultValue: false)
|
||||
bool trusted;
|
||||
|
||||
@HiveField(6)
|
||||
bool isPow;
|
||||
String? socksProxyAddress;
|
||||
|
||||
@HiveField(7, defaultValue: '')
|
||||
String? path;
|
||||
|
||||
@HiveField(8)
|
||||
bool? isElectrs;
|
||||
|
||||
@HiveField(9)
|
||||
bool? supportsSilentPayments;
|
||||
|
||||
@HiveField(10)
|
||||
bool? supportsMweb;
|
||||
|
||||
@HiveField(11, defaultValue: false)
|
||||
bool isEnabledForAutoSwitching;
|
||||
bool isOfficial;
|
||||
bool isBuiltin;
|
||||
bool isDefault;
|
||||
|
||||
@HiveField(12, defaultValue: '')
|
||||
String? label;
|
||||
|
||||
static String get tableName => "Node";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
bool get isSSL => useSSL ?? false;
|
||||
|
||||
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
|
||||
|
||||
Uri get uri {
|
||||
try {
|
||||
return _uri;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
return Uri();
|
||||
}
|
||||
}
|
||||
|
||||
Uri get _uri {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
case WalletType.wownero:
|
||||
@@ -161,18 +325,10 @@ class Node extends HiveObject with Keyable {
|
||||
socksProxyAddress.hashCode ^
|
||||
path.hashCode;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex {
|
||||
_keyIndex ??= key;
|
||||
return _keyIndex;
|
||||
}
|
||||
|
||||
WalletType get type => deserializeFromInt(typeRaw);
|
||||
|
||||
set type(WalletType type) => typeRaw = serializeToInt(type);
|
||||
|
||||
dynamic _keyIndex;
|
||||
|
||||
Future<bool> requestNode() async {
|
||||
try {
|
||||
switch (type) {
|
||||
@@ -210,15 +366,13 @@ class Node extends HiveObject with Keyable {
|
||||
final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
|
||||
|
||||
try {
|
||||
final client = ProxyWrapper().getHttpIOClient();
|
||||
|
||||
final jsonBody = json.encode(body);
|
||||
|
||||
final response = await client.post(
|
||||
rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonBody,
|
||||
);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonBody,
|
||||
allowMitmMoneroBypassSSLCheck: true);
|
||||
// Check if we received a 401 Unauthorized response
|
||||
if (response.statusCode == 401) {
|
||||
final daemonRpc = DaemonRpc(
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
import 'package:cw_core/node_list.dart';
|
||||
import 'package:cw_core/utils/proxy_socket/abstract.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
import 'cake_hive.dart';
|
||||
import 'node.dart' as node_new;
|
||||
|
||||
part 'node_legacy.part.dart';
|
||||
|
||||
Future<void> performNodeHiveMigration() async {
|
||||
if (!CakeHive.isAdapterRegistered(Node.typeId)) {
|
||||
CakeHive.registerAdapter(NodeAdapter());
|
||||
}
|
||||
|
||||
final nodeBox = await CakeHive.openBox<Node>(Node.boxName);
|
||||
final powNodeBox = await CakeHive.openBox<Node>(Node.boxName + "pow");
|
||||
final builtinNodes = await loadAllDefaultNodes();
|
||||
final builtinPowNodes = await loadDefaultNanoPowNodes();
|
||||
await Node.migrateAllToSqlite(nodeBox, powNodeBox, builtinNodes, builtinPowNodes);
|
||||
}
|
||||
|
||||
Uri createUriFromElectrumAddress(String address, String path) =>
|
||||
Uri.tryParse('tcp://$address$path')!;
|
||||
|
||||
@HiveType(typeId: Node.typeId)
|
||||
class Node extends HiveObject with Keyable {
|
||||
Node({
|
||||
this.label,
|
||||
this.login,
|
||||
this.password,
|
||||
this.useSSL,
|
||||
this.trusted = false,
|
||||
this.socksProxyAddress,
|
||||
this.path = '',
|
||||
this.isEnabledForAutoSwitching = false,
|
||||
String? uri,
|
||||
WalletType? type,
|
||||
}) {
|
||||
if (uri != null) {
|
||||
uriRaw = uri;
|
||||
}
|
||||
if (type != null) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> migrateAllToSqlite(Box<Node> nodeBox, Box<Node> powNodeBox,
|
||||
List<node_new.Node> defaultNodes, List<node_new.Node> defaultPowNodes) async {
|
||||
final list = nodeBox.values.toList();
|
||||
final powList = powNodeBox.values.toList();
|
||||
for (final node in list) {
|
||||
if (defaultNodes.any((item) => item.uri == node.uri)) {
|
||||
// default nodes will be added by validateBuiltinNodes(), no need to migrate them.
|
||||
await node.delete();
|
||||
continue;
|
||||
}
|
||||
await node.migrateToSqlite(isPow: false, isBuiltin: false, isOfficial: false);
|
||||
await node.delete();
|
||||
}
|
||||
for (final node in powList) {
|
||||
if (defaultPowNodes.any((item) => item.uri == node.uri)) {
|
||||
await node.delete();
|
||||
continue;
|
||||
}
|
||||
await node.migrateToSqlite(isPow: true, isBuiltin: false, isOfficial: false);
|
||||
await node.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite(
|
||||
{required bool isPow, required bool isBuiltin, required bool isOfficial}) async {
|
||||
final newNode = node_new.Node(
|
||||
id: key as int,
|
||||
login: login,
|
||||
label: label,
|
||||
password: password,
|
||||
type: type,
|
||||
useSSL: useSSL,
|
||||
trusted: trusted,
|
||||
socksProxyAddress: socksProxyAddress,
|
||||
isPow: isPow,
|
||||
path: path,
|
||||
uri: uriRaw,
|
||||
isEnabledForAutoSwitching: isEnabledForAutoSwitching,
|
||||
isOfficial: isOfficial,
|
||||
isBuiltin: isBuiltin,
|
||||
);
|
||||
await newNode.save();
|
||||
}
|
||||
|
||||
Node.fromMap(Map<String, Object?> map)
|
||||
: uriRaw = map['uri'] as String? ?? '',
|
||||
path = map['path'] as String? ?? '',
|
||||
login = map['login'] as String?,
|
||||
label = map['label'] as String?,
|
||||
password = map['password'] as String?,
|
||||
useSSL = map['useSSL'] as bool?,
|
||||
trusted = map['trusted'] as bool? ?? false,
|
||||
socksProxyAddress = map['socksProxyPort'] as String?,
|
||||
isEnabledForAutoSwitching = map['isEnabledForAutoSwitching'] as bool? ?? false;
|
||||
|
||||
static const typeId = NODE_TYPE_ID;
|
||||
static const boxName = 'Nodes';
|
||||
|
||||
@HiveField(0, defaultValue: '')
|
||||
late String uriRaw;
|
||||
|
||||
@HiveField(1)
|
||||
String? login;
|
||||
|
||||
@HiveField(2)
|
||||
String? password;
|
||||
|
||||
@HiveField(3, defaultValue: 0)
|
||||
late int typeRaw;
|
||||
|
||||
@HiveField(4)
|
||||
bool? useSSL;
|
||||
|
||||
@HiveField(5, defaultValue: false)
|
||||
bool trusted;
|
||||
|
||||
@HiveField(6)
|
||||
String? socksProxyAddress;
|
||||
|
||||
@HiveField(7, defaultValue: '')
|
||||
String? path;
|
||||
|
||||
@HiveField(8)
|
||||
bool? isElectrs;
|
||||
|
||||
@HiveField(9)
|
||||
bool? supportsSilentPayments;
|
||||
|
||||
@HiveField(10)
|
||||
bool? supportsMweb;
|
||||
|
||||
@HiveField(11, defaultValue: false)
|
||||
bool isEnabledForAutoSwitching;
|
||||
|
||||
@HiveField(12, defaultValue: '')
|
||||
String? label;
|
||||
|
||||
bool get isSSL => useSSL ?? false;
|
||||
|
||||
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
|
||||
|
||||
Uri get uri {
|
||||
try {
|
||||
return _uri;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
return Uri();
|
||||
}
|
||||
}
|
||||
|
||||
Uri get _uri {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
case WalletType.wownero:
|
||||
return Uri.http(uriRaw, '');
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.dogecoin:
|
||||
return createUriFromElectrumAddress(uriRaw, path!);
|
||||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.base:
|
||||
case WalletType.bsc:
|
||||
case WalletType.arbitrum:
|
||||
return Uri.parse(
|
||||
"http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}");
|
||||
case WalletType.none:
|
||||
throw Exception('Unexpected type ${type.toString()} for Node uri');
|
||||
}
|
||||
}
|
||||
|
||||
bool get isValidProxyAddress => socksProxyAddress?.contains(':') ?? false;
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is Node &&
|
||||
(other.uriRaw == uriRaw &&
|
||||
other.login == login &&
|
||||
other.label == label &&
|
||||
other.password == password &&
|
||||
other.typeRaw == typeRaw &&
|
||||
other.useSSL == useSSL &&
|
||||
other.trusted == trusted &&
|
||||
other.socksProxyAddress == socksProxyAddress &&
|
||||
other.path == path);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
uriRaw.hashCode ^
|
||||
login.hashCode ^
|
||||
label.hashCode ^
|
||||
password.hashCode ^
|
||||
typeRaw.hashCode ^
|
||||
useSSL.hashCode ^
|
||||
trusted.hashCode ^
|
||||
socksProxyAddress.hashCode ^
|
||||
path.hashCode;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex {
|
||||
_keyIndex ??= key;
|
||||
return _keyIndex;
|
||||
}
|
||||
|
||||
WalletType get type => deserializeFromInt(typeRaw);
|
||||
|
||||
set type(WalletType type) => typeRaw = serializeToInt(type);
|
||||
|
||||
dynamic _keyIndex;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'node_legacy.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class NodeAdapter extends TypeAdapter<Node> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
Node read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Node(
|
||||
label: fields[12] == null ? '' : fields[12] as String?,
|
||||
login: fields[1] as String?,
|
||||
password: fields[2] as String?,
|
||||
useSSL: fields[4] as bool?,
|
||||
trusted: fields[5] == null ? false : fields[5] as bool,
|
||||
socksProxyAddress: fields[6] as String?,
|
||||
path: fields[7] == null ? '' : fields[7] as String?,
|
||||
isEnabledForAutoSwitching: fields[11] == null ? false : fields[11] as bool,
|
||||
)
|
||||
..uriRaw = fields[0] == null ? '' : fields[0] as String
|
||||
..typeRaw = fields[3] == null ? 0 : fields[3] as int
|
||||
..isElectrs = fields[8] as bool?
|
||||
..supportsSilentPayments = fields[9] as bool?
|
||||
..supportsMweb = fields[10] as bool?;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Node obj) {
|
||||
writer
|
||||
..writeByte(13)
|
||||
..writeByte(0)
|
||||
..write(obj.uriRaw)
|
||||
..writeByte(1)
|
||||
..write(obj.login)
|
||||
..writeByte(2)
|
||||
..write(obj.password)
|
||||
..writeByte(3)
|
||||
..write(obj.typeRaw)
|
||||
..writeByte(4)
|
||||
..write(obj.useSSL)
|
||||
..writeByte(5)
|
||||
..write(obj.trusted)
|
||||
..writeByte(6)
|
||||
..write(obj.socksProxyAddress)
|
||||
..writeByte(7)
|
||||
..write(obj.path)
|
||||
..writeByte(8)
|
||||
..write(obj.isElectrs)
|
||||
..writeByte(9)
|
||||
..write(obj.supportsSilentPayments)
|
||||
..writeByte(10)
|
||||
..write(obj.supportsMweb)
|
||||
..writeByte(11)
|
||||
..write(obj.isEnabledForAutoSwitching)
|
||||
..writeByte(12)
|
||||
..write(obj.label);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is NodeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import "package:yaml/yaml.dart";
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
Future<List<Node>> loadDefaultNodes(WalletType type) async {
|
||||
String path;
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
path = 'assets/node_list.yml';
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
path = 'assets/bitcoin_electrum_server_list.yml';
|
||||
break;
|
||||
case WalletType.litecoin:
|
||||
path = 'assets/litecoin_electrum_server_list.yml';
|
||||
break;
|
||||
// TODO: (refactoring) each wallet would have its path, so `wallet.nodePath` would be decided based on chain id in Evm wallet
|
||||
case WalletType.ethereum:
|
||||
path = 'assets/ethereum_server_list.yml';
|
||||
break;
|
||||
case WalletType.nano:
|
||||
path = 'assets/nano_node_list.yml';
|
||||
break;
|
||||
case WalletType.bitcoinCash:
|
||||
path = 'assets/bitcoin_cash_electrum_server_list.yml';
|
||||
break;
|
||||
case WalletType.polygon:
|
||||
path = 'assets/polygon_node_list.yml';
|
||||
break;
|
||||
case WalletType.wownero:
|
||||
path = 'assets/wownero_node_list.yml';
|
||||
break;
|
||||
case WalletType.dogecoin:
|
||||
path = 'assets/dogecoin_electrum_server_list.yml';
|
||||
break;
|
||||
case WalletType.base:
|
||||
path = 'assets/base_node_list.yml';
|
||||
break;
|
||||
case WalletType.arbitrum:
|
||||
path = 'assets/arbitrum_node_list.yml';
|
||||
break;
|
||||
case WalletType.bsc:
|
||||
path = 'assets/bsc_node_list.yml';
|
||||
break;
|
||||
case WalletType.banano:
|
||||
case WalletType.none:
|
||||
path = '';
|
||||
break;
|
||||
}
|
||||
|
||||
final nodesRaw = await rootBundle.loadString(path);
|
||||
final loadedNodes = loadYaml(nodesRaw) as YamlList;
|
||||
final nodes = <Node>[];
|
||||
|
||||
for (final raw in loadedNodes) {
|
||||
if (raw is Map) {
|
||||
final map = Map.from(raw);
|
||||
map["isBuiltin"] = 1;
|
||||
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
|
||||
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
|
||||
|
||||
final node = Node.fromMap(Map<String, Object>.from(map));
|
||||
node.type = type;
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
Future<List<Node>> loadDefaultNanoPowNodes() async {
|
||||
final powNodesRaw = await rootBundle.loadString('assets/nano_pow_node_list.yml');
|
||||
final loadedPowNodes = loadYaml(powNodesRaw) as YamlList;
|
||||
final nodes = <Node>[];
|
||||
|
||||
for (final raw in loadedPowNodes) {
|
||||
if (raw is Map) {
|
||||
final map = Map.from(raw);
|
||||
map["isBuiltin"] = 1;
|
||||
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
|
||||
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
|
||||
final node = Node.fromMap(Map<String, Object>.from(map));
|
||||
node.isPow = true;
|
||||
node.type = WalletType.nano;
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
Future<List<Node>> loadAllDefaultNodes() async {
|
||||
final wowneroNodes = await loadDefaultNodes(WalletType.wownero);
|
||||
final moneroNodes = await loadDefaultNodes(WalletType.monero);
|
||||
final bitcoinElectrumServerList = await loadDefaultNodes(WalletType.bitcoin);
|
||||
final litecoinElectrumServerList = await loadDefaultNodes(WalletType.litecoin);
|
||||
final bitcoinCashElectrumServerList = await loadDefaultNodes(WalletType.bitcoinCash);
|
||||
final ethereumNodes = await loadDefaultNodes(WalletType.ethereum);
|
||||
final nanoNodes = await loadDefaultNodes(WalletType.nano);
|
||||
final polygonNodes = await loadDefaultNodes(WalletType.polygon);
|
||||
final dogecoinElectrumServerList = await loadDefaultNodes(WalletType.dogecoin);
|
||||
final baseNodes = await loadDefaultNodes(WalletType.base);
|
||||
final arbitrumNodes = await loadDefaultNodes(WalletType.arbitrum);
|
||||
final bscNodes = await loadDefaultNodes(WalletType.bsc);
|
||||
|
||||
return wowneroNodes +
|
||||
moneroNodes +
|
||||
bitcoinElectrumServerList +
|
||||
litecoinElectrumServerList +
|
||||
ethereumNodes +
|
||||
bitcoinCashElectrumServerList +
|
||||
nanoNodes +
|
||||
polygonNodes +
|
||||
dogecoinElectrumServerList +
|
||||
baseNodes +
|
||||
arbitrumNodes +
|
||||
bscNodes;
|
||||
}
|
||||
|
||||
Future<void> resetToDefault() async {
|
||||
final nodes = await loadAllDefaultNodes();
|
||||
|
||||
final currentNodes = await Node.getAll();
|
||||
|
||||
// after reset, nodes should retain the id to correspond with the ones in shared prefs.
|
||||
// alternative would be nuking the shared prefs on reset
|
||||
for (final node in nodes) {
|
||||
try {
|
||||
final curr = currentNodes.firstWhere(
|
||||
(item) => item.uri == node.uri && item.typeRaw == node.typeRaw,
|
||||
);
|
||||
node.id = curr.id;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
await Node.deleteAll();
|
||||
for (final node in nodes) {
|
||||
await node.save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetPowToDefault() async {
|
||||
final nanoPowNodes = await loadDefaultNanoPowNodes();
|
||||
await Node.deleteAllPow();
|
||||
for (final node in nanoPowNodes) {
|
||||
node.save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Node> getDefaultNodeFromFiles(WalletType type, {bool isPow = false}) async {
|
||||
final nodes = isPow ? await loadDefaultNanoPowNodes() : await loadDefaultNodes(type);
|
||||
try {
|
||||
return nodes.firstWhere((item) => item.isDefault);
|
||||
} catch (e) {
|
||||
// ideally the yamls would always have a default, but this is better than nothing
|
||||
return nodes.first;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
class OutputInfo {
|
||||
const OutputInfo(
|
||||
{required this.address,
|
||||
required this.sendAll,
|
||||
required this.isParsedAddress,
|
||||
this.cryptoAmount,
|
||||
this.formattedCryptoAmount,
|
||||
this.fiatAmount,
|
||||
this.note,
|
||||
this.extractedAddress,
|
||||
this.memo,
|
||||
this.extra = const {}});
|
||||
const OutputInfo({
|
||||
required this.address,
|
||||
required this.sendAll,
|
||||
required this.isParsedAddress,
|
||||
required this.cryptoAmount,
|
||||
this.fiatAmount,
|
||||
this.note,
|
||||
this.extractedAddress,
|
||||
this.memo,
|
||||
this.extra = const {},
|
||||
});
|
||||
|
||||
final String? fiatAmount;
|
||||
final String? cryptoAmount;
|
||||
final Money cryptoAmount;
|
||||
final String address;
|
||||
final String? note;
|
||||
final String? extractedAddress;
|
||||
final bool sendAll;
|
||||
final bool isParsedAddress;
|
||||
final int? formattedCryptoAmount;
|
||||
final String? memo;
|
||||
final Map<String, dynamic> extra;
|
||||
}
|
||||
|
||||
@@ -29,32 +29,49 @@ BigInt? tryParseFixed(String value, int decimals) {
|
||||
BigInt parseFixed(String value, int decimals) {
|
||||
final multiplier = getMultiplier(decimals);
|
||||
|
||||
/// handle weird cases where users enter spaces and currency after the amount
|
||||
/// This should be handled from UI field to prevent non numerical values
|
||||
/// but will be in the refactoring
|
||||
if (value.contains(" ")) {
|
||||
value = value.split(" ").first;
|
||||
}
|
||||
|
||||
final negative = value.startsWith("-");
|
||||
if (negative) value = value.substring(1);
|
||||
if (negative) {
|
||||
value = value.substring(1);
|
||||
}
|
||||
|
||||
if (value == ".") throw FormatException("missing value, value, $value");
|
||||
if (value == ".") {
|
||||
throw FormatException("missing value, value, $value");
|
||||
}
|
||||
|
||||
if (value.startsWith(".")) value = "0$value";
|
||||
if (value.startsWith(".")) {
|
||||
value = "0$value";
|
||||
}
|
||||
|
||||
final comps = value.split(".");
|
||||
if (comps.length > 2) {
|
||||
throw FormatException("too many decimal points, value, $value");
|
||||
}
|
||||
|
||||
var whole = comps.isNotEmpty ? comps[0] : "0";
|
||||
var fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0");
|
||||
final whole = comps.isNotEmpty ? comps[0] : "0";
|
||||
final fraction = (comps.length == 2 ? comps[1] : "").padRight(decimals, "0");
|
||||
|
||||
if (fraction.length > multiplier.length - 1) {
|
||||
throw FormatException("fractional component exceeds decimals, underflow, parseFixed");
|
||||
throw FormatException(
|
||||
"fractional component(${fraction.length}) exceeds decimals(${decimals}), underflow, parseFixed",
|
||||
);
|
||||
}
|
||||
|
||||
final wholeValue = BigInt.parse(whole);
|
||||
final fractionValue = BigInt.parse(fraction);
|
||||
final fractionValue = fraction.isEmpty ? BigInt.zero : BigInt.parse(fraction);
|
||||
final multiplierValue = BigInt.parse(multiplier);
|
||||
|
||||
var wei = (wholeValue * multiplierValue) + fractionValue;
|
||||
|
||||
if (negative) wei *= BigInt.from(-1);
|
||||
if (negative) {
|
||||
wei *= BigInt.from(-1);
|
||||
}
|
||||
|
||||
return wei;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
Future<String> pathForWalletTypeDir({required WalletType type}) async {
|
||||
final root = await getAppDir();
|
||||
@@ -35,3 +36,43 @@ Future<String> outdatedAndroidPathForWalletDir({required String name}) async {
|
||||
|
||||
return pathDir;
|
||||
}
|
||||
|
||||
Future<void> copyWalletFilesTo({
|
||||
required String fromName,
|
||||
required String toName,
|
||||
required WalletType type,
|
||||
}) async {
|
||||
if (fromName == toName) return;
|
||||
|
||||
final typeRoot = await pathForWalletTypeDir(type: type);
|
||||
final sourceDir = Directory(p.join(typeRoot, fromName));
|
||||
if (!sourceDir.existsSync()) {
|
||||
throw "Source wallet not found: $fromName $type";
|
||||
}
|
||||
|
||||
if (Directory(p.join(typeRoot, toName)).existsSync()) {
|
||||
throw Exception('Cannot rename wallet: "$toName" already exists');
|
||||
}
|
||||
|
||||
final destinationDir = Directory(p.join(typeRoot, toName));
|
||||
await _copyDirectory(sourceDir, destinationDir);
|
||||
|
||||
for (final suffix in const ['', '.keys', '.keys.backup']) {
|
||||
final file = File(p.join(destinationDir.path, '$fromName$suffix'));
|
||||
if (file.existsSync()) {
|
||||
await file.rename(p.join(destinationDir.path, '$toName$suffix'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _copyDirectory(Directory source, Directory destination) async {
|
||||
await destination.create(recursive: true);
|
||||
await for (final entity in source.list(followLinks: false)) {
|
||||
final name = p.basename(entity.path);
|
||||
if (entity is File) {
|
||||
await entity.copy(p.join(destination.path, name));
|
||||
} else if (entity is Directory) {
|
||||
await _copyDirectory(entity, Directory(p.join(destination.path, name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'payjoin_session.g.dart';
|
||||
part 'payjoin_session.part.dart';
|
||||
|
||||
@HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
|
||||
// @HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
|
||||
class PayjoinSession extends HiveObject {
|
||||
PayjoinSession({
|
||||
required this.walletId,
|
||||
@@ -25,31 +25,31 @@ class PayjoinSession extends HiveObject {
|
||||
static const typeId = PAYJOIN_SESSION_TYPE_ID;
|
||||
static const boxName = 'PayjoinSessions';
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
final String walletId;
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
final String? sender;
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
final String? receiver;
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
final String? pjUri;
|
||||
|
||||
@HiveField(4)
|
||||
// @HiveField(4)
|
||||
String status;
|
||||
|
||||
@HiveField(5)
|
||||
// @HiveField(5)
|
||||
DateTime? inProgressSince;
|
||||
|
||||
@HiveField(6)
|
||||
// @HiveField(6)
|
||||
String? txId;
|
||||
|
||||
@HiveField(7)
|
||||
// @HiveField(7)
|
||||
String? rawAmount;
|
||||
|
||||
@HiveField(8)
|
||||
// @HiveField(8)
|
||||
String? error;
|
||||
|
||||
bool get isSenderSession => sender != null;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'payjoin_session.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class PayjoinSessionAdapter extends TypeAdapter<PayjoinSession> {
|
||||
@override
|
||||
final int typeId = 23;
|
||||
|
||||
@override
|
||||
PayjoinSession read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PayjoinSession(
|
||||
walletId: fields[0] as String,
|
||||
receiver: fields[2] as String?,
|
||||
sender: fields[1] as String?,
|
||||
pjUri: fields[3] as String?,
|
||||
status: fields[4] as String,
|
||||
inProgressSince: fields[5] as DateTime?,
|
||||
rawAmount: fields[7] as String?,
|
||||
)
|
||||
..txId = fields[6] as String?
|
||||
..error = fields[8] as String?;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PayjoinSession obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.walletId)
|
||||
..writeByte(1)
|
||||
..write(obj.sender)
|
||||
..writeByte(2)
|
||||
..write(obj.receiver)
|
||||
..writeByte(3)
|
||||
..write(obj.pjUri)
|
||||
..writeByte(4)
|
||||
..write(obj.status)
|
||||
..writeByte(5)
|
||||
..write(obj.inProgressSince)
|
||||
..writeByte(6)
|
||||
..write(obj.txId)
|
||||
..writeByte(7)
|
||||
..write(obj.rawAmount)
|
||||
..writeByte(8)
|
||||
..write(obj.error);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PayjoinSessionAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
+174
-191
@@ -1,4 +1,4 @@
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
import "package:cw_core/format_fixed.dart";
|
||||
|
||||
abstract class PaymentURI {
|
||||
PaymentURI({required this.amount, required this.address});
|
||||
@@ -7,12 +7,19 @@ abstract class PaymentURI {
|
||||
final String address;
|
||||
}
|
||||
|
||||
class ExternalAddressURI extends PaymentURI {
|
||||
ExternalAddressURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() => address;
|
||||
}
|
||||
|
||||
class MoneroURI extends PaymentURI {
|
||||
MoneroURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'monero:$address';
|
||||
var base = "monero:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -27,7 +34,7 @@ class HavenURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'haven:$address';
|
||||
var base = "haven:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -38,7 +45,7 @@ class HavenURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class BitcoinURI extends PaymentURI {
|
||||
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
|
||||
BitcoinURI({required super.amount, required super.address, this.pjUri = ""});
|
||||
|
||||
final String pjUri;
|
||||
|
||||
@@ -46,13 +53,16 @@ class BitcoinURI extends PaymentURI {
|
||||
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;
|
||||
if (amount.isNotEmpty) {
|
||||
qp["amount"] = amount.replaceAll(",", ".");
|
||||
}
|
||||
|
||||
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
|
||||
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
|
||||
qp["pjos"] = "0";
|
||||
qp["pj"] = pjUri;
|
||||
}
|
||||
|
||||
return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +71,7 @@ class LitecoinURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'litecoin:$address';
|
||||
var base = "litecoin:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -76,52 +86,7 @@ class EthereumURI extends PaymentURI {
|
||||
|
||||
@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 BSCURI extends PaymentURI {
|
||||
BSCURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'bsc:$address';
|
||||
var base = "ethereum:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -151,22 +116,7 @@ class NanoURI extends PaymentURI {
|
||||
|
||||
@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';
|
||||
|
||||
var base = "nano:$address";
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
@@ -176,14 +126,23 @@ class PolygonURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class SolanaURI extends PaymentURI {
|
||||
SolanaURI({required super.amount, required super.address});
|
||||
SolanaURI({required super.amount, required super.address, this.contractAddress});
|
||||
|
||||
final String? contractAddress;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'solana:$address';
|
||||
var base = "solana:$address";
|
||||
final params = <String>[];
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
params.add('amount=${amount.replaceAll(',', '.')}');
|
||||
}
|
||||
if (contractAddress != null && contractAddress!.isNotEmpty) {
|
||||
params.add("spl-token=$contractAddress");
|
||||
}
|
||||
if (params.isNotEmpty) {
|
||||
base += '?${params.join('&')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -191,14 +150,23 @@ class SolanaURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class TronURI extends PaymentURI {
|
||||
TronURI({required super.amount, required super.address});
|
||||
TronURI({required super.amount, required super.address, this.contractAddress});
|
||||
|
||||
final String? contractAddress;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'tron:$address';
|
||||
var base = "tron:$address";
|
||||
final params = <String>[];
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
params.add('amount=${amount.replaceAll(',', '.')}');
|
||||
}
|
||||
if (contractAddress != null && contractAddress!.isNotEmpty) {
|
||||
params.add("token=$contractAddress");
|
||||
}
|
||||
if (params.isNotEmpty) {
|
||||
base += '?${params.join('&')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -210,7 +178,7 @@ class WowneroURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'wownero:$address';
|
||||
var base = "wownero:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -221,12 +189,11 @@ class WowneroURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class ZanoURI extends PaymentURI {
|
||||
ZanoURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
ZanoURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'zano:' + address;
|
||||
var base = "zano:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -237,12 +204,11 @@ class ZanoURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class DecredURI extends PaymentURI {
|
||||
DecredURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
DecredURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'decred:' + address;
|
||||
var base = "decred:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -253,12 +219,11 @@ class DecredURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class DogeURI extends PaymentURI {
|
||||
DogeURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
DogeURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'doge:' + address;
|
||||
var base = "doge:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -273,7 +238,7 @@ class ZcashURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'zcash:$address';
|
||||
var base = "zcash:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -284,93 +249,45 @@ class ZcashURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class ERC681URI extends PaymentURI {
|
||||
final int chainId;
|
||||
final String? contractAddress;
|
||||
|
||||
ERC681URI({
|
||||
required this.chainId,
|
||||
required super.address,
|
||||
required super.amount,
|
||||
required this.contractAddress,
|
||||
this.tokenDecimals = 18,
|
||||
this.rawTokenAmount,
|
||||
});
|
||||
|
||||
@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"];
|
||||
final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress;
|
||||
|
||||
var formatedAmount = "";
|
||||
String? rawTokenAmount;
|
||||
|
||||
if (amountParam != null) {
|
||||
final normalized = _normalizeToIntegerWei(amountParam);
|
||||
formatedAmount = formatFixed(BigInt.parse(normalized), 18);
|
||||
} else {
|
||||
if (isContract) {
|
||||
formatedAmount = uri.queryParameters["amount"] ?? "";
|
||||
final uint256Param = uri.queryParameters["uint256"];
|
||||
if (uint256Param != null) {
|
||||
final raw = uint256Param.replaceAll(",", ".").trim();
|
||||
if (!_scientificPattern.hasMatch(raw) && raw.contains(".")) {
|
||||
if (formatedAmount.isEmpty) {
|
||||
formatedAmount = raw;
|
||||
}
|
||||
} else {
|
||||
rawTokenAmount = _normalizeToIntegerWei(raw);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final valueParam = uri.queryParameters["value"];
|
||||
if (valueParam != null) {
|
||||
final normalized = BigInt.tryParse(_normalizeToIntegerWei(valueParam));
|
||||
formatedAmount = normalized != null ? formatFixed(normalized, 18) : "";
|
||||
} else {
|
||||
formatedAmount = uri.queryParameters["amount"] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
return ERC681URI(
|
||||
@@ -378,20 +295,85 @@ class ERC681URI extends PaymentURI {
|
||||
address: address,
|
||||
amount: formatedAmount,
|
||||
contractAddress: isContract ? targetAddress : null,
|
||||
rawTokenAmount: rawTokenAmount,
|
||||
);
|
||||
}
|
||||
|
||||
static int _getChainID(String path) {
|
||||
return int.parse(RegExp(
|
||||
r'@\d*',
|
||||
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
|
||||
"1");
|
||||
final int chainId;
|
||||
final String? contractAddress;
|
||||
final int tokenDecimals;
|
||||
final String? rawTokenAmount;
|
||||
|
||||
static final RegExp _scientificPattern = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$");
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var uri = "ethereum:";
|
||||
|
||||
final targetAddress = contractAddress ?? address;
|
||||
uri += targetAddress;
|
||||
|
||||
uri += "@$chainId";
|
||||
|
||||
if (contractAddress != null) {
|
||||
uri += "/transfer";
|
||||
}
|
||||
|
||||
final params = <String, String>{};
|
||||
|
||||
if (contractAddress != null) {
|
||||
params["address"] = address;
|
||||
// only emit an atomic uint256 when it is unambiguous, either stored raw or 18 decimals,
|
||||
// old app versions divide any uint256 by 1e18 so other tokens rely on the amount param
|
||||
final uint256 = rawTokenAmount ??
|
||||
(tokenDecimals == 18 && amount.isNotEmpty
|
||||
? _expandDecimal(amount.replaceAll(",", "."), 18)
|
||||
: null);
|
||||
if (uint256 != null && uint256.isNotEmpty) {
|
||||
params["uint256"] = uint256;
|
||||
}
|
||||
if (amount.isNotEmpty) {
|
||||
params["amount"] = amount.replaceAll(",", ".");
|
||||
}
|
||||
} else {
|
||||
if (amount.isNotEmpty) {
|
||||
params["value"] = _formatAmountForNative(amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.isNotEmpty) {
|
||||
uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}";
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/// 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(",", ".");
|
||||
}
|
||||
}
|
||||
|
||||
static int _getChainID(String path) =>
|
||||
int.tryParse(
|
||||
RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1",
|
||||
) ??
|
||||
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);
|
||||
// I saw in the schema (thanks Konsti) that EIP-681 allows an optional "pay-" prefix before the target address, so adding a check for it here
|
||||
final cleaned = path.startsWith("pay-") ? path.substring(4) : path;
|
||||
final match = RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(cleaned);
|
||||
final targetAddress = match?.group(0) ?? cleaned.split("@").first.split("/").first;
|
||||
return (cleaned.contains("/"), targetAddress);
|
||||
}
|
||||
|
||||
/// Normalize an input amount into an integer wei string.
|
||||
@@ -401,25 +383,26 @@ class ERC681URI extends PaymentURI {
|
||||
/// - 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();
|
||||
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'^\+'), '');
|
||||
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]);
|
||||
if (_scientificPattern.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('.')) {
|
||||
if (raw.contains(".")) {
|
||||
return _expandDecimal(raw, 18);
|
||||
}
|
||||
return raw;
|
||||
@@ -437,34 +420,34 @@ class ERC681URI extends PaymentURI {
|
||||
/// _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] == '-' ? '-' : '';
|
||||
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 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 head = digits.isEmpty ? "0" : digits;
|
||||
final zeros = List.filled(shift, "0").join();
|
||||
final res = head + zeros;
|
||||
return sign + (res.isEmpty ? '0' : res);
|
||||
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';
|
||||
return "0";
|
||||
}
|
||||
final res = digits.substring(0, cut);
|
||||
return sign + (res.isEmpty ? '0' : res);
|
||||
return sign + (res.isEmpty ? "0" : res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
class PendingChange {
|
||||
final String address;
|
||||
final BigInt amount;
|
||||
@@ -7,22 +9,20 @@ class PendingChange {
|
||||
|
||||
mixin PendingTransaction {
|
||||
String get id;
|
||||
|
||||
Money get amount;
|
||||
Money get fee;
|
||||
Money? get additionalCost => null;
|
||||
|
||||
String get amountFormatted;
|
||||
String get feeFormatted;
|
||||
String get feeFormattedValue;
|
||||
String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8);
|
||||
String get feeFormattedValue => fee.toStringWithPrecision(fractionalDigits: 8);
|
||||
String? feeRate;
|
||||
String get hex;
|
||||
String? get evmTxHashFromRawHex => null;
|
||||
int? get outputCount => null;
|
||||
PendingChange? change;
|
||||
|
||||
String get roundedAmountFormatted {
|
||||
if (amountFormatted.split(".").last.length <= 8) {
|
||||
return amountFormatted;
|
||||
}
|
||||
return double.parse(amountFormatted).toStringAsFixed(8);
|
||||
}
|
||||
|
||||
bool shouldCommitUR() => false;
|
||||
|
||||
Future<void> commit();
|
||||
|
||||
@@ -3,8 +3,6 @@ import 'package:cw_core/enumerate.dart';
|
||||
class ReceivePageOption implements Enumerate {
|
||||
static const mainnet = ReceivePageOption._('mainnet');
|
||||
static const testnet = ReceivePageOption._('testnet');
|
||||
static const anonPayInvoice = ReceivePageOption._('anonPayInvoice');
|
||||
static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink');
|
||||
|
||||
const ReceivePageOption._(this.value,
|
||||
{this.iconPath, this.description, this.isCommon = false, this.addAddressWord = false});
|
||||
@@ -22,7 +20,4 @@ class ReceivePageOption implements Enumerate {
|
||||
|
||||
const ReceivePageOptions = [
|
||||
ReceivePageOption.mainnet,
|
||||
// FIXME anonpay disabed for new ui beta. restore after screens are ready
|
||||
// ReceivePageOption.anonPayInvoice,
|
||||
// ReceivePageOption.anonPayDonationLink
|
||||
];
|
||||
|
||||
+146
-59
@@ -1,42 +1,8 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'spl_token.g.dart';
|
||||
|
||||
@HiveType(typeId: SPLToken.typeId)
|
||||
class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
@override
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
@HiveField(2)
|
||||
final String mintAddress;
|
||||
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
@HiveField(5)
|
||||
final String mint;
|
||||
|
||||
@override
|
||||
@HiveField(6)
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
@HiveField(7)
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
@HiveField(8, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class SPLToken extends CryptoCurrency {
|
||||
SPLToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
@@ -44,9 +10,12 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
required this.decimal,
|
||||
required this.mint,
|
||||
this.iconPath,
|
||||
this.tag = 'SOL',
|
||||
this.tag = "SOL",
|
||||
bool enabled = true,
|
||||
this.isPotentialScam = false,
|
||||
super.groups,
|
||||
this.id = 0,
|
||||
this.walletName,
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: mint.toLowerCase(),
|
||||
@@ -63,27 +32,21 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
required String mint,
|
||||
required String symbol,
|
||||
required String mintAddress,
|
||||
required int decimal,
|
||||
String? iconPath,
|
||||
bool isPotentialScam = false,
|
||||
}) {
|
||||
return SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: 0,
|
||||
mint: mint,
|
||||
iconPath: iconPath,
|
||||
isPotentialScam: isPotentialScam,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: decimal,
|
||||
mint: mint,
|
||||
iconPath: iconPath,
|
||||
isPotentialScam: isPotentialScam,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled})
|
||||
SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName})
|
||||
: name = other.name,
|
||||
symbol = other.symbol,
|
||||
mintAddress = other.mintAddress,
|
||||
@@ -93,6 +56,8 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
tag = tag ?? other.tag,
|
||||
iconPath = icon ?? other.iconPath,
|
||||
isPotentialScam = other.isPotentialScam,
|
||||
id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
super(
|
||||
title: other.symbol.toUpperCase(),
|
||||
name: other.symbol.toLowerCase(),
|
||||
@@ -101,10 +66,129 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
tag: other.tag,
|
||||
iconPath: icon,
|
||||
isPotentialScam: other.isPotentialScam,
|
||||
groups: other.groups,
|
||||
);
|
||||
|
||||
static const typeId = SPL_TOKEN_TYPE_ID;
|
||||
static const boxName = 'SPLTokens';
|
||||
SPLToken.fromMap(Map<String, Object?> map)
|
||||
: this(
|
||||
name: map["name"] as String? ?? "",
|
||||
symbol: map["symbol"] as String? ?? "",
|
||||
mintAddress: map["mintAddress"] as String? ?? "",
|
||||
decimal: (map["decimal"] ?? 0) as int,
|
||||
mint: map["mint"] as String? ?? "",
|
||||
enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
|
||||
iconPath: map["iconPath"] as String?,
|
||||
tag: map["tag"] as String?,
|
||||
isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
|
||||
id: (map[selfIdColumn] ?? 0) as int,
|
||||
walletName: map["walletName"] as String?,
|
||||
);
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
final String mintAddress;
|
||||
|
||||
final int decimal;
|
||||
|
||||
bool _enabled;
|
||||
|
||||
final String mint;
|
||||
|
||||
@override
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
bool isPotentialScam;
|
||||
|
||||
int id;
|
||||
String? walletName;
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"mintAddress": mintAddress,
|
||||
"decimal": decimal,
|
||||
"mint": mint,
|
||||
"enabled": _enabled ? 1 : 0,
|
||||
"iconPath": iconPath,
|
||||
"tag": tag,
|
||||
"isPotentialScam": isPotentialScam ? 1 : 0,
|
||||
};
|
||||
|
||||
static String get tableName => "SPLToken";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
Future<int> save() async {
|
||||
if (walletName == null) {
|
||||
throw StateError("SPLToken.save() requires walletName to be set");
|
||||
}
|
||||
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<SPLToken>> selectList(String where, List<dynamic> whereArgs,
|
||||
{String? orderBy}) async {
|
||||
orderBy ??= selfIdColumn;
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => SPLToken.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<List<SPLToken>> getAllForWallet(String walletName) async =>
|
||||
selectList("walletName = ?", [walletName]);
|
||||
|
||||
static Future<SPLToken?> getByMint(String walletName, String mintAddress) async {
|
||||
final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
static Future<int> deleteForWallet(String walletName, String mintAddress) => db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND mintAddress = ?",
|
||||
whereArgs: [walletName, mintAddress],
|
||||
);
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName) =>
|
||||
db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
|
||||
|
||||
static Future<void> renameWallet(String oldName, String newName) async {
|
||||
await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
|
||||
await db!
|
||||
.update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
@@ -113,4 +197,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
|
||||
@override
|
||||
int get hashCode => mintAddress.hashCode;
|
||||
|
||||
@override
|
||||
String get apiString => "sol.$mintAddress";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import "package:cw_core/cake_hive.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/hive_type_ids.dart";
|
||||
import "package:cw_core/spl_token.dart" as spl_new;
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:hive/hive.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
part "spl_token_legacy.part.dart";
|
||||
|
||||
Future<void> performSplTokenHiveMigration() async {
|
||||
try {
|
||||
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
|
||||
CakeHive.registerAdapter(SPLTokenAdapter());
|
||||
}
|
||||
|
||||
final wallets = await WalletInfo.getAll();
|
||||
await SPLToken.migrateAllToSqlite(wallets);
|
||||
} catch (e) {
|
||||
printV("Error performing SPLToken Hive migration: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
// @HiveType(typeId: SPLToken.typeId)
|
||||
class SPLToken extends HiveObject {
|
||||
SPLToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.mintAddress,
|
||||
required this.decimal,
|
||||
required this.mint,
|
||||
this.iconPath,
|
||||
this.tag = "SOL",
|
||||
bool enabled = true,
|
||||
this.isPotentialScam = false,
|
||||
}) : _enabled = enabled;
|
||||
// @HiveField(0)
|
||||
final String name;
|
||||
|
||||
// @HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
// @HiveField(2)
|
||||
final String mintAddress;
|
||||
|
||||
// @HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
// @HiveField(5)
|
||||
final String mint;
|
||||
|
||||
// @HiveField(6)
|
||||
final String? iconPath;
|
||||
|
||||
// @HiveField(7)
|
||||
final String? tag;
|
||||
|
||||
// @HiveField(8, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static const typeId = SPL_TOKEN_TYPE_ID;
|
||||
static const boxName = "SPLTokens";
|
||||
|
||||
static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
|
||||
final sanitizedToRawNames = <String, Set<String>>{};
|
||||
for (final wallet in wallets) {
|
||||
// Hash Bags: solana is removed, so no wallet has tokens to migrate.
|
||||
continue;
|
||||
|
||||
sanitizedToRawNames
|
||||
.putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
|
||||
.add(wallet.name);
|
||||
}
|
||||
|
||||
for (final entry in sanitizedToRawNames.entries) {
|
||||
final tokenBoxName = "${entry.key}_$boxName";
|
||||
try {
|
||||
if (!await CakeHive.boxExists(tokenBoxName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<SPLToken>(tokenBoxName);
|
||||
|
||||
for (final key in box.keys.toList()) {
|
||||
final token = box.get(key);
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final rawName in entry.value) {
|
||||
await token.migrateToSqlite(walletName: rawName);
|
||||
}
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating spl token box $tokenBoxName: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite({required String walletName}) async {
|
||||
final row = spl_new.SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: decimal,
|
||||
mint: mint,
|
||||
enabled: _enabled,
|
||||
iconPath: iconPath,
|
||||
tag: tag,
|
||||
isPotentialScam: isPotentialScam,
|
||||
walletName: walletName,
|
||||
).toMap();
|
||||
row[spl_new.SPLToken.selfIdColumn] = null;
|
||||
|
||||
await db!.insert(
|
||||
spl_new.SPLToken.tableName,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of "spl_token_legacy.dart";
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class SPLTokenAdapter extends TypeAdapter<SPLToken> {
|
||||
@override
|
||||
final int typeId = 16;
|
||||
|
||||
@override
|
||||
SPLToken read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return SPLToken(
|
||||
name: fields[0] as String,
|
||||
symbol: fields[1] as String,
|
||||
mintAddress: fields[2] as String,
|
||||
decimal: fields[3] as int,
|
||||
mint: fields[5] as String,
|
||||
iconPath: fields[6] as String?,
|
||||
tag: fields[7] as String?,
|
||||
isPotentialScam: fields[8] == null ? false : fields[8] as bool,
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SPLToken obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.symbol)
|
||||
..writeByte(2)
|
||||
..write(obj.mintAddress)
|
||||
..writeByte(3)
|
||||
..write(obj.decimal)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.mint)
|
||||
..writeByte(6)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(7)
|
||||
..write(obj.tag)
|
||||
..writeByte(8)
|
||||
..write(obj.isPotentialScam);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SPLTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
|
||||
abstract class TransactionInfo extends Object with Keyable {
|
||||
late String id;
|
||||
late String txHash = id;
|
||||
late int amount;
|
||||
int? fee;
|
||||
late Money amount;
|
||||
Money? fee;
|
||||
late TransactionDirection direction;
|
||||
late bool isPending;
|
||||
late DateTime date;
|
||||
int? height;
|
||||
int confirmations = 0;
|
||||
String amountFormatted();
|
||||
String fiatAmount();
|
||||
String? feeFormatted();
|
||||
void changeFiatAmount(String amount);
|
||||
String? to;
|
||||
String? from;
|
||||
String? evmSignatureName;
|
||||
@@ -26,4 +24,8 @@ abstract class TransactionInfo extends Object with Keyable {
|
||||
dynamic get keyIndex => id;
|
||||
|
||||
Map<String, dynamic> additionalInfo = {};
|
||||
|
||||
String? _fiatAmount;
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
}
|
||||
|
||||
+135
-42
@@ -1,41 +1,8 @@
|
||||
// ignore_for_file: annotate_overrides, overridden_fields
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'tron_token.g.dart';
|
||||
|
||||
@HiveType(typeId: TronToken.typeId)
|
||||
class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
@HiveField(2)
|
||||
final String contractAddress;
|
||||
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
@HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
@HiveField(6)
|
||||
final String? tag;
|
||||
|
||||
@HiveField(7, defaultValue: false)
|
||||
final bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class TronToken extends CryptoCurrency {
|
||||
TronToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
@@ -43,8 +10,10 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
this.tag = 'TRX',
|
||||
this.tag = "TRX",
|
||||
this.isPotentialScam = false,
|
||||
this.id = 0,
|
||||
this.walletName,
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: symbol.toLowerCase(),
|
||||
@@ -55,9 +24,27 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
decimals: decimal,
|
||||
isPotentialScam: isPotentialScam,
|
||||
);
|
||||
TronToken.fromMap(Map<String, Object?> map)
|
||||
: this(
|
||||
name: map["name"] as String? ?? "",
|
||||
symbol: map["symbol"] as String? ?? "",
|
||||
contractAddress: map["contractAddress"] as String? ?? "",
|
||||
decimal: (map["decimal"] ?? 0) as int,
|
||||
enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
|
||||
iconPath: map["iconPath"] as String?,
|
||||
tag: map["tag"] as String?,
|
||||
isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
|
||||
id: (map[selfIdColumn] ?? 0) as int,
|
||||
walletName: map["walletName"] as String?,
|
||||
);
|
||||
|
||||
TronToken.copyWith(TronToken other, {String? icon, String? tag, bool? enabled})
|
||||
: name = other.name,
|
||||
TronToken.copyWith(
|
||||
TronToken other, {
|
||||
String? icon,
|
||||
String? tag,
|
||||
bool? enabled,
|
||||
String? walletName,
|
||||
}) : name = other.name,
|
||||
symbol = other.symbol,
|
||||
contractAddress = other.contractAddress,
|
||||
decimal = other.decimal,
|
||||
@@ -65,6 +52,8 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
tag = tag ?? other.tag,
|
||||
iconPath = icon ?? other.iconPath,
|
||||
isPotentialScam = other.isPotentialScam,
|
||||
id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
super(
|
||||
name: other.name,
|
||||
title: other.symbol.toUpperCase(),
|
||||
@@ -74,9 +63,110 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
decimals: other.decimal,
|
||||
isPotentialScam: other.isPotentialScam,
|
||||
);
|
||||
@override
|
||||
final String name;
|
||||
|
||||
static const typeId = TRON_TOKEN_TYPE_ID;
|
||||
static const boxName = 'TronTokens';
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
final bool isPotentialScam;
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
int id;
|
||||
bool _enabled;
|
||||
final int decimal;
|
||||
String? walletName;
|
||||
final String contractAddress;
|
||||
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"contractAddress": contractAddress,
|
||||
"decimal": decimal,
|
||||
"enabled": _enabled ? 1 : 0,
|
||||
"iconPath": iconPath,
|
||||
"tag": tag,
|
||||
"isPotentialScam": isPotentialScam ? 1 : 0,
|
||||
};
|
||||
|
||||
static String get tableName => "TronToken";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
Future<int> save() async {
|
||||
if (walletName == null) {
|
||||
throw StateError("TronToken.save() requires walletName to be set");
|
||||
}
|
||||
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<TronToken>> selectList(
|
||||
String where,
|
||||
List<dynamic> whereArgs, {
|
||||
String? orderBy,
|
||||
}) async {
|
||||
orderBy ??= selfIdColumn;
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => TronToken.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<List<TronToken>> getAllForWallet(String walletName) =>
|
||||
selectList("walletName = ?", [walletName]);
|
||||
|
||||
static Future<TronToken?> getByContract(String walletName, String contractAddress) async {
|
||||
final list =
|
||||
await selectList("walletName = ? AND contractAddress = ?", [walletName, contractAddress]);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
static Future<int> deleteForWallet(String walletName, String contractAddress) => db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND contractAddress = ?",
|
||||
whereArgs: [walletName, contractAddress],
|
||||
);
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName) =>
|
||||
db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
|
||||
|
||||
static Future<void> renameWallet(String oldName, String newName) async {
|
||||
await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
|
||||
await db!
|
||||
.update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
@@ -85,4 +175,7 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
|
||||
@override
|
||||
int get hashCode => contractAddress.hashCode;
|
||||
|
||||
@override
|
||||
String get apiString => "trx.$contractAddress";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import "package:cw_core/cake_hive.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/hive_type_ids.dart";
|
||||
import "package:cw_core/tron_token.dart" as tron_new;
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:hive/hive.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
part "tron_token_legacy.part.dart";
|
||||
|
||||
Future<void> performTronTokenHiveMigration() async {
|
||||
try {
|
||||
if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
|
||||
CakeHive.registerAdapter(TronTokenAdapter());
|
||||
}
|
||||
|
||||
final wallets = await WalletInfo.getAll();
|
||||
await TronToken.migrateAllToSqlite(wallets);
|
||||
} catch (e) {
|
||||
printV("Error performing TronToken Hive migration: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
// @HiveType(typeId: TronToken.typeId)
|
||||
class TronToken extends HiveObject {
|
||||
TronToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.contractAddress,
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
this.tag = "TRX",
|
||||
this.isPotentialScam = false,
|
||||
}) : _enabled = enabled;
|
||||
// @HiveField(0)
|
||||
final String name;
|
||||
|
||||
// @HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
// @HiveField(2)
|
||||
final String contractAddress;
|
||||
|
||||
// @HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
// @HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
// @HiveField(6)
|
||||
final String? tag;
|
||||
|
||||
// @HiveField(7, defaultValue: false)
|
||||
final bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static const typeId = TRON_TOKEN_TYPE_ID;
|
||||
static const boxName = "TronTokens";
|
||||
|
||||
static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
|
||||
final sanitizedToRawNames = <String, Set<String>>{};
|
||||
for (final wallet in wallets) {
|
||||
// Hash Bags: tron is removed, so no wallet has tokens to migrate.
|
||||
continue;
|
||||
|
||||
sanitizedToRawNames
|
||||
.putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
|
||||
.add(wallet.name);
|
||||
}
|
||||
|
||||
for (final entry in sanitizedToRawNames.entries) {
|
||||
final tokenBoxName = "${entry.key}_$boxName";
|
||||
try {
|
||||
if (!await CakeHive.boxExists(tokenBoxName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<TronToken>(tokenBoxName);
|
||||
|
||||
for (final key in box.keys.toList()) {
|
||||
final token = box.get(key);
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final rawName in entry.value) {
|
||||
await token.migrateToSqlite(walletName: rawName);
|
||||
}
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating tron token box $tokenBoxName: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite({required String walletName}) async {
|
||||
final row = tron_new.TronToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal,
|
||||
enabled: _enabled,
|
||||
iconPath: iconPath,
|
||||
tag: tag,
|
||||
isPotentialScam: isPotentialScam,
|
||||
walletName: walletName,
|
||||
).toMap();
|
||||
row[tron_new.TronToken.selfIdColumn] = null;
|
||||
|
||||
await db!.insert(
|
||||
tron_new.TronToken.tableName,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of "tron_token_legacy.dart";
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class TronTokenAdapter extends TypeAdapter<TronToken> {
|
||||
@override
|
||||
final int typeId = 18;
|
||||
|
||||
@override
|
||||
TronToken read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return TronToken(
|
||||
name: fields[0] as String,
|
||||
symbol: fields[1] as String,
|
||||
contractAddress: fields[2] as String,
|
||||
decimal: fields[3] as int,
|
||||
iconPath: fields[5] as String?,
|
||||
tag: fields[6] as String?,
|
||||
isPotentialScam: fields[7] == null ? false : fields[7] as bool,
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, TronToken obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.symbol)
|
||||
..writeByte(2)
|
||||
..write(obj.contractAddress)
|
||||
..writeByte(3)
|
||||
..write(obj.decimal)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(6)
|
||||
..write(obj.tag)
|
||||
..writeByte(7)
|
||||
..write(obj.isPotentialScam);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TronTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/unspent_comparable_mixin.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'unspent_coins_info.g.dart';
|
||||
part 'unspent_coins_info.part.dart';
|
||||
|
||||
@HiveType(typeId: UnspentCoinsInfo.typeId)
|
||||
// @HiveType(typeId: UnspentCoinsInfo.typeId)
|
||||
class UnspentCoinsInfo extends HiveObject with UnspentComparable {
|
||||
UnspentCoinsInfo({
|
||||
required this.walletId,
|
||||
@@ -25,40 +25,40 @@ class UnspentCoinsInfo extends HiveObject with UnspentComparable {
|
||||
static const boxName = 'Unspent';
|
||||
static const boxKey = 'unspentBoxKey';
|
||||
|
||||
@HiveField(0, defaultValue: '')
|
||||
// @HiveField(0, defaultValue: '')
|
||||
String walletId;
|
||||
|
||||
@HiveField(1, defaultValue: '')
|
||||
// @HiveField(1, defaultValue: '')
|
||||
String hash;
|
||||
|
||||
@HiveField(2, defaultValue: false)
|
||||
// @HiveField(2, defaultValue: false)
|
||||
bool isFrozen;
|
||||
|
||||
@HiveField(3, defaultValue: false)
|
||||
// @HiveField(3, defaultValue: false)
|
||||
bool isSending;
|
||||
|
||||
@HiveField(4)
|
||||
// @HiveField(4)
|
||||
String? noteRaw;
|
||||
|
||||
@HiveField(5, defaultValue: '')
|
||||
// @HiveField(5, defaultValue: '')
|
||||
String address;
|
||||
|
||||
@HiveField(6, defaultValue: 0)
|
||||
// @HiveField(6, defaultValue: 0)
|
||||
int value;
|
||||
|
||||
@HiveField(7, defaultValue: 0)
|
||||
// @HiveField(7, defaultValue: 0)
|
||||
int vout;
|
||||
|
||||
@HiveField(8, defaultValue: null)
|
||||
// @HiveField(8, defaultValue: null)
|
||||
String? keyImage;
|
||||
|
||||
@HiveField(9, defaultValue: false)
|
||||
// @HiveField(9, defaultValue: false)
|
||||
bool isChange;
|
||||
|
||||
@HiveField(10, defaultValue: 0)
|
||||
// @HiveField(10, defaultValue: 0)
|
||||
int accountIndex;
|
||||
|
||||
@HiveField(11, defaultValue: false)
|
||||
// @HiveField(11, defaultValue: false)
|
||||
bool? isSilentPayment;
|
||||
|
||||
String get note => noteRaw ?? '';
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'unspent_coins_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class UnspentCoinsInfoAdapter extends TypeAdapter<UnspentCoinsInfo> {
|
||||
@override
|
||||
final int typeId = 9;
|
||||
|
||||
@override
|
||||
UnspentCoinsInfo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return UnspentCoinsInfo(
|
||||
walletId: fields[0] == null ? '' : fields[0] as String,
|
||||
hash: fields[1] == null ? '' : fields[1] as String,
|
||||
isFrozen: fields[2] == null ? false : fields[2] as bool,
|
||||
isSending: fields[3] == null ? false : fields[3] as bool,
|
||||
noteRaw: fields[4] as String?,
|
||||
address: fields[5] == null ? '' : fields[5] as String,
|
||||
vout: fields[7] == null ? 0 : fields[7] as int,
|
||||
value: fields[6] == null ? 0 : fields[6] as int,
|
||||
keyImage: fields[8] as String?,
|
||||
isChange: fields[9] == null ? false : fields[9] as bool,
|
||||
accountIndex: fields[10] == null ? 0 : fields[10] as int,
|
||||
isSilentPayment: fields[11] == null ? false : fields[11] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UnspentCoinsInfo obj) {
|
||||
writer
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.walletId)
|
||||
..writeByte(1)
|
||||
..write(obj.hash)
|
||||
..writeByte(2)
|
||||
..write(obj.isFrozen)
|
||||
..writeByte(3)
|
||||
..write(obj.isSending)
|
||||
..writeByte(4)
|
||||
..write(obj.noteRaw)
|
||||
..writeByte(5)
|
||||
..write(obj.address)
|
||||
..writeByte(6)
|
||||
..write(obj.value)
|
||||
..writeByte(7)
|
||||
..write(obj.vout)
|
||||
..writeByte(8)
|
||||
..write(obj.keyImage)
|
||||
..writeByte(9)
|
||||
..write(obj.isChange)
|
||||
..writeByte(10)
|
||||
..write(obj.accountIndex)
|
||||
..writeByte(11)
|
||||
..write(obj.isSilentPayment);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UnspentCoinsInfoAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
+128
-13
@@ -1,21 +1,84 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/key.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cake_backup/backup.dart' as cwb;
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
Future<void> write({required String path, required String password, required String data}) async =>
|
||||
writeData(path: path, password: password, data: data);
|
||||
const _ivEncodedStringLength = 12;
|
||||
|
||||
Future<void> writeData(
|
||||
{required String path, required String password, required String data}) async {
|
||||
final keys = extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypted = await encode(key: key, iv: iv, data: data);
|
||||
final f = File(path);
|
||||
f.writeAsStringSync(encrypted);
|
||||
Future<void> write({
|
||||
required String path,
|
||||
required String password,
|
||||
required String data,
|
||||
bool? highEntropyPassphrase,
|
||||
}) async {
|
||||
await _writeXChaCha20(
|
||||
path: path,
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: highEntropyPassphrase ?? !Platform.isLinux,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> read({required String path, required String password}) async {
|
||||
Future<String> read({
|
||||
required String path,
|
||||
required String password,
|
||||
bool? highEntropyPassphrase,
|
||||
}) async {
|
||||
final useHighEntropy = highEntropyPassphrase ?? !Platform.isLinux;
|
||||
try {
|
||||
return await _readXChaCha20(path: path, password: password);
|
||||
} catch (e) {
|
||||
final encrypted = await File(path).readAsBytes();
|
||||
if (encrypted.isNotEmpty &&
|
||||
(encrypted[0] == cwb.lowEntropyVersion || encrypted[0] == cwb.highEntropyVersion)) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final String data;
|
||||
try {
|
||||
data = await _readLegacy(path: path, password: password);
|
||||
} catch (_) {
|
||||
throw Exception('Failed to decrypt legacy file: invalid password or corrupted data');
|
||||
}
|
||||
if (data.isEmpty) {
|
||||
throw Exception('Failed to read data');
|
||||
}
|
||||
|
||||
// Salsa20 is unauthenticated, so a wrong password decrypts to garbage instead of
|
||||
// failing. Every payload written here is a JSON object with at least one key, so a
|
||||
// correctly decrypted file always starts with '{"' and anything else is a bad
|
||||
// password.
|
||||
// There's 1 in 2^16 chance that this will be a false positive, but check later
|
||||
// prevents any damage to the file caused by re-encryption.
|
||||
if (!data.startsWith('{"')) {
|
||||
throw Exception('Failed to decrypt legacy file: invalid password or corrupted data');
|
||||
}
|
||||
|
||||
if (_isJson(data)) {
|
||||
await _writeXChaCha20(
|
||||
path: path,
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: useHighEntropy,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isJson(String data) {
|
||||
try {
|
||||
json.decode(data);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _readLegacy({required String path, required String password}) async {
|
||||
final file = File(path);
|
||||
|
||||
if (!file.existsSync()) {
|
||||
@@ -24,5 +87,57 @@ Future<String> read({required String path, required String password}) async {
|
||||
|
||||
final encrypted = file.readAsStringSync();
|
||||
|
||||
return decode(password: password, data: encrypted);
|
||||
return _decode(password: password, data: encrypted);
|
||||
}
|
||||
|
||||
Future<void> _writeXChaCha20({
|
||||
required String path,
|
||||
required String password,
|
||||
required String data,
|
||||
required bool highEntropyPassphrase,
|
||||
}) async {
|
||||
final encrypted = await cwb.encrypt(
|
||||
password,
|
||||
Uint8List.fromList(utf8.encode(data)),
|
||||
highEntropyPassphrase: highEntropyPassphrase,
|
||||
);
|
||||
|
||||
final tmpFile = File('$path.tmp');
|
||||
tmpFile.writeAsBytesSync(encrypted, flush: true);
|
||||
tmpFile.renameSync(path);
|
||||
}
|
||||
|
||||
Future<String> _readXChaCha20({
|
||||
required String path,
|
||||
required String password,
|
||||
}) async {
|
||||
final encrypted = await File(path).readAsBytes();
|
||||
final bytes = await cwb.decrypt(password, encrypted);
|
||||
return _decodeUtf8(bytes);
|
||||
}
|
||||
|
||||
// Linux wallets written by the old XChaCha20EncryptionFileUtils stored one byte per
|
||||
// UTF-16 code unit, so bytes above 0x7F in those files are Latin-1 rather than UTF-8.
|
||||
String _decodeUtf8(Uint8List bytes) {
|
||||
try {
|
||||
return utf8.decode(bytes);
|
||||
} on FormatException {
|
||||
return String.fromCharCodes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _extractKeys(String key) {
|
||||
final k = key.substring(0, key.length - _ivEncodedStringLength);
|
||||
final iv = key.substring(key.length - _ivEncodedStringLength);
|
||||
|
||||
return [k, iv];
|
||||
}
|
||||
|
||||
Future<String> _decode({required String password, required String data}) async {
|
||||
final keys = _extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
|
||||
|
||||
return encrypter.decrypt64(data, iv: iv);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const _ipfsGateway = "https://ipfs.io/ipfs/";
|
||||
const _ipfsScheme = "ipfs://";
|
||||
|
||||
String? tryNormalizeIpfsUrl(String? url) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url.toLowerCase().startsWith(_ipfsScheme)) {
|
||||
return url.toLowerCase().startsWith("https://") ? url : null;
|
||||
}
|
||||
|
||||
var cid = url.substring(_ipfsScheme.length);
|
||||
|
||||
// Some minters write ipfs://ipfs/<cid>, which would double the gateway path.
|
||||
if (cid.toLowerCase().startsWith("ipfs/")) {
|
||||
cid = cid.substring(5);
|
||||
}
|
||||
|
||||
if (cid.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "$_ipfsGateway$cid";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Control characters and bidi overrides let a mint's own metadata fake extra
|
||||
// lines or reverse the text around it wherever the name is shown.
|
||||
final _unsafeDisplayCharacters = RegExp(
|
||||
"[\u0000-\u001F\u007F\u00AD\u034F\u061C\u115F\u1160\u180E\u200B\u200E\u200F"
|
||||
"\u2028\u2029\u202A-\u202E\u2060-\u2064\u2066-\u2069\u3164\uFEFF\uFFA0]"
|
||||
"|[\u{E0000}-\u{E007F}]",
|
||||
unicode: true,
|
||||
);
|
||||
|
||||
const _maxDisplayTextLength = 512;
|
||||
|
||||
String? sanitizeNFTText(String? text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final cleaned = text.replaceAll(_unsafeDisplayCharacters, " ").trim();
|
||||
final runes = cleaned.runes;
|
||||
|
||||
if (runes.length <= _maxDisplayTextLength) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
return "${String.fromCharCodes(runes.take(_maxDisplayTextLength))}\u2026";
|
||||
}
|
||||
@@ -71,11 +71,12 @@ abstract class WalletAddresses {
|
||||
Future<void> saveAddressesInBox() async {
|
||||
try {
|
||||
walletInfo.address = address;
|
||||
walletInfo.setAddresses(addressesMap);
|
||||
walletInfo.setAddressInfos(addressInfos);
|
||||
walletInfo.setUsedAddresses(usedAddresses.toList());
|
||||
walletInfo.setHiddenAddresses(hiddenAddresses.toList());
|
||||
walletInfo.setManualAddresses(manualAddresses.toList());
|
||||
// TODO: check if it will affect the performance of each wallet
|
||||
await walletInfo.setAddresses(addressesMap);
|
||||
await walletInfo.setAddressInfos(addressInfos);
|
||||
await walletInfo.setUsedAddresses(usedAddresses.toList());
|
||||
await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
|
||||
await walletInfo.setManualAddresses(manualAddresses.toList());
|
||||
|
||||
await walletInfo.save();
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "package:cw_core/receive_page_option.dart";
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
@@ -12,6 +13,7 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
|
||||
abstract class WalletBase<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
|
||||
TransactionType extends TransactionInfo> {
|
||||
@@ -43,8 +45,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
|
||||
ObservableMap<CryptoCurrency, BalanceType> get balance;
|
||||
|
||||
String formatCryptoAmount(String amount) => amount;
|
||||
|
||||
SyncStatus get syncStatus;
|
||||
|
||||
set syncStatus(SyncStatus status);
|
||||
@@ -117,7 +117,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
|
||||
|
||||
Future<void> renameWalletFiles(String newWalletName);
|
||||
Future<void> renameWalletFiles(String newWalletName) =>
|
||||
copyWalletFilesTo(fromName: walletInfo.name, toName: newWalletName, type: type);
|
||||
|
||||
Future<String> signMessage(String message, {String? address = null});
|
||||
|
||||
@@ -136,4 +137,19 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
/// Each wallet implementation should override this to make a single, efficient call
|
||||
/// Returns true if the node is healthy, false otherwise
|
||||
Future<bool> checkNodeHealth();
|
||||
|
||||
bool get hasPayjoinSupport => false;
|
||||
bool get hasLightningSupport => false;
|
||||
bool get hasSilentPaymentsScanning => false;
|
||||
|
||||
bool get hasCoinControl => false;
|
||||
bool get hasAccountsSupport => false;
|
||||
bool get hasNativeAccounts => false;
|
||||
bool get canToggleMultiAccounts => hasAccountsSupport && !hasNativeAccounts;
|
||||
|
||||
// hardware wallet - bitbox, ledger, trezor.
|
||||
// we also have airgap wallets but those can't sign messages
|
||||
bool get canSignMessages => walletInfo.hardwareWalletType == null || walletInfo.isHardwareWallet;
|
||||
|
||||
bool receiveOptionAvailable(ReceivePageOption option) => true;
|
||||
}
|
||||
|
||||
+235
-57
@@ -44,7 +44,7 @@ enum HardwareWalletType {
|
||||
coldcard,
|
||||
seedsigner,
|
||||
keystone,
|
||||
trezor,
|
||||
trezor;
|
||||
}
|
||||
|
||||
enum WalletInfoAddressType {
|
||||
@@ -244,6 +244,68 @@ class WalletInfoAddress {
|
||||
}
|
||||
}
|
||||
|
||||
class WalletInfoAccount {
|
||||
WalletInfoAccount({
|
||||
required this.walletInfoId,
|
||||
required this.accountIndex,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
factory WalletInfoAccount.fromJson(Map<String, dynamic> json) => WalletInfoAccount(
|
||||
walletInfoId: json["walletInfoId"] as int,
|
||||
accountIndex: json["accountIndex"] as int,
|
||||
label: json["label"] as String,
|
||||
);
|
||||
|
||||
int walletInfoId;
|
||||
int accountIndex;
|
||||
String label;
|
||||
|
||||
static String get tableName => "walletInfoAccount";
|
||||
|
||||
static Future<List<WalletInfoAccount>> selectList(int walletInfoId) async {
|
||||
final query = await db!.query(
|
||||
tableName,
|
||||
where: "walletInfoId = ?",
|
||||
whereArgs: [walletInfoId],
|
||||
orderBy: "accountIndex ASC",
|
||||
);
|
||||
|
||||
return List.generate(query.length, (index) => WalletInfoAccount.fromJson(query[index]));
|
||||
}
|
||||
|
||||
static Future<int> deleteByWalletInfoId(int walletInfoId) async =>
|
||||
await db!.delete(tableName, where: "walletInfoId = ?", whereArgs: [walletInfoId]);
|
||||
|
||||
static Future<int> insertOrUpdate({
|
||||
required int walletInfoId,
|
||||
required int accountIndex,
|
||||
required String label,
|
||||
}) async =>
|
||||
await db!.transaction((txn) async {
|
||||
final updated = await txn.update(
|
||||
tableName,
|
||||
{"label": label},
|
||||
where: "walletInfoId = ? AND accountIndex = ?",
|
||||
whereArgs: [walletInfoId, accountIndex],
|
||||
);
|
||||
|
||||
if (updated > 0) return updated;
|
||||
|
||||
return await txn.insert(tableName, {
|
||||
"walletInfoId": walletInfoId,
|
||||
"accountIndex": accountIndex,
|
||||
"label": label,
|
||||
});
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"walletInfoId": walletInfoId,
|
||||
"accountIndex": accountIndex,
|
||||
"label": label,
|
||||
};
|
||||
}
|
||||
|
||||
class DerivationInfo {
|
||||
DerivationInfo({
|
||||
this.id = 0,
|
||||
@@ -346,58 +408,73 @@ class WalletInfo {
|
||||
this.hashedWalletIdentifier,
|
||||
this.isNonSeedWallet,
|
||||
this.sortOrder,
|
||||
this.currentAccountIndex,
|
||||
this.addressPageType,
|
||||
this.receiveInfoboxDismissed,
|
||||
this.showCombinedBalance,
|
||||
this.favoriteTokenAddress)
|
||||
this.favoriteTokenAddress,
|
||||
this.showSeedBackupReminder)
|
||||
: _yatLastUsedAddressController = StreamController<String>.broadcast();
|
||||
|
||||
factory WalletInfo.external(
|
||||
{required String id,
|
||||
required String name,
|
||||
required WalletType type,
|
||||
required bool isRecovery,
|
||||
required int restoreHeight,
|
||||
required DateTime date,
|
||||
required String dirPath,
|
||||
required String path,
|
||||
required String address,
|
||||
bool? showIntroCakePayCard,
|
||||
String yatEid = '',
|
||||
String yatLastUsedAddressRaw = '',
|
||||
int? derivationInfoId,
|
||||
HardwareWalletType? hardwareWalletType,
|
||||
String? parentAddress,
|
||||
String? hashedWalletIdentifier,
|
||||
bool? isNonSeedWallet,
|
||||
int? sortOrder,
|
||||
bool? receiveInfoboxDismissed,
|
||||
bool? showCombinedBalance,
|
||||
String? favoriteTokenAddress}) {
|
||||
return WalletInfo(
|
||||
0,
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
isRecovery,
|
||||
restoreHeight,
|
||||
date.millisecondsSinceEpoch,
|
||||
dirPath,
|
||||
path,
|
||||
address,
|
||||
yatEid,
|
||||
yatLastUsedAddressRaw,
|
||||
showIntroCakePayCard,
|
||||
derivationInfoId ?? -1,
|
||||
hardwareWalletType,
|
||||
parentAddress,
|
||||
hashedWalletIdentifier,
|
||||
isNonSeedWallet ?? false,
|
||||
sortOrder ?? 0,
|
||||
null,
|
||||
receiveInfoboxDismissed ?? false,
|
||||
showCombinedBalance ?? true,
|
||||
favoriteTokenAddress);
|
||||
factory WalletInfo.external({
|
||||
required String id,
|
||||
required String name,
|
||||
required WalletType type,
|
||||
required bool isRecovery,
|
||||
required int restoreHeight,
|
||||
required DateTime date,
|
||||
required String dirPath,
|
||||
required String path,
|
||||
required String address,
|
||||
bool? showIntroCakePayCard,
|
||||
String yatEid = "",
|
||||
String yatLastUsedAddressRaw = "",
|
||||
int? derivationInfoId,
|
||||
HardwareWalletType? hardwareWalletType,
|
||||
String? parentAddress,
|
||||
String? hashedWalletIdentifier,
|
||||
bool? isNonSeedWallet,
|
||||
int? sortOrder,
|
||||
int currentAccountIndex = 0,
|
||||
bool? receiveInfoboxDismissed,
|
||||
bool? showCombinedBalance,
|
||||
String? favoriteTokenAddress,
|
||||
}) {
|
||||
final wi = WalletInfo(
|
||||
0,
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
isRecovery,
|
||||
restoreHeight,
|
||||
date.millisecondsSinceEpoch,
|
||||
dirPath,
|
||||
path,
|
||||
address,
|
||||
yatEid.isNotEmpty ? yatEid : null,
|
||||
yatLastUsedAddressRaw.isNotEmpty ? yatLastUsedAddressRaw : null,
|
||||
showIntroCakePayCard ?? false,
|
||||
derivationInfoId ?? -1,
|
||||
hardwareWalletType,
|
||||
parentAddress,
|
||||
hashedWalletIdentifier,
|
||||
isNonSeedWallet ?? false,
|
||||
sortOrder ?? 0,
|
||||
currentAccountIndex,
|
||||
null,
|
||||
receiveInfoboxDismissed ?? false,
|
||||
showCombinedBalance ?? true,
|
||||
favoriteTokenAddress?.isNotEmpty == true ? favoriteTokenAddress : null,
|
||||
false,
|
||||
);
|
||||
|
||||
if (type == WalletType.bitcoin) {
|
||||
wi.isMultiAccountsEnabled = false;
|
||||
} else if (type == WalletType.monero || type == WalletType.wownero) {
|
||||
wi.isMultiAccountsEnabled = true;
|
||||
}
|
||||
|
||||
return wi;
|
||||
}
|
||||
|
||||
static String get tableName => 'walletInfo';
|
||||
@@ -406,6 +483,8 @@ class WalletInfo {
|
||||
|
||||
int internalId;
|
||||
|
||||
int currentAccountIndex;
|
||||
|
||||
String id;
|
||||
String name;
|
||||
WalletType type;
|
||||
@@ -418,6 +497,7 @@ class WalletInfo {
|
||||
bool receiveInfoboxDismissed;
|
||||
bool showCombinedBalance;
|
||||
String? favoriteTokenAddress;
|
||||
bool showSeedBackupReminder;
|
||||
|
||||
Future<Map<String, String>> getAddresses() async {
|
||||
final list = await WalletInfoAddressMap.selectList(internalId);
|
||||
@@ -426,11 +506,9 @@ class WalletInfo {
|
||||
|
||||
Future<void> setAddresses(Map<String, String> addresses) async {
|
||||
await WalletInfoAddressMap.deleteByWalletInfoId(internalId);
|
||||
final keys = addresses.keys.toList();
|
||||
final values = addresses.values.toList();
|
||||
// ToDo: check why the addresses list gets changed half way through
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
await WalletInfoAddressMap.insert(internalId, keys[i], values[i]);
|
||||
final entries = addresses.entries.toList();
|
||||
for (final entry in entries) {
|
||||
await WalletInfoAddressMap.insert(internalId, entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,8 +583,76 @@ class WalletInfo {
|
||||
await WalletInfoAddress.insert(internalId, type, address);
|
||||
}
|
||||
|
||||
Future<List<WalletInfoAccount>> getAccounts() async {
|
||||
var accounts = await WalletInfoAccount.selectList(internalId);
|
||||
|
||||
if (accounts.isEmpty) {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: 0,
|
||||
label: "Primary account",
|
||||
);
|
||||
|
||||
accounts = await WalletInfoAccount.selectList(internalId);
|
||||
}
|
||||
|
||||
if (!accounts.any((a) => a.accountIndex == currentAccountIndex)) {
|
||||
currentAccountIndex = accounts.first.accountIndex;
|
||||
await save();
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
Future<void> setSelectedAccount(int accountIndex) async {
|
||||
currentAccountIndex = accountIndex;
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> setAccounts(List<WalletInfoAccount> accounts) async {
|
||||
await WalletInfoAccount.deleteByWalletInfoId(internalId);
|
||||
|
||||
for (final account in accounts) {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: account.accountIndex,
|
||||
label: account.label,
|
||||
);
|
||||
}
|
||||
|
||||
if (accounts.isNotEmpty && !accounts.any((a) => a.accountIndex == currentAccountIndex)) {
|
||||
currentAccountIndex = accounts.first.accountIndex;
|
||||
await save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addAccount({
|
||||
required int accountIndex,
|
||||
required String label,
|
||||
}) async {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: accountIndex,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> renameAccount({
|
||||
required int accountIndex,
|
||||
required String label,
|
||||
}) async {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: accountIndex,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
String? addressPageType;
|
||||
String? network;
|
||||
int? accountDiscoveryLimit;
|
||||
bool? isMultiAccountsEnabled;
|
||||
|
||||
int derivationInfoId;
|
||||
DerivationInfo? _derivationInfo;
|
||||
|
||||
@@ -582,14 +728,20 @@ class WalletInfo {
|
||||
"hashedWalletIdentifier": hashedWalletIdentifier,
|
||||
"isNonSeedWallet": isNonSeedWallet ? 1 : 0,
|
||||
"sortOrder": sortOrder,
|
||||
"currentAccountIndex": currentAccountIndex,
|
||||
"addressPageType": addressPageType,
|
||||
"receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
|
||||
"showCombinedBalance": showCombinedBalance ? 1 : 0,
|
||||
"favoriteTokenAddress": favoriteTokenAddress
|
||||
"favoriteTokenAddress": favoriteTokenAddress,
|
||||
"showSeedBackupReminder": showSeedBackupReminder ? 1 : 0,
|
||||
"network": network,
|
||||
"accountDiscoveryLimit": accountDiscoveryLimit,
|
||||
"isMultiAccountsEnabled":
|
||||
isMultiAccountsEnabled == null ? null : (isMultiAccountsEnabled! ? 1 : 0),
|
||||
};
|
||||
|
||||
factory WalletInfo.fromJson(Map<String, dynamic> json) {
|
||||
return WalletInfo(
|
||||
final info = WalletInfo(
|
||||
json[selfIdColumn] as int,
|
||||
json['id'] as String,
|
||||
json['name'] as String,
|
||||
@@ -611,10 +763,18 @@ class WalletInfo {
|
||||
json['hashedWalletIdentifier'] as String?,
|
||||
(json['isNonSeedWallet'] as int) == 1,
|
||||
json['sortOrder'] as int? ?? 0,
|
||||
json['currentAccountIndex'] as int? ?? 0,
|
||||
json['addressPageType'] as String? ?? null,
|
||||
json['receiveInfoboxDismissed'] != 0,
|
||||
json["showCombinedBalance"] != 0,
|
||||
json["favoriteTokenAddress"] as String? ?? null);
|
||||
json["favoriteTokenAddress"] as String? ?? null,
|
||||
json["showSeedBackupReminder"] == 1);
|
||||
info.network = json['network'] as String?;
|
||||
info.accountDiscoveryLimit = json['accountDiscoveryLimit'] as int?;
|
||||
final rawIsMultiAccountsEnabled = json['isMultiAccountsEnabled'];
|
||||
info.isMultiAccountsEnabled =
|
||||
rawIsMultiAccountsEnabled == null ? null : (rawIsMultiAccountsEnabled as int) == 1;
|
||||
return info;
|
||||
}
|
||||
|
||||
Future<int> save() async {
|
||||
@@ -630,7 +790,8 @@ class WalletInfo {
|
||||
}
|
||||
|
||||
static Future<int> delete(WalletInfo walletInfo) async {
|
||||
return await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
|
||||
final deleted = await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
static Future<List<WalletInfo>> selectList(String where, List<dynamic> whereArgs,
|
||||
@@ -660,4 +821,21 @@ class WalletInfo {
|
||||
restoreHeight = height;
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> setAccountDiscoveryLimit(int limit) async {
|
||||
accountDiscoveryLimit = limit;
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> clearSeedBackupReminder() async {
|
||||
final previous = showSeedBackupReminder;
|
||||
showSeedBackupReminder = false;
|
||||
|
||||
try {
|
||||
await save();
|
||||
} catch (e) {
|
||||
showSeedBackupReminder = previous;
|
||||
printV("Failed to save the seed backup reminder flag: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,10 +268,13 @@ class WalletInfo extends HiveObject {
|
||||
hashedWalletIdentifier,
|
||||
isNonSeedWallet,
|
||||
0,
|
||||
0,
|
||||
addressPageType,
|
||||
false,
|
||||
true,
|
||||
null);
|
||||
null,
|
||||
false)
|
||||
..network = network;
|
||||
final wiId = await walletInfo.save();
|
||||
for (final address in usedAddresses ?? <String>[]) {
|
||||
await newWi.WalletInfoAddress.insert(wiId, newWi.WalletInfoAddressType.used, address);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/imported_nft.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/utils/file.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_keys_file.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
|
||||
RFK extends WalletCredentials, RFH extends WalletCredentials> {
|
||||
@@ -25,7 +30,38 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
||||
|
||||
Future<void> remove(String wallet);
|
||||
|
||||
Future<void> rename(String currentName, String password, String newName);
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
if (currentName == newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentWalletInfo = await WalletInfo.get(currentName, getType());
|
||||
if (currentWalletInfo == null) {
|
||||
throw Exception("Wallet not found");
|
||||
}
|
||||
|
||||
await copyWalletFilesTo(fromName: currentName, toName: newName, type: getType());
|
||||
await saveBackup(newName);
|
||||
|
||||
currentWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
currentWalletInfo.name = newName;
|
||||
await currentWalletInfo.save();
|
||||
|
||||
await _renameTokenRows(currentName, newName);
|
||||
|
||||
final oldDir = Directory(p.join(await pathForWalletTypeDir(type: getType()), currentName));
|
||||
if (oldDir.existsSync()) {
|
||||
try {
|
||||
await oldDir.delete(recursive: true);
|
||||
} catch (e) {
|
||||
printV('rename: failed to delete old wallet dir "$currentName": $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _renameTokenRows(String currentName, String newName) async {
|
||||
|
||||
}
|
||||
|
||||
Future<void> restoreWalletFilesFromBackup(String name) async {
|
||||
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
|
||||
@@ -47,18 +83,25 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
||||
|
||||
Future<String> getSeeds(String name, String password, WalletType type) async {
|
||||
try {
|
||||
final encryption = encryptionFileUtilsFor(Platform.isLinux);
|
||||
|
||||
if (await WalletKeysFile.hasKeysFile(name, type)) {
|
||||
final keysData = await WalletKeysFile.readKeysFile(name, type, password, encryption);
|
||||
return keysData.mnemonic ?? keysData.altMnemonic ?? keysData.privateKey ?? '';
|
||||
}
|
||||
|
||||
final path = await pathForWallet(name: name, type: type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final jsonSource = await encryption.read(path: path, password: password);
|
||||
try {
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
return data['mnemonic'] as String? ?? '';
|
||||
return data["mnemonic"] as String? ?? "";
|
||||
} catch (_) {
|
||||
// if not a valid json
|
||||
return jsonSource.substring(0, 200);
|
||||
}
|
||||
} catch (_) {
|
||||
// if the file couldn't be opened or read
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'wallet_type.g.dart';
|
||||
part 'wallet_type.part.dart';
|
||||
|
||||
const walletTypes = [
|
||||
WalletType.monero,
|
||||
@@ -35,52 +36,56 @@ const evmWalletTypes = [
|
||||
WalletType.bsc
|
||||
];
|
||||
|
||||
// HiveField IDs are pinned — never renumber. Existing user wallets persist
|
||||
// type via the int ID, so changing assignments here would re-interpret saved
|
||||
// wallet types on next launch. To remove a type, drop its enum entry; do not
|
||||
// re-use its old ID.
|
||||
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
|
||||
// Two encodings are persisted; never change either.
|
||||
// - The HiveField IDs are the bytes WalletTypeAdapter (wallet_type.part.dart) reads when legacy
|
||||
// Hive data is migrated. To remove a type, drop it; never reuse its ID.
|
||||
// - WalletType.index is what the SQLite wallet table stores (wallet_info.dart), so the enum
|
||||
// ORDER is persisted as well: only ever append. Our order differs from upstream's, so a merge
|
||||
// must never take upstream's enum.
|
||||
// hive_generator no longer runs (upstream froze the adapters into *.part.dart); the annotations
|
||||
// below are kept as the record of the IDs.
|
||||
// @HiveType(typeId: WALLET_TYPE_TYPE_ID)
|
||||
enum WalletType {
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
monero,
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
none,
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
bitcoin,
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
litecoin,
|
||||
|
||||
@HiveField(5)
|
||||
// @HiveField(5)
|
||||
ethereum,
|
||||
|
||||
@HiveField(6)
|
||||
// @HiveField(6)
|
||||
nano,
|
||||
|
||||
@HiveField(7)
|
||||
// @HiveField(7)
|
||||
banano,
|
||||
|
||||
@HiveField(8)
|
||||
// @HiveField(8)
|
||||
bitcoinCash,
|
||||
|
||||
@HiveField(9)
|
||||
// @HiveField(9)
|
||||
polygon,
|
||||
|
||||
@HiveField(12)
|
||||
// @HiveField(12)
|
||||
wownero,
|
||||
|
||||
@HiveField(15)
|
||||
// @HiveField(15)
|
||||
dogecoin,
|
||||
|
||||
@HiveField(16)
|
||||
// @HiveField(16)
|
||||
base,
|
||||
|
||||
@HiveField(17)
|
||||
// @HiveField(17)
|
||||
arbitrum,
|
||||
|
||||
@HiveField(19)
|
||||
// @HiveField(19)
|
||||
bsc,
|
||||
}
|
||||
|
||||
@@ -183,38 +188,39 @@ String walletTypeToString(WalletType type) {
|
||||
}
|
||||
}
|
||||
|
||||
String walletTypeToDisplayName(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return 'Monero (XMR)';
|
||||
case WalletType.bitcoin:
|
||||
return 'Bitcoin (BTC)';
|
||||
case WalletType.litecoin:
|
||||
return 'Litecoin (LTC)';
|
||||
case WalletType.ethereum:
|
||||
return 'Ethereum (ETH)';
|
||||
case WalletType.bitcoinCash:
|
||||
return 'Bitcoin Cash (BCH)';
|
||||
case WalletType.nano:
|
||||
return 'Nano (XNO)';
|
||||
case WalletType.banano:
|
||||
return 'Banano (BAN)';
|
||||
case WalletType.polygon:
|
||||
return 'Polygon (POL)';
|
||||
case WalletType.wownero:
|
||||
return 'Wownero (WOW)';
|
||||
case WalletType.dogecoin:
|
||||
return 'Dogecoin (DOGE)';
|
||||
case WalletType.base:
|
||||
return 'Base';
|
||||
case WalletType.arbitrum:
|
||||
return 'Arbitrum (ARB)';
|
||||
case WalletType.bsc:
|
||||
return 'BNB Smart Chain (BNB)';
|
||||
case WalletType.none:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
String walletTypeToDisplayName(WalletType type) => switch (type) {
|
||||
WalletType.monero => 'Monero',
|
||||
WalletType.bitcoin => 'Bitcoin',
|
||||
WalletType.litecoin => 'Litecoin',
|
||||
WalletType.ethereum => 'Ethereum',
|
||||
WalletType.bitcoinCash => 'Bitcoin Cash',
|
||||
WalletType.nano => 'Nano',
|
||||
WalletType.banano => 'Banano',
|
||||
WalletType.polygon => 'Polygon',
|
||||
WalletType.wownero => 'Wownero',
|
||||
WalletType.dogecoin => 'Dogecoin',
|
||||
WalletType.base => 'Base',
|
||||
WalletType.arbitrum => 'Arbitrum',
|
||||
WalletType.bsc => 'BNB Smart Chain',
|
||||
WalletType.none => ''
|
||||
};
|
||||
|
||||
String walletTypeToDisplayTicker(WalletType type) => switch (type) {
|
||||
WalletType.monero => 'XMR',
|
||||
WalletType.bitcoin => 'BTC',
|
||||
WalletType.litecoin => 'LTC',
|
||||
WalletType.ethereum => 'ETH',
|
||||
WalletType.bitcoinCash => 'BCH',
|
||||
WalletType.nano => 'XNO',
|
||||
WalletType.banano => 'BAN',
|
||||
WalletType.polygon => 'POL',
|
||||
WalletType.wownero => 'WOW',
|
||||
WalletType.dogecoin => 'DOGE',
|
||||
WalletType.base => '',
|
||||
WalletType.arbitrum => 'ARB',
|
||||
WalletType.bsc => 'BNB',
|
||||
WalletType.none => ''
|
||||
};
|
||||
|
||||
WalletType? _cryptoCurrencyToWalletType(CryptoCurrency type) {
|
||||
switch (type) {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'wallet_type.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class WalletTypeAdapter extends TypeAdapter<WalletType> {
|
||||
@override
|
||||
final int typeId = 5;
|
||||
|
||||
@override
|
||||
WalletType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return WalletType.monero;
|
||||
case 1:
|
||||
return WalletType.none;
|
||||
case 2:
|
||||
return WalletType.bitcoin;
|
||||
case 3:
|
||||
return WalletType.litecoin;
|
||||
case 5:
|
||||
return WalletType.ethereum;
|
||||
case 6:
|
||||
return WalletType.nano;
|
||||
case 7:
|
||||
return WalletType.banano;
|
||||
case 8:
|
||||
return WalletType.bitcoinCash;
|
||||
case 9:
|
||||
return WalletType.polygon;
|
||||
case 12:
|
||||
return WalletType.wownero;
|
||||
case 15:
|
||||
return WalletType.dogecoin;
|
||||
case 16:
|
||||
return WalletType.base;
|
||||
case 17:
|
||||
return WalletType.arbitrum;
|
||||
case 19:
|
||||
return WalletType.bsc;
|
||||
default:
|
||||
return WalletType.monero;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, WalletType obj) {
|
||||
switch (obj) {
|
||||
case WalletType.monero:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case WalletType.none:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
case WalletType.litecoin:
|
||||
writer.writeByte(3);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
writer.writeByte(5);
|
||||
break;
|
||||
case WalletType.nano:
|
||||
writer.writeByte(6);
|
||||
break;
|
||||
case WalletType.banano:
|
||||
writer.writeByte(7);
|
||||
break;
|
||||
case WalletType.bitcoinCash:
|
||||
writer.writeByte(8);
|
||||
break;
|
||||
case WalletType.polygon:
|
||||
writer.writeByte(9);
|
||||
break;
|
||||
case WalletType.wownero:
|
||||
writer.writeByte(12);
|
||||
break;
|
||||
case WalletType.dogecoin:
|
||||
writer.writeByte(15);
|
||||
break;
|
||||
case WalletType.base:
|
||||
writer.writeByte(16);
|
||||
break;
|
||||
case WalletType.arbitrum:
|
||||
writer.writeByte(17);
|
||||
break;
|
||||
case WalletType.bsc:
|
||||
writer.writeByte(19);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is WalletTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/wownero_amount_format.dart';
|
||||
|
||||
class WowneroBalance extends Balance {
|
||||
WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0})
|
||||
: formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance),
|
||||
super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance);
|
||||
|
||||
final int fullBalance;
|
||||
final int unlockedBalance;
|
||||
final int frozenBalance;
|
||||
final String formattedFrozenBalance;
|
||||
|
||||
@override
|
||||
String get formattedUnAvailableBalance =>
|
||||
formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance;
|
||||
}
|
||||
+15
-15
@@ -4,38 +4,38 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'zano_asset.g.dart';
|
||||
part 'zano_asset.part.dart';
|
||||
|
||||
@HiveType(typeId: ZanoAsset.typeId)
|
||||
// @HiveType(typeId: ZanoAsset.typeId)
|
||||
class ZanoAsset extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
final String fullName;
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
final String ticker;
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
final String assetId;
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
final int decimalPoint;
|
||||
@HiveField(4, defaultValue: true)
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
@HiveField(5)
|
||||
// @HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
// @HiveField(6)
|
||||
// final String? tag;
|
||||
@HiveField(6)
|
||||
// @HiveField(6)
|
||||
final String owner;
|
||||
@HiveField(7)
|
||||
// @HiveField(7)
|
||||
final String metaInfo;
|
||||
@HiveField(8)
|
||||
// @HiveField(8)
|
||||
final BigInt currentSupply;
|
||||
@HiveField(9)
|
||||
// @HiveField(9)
|
||||
final bool hiddenSupply;
|
||||
@HiveField(10)
|
||||
// @HiveField(10)
|
||||
final BigInt totalMaxSupply;
|
||||
@HiveField(11)
|
||||
// @HiveField(11)
|
||||
final bool isInGlobalWhitelist;
|
||||
@HiveField(12, defaultValue: null)
|
||||
// @HiveField(12, defaultValue: null)
|
||||
final Map<String, dynamic>? info;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'zano_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ZanoAssetAdapter extends TypeAdapter<ZanoAsset> {
|
||||
@override
|
||||
final int typeId = 22;
|
||||
|
||||
@override
|
||||
ZanoAsset read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ZanoAsset(
|
||||
fullName: fields[0] as String,
|
||||
ticker: fields[1] as String,
|
||||
assetId: fields[2] as String,
|
||||
decimalPoint: fields[3] as int,
|
||||
iconPath: fields[5] as String?,
|
||||
owner: fields[6] as String,
|
||||
metaInfo: fields[7] as String,
|
||||
currentSupply: fields[8] as BigInt,
|
||||
hiddenSupply: fields[9] as bool,
|
||||
totalMaxSupply: fields[10] as BigInt,
|
||||
isInGlobalWhitelist: fields[11] as bool,
|
||||
info: (fields[12] as Map?)?.cast<String, dynamic>(),
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ZanoAsset obj) {
|
||||
writer
|
||||
..writeByte(13)
|
||||
..writeByte(0)
|
||||
..write(obj.fullName)
|
||||
..writeByte(1)
|
||||
..write(obj.ticker)
|
||||
..writeByte(2)
|
||||
..write(obj.assetId)
|
||||
..writeByte(3)
|
||||
..write(obj.decimalPoint)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(6)
|
||||
..write(obj.owner)
|
||||
..writeByte(7)
|
||||
..write(obj.metaInfo)
|
||||
..writeByte(8)
|
||||
..write(obj.currentSupply)
|
||||
..writeByte(9)
|
||||
..write(obj.hiddenSupply)
|
||||
..writeByte(10)
|
||||
..write(obj.totalMaxSupply)
|
||||
..writeByte(11)
|
||||
..write(obj.isInGlobalWhitelist)
|
||||
..writeByte(12)
|
||||
..write(obj.info);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ZanoAssetAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,911 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: "1c296cd268f486cabcc3930e9b93a8133169305f18d722916e675959a88f6d2c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.9"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
bech32:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "05755063b593aa6cca0a4820a318e0ce17de6192"
|
||||
resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192"
|
||||
url: "https://github.com/cake-tech/bech32.git"
|
||||
source: git
|
||||
version: "0.2.2"
|
||||
blockchain_utils:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: cake-update-v2
|
||||
resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57"
|
||||
url: "https://github.com/cake-tech/blockchain_utils"
|
||||
source: git
|
||||
version: "3.3.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.15"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.10.1"
|
||||
cake_backup:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "3aba867dcab6737f6707782f5db15d71f303db38"
|
||||
resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38"
|
||||
url: "https://github.com/cake-tech/cake_backup.git"
|
||||
source: git
|
||||
version: "1.0.0+1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
cryptography:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cryptography
|
||||
sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
cupertino_icons:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.8"
|
||||
decimal:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: decimal
|
||||
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
encrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: encrypt
|
||||
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_mobx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_mobx
|
||||
sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
hive_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: hive_generator
|
||||
sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mobx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mobx
|
||||
sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
mobx_codegen:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mobx_codegen
|
||||
sha256: e0abbbc651a69550440f6b65c99ec222a1e2a4afd7baec8ba0f3088c7ca582a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
on_chain:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "096865a8c6b89c260beadfec04f7e184c40a3273"
|
||||
resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273"
|
||||
url: "https://github.com/cake-tech/on_chain.git"
|
||||
source: git
|
||||
version: "3.7.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.17"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.1"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
rational:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rational
|
||||
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
socks5_proxy:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "27ad7c2efae8d7460325c74b90f660085cbd0685"
|
||||
resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685"
|
||||
url: "https://github.com/LacticWhale/socks_dart"
|
||||
source: git
|
||||
version: "2.1.0"
|
||||
socks_socket:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: e6232c53c1595469931ababa878759a067c02e94
|
||||
resolved-ref: e6232c53c1595469931ababa878759a067c02e94
|
||||
url: "https://github.com/sneurlax/socks_socket"
|
||||
source: git
|
||||
version: "1.1.1"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4+6"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4+4"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1+1"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.40"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0+3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
torch_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../scripts/torch_dart"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: tuple
|
||||
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
unorm_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: unorm_dart
|
||||
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
@@ -14,16 +14,15 @@ dependencies:
|
||||
http: ^1.1.0
|
||||
file: ^7.0.0
|
||||
path_provider: ^2.1.5
|
||||
mobx: ^2.0.7+4
|
||||
flutter_mobx: ^2.0.6+1
|
||||
mobx: ^2.6.0
|
||||
flutter_mobx: ^2.3.0
|
||||
intl: any
|
||||
encrypt: ^5.0.1
|
||||
decimal: ^2.3.3
|
||||
cake_backup:
|
||||
git:
|
||||
url: https://github.com/cake-tech/cake_backup.git
|
||||
ref: 3aba867dcab6737f6707782f5db15d71f303db38
|
||||
version: 1.0.0
|
||||
ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
|
||||
socks5_proxy:
|
||||
git:
|
||||
url: https://github.com/LacticWhale/socks_dart
|
||||
@@ -42,7 +41,7 @@ dependencies:
|
||||
blockchain_utils:
|
||||
git:
|
||||
url: https://github.com/cake-tech/blockchain_utils
|
||||
ref: cake-update-v2
|
||||
ref: 59fdf29d72068e0522a96a8953ed7272833a9f57
|
||||
sqflite: ^2.4.1
|
||||
sqflite_common_ffi: ^2.3.4+4
|
||||
bech32:
|
||||
@@ -54,10 +53,10 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.15
|
||||
build_resolvers: ^2.4.4
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^2.0.1
|
||||
build_runner: ^2.15.0
|
||||
mobx_codegen: ^2.7.7
|
||||
hive: ^2.2.3
|
||||
# hive_generator: ^2.0.1
|
||||
|
||||
dependency_overrides:
|
||||
watcher: ^1.1.0
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../pubspec_overrides.yaml
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:cw_core/amount/amount_sanitizer.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('smartAmountSanitizer', () {
|
||||
test('sanitize decimal dot', () {
|
||||
final amount = "1,000.00".sanitized();
|
||||
expect(amount, "1000.00");
|
||||
});
|
||||
|
||||
test('sanitize decimal comma', () {
|
||||
final amount = "1.000,00".sanitized();
|
||||
expect(amount, "1000.00");
|
||||
});
|
||||
|
||||
test('general case', () {
|
||||
final amount = "1000.00".sanitized();
|
||||
expect(amount, "1000.00");
|
||||
});
|
||||
|
||||
test('no decimals', () {
|
||||
final amount = "1,000".sanitized();
|
||||
expect(amount, "1.000");
|
||||
});
|
||||
|
||||
test('no decimals', () {
|
||||
final amount = "1.000".sanitized();
|
||||
expect(amount, "1.000");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import 'package:cw_core/amount/exchange_rate.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
void main() {
|
||||
group('ExchangeRate', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000", EUR),
|
||||
);
|
||||
|
||||
test('convert base currency to quote currency: 1 BTC to 60.000 EUR', () {
|
||||
final result = rate.convert(Money.parse('1', CryptoCurrency.btc));
|
||||
expect(result, Money.parse("60000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert base currency to quote currency: 0.5 BTC to 30.000 EUR', () {
|
||||
final result = rate.convert(Money.parse('0.5', CryptoCurrency.btc));
|
||||
expect(result, Money.parse("30000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert quote currency to base currency: 60.000 EUR to 1 BTC', () {
|
||||
final result = rate.convert(Money.parse("60000.00", EUR));
|
||||
expect(result, Money.parse('1', CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('convert quote currency to base currency: 30 EUR to 0.0005 BTC', () {
|
||||
final result = rate.convert(Money.parse("30.00", EUR));
|
||||
expect(result, Money.parse('0.0005', CryptoCurrency.btc));
|
||||
});
|
||||
});
|
||||
|
||||
group('Edge Cases', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000", EUR),
|
||||
);
|
||||
|
||||
test('Converting zero turns into zero', () {
|
||||
expect(rate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR));
|
||||
expect(rate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('truncate to smallest unit', () {
|
||||
final result = rate.convert(Money(BigInt.one, CryptoCurrency.btc)); // 1 sat
|
||||
expect(result, Money.zero(EUR));
|
||||
});
|
||||
|
||||
test('Truncate base unit of base currency', () {
|
||||
final result = rate.convert(Money(BigInt.one, EUR));
|
||||
expect(result, Money.fromInt(16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('No int overflow with large numbers', () {
|
||||
final allBtc = Money.parse('21000000', CryptoCurrency.btc);
|
||||
final result = rate.convert(allBtc);
|
||||
expect(result, Money(BigInt.from(126000000000000), EUR));
|
||||
});
|
||||
|
||||
test('truncation for negative amounts', () {
|
||||
final result = rate.convert(Money.fromInt(-1, EUR));
|
||||
expect(result, Money.fromInt(-16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('Base currency without decimals places: JPY', () {
|
||||
final rate = ExchangeRate(
|
||||
base: JPY,
|
||||
quote: Money.fromInt(1, EUR),
|
||||
);
|
||||
|
||||
expect(rate.convert(Money.fromInt(500, JPY)), Money.fromInt(500, EUR));
|
||||
expect(rate.convert(Money.fromInt(500, EUR)), Money.fromInt(500, JPY));
|
||||
});
|
||||
|
||||
test('Base currency with 18 decimals places: ETH', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.eth,
|
||||
quote: Money.fromInt(300000, EUR),
|
||||
);
|
||||
|
||||
expect(rate.convert(Money.parse('1', CryptoCurrency.eth)), Money.fromInt(300000, EUR));
|
||||
expect(rate.convert(Money.fromInt(3000, EUR)), Money.parse('0.01', CryptoCurrency.eth));
|
||||
});
|
||||
|
||||
test('Smalles base unit is less than smalles base unit of quote', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.eth,
|
||||
quote: Money.fromInt(300000, EUR),
|
||||
);
|
||||
expect(rate.convert(Money(BigInt.one, CryptoCurrency.eth)), Money(BigInt.zero, EUR));
|
||||
});
|
||||
|
||||
test('Cannot convert currency outside of pair', () {
|
||||
expect(() => rate.convert(Money.parse("60000", USD)), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('handle quote being 0 correctly', () {
|
||||
final zeroRate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money(BigInt.zero, EUR, EUR.decimals),
|
||||
);
|
||||
expect(zeroRate.convert(Money.fromInt(100, CryptoCurrency.btc)), Money(BigInt.zero, EUR));
|
||||
expect(zeroRate.convert(Money.fromInt(100, EUR)), Money(BigInt.zero, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('handle super low quote correctly', () {
|
||||
final zeroRate = ExchangeRate(
|
||||
base: CryptoCurrency.shib,
|
||||
quote: Money.parse("0.00000444", EUR, strictParsing: false),
|
||||
);
|
||||
|
||||
expect(
|
||||
zeroRate.convert(Money.parse("1", CryptoCurrency.shib)),
|
||||
Money.parse("0.00000444", EUR, strictParsing: false),
|
||||
);
|
||||
expect(
|
||||
zeroRate.convert(Money.fromInt(10000, EUR)),
|
||||
Money.parse("22522522.522522522522522522", CryptoCurrency.shib),
|
||||
);
|
||||
|
||||
expect(
|
||||
zeroRate.convert(Money.parse("200", CryptoCurrency.shib)),
|
||||
Money.parse("0.000888", EUR, strictParsing: false),
|
||||
);
|
||||
});
|
||||
|
||||
group('Precision and scale', () {
|
||||
final rate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("60000", EUR));
|
||||
|
||||
test('convert is insensitive to the input scale (forward)', () {
|
||||
// Same value, three different internal scales.
|
||||
final coarse = Money(BigInt.one, CryptoCurrency.btc, 0);
|
||||
final normal = Money.parse("1", CryptoCurrency.btc);
|
||||
final fine = Money.parse("1.000000000000000000", CryptoCurrency.btc, strictParsing: false);
|
||||
|
||||
expect(rate.convert(coarse), Money.parse("60000.00", EUR));
|
||||
expect(rate.convert(normal), Money.parse("60000.00", EUR));
|
||||
expect(rate.convert(fine), Money.parse("60000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert is insensitive to the input scale (reverse)', () {
|
||||
expect(
|
||||
rate.convert(Money(BigInt.one, EUR, 0)), // €1 at scale 0
|
||||
rate.convert(Money.parse("1", EUR)), // €1 at scale 2
|
||||
);
|
||||
});
|
||||
|
||||
test('sub-unit precision in the input does not leak into the result', () {
|
||||
// ad = 18 here, well past BTC's 8. The divisor must cancel the *amount's*
|
||||
// scale, not the base currency's.
|
||||
final overPrecise =
|
||||
Money.parse("1.000000000000000001", CryptoCurrency.btc, strictParsing: false);
|
||||
|
||||
expect(rate.convert(overPrecise), Money.parse("60000.00", EUR));
|
||||
});
|
||||
|
||||
test('the result carries the quote scale, not the currency default', () {
|
||||
final preciseRate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000.123456", EUR, strictParsing: false),
|
||||
);
|
||||
|
||||
final result = preciseRate.convert(Money.parse("1", CryptoCurrency.btc));
|
||||
|
||||
expect(result.decimals, 6);
|
||||
expect(result, Money.parse("60000.123456", EUR, strictParsing: false));
|
||||
});
|
||||
|
||||
test('a quote scale finer than the currency survives the round trip', () {
|
||||
final preciseRate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000.50", EUR, strictParsing: false),
|
||||
);
|
||||
|
||||
expect(
|
||||
preciseRate.convert(preciseRate.convert(Money.parse("1", CryptoCurrency.btc))),
|
||||
Money.parse("1", CryptoCurrency.btc),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Truncation', () {
|
||||
final rate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("60000", EUR));
|
||||
|
||||
test('truncates rather than rounding, even past the halfway point', () {
|
||||
// 9 sats = 0.54 cents. Rounding would give 1.
|
||||
expect(rate.convert(Money(BigInt.from(9), CryptoCurrency.btc)), Money.zero(EUR));
|
||||
// 1 cent = 16.66 sats. Rounding would give 17.
|
||||
expect(rate.convert(Money(BigInt.one, EUR)), Money.fromInt(16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('truncates toward zero for negatives, not downward', () {
|
||||
// -0.54 cents must be 0, not -1.
|
||||
expect(rate.convert(Money(BigInt.from(-9), CryptoCurrency.btc)), Money.zero(EUR));
|
||||
expect(rate.convert(Money(BigInt.from(-1), EUR)), Money.fromInt(-16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('truncation is symmetric around zero', () {
|
||||
final positive = rate.convert(Money(BigInt.from(7), EUR));
|
||||
final negative = rate.convert(Money(BigInt.from(-7), EUR));
|
||||
|
||||
expect(negative, -positive);
|
||||
});
|
||||
|
||||
test('round trip loses less than one quote base unit', () {
|
||||
final original = Money.parse("0.12345678", CryptoCurrency.btc);
|
||||
final back = rate.convert(rate.convert(original));
|
||||
final oneQuoteUnitInBaseUnits =
|
||||
BigInt.from(10).pow(CryptoCurrency.btc.decimals) ~/ rate.quote.amount;
|
||||
|
||||
expect(
|
||||
(original - back).amount.abs(),
|
||||
lessThanOrEqualTo(oneQuoteUnitInBaseUnits + BigInt.one),
|
||||
);
|
||||
});
|
||||
|
||||
test('round trip never inflates the amount', () {
|
||||
// Both truncations round toward zero, so the result can only shrink.
|
||||
for (final source in ["0.12345678", "1", "0.5", "21000000"]) {
|
||||
final original = Money.parse(source, CryptoCurrency.btc);
|
||||
expect(
|
||||
rate.convert(rate.convert(original)) <= original,
|
||||
isTrue,
|
||||
reason: "round trip grew $source",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a value below one base unit of the quote collapses to zero', () {
|
||||
final back = rate.convert(rate.convert(Money(BigInt.one, CryptoCurrency.btc)));
|
||||
|
||||
expect(back.isZero, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('Extreme decimal spreads', () {
|
||||
test('30 decimals against 0 decimals: NANO/JPY', () {
|
||||
final rate = ExchangeRate(base: CryptoCurrency.nano, quote: Money.parse("150", JPY));
|
||||
|
||||
expect(rate.convert(Money.parse("1", CryptoCurrency.nano)), Money.fromInt(150, JPY));
|
||||
expect(rate.convert(Money.fromInt(150, JPY)), Money.parse("1", CryptoCurrency.nano));
|
||||
expect(rate.convert(Money.fromInt(1, JPY)).decimals, 30);
|
||||
});
|
||||
|
||||
test('0 decimals against 18 decimals: JPY/SHIB', () {
|
||||
final rate = ExchangeRate(base: JPY, quote: Money.parse("2500", CryptoCurrency.shib));
|
||||
|
||||
expect(rate.convert(Money.fromInt(2, JPY)), Money.parse("5000", CryptoCurrency.shib));
|
||||
expect(rate.convert(Money.parse("5000", CryptoCurrency.shib)), Money.fromInt(2, JPY));
|
||||
});
|
||||
|
||||
test('no overflow at the extremes', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.shib,
|
||||
quote: Money.parse("0.00000444", EUR, strictParsing: false),
|
||||
);
|
||||
final totalSupply = Money.parse("589000000000000", CryptoCurrency.shib);
|
||||
|
||||
expect(rate.convert(totalSupply).isNegative, isFalse);
|
||||
expect(rate.convert(rate.convert(totalSupply)) <= totalSupply, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('Degenerate pairs', () {
|
||||
test('identity pair returns the amount untouched', () {
|
||||
final identity = ExchangeRate(base: EUR, quote: Money.parse("1", EUR));
|
||||
|
||||
expect(identity.convert(Money.parse("42.42", EUR)), Money.parse("42.42", EUR));
|
||||
expect(identity.convert(Money.zero(EUR)), Money.zero(EUR));
|
||||
});
|
||||
|
||||
test('a same-currency pair silently ignores its own rate', () {
|
||||
final bogus = ExchangeRate(base: EUR, quote: Money.parse("2", EUR));
|
||||
expect(bogus.convert(Money.parse("10", EUR)), Money.parse("10", EUR));
|
||||
});
|
||||
|
||||
test('a rate of exactly one is a no-op in both directions', () {
|
||||
final rate = ExchangeRate(base: JPY, quote: Money.fromInt(100, EUR));
|
||||
|
||||
expect(rate.convert(Money.fromInt(7, JPY)), Money.fromInt(700, EUR));
|
||||
expect(rate.convert(Money.fromInt(700, EUR)), Money.fromInt(7, JPY));
|
||||
});
|
||||
|
||||
test('negative quotes propagate their sign', () {
|
||||
final inverted = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("-60000", EUR));
|
||||
|
||||
expect(
|
||||
inverted.convert(Money.parse("1", CryptoCurrency.btc)),
|
||||
Money.parse("-60000.00", EUR),
|
||||
);
|
||||
expect(
|
||||
inverted.convert(Money.parse("-60000.00", EUR)),
|
||||
Money.parse("1", CryptoCurrency.btc),
|
||||
);
|
||||
});
|
||||
|
||||
test('zero amount with a zero quote', () {
|
||||
final zeroRate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.zero(EUR));
|
||||
|
||||
expect(zeroRate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR));
|
||||
expect(zeroRate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('rejects a foreign currency in both slots', () {
|
||||
final rate = ExchangeRate(base: CryptoCurrency.btc, quote: Money.parse("60000", EUR));
|
||||
|
||||
expect(() => rate.convert(Money.parse("1", CryptoCurrency.xmr)), throwsArgumentError);
|
||||
expect(() => rate.convert(Money.parse("1", USD)), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/amount/money_local.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:intl/intl.dart";
|
||||
|
||||
final _btc012 = Money.fromInt(12345678, CryptoCurrency.btc); // 0.12345678 BTC
|
||||
final _btcTrailing = Money.fromInt(12000000, CryptoCurrency.btc); // 0.12 BTC
|
||||
final _btcGrouping = Money.fromInt(123450000000, CryptoCurrency.btc); // 1234.5 BTC
|
||||
final _xmrHalf = Money.fromInt(500000000000, CryptoCurrency.xmr); // 0.5 XMR
|
||||
|
||||
void main() {
|
||||
group("toLocalStringWithPrecision", () {
|
||||
test("localizes grouping (en_US)", () {
|
||||
expect(_btcGrouping.toLocalStringWithPrecision(locale: "en_US"), "1,234.5");
|
||||
});
|
||||
|
||||
test("no grouping for sub-1 values", () {
|
||||
expect(_btc012.toLocalStringWithPrecision(locale: "en_US"), "0.12345678");
|
||||
});
|
||||
|
||||
test("swaps grouping/decimal separators (de_DE)", () {
|
||||
expect(_btcGrouping.toLocalStringWithPrecision(locale: "de_DE"), "1.234,5");
|
||||
});
|
||||
|
||||
test("formats as English when intl has no number data for the locale (yo)", () {
|
||||
expect(_btcGrouping.toLocalStringWithPrecision(locale: "yo"), "1,234.5");
|
||||
});
|
||||
|
||||
test("base unit renders integer sats with grouping", () {
|
||||
expect(
|
||||
_btc012.toLocalStringWithPrecision(useBaseUnit: true, locale: "en_US"),
|
||||
"12,345,678",
|
||||
);
|
||||
});
|
||||
|
||||
test("fractionalDigits truncates (does not round)", () {
|
||||
// 0.12345678 -> 0.12 (NOT 0.13).
|
||||
expect(_btc012.toLocalStringWithPrecision(fractionalDigits: 2, locale: "en_US"), "0.12");
|
||||
});
|
||||
|
||||
test("trimZeros:false keeps trailing zeros", () {
|
||||
expect(
|
||||
_btcTrailing.toLocalStringWithPrecision(trimZeros: false, locale: "en_US"),
|
||||
"0.12000000",
|
||||
);
|
||||
});
|
||||
|
||||
test("respects a higher-precision currency (XMR)", () {
|
||||
expect(_xmrHalf.toLocalStringWithPrecision(locale: "en_US"), "0.5");
|
||||
});
|
||||
|
||||
test("falls back to the ambient Intl locale when locale is null", () {
|
||||
final previous = Intl.defaultLocale;
|
||||
addTearDown(() => Intl.defaultLocale = previous);
|
||||
Intl.defaultLocale = "de_DE";
|
||||
|
||||
// Proves the extension passes null through to NumberFormat rather than
|
||||
// hardcoding a locale.
|
||||
expect(_btcGrouping.toLocalStringWithPrecision(), "1.234,5");
|
||||
});
|
||||
});
|
||||
|
||||
group("toLocalStringWithSymbol", () {
|
||||
test("suffixes the symbol by default", () {
|
||||
expect(_btcGrouping.toLocalStringWithSymbol(locale: "en_US"), "1,234.5 BTC");
|
||||
});
|
||||
|
||||
test("prefixes the symbol when withSymbolPrefix is true", () {
|
||||
expect(
|
||||
_btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "en_US"),
|
||||
"BTC 1,234.5",
|
||||
);
|
||||
});
|
||||
|
||||
test("suffixes the base-unit ticker", () {
|
||||
expect(
|
||||
_btc012.toLocalStringWithSymbol(useBaseUnit: true, locale: "en_US"),
|
||||
"12,345,678 sats",
|
||||
);
|
||||
});
|
||||
|
||||
test("prefixes the base-unit ticker", () {
|
||||
expect(
|
||||
_btc012.toLocalStringWithSymbol(
|
||||
useBaseUnit: true,
|
||||
withSymbolPrefix: true,
|
||||
locale: "en_US",
|
||||
),
|
||||
"sats 12,345,678",
|
||||
);
|
||||
});
|
||||
|
||||
test("prefix respects de_DE separators", () {
|
||||
expect(
|
||||
_btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "de_DE"),
|
||||
"BTC 1.234,5",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the currency symbol for non-BTC", () {
|
||||
expect(_xmrHalf.toLocalStringWithSymbol(locale: "en_US"), "0.5 XMR");
|
||||
});
|
||||
|
||||
test("forwards fractionalDigits to the precision string", () {
|
||||
expect(_btc012.toLocalStringWithSymbol(fractionalDigits: 2, locale: "en_US"), "0.12 BTC");
|
||||
});
|
||||
|
||||
test("forwards trimZeros to the precision string", () {
|
||||
expect(
|
||||
_btcTrailing.toLocalStringWithSymbol(trimZeros: false, locale: "en_US"),
|
||||
"0.12000000 BTC",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group("negative amounts", () {
|
||||
test("keeps the sign when the integer part is non-zero", () {
|
||||
expect((-_btcGrouping).toLocalStringWithSymbol(locale: "en_US"), "-1,234.5 BTC");
|
||||
});
|
||||
|
||||
// NOTE: currently fails. For |amount| < 1 the sign is dropped:
|
||||
// formatFixed yields "-0.5", but _withLocalSeparator re-parses the integer
|
||||
// part and int.tryParse("-0") == 0, so the "-" is lost -> "0.5 XMR".
|
||||
// Fix in _withLocalSeparator by capturing the sign before parsing, e.g.
|
||||
// final negative = amount.startsWith("-");
|
||||
// ... then re-apply it to the formatted result.
|
||||
test("keeps the sign for sub-1 amounts", () {
|
||||
expect((-_xmrHalf).toLocalStringWithSymbol(locale: "en_US"), "-0.5 XMR");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
import "utils.dart";
|
||||
|
||||
void main() {
|
||||
group("Money", () {
|
||||
test("parse", () {
|
||||
var money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, BigInt.from(100000000));
|
||||
|
||||
money = Money.parse("-1", CryptoCurrency.btc);
|
||||
expect(money.amount, BigInt.from(-100000000));
|
||||
|
||||
money = Money.parse("1.11", CryptoCurrency.btc);
|
||||
expect(money.amount, BigInt.from(111000000));
|
||||
|
||||
money = Money.parse("-1.11", CryptoCurrency.btc);
|
||||
expect(money.amount, BigInt.from(-111000000));
|
||||
|
||||
money = Money.parse("-.11", CryptoCurrency.btc);
|
||||
expect(money.amount, BigInt.from(-11000000));
|
||||
|
||||
money = Money.parse(".11", CryptoCurrency.btc);
|
||||
expect(money.amount, BigInt.from(11000000));
|
||||
|
||||
// Invalid representation
|
||||
expect(() => Money.parse("1,11", CryptoCurrency.btc), throwsFormatException);
|
||||
|
||||
// To many decimals
|
||||
expect(() => Money.parse("-1.0000000000000001", CryptoCurrency.btc), throwsFormatException);
|
||||
|
||||
money = Money.parse("1", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money.amount, BigInt.from(1));
|
||||
|
||||
money = Money.parse("0", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money.amount, BigInt.from(0));
|
||||
|
||||
money = Money.parse("-1", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money.amount, BigInt.from(-1));
|
||||
|
||||
// canonical representation when expecting base units
|
||||
expect(() => Money.parse("1.0", CryptoCurrency.btc, isBaseUnit: true), throwsFormatException);
|
||||
});
|
||||
|
||||
test("tryParse", () {
|
||||
var money = Money.tryParse("1", CryptoCurrency.btc);
|
||||
expect(money?.amount, BigInt.from(100000000));
|
||||
|
||||
money = Money.tryParse("-1", CryptoCurrency.btc);
|
||||
expect(money?.amount, BigInt.from(-100000000));
|
||||
|
||||
money = Money.tryParse("1.11", CryptoCurrency.btc);
|
||||
expect(money?.amount, BigInt.from(111000000));
|
||||
|
||||
money = Money.tryParse("-1.11", CryptoCurrency.btc);
|
||||
expect(money?.amount, BigInt.from(-111000000));
|
||||
|
||||
money = Money.tryParse("-.11", CryptoCurrency.btc);
|
||||
expect(money?.amount, BigInt.from(-11000000));
|
||||
|
||||
money = Money.tryParse(".11", CryptoCurrency.btc);
|
||||
expect(money?.amount, BigInt.from(11000000));
|
||||
|
||||
// Invalid representation
|
||||
money = Money.tryParse("1,11", CryptoCurrency.btc);
|
||||
expect(money?.amount, isNull);
|
||||
|
||||
// To many decimals
|
||||
money = Money.tryParse("-1.0000000000000001", CryptoCurrency.btc);
|
||||
expect(money?.amount, isNull);
|
||||
|
||||
money = Money.tryParse("1", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money?.amount, BigInt.from(1));
|
||||
|
||||
money = Money.tryParse("0", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money?.amount, BigInt.from(0));
|
||||
|
||||
money = Money.tryParse("-1", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money?.amount, BigInt.from(-1));
|
||||
|
||||
// canonical representation when expecting base units
|
||||
money = Money.tryParse("1.0", CryptoCurrency.btc, isBaseUnit: true);
|
||||
expect(money?.amount, isNull);
|
||||
});
|
||||
|
||||
test("sign", () {
|
||||
expect(Money(BigInt.from(0), CryptoCurrency.btc).sign, 0);
|
||||
|
||||
expect(Money(BigInt.from(1), CryptoCurrency.btc).sign, 1);
|
||||
|
||||
expect(Money(BigInt.from(10), CryptoCurrency.btc).sign, 1);
|
||||
|
||||
expect(Money(BigInt.from(-1), CryptoCurrency.btc).sign, -1);
|
||||
|
||||
expect(Money(BigInt.from(-10), CryptoCurrency.btc).sign, -1);
|
||||
});
|
||||
|
||||
test("isZero", () {
|
||||
expect(Money(BigInt.from(0), CryptoCurrency.btc).isZero, isTrue);
|
||||
|
||||
expect(Money(BigInt.from(1), CryptoCurrency.btc).isZero, isFalse);
|
||||
|
||||
expect(Money(BigInt.from(-1), CryptoCurrency.btc).isZero, isFalse);
|
||||
});
|
||||
|
||||
test("isNegative", () {
|
||||
expect(Money(BigInt.from(0), CryptoCurrency.btc).isNegative, isFalse);
|
||||
|
||||
expect(Money(BigInt.from(1), CryptoCurrency.btc).isNegative, isFalse);
|
||||
|
||||
expect(Money(BigInt.from(-1), CryptoCurrency.btc).isNegative, isTrue);
|
||||
});
|
||||
|
||||
group("Comparison", () {
|
||||
final fourBitcoin = Money(BigInt.from(400000000), CryptoCurrency.btc);
|
||||
final fiveBitcoin = Money(BigInt.from(500000000), CryptoCurrency.btc);
|
||||
final sixBitcoin = Money(BigInt.from(600000000), CryptoCurrency.btc);
|
||||
|
||||
final fiveMonero = Money.parse("5", CryptoCurrency.xmr);
|
||||
|
||||
test("==", () {
|
||||
expect(fiveBitcoin, equals(Money(BigInt.from(500000000), CryptoCurrency.btc)));
|
||||
expect(fiveBitcoin, isNot(equals(fourBitcoin)));
|
||||
expect(fiveBitcoin, isNot(equals(sixBitcoin)));
|
||||
expect(fiveBitcoin, isNot(equals(fiveMonero)));
|
||||
// intentional check of incompatible types.
|
||||
// ignore: unrelated_type_equality_checks
|
||||
expect(fiveBitcoin == "Not money", equals(false));
|
||||
});
|
||||
|
||||
test("<", () {
|
||||
expect(fiveBitcoin < sixBitcoin, isTrue);
|
||||
expect(fiveBitcoin < fiveBitcoin, isFalse);
|
||||
expect(fiveBitcoin < fourBitcoin, isFalse);
|
||||
|
||||
// Cannot compare money in different currencies:
|
||||
expect(() => fiveBitcoin < fiveMonero, throwsArgumentError);
|
||||
});
|
||||
|
||||
test("<=", () {
|
||||
expect(fiveBitcoin <= sixBitcoin, isTrue);
|
||||
expect(fiveBitcoin <= fiveBitcoin, isTrue);
|
||||
expect(fiveBitcoin <= fourBitcoin, isFalse);
|
||||
|
||||
// Cannot compare money in different currencies:
|
||||
expect(() => fiveBitcoin <= fiveMonero, throwsArgumentError);
|
||||
});
|
||||
|
||||
test(">", () {
|
||||
expect(fiveBitcoin > fourBitcoin, isTrue);
|
||||
expect(fiveBitcoin > fiveBitcoin, isFalse);
|
||||
expect(fiveBitcoin > sixBitcoin, isFalse);
|
||||
|
||||
// Cannot compare money in different currencies:
|
||||
expect(() => fiveBitcoin > fiveMonero, throwsArgumentError);
|
||||
});
|
||||
|
||||
test(">=", () {
|
||||
expect(fiveBitcoin >= fourBitcoin, isTrue);
|
||||
expect(fiveBitcoin >= fiveBitcoin, isTrue);
|
||||
expect(fiveBitcoin >= sixBitcoin, isFalse);
|
||||
|
||||
// Cannot compare money in different currencies:
|
||||
expect(() => fiveBitcoin >= fiveMonero, throwsArgumentError);
|
||||
});
|
||||
|
||||
test("conformance to Comparable", () {
|
||||
expect(fiveBitcoin, isA<Comparable<Money>>());
|
||||
|
||||
expect(fiveBitcoin.compareTo(fiveBitcoin), isZero);
|
||||
expect(fiveBitcoin.compareTo(fourBitcoin), isPositive);
|
||||
expect(fiveBitcoin.compareTo(sixBitcoin), isNegative);
|
||||
expect(() => fiveBitcoin.compareTo(fiveMonero), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
group("Math", () {
|
||||
test("+", () {
|
||||
final oneBitcoin = Money.parse("1", CryptoCurrency.btc);
|
||||
final twoBitcoin = Money.parse("2", CryptoCurrency.btc);
|
||||
final threeBitcoin = Money.parse("3", CryptoCurrency.btc);
|
||||
|
||||
expect(oneBitcoin + twoBitcoin, equals(threeBitcoin));
|
||||
});
|
||||
|
||||
test("cannot add different currencies", () {
|
||||
final oneBitcoin = Money(BigInt.one, CryptoCurrency.btc);
|
||||
final oneMonero = Money(BigInt.one, CryptoCurrency.xmr);
|
||||
|
||||
expect(() => oneBitcoin + oneMonero, throwsArgumentError);
|
||||
});
|
||||
|
||||
test("invert money", () {
|
||||
final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
|
||||
final negativeOneSatoshi = Money(BigInt.from(-1), CryptoCurrency.btc);
|
||||
|
||||
expect(-oneSatoshi, equals(negativeOneSatoshi));
|
||||
expect(-negativeOneSatoshi, equals(oneSatoshi));
|
||||
});
|
||||
|
||||
test("-", () {
|
||||
final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
|
||||
final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc);
|
||||
final threeSatoshi = Money(BigInt.from(3), CryptoCurrency.btc);
|
||||
|
||||
expect(threeSatoshi - oneSatoshi, equals(twoSatoshi));
|
||||
});
|
||||
|
||||
test("cannot subtract different currencies", () {
|
||||
final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
|
||||
final oneMonero = Money(BigInt.one, CryptoCurrency.xmr);
|
||||
|
||||
expect(() => oneSatoshi - oneMonero, throwsArgumentError);
|
||||
});
|
||||
|
||||
test("*", () {
|
||||
final zeroSatoshi = Money(BigInt.zero, CryptoCurrency.btc);
|
||||
final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc);
|
||||
final twoSatoshi = Money(BigInt.two, CryptoCurrency.btc);
|
||||
|
||||
expect(oneSatoshi * BigInt.zero, equals(zeroSatoshi));
|
||||
expect(oneSatoshi * BigInt.two, equals(twoSatoshi));
|
||||
expect(oneSatoshi * BigInt.from(-2), equals(-twoSatoshi));
|
||||
});
|
||||
|
||||
group("/", () {
|
||||
final threeBI = BigInt.from(3);
|
||||
final fourBI = BigInt.from(4);
|
||||
|
||||
final negThreeBI = BigInt.from(-3);
|
||||
final negFourBI = BigInt.from(-4);
|
||||
|
||||
test("rounds down when fractional part is < 0.5", () {
|
||||
expect((Money(BigInt.from(10), CryptoCurrency.btc) / threeBI).amount, threeBI);
|
||||
expect((Money(BigInt.from(-10), CryptoCurrency.btc) / threeBI).amount, negThreeBI);
|
||||
expect((Money(BigInt.from(10), CryptoCurrency.btc) / negThreeBI).amount, negThreeBI);
|
||||
expect((Money(BigInt.from(-10), CryptoCurrency.btc) / negThreeBI).amount, threeBI);
|
||||
});
|
||||
|
||||
test("rounds up when fractional part is > 0.5", () {
|
||||
expect((Money(BigInt.from(11), CryptoCurrency.btc) / threeBI).amount, fourBI); // 3.66...
|
||||
expect((Money(BigInt.from(-11), CryptoCurrency.btc) / threeBI).amount, negFourBI);
|
||||
expect((Money(BigInt.from(11), CryptoCurrency.btc) / negThreeBI).amount, negFourBI);
|
||||
expect((Money(BigInt.from(-11), CryptoCurrency.btc) / negThreeBI).amount, fourBI);
|
||||
});
|
||||
|
||||
test("rounds AWAY from zero when fractional part is exactly 0.5 (Ties)", () {
|
||||
// 5 / 2 = 2.5 -> 3
|
||||
expect((Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.two).amount, threeBI);
|
||||
// -5 / 2 = -2.5 -> -3
|
||||
expect((Money(BigInt.from(-5), CryptoCurrency.btc) / BigInt.two).amount, negThreeBI);
|
||||
// 5 / -2 = -2.5 -> -3
|
||||
expect((Money(BigInt.from(5), CryptoCurrency.btc) / -BigInt.two).amount, negThreeBI);
|
||||
// -5 / -2 = 2.5 -> 3
|
||||
expect((Money(BigInt.from(-5), CryptoCurrency.btc) / -BigInt.two).amount, threeBI);
|
||||
});
|
||||
|
||||
test("handles zero numerator correctly", () {
|
||||
expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(5)).amount, BigInt.zero);
|
||||
expect((Money(BigInt.zero, CryptoCurrency.btc) / BigInt.from(-5)).amount, BigInt.zero);
|
||||
});
|
||||
|
||||
test("throws Exception on division by zero", () {
|
||||
expect(
|
||||
() => Money(BigInt.from(5), CryptoCurrency.btc) / BigInt.zero,
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test("handles very large BigInts without precision loss", () {
|
||||
// A number that would lose precision if converted to a standard double
|
||||
final hugeA = BigInt.parse("10000000000000000000000000000000000005");
|
||||
final hugeB = BigInt.parse("10");
|
||||
// 1000...005 / 10 = 1000...000.5 -> Should round up to 1000...001
|
||||
final expected = BigInt.parse("1000000000000000000000000000000000001");
|
||||
|
||||
expect((Money(hugeA, CryptoCurrency.btc) / hugeB).amount, expected);
|
||||
});
|
||||
|
||||
test("handles exact division perfectly", () {
|
||||
expect(
|
||||
(Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
|
||||
BigInt.from(5),
|
||||
);
|
||||
expect(
|
||||
(Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
|
||||
BigInt.from(-5),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("copyWith", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
|
||||
test("currency", () {
|
||||
final copy = money.copyWith(currency: CryptoCurrency.eth);
|
||||
expect(copy.amount, equals(BigInt.parse("1000000000000000000")));
|
||||
expect(copy.currency.decimals, equals(18));
|
||||
expect(copy.toString(), "1");
|
||||
});
|
||||
|
||||
test("currency nano", () {
|
||||
final copy = money.copyWith(currency: CryptoCurrency.nano);
|
||||
expect(copy.amount, equals(BigInt.parse("1000000000000000000000000000000")));
|
||||
expect(copy.currency.decimals, equals(30));
|
||||
expect(copy.toString(), "1");
|
||||
});
|
||||
|
||||
test("amount", () {
|
||||
final copy = money.copyWith(amount: BigInt.one);
|
||||
expect(copy.amount, BigInt.one);
|
||||
expect(copy.currency.decimals, 8);
|
||||
});
|
||||
|
||||
test("amount and currency", () {
|
||||
final copy = money.copyWith(amount: BigInt.one);
|
||||
expect(copy.amount, BigInt.one);
|
||||
expect(copy.currency.decimals, 8);
|
||||
});
|
||||
});
|
||||
|
||||
group("toStringWithPrecision", () {
|
||||
test("with fractionalDigits and padded with zeros", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, equals(BigInt.parse("100000000")));
|
||||
expect(money.currency.decimals, equals(8));
|
||||
expect(money.toStringWithPrecision(fractionalDigits: 5, trimZeros: false), "1.00000");
|
||||
});
|
||||
|
||||
test("using base unit sats", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, equals(BigInt.parse("100000000")));
|
||||
expect(money.currency.decimals, equals(8));
|
||||
expect(money.toStringWithPrecision(useBaseUnit: true), "100000000");
|
||||
});
|
||||
});
|
||||
|
||||
group("toStringWithSymbol", () {
|
||||
test("with fractionalDigits and padded with zeros", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, equals(BigInt.parse("100000000")));
|
||||
expect(money.currency.decimals, equals(8));
|
||||
expect(money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false), "1.00000 BTC");
|
||||
});
|
||||
|
||||
test("using base unit sats", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, equals(BigInt.parse("100000000")));
|
||||
expect(money.currency.decimals, equals(8));
|
||||
expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats");
|
||||
});
|
||||
|
||||
group("withSymbolPrefix", () {
|
||||
test("with fractionalDigits and padded with zeros", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, equals(BigInt.parse("100000000")));
|
||||
expect(money.currency.decimals, equals(8));
|
||||
expect(
|
||||
money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false, withSymbolPrefix: true),
|
||||
"BTC 1.00000",
|
||||
);
|
||||
});
|
||||
|
||||
test("using base unit sats", () {
|
||||
final money = Money.parse("1", CryptoCurrency.btc);
|
||||
expect(money.amount, equals(BigInt.parse("100000000")));
|
||||
expect(money.currency.decimals, equals(8));
|
||||
expect(
|
||||
money.toStringWithSymbol(useBaseUnit: true, withSymbolPrefix: true),
|
||||
"sats 100000000",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("different scales", () {
|
||||
final coarseOne = Money(BigInt.one, CryptoCurrency.btc, 0);
|
||||
final fineOne = Money(BigInt.from(100000000), CryptoCurrency.btc, 8);
|
||||
|
||||
test("== across scales", () {
|
||||
expect(coarseOne, equals(fineOne));
|
||||
expect(coarseOne, isNot(equals(Money(BigInt.two, CryptoCurrency.btc, 0))));
|
||||
});
|
||||
|
||||
test("equal values across scales hash equally", () {
|
||||
expect(coarseOne.hashCode, equals(fineOne.hashCode));
|
||||
|
||||
expect(
|
||||
Money(BigInt.from(-11), CryptoCurrency.btc, 1).hashCode,
|
||||
equals(Money(BigInt.from(-110000000), CryptoCurrency.btc, 8).hashCode),
|
||||
);
|
||||
});
|
||||
|
||||
test("zero hashes equally at every scale", () {
|
||||
final hashes = [0, 1, 8, 18, 30]
|
||||
.map((s) => Money(BigInt.zero, CryptoCurrency.btc, s).hashCode)
|
||||
.toSet();
|
||||
|
||||
expect(hashes, hasLength(1));
|
||||
});
|
||||
|
||||
test("whole units are not confused with their digits", () {
|
||||
// (100, scale 0) is one hundred, not one: stripping must stop at 0.
|
||||
final oneHundred = Money(BigInt.from(100), CryptoCurrency.btc, 0);
|
||||
|
||||
expect(oneHundred, isNot(equals(coarseOne)));
|
||||
expect(oneHundred.hashCode, isNot(equals(coarseOne.hashCode)));
|
||||
});
|
||||
|
||||
test("Set deduplicates across scales", () {
|
||||
expect(
|
||||
{
|
||||
Money(BigInt.from(11), CryptoCurrency.btc, 1),
|
||||
Money(BigInt.from(110000000), CryptoCurrency.btc, 8),
|
||||
Money(BigInt.from(1100), CryptoCurrency.btc, 3),
|
||||
},
|
||||
hasLength(1),
|
||||
);
|
||||
});
|
||||
|
||||
test("+ aligns operands", () {
|
||||
expect(coarseOne + fineOne, equals(Money(BigInt.two, CryptoCurrency.btc, 0)));
|
||||
expect(coarseOne + fineOne, equals(fineOne + coarseOne));
|
||||
});
|
||||
|
||||
test("- aligns operands", () {
|
||||
// Regression: this used to subtract the raw amounts, i.e. 1 - 100000000.
|
||||
expect((coarseOne - fineOne).isZero, isTrue);
|
||||
expect((fineOne - coarseOne).isZero, isTrue);
|
||||
|
||||
final threeCoarse = Money(BigInt.from(3), CryptoCurrency.btc, 0);
|
||||
expect(threeCoarse - fineOne, equals(Money(BigInt.two, CryptoCurrency.btc, 0)));
|
||||
expect(threeCoarse - fineOne, equals(-(fineOne - threeCoarse)));
|
||||
});
|
||||
|
||||
test("the result keeps the finer scale", () {
|
||||
expect((coarseOne + fineOne).decimals, greaterThanOrEqualTo(fineOne.decimals));
|
||||
});
|
||||
|
||||
test("sub-unit precision survives a coarse operand", () {
|
||||
final oneSatoshi = Money(BigInt.one, CryptoCurrency.btc, 8);
|
||||
|
||||
expect(
|
||||
coarseOne + oneSatoshi,
|
||||
equals(Money(BigInt.from(100000001), CryptoCurrency.btc, 8)),
|
||||
);
|
||||
});
|
||||
|
||||
test("comparison operators align", () {
|
||||
expect(coarseOne < fineOne, isFalse);
|
||||
expect(coarseOne <= fineOne, isTrue);
|
||||
expect(coarseOne >= fineOne, isTrue);
|
||||
expect(coarseOne > fineOne, isFalse);
|
||||
expect(Money(BigInt.two, CryptoCurrency.btc, 0) > fineOne, isTrue);
|
||||
});
|
||||
|
||||
test("compareTo aligns and agrees with ==", () {
|
||||
expect(coarseOne.compareTo(fineOne), isZero);
|
||||
expect(coarseOne.compareTo(fineOne) == 0, equals(coarseOne == fineOne));
|
||||
expect(Money(BigInt.two, CryptoCurrency.btc, 0).compareTo(fineOne), isPositive);
|
||||
expect(Money(BigInt.zero, CryptoCurrency.btc, 0).compareTo(fineOne), isNegative);
|
||||
});
|
||||
|
||||
test("toStringWithPrecision", () {
|
||||
expect(
|
||||
Money.parse("60000.123456", USD, strictParsing: false).toStringWithPrecision(),
|
||||
"60000.12",
|
||||
);
|
||||
expect(
|
||||
Money.parse("1.23456789", USD, strictParsing: false).toStringWithPrecision(),
|
||||
"1.23",
|
||||
);
|
||||
expect(Money(BigInt.one, CryptoCurrency.btc, 0).toStringWithPrecision(), "1");
|
||||
expect(
|
||||
Money(BigInt.one, CryptoCurrency.btc, 0).toStringWithPrecision(useBaseUnit: true),
|
||||
"100000000",
|
||||
);
|
||||
});
|
||||
|
||||
test("toString", () {
|
||||
expect(
|
||||
Money.parse("60000.123456", CryptoCurrency.btc, strictParsing: false).toString(),
|
||||
"60000.123456",
|
||||
);
|
||||
expect(
|
||||
Money.parse("1.23456789", CryptoCurrency.btc, strictParsing: false).toString(),
|
||||
"1.23456789",
|
||||
);
|
||||
expect(Money(BigInt.one, CryptoCurrency.btc, 0).toString(), "1");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/amount/money_double.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/parse_fixed.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
/// Every non-finite spelling that `double.tryParse` accepts.
|
||||
///
|
||||
/// These matter because a `double`-based amount guard cannot reject them:
|
||||
/// `Infinity <= 0` is false and *every* comparison against `NaN` is false, so a
|
||||
/// non-finite amount sails through an `if (amount <= 0) throw` check and then
|
||||
/// collapses to zero once it reaches Money/BigInt -- i.e. a zero-amount
|
||||
/// transaction that passed validation. The Money layer is the enforcement point
|
||||
/// for that invariant, so pin it here.
|
||||
const nonFiniteSpellings = <String>[
|
||||
"Infinity",
|
||||
"+Infinity",
|
||||
"-Infinity",
|
||||
"NaN",
|
||||
"1e999", // overflows to Infinity as a double
|
||||
"1E400",
|
||||
" Infinity ",
|
||||
];
|
||||
|
||||
void main() {
|
||||
group("non-finite amounts", () {
|
||||
test("double.tryParse accepts them (this is why Money must reject them)", () {
|
||||
for (final spelling in nonFiniteSpellings) {
|
||||
final parsed = double.tryParse(spelling);
|
||||
expect(parsed, isNotNull, reason: "double.tryParse rejected $spelling");
|
||||
expect(parsed!.isFinite, isFalse, reason: "$spelling parsed as finite");
|
||||
// The point of the whole exercise: a `<= 0` guard does not stop these.
|
||||
if (!parsed.isNegative) {
|
||||
expect(parsed <= 0, isFalse, reason: "$spelling was caught by a <= 0 guard");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Money.tryParse returns null for every non-finite spelling", () {
|
||||
for (final spelling in nonFiniteSpellings) {
|
||||
expect(
|
||||
Money.tryParse(spelling, CryptoCurrency.btc),
|
||||
isNull,
|
||||
reason: "Money.tryParse accepted $spelling",
|
||||
);
|
||||
expect(
|
||||
Money.tryParse(spelling, CryptoCurrency.btc, strictParsing: false),
|
||||
isNull,
|
||||
reason: "Money.tryParse(strictParsing: false) accepted $spelling",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Money.parse throws for every non-finite spelling", () {
|
||||
for (final spelling in nonFiniteSpellings) {
|
||||
expect(
|
||||
() => Money.parse(spelling, CryptoCurrency.btc),
|
||||
throwsA(isA<FormatException>()),
|
||||
reason: "Money.parse accepted $spelling",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("CryptoCurrency.tryParseAmount returns null, parseAmount throws", () {
|
||||
for (final spelling in nonFiniteSpellings) {
|
||||
expect(CryptoCurrency.btc.tryParseAmount(spelling), isNull, reason: spelling);
|
||||
expect(
|
||||
() => CryptoCurrency.btc.parseAmount(spelling),
|
||||
throwsA(isA<FormatException>()),
|
||||
reason: spelling,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("tryParseFixed returns null / parseFixed throws (the underlying gate)", () {
|
||||
for (final spelling in nonFiniteSpellings) {
|
||||
expect(tryParseFixed(spelling, 8), isNull, reason: spelling);
|
||||
expect(
|
||||
() => parseFixed(spelling, 8),
|
||||
throwsA(isA<FormatException>()),
|
||||
reason: spelling,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("double.tryToMoney returns null for non-finite doubles", () {
|
||||
// Relied on by ExchangeViewModel.changeReceiveAmount/changeDepositAmount, which
|
||||
// divide by `bestRate` and can produce Infinity when the rate is still 0.
|
||||
expect(double.infinity.tryToMoney(CryptoCurrency.btc), isNull);
|
||||
expect(double.negativeInfinity.tryToMoney(CryptoCurrency.btc), isNull);
|
||||
expect(double.nan.tryToMoney(CryptoCurrency.btc), isNull);
|
||||
});
|
||||
|
||||
test("a rejected amount funnels to Money.zero, which is the sweep-all sentinel", () {
|
||||
// Why the cw_monero/cw_wownero guards exist. MONERO_Wallet_createTransaction:
|
||||
// Monero::optional<uint64_t> optAmount;
|
||||
// if (amount != 0) { optAmount = amount; }
|
||||
// so amount == 0 means "no amount given" and wallet2 sweeps the whole account.
|
||||
// Output.cryptoAmountMoney falls back to Money.zero on any parse failure, and a
|
||||
// zero Money stringifies to an all-zeros decimal -- exactly the value the native
|
||||
// layer reads as "send everything". Nothing between them may treat 0 as an amount.
|
||||
for (final spelling in nonFiniteSpellings) {
|
||||
final fallback =
|
||||
CryptoCurrency.btc.tryParseAmount(spelling) ?? Money.zero(CryptoCurrency.xmr);
|
||||
expect(fallback.amount, BigInt.zero, reason: spelling);
|
||||
}
|
||||
|
||||
final zeroXmr = Money.zero(CryptoCurrency.xmr);
|
||||
expect(zeroXmr.amount, BigInt.zero);
|
||||
expect(double.tryParse(zeroXmr.toString()), 0.0,
|
||||
reason: "a zero Money must stringify to a numeric zero, i.e. the sweep-all sentinel");
|
||||
expect(RegExp(r"^0(\.0+)?$").hasMatch(zeroXmr.toString()), isTrue,
|
||||
reason: "unexpected zero rendering: ${zeroXmr.toString()}");
|
||||
|
||||
// One piconero is NOT zero -- the guards must not reject genuine dust.
|
||||
expect(Money(BigInt.one, CryptoCurrency.xmr).sign, 1);
|
||||
expect(double.parse(Money(BigInt.one, CryptoCurrency.xmr).toString()) > 0, isTrue);
|
||||
});
|
||||
|
||||
test("finite amounts still round-trip (guard did not over-reject)", () {
|
||||
expect(Money.tryParse("0", CryptoCurrency.btc)!.amount, BigInt.zero);
|
||||
expect(Money.tryParse("1", CryptoCurrency.btc)!.amount, BigInt.from(100000000));
|
||||
expect(Money.tryParse("0.00000001", CryptoCurrency.btc)!.amount, BigInt.one);
|
||||
expect(1.5.tryToMoney(CryptoCurrency.btc)!.amount, BigInt.from(150000000));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
|
||||
class FiatCurrency implements Currency {
|
||||
const FiatCurrency({
|
||||
required this.symbol,
|
||||
required this.countryCode,
|
||||
required this.fullName,
|
||||
this.decimals = 2,
|
||||
});
|
||||
|
||||
final String countryCode;
|
||||
|
||||
@override
|
||||
final String fullName;
|
||||
|
||||
@override
|
||||
final int decimals;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
String? get iconPath => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get name => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String? get tag => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get apiString => "fiat.$symbol";
|
||||
|
||||
@override
|
||||
Money parseAmount(String value) => Money.parse(value, this);
|
||||
|
||||
@override
|
||||
Money? tryParseAmount(String value) => Money.tryParse(value, this);
|
||||
}
|
||||
|
||||
const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro");
|
||||
const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar");
|
||||
const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0);
|
||||
@@ -108,6 +108,15 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group("Locale without intl number data", () {
|
||||
test("should format as English instead of throwing (ha)", () {
|
||||
const input = "1123.4567";
|
||||
final result = input.withLocalSeperator("ha");
|
||||
|
||||
expect(result, equals("1,123.4567"));
|
||||
});
|
||||
});
|
||||
|
||||
group('With XMR Suffix', () {
|
||||
const locale = 'de_CH';
|
||||
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cake_backup/backup.dart' as cwb;
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/key.dart';
|
||||
import 'package:cw_core/utils/file.dart' as encrypted_file;
|
||||
import 'package:cw_core/wallet_keys_file.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
late Directory tmpDir;
|
||||
|
||||
setUp(() {
|
||||
tmpDir = Directory.systemTemp.createTempSync('cw_enc_');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
if (tmpDir.existsSync()) {
|
||||
tmpDir.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
String path([String name = 'wallet.json']) => '${tmpDir.path}/$name';
|
||||
|
||||
const walletJson = '{"mnemonic":"abandon ability able","privateKey":null}';
|
||||
|
||||
group('XChaCha20 round-trip', () {
|
||||
test('writes and reads JSON with a generated high-entropy password', () async {
|
||||
final password = generateKey();
|
||||
await encrypted_file.write(
|
||||
path: path(),
|
||||
password: password,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
|
||||
expect(await encrypted_file.read(path: path(), password: password), walletJson);
|
||||
expect(File('${path()}.tmp').existsSync(), isFalse);
|
||||
});
|
||||
|
||||
test('uses high-entropy (v3) vs low-entropy (v2) version bytes', () async {
|
||||
final password = generateKey();
|
||||
|
||||
await encrypted_file.write(
|
||||
path: path('high'),
|
||||
password: password,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
expect(File(path('high')).readAsBytesSync().first, cwb.highEntropyVersion);
|
||||
|
||||
await encrypted_file.write(
|
||||
path: path('low'),
|
||||
password: password,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: false,
|
||||
);
|
||||
expect(File(path('low')).readAsBytesSync().first, cwb.lowEntropyVersion);
|
||||
|
||||
expect(await encrypted_file.read(path: path('high'), password: password), walletJson);
|
||||
expect(await encrypted_file.read(path: path('low'), password: password), walletJson);
|
||||
});
|
||||
|
||||
test('read ignores highEntropyPassphrase because version is in the file', () async {
|
||||
final password = generateKey();
|
||||
await encrypted_file.write(
|
||||
path: path(),
|
||||
password: password,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
await encrypted_file.read(
|
||||
path: path(),
|
||||
password: password,
|
||||
highEntropyPassphrase: false,
|
||||
),
|
||||
walletJson,
|
||||
);
|
||||
});
|
||||
|
||||
test('round-trips UTF-8 JSON with non-ASCII characters', () async {
|
||||
final password = generateKey();
|
||||
const data = '{"name":"Café","mnemonic":"abandon"}';
|
||||
await encrypted_file.write(
|
||||
path: path(),
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
|
||||
expect(await encrypted_file.read(path: path(), password: password), data);
|
||||
});
|
||||
|
||||
test('wrong password does not fall back to Salsa20 or rewrite the file', () async {
|
||||
final password = generateKey();
|
||||
await encrypted_file.write(
|
||||
path: path(),
|
||||
password: password,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
final before = File(path()).readAsBytesSync();
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: generateKey()),
|
||||
throwsA(anything),
|
||||
);
|
||||
expect(File(path()).readAsBytesSync(), before);
|
||||
});
|
||||
|
||||
test('corrupt XChaCha20 blob is not treated as a legacy Salsa20 file', () async {
|
||||
final password = generateKey();
|
||||
await encrypted_file.write(
|
||||
path: path(),
|
||||
password: password,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
final bytes = File(path()).readAsBytesSync();
|
||||
bytes[bytes.length - 1] = bytes[bytes.length - 1] ^ 0xFF;
|
||||
File(path()).writeAsBytesSync(bytes);
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: password),
|
||||
throwsA(anything),
|
||||
);
|
||||
expect(File(path()).readAsBytesSync().first, cwb.highEntropyVersion);
|
||||
});
|
||||
});
|
||||
|
||||
group('encryptionFileUtilsFor', () {
|
||||
test('isDirect=false (generated password) writes high-entropy v3', () async {
|
||||
final password = generateKey();
|
||||
final encryption = encryptionFileUtilsFor(false);
|
||||
await encryption.write(path: path(), password: password, data: walletJson);
|
||||
|
||||
expect(File(path()).readAsBytesSync().first, cwb.highEntropyVersion);
|
||||
expect(await encryption.read(path: path(), password: password), walletJson);
|
||||
});
|
||||
|
||||
test('isDirect=true (user password) writes low-entropy v2', () async {
|
||||
const password = 'user-chosen-passphrase';
|
||||
final encryption = encryptionFileUtilsFor(true);
|
||||
await encryption.write(path: path(), password: password, data: walletJson);
|
||||
|
||||
expect(File(path()).readAsBytesSync().first, cwb.lowEntropyVersion);
|
||||
expect(await encryption.read(path: path(), password: password), walletJson);
|
||||
});
|
||||
|
||||
test('WalletKeysData JSON survives a write/read used by getSeeds', () async {
|
||||
final password = generateKey();
|
||||
final keys = WalletKeysData(
|
||||
mnemonic: 'abandon ability able about above absent absorb abstract',
|
||||
privateKey: 'deadbeef',
|
||||
);
|
||||
final encryption = encryptionFileUtilsFor(false);
|
||||
await encryption.write(path: path(), password: password, data: keys.toJSON());
|
||||
|
||||
final decoded = json.decode(await encryption.read(path: path(), password: password))
|
||||
as Map<String, dynamic>;
|
||||
final restored = WalletKeysData.fromJSON(decoded);
|
||||
expect(restored.mnemonic, keys.mnemonic);
|
||||
expect(restored.privateKey, keys.privateKey);
|
||||
});
|
||||
});
|
||||
|
||||
group('legacy Salsa20 migration', () {
|
||||
test('reads a Salsa20 wallet file and re-encrypts it as XChaCha20', () async {
|
||||
final password = generateKey();
|
||||
File(path()).writeAsStringSync(_salsa20Encrypt(password, walletJson));
|
||||
expect(File(path()).readAsBytesSync().first, isNot(cwb.lowEntropyVersion));
|
||||
expect(File(path()).readAsBytesSync().first, isNot(cwb.highEntropyVersion));
|
||||
|
||||
final result = await encrypted_file.read(
|
||||
path: path(),
|
||||
password: password,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
|
||||
expect(result, walletJson);
|
||||
expect(File(path()).readAsBytesSync().first, cwb.highEntropyVersion);
|
||||
expect(await encrypted_file.read(path: path(), password: password), walletJson);
|
||||
});
|
||||
|
||||
test('migrated file can be opened with EncryptionFileUtils', () async {
|
||||
final password = generateKey();
|
||||
File(path()).writeAsStringSync(_salsa20Encrypt(password, walletJson));
|
||||
|
||||
final encryption = encryptionFileUtilsFor(false);
|
||||
expect(await encryption.read(path: path(), password: password), walletJson);
|
||||
expect(await encryption.read(path: path(), password: password), walletJson);
|
||||
});
|
||||
|
||||
test('wrong password does not rewrite the Salsa20 file', () async {
|
||||
final password = generateKey();
|
||||
final salsa = _salsa20Encrypt(password, walletJson);
|
||||
File(path()).writeAsStringSync(salsa);
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: generateKey()),
|
||||
throwsLegacyDecryptError,
|
||||
);
|
||||
expect(File(path()).readAsStringSync(), salsa);
|
||||
});
|
||||
|
||||
test('short or malformed password does not leak _readLegacy errors', () async {
|
||||
final password = generateKey();
|
||||
final salsa = _salsa20Encrypt(password, walletJson);
|
||||
File(path()).writeAsStringSync(salsa);
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: 'user-pass'),
|
||||
throwsLegacyDecryptError,
|
||||
);
|
||||
expect(File(path()).readAsStringSync(), salsa);
|
||||
});
|
||||
|
||||
test('corrupt non-base64 file does not leak _readLegacy errors', () async {
|
||||
File(path()).writeAsStringSync('not-valid-base64!!!');
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: generateKey()),
|
||||
throwsLegacyDecryptError,
|
||||
);
|
||||
expect(File(path()).readAsStringSync(), 'not-valid-base64!!!');
|
||||
});
|
||||
|
||||
test('invalid UTF-8 legacy bytes do not leak _readLegacy errors', () async {
|
||||
File(path()).writeAsBytesSync(const [0x00, 0xFF, 0xFE, 0x01]);
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: generateKey()),
|
||||
throwsLegacyDecryptError,
|
||||
);
|
||||
expect(File(path()).readAsBytesSync(), const [0x00, 0xFF, 0xFE, 0x01]);
|
||||
});
|
||||
|
||||
test('rejects Salsa20 plaintext that does not start with {"', () async {
|
||||
final password = generateKey();
|
||||
File(path()).writeAsStringSync(_salsa20Encrypt(password, 'not-json-payload'));
|
||||
|
||||
await expectLater(
|
||||
encrypted_file.read(path: path(), password: password),
|
||||
throwsLegacyDecryptError,
|
||||
);
|
||||
expect(File(path()).readAsBytesSync().first, isNot(cwb.highEntropyVersion));
|
||||
expect(File(path()).readAsBytesSync().first, isNot(cwb.lowEntropyVersion));
|
||||
});
|
||||
|
||||
test('does not re-encrypt Salsa20 data that starts with {" but is not JSON', () async {
|
||||
final password = generateKey();
|
||||
const garbage = '{"not valid json';
|
||||
File(path()).writeAsStringSync(_salsa20Encrypt(password, garbage));
|
||||
|
||||
expect(await encrypted_file.read(path: path(), password: password), garbage);
|
||||
expect(File(path()).readAsBytesSync().first, isNot(cwb.highEntropyVersion));
|
||||
expect(File(path()).readAsBytesSync().first, isNot(cwb.lowEntropyVersion));
|
||||
});
|
||||
|
||||
test(
|
||||
'wrong password that decrypts to {" does not rewrite the file',
|
||||
() async {
|
||||
final password = generateKey();
|
||||
final salsa = _salsa20Encrypt(password, walletJson);
|
||||
File(path()).writeAsStringSync(salsa);
|
||||
|
||||
final collision = _findWrongPasswordWithJsonObjectPrefix(salsa);
|
||||
expect(collision, isNot(password));
|
||||
expect(_salsa20Decrypt(collision, salsa).startsWith('{"'), isTrue);
|
||||
expect(() => json.decode(_salsa20Decrypt(collision, salsa)), throwsA(anything));
|
||||
|
||||
final result = await encrypted_file.read(path: path(), password: collision);
|
||||
|
||||
expect(result.startsWith('{"'), isTrue);
|
||||
expect(result, isNot(walletJson));
|
||||
expect(File(path()).readAsStringSync(), salsa);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('legacy Linux XChaCha20 Latin-1 payloads', () {
|
||||
test('decodes files stored as one byte per UTF-16 code unit', () async {
|
||||
final password = generateKey();
|
||||
const data = '{"name":"Café","mnemonic":"abandon"}';
|
||||
// Old XChaCha20EncryptionFileUtils used String.codeUnits instead of utf8.encode.
|
||||
final encrypted = await cwb.encrypt(
|
||||
password,
|
||||
Uint8List.fromList(data.codeUnits),
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
File(path()).writeAsBytesSync(encrypted);
|
||||
|
||||
expect(await encrypted_file.read(path: path(), password: password), data);
|
||||
});
|
||||
});
|
||||
|
||||
group('generateKey', () {
|
||||
test('produces unique passwords that still encrypt and decrypt', () async {
|
||||
final a = generateKey();
|
||||
final b = generateKey();
|
||||
expect(a, isNot(b));
|
||||
expect(a.length, greaterThan(12));
|
||||
|
||||
await encrypted_file.write(
|
||||
path: path(),
|
||||
password: a,
|
||||
data: walletJson,
|
||||
highEntropyPassphrase: true,
|
||||
);
|
||||
expect(await encrypted_file.read(path: path(), password: a), walletJson);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final throwsLegacyDecryptError = throwsA(
|
||||
isA<Exception>().having(
|
||||
(e) => e.toString(),
|
||||
'message',
|
||||
contains('Failed to decrypt legacy file'),
|
||||
),
|
||||
);
|
||||
|
||||
({encrypt.Key key, encrypt.IV iv}) _salsa20Keys(String password) {
|
||||
const ivEncodedStringLength = 12;
|
||||
return (
|
||||
key: encrypt.Key.fromBase64(password.substring(0, password.length - ivEncodedStringLength)),
|
||||
iv: encrypt.IV.fromBase64(password.substring(password.length - ivEncodedStringLength)),
|
||||
);
|
||||
}
|
||||
|
||||
encrypt.Encrypter _salsa20(String password) {
|
||||
final keys = _salsa20Keys(password);
|
||||
return encrypt.Encrypter(encrypt.Salsa20(keys.key));
|
||||
}
|
||||
|
||||
String _salsa20Encrypt(String password, String data) {
|
||||
final keys = _salsa20Keys(password);
|
||||
return _salsa20(password).encrypt(data, iv: keys.iv).base64;
|
||||
}
|
||||
|
||||
String _salsa20Decrypt(String password, String data) {
|
||||
final keys = _salsa20Keys(password);
|
||||
return _salsa20(password).decrypt64(data, iv: keys.iv);
|
||||
}
|
||||
|
||||
List<int> _salsa20DecryptBytes(String password, String data) {
|
||||
final keys = _salsa20Keys(password);
|
||||
return _salsa20(password).decryptBytes(encrypt.Encrypted.fromBase64(data), iv: keys.iv);
|
||||
}
|
||||
|
||||
String _passwordFromNonce(int nonce) {
|
||||
final ivBytes = Uint8List(8);
|
||||
ivBytes.buffer.asByteData().setUint64(0, nonce, Endian.little);
|
||||
return encrypt.Key(Uint8List(32)).base64 + encrypt.IV(ivBytes).base64;
|
||||
}
|
||||
|
||||
/// Salsa20 has no MAC, so a wrong key decrypts to garbage. The first two bytes
|
||||
/// are `{"` about once in 2^16 tries; keep going until that happens, then skip
|
||||
/// the much rarer case where the garbage is also valid JSON (that would rewrite).
|
||||
String _findWrongPasswordWithJsonObjectPrefix(String ciphertext) {
|
||||
const maxAttempts = 1 << 20;
|
||||
for (var nonce = 0; nonce < maxAttempts; nonce++) {
|
||||
final candidate = _passwordFromNonce(nonce);
|
||||
final bytes = _salsa20DecryptBytes(candidate, ciphertext);
|
||||
if (bytes.length < 2 || bytes[0] != 0x7b || bytes[1] != 0x22) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
json.decode(_salsa20Decrypt(candidate, ciphertext));
|
||||
} catch (_) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
fail('no Salsa20 {" prefix collision in $maxAttempts attempts');
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/imported_nft.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:path_provider_platform_interface/path_provider_platform_interface.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
// Faking the documents dir keeps getAppDir() off the platform channel, so the
|
||||
// test runs with a plain `flutter test` on any host and in CI.
|
||||
class _FakePathProviderPlatform extends PathProviderPlatform {
|
||||
_FakePathProviderPlatform(this.root);
|
||||
|
||||
final String root;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationDocumentsPath() async => root;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationSupportPath() async => root;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
final dataRoot = Directory("./test/data/imported_nft");
|
||||
|
||||
Future<void> insert(String walletName, String chain, String identifier) =>
|
||||
ImportedNFT(walletName: walletName, chain: chain, identifier: identifier, name: identifier)
|
||||
.save();
|
||||
|
||||
Future<List<String>> identifiersFor(String walletName, [String? chain]) async =>
|
||||
(await ImportedNFT.getAllForWallet(walletName, chain)).map((nft) => nft.identifier).toList();
|
||||
|
||||
setUpAll(() async {
|
||||
if (dataRoot.existsSync()) {
|
||||
dataRoot.deleteSync(recursive: true);
|
||||
}
|
||||
dataRoot.createSync(recursive: true);
|
||||
Directory("${dataRoot.path}/cake_wallet").createSync(recursive: true);
|
||||
|
||||
PathProviderPlatform.instance = _FakePathProviderPlatform(dataRoot.absolute.path);
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
await initDb();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
await db!.delete(ImportedNFT.tableName);
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
if (dataRoot.existsSync()) {
|
||||
dataRoot.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
group("ImportedNFT wallet lifecycle", () {
|
||||
test("reading without a chain returns every chain the wallet imported on", () async {
|
||||
await insert("Main", ImportedNFT.solanaChain, "SolMint");
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
await insert("Main", "polygon", "0xother:7");
|
||||
|
||||
expect(await identifiersFor("Main", "eth"), ["0xcontract:1"]);
|
||||
expect(
|
||||
(await identifiersFor("Main"))..sort(),
|
||||
["0xcontract:1", "0xother:7", "SolMint"],
|
||||
);
|
||||
});
|
||||
|
||||
test("rename moves every chain and leaves other wallets alone", () async {
|
||||
await insert("Main", ImportedNFT.solanaChain, "SolMint");
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
await insert("Sidekick", "eth", "0xkeep:9");
|
||||
|
||||
await ImportedNFT.renameWallet("Main", "Renamed");
|
||||
|
||||
expect(await identifiersFor("Main"), isEmpty);
|
||||
expect((await identifiersFor("Renamed"))..sort(), ["0xcontract:1", "SolMint"]);
|
||||
expect(await identifiersFor("Sidekick"), ["0xkeep:9"]);
|
||||
});
|
||||
|
||||
test("renaming into a name that still has orphan rows does not trip the unique index",
|
||||
() async {
|
||||
await insert("Orphaned", "eth", "0xcontract:1");
|
||||
await insert("Orphaned", "polygon", "0xsurvivor:5");
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
await insert("Main", "eth", "0xsecond:2");
|
||||
|
||||
await ImportedNFT.renameWallet("Main", "Orphaned", chains: const ["eth"]);
|
||||
|
||||
// Only the colliding chain is cleared. A row the destination holds on
|
||||
// another chain is not ours to delete.
|
||||
expect(
|
||||
(await identifiersFor("Orphaned"))..sort(),
|
||||
["0xcontract:1", "0xsecond:2", "0xsurvivor:5"],
|
||||
);
|
||||
});
|
||||
|
||||
test("renaming a solana wallet leaves a same-named evm wallet's NFTs alone", () async {
|
||||
await insert("Sol", ImportedNFT.solanaChain, "SolMint");
|
||||
await insert("Vault", "eth", "0xEthNFT:1");
|
||||
|
||||
await ImportedNFT.renameWallet(
|
||||
"Sol",
|
||||
"Vault",
|
||||
chains: const [ImportedNFT.solanaChain],
|
||||
);
|
||||
|
||||
expect((await identifiersFor("Vault"))..sort(), ["0xEthNFT:1", "SolMint"]);
|
||||
});
|
||||
|
||||
test("deleting a solana wallet leaves a same-named evm wallet's NFTs alone", () async {
|
||||
await insert("Vault", ImportedNFT.solanaChain, "SolMint");
|
||||
await insert("Vault", "eth", "0xEthNFT:1");
|
||||
|
||||
await ImportedNFT.deleteAllForWallet(
|
||||
"Vault",
|
||||
chains: const [ImportedNFT.solanaChain],
|
||||
);
|
||||
|
||||
expect(await identifiersFor("Vault"), ["0xEthNFT:1"]);
|
||||
});
|
||||
|
||||
test("an empty chain scope deletes nothing rather than everything", () async {
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
|
||||
await ImportedNFT.deleteAllForWallet("Main", chains: const []);
|
||||
|
||||
expect(await identifiersFor("Main"), ["0xcontract:1"]);
|
||||
});
|
||||
|
||||
test("delete removes every chain for that wallet only", () async {
|
||||
await insert("Main", ImportedNFT.solanaChain, "SolMint");
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
await insert("Sidekick", ImportedNFT.solanaChain, "OtherMint");
|
||||
|
||||
await ImportedNFT.deleteAllForWallet("Main");
|
||||
|
||||
expect(await identifiersFor("Main"), isEmpty);
|
||||
expect(await identifiersFor("Sidekick"), ["OtherMint"]);
|
||||
});
|
||||
|
||||
test("deleting one leaves the wallet's other imports in place", () async {
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
await insert("Main", "eth", "0xsecond:2");
|
||||
await insert("Main", "polygon", "0xcontract:1");
|
||||
|
||||
await ImportedNFT.deleteOne("Main", "eth", "0xcontract:1");
|
||||
|
||||
expect(await identifiersFor("Main", "eth"), ["0xsecond:2"]);
|
||||
expect(await identifiersFor("Main", "polygon"), ["0xcontract:1"]);
|
||||
});
|
||||
|
||||
test("copying to a new name keeps the rows under the old one", () async {
|
||||
await insert("Main", ImportedNFT.solanaChain, "SolMint");
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
|
||||
for (final nft in await ImportedNFT.getAllForWallet("Main")) {
|
||||
await ImportedNFT.copyWith(nft, walletName: "Copy").save();
|
||||
}
|
||||
|
||||
expect((await identifiersFor("Main"))..sort(), ["0xcontract:1", "SolMint"]);
|
||||
expect((await identifiersFor("Copy"))..sort(), ["0xcontract:1", "SolMint"]);
|
||||
});
|
||||
|
||||
test("a copy is a new row, so saving it does not overwrite the original", () async {
|
||||
await insert("Main", "eth", "0xcontract:1");
|
||||
|
||||
final original = (await ImportedNFT.getAllForWallet("Main")).single;
|
||||
final copy = ImportedNFT.copyWith(original, walletName: "Copy");
|
||||
await copy.save();
|
||||
|
||||
expect(copy.id, isNot(original.id));
|
||||
expect((await ImportedNFT.getAllForWallet("Main")).single.id, original.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import "package:cw_core/utils/ipfs_url.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("tryNormalizeIpfsUrl", () {
|
||||
test("rewrites an ipfs uri to the gateway", () {
|
||||
expect(tryNormalizeIpfsUrl("ipfs://QmAbC123"), "https://ipfs.io/ipfs/QmAbC123");
|
||||
});
|
||||
|
||||
test("does not double the gateway path when the uri already says ipfs/", () {
|
||||
expect(tryNormalizeIpfsUrl("ipfs://ipfs/QmAbC123"), "https://ipfs.io/ipfs/QmAbC123");
|
||||
});
|
||||
|
||||
test("matches the scheme whatever its case", () {
|
||||
expect(tryNormalizeIpfsUrl("IPFS://QmAbC123"), "https://ipfs.io/ipfs/QmAbC123");
|
||||
expect(tryNormalizeIpfsUrl("Ipfs://QmAbC123"), "https://ipfs.io/ipfs/QmAbC123");
|
||||
});
|
||||
|
||||
test("returns null for an ipfs uri with no cid, rather than a bare gateway url", () {
|
||||
expect(tryNormalizeIpfsUrl("ipfs://"), isNull);
|
||||
expect(tryNormalizeIpfsUrl("ipfs://ipfs/"), isNull);
|
||||
});
|
||||
|
||||
test("passes through an https url that is not ipfs", () {
|
||||
expect(tryNormalizeIpfsUrl("https://arweave.net/abc"), "https://arweave.net/abc");
|
||||
});
|
||||
|
||||
test("drops a scheme nothing can render, rather than handing it to the image widget", () {
|
||||
expect(tryNormalizeIpfsUrl("ar://abc123"), isNull);
|
||||
expect(tryNormalizeIpfsUrl(""), isNull);
|
||||
});
|
||||
|
||||
test("returns null for null", () {
|
||||
expect(tryNormalizeIpfsUrl(null), isNull);
|
||||
});
|
||||
|
||||
test("is idempotent, so a value read back from storage stays stable", () {
|
||||
final once = tryNormalizeIpfsUrl("ipfs://QmAbC123");
|
||||
expect(tryNormalizeIpfsUrl(once), once);
|
||||
});
|
||||
|
||||
test("drops any scheme that is not https", () {
|
||||
// These render through Image.network, which does not go through the proxy.
|
||||
expect(tryNormalizeIpfsUrl("http://127.0.0.1:8080/x.png"), isNull);
|
||||
expect(tryNormalizeIpfsUrl("file:///etc/passwd"), isNull);
|
||||
expect(tryNormalizeIpfsUrl("javascript:alert(1)"), isNull);
|
||||
expect(tryNormalizeIpfsUrl("data:text/html,x"), isNull);
|
||||
});
|
||||
|
||||
test("keeps an https url whatever its casing", () {
|
||||
expect(tryNormalizeIpfsUrl("https://ok.example/a.png"), "https://ok.example/a.png");
|
||||
expect(tryNormalizeIpfsUrl("HTTPS://Up.example/a.png"), "HTTPS://Up.example/a.png");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/tor/abstract.dart';
|
||||
@@ -11,13 +13,14 @@ void main() {
|
||||
expect(content, Uri.parse("https://cake.cash/.well-known/lnurlp/konsti"));
|
||||
});
|
||||
|
||||
test('get amount', () async {
|
||||
CakeTor.instance = await CakeTorInstance.getInstance();
|
||||
|
||||
final content = await LNURL.getPayRequestAmount(
|
||||
"LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ");
|
||||
expect(content, 8500);
|
||||
});
|
||||
// This tests includes a rest request to an URL that expired and due to the lack of an alternative URL we ignore this test
|
||||
// test('get amount', () async {
|
||||
// CakeTor.instance = await CakeTorInstance.getInstance();
|
||||
//
|
||||
// final content = await LNURL.getPayRequestAmount(
|
||||
// "LNURL1DP68GURN8GHJ7CTSWQHXY6T5WFJHZAT9WD6ZU6T09AUZ7URJDAU8JTMKXYHKCM308A5N6MNHVVNXJEPAXGMNWWPEX3JRGDTPXVNXZ0FCX5CRQVPSXQNX602VFE24YNP9XGC8W6T5DQJNYVRPD4HH2MN5GTRNLZ");
|
||||
// expect(content, Money.fromInt(8500, CryptoCurrency.btcln));
|
||||
// });
|
||||
|
||||
test('get no amount', () async {
|
||||
CakeTor.instance = await CakeTorInstance.getInstance();
|
||||
@@ -70,25 +73,25 @@ void main() {
|
||||
test("should get the amount 1 BTC encoded as bitcoin", () {
|
||||
final invoice =
|
||||
"lnbc11p5exaxspp5u6e6lx90rw2vqr2e5axt8ur3v98qnmqfsmh8r0l0mcm7ry70vyvsdqqcqzzsxqrrsssp5eqydmrufk39dwhuxzajwx4xyffn0mlcu3qa8vjwry6qev5gsepkq9qxpqysgqt0zldve9uxt3txjt8wp0gev52lquljgugy26nneue85pjwk5yak4hs2d8533tus07pq5ka6zrnd0ruaeepwres8p7ws5mx63xlwff5sqlxcy8a";
|
||||
expect(getBolt11Amount(invoice), 100000000);
|
||||
expect(getBolt11Amount(invoice), Money.fromInt(100000000, CryptoCurrency.btcln));
|
||||
});
|
||||
|
||||
test("should get the amount 0,1 BTC encoded as millibitcoin", () {
|
||||
final invoice =
|
||||
"lnbc100m1p5exagmpp5n4c5tml3h8h7k4gthc9papkyeymceqqvcldkng4nvxwzj7nu9jnsdqqcqzzsxqrrsssp58kgzrh7y3pevantduc4yg33aa6r79ed6xgmhr0dy40zc8pxrz5ms9qxpqysgqcq27km3v3wcz6pntmsfnz259zy6tff0uwjz9kamdrvmxmdv09gp44mh0ygn7gweqf4zdev4x8e67p5kcvahethchdkkjexhtk7emuqcpasz0lp";
|
||||
expect(getBolt11Amount(invoice), 10000000);
|
||||
expect(getBolt11Amount(invoice), Money.fromInt(10000000, CryptoCurrency.btcln));
|
||||
});
|
||||
|
||||
test("should get the amount 1000 SATS encoded as microbitcoin", () {
|
||||
final invoice =
|
||||
"lnbc10u1p5exfgvpp5n3s9dsw9ddax5c3h8437ya7y4g582uyufyna626yr9vqdfjrhwzqdqqcqzzsxqrrsssp5mmkyp5h35fpaxm4063f3kc7d4nmgd4gd8fv8086wxd784m76kmpq9qxpqysgqye9r0vahpjy9l2je6vakxzk3cjnsyx76r29c4amkz4cmapq5tqt5l7rl0emkd003jjvz2d8jqrw6wc4cduvapuyeseh7a855l82wutcpeq30kg";
|
||||
expect(getBolt11Amount(invoice), 1000);
|
||||
expect(getBolt11Amount(invoice), Money.fromInt(1000, CryptoCurrency.btcln));
|
||||
});
|
||||
|
||||
test("should get the amount 1 SAT encoded as nanobitcoin", () {
|
||||
final invoice =
|
||||
"lnbc10n1p5exazppp5szh273r6nzvwydzsdlg0ws0vhxwnc6nqtqt4sjt0vu6x6l8t5csqdqqcqzzsxqrrsssp5nkup6uht7xlfqaj8lxpe5kx0ejum9p4pvv3nlnndr9ykzge3393s9qxpqysgqufh05a7qs0ezeckvmh7tjkr9ngng96nq274twg20z45ysp7afpdrttm2ky73cpjwpjqsmtzzd79w6lyvszgmkanwn8mj0jcfunrwv3qprg3z3j";
|
||||
expect(getBolt11Amount(invoice), 1);
|
||||
expect(getBolt11Amount(invoice), Money.fromInt(1, CryptoCurrency.btcln));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import "package:cw_core/utils/nft_text.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("sanitizeNFTText", () {
|
||||
test("removes the separators that would fake extra lines", () {
|
||||
expect(sanitizeNFTText("Ape\u2028Address: 9xQeW"), "Ape Address: 9xQeW");
|
||||
expect(sanitizeNFTText("Ape\u2029Address: 9xQeW"), "Ape Address: 9xQeW");
|
||||
expect(sanitizeNFTText("Ape\nAddress: 9xQeW"), "Ape Address: 9xQeW");
|
||||
});
|
||||
|
||||
test("removes bidi overrides and isolates that reorder what follows", () {
|
||||
expect(sanitizeNFTText("evil\u202Etxen"), "evil txen");
|
||||
expect(sanitizeNFTText("a\u2066b\u2069c"), "a b c");
|
||||
});
|
||||
|
||||
test("removes invisibles that hide or reorder characters", () {
|
||||
expect(sanitizeNFTText("A\u200BB"), "A B");
|
||||
expect(sanitizeNFTText("A\u200FB"), "A B");
|
||||
expect(sanitizeNFTText("A\u061CB"), "A B");
|
||||
expect(sanitizeNFTText("A\u2060B"), "A B");
|
||||
});
|
||||
|
||||
test("removes control characters and delete", () {
|
||||
expect(sanitizeNFTText("A\u0000B"), "A B");
|
||||
expect(sanitizeNFTText("A\u001FB"), "A B");
|
||||
expect(sanitizeNFTText("A\u007FB"), "A B");
|
||||
});
|
||||
|
||||
test("leaves ordinary text alone", () {
|
||||
expect(sanitizeNFTText("DeGod #1234"), "DeGod #1234");
|
||||
expect(sanitizeNFTText(" padded "), "padded");
|
||||
});
|
||||
|
||||
test("returns null for null", () {
|
||||
expect(sanitizeNFTText(null), isNull);
|
||||
});
|
||||
|
||||
test("counts the cap in characters, so emoji are not cut in half", () {
|
||||
final long = sanitizeNFTText("\u{1F600}" * 600)!;
|
||||
|
||||
expect(long.runes.length, 513);
|
||||
expect(long.runes.last, 0x2026);
|
||||
|
||||
final units = long.codeUnits;
|
||||
final endsOnHighSurrogate = units.isNotEmpty && units.last >= 0xD800 && units.last <= 0xDBFF;
|
||||
expect(endsOnHighSurrogate, isFalse);
|
||||
});
|
||||
|
||||
test("does not truncate a name that is long in code units but short in characters", () {
|
||||
expect(sanitizeNFTText("\u{1F600}" * 400)!.runes.length, 400);
|
||||
});
|
||||
|
||||
test("is idempotent, so re-reading a saved value does not change it", () {
|
||||
const raw = "Ape\u2028Name\u202E with \u{1F600}";
|
||||
final once = sanitizeNFTText(raw);
|
||||
|
||||
expect(sanitizeNFTText(once), once);
|
||||
});
|
||||
|
||||
test("keeps the joiners that carry meaning in persian and emoji", () {
|
||||
// Replacing these put a visible space inside a Persian word and split
|
||||
// a two-person emoji into two glyphs.
|
||||
expect(sanitizeNFTText("\u{1F468}\u200D\u{1F4BB}"), "\u{1F468}\u200D\u{1F4BB}");
|
||||
expect(
|
||||
sanitizeNFTText("\u0645\u06CC\u200C\u062E\u0648\u0627\u0647\u0645"),
|
||||
"\u0645\u06CC\u200C\u062E\u0648\u0627\u0647\u0645",
|
||||
);
|
||||
});
|
||||
|
||||
test("strips the invisibles a mint can hide text behind", () {
|
||||
for (final invisible in [
|
||||
"\u00AD", // soft hyphen
|
||||
"\u034F", // combining grapheme joiner
|
||||
"\u180E", // mongolian vowel separator
|
||||
"\u3164", // hangul filler
|
||||
"\uFEFF", // zero width no-break space
|
||||
"\uFFA0", // halfwidth hangul filler
|
||||
"\u{E0041}", // tag latin capital A
|
||||
]) {
|
||||
expect(
|
||||
sanitizeNFTText("A${invisible}B"),
|
||||
"A B",
|
||||
reason: "U+${invisible.runes.first.toRadixString(16).toUpperCase()} survived",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,19 @@ void main() {
|
||||
test('should fail to parse `.`, missing value',
|
||||
() => expect(() => parseFixed(".", 6), throwsFormatException));
|
||||
});
|
||||
|
||||
group("parseFixed, zero decimal currency", () {
|
||||
test("should parse 5 as 5", () => expect(parseFixed("5", 0), BigInt.from(5)));
|
||||
|
||||
test("should parse 0 as 0", () => expect(parseFixed("0", 0), BigInt.from(0)));
|
||||
|
||||
test("should parse 1. as 1", () => expect(parseFixed("1.", 0), BigInt.from(1)));
|
||||
|
||||
test("should parse -3 as -3", () => expect(parseFixed("-3", 0), BigInt.from(-3)));
|
||||
|
||||
test("should fail to parse 5.5, fractional component exceeds decimals",
|
||||
() => expect(() => parseFixed("5.5", 0), throwsFormatException));
|
||||
});
|
||||
});
|
||||
|
||||
group('tryParseFixed', () {
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import "package:cw_core/payment_uris.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("TronURI", () {
|
||||
const address = "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL";
|
||||
const contract = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
|
||||
|
||||
test("includes the TRC20 contract as the token param", () {
|
||||
final uri = TronURI(amount: "1.5", address: address, contractAddress: contract);
|
||||
|
||||
expect(uri.toString(), "tron:$address?amount=1.5&token=$contract");
|
||||
});
|
||||
|
||||
test("omits the token param for native TRX", () {
|
||||
final uri = TronURI(amount: "1.5", address: address);
|
||||
|
||||
expect(uri.toString(), "tron:$address?amount=1.5");
|
||||
expect(uri.toString().contains("token="), false);
|
||||
});
|
||||
|
||||
test("includes the token param when there is no amount", () {
|
||||
final uri = TronURI(amount: "", address: address, contractAddress: contract);
|
||||
|
||||
expect(uri.toString(), "tron:$address?token=$contract");
|
||||
});
|
||||
|
||||
test("converts a comma decimal separator in the amount", () {
|
||||
final uri = TronURI(amount: "1,5", address: address);
|
||||
|
||||
expect(uri.toString(), "tron:$address?amount=1.5");
|
||||
});
|
||||
|
||||
test("emits a bare address without amount or contract", () {
|
||||
final uri = TronURI(amount: "", address: address);
|
||||
|
||||
expect(uri.toString(), "tron:$address");
|
||||
});
|
||||
});
|
||||
|
||||
group("SolanaURI", () {
|
||||
const address = "4Nd1mYvNQyJ8BDVXLgkvSGpVdQMZ3hxwVFkfwXNq6Wgk";
|
||||
const mint = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
|
||||
|
||||
test("includes the SPL mint as the spl-token param", () {
|
||||
final uri = SolanaURI(amount: "2", address: address, contractAddress: mint);
|
||||
|
||||
expect(uri.toString(), "solana:$address?amount=2&spl-token=$mint");
|
||||
});
|
||||
|
||||
test("omits the spl-token param for native SOL", () {
|
||||
final uri = SolanaURI(amount: "2", address: address);
|
||||
|
||||
expect(uri.toString(), "solana:$address?amount=2");
|
||||
expect(uri.toString().contains("spl-token="), false);
|
||||
});
|
||||
|
||||
test("includes the spl-token param when there is no amount", () {
|
||||
final uri = SolanaURI(amount: "", address: address, contractAddress: mint);
|
||||
|
||||
expect(uri.toString(), "solana:$address?spl-token=$mint");
|
||||
});
|
||||
|
||||
test("converts a comma decimal separator in the amount", () {
|
||||
final uri = SolanaURI(amount: "0,5", address: address);
|
||||
|
||||
expect(uri.toString(), "solana:$address?amount=0.5");
|
||||
});
|
||||
|
||||
test("emits a bare address without amount or mint", () {
|
||||
final uri = SolanaURI(amount: "", address: address);
|
||||
|
||||
expect(uri.toString(), "solana:$address");
|
||||
});
|
||||
});
|
||||
|
||||
group("ERC681URI", () {
|
||||
const recipient = "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6";
|
||||
const contract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
|
||||
test("includes the mainnet chainId on a plain native transfer", () {
|
||||
final uri = ERC681URI(chainId: 1, address: recipient, amount: "", contractAddress: null);
|
||||
|
||||
expect(uri.toString(), "ethereum:$recipient@1");
|
||||
});
|
||||
|
||||
test("appends the chainId for native transfers", () {
|
||||
final uri = ERC681URI(chainId: 137, address: recipient, amount: "", contractAddress: null);
|
||||
|
||||
expect(uri.toString(), "ethereum:$recipient@137");
|
||||
});
|
||||
|
||||
test("emits the native amount in ERC-681 scientific notation", () {
|
||||
final uri = ERC681URI(chainId: 1, address: recipient, amount: "1", contractAddress: null);
|
||||
|
||||
expect(uri.toString(), "ethereum:$recipient@1?value=1.0e18");
|
||||
});
|
||||
|
||||
test("includes the mainnet chainId in the transfer form", () {
|
||||
final uri = ERC681URI(
|
||||
chainId: 1,
|
||||
address: recipient,
|
||||
amount: "200.172148",
|
||||
contractAddress: contract,
|
||||
tokenDecimals: 6,
|
||||
);
|
||||
|
||||
expect(
|
||||
uri.toString(),
|
||||
"ethereum:$contract@1/transfer?address=$recipient&amount=200.172148",
|
||||
);
|
||||
});
|
||||
|
||||
test("builds the transfer form for a token", () {
|
||||
final uri =
|
||||
ERC681URI(chainId: 137, address: recipient, amount: "", contractAddress: contract);
|
||||
|
||||
expect(uri.toString(), "ethereum:$contract@137/transfer?address=$recipient");
|
||||
});
|
||||
|
||||
test("parses the transfer form back into recipient, contract and chainId", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse("ethereum:$contract@1/transfer?address=$recipient&uint256=1000000"),
|
||||
);
|
||||
|
||||
expect(parsed.address, recipient);
|
||||
expect(parsed.contractAddress, contract);
|
||||
expect(parsed.chainId, 1);
|
||||
expect(parsed.rawTokenAmount, "1000000");
|
||||
expect(parsed.amount, "");
|
||||
});
|
||||
|
||||
test("keeps the uint256 raw so the caller can apply the token decimals", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse(
|
||||
"ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7@1/transfer?address=$recipient&uint256=1000000",
|
||||
),
|
||||
);
|
||||
|
||||
expect(parsed.rawTokenAmount, "1000000");
|
||||
expect(parsed.amount, "");
|
||||
});
|
||||
|
||||
test("prefers the amount param over the uint256 when both are present", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse(
|
||||
"ethereum:$contract@1/transfer?address=$recipient&uint256=1000000000000000000&amount=1",
|
||||
),
|
||||
);
|
||||
|
||||
expect(parsed.amount, "1");
|
||||
expect(parsed.rawTokenAmount, "1000000000000000000");
|
||||
});
|
||||
|
||||
test("treats a uint256 with a decimal point as a display amount", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse("ethereum:$contract@1/transfer?address=$recipient&uint256=1.5"),
|
||||
);
|
||||
|
||||
expect(parsed.amount, "1.5");
|
||||
expect(parsed.rawTokenAmount, null);
|
||||
});
|
||||
|
||||
test("parses a native transfer with chainId", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse("ethereum:$recipient@137?value=2000000000000000000"),
|
||||
);
|
||||
|
||||
expect(parsed.address, recipient);
|
||||
expect(parsed.contractAddress, null);
|
||||
expect(parsed.chainId, 137);
|
||||
expect(parsed.amount, "2");
|
||||
});
|
||||
|
||||
test("emits the uint256 and the amount param for an 18 decimal token", () {
|
||||
final uri =
|
||||
ERC681URI(chainId: 137, address: recipient, amount: "1", contractAddress: contract);
|
||||
|
||||
expect(
|
||||
uri.toString(),
|
||||
"ethereum:$contract@137/transfer?address=$recipient&uint256=1000000000000000000&amount=1",
|
||||
);
|
||||
});
|
||||
|
||||
test("omits the uint256 for tokens that do not use 18 decimals", () {
|
||||
final uri = ERC681URI(
|
||||
chainId: 137,
|
||||
address: recipient,
|
||||
amount: "1.5",
|
||||
contractAddress: contract,
|
||||
tokenDecimals: 6,
|
||||
);
|
||||
|
||||
expect(uri.toString(), "ethereum:$contract@137/transfer?address=$recipient&amount=1.5");
|
||||
});
|
||||
|
||||
test("emits a stored raw token amount verbatim", () {
|
||||
final uri = ERC681URI(
|
||||
chainId: 1,
|
||||
address: recipient,
|
||||
amount: "",
|
||||
contractAddress: contract,
|
||||
tokenDecimals: 6,
|
||||
rawTokenAmount: "1000000",
|
||||
);
|
||||
|
||||
expect(uri.toString(), "ethereum:$contract@1/transfer?address=$recipient&uint256=1000000");
|
||||
});
|
||||
|
||||
test("parses a scientific notation value", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse("ethereum:$recipient@1?value=2.014e18"),
|
||||
);
|
||||
|
||||
expect(parsed.amount, "2.014");
|
||||
});
|
||||
|
||||
test("parses a Cake style amount param", () {
|
||||
final parsed = ERC681URI.fromUri(
|
||||
Uri.parse("ethereum:$recipient?amount=1.5"),
|
||||
);
|
||||
|
||||
expect(parsed.address, recipient);
|
||||
expect(parsed.amount, "1.5");
|
||||
expect(parsed.chainId, 1);
|
||||
});
|
||||
|
||||
test("defaults to mainnet when the chainId is absent", () {
|
||||
final parsed = ERC681URI.fromUri(Uri.parse("ethereum:$recipient"));
|
||||
|
||||
expect(parsed.address, recipient);
|
||||
expect(parsed.contractAddress, null);
|
||||
expect(parsed.chainId, 1);
|
||||
expect(parsed.amount, "");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/spl_token.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("SPL token raw amounts", () {
|
||||
test("6 decimal token renders raw base units", () {
|
||||
final jup = SPLToken(
|
||||
name: "Jupiter",
|
||||
symbol: "JUP",
|
||||
mint: "jup",
|
||||
mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
|
||||
decimal: 6,
|
||||
);
|
||||
|
||||
expect(Money(BigInt.from(100000), jup).toStringWithPrecision(), "0.1");
|
||||
expect(Money(BigInt.from(931614), jup).toStringWithPrecision(), "0.931614");
|
||||
});
|
||||
|
||||
test("8 decimal token renders raw base units", () {
|
||||
final xstock = SPLToken(
|
||||
name: "Amazon xStock",
|
||||
symbol: "AMZNX",
|
||||
mint: "amznx",
|
||||
mintAddress: "mintAddressWithEightDecimals",
|
||||
decimal: 8,
|
||||
);
|
||||
|
||||
expect(Money(BigInt.from(341694), xstock).toStringWithPrecision(), "0.00341694");
|
||||
});
|
||||
|
||||
test("the same raw amount means different values per decimals", () {
|
||||
final raw = BigInt.from(100000);
|
||||
|
||||
final six = SPLToken(
|
||||
name: "Six",
|
||||
symbol: "SIX",
|
||||
mint: "six",
|
||||
mintAddress: "mintAddressWithSixDecimals",
|
||||
decimal: 6,
|
||||
);
|
||||
|
||||
final nine = SPLToken(
|
||||
name: "Nine",
|
||||
symbol: "NINE",
|
||||
mint: "nine",
|
||||
mintAddress: "mintAddressWithNineDecimals",
|
||||
decimal: 9,
|
||||
);
|
||||
|
||||
final zero = SPLToken(
|
||||
name: "Zero",
|
||||
symbol: "ZERO",
|
||||
mint: "zero",
|
||||
mintAddress: "mintAddressWithZeroDecimals",
|
||||
decimal: 0,
|
||||
);
|
||||
|
||||
expect(Money(raw, six).toStringWithPrecision(), "0.1");
|
||||
expect(Money(raw, nine).toStringWithPrecision(), "0.0001");
|
||||
expect(Money(raw, zero).toStringWithPrecision(), "100000");
|
||||
});
|
||||
});
|
||||
|
||||
group("SPL token amount parsing", () {
|
||||
test("parses whole amounts for a zero decimal token", () {
|
||||
final zero = SPLToken(
|
||||
name: "Zero",
|
||||
symbol: "ZERO",
|
||||
mint: "zero",
|
||||
mintAddress: "mintAddressWithZeroDecimals",
|
||||
decimal: 0,
|
||||
);
|
||||
|
||||
expect(Money.parse("5", zero).amount, BigInt.from(5));
|
||||
expect(Money.parse("0", zero).amount, BigInt.zero);
|
||||
});
|
||||
|
||||
test("parses fractional amounts for a 6 decimal token", () {
|
||||
final jup = SPLToken(
|
||||
name: "Jupiter",
|
||||
symbol: "JUP",
|
||||
mint: "jup",
|
||||
mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
|
||||
decimal: 6,
|
||||
);
|
||||
|
||||
expect(Money.parse("0.1", jup).amount, BigInt.from(100000));
|
||||
expect(Money.parse("10.339089", jup).amount, BigInt.from(10339089));
|
||||
});
|
||||
|
||||
test("rejects more fractional digits than the token has decimals", () {
|
||||
final zero = SPLToken(
|
||||
name: "Zero",
|
||||
symbol: "ZERO",
|
||||
mint: "zero",
|
||||
mintAddress: "mintAddressWithZeroDecimals",
|
||||
decimal: 0,
|
||||
);
|
||||
|
||||
expect(() => Money.parse("5.5", zero), throwsFormatException);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user