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:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
+26 -6
View File
@@ -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,
};
}
-15
View File
@@ -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();
}
+5 -5
View File
@@ -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;
}
+45
View File
@@ -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;
}
+15
View File
@@ -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);
}
+48
View File
@@ -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",
);
}
}
+316
View File
@@ -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());
}
+16
View File
@@ -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)));
}
+44
View File
@@ -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;
}
}
+13
View File
@@ -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;
}
-73
View File
@@ -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
View File
@@ -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;
}
+125
View File
@@ -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;
}
+104 -4
View File
@@ -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
View File
@@ -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,
});
}
+4 -1
View File
@@ -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";
+85 -30
View File
@@ -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);
}
+13
View File
@@ -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];
+18 -1
View File
@@ -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;
}
+39
View File
@@ -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/");
+4
View File
@@ -0,0 +1,4 @@
abstract class CurrencyGroups {
static const stablecoin = 'stablecoin';
static const tokenizedStock = 'tokenized_stock';
}
+357 -72
View File
@@ -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
);
""");
}
+177
View File
@@ -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();
}
}
+18 -25
View File
@@ -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,
);
}
}
-2
View File
@@ -1,5 +1,3 @@
import 'package:flutter/foundation.dart';
abstract class EnumerableItem<T> {
const EnumerableItem({required this.title, required this.raw});
+1 -1
View File
@@ -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
View File
@@ -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";
}
+213
View File
@@ -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;
}
+59
View File
@@ -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;
}
+27 -5
View File
@@ -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;
}
+19 -8
View File
@@ -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 -2
View File
@@ -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,
+416
View File
@@ -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();
+138
View File
@@ -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],
);
}
}
-27
View File
@@ -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
View File
@@ -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;
-17
View File
@@ -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;
}
+8 -8
View File
@@ -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;
}
+54
View File
@@ -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;
}
+6 -6
View File
@@ -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;
}
+48
View File
@@ -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
View File
@@ -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(
+230
View File
@@ -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;
}
+75
View File
@@ -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;
}
+159
View File
@@ -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;
}
}
+14 -13
View File
@@ -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;
}
+25 -8
View File
@@ -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;
}
+41
View File
@@ -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)));
}
}
}
+11 -11
View File
@@ -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;
+63
View File
@@ -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
View File
@@ -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);
}
}
}
+9 -9
View File
@@ -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();
-5
View File
@@ -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
View File
@@ -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";
}
+133
View File
@@ -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,
);
}
}
+62
View File
@@ -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;
}
+8 -6
View File
@@ -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
View File
@@ -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";
}
+128
View File
@@ -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,
);
}
}
+59
View File
@@ -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;
}
+14 -14
View File
@@ -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 ?? '';
+74
View File
@@ -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
View File
@@ -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);
}
+25
View File
@@ -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";
}
+25
View File
@@ -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";
}
+6 -5
View File
@@ -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) {
+19 -3
View File
@@ -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
View File
@@ -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");
}
}
}
+4 -1
View File
@@ -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);
+50 -7
View File
@@ -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 "";
}
}
+58 -52
View File
@@ -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) {
+104
View File
@@ -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;
}
-17
View File
@@ -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
View File
@@ -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;
+74
View File
@@ -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;
}