mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -1,12 +1,32 @@
|
||||
class Account {
|
||||
Account({required this.id, required this.label, this.balance});
|
||||
Account({
|
||||
required this.id,
|
||||
required this.label,
|
||||
String? balance,
|
||||
}) : balance = balance ?? "0.00";
|
||||
|
||||
Account.fromMap(Map<String, Object> map)
|
||||
: this.id = map['id'] == null ? 0 : int.parse(map['id'] as String),
|
||||
this.label = (map['label'] ?? '') as String,
|
||||
this.balance = (map['balance'] ?? '0.00') as String;
|
||||
Account.fromMap(Map<String, Object?> map)
|
||||
: id = map["id"] is int ? map["id"] as int : int.tryParse(map["id"]?.toString() ?? "") ?? 0,
|
||||
label = (map["label"] ?? "") as String,
|
||||
balance = (map["balance"] ?? "0.00") as String;
|
||||
|
||||
final int id;
|
||||
final String label;
|
||||
final String? balance;
|
||||
final String balance;
|
||||
|
||||
Account copyWith({
|
||||
int? id,
|
||||
String? label,
|
||||
String? balance,
|
||||
}) => Account(
|
||||
id: id ?? this.id,
|
||||
label: label ?? this.label,
|
||||
balance: balance ?? this.balance,
|
||||
);
|
||||
|
||||
Map<String, Object> toMap() => {
|
||||
"id": id.toString(),
|
||||
"label": label,
|
||||
"balance": balance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
abstract class AccountList<T> {
|
||||
ObservableList<T> get accounts;
|
||||
|
||||
void update();
|
||||
|
||||
List<T> getAll();
|
||||
|
||||
Future<void> addAccount({required String label});
|
||||
|
||||
Future<void> setLabelAccount({required int accountIndex, required String label});
|
||||
|
||||
void refresh();
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'address_info.g.dart';
|
||||
part 'address_info.part.dart';
|
||||
|
||||
@HiveType(typeId: ADDRESS_INFO_TYPE_ID)
|
||||
// @HiveType(typeId: ADDRESS_INFO_TYPE_ID)
|
||||
class AddressInfo extends HiveObject {
|
||||
AddressInfo({required this.address, this.accountIndex, required this.label});
|
||||
|
||||
static const typeId = ADDRESS_INFO_TYPE_ID;
|
||||
static const boxName = 'AddressInfo';
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
int? accountIndex;
|
||||
|
||||
@HiveField(1, defaultValue: '')
|
||||
// @HiveField(1, defaultValue: '')
|
||||
String address;
|
||||
|
||||
@HiveField(2, defaultValue: '')
|
||||
// @HiveField(2, defaultValue: '')
|
||||
String label;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'address_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AddressInfoAdapter extends TypeAdapter<AddressInfo> {
|
||||
@override
|
||||
final int typeId = 11;
|
||||
|
||||
@override
|
||||
AddressInfo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AddressInfo(
|
||||
address: fields[1] == null ? '' : fields[1] as String,
|
||||
accountIndex: fields[0] as int?,
|
||||
label: fields[2] == null ? '' : fields[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AddressInfo obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.accountIndex)
|
||||
..writeByte(1)
|
||||
..write(obj.address)
|
||||
..writeByte(2)
|
||||
..write(obj.label);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AddressInfoAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
String _smartAmountSanitizer(String amount) {
|
||||
if (amount.contains(RegExp(r'^(?=.*[.])(?=.*,).*$'))) {
|
||||
if (amount.indexOf(".") > amount.indexOf(",")) {
|
||||
amount = amount.replaceAll(",", "");
|
||||
} else {
|
||||
amount = amount.replaceAll(".", "");
|
||||
}
|
||||
}
|
||||
|
||||
return amount.replaceAll(",", ".");
|
||||
}
|
||||
|
||||
extension SanitizerUserAmounts on String {
|
||||
String sanitized() => _smartAmountSanitizer(this);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
|
||||
class ExchangeRate {
|
||||
const ExchangeRate({required this.base, required this.quote});
|
||||
|
||||
/// The currency being priced (e.g. BTC in a BTC/USD pair).
|
||||
final Currency base;
|
||||
|
||||
/// The price of one whole unit of [base], in the quote currency
|
||||
/// (e.g. 45000 USD in a BTC/USD pair).
|
||||
final Money quote;
|
||||
|
||||
/// Converts [amount] between the base and quote currencies.
|
||||
///
|
||||
/// Results are truncated toward zero.
|
||||
///
|
||||
/// Throws an [ArgumentError] if [amount]'s currency is not part of
|
||||
/// this pair.
|
||||
Money convert(Money amount) {
|
||||
if (base == quote.currency && base == amount.currency) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
if (amount.currency == base) {
|
||||
final scale = BigInt.from(10).pow(amount.decimals);
|
||||
|
||||
return quote.isZero
|
||||
? Money.zero(quote.currency)
|
||||
: Money(amount.amount * quote.amount ~/ scale, quote.currency, quote.decimals);
|
||||
}
|
||||
|
||||
if (amount.currency == quote.currency) {
|
||||
if (quote.isZero) {
|
||||
return Money.zero(base);
|
||||
}
|
||||
|
||||
final numerator = amount.amount * BigInt.from(10).pow(base.decimals + quote.decimals);
|
||||
final denominator = quote.amount * BigInt.from(10).pow(amount.decimals);
|
||||
|
||||
return Money(numerator ~/ denominator, base);
|
||||
}
|
||||
|
||||
throw ArgumentError(
|
||||
"Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import "dart:math";
|
||||
|
||||
import "package:cw_core/amount/utils.dart";
|
||||
import "package:cw_core/crypto_amount_format.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:cw_core/format_fixed.dart";
|
||||
import "package:cw_core/parse_fixed.dart";
|
||||
|
||||
export "money_local.dart";
|
||||
|
||||
class Money implements Comparable<Money> {
|
||||
const Money(this.amount, this.currency, [int? overrideDecimals])
|
||||
: _overrideDecimals = overrideDecimals;
|
||||
|
||||
factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
|
||||
|
||||
factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency);
|
||||
|
||||
/// Parse the [source] and turn it into [Money] trimming trailing 0s
|
||||
///
|
||||
/// Throws a [FormatException] if the [source] is not a valid decimal or
|
||||
/// not in canonical representation or if it is a decimal when [isBaseUnit]
|
||||
factory Money.parse(
|
||||
String source,
|
||||
Currency currency, {
|
||||
bool isBaseUnit = false,
|
||||
bool strictParsing = true,
|
||||
}) {
|
||||
if (!isBaseUnit) {
|
||||
source = trimTrailingFractionZeros(source);
|
||||
}
|
||||
final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
|
||||
final amount = isBaseUnit ? BigInt.parse(source) : parseFixed(source, decimals);
|
||||
|
||||
return Money(amount, currency, decimals);
|
||||
}
|
||||
|
||||
/// Parse the [source] and turn it into [Money] if possible trimming trailing 0s
|
||||
///
|
||||
/// As [parse] except that this method returns `null` if the input is not
|
||||
/// valid or if it is a decimal when [isBaseUnit]
|
||||
static Money? tryParse(
|
||||
String source,
|
||||
Currency currency, {
|
||||
bool isBaseUnit = false,
|
||||
bool strictParsing = true,
|
||||
}) {
|
||||
try {
|
||||
if (!isBaseUnit) {
|
||||
source = trimTrailingFractionZeros(source);
|
||||
}
|
||||
final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
|
||||
final amount = isBaseUnit ? BigInt.tryParse(source) : tryParseFixed(source, decimals);
|
||||
|
||||
return amount != null ? Money(amount, currency, decimals) : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Money? trySafeParse(source, Currency currency, {bool isBaseUnit = false}) {
|
||||
final amount = isBaseUnit
|
||||
? BigInt.tryParse(source.toString())
|
||||
: tryParseFixed(source.toString().withDecimals(currency.decimals), currency.decimals);
|
||||
|
||||
return amount != null ? Money(amount, currency) : null;
|
||||
}
|
||||
|
||||
final BigInt amount;
|
||||
final Currency currency;
|
||||
|
||||
final int? _overrideDecimals;
|
||||
|
||||
/// Returns the amount of decimals of [currency]
|
||||
int get decimals => _overrideDecimals ?? currency.decimals;
|
||||
|
||||
/// Returns the sign of this [BigInt] amount.
|
||||
/// Returns 0 for zero, -1 for values less than zero and +1 for values
|
||||
/// greater than zero.
|
||||
int get sign => amount.sign;
|
||||
|
||||
/// Returns `true` when amount of this money is zero.
|
||||
bool get isZero => amount == BigInt.zero;
|
||||
|
||||
/// Returns `true` when amount of this money is negative.
|
||||
bool get isNegative => amount.isNegative;
|
||||
|
||||
/// Compares this to [other].
|
||||
///
|
||||
/// [other] has to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
@override
|
||||
int compareTo(Money other) {
|
||||
_assertSameCurrency(other);
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a.compareTo(aligned.b);
|
||||
}
|
||||
|
||||
/// Returns `true` if [other] is the same amount of money in
|
||||
/// the same currency.
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other is! Money || other.currency != currency) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a == aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is less than [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator <(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a < aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is less than or equal to [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator <=(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a <= aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is greater than [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator >(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a > aligned.b;
|
||||
}
|
||||
|
||||
/// Returns `true` when this money is greater than or equal to [other].
|
||||
///
|
||||
/// Both operands have to be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
bool operator >=(Money other) {
|
||||
_assertSameCurrency(other, "Cannot compare money in different currencies.");
|
||||
|
||||
final aligned = _align(other);
|
||||
return aligned.a >= aligned.b;
|
||||
}
|
||||
|
||||
/// Adds the amount of [other] to this amount.
|
||||
///
|
||||
/// Both operands must be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
Money operator +(Money other) {
|
||||
_assertSameCurrency(other);
|
||||
|
||||
final aligned = _align(other);
|
||||
return copyWith(amount: aligned.a + aligned.b, decimals: aligned.decimals);
|
||||
}
|
||||
|
||||
/// unary minus operator.
|
||||
Money operator -() => copyWith(amount: -amount);
|
||||
|
||||
/// Subtracts the amount of [other] from this amount.
|
||||
///
|
||||
/// Both operands must be in same currency, [ArgumentError] will be thrown
|
||||
/// otherwise.
|
||||
Money operator -(Money other) {
|
||||
_assertSameCurrency(other);
|
||||
|
||||
final aligned = _align(other);
|
||||
return copyWith(amount: aligned.a - aligned.b, decimals: aligned.decimals);
|
||||
}
|
||||
|
||||
/// Returns [Money] multiplied by [other].
|
||||
///
|
||||
/// The result is again [Money].
|
||||
Money operator *(BigInt other) => copyWith(amount: amount * other);
|
||||
|
||||
/// Returns [Money] divided by [other].
|
||||
///
|
||||
/// The result is again [Money].
|
||||
Money operator /(BigInt other) {
|
||||
if (other == BigInt.zero) {
|
||||
throw Exception("Division by zero.");
|
||||
}
|
||||
|
||||
final neg = (amount.isNegative) ^ (other.isNegative);
|
||||
final A = amount.abs();
|
||||
final B = other.abs();
|
||||
|
||||
final q = A ~/ B;
|
||||
final r = A % B;
|
||||
|
||||
// Compare 2*r with B to detect half/up/down
|
||||
final twiceR = r << 1;
|
||||
final mag = (twiceR >= B) ? q + BigInt.one : q;
|
||||
|
||||
return copyWith(amount: neg ? -mag : mag);
|
||||
}
|
||||
|
||||
/// Creates a copy of this [Money] object with optional new values
|
||||
/// for [amount], [currency].
|
||||
///
|
||||
/// If just [currency] is provided and the decimals missmatch
|
||||
/// the amount will be transformed to keep its canonical representation
|
||||
Money copyWith({BigInt? amount, Currency? currency, int? decimals}) {
|
||||
if (currency != null && amount == null && decimals == null &&
|
||||
currency.decimals != this.decimals) {
|
||||
return Money(
|
||||
_transformAmount(this.amount, this.decimals, currency.decimals),
|
||||
currency,
|
||||
currency.decimals,
|
||||
);
|
||||
}
|
||||
|
||||
return Money(amount ?? this.amount, currency ?? this.currency, decimals ?? this.decimals);
|
||||
}
|
||||
|
||||
void _assertSameCurrency(Money other, [String? message]) {
|
||||
if (currency != other.currency) {
|
||||
throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
|
||||
}
|
||||
}
|
||||
|
||||
static BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
|
||||
final diff = targetDecimals - sourceDecimals;
|
||||
|
||||
return diff == 0
|
||||
? source
|
||||
: diff > 0
|
||||
? source * BigInt.from(10).pow(diff)
|
||||
: source ~/ BigInt.from(10).pow(-diff);
|
||||
}
|
||||
|
||||
({BigInt a, BigInt b, int decimals}) _align(Money other) {
|
||||
final decimals = max(this.decimals, other.decimals);
|
||||
return (
|
||||
a: _transformAmount(amount, this.decimals, decimals),
|
||||
b: _transformAmount(other.amount, other.decimals, decimals),
|
||||
decimals: decimals,
|
||||
);
|
||||
}
|
||||
|
||||
static int _getActualDecimals(String value, Currency currency) {
|
||||
final comps = value.split(".");
|
||||
if (comps.length > 2) {
|
||||
throw FormatException("Money._getActualDecimals: too many decimal points, value, $value");
|
||||
}
|
||||
|
||||
return max(comps.length == 2 ? comps[1].length : 0, currency.decimals);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
final value = formatFixed(amount, this.decimals, trimZeros: true);
|
||||
final decimals = _getActualDecimals(value, currency);
|
||||
|
||||
return Object.hash(value, decimals, currency);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => formatFixed(amount, decimals);
|
||||
|
||||
String toStringWithSymbol({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
bool withSymbolPrefix = false,
|
||||
}) {
|
||||
final amount = toStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit,
|
||||
);
|
||||
final symbol = getSymbol(useBaseUnit: useBaseUnit);
|
||||
|
||||
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
|
||||
}
|
||||
|
||||
String toStringWithPrecision({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
}) =>
|
||||
formatFixed(
|
||||
decimals != currency.decimals
|
||||
? _transformAmount(amount, decimals, currency.decimals)
|
||||
: amount,
|
||||
useBaseUnit ? 0 : currency.decimals,
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
);
|
||||
|
||||
// To Override the symbol with the ticker of the base unit
|
||||
String getSymbol({required bool useBaseUnit}) {
|
||||
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
|
||||
return "sats";
|
||||
}
|
||||
return currency.symbol;
|
||||
}
|
||||
|
||||
/// new [Money] with absolute value of this instance
|
||||
Money abs() => copyWith(amount: amount.abs());
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import "dart:math";
|
||||
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/amount/utils.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
|
||||
/// Turn a double representation of a currency amount to a proper Money representation
|
||||
/// truncating currencies with more than 20 decimals because double can not handle more
|
||||
extension ToMoney on double {
|
||||
Money? tryToMoney(Currency currency) => Money.tryParse(_toSafeString(currency), currency);
|
||||
|
||||
Money toMoney(Currency currency) => Money.parse(_toSafeString(currency), currency);
|
||||
|
||||
String _toSafeString(Currency currency) =>
|
||||
trimTrailingFractionZeros(toStringAsFixed(min(currency.decimals, 20)));
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_amount_format.dart";
|
||||
|
||||
extension WithLocalSeparator on Money {
|
||||
String toLocalStringWithSymbol({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
bool withSymbolPrefix = false,
|
||||
String? locale,
|
||||
}) {
|
||||
final amount = toLocalStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit,
|
||||
locale: locale,
|
||||
);
|
||||
final symbol = getSymbol(useBaseUnit: useBaseUnit);
|
||||
|
||||
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
|
||||
}
|
||||
|
||||
String toLocalStringWithPrecision({
|
||||
int? fractionalDigits,
|
||||
bool trimZeros = true,
|
||||
bool useBaseUnit = false,
|
||||
String? locale,
|
||||
}) =>
|
||||
_withLocalSeparator(
|
||||
toStringWithPrecision(
|
||||
fractionalDigits: fractionalDigits,
|
||||
trimZeros: trimZeros,
|
||||
useBaseUnit: useBaseUnit,
|
||||
),
|
||||
locale: locale,
|
||||
);
|
||||
|
||||
String _withLocalSeparator(String amount, {String? locale}) {
|
||||
final isNegative = amount.startsWith("-");
|
||||
final formatted = (isNegative ? amount.substring(1) : amount).withLocalSeperator(locale);
|
||||
|
||||
return isNegative ? "-$formatted" : formatted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
String trimTrailingFractionZeros(String value) {
|
||||
if (!value.contains('.')) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var end = value.length;
|
||||
while (end > 0 && value[end - 1] == "0") {
|
||||
end--;
|
||||
}
|
||||
|
||||
final trimmed = end == value.length ? value : value.substring(0, end);
|
||||
return trimmed.endsWith('.') ? trimmed.substring(0, trimmed.length - 1) : trimmed;
|
||||
}
|
||||
@@ -1,77 +1,4 @@
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:decimal/intl.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class AmountConverter {
|
||||
static const _moneroAmountLength = 12;
|
||||
static const _moneroAmountDivider = 1000000000000;
|
||||
static const _wowneroAmountLength = 11;
|
||||
static const _wowneroAmountDivider = 100000000000;
|
||||
static const _bitcoinAmountDivider = 100000000;
|
||||
static const _bitcoinAmountLength = 8;
|
||||
static final _bitcoinAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = _bitcoinAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
static final _moneroAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = _moneroAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
static final _wowneroAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = _wowneroAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
|
||||
static String amountIntToString(CryptoCurrency cryptoCurrency, int amount) {
|
||||
switch (cryptoCurrency) {
|
||||
case CryptoCurrency.xmr:
|
||||
return _moneroAmountToString(amount);
|
||||
case CryptoCurrency.wow:
|
||||
return _wowneroAmountToString(amount);
|
||||
case CryptoCurrency.btc:
|
||||
case CryptoCurrency.bch:
|
||||
case CryptoCurrency.ltc:
|
||||
case CryptoCurrency.dcr:
|
||||
case CryptoCurrency.doge:
|
||||
return _bitcoinAmountToString(amount);
|
||||
case CryptoCurrency.xhv:
|
||||
case CryptoCurrency.xag:
|
||||
case CryptoCurrency.xau:
|
||||
case CryptoCurrency.xaud:
|
||||
case CryptoCurrency.xbtc:
|
||||
case CryptoCurrency.xcad:
|
||||
case CryptoCurrency.xchf:
|
||||
case CryptoCurrency.xcny:
|
||||
case CryptoCurrency.xeur:
|
||||
case CryptoCurrency.xgbp:
|
||||
case CryptoCurrency.xjpy:
|
||||
case CryptoCurrency.xnok:
|
||||
case CryptoCurrency.xnzd:
|
||||
case CryptoCurrency.xusd:
|
||||
return _moneroAmountToString(amount);
|
||||
case CryptoCurrency.zano:
|
||||
return _moneroAmountToStringUsingDecimals(amount);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
static double cryptoAmountToDouble({required num amount, required num divider}) =>
|
||||
amount / divider;
|
||||
|
||||
static String _moneroAmountToString(int amount) => _moneroAmountFormat
|
||||
.format(cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider));
|
||||
|
||||
static String _bitcoinAmountToString(int amount) => _bitcoinAmountFormat
|
||||
.format(cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider));
|
||||
|
||||
static String _wowneroAmountToString(int amount) => _wowneroAmountFormat
|
||||
.format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider));
|
||||
|
||||
static Decimal cryptoAmountToDecimal({required int amount, required int divider}) =>
|
||||
(Decimal.fromInt(amount) / Decimal.fromInt(divider)).toDecimal();
|
||||
|
||||
static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat
|
||||
.format(DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider)));
|
||||
|
||||
static String toBaseUnits(String amount, int decimals) {
|
||||
amount = amount.trim();
|
||||
if (amount.isEmpty) return '0';
|
||||
|
||||
+14
-20
@@ -1,26 +1,20 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
/// Balance Data class with all amounts in the lowest possible currency (e.g. satoshis or wei)
|
||||
abstract class Balance {
|
||||
const Balance(this.available, this.additional,
|
||||
{this.secondAvailable, this.secondAdditional, this.frozen});
|
||||
const Balance(
|
||||
this.available,
|
||||
this.unavailable, {
|
||||
this.secondAvailable,
|
||||
this.secondUnavailable,
|
||||
this.frozen,
|
||||
});
|
||||
|
||||
Balance.fromInt(int available, int additional,
|
||||
{int? secondAvailable, int? secondAdditional, int? frozen})
|
||||
: available = BigInt.from(available),
|
||||
additional = BigInt.from(additional),
|
||||
secondAvailable = secondAvailable == null ? null : BigInt.from(secondAvailable),
|
||||
secondAdditional = secondAdditional == null ? null : BigInt.from(secondAdditional),
|
||||
frozen = frozen == null ? null : BigInt.from(frozen) {}
|
||||
final Money available;
|
||||
final Money unavailable;
|
||||
|
||||
final BigInt available;
|
||||
final BigInt additional;
|
||||
final Money? secondAvailable;
|
||||
final Money? secondUnavailable;
|
||||
|
||||
final BigInt? secondAvailable;
|
||||
final BigInt? secondAdditional;
|
||||
|
||||
final BigInt? frozen;
|
||||
|
||||
BigInt get fullAvailableBalance => available;
|
||||
|
||||
@deprecated
|
||||
String get formattedUnAvailableBalance => '';
|
||||
final Money? frozen;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
|
||||
class BalanceCardLayout {
|
||||
BalanceCardLayout._(
|
||||
this._settings, {
|
||||
required List<int> visible,
|
||||
required List<int> hidden,
|
||||
required this.needsRepair,
|
||||
}) : visible = List<int>.unmodifiable(visible),
|
||||
hidden = List<int>.unmodifiable(hidden);
|
||||
|
||||
factory BalanceCardLayout.resolve({
|
||||
required List<int> accountIndices,
|
||||
required List<BalanceCardStyleSettings> settings,
|
||||
}) {
|
||||
final List<int> accounts = [];
|
||||
for (final accountIndex in accountIndices) {
|
||||
if (!accounts.contains(accountIndex)) {
|
||||
accounts.add(accountIndex);
|
||||
}
|
||||
}
|
||||
|
||||
final settingsByAccount = <int, BalanceCardStyleSettings>{};
|
||||
for (final setting in settings) {
|
||||
if (accounts.contains(setting.accountIndex)) {
|
||||
settingsByAccount.putIfAbsent(setting.accountIndex, () => setting);
|
||||
}
|
||||
}
|
||||
|
||||
final hidden = [
|
||||
for (final accountIndex in accounts)
|
||||
if (settingsByAccount[accountIndex]?.hidden ?? false) accountIndex,
|
||||
]..sort();
|
||||
|
||||
final visible = [
|
||||
for (final accountIndex in accounts)
|
||||
if (!hidden.contains(accountIndex)) accountIndex,
|
||||
];
|
||||
|
||||
visible.sort((a, b) => _compareStackPosition(a, b, settingsByAccount));
|
||||
|
||||
var needsRepair = false;
|
||||
for (var position = 0; position < visible.length; position++) {
|
||||
final stored = settingsByAccount[visible[position]];
|
||||
if (stored != null && stored.cardOrder != position) {
|
||||
needsRepair = true;
|
||||
}
|
||||
}
|
||||
|
||||
return BalanceCardLayout._(
|
||||
settingsByAccount,
|
||||
visible: visible,
|
||||
hidden: hidden,
|
||||
needsRepair: needsRepair,
|
||||
);
|
||||
}
|
||||
|
||||
final Map<int, BalanceCardStyleSettings> _settings;
|
||||
|
||||
final List<int> visible;
|
||||
final List<int> hidden;
|
||||
|
||||
// flag when db order is broken
|
||||
final bool needsRepair;
|
||||
|
||||
List<int> get designOrder => designOrderOf(visible);
|
||||
|
||||
static List<int> stackFromOrder(Map<int, int> cardOrder, {bool forceSingleCard = false}) {
|
||||
final positions = cardOrder.keys.toList()..sort();
|
||||
final accountIndices = [for (final position in positions) cardOrder[position]!];
|
||||
|
||||
if (accountIndices.isEmpty || !forceSingleCard) {
|
||||
return accountIndices;
|
||||
}
|
||||
return [accountIndices.first];
|
||||
}
|
||||
|
||||
static List<int> designOrderOf(Iterable<int> accountIndices) =>
|
||||
List<int>.of(accountIndices)..sort();
|
||||
|
||||
Map<int, int> get orders =>
|
||||
{for (var position = 0; position < visible.length; position++) visible[position]: position};
|
||||
|
||||
BalanceCardStyleSettings? settingFor(int accountIndex) => _settings[accountIndex];
|
||||
|
||||
BalanceCardLayout unhiding(int accountIndex) {
|
||||
if (!hidden.contains(accountIndex)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return BalanceCardLayout._(
|
||||
_settings,
|
||||
visible: [...visible, accountIndex],
|
||||
hidden: [
|
||||
for (final hiddenAccount in hidden)
|
||||
if (hiddenAccount != accountIndex) hiddenAccount,
|
||||
],
|
||||
needsRepair: true,
|
||||
);
|
||||
}
|
||||
|
||||
static int _compareStackPosition(
|
||||
int a,
|
||||
int b,
|
||||
Map<int, BalanceCardStyleSettings> settingsByAccount,
|
||||
) {
|
||||
final orderA = _storedOrder(settingsByAccount[a]);
|
||||
final orderB = _storedOrder(settingsByAccount[b]);
|
||||
|
||||
if (orderA != orderB) {
|
||||
if (orderA == null) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB == null) {
|
||||
return -1;
|
||||
}
|
||||
return orderA.compareTo(orderB);
|
||||
}
|
||||
|
||||
return a.compareTo(b);
|
||||
}
|
||||
|
||||
static int? _storedOrder(BalanceCardStyleSettings? setting) =>
|
||||
setting == null || setting.cardOrder < 0 ? null : setting.cardOrder;
|
||||
}
|
||||
@@ -7,7 +7,10 @@ class BalanceCardStyleSettings {
|
||||
final int accountIndex;
|
||||
final int gradientIndex;
|
||||
final bool useSpecialDesign;
|
||||
final bool hidden;
|
||||
final String backgroundImagePath;
|
||||
final int iconStyleIndex;
|
||||
final bool isGradientOnly;
|
||||
final int cardOrder;
|
||||
|
||||
BalanceCardStyleSettings(
|
||||
@@ -15,7 +18,10 @@ class BalanceCardStyleSettings {
|
||||
required this.accountIndex,
|
||||
required this.gradientIndex,
|
||||
required this.useSpecialDesign,
|
||||
required this.hidden,
|
||||
required this.backgroundImagePath,
|
||||
this.iconStyleIndex = 0,
|
||||
this.isGradientOnly = false,
|
||||
required this.cardOrder});
|
||||
|
||||
static const tableName = "BalanceCardStyleSettings";
|
||||
@@ -25,8 +31,11 @@ class BalanceCardStyleSettings {
|
||||
"walletInfoId": walletInfoId,
|
||||
"accountIndex": accountIndex,
|
||||
"gradientIndex": gradientIndex,
|
||||
"hidden": hidden ? 1 : 0,
|
||||
"useSpecialDesign": useSpecialDesign ? 1 : 0,
|
||||
"backgroundImagePath": backgroundImagePath,
|
||||
"iconStyleIndex": iconStyleIndex,
|
||||
"isGradientOnly": isGradientOnly ? 1 : 0,
|
||||
"cardOrder": cardOrder,
|
||||
};
|
||||
return ret;
|
||||
@@ -36,22 +45,61 @@ class BalanceCardStyleSettings {
|
||||
return BalanceCardStyleSettings(
|
||||
walletInfoId: json["walletInfoId"] as int,
|
||||
accountIndex: json["accountIndex"] as int,
|
||||
hidden: json["hidden"] == 1,
|
||||
gradientIndex: json["gradientIndex"] as int,
|
||||
useSpecialDesign: json["useSpecialDesign"] == 1,
|
||||
backgroundImagePath: json["backgroundImagePath"] as String? ?? "",
|
||||
iconStyleIndex: json["iconStyleIndex"] as int? ?? 0,
|
||||
isGradientOnly: json["isGradientOnly"] == 1,
|
||||
cardOrder: json["cardOrder"] as int? ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
static BalanceCardStyleSettings fromCardDesign(
|
||||
int walletInfoId, int accountIndex, int cardOrder, CardDesign design) {
|
||||
BalanceCardStyleSettings copyWith({
|
||||
int? walletInfoId,
|
||||
int? accountIndex,
|
||||
int? gradientIndex,
|
||||
int? iconStyleIndex,
|
||||
bool? isGradientOnly,
|
||||
bool? useSpecialDesign,
|
||||
bool? hidden,
|
||||
String? backgroundImagePath,
|
||||
int? cardOrder,
|
||||
}) {
|
||||
return BalanceCardStyleSettings(
|
||||
walletInfoId: walletInfoId ?? this.walletInfoId,
|
||||
accountIndex: accountIndex ?? this.accountIndex,
|
||||
gradientIndex: gradientIndex ?? this.gradientIndex,
|
||||
useSpecialDesign: useSpecialDesign ?? this.useSpecialDesign,
|
||||
hidden: hidden ?? this.hidden,
|
||||
backgroundImagePath: backgroundImagePath ?? this.backgroundImagePath,
|
||||
cardOrder: cardOrder ?? this.cardOrder,
|
||||
iconStyleIndex: iconStyleIndex ?? this.iconStyleIndex,
|
||||
isGradientOnly: isGradientOnly ?? this.isGradientOnly
|
||||
);
|
||||
}
|
||||
|
||||
static BalanceCardStyleSettings fromCardDesign({
|
||||
required int walletInfoId,
|
||||
required int accountIndex,
|
||||
required int cardOrder,
|
||||
required CardDesign design,
|
||||
int iconStyleIndex = 0,
|
||||
int? gradientIndexOverride,
|
||||
bool hidden = false
|
||||
}) {
|
||||
final int gradientIndex =
|
||||
gradientIndexOverride ?? CardDesign.allGradients.indexOf(design.gradient);
|
||||
return BalanceCardStyleSettings(
|
||||
walletInfoId: walletInfoId,
|
||||
accountIndex: accountIndex,
|
||||
gradientIndex: CardDesign.allGradients.indexOf(design.gradient),
|
||||
gradientIndex: gradientIndex,
|
||||
useSpecialDesign: design.backgroundType == CardDesignBackgroundTypes.svgFull,
|
||||
hidden: hidden,
|
||||
backgroundImagePath:
|
||||
design.backgroundType == CardDesignBackgroundTypes.image ? design.imagePath : "",
|
||||
iconStyleIndex: iconStyleIndex,
|
||||
isGradientOnly: design.backgroundType == CardDesignBackgroundTypes.gradientOnly,
|
||||
cardOrder: cardOrder,
|
||||
);
|
||||
}
|
||||
@@ -80,6 +128,58 @@ class BalanceCardStyleSettings {
|
||||
}
|
||||
|
||||
Future<void> insert() async {
|
||||
db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
static Future<void> setVisibleOrder(int walletInfoId, Map<int, int> orderByAccountIndex) =>
|
||||
_setOrder(walletInfoId, orderByAccountIndex, unhide: true);
|
||||
|
||||
static Future<void> setOrder(int walletInfoId, Map<int, int> orderByAccountIndex) =>
|
||||
_setOrder(walletInfoId, orderByAccountIndex, unhide: false);
|
||||
|
||||
static Future<void> _setOrder(
|
||||
int walletInfoId,
|
||||
Map<int, int> orderByAccountIndex, {
|
||||
required bool unhide,
|
||||
}) async {
|
||||
if (orderByAccountIndex.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final entries = orderByAccountIndex.entries.toList();
|
||||
final batch = db!.batch();
|
||||
|
||||
for (final entry in entries) {
|
||||
batch.update(
|
||||
tableName,
|
||||
{"cardOrder": entry.value, if (unhide) "hidden": 0},
|
||||
where: "walletInfoId = ? AND accountIndex = ?",
|
||||
whereArgs: [walletInfoId, entry.key],
|
||||
);
|
||||
}
|
||||
|
||||
final updatedRowCounts = await batch.commit();
|
||||
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
if (updatedRowCounts[i] == 0) {
|
||||
await db!.insert(
|
||||
tableName,
|
||||
positionOnly(walletInfoId, entries[i].key, entries[i].value).toJson(),
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static BalanceCardStyleSettings positionOnly(
|
||||
int walletInfoId, int accountIndex, int cardOrder) =>
|
||||
BalanceCardStyleSettings(
|
||||
walletInfoId: walletInfoId,
|
||||
accountIndex: accountIndex,
|
||||
gradientIndex: -1,
|
||||
useSpecialDesign: true,
|
||||
hidden: false,
|
||||
backgroundImagePath: "",
|
||||
cardOrder: cardOrder,
|
||||
);
|
||||
}
|
||||
|
||||
+161
-20
@@ -3,7 +3,7 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CardDesignBackgroundTypes { image, svgIcon, svgFull }
|
||||
enum CardDesignBackgroundTypes { image, svgIcon, svgFull, gradientOnly }
|
||||
|
||||
class CardColorCombination {
|
||||
final Color textColor;
|
||||
@@ -33,18 +33,27 @@ class CardColorCombination {
|
||||
);
|
||||
}
|
||||
|
||||
class CardIconPath {
|
||||
final String path;
|
||||
final bool preColored;
|
||||
|
||||
const CardIconPath(this.path, {this.preColored = false});
|
||||
}
|
||||
|
||||
class CardDesign {
|
||||
final Gradient gradient;
|
||||
final String imagePath;
|
||||
final CardDesignBackgroundTypes backgroundType;
|
||||
final CardColorCombination colors;
|
||||
final bool preColoredIcon;
|
||||
|
||||
const CardDesign(
|
||||
{this.backgroundType = CardDesignBackgroundTypes.svgIcon,
|
||||
this.gradient = const LinearGradient(
|
||||
colors: [Colors.black], begin: Alignment.topCenter, end: Alignment.bottomCenter),
|
||||
this.imagePath = "assets/new-ui/blank.svg",
|
||||
this.colors = CardColorCombination.dark});
|
||||
this.colors = CardColorCombination.dark,
|
||||
this.preColoredIcon = false});
|
||||
|
||||
static const LinearGradient gradientOrange = LinearGradient(
|
||||
colors: <Color>[Color(0xFFFF7C02), Color(0xFFFF5602)],
|
||||
@@ -120,6 +129,9 @@ class CardDesign {
|
||||
|
||||
static const genericDefault = CardDesign(gradient: gradientBlue);
|
||||
|
||||
static const gradientOnlyDesign =
|
||||
CardDesign(backgroundType: CardDesignBackgroundTypes.gradientOnly, gradient: gradientBlue);
|
||||
|
||||
static const btc = CardDesign(imagePath: "assets/new-ui/balance_card_icons/bitcoin.svg");
|
||||
|
||||
static const eth = CardDesign(imagePath: "assets/new-ui/balance_card_icons/ethereum.svg");
|
||||
@@ -156,6 +168,8 @@ class CardDesign {
|
||||
|
||||
static const bnb = CardDesign(imagePath: "assets/new-ui/balance_card_icons/bnb.svg");
|
||||
|
||||
static const robinhood = CardDesign(imagePath: "assets/new-ui/balance_card_icons/robinhood.svg");
|
||||
|
||||
static const ethSpecial = CardDesign(
|
||||
gradient: const LinearGradient(
|
||||
colors: <Color>[Color(0xFF6259FF), Color(0xFF3B20E6)],
|
||||
@@ -311,11 +325,20 @@ class CardDesign {
|
||||
backgroundType: CardDesignBackgroundTypes.svgFull,
|
||||
imagePath: "assets/new-ui/balance_card_backgrounds/bnb.svg");
|
||||
|
||||
static const robinhoodSpecial = CardDesign(
|
||||
gradient: const LinearGradient(
|
||||
colors: <Color>[Color(0xFFBDFE00), Color(0xFF85BE00)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter),
|
||||
backgroundType: CardDesignBackgroundTypes.svgFull,
|
||||
imagePath: "assets/new-ui/balance_card_backgrounds/robinhood.svg");
|
||||
|
||||
CardDesign withGradient(Gradient gradient) => CardDesign(
|
||||
gradient: gradient,
|
||||
colors: preferredColorCombinations[gradient] ?? colors,
|
||||
imagePath: imagePath,
|
||||
backgroundType: backgroundType);
|
||||
backgroundType: backgroundType,
|
||||
preColoredIcon: preColoredIcon);
|
||||
|
||||
CardDesign withGradientAndColorCombination(
|
||||
Gradient gradient, CardColorCombination cardColorCombination) =>
|
||||
@@ -323,7 +346,61 @@ class CardDesign {
|
||||
gradient: gradient,
|
||||
colors: cardColorCombination,
|
||||
imagePath: imagePath,
|
||||
backgroundType: backgroundType);
|
||||
backgroundType: backgroundType,
|
||||
preColoredIcon: preColoredIcon);
|
||||
|
||||
CardDesign withIcon(CardIconPath icon) => CardDesign(
|
||||
gradient: gradient,
|
||||
colors: colors,
|
||||
imagePath: icon.path,
|
||||
backgroundType: backgroundType,
|
||||
preColoredIcon: icon.preColored);
|
||||
|
||||
static const String _balanceCardIconPrefix = "assets/new-ui/balance_card_icons";
|
||||
static const String _chainIconPrefix = "assets/new-ui/card_icons/chain_icons";
|
||||
static const String _ogIconPrefix = "assets/new-ui/card_icons/og_icons";
|
||||
static const String _outlineIconPrefix = "assets/new-ui/card_icons/outline_icons";
|
||||
static const String _symbolIconPrefix = "assets/new-ui/card_icons/symbol_icons";
|
||||
static const String _genericCakeIcon = "$_balanceCardIconPrefix/cake-card-icon.svg";
|
||||
|
||||
static const Map<CryptoCurrency, _CurrencyIconNames> _iconNames = {
|
||||
CryptoCurrency.arbEth: _CurrencyIconNames(ticker: 'arb', longName: 'arbitrum'),
|
||||
CryptoCurrency.baseEth:
|
||||
_CurrencyIconNames(ticker: 'base', longName: 'base', chainFile: 'base_icon'),
|
||||
CryptoCurrency.robEth: _CurrencyIconNames(ticker: "rob", longName: "robinhood"),
|
||||
CryptoCurrency.bch:
|
||||
_CurrencyIconNames(ticker: 'bch', longName: 'bitcoin_cash', chainFile: 'bitcoin-cash'),
|
||||
CryptoCurrency.btc: _CurrencyIconNames(ticker: 'btc', longName: 'bitcoin', outlineFile: 'BTC'),
|
||||
CryptoCurrency.bnb: _CurrencyIconNames(ticker: 'bnb', longName: 'bnb'),
|
||||
CryptoCurrency.dcr: _CurrencyIconNames(ticker: 'dcr', longName: 'decred'),
|
||||
CryptoCurrency.doge: _CurrencyIconNames(ticker: 'doge', longName: 'dogecoin'),
|
||||
CryptoCurrency.eth: _CurrencyIconNames(ticker: 'eth', longName: 'ethereum'),
|
||||
CryptoCurrency.btcln: _CurrencyIconNames(ticker: 'ln', longName: 'lightning'),
|
||||
CryptoCurrency.ltc: _CurrencyIconNames(ticker: 'ltc', longName: 'litecoin'),
|
||||
CryptoCurrency.xmr:
|
||||
_CurrencyIconNames(ticker: 'xmr', longName: 'monero', ogPath: 'assets/images/xmr-og.webp'),
|
||||
CryptoCurrency.nano: _CurrencyIconNames(ticker: 'xno', longName: 'nano'),
|
||||
CryptoCurrency.maticpoly: _CurrencyIconNames(ticker: 'pol', longName: 'polygon'),
|
||||
CryptoCurrency.sol: _CurrencyIconNames(ticker: 'sol', longName: 'solana'),
|
||||
CryptoCurrency.trx:
|
||||
_CurrencyIconNames(ticker: 'trx', longName: 'tron', ogPath: '$_ogIconPrefix/tron-og.svg'),
|
||||
CryptoCurrency.zano: _CurrencyIconNames(ticker: 'zano', longName: 'zano'),
|
||||
CryptoCurrency.zec: _CurrencyIconNames(ticker: 'zec', longName: 'zcash'),
|
||||
};
|
||||
|
||||
static List<CardIconPath> iconPathsForWalletType(CryptoCurrency currency) {
|
||||
final n = _iconNames[currency];
|
||||
if (n == null) return const [];
|
||||
|
||||
return [
|
||||
CardIconPath('$_symbolIconPrefix/${n.ticker}-symbol.svg'),
|
||||
CardIconPath('$_outlineIconPrefix/${n.outlineFile ?? n.ticker}-outline.svg'),
|
||||
CardIconPath('$_balanceCardIconPrefix/${n.longName}.svg'),
|
||||
CardIconPath('$_chainIconPrefix/${n.chainFile ?? n.longName}.svg', preColored: true),
|
||||
CardIconPath(n.ogPath ?? '$_ogIconPrefix/${n.ticker}-og.svg', preColored: true),
|
||||
const CardIconPath(_genericCakeIcon),
|
||||
];
|
||||
}
|
||||
|
||||
static const List<CardDesign> all = [
|
||||
genericDefault,
|
||||
@@ -343,6 +420,7 @@ class CardDesign {
|
||||
arbitrum,
|
||||
zec,
|
||||
bnb,
|
||||
robinhood,
|
||||
ethSpecial,
|
||||
btcSpecial,
|
||||
xmrSpecial,
|
||||
@@ -357,7 +435,8 @@ class CardDesign {
|
||||
zanoSpecial,
|
||||
arbitrumSpecial,
|
||||
zecSpecial,
|
||||
bnbSpecial
|
||||
bnbSpecial,
|
||||
robinhoodSpecial,
|
||||
];
|
||||
|
||||
static CardDesign forCurrencySpecial(CryptoCurrency currency) {
|
||||
@@ -387,6 +466,7 @@ class CardDesign {
|
||||
CryptoCurrency.zec: zec,
|
||||
CryptoCurrency.dcr: dcr,
|
||||
CryptoCurrency.bnb: bnb,
|
||||
CryptoCurrency.robEth: robinhood,
|
||||
};
|
||||
|
||||
static const Map<CryptoCurrency, CardDesign> specialDesignsForCurrencies = {
|
||||
@@ -408,6 +488,7 @@ class CardDesign {
|
||||
CryptoCurrency.arbEth: arbitrumSpecial,
|
||||
CryptoCurrency.zec: zecSpecial,
|
||||
CryptoCurrency.bnb: bnbSpecial,
|
||||
CryptoCurrency.robEth: robinhoodSpecial,
|
||||
};
|
||||
|
||||
static Map<Gradient, CardColorCombination> preferredColorCombinations = {
|
||||
@@ -423,29 +504,89 @@ class CardDesign {
|
||||
CardDesign.gradientBlack: CardColorCombination.black,
|
||||
};
|
||||
|
||||
CardDesign withIconStyleIndex(int iconStyleIndex, CryptoCurrency currency) {
|
||||
final paths = iconPathsForWalletType(currency);
|
||||
if (paths.isEmpty) return this;
|
||||
if (iconStyleIndex < 0 || iconStyleIndex >= paths.length) return this;
|
||||
return withIcon(paths[iconStyleIndex]);
|
||||
}
|
||||
|
||||
static Gradient gradientForStoredIndex(
|
||||
int gradientIndex,
|
||||
CryptoCurrency walletCurrency,
|
||||
) {
|
||||
final n = CardDesign.allGradients.length;
|
||||
final special = specialDesignsForCurrencies[walletCurrency];
|
||||
if (gradientIndex >= 0 && gradientIndex < n) {
|
||||
return CardDesign.allGradients[gradientIndex];
|
||||
}
|
||||
if (gradientIndex == n && special != null) {
|
||||
return special.gradient;
|
||||
}
|
||||
return gradientBlue;
|
||||
}
|
||||
|
||||
static CardDesign fromStyleSettings(
|
||||
BalanceCardStyleSettings? setting, CryptoCurrency walletCurrency) {
|
||||
if (setting == null) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
} else if (setting.backgroundImagePath.isNotEmpty) {
|
||||
return CardDesign(
|
||||
imagePath: setting.backgroundImagePath,
|
||||
}
|
||||
|
||||
if (setting.isGradientOnly) {
|
||||
final gradient = gradientForStoredIndex(setting.gradientIndex, walletCurrency);
|
||||
final textColors = preferredColorCombinations[gradient] ??
|
||||
specialDesignsForCurrencies[walletCurrency]?.colors ??
|
||||
gradientOnlyDesign.colors;
|
||||
return gradientOnlyDesign.withGradientAndColorCombination(gradient, textColors);
|
||||
}
|
||||
|
||||
if (setting.backgroundImagePath.isNotEmpty) {
|
||||
return CardDesign(imagePath: setting.backgroundImagePath);
|
||||
}
|
||||
|
||||
if (setting.useSpecialDesign && setting.gradientIndex != -1) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency).withGradient(
|
||||
gradientForStoredIndex(setting.gradientIndex, walletCurrency),
|
||||
);
|
||||
} else if (setting.useSpecialDesign && setting.gradientIndex != -1) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency)
|
||||
.withGradient(CardDesign.allGradients[setting.gradientIndex]);
|
||||
} else if (!setting.useSpecialDesign && setting.gradientIndex == -1) {
|
||||
}
|
||||
|
||||
if (!setting.useSpecialDesign && setting.gradientIndex == -1) {
|
||||
final specialColors = specialDesignsForCurrencies[walletCurrency] ?? genericDefault;
|
||||
return CardDesign.forCurrencyIcon(walletCurrency)
|
||||
final design = CardDesign.forCurrencyIcon(walletCurrency)
|
||||
.withGradientAndColorCombination(specialColors.gradient, specialColors.colors);
|
||||
} else if (setting.useSpecialDesign) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
} else if (setting.gradientIndex != -1) {
|
||||
return CardDesign.forCurrencyIcon(walletCurrency)
|
||||
.withGradient(CardDesign.allGradients[setting.gradientIndex]);
|
||||
} else {
|
||||
printV("somehow, the user saved the design settings with literally no customization?");
|
||||
return design.withIconStyleIndex(setting.iconStyleIndex, walletCurrency);
|
||||
}
|
||||
if (setting.useSpecialDesign) {
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
}
|
||||
if (setting.gradientIndex != -1) {
|
||||
final baseIcon = CardDesign.forCurrencyIcon(walletCurrency);
|
||||
final gradient = gradientForStoredIndex(setting.gradientIndex, walletCurrency);
|
||||
final textColors = preferredColorCombinations[gradient] ??
|
||||
specialDesignsForCurrencies[walletCurrency]?.colors ??
|
||||
baseIcon.colors;
|
||||
return baseIcon
|
||||
.withGradientAndColorCombination(gradient, textColors)
|
||||
.withIconStyleIndex(setting.iconStyleIndex, walletCurrency);
|
||||
}
|
||||
printV("somehow, the user saved the design settings with literally no "
|
||||
"customization?");
|
||||
return CardDesign.forCurrencySpecial(walletCurrency);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurrencyIconNames {
|
||||
final String ticker;
|
||||
final String longName;
|
||||
final String? outlineFile;
|
||||
final String? chainFile;
|
||||
final String? ogPath;
|
||||
|
||||
const _CurrencyIconNames({
|
||||
required this.ticker,
|
||||
required this.longName,
|
||||
this.outlineFile,
|
||||
this.chainFile,
|
||||
this.ogPath,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,10 @@ extension MaxDecimals on String {
|
||||
return [parts.first.withLocalSeperator(locale), ...parts.sublist(1)].join(" ");
|
||||
}
|
||||
|
||||
final formater = NumberFormat("#,###", locale);
|
||||
final formater = NumberFormat(
|
||||
"#,###",
|
||||
Intl.verifiedLocale(locale, NumberFormat.localeExists, onFailure: (_) => "en"),
|
||||
);
|
||||
final parts = replaceAll(",", "").split(".");
|
||||
if (parts.first.contains("< 0")) parts.first = "0";
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/currency_groups.dart';
|
||||
import 'package:cw_core/enumerable_item.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
import 'package:cw_core/parse_fixed.dart';
|
||||
|
||||
class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implements Currency {
|
||||
const CryptoCurrency({
|
||||
@@ -17,18 +18,34 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
this.chainIconPath,
|
||||
this.enabled = false,
|
||||
this.isPotentialScam = false,
|
||||
this.groups = const {},
|
||||
}) : super(title: title, raw: raw);
|
||||
|
||||
final String name;
|
||||
final String? tag;
|
||||
final String? fullName;
|
||||
final String? iconPath;
|
||||
final String? flatIconPath;
|
||||
final String? chainIconPath;
|
||||
@override
|
||||
final int decimals;
|
||||
final bool enabled;
|
||||
final bool isPotentialScam;
|
||||
final Set<String> groups;
|
||||
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
final String? fullName;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
final int decimals;
|
||||
|
||||
@override
|
||||
String get symbol => title;
|
||||
@override
|
||||
String get apiString => "crypto.$title";
|
||||
|
||||
set enabled(bool value) => this.enabled = value;
|
||||
|
||||
@@ -126,6 +143,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
CryptoCurrency.arbEth,
|
||||
CryptoCurrency.usdcArb,
|
||||
CryptoCurrency.usdtArb,
|
||||
CryptoCurrency.robEth,
|
||||
];
|
||||
|
||||
static const havenCurrencies = [
|
||||
@@ -197,7 +215,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 5,
|
||||
name: 'dai',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/dai.svg',
|
||||
decimals: 18);
|
||||
decimals: 18,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const dash = CryptoCurrency(
|
||||
title: 'DASH',
|
||||
fullName: 'Dash',
|
||||
@@ -254,7 +273,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 12,
|
||||
name: 'usdt',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdterc20 = CryptoCurrency(
|
||||
title: 'USDT',
|
||||
tag: 'ETH',
|
||||
@@ -262,7 +282,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 13,
|
||||
name: 'usdterc20',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const xlm = CryptoCurrency(
|
||||
title: 'XLM',
|
||||
fullName: 'Stellar',
|
||||
@@ -359,7 +380,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 36,
|
||||
name: 'usdttrc20',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const hbar = CryptoCurrency(
|
||||
title: 'HBAR',
|
||||
fullName: 'Hedera',
|
||||
@@ -390,7 +412,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 40,
|
||||
name: 'usdc',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdcsol = CryptoCurrency(
|
||||
title: 'USDC',
|
||||
tag: 'SOL',
|
||||
@@ -398,7 +421,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 41,
|
||||
name: 'usdcsol',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const zaddr = CryptoCurrency(
|
||||
title: 'ZZEC',
|
||||
tag: 'ZEC',
|
||||
@@ -437,7 +461,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 46,
|
||||
name: 'usdcpoly',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const dcr = CryptoCurrency(
|
||||
title: 'DCR',
|
||||
fullName: 'Decred',
|
||||
@@ -758,7 +783,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 87,
|
||||
name: 'usdtpoly',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdcEPoly = CryptoCurrency(
|
||||
title: 'USDC.E',
|
||||
tag: 'POL',
|
||||
@@ -788,7 +814,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 91,
|
||||
name: 'usdtsol',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdcTrc20 = CryptoCurrency(
|
||||
title: 'USDC',
|
||||
tag: 'TRX',
|
||||
@@ -796,7 +823,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 92,
|
||||
name: 'usdctrc20',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const tbtc = CryptoCurrency(
|
||||
title: 'tBTC',
|
||||
fullName: 'Testnet Bitcoin',
|
||||
@@ -844,7 +872,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 98,
|
||||
name: 'deuro',
|
||||
iconPath: 'assets/images/deuro_icon.png',
|
||||
decimals: 18);
|
||||
decimals: 18,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdtbsc = CryptoCurrency(
|
||||
title: 'USDT',
|
||||
tag: 'BSC',
|
||||
@@ -852,7 +881,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 99,
|
||||
name: 'usdtbsc',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 18);
|
||||
decimals: 18,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const ndeps = CryptoCurrency(
|
||||
title: 'NDEPS',
|
||||
tag: 'ETH',
|
||||
@@ -891,7 +921,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
fullName: 'Ethereum',
|
||||
raw: 104,
|
||||
name: 'baseth',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/base.svg',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/ethereum.svg',
|
||||
decimals: 18,
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/base.svg",
|
||||
chainIconPath: "assets/new-ui/chain_badges/base.svg");
|
||||
@@ -911,7 +941,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
name: 'arbeth',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/ethereum.svg',
|
||||
decimals: 18,
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/arbitrum.svg");
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/arbitrum.svg",
|
||||
chainIconPath: "assets/new-ui/chain_badges/arbitrum.svg");
|
||||
static const zec = CryptoCurrency(
|
||||
title: 'ZEC',
|
||||
fullName: 'Zcash',
|
||||
@@ -927,7 +958,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 108,
|
||||
name: 'usdcarb',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdc.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const usdtArb = CryptoCurrency(
|
||||
title: 'USDT',
|
||||
tag: 'ARB',
|
||||
@@ -935,7 +967,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
raw: 109,
|
||||
name: 'usdtarb',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/usdt.svg',
|
||||
decimals: 6);
|
||||
decimals: 6,
|
||||
groups: const {CurrencyGroups.stablecoin});
|
||||
static const ltcmweb = CryptoCurrency(
|
||||
title: 'LTC',
|
||||
fullName: 'Litecoin MWeb',
|
||||
@@ -943,6 +976,16 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
name: 'ltcmweb',
|
||||
iconPath: 'assets/new-ui/crypto_full_icons/litecoin.svg',
|
||||
decimals: 8);
|
||||
static const robEth = CryptoCurrency(
|
||||
title: "ETH",
|
||||
tag: "ROB",
|
||||
fullName: "Ethereum",
|
||||
raw: 111,
|
||||
name: "robeth",
|
||||
iconPath: "assets/new-ui/crypto_full_icons/ethereum.svg",
|
||||
decimals: 18,
|
||||
flatIconPath: "assets/new-ui/balance_card_icons/robinhood.svg",
|
||||
chainIconPath: "assets/new-ui/chain_badges/robinhood.svg");
|
||||
|
||||
static final Map<int, CryptoCurrency> _rawCurrencyMap = [
|
||||
...all,
|
||||
@@ -968,7 +1011,14 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
...zcashCurrencies
|
||||
].fold<Map<String, CryptoCurrency>>(<String, CryptoCurrency>{}, (acc, item) {
|
||||
if (item.fullName != null) {
|
||||
acc.addAll({item.fullName!.toLowerCase(): item});
|
||||
final fullName = item.fullName!.toLowerCase();
|
||||
final existing = acc[fullName];
|
||||
|
||||
// Mainnet ETH and the L2 ETHs share the "Ethereum" full name,
|
||||
// so we make the untagged coin keeps the name
|
||||
if (existing == null || existing.tag != null) {
|
||||
acc[fullName] = item;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
});
|
||||
@@ -978,6 +1028,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
'bitcoin': btc,
|
||||
'bitcoincash': bch,
|
||||
'polygon': maticpoly,
|
||||
'solana': sol,
|
||||
'nano-gpt': nano,
|
||||
'secret': scrt,
|
||||
'stellar': xlm,
|
||||
@@ -993,6 +1044,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
'base': baseEth,
|
||||
'arbitrum': arbEth,
|
||||
'bsc': bnb,
|
||||
"rob": robEth,
|
||||
"robinhood": robEth,
|
||||
};
|
||||
|
||||
static CryptoCurrency deserialize({required int raw}) {
|
||||
@@ -1094,6 +1147,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
int? decimals,
|
||||
bool? enabled,
|
||||
bool? isPotentialScam,
|
||||
Set<String>? groups,
|
||||
}) =>
|
||||
CryptoCurrency(
|
||||
title: title ?? this.title,
|
||||
@@ -1105,6 +1159,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
decimals: decimals ?? this.decimals,
|
||||
enabled: enabled ?? this.enabled,
|
||||
isPotentialScam: isPotentialScam ?? this.isPotentialScam,
|
||||
groups: groups ?? this.groups,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -1112,13 +1167,13 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
|
||||
|
||||
bool titleAndTagEqual(CryptoCurrency other) => title == other.title && tag == other.tag;
|
||||
|
||||
@override
|
||||
Money parseAmount(String value) => Money.parse(value, this);
|
||||
|
||||
@override
|
||||
Money? tryParseAmount(String value) => Money.tryParse(value, this);
|
||||
|
||||
/// Format the raw amount into its decimal representation eg. turn Sats into Bitcoin
|
||||
String formatAmount(BigInt amount, {int? fractionalDigits, bool trimZeros = true}) =>
|
||||
formatFixed(amount, decimals, fractionalDigits: fractionalDigits, trimZeros: trimZeros);
|
||||
|
||||
/// Parse the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
|
||||
BigInt parseAmount(String value) => parseFixed(value, decimals);
|
||||
|
||||
/// Try parsing the [value] and turn it into the smallest denomination eg. turn Bitcoin into Sats
|
||||
BigInt? tryParseAmount(String value) => tryParseFixed(value, decimals);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
enum MemoLabelType { destinationTag, memo }
|
||||
|
||||
const Map<CryptoCurrency, MemoLabelType> _currenciesRequiringMemo = {
|
||||
CryptoCurrency.xrp: MemoLabelType.destinationTag,
|
||||
CryptoCurrency.xlm: MemoLabelType.memo,
|
||||
CryptoCurrency.ton: MemoLabelType.memo,
|
||||
CryptoCurrency.eos: MemoLabelType.memo,
|
||||
CryptoCurrency.hbar: MemoLabelType.memo,
|
||||
};
|
||||
|
||||
MemoLabelType? memoLabelTypeFor(CryptoCurrency currency) => _currenciesRequiringMemo[currency];
|
||||
@@ -1,7 +1,24 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
abstract class Currency {
|
||||
String get symbol;
|
||||
String get name;
|
||||
String? get tag;
|
||||
String? get fullName;
|
||||
String? get iconPath;
|
||||
int get decimals;
|
||||
String? get tag;
|
||||
|
||||
/// Parse the [value] and turn it into [Money]
|
||||
Money parseAmount(String value);
|
||||
|
||||
/// Try parsing the [value] and turn it into [Money]
|
||||
Money? tryParseAmount(String value);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Currency && other.decimals == decimals && other.symbol == symbol && other.tag == tag;
|
||||
|
||||
@override
|
||||
int get hashCode => decimals.hashCode ^ symbol.hashCode ^ tag.hashCode;
|
||||
String get apiString;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ CryptoCurrency getCryptoCurrencyByChainId(int chainId) {
|
||||
return CryptoCurrency.arbEth;
|
||||
case 56:
|
||||
return CryptoCurrency.bnb;
|
||||
case 4663:
|
||||
return CryptoCurrency.robEth;
|
||||
default:
|
||||
return CryptoCurrency.eth;
|
||||
}
|
||||
@@ -73,6 +75,8 @@ int? getChainIdByCryptoCurrency(CryptoCurrency currency) {
|
||||
return 42161;
|
||||
case CryptoCurrency.bnb:
|
||||
return 56;
|
||||
case CryptoCurrency.robEth:
|
||||
return 4663;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -97,5 +101,40 @@ String getCryptoCurrencyIconForWalletListItem(WalletType type,
|
||||
return "assets/new-ui/crypto_full_icons/base.svg";
|
||||
}
|
||||
|
||||
|
||||
return walletTypeToCryptoCurrency(type, isTestnet: isTestnet, chainId: chainId).iconPath ?? "";
|
||||
}
|
||||
|
||||
String? symbolIconPathForWalletType(WalletType type) {
|
||||
const prefix = "assets/new-ui/card_icons/symbol_icons";
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return "$prefix/xmr-symbol.svg";
|
||||
case WalletType.bitcoin:
|
||||
return "$prefix/btc-symbol.svg";
|
||||
case WalletType.litecoin:
|
||||
return "$prefix/ltc-symbol.svg";
|
||||
case WalletType.ethereum:
|
||||
return "$prefix/eth-symbol.svg";
|
||||
case WalletType.base:
|
||||
return "$prefix/base-symbol.svg";
|
||||
case WalletType.arbitrum:
|
||||
return "$prefix/arb-symbol.svg";
|
||||
case WalletType.bsc:
|
||||
return "$prefix/bnb-symbol.svg";
|
||||
case WalletType.bitcoinCash:
|
||||
return "$prefix/bch-symbol.svg";
|
||||
case WalletType.polygon:
|
||||
return "$prefix/pol-symbol.svg";
|
||||
case WalletType.dogecoin:
|
||||
return "$prefix/doge-symbol.svg";
|
||||
case WalletType.nano:
|
||||
return "$prefix/xno-symbol.svg";
|
||||
case WalletType.wownero:
|
||||
case WalletType.banano:
|
||||
case WalletType.none:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool isMonochromeSymbolIcon(String path) => path.contains("/symbol_icons/");
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
abstract class CurrencyGroups {
|
||||
static const stablecoin = 'stablecoin';
|
||||
static const tokenizedStock = 'tokenized_stock';
|
||||
}
|
||||
+357
-72
@@ -1,28 +1,54 @@
|
||||
import 'dart:io';
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import "package:cw_core/db/sqlite_debug.dart";
|
||||
import "package:cw_core/root_dir.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:path/path.dart" as p;
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
Database? db;
|
||||
|
||||
Future<void> _addColumnIfNotExists(
|
||||
Database db, {
|
||||
required String table,
|
||||
required String column,
|
||||
required String definition,
|
||||
}) async {
|
||||
Database db, {
|
||||
required String table,
|
||||
required String column,
|
||||
required String definition,
|
||||
}) async {
|
||||
final result = await db.rawQuery("PRAGMA table_info($table)");
|
||||
final columnExists = result.any((row) => row['name'] == column);
|
||||
final columnExists = result.any((row) => row["name"] == column);
|
||||
|
||||
if (!columnExists) {
|
||||
await db.execute(
|
||||
'ALTER TABLE $table ADD COLUMN $column $definition;',
|
||||
"ALTER TABLE $table ADD COLUMN $column $definition;",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> sqliteDebugMarkerFile() async {
|
||||
final appDir = await getAppDir();
|
||||
final dbDebugMarker = p.join(appDir.path, ".sqlite_db_debug");
|
||||
return File(dbDebugMarker);
|
||||
}
|
||||
|
||||
Future<void> initDb({String? pathOverride}) async {
|
||||
if (!kDebugMode && !kProfileMode) {
|
||||
await _initDb(pathOverride: pathOverride);
|
||||
return;
|
||||
}
|
||||
final dbDebugMarker = await sqliteDebugMarkerFile();
|
||||
try {
|
||||
if (dbDebugMarker.existsSync()) {
|
||||
throw Exception("Debug marker is present");
|
||||
}
|
||||
await _initDb(pathOverride: pathOverride);
|
||||
} catch (e, s) {
|
||||
await handleSqliteError(e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initDb({String? pathOverride}) async {
|
||||
if (Platform.isLinux || Platform.isWindows) {
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
}
|
||||
@@ -38,11 +64,13 @@ Future<void> initDb({String? pathOverride}) async {
|
||||
}
|
||||
}
|
||||
await db?.close();
|
||||
db = await openDatabase(dbFile.path, version: 6,
|
||||
onUpgrade: (Database db, int oldVersion, int newVersion) async {
|
||||
printV("migrating: $oldVersion, $newVersion");
|
||||
if (oldVersion <= 1) {
|
||||
await db.execute('''
|
||||
db = await openDatabase(
|
||||
dbFile.path,
|
||||
version: 15,
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
printV("migrating: $oldVersion, $newVersion");
|
||||
if (oldVersion <= 1) {
|
||||
await db.execute("""
|
||||
DELETE FROM WalletInfo
|
||||
WHERE walletInfoId NOT IN (
|
||||
SELECT MIN(walletInfoId)
|
||||
@@ -52,10 +80,10 @@ WHERE walletInfoId NOT IN (
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique
|
||||
ON WalletInfo (id);
|
||||
''');
|
||||
}
|
||||
if (oldVersion <= 2) {
|
||||
await db.execute('''
|
||||
""");
|
||||
}
|
||||
if (oldVersion <= 2) {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
@@ -66,37 +94,116 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
|
||||
FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'WalletInfo',
|
||||
column: 'receiveInfoboxDismissed',
|
||||
definition: 'BOOLEAN DEFAULT FALSE',
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "receiveInfoboxDismissed",
|
||||
definition: "BOOLEAN DEFAULT FALSE",
|
||||
);
|
||||
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'cardOrder',
|
||||
definition: 'INTEGER DEFAULT 0',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 3) {
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE");
|
||||
// null - primary token (eth, sol etc)
|
||||
// not null - address of fav token
|
||||
// if address doesn't correspond to a valid token, fallback to primary token
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
|
||||
}
|
||||
if (oldVersion <= 4) {
|
||||
await _createBridgeTransferTable(db);
|
||||
}
|
||||
if (oldVersion <= 5) {
|
||||
await _createTradeTable(db);
|
||||
}
|
||||
}, onCreate: (Database db, int version) async {
|
||||
await db.execute('''
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'cardOrder',
|
||||
definition: 'INTEGER DEFAULT 0',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 3) {
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE");
|
||||
// null - primary token (eth, sol etc)
|
||||
// not null - address of fav token
|
||||
// if address doesn't correspond to a valid token, fallback to primary token
|
||||
await _addColumnIfNotExists(db,
|
||||
table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
if (oldVersion <= 4) {
|
||||
await _createBridgeTransferTable(db);
|
||||
}
|
||||
|
||||
if (oldVersion <= 5) {
|
||||
await _createTradeTable(db);
|
||||
}
|
||||
if (oldVersion <= 6) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'Trade',
|
||||
column: 'toAddressExtraId',
|
||||
definition: 'TEXT',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 7) {
|
||||
await _createNodeTable(db);
|
||||
}
|
||||
if (oldVersion <= 8) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'iconStyleIndex',
|
||||
definition: 'INTEGER DEFAULT 0',
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: 'BalanceCardStyleSettings',
|
||||
column: 'isGradientOnly',
|
||||
definition: 'BOOLEAN DEFAULT FALSE',
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 9) {
|
||||
await _createErc20TokenTable(db);
|
||||
await _createSplTokenTable(db);
|
||||
await _createTronTokenTable(db);
|
||||
}
|
||||
if (oldVersion <= 10) {
|
||||
await _createImportedNFTTable(db);
|
||||
}
|
||||
if (oldVersion <= 11) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "showSeedBackupReminder",
|
||||
definition: "BOOLEAN DEFAULT FALSE",
|
||||
);
|
||||
}
|
||||
if (oldVersion <= 12) {
|
||||
await _createChartsTables(db);
|
||||
}
|
||||
|
||||
if (oldVersion <= 13) {
|
||||
await _createWalletInfoAccountTable(db);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "accountDiscoveryLimit",
|
||||
definition: "INTEGER DEFAULT NULL",
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "isMultiAccountsEnabled",
|
||||
definition: "INTEGER DEFAULT NULL",
|
||||
);
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "WalletInfo",
|
||||
column: "currentAccountIndex",
|
||||
definition: "INTEGER NOT NULL DEFAULT 0",
|
||||
);
|
||||
|
||||
await _migrateBitcoinCardStylesForAccounts(db);
|
||||
}
|
||||
if(oldVersion <= 14) {
|
||||
await _addColumnIfNotExists(
|
||||
db,
|
||||
table: "BalanceCardStyleSettings",
|
||||
column: "hidden",
|
||||
definition: "BOOLEAN DEFAULT FALSE",
|
||||
);
|
||||
}
|
||||
},
|
||||
onCreate: (Database db, int version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE WalletInfo (
|
||||
walletInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
id TEXT NOT NULL,
|
||||
@@ -119,13 +226,17 @@ CREATE TABLE WalletInfo (
|
||||
hashedWalletIdentifier TEXT,
|
||||
isNonSeedWallet INTEGER DEFAULT (0) NOT NULL,
|
||||
sortOrder INTEGER DEFAULT (0) NOT NULL,
|
||||
currentAccountIndex INTEGER NOT NULL DEFAULT 0,
|
||||
receiveInfoboxDismissed BOOLEAN DEFAULT FALSE,
|
||||
showCombinedBalance BOOLEAN DEFAULT TRUE,
|
||||
favoriteTokenAddress TEXT DEFAULT NULL
|
||||
favoriteTokenAddress TEXT DEFAULT NULL,
|
||||
accountDiscoveryLimit INTEGER DEFAULT NULL,
|
||||
isMultiAccountsEnabled INTEGER DEFAULT NULL,
|
||||
showSeedBackupReminder BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE WalletInfoDerivationInfo (
|
||||
walletInfoDerivationInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
address TEXT NOT NULL,
|
||||
@@ -136,9 +247,9 @@ CREATE TABLE WalletInfoDerivationInfo (
|
||||
scriptType TEXT,
|
||||
description TEXT
|
||||
);
|
||||
''');
|
||||
""");
|
||||
|
||||
await db.execute('''
|
||||
await db.execute('''
|
||||
CREATE TABLE WalletInfoAddress (
|
||||
walletInfoAddressId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
walletInfoId INTEGER,
|
||||
@@ -148,7 +259,7 @@ CREATE TABLE WalletInfoAddress (
|
||||
);
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE WalletInfoAddressInfo (
|
||||
walletInfoAddressInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletInfoId INTEGER NOT NULL,
|
||||
@@ -158,9 +269,9 @@ CREATE TABLE WalletInfoAddressInfo (
|
||||
mapValueLabel TEXT NOT NULL,
|
||||
CONSTRAINT WalletInfoAddressInfo_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
""");
|
||||
|
||||
await db.execute('''
|
||||
await db.execute('''
|
||||
CREATE TABLE "WalletInfoAddressMap" (
|
||||
walletInfoAddressMapId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletInfoId INTEGER NOT NULL,
|
||||
@@ -169,29 +280,58 @@ CREATE TABLE "WalletInfoAddressMap" (
|
||||
CONSTRAINT WalletInfoAddress_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_walletinfo_id_unique
|
||||
ON WalletInfo (id);
|
||||
''');
|
||||
await db.execute('''
|
||||
""");
|
||||
await db.execute('''
|
||||
CREATE TABLE BalanceCardStyleSettings (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
PRIMARY KEY (walletInfoId, accountIndex),
|
||||
FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await _createBridgeTransferTable(db);
|
||||
await _createTradeTable(db);
|
||||
});
|
||||
await _createBridgeTransferTable(db);
|
||||
await _createNodeTable(db);
|
||||
await _createTradeTable(db);
|
||||
await _createChartsTables(db);
|
||||
await _createErc20TokenTable(db);
|
||||
await _createSplTokenTable(db);
|
||||
await _createTronTokenTable(db);
|
||||
await _createImportedNFTTable(db);
|
||||
await _createWalletInfoAccountTable(db);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createChartsTables(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE PriceData (
|
||||
fromCurrency TEXT NOT NULL,
|
||||
toCurrency TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
price TEXT NOT NULL,
|
||||
PRIMARY KEY (fromCurrency, toCurrency, timestamp)
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE TABLE ChartsAssets (
|
||||
asset TEXT PRIMARY KEY,
|
||||
isFavorite BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createTradeTable(Database db) async {
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS Trade (
|
||||
tradeId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
id TEXT NOT NULL,
|
||||
@@ -225,6 +365,7 @@ CREATE TABLE IF NOT EXISTS Trade (
|
||||
refundAddress TEXT,
|
||||
walletId TEXT,
|
||||
payoutAddress TEXT,
|
||||
toAddressExtraId TEXT,
|
||||
password TEXT,
|
||||
providerId TEXT,
|
||||
providerName TEXT,
|
||||
@@ -245,11 +386,48 @@ CREATE TABLE IF NOT EXISTS Trade (
|
||||
chainId INTEGER,
|
||||
fee REAL
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_trade_id_unique
|
||||
ON Trade (id);
|
||||
''');
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createWalletInfoAccountTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS WalletInfoAccount (
|
||||
walletInfoId INTEGER NOT NULL,
|
||||
accountIndex INTEGER NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
PRIMARY KEY (walletInfoId, accountIndex),
|
||||
CONSTRAINT WalletInfoAccount_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
""");
|
||||
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_walletinfoaccount_walletinfoid
|
||||
ON WalletInfoAccount(walletInfoId);
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
Future<void> _migrateBitcoinCardStylesForAccounts(Database db) async {
|
||||
const bitcoinWallets = 'SELECT walletInfoId FROM WalletInfo WHERE "type" = ?';
|
||||
final btc = WalletType.bitcoin.index;
|
||||
|
||||
// Lightning: 0 -> -2 (first, so accountIndex 0 is free for the Bitcoin card)
|
||||
await db.rawUpdate(
|
||||
"UPDATE OR REPLACE BalanceCardStyleSettings SET accountIndex = -2 "
|
||||
"WHERE accountIndex = 0 AND walletInfoId IN ($bitcoinWallets)",
|
||||
[btc],
|
||||
);
|
||||
|
||||
// Bitcoin: -1 -> 0 (primary account, first in order)
|
||||
await db.rawUpdate(
|
||||
"UPDATE OR REPLACE BalanceCardStyleSettings SET accountIndex = 0, cardOrder = 0 "
|
||||
"WHERE accountIndex = -1 AND walletInfoId IN ($bitcoinWallets)",
|
||||
[btc],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> dumpDb() async {
|
||||
@@ -265,7 +443,7 @@ Future<Map<String, dynamic>> dumpDb() async {
|
||||
|
||||
Future<List<String>> _getTableNames(Database db) async {
|
||||
final tableNames = await db.rawQuery('SELECT name FROM sqlite_master WHERE type = "table"');
|
||||
return tableNames.map((e) => (e["name"]).toString()).toList();
|
||||
return tableNames.map((e) => e["name"].toString()).toList();
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _dumpDb() async {
|
||||
@@ -288,7 +466,7 @@ Future<Map<String, dynamic>> dumpCustomDb(String path) async {
|
||||
}
|
||||
|
||||
Future<void> _createBridgeTransferTable(Database db) async {
|
||||
await db.execute('''
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS BridgeTransfer (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
wallet_id TEXT NOT NULL,
|
||||
@@ -307,9 +485,116 @@ CREATE TABLE IF NOT EXISTS BridgeTransfer (
|
||||
error_message TEXT,
|
||||
status_message TEXT
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_bridgetransfer_wallet_id
|
||||
ON BridgeTransfer(wallet_id);
|
||||
''');
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createErc20TokenTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS Erc20Token (
|
||||
Erc20TokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
chainId INTEGER NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
symbol TEXT NOT NULL DEFAULT '',
|
||||
contractAddress TEXT NOT NULL,
|
||||
decimal INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
iconPath TEXT,
|
||||
tag TEXT,
|
||||
isPotentialScam INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_erc20token_wallet_chain_contract
|
||||
ON Erc20Token (walletName, chainId, contractAddress);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createSplTokenTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS SPLToken (
|
||||
SPLTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
symbol TEXT NOT NULL DEFAULT '',
|
||||
mintAddress TEXT NOT NULL,
|
||||
decimal INTEGER NOT NULL DEFAULT 0,
|
||||
mint TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
iconPath TEXT,
|
||||
tag TEXT,
|
||||
isPotentialScam INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_spltoken_wallet_mint
|
||||
ON SPLToken (walletName, mintAddress);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createImportedNFTTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ImportedNFT (
|
||||
ImportedNFTId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
identifier TEXT NOT NULL,
|
||||
name TEXT,
|
||||
symbol TEXT,
|
||||
description TEXT,
|
||||
imageUrl TEXT,
|
||||
isOwned INTEGER
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_importednft_wallet_chain_identifier
|
||||
ON ImportedNFT (walletName, chain, identifier);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createTronTokenTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS TronToken (
|
||||
TronTokenId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
walletName TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
symbol TEXT NOT NULL DEFAULT '',
|
||||
contractAddress TEXT NOT NULL,
|
||||
decimal INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
iconPath TEXT,
|
||||
tag TEXT,
|
||||
isPotentialScam INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
await db.execute("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_trontoken_wallet_contract
|
||||
ON TronToken (walletName, contractAddress);
|
||||
""");
|
||||
}
|
||||
|
||||
Future<void> _createNodeTable(Database db) async {
|
||||
await db.execute("""
|
||||
CREATE TABLE Node (
|
||||
NodeId INTEGER PRIMARY KEY,
|
||||
uri TEXT NOT NULL,
|
||||
path TEXT,
|
||||
login TEXT,
|
||||
label TEXT,
|
||||
password TEXT,
|
||||
isPow INTEGER NOT NULL,
|
||||
useSSL INTEGER,
|
||||
typeRaw INTEGER NOT NULL,
|
||||
trusted INTEGER NOT NULL,
|
||||
socksProxyAddress TEXT,
|
||||
isEnabledForAutoSwitching BOOLEAN DEFAULT FALSE,
|
||||
isOfficial BOOLEAN DEFAULT FALSE,
|
||||
isBuiltin BOOLEAN DEFAULT FALSE,
|
||||
isDefault BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/db/sqlite.dart';
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> handleSqliteError(final dynamic error, final StackTrace stackTrace) async {
|
||||
try {
|
||||
final f = await sqliteDebugMarkerFile();
|
||||
f.createSync();
|
||||
} catch (_) {}
|
||||
runApp(
|
||||
SqliteErrorHandlerApp(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
),
|
||||
);
|
||||
await Future.delayed(Duration(hours: 24));
|
||||
}
|
||||
|
||||
class SqliteErrorHandlerApp extends StatelessWidget {
|
||||
const SqliteErrorHandlerApp({
|
||||
super.key,
|
||||
required this.error,
|
||||
required this.stackTrace,
|
||||
});
|
||||
|
||||
final dynamic error;
|
||||
final StackTrace? stackTrace;
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Cake Wallet Crash',
|
||||
themeMode: ThemeMode.light,
|
||||
home: SqliteErrorHandler(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteErrorHandler extends StatefulWidget {
|
||||
SqliteErrorHandler({
|
||||
super.key,
|
||||
required this.error,
|
||||
required this.stackTrace,
|
||||
});
|
||||
|
||||
final dynamic error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
@override
|
||||
State<SqliteErrorHandler> createState() => _SqliteErrorHandlerState();
|
||||
}
|
||||
|
||||
class _SqliteErrorHandlerState extends State<SqliteErrorHandler> {
|
||||
String log = "";
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Sqlite Recovery Screen"),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildNotice(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _text(final dynamic element) {
|
||||
return SelectableText(
|
||||
element.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
void _dbgRemoveDebugMarker() async {
|
||||
final f = await sqliteDebugMarkerFile();
|
||||
f.delete();
|
||||
}
|
||||
|
||||
void _corruptTheDb() async {
|
||||
final dbFile = File("${(await getAppDir()).path}/cake.db");
|
||||
await writeBytesToFile(dbFile);
|
||||
}
|
||||
|
||||
void _dumpDb() async {
|
||||
try {
|
||||
final dbFile = File("${(await getAppDir()).path}/cake.db");
|
||||
final jsonText = JsonEncoder.withIndent(" ").convert(await dumpCustomDb(dbFile.path));
|
||||
setState(() {
|
||||
log = jsonText;
|
||||
});
|
||||
} catch (e, s) {
|
||||
setState(() {
|
||||
log = """
|
||||
e: $e
|
||||
|
||||
s: $s
|
||||
""";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _removeDb() async {
|
||||
try {
|
||||
File("${(await getAppDir()).path}/cake.db.old").deleteSync();
|
||||
} catch (_) {}
|
||||
final dbFile = File("${(await getAppDir()).path}/cake.db");
|
||||
dbFile.renameSync("${(await getAppDir()).path}/cake.db.old");
|
||||
}
|
||||
|
||||
List<Widget> _debugOptions() {
|
||||
return [
|
||||
_debugButton("Remove debug marker", _dbgRemoveDebugMarker),
|
||||
if (kDebugMode || kProfileMode) _debugButton("[DO NOT PRESS] Corrupt the DB", _corruptTheDb),
|
||||
_debugButton("Remove DB", _removeDb),
|
||||
_debugButton("Dump DB", _dumpDb),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _debugButton(String text, VoidCallback callback) {
|
||||
return ElevatedButton(
|
||||
onPressed: callback,
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildNotice() {
|
||||
return [
|
||||
_text("""Critical error occured in SQLite
|
||||
This is very rare, usually caused by the device killing the app before it finishes writing file.
|
||||
Here you can recover the database or recreate it to regain access to your wallets.
|
||||
During this process some of the metadata may get lost.
|
||||
If you need help, report it at https://hash.boats/report."""),
|
||||
Divider(),
|
||||
..._debugOptions(),
|
||||
if (log.isNotEmpty) ...[
|
||||
Divider(),
|
||||
SelectableText(
|
||||
log,
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
Divider(),
|
||||
_text(widget.error),
|
||||
Divider(),
|
||||
_text(widget.stackTrace),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> writeBytesToFile(File file) async {
|
||||
const int seek = 5;
|
||||
const int count = 50000;
|
||||
final raf = await file.open(mode: FileMode.writeOnlyAppend);
|
||||
try {
|
||||
await raf.setPosition(seek);
|
||||
final zeros = Uint8List.fromList(List.generate(count, (_) => 0x42));
|
||||
await raf.writeFrom(zeros);
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,34 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:cw_core/utils/file.dart' as file;
|
||||
import 'package:cake_backup/backup.dart' as cwb;
|
||||
|
||||
EncryptionFileUtils encryptionFileUtilsFor(bool direct) =>
|
||||
direct ? XChaCha20EncryptionFileUtils() : Salsa20EncryhptionFileUtils();
|
||||
EncryptionFileUtils encryptionFileUtilsFor(bool isDirect) =>
|
||||
XChaCha20MigratingEncryptionFileUtils(isDirect: isDirect);
|
||||
|
||||
abstract class EncryptionFileUtils {
|
||||
Future<void> write({required String path, required String password, required String data});
|
||||
Future<String> read({required String path, required String password});
|
||||
}
|
||||
|
||||
class Salsa20EncryhptionFileUtils extends EncryptionFileUtils {
|
||||
// Requires legacy complex key + iv as password
|
||||
@override
|
||||
Future<void> write(
|
||||
{required String path, required String password, required String data}) async =>
|
||||
await file.write(path: path, password: password, data: data);
|
||||
class XChaCha20MigratingEncryptionFileUtils extends EncryptionFileUtils {
|
||||
XChaCha20MigratingEncryptionFileUtils({required this.isDirect});
|
||||
|
||||
// Requires legacy complex key + iv as password
|
||||
@override
|
||||
Future<String> read({required String path, required String password}) async =>
|
||||
await file.read(path: path, password: password);
|
||||
}
|
||||
final bool isDirect;
|
||||
|
||||
class XChaCha20EncryptionFileUtils extends EncryptionFileUtils {
|
||||
@override
|
||||
Future<void> write({required String path, required String password, required String data}) async {
|
||||
final encrypted = await cwb.encrypt(password, Uint8List.fromList(data.codeUnits));
|
||||
await File(path).writeAsBytes(encrypted);
|
||||
Future<void> write({required String path, required String password, required String data}) {
|
||||
return file.write(
|
||||
path: path,
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: !isDirect,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> read({required String path, required String password}) async {
|
||||
final file = File(path);
|
||||
final encrypted = await file.readAsBytes();
|
||||
final bytes = await cwb.decrypt(password, encrypted);
|
||||
return String.fromCharCodes(bytes);
|
||||
Future<String> read({required String path, required String password}) {
|
||||
return file.read(
|
||||
path: path,
|
||||
password: password,
|
||||
highEntropyPassphrase: !isDirect,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
abstract class EnumerableItem<T> {
|
||||
const EnumerableItem({required this.title, required this.raw});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ abstract class Enumerate {
|
||||
String get value;
|
||||
|
||||
@override
|
||||
operator ==(other) {
|
||||
bool operator ==(other) {
|
||||
if (identical(other, this)) return true;
|
||||
if (other is! Enumerate) return false;
|
||||
return other.runtimeType == runtimeType && value == other.value;
|
||||
|
||||
+159
-45
@@ -1,32 +1,8 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'erc20_token.g.dart';
|
||||
|
||||
@HiveType(typeId: Erc20Token.typeId)
|
||||
class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
@HiveField(2)
|
||||
final String contractAddress;
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
@HiveField(5)
|
||||
String? iconPath;
|
||||
@HiveField(6)
|
||||
final String? tag;
|
||||
@HiveField(7, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class Erc20Token extends CryptoCurrency {
|
||||
Erc20Token({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
@@ -36,6 +12,10 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
this.iconPath,
|
||||
this.tag,
|
||||
this.isPotentialScam = false,
|
||||
this.id = 0,
|
||||
this.walletName,
|
||||
this.chainId,
|
||||
Set<String> groups = const {},
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: symbol.toLowerCase(),
|
||||
@@ -45,38 +25,172 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
iconPath: iconPath,
|
||||
decimals: decimal,
|
||||
isPotentialScam: isPotentialScam,
|
||||
groups: groups,
|
||||
);
|
||||
|
||||
Erc20Token.copyWith(Erc20Token other, {String? icon, String? tag, bool? enabled})
|
||||
: this.name = other.name,
|
||||
this.symbol = other.symbol,
|
||||
this.contractAddress = other.contractAddress,
|
||||
this.decimal = other.decimal,
|
||||
this._enabled = enabled ?? other.enabled,
|
||||
this.tag = tag ?? other.tag,
|
||||
this.iconPath = icon ?? other.iconPath,
|
||||
this.isPotentialScam = other.isPotentialScam,
|
||||
Erc20Token.copyWith(
|
||||
Erc20Token other, {
|
||||
String? icon,
|
||||
super.tag,
|
||||
bool? enabled,
|
||||
String? walletName,
|
||||
int? chainId,
|
||||
}) : name = other.name,
|
||||
symbol = other.symbol,
|
||||
contractAddress = other.contractAddress,
|
||||
decimal = other.decimal,
|
||||
_enabled = enabled ?? other.enabled,
|
||||
tag = tag ?? other.tag,
|
||||
iconPath = icon ?? other.iconPath,
|
||||
isPotentialScam = other.isPotentialScam,
|
||||
id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
chainId = chainId ?? other.chainId,
|
||||
super(
|
||||
name: other.name,
|
||||
title: other.symbol.toUpperCase(),
|
||||
fullName: other.name,
|
||||
tag: tag,
|
||||
iconPath: icon,
|
||||
decimals: other.decimal,
|
||||
isPotentialScam: other.isPotentialScam,
|
||||
groups: other.groups,
|
||||
);
|
||||
|
||||
static const typeId = ERC20_TOKEN_TYPE_ID;
|
||||
static const boxName = 'Erc20Tokens';
|
||||
static const ethereumBoxName = 'EthereumErc20Tokens';
|
||||
static const polygonBoxName = 'PolygonErc20Tokens';
|
||||
static const baseBoxName = 'BaseErc20Tokens';
|
||||
static const arbitrumBoxName = 'ArbitrumErc20Tokens';
|
||||
static const bscBoxName = 'BscErc20Tokens';
|
||||
Erc20Token.fromMap(Map<String, Object?> map)
|
||||
: this(
|
||||
name: map["name"] as String? ?? "",
|
||||
symbol: map["symbol"] as String? ?? "",
|
||||
contractAddress: map["contractAddress"] as String? ?? "",
|
||||
decimal: (map["decimal"] ?? 0) as int,
|
||||
enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
|
||||
iconPath: map["iconPath"] as String?,
|
||||
tag: map["tag"] as String?,
|
||||
isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
|
||||
id: (map[selfIdColumn] ?? 0) as int,
|
||||
walletName: map["walletName"] as String?,
|
||||
chainId: map["chainId"] as int?,
|
||||
);
|
||||
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
String? iconPath;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
bool isPotentialScam;
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
int id;
|
||||
int? chainId;
|
||||
bool _enabled;
|
||||
final int decimal;
|
||||
String? walletName;
|
||||
final String contractAddress;
|
||||
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"chainId": chainId,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"contractAddress": contractAddress.toLowerCase(),
|
||||
"decimal": decimal,
|
||||
"enabled": _enabled ? 1 : 0,
|
||||
"iconPath": iconPath,
|
||||
"tag": tag,
|
||||
"isPotentialScam": isPotentialScam ? 1 : 0,
|
||||
};
|
||||
|
||||
static String get tableName => "Erc20Token";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
Future<int> save() async {
|
||||
if (walletName == null || chainId == null) {
|
||||
throw StateError("Erc20Token.save() requires walletName and chainId to be set");
|
||||
}
|
||||
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<Erc20Token>> selectList(
|
||||
String where,
|
||||
List<dynamic> whereArgs, {
|
||||
String? orderBy,
|
||||
}) async {
|
||||
orderBy ??= selfIdColumn;
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => Erc20Token.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<List<Erc20Token>> getAllForWallet(String walletName, int chainId) =>
|
||||
selectList("walletName = ? AND chainId = ?", [walletName, chainId]);
|
||||
|
||||
static Future<Erc20Token?> getByContract(
|
||||
String walletName,
|
||||
int chainId,
|
||||
String contractAddress,
|
||||
) async {
|
||||
final list = await selectList(
|
||||
"walletName = ? AND chainId = ? AND contractAddress = ?",
|
||||
[walletName, chainId, contractAddress.toLowerCase()],
|
||||
);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
static Future<int> deleteForWallet(String walletName, int chainId, String contractAddress) =>
|
||||
db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND chainId = ? AND contractAddress = ?",
|
||||
whereArgs: [walletName, chainId, contractAddress.toLowerCase()],
|
||||
);
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName) =>
|
||||
db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
|
||||
|
||||
static Future<void> renameWallet(String oldName, String newName) async {
|
||||
await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
|
||||
await db!
|
||||
.update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is Erc20Token && other.contractAddress == contractAddress;
|
||||
|
||||
@override
|
||||
int get hashCode => contractAddress.hashCode;
|
||||
|
||||
@override
|
||||
String get apiString => "evm.$contractAddress";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import "package:cw_core/cake_hive.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/erc20_token.dart" as erc20_new;
|
||||
import "package:cw_core/hive_type_ids.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:hive/hive.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
part "erc20_token_legacy.part.dart";
|
||||
|
||||
Future<void> performErc20TokenHiveMigration() async {
|
||||
try {
|
||||
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
|
||||
CakeHive.registerAdapter(Erc20TokenAdapter());
|
||||
}
|
||||
|
||||
final wallets = await WalletInfo.getAll();
|
||||
await Erc20Token.migrateAllToSqlite(wallets);
|
||||
} catch (e) {
|
||||
printV("Error performing Erc20Token Hive migration: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
// @HiveType(typeId: Erc20Token.typeId)
|
||||
class Erc20Token extends HiveObject {
|
||||
Erc20Token({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.contractAddress,
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
this.tag,
|
||||
this.isPotentialScam = false,
|
||||
}) : _enabled = enabled;
|
||||
// @HiveField(0)
|
||||
final String name;
|
||||
// @HiveField(1)
|
||||
final String symbol;
|
||||
// @HiveField(2)
|
||||
final String contractAddress;
|
||||
// @HiveField(3)
|
||||
final int decimal;
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
// @HiveField(5)
|
||||
String? iconPath;
|
||||
// @HiveField(6)
|
||||
final String? tag;
|
||||
// @HiveField(7, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static const typeId = ERC20_TOKEN_TYPE_ID;
|
||||
static const boxName = "Erc20Tokens";
|
||||
static const ethereumBoxName = "EthereumErc20Tokens";
|
||||
static const polygonBoxName = "PolygonErc20Tokens";
|
||||
static const baseBoxName = "BaseErc20Tokens";
|
||||
static const arbitrumBoxName = "ArbitrumErc20Tokens";
|
||||
static const bscBoxName = "BscErc20Tokens";
|
||||
|
||||
static const chainIdToBoxSuffix = {
|
||||
1: ethereumBoxName,
|
||||
137: polygonBoxName,
|
||||
8453: baseBoxName,
|
||||
42161: arbitrumBoxName,
|
||||
56: bscBoxName,
|
||||
};
|
||||
|
||||
static const evmWalletTypes = [
|
||||
WalletType.ethereum,
|
||||
WalletType.polygon,
|
||||
WalletType.base,
|
||||
WalletType.arbitrum,
|
||||
WalletType.bsc,
|
||||
];
|
||||
|
||||
static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
|
||||
await _migrateLegacyGlobalBox(wallets);
|
||||
|
||||
final sanitizedToRawNames = <String, Set<String>>{};
|
||||
for (final wallet in wallets) {
|
||||
if (!evmWalletTypes.contains(wallet.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sanitizedToRawNames
|
||||
.putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
|
||||
.add(wallet.name);
|
||||
}
|
||||
|
||||
for (final entry in sanitizedToRawNames.entries) {
|
||||
for (final chainEntry in chainIdToBoxSuffix.entries) {
|
||||
final tokenBoxName = "${entry.key}_${chainEntry.value}";
|
||||
try {
|
||||
if (!await CakeHive.boxExists(tokenBoxName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<Erc20Token>(tokenBoxName);
|
||||
|
||||
for (final group in _mergeByLowercaseContract(box)) {
|
||||
for (final rawName in entry.value) {
|
||||
await group.token.migrateToSqlite(walletName: rawName, chainId: chainEntry.key);
|
||||
}
|
||||
await box.deleteAll(group.sourceKeys);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating erc20 token box $tokenBoxName: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _migrateLegacyGlobalBox(List<WalletInfo> wallets) async {
|
||||
try {
|
||||
if (!await CakeHive.boxExists(boxName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wallets.any((wallet) => wallet.type == WalletType.ethereum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<Erc20Token>(boxName);
|
||||
final ethereumWallets =
|
||||
wallets.where((wallet) => wallet.type == WalletType.ethereum).toList();
|
||||
|
||||
for (final group in _mergeByLowercaseContract(box)) {
|
||||
for (final wallet in ethereumWallets) {
|
||||
await group.token.migrateToSqlite(walletName: wallet.name, chainId: 1);
|
||||
}
|
||||
await box.deleteAll(group.sourceKeys);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating legacy global erc20 token box: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
static List<_MergedTokenGroup> _mergeByLowercaseContract(Box<Erc20Token> box) {
|
||||
final groups = <String, _MergedTokenGroup>{};
|
||||
|
||||
for (final key in box.keys) {
|
||||
final token = box.get(key);
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final lowerKey = token.contractAddress.toLowerCase();
|
||||
final existing = groups[lowerKey];
|
||||
|
||||
if (existing == null) {
|
||||
groups[lowerKey] = _MergedTokenGroup(token: token, sourceKeys: [key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
groups[lowerKey] = _MergedTokenGroup(
|
||||
token: Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
contractAddress: lowerKey,
|
||||
decimal: token.decimal,
|
||||
enabled: token.enabled || existing.token.enabled,
|
||||
iconPath:
|
||||
(token.iconPath?.isNotEmpty ?? false) ? token.iconPath : existing.token.iconPath,
|
||||
tag: token.tag ?? existing.token.tag,
|
||||
isPotentialScam: token.isPotentialScam || existing.token.isPotentialScam,
|
||||
),
|
||||
sourceKeys: [...existing.sourceKeys, key],
|
||||
);
|
||||
}
|
||||
|
||||
return groups.values.toList();
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite({required String walletName, required int chainId}) async {
|
||||
final row = erc20_new.Erc20Token(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal,
|
||||
enabled: _enabled,
|
||||
iconPath: iconPath,
|
||||
tag: tag,
|
||||
isPotentialScam: isPotentialScam,
|
||||
walletName: walletName,
|
||||
chainId: chainId,
|
||||
).toMap();
|
||||
row[erc20_new.Erc20Token.selfIdColumn] = null;
|
||||
|
||||
await db!.insert(
|
||||
erc20_new.Erc20Token.tableName,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MergedTokenGroup {
|
||||
_MergedTokenGroup({required this.token, required this.sourceKeys});
|
||||
|
||||
final Erc20Token token;
|
||||
final List<dynamic> sourceKeys;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of "erc20_token_legacy.dart";
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class Erc20TokenAdapter extends TypeAdapter<Erc20Token> {
|
||||
@override
|
||||
final int typeId = 12;
|
||||
|
||||
@override
|
||||
Erc20Token read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Erc20Token(
|
||||
name: fields[0] as String,
|
||||
symbol: fields[1] as String,
|
||||
contractAddress: fields[2] as String,
|
||||
decimal: fields[3] as int,
|
||||
iconPath: fields[5] as String?,
|
||||
tag: fields[6] as String?,
|
||||
isPotentialScam: fields[7] == null ? false : fields[7] as bool,
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Erc20Token obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.symbol)
|
||||
..writeByte(2)
|
||||
..write(obj.contractAddress)
|
||||
..writeByte(3)
|
||||
..write(obj.decimal)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(6)
|
||||
..write(obj.tag)
|
||||
..writeByte(7)
|
||||
..write(obj.isPotentialScam);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Erc20TokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -26,9 +26,7 @@ class TransactionCommitFailed implements Exception {
|
||||
TransactionCommitFailed({this.errorMessage});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return errorMessage ?? "unknown error";
|
||||
}
|
||||
String toString() => errorMessage ?? "unknown error";
|
||||
}
|
||||
|
||||
class TransactionCommitFailedDustChange implements Exception {}
|
||||
@@ -57,11 +55,35 @@ class SignSPLTokenTransactionRentException implements Exception {}
|
||||
|
||||
class NoAssociatedTokenAccountException implements Exception {}
|
||||
|
||||
/// ==============================================================================
|
||||
/// ==============================================================================
|
||||
class NotAnNFTException implements Exception {}
|
||||
|
||||
class AmbiguousTokenSymbolException implements Exception {
|
||||
AmbiguousTokenSymbolException(this.symbol);
|
||||
|
||||
final String symbol;
|
||||
}
|
||||
|
||||
class RestoreFromSeedException implements Exception {
|
||||
final String message;
|
||||
|
||||
RestoreFromSeedException(this.message);
|
||||
}
|
||||
|
||||
class WalletDeprecationException implements Exception {
|
||||
final String seed;
|
||||
final CryptoCurrency curr;
|
||||
|
||||
@override
|
||||
String toString() => "Wallet type no longer supported";
|
||||
|
||||
WalletDeprecationException({required this.seed, required this.curr});
|
||||
}
|
||||
|
||||
class WalletSwitchException implements Exception {
|
||||
WalletSwitchException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
@@ -4,33 +4,44 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri
|
||||
decimals ??= 0;
|
||||
fractionalDigits ??= decimals;
|
||||
|
||||
var multiplier = getMultiplier(decimals);
|
||||
final multiplier = getMultiplier(decimals);
|
||||
// Make sure wei is a big number (convert as necessary)
|
||||
var negative = value.isNegative;
|
||||
if (negative) value = value * BigInt.from(-1);
|
||||
final negative = value.isNegative;
|
||||
if (negative) {
|
||||
value = -value;
|
||||
}
|
||||
|
||||
var fraction =
|
||||
value.modPow(BigInt.one, BigInt.parse(multiplier)).toString().padLeft(decimals, "0");
|
||||
|
||||
if (fractionalDigits < 0) fractionalDigits = 0;
|
||||
if (fractionalDigits > decimals) fractionalDigits = decimals;
|
||||
if (fractionalDigits < 0) {
|
||||
fractionalDigits = 0;
|
||||
}
|
||||
if (fractionalDigits > decimals) {
|
||||
fractionalDigits = decimals;
|
||||
}
|
||||
fraction = fraction.substring(0, fractionalDigits);
|
||||
|
||||
if (trimZeros) {
|
||||
fraction = removeTrailing("0", fraction);
|
||||
}
|
||||
|
||||
final whole = (value ~/ BigInt.parse(multiplier));
|
||||
final whole = value ~/ BigInt.parse(multiplier);
|
||||
|
||||
final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction";
|
||||
|
||||
if (negative) return "-$valString";
|
||||
if (negative) {
|
||||
return "-$valString";
|
||||
}
|
||||
|
||||
return valString;
|
||||
}
|
||||
|
||||
String removeTrailing(String pattern, String from) {
|
||||
if (pattern.isEmpty) return from;
|
||||
if (pattern.isEmpty) {
|
||||
return from;
|
||||
}
|
||||
|
||||
var i = from.length;
|
||||
while (i > 0 && from.startsWith(pattern, i - pattern.length)) {
|
||||
i -= pattern.length;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cw_core/get_height_by_date_xmr.dart';
|
||||
import 'dart:convert';
|
||||
@@ -7,7 +6,7 @@ import 'dart:convert';
|
||||
// FIXME: Hardcoded values; Works only for monero
|
||||
|
||||
int getMoneroHeigthByDate({required DateTime date}) =>
|
||||
MoneroHeight.getBlockHeightByTime(date..subtract(Duration(days: 1)));
|
||||
MoneroHeight.getBlockHeightByTime(date.subtract(const Duration(days: 1)));
|
||||
|
||||
const havenDates = {
|
||||
"2023-05": 1352995,
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
class ZcashHeight {
|
||||
// for i in $(seq 1 331); do echo "_Checkpoint($(date -j -f "%Y-%m-%d %H:%M:%S" "$(curl -s "https://mainnet.zcashexplorer.app/blocks/${i}0000" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')" "+%s"), ${i}0000),"; done > points.txt
|
||||
static final List<_Checkpoint> _points = [
|
||||
_Checkpoint(1479114667, 10000),
|
||||
_Checkpoint(1480618460, 20000),
|
||||
_Checkpoint(1482129143, 30000),
|
||||
_Checkpoint(1483635610, 40000),
|
||||
_Checkpoint(1485145514, 50000),
|
||||
_Checkpoint(1486655742, 60000),
|
||||
_Checkpoint(1488163642, 70000),
|
||||
_Checkpoint(1489668244, 80000),
|
||||
_Checkpoint(1491175107, 90000),
|
||||
_Checkpoint(1492681094, 100000),
|
||||
_Checkpoint(1494185626, 110000),
|
||||
_Checkpoint(1495691260, 120000),
|
||||
_Checkpoint(1497196465, 130000),
|
||||
_Checkpoint(1498702428, 140000),
|
||||
_Checkpoint(1500209116, 150000),
|
||||
_Checkpoint(1501716823, 160000),
|
||||
_Checkpoint(1503222893, 170000),
|
||||
_Checkpoint(1504730866, 180000),
|
||||
_Checkpoint(1506236963, 190000),
|
||||
_Checkpoint(1507744233, 200000),
|
||||
_Checkpoint(1509254578, 210000),
|
||||
_Checkpoint(1510768449, 220000),
|
||||
_Checkpoint(1512275502, 230000),
|
||||
_Checkpoint(1513783951, 240000),
|
||||
_Checkpoint(1515292290, 250000),
|
||||
_Checkpoint(1516799219, 260000),
|
||||
_Checkpoint(1518306180, 270000),
|
||||
_Checkpoint(1519813817, 280000),
|
||||
_Checkpoint(1521319150, 290000),
|
||||
_Checkpoint(1522828107, 300000),
|
||||
_Checkpoint(1524336223, 310000),
|
||||
_Checkpoint(1525844954, 320000),
|
||||
_Checkpoint(1527354342, 330000),
|
||||
_Checkpoint(1528860301, 340000),
|
||||
_Checkpoint(1530365206, 350000),
|
||||
_Checkpoint(1531873693, 360000),
|
||||
_Checkpoint(1533380392, 370000),
|
||||
_Checkpoint(1534888873, 380000),
|
||||
_Checkpoint(1536393381, 390000),
|
||||
_Checkpoint(1537899117, 400000),
|
||||
_Checkpoint(1539406330, 410000),
|
||||
_Checkpoint(1540912214, 420000),
|
||||
_Checkpoint(1542422891, 430000),
|
||||
_Checkpoint(1543931111, 440000),
|
||||
_Checkpoint(1545437758, 450000),
|
||||
_Checkpoint(1546945529, 460000),
|
||||
_Checkpoint(1548450107, 470000),
|
||||
_Checkpoint(1549957767, 480000),
|
||||
_Checkpoint(1551466960, 490000),
|
||||
_Checkpoint(1552969806, 500000),
|
||||
_Checkpoint(1554477648, 510000),
|
||||
_Checkpoint(1555981451, 520000),
|
||||
_Checkpoint(1557489241, 530000),
|
||||
_Checkpoint(1558997282, 540000),
|
||||
_Checkpoint(1560503840, 550000),
|
||||
_Checkpoint(1562011717, 560000),
|
||||
_Checkpoint(1563518091, 570000),
|
||||
_Checkpoint(1565027136, 580000),
|
||||
_Checkpoint(1566532760, 590000),
|
||||
_Checkpoint(1568039395, 600000),
|
||||
_Checkpoint(1569547911, 610000),
|
||||
_Checkpoint(1571051853, 620000),
|
||||
_Checkpoint(1572559831, 630000),
|
||||
_Checkpoint(1574069743, 640000),
|
||||
_Checkpoint(1575576895, 650000),
|
||||
_Checkpoint(1576603014, 660000),
|
||||
_Checkpoint(1577356346, 670000),
|
||||
_Checkpoint(1578109375, 680000),
|
||||
_Checkpoint(1578863180, 690000),
|
||||
_Checkpoint(1579616443, 700000),
|
||||
_Checkpoint(1580369806, 710000),
|
||||
_Checkpoint(1581122786, 720000),
|
||||
_Checkpoint(1581876634, 730000),
|
||||
_Checkpoint(1582630471, 740000),
|
||||
_Checkpoint(1583383678, 750000),
|
||||
_Checkpoint(1584133944, 760000),
|
||||
_Checkpoint(1584887940, 770000),
|
||||
_Checkpoint(1585641057, 780000),
|
||||
_Checkpoint(1586395543, 790000),
|
||||
_Checkpoint(1587148500, 800000),
|
||||
_Checkpoint(1587901493, 810000),
|
||||
_Checkpoint(1588655183, 820000),
|
||||
_Checkpoint(1589408476, 830000),
|
||||
_Checkpoint(1590163464, 840000),
|
||||
_Checkpoint(1590916379, 850000),
|
||||
_Checkpoint(1591669786, 860000),
|
||||
_Checkpoint(1592423887, 870000),
|
||||
_Checkpoint(1593176771, 880000),
|
||||
_Checkpoint(1593930930, 890000),
|
||||
_Checkpoint(1594684380, 900000),
|
||||
_Checkpoint(1595439209, 910000),
|
||||
_Checkpoint(1596192463, 920000),
|
||||
_Checkpoint(1596946009, 930000),
|
||||
_Checkpoint(1597700256, 940000),
|
||||
_Checkpoint(1598453538, 950000),
|
||||
_Checkpoint(1599207483, 960000),
|
||||
_Checkpoint(1599960598, 970000),
|
||||
_Checkpoint(1600714563, 980000),
|
||||
_Checkpoint(1601467884, 990000),
|
||||
_Checkpoint(1602220941, 1000000),
|
||||
_Checkpoint(1602974722, 1010000),
|
||||
_Checkpoint(1603728848, 1020000),
|
||||
_Checkpoint(1604485637, 1030000),
|
||||
_Checkpoint(1605238381, 1040000),
|
||||
_Checkpoint(1605992807, 1050000),
|
||||
_Checkpoint(1606747750, 1060000),
|
||||
_Checkpoint(1607500606, 1070000),
|
||||
_Checkpoint(1608255278, 1080000),
|
||||
_Checkpoint(1609008684, 1090000),
|
||||
_Checkpoint(1609763782, 1100000),
|
||||
_Checkpoint(1610517871, 1110000),
|
||||
_Checkpoint(1611271575, 1120000),
|
||||
_Checkpoint(1612025546, 1130000),
|
||||
_Checkpoint(1612779940, 1140000),
|
||||
_Checkpoint(1613533993, 1150000),
|
||||
_Checkpoint(1614287669, 1160000),
|
||||
_Checkpoint(1615042297, 1170000),
|
||||
_Checkpoint(1615792993, 1180000),
|
||||
_Checkpoint(1616546145, 1190000),
|
||||
_Checkpoint(1617299867, 1200000),
|
||||
_Checkpoint(1618054038, 1210000),
|
||||
_Checkpoint(1618808513, 1220000),
|
||||
_Checkpoint(1619563243, 1230000),
|
||||
_Checkpoint(1620318159, 1240000),
|
||||
_Checkpoint(1621071298, 1250000),
|
||||
_Checkpoint(1621826156, 1260000),
|
||||
_Checkpoint(1622580237, 1270000),
|
||||
_Checkpoint(1623335553, 1280000),
|
||||
_Checkpoint(1624090141, 1290000),
|
||||
_Checkpoint(1624844712, 1300000),
|
||||
_Checkpoint(1625597720, 1310000),
|
||||
_Checkpoint(1626351652, 1320000),
|
||||
_Checkpoint(1627106054, 1330000),
|
||||
_Checkpoint(1627860648, 1340000),
|
||||
_Checkpoint(1628614015, 1350000),
|
||||
_Checkpoint(1629368398, 1360000),
|
||||
_Checkpoint(1630122679, 1370000),
|
||||
_Checkpoint(1630877190, 1380000),
|
||||
_Checkpoint(1631631884, 1390000),
|
||||
_Checkpoint(1632385851, 1400000),
|
||||
_Checkpoint(1633141006, 1410000),
|
||||
_Checkpoint(1633895592, 1420000),
|
||||
_Checkpoint(1634649550, 1430000),
|
||||
_Checkpoint(1635403121, 1440000),
|
||||
_Checkpoint(1636157669, 1450000),
|
||||
_Checkpoint(1636915133, 1460000),
|
||||
_Checkpoint(1637670036, 1470000),
|
||||
_Checkpoint(1638424121, 1480000),
|
||||
_Checkpoint(1639177413, 1490000),
|
||||
_Checkpoint(1639931234, 1500000),
|
||||
_Checkpoint(1640684476, 1510000),
|
||||
_Checkpoint(1641438827, 1520000),
|
||||
_Checkpoint(1642192402, 1530000),
|
||||
_Checkpoint(1642946383, 1540000),
|
||||
_Checkpoint(1643700901, 1550000),
|
||||
_Checkpoint(1644455522, 1560000),
|
||||
_Checkpoint(1645208504, 1570000),
|
||||
_Checkpoint(1645962081, 1580000),
|
||||
_Checkpoint(1646715150, 1590000),
|
||||
_Checkpoint(1647465727, 1600000),
|
||||
_Checkpoint(1648219045, 1610000),
|
||||
_Checkpoint(1648973131, 1620000),
|
||||
_Checkpoint(1649727422, 1630000),
|
||||
_Checkpoint(1650481408, 1640000),
|
||||
_Checkpoint(1651234287, 1650000),
|
||||
_Checkpoint(1651989865, 1660000),
|
||||
_Checkpoint(1652743327, 1670000),
|
||||
_Checkpoint(1653497455, 1680000),
|
||||
_Checkpoint(1654252094, 1690000),
|
||||
_Checkpoint(1655005944, 1700000),
|
||||
_Checkpoint(1655759708, 1710000),
|
||||
_Checkpoint(1656513765, 1720000),
|
||||
_Checkpoint(1657268358, 1730000),
|
||||
_Checkpoint(1658022453, 1740000),
|
||||
_Checkpoint(1658777019, 1750000),
|
||||
_Checkpoint(1659531133, 1760000),
|
||||
_Checkpoint(1660283907, 1770000),
|
||||
_Checkpoint(1661037645, 1780000),
|
||||
_Checkpoint(1661791977, 1790000),
|
||||
_Checkpoint(1662544394, 1800000),
|
||||
_Checkpoint(1663298018, 1810000),
|
||||
_Checkpoint(1664051450, 1820000),
|
||||
_Checkpoint(1664805547, 1830000),
|
||||
_Checkpoint(1665558994, 1840000),
|
||||
_Checkpoint(1666312456, 1850000),
|
||||
_Checkpoint(1667066473, 1860000),
|
||||
_Checkpoint(1667823393, 1870000),
|
||||
_Checkpoint(1668576623, 1880000),
|
||||
_Checkpoint(1669330657, 1890000),
|
||||
_Checkpoint(1670084156, 1900000),
|
||||
_Checkpoint(1670837831, 1910000),
|
||||
_Checkpoint(1671591094, 1920000),
|
||||
_Checkpoint(1672343975, 1930000),
|
||||
_Checkpoint(1673097703, 1940000),
|
||||
_Checkpoint(1673852016, 1950000),
|
||||
_Checkpoint(1674605810, 1960000),
|
||||
_Checkpoint(1675360050, 1970000),
|
||||
_Checkpoint(1676113710, 1980000),
|
||||
_Checkpoint(1676867174, 1990000),
|
||||
_Checkpoint(1677620242, 2000000),
|
||||
_Checkpoint(1678374852, 2010000),
|
||||
_Checkpoint(1679125063, 2020000),
|
||||
_Checkpoint(1679878121, 2030000),
|
||||
_Checkpoint(1680632254, 2040000),
|
||||
_Checkpoint(1681386518, 2050000),
|
||||
_Checkpoint(1682140381, 2060000),
|
||||
_Checkpoint(1682894564, 2070000),
|
||||
_Checkpoint(1683648178, 2080000),
|
||||
_Checkpoint(1684401932, 2090000),
|
||||
_Checkpoint(1685156464, 2100000),
|
||||
_Checkpoint(1685910040, 2110000),
|
||||
_Checkpoint(1686663406, 2120000),
|
||||
_Checkpoint(1687417875, 2130000),
|
||||
_Checkpoint(1688172069, 2140000),
|
||||
_Checkpoint(1688925610, 2150000),
|
||||
_Checkpoint(1689679500, 2160000),
|
||||
_Checkpoint(1690432691, 2170000),
|
||||
_Checkpoint(1691186349, 2180000),
|
||||
_Checkpoint(1691940901, 2190000),
|
||||
_Checkpoint(1692694780, 2200000),
|
||||
_Checkpoint(1693449743, 2210000),
|
||||
_Checkpoint(1694203665, 2220000),
|
||||
_Checkpoint(1694957643, 2230000),
|
||||
_Checkpoint(1695710712, 2240000),
|
||||
_Checkpoint(1696465787, 2250000),
|
||||
_Checkpoint(1697218701, 2260000),
|
||||
_Checkpoint(1697973031, 2270000),
|
||||
_Checkpoint(1698726864, 2280000),
|
||||
_Checkpoint(1699483348, 2290000),
|
||||
_Checkpoint(1700237650, 2300000),
|
||||
_Checkpoint(1700991210, 2310000),
|
||||
_Checkpoint(1701745742, 2320000),
|
||||
_Checkpoint(1702500723, 2330000),
|
||||
_Checkpoint(1703253121, 2340000),
|
||||
_Checkpoint(1704007238, 2350000),
|
||||
_Checkpoint(1704760935, 2360000),
|
||||
_Checkpoint(1705514669, 2370000),
|
||||
_Checkpoint(1706268087, 2380000),
|
||||
_Checkpoint(1707021423, 2390000),
|
||||
_Checkpoint(1707776257, 2400000),
|
||||
_Checkpoint(1708529542, 2410000),
|
||||
_Checkpoint(1709283065, 2420000),
|
||||
_Checkpoint(1710037165, 2430000),
|
||||
_Checkpoint(1710787780, 2440000),
|
||||
_Checkpoint(1711541488, 2450000),
|
||||
_Checkpoint(1712294996, 2460000),
|
||||
_Checkpoint(1713048843, 2470000),
|
||||
_Checkpoint(1713803486, 2480000),
|
||||
_Checkpoint(1714557127, 2490000),
|
||||
_Checkpoint(1715311181, 2500000),
|
||||
_Checkpoint(1716064271, 2510000),
|
||||
_Checkpoint(1716818792, 2520000),
|
||||
_Checkpoint(1717572765, 2530000),
|
||||
_Checkpoint(1718326333, 2540000),
|
||||
_Checkpoint(1719080275, 2550000),
|
||||
_Checkpoint(1719834297, 2560000),
|
||||
_Checkpoint(1720588042, 2570000),
|
||||
_Checkpoint(1721341832, 2580000),
|
||||
_Checkpoint(1722095122, 2590000),
|
||||
_Checkpoint(1722848604, 2600000),
|
||||
_Checkpoint(1723602357, 2610000),
|
||||
_Checkpoint(1724355843, 2620000),
|
||||
_Checkpoint(1725110085, 2630000),
|
||||
_Checkpoint(1725863681, 2640000),
|
||||
_Checkpoint(1726616892, 2650000),
|
||||
_Checkpoint(1727371443, 2660000),
|
||||
_Checkpoint(1728124893, 2670000),
|
||||
_Checkpoint(1728878141, 2680000),
|
||||
_Checkpoint(1729631572, 2690000),
|
||||
_Checkpoint(1730385888, 2700000),
|
||||
_Checkpoint(1731142437, 2710000),
|
||||
_Checkpoint(1731896266, 2720000),
|
||||
_Checkpoint(1732650193, 2730000),
|
||||
_Checkpoint(1733403626, 2740000),
|
||||
_Checkpoint(1734157390, 2750000),
|
||||
_Checkpoint(1734911442, 2760000),
|
||||
_Checkpoint(1735665720, 2770000),
|
||||
_Checkpoint(1736419845, 2780000),
|
||||
_Checkpoint(1737173511, 2790000),
|
||||
_Checkpoint(1737926795, 2800000),
|
||||
_Checkpoint(1738681511, 2810000),
|
||||
_Checkpoint(1739434236, 2820000),
|
||||
_Checkpoint(1740188214, 2830000),
|
||||
_Checkpoint(1740941352, 2840000),
|
||||
_Checkpoint(1741692338, 2850000),
|
||||
_Checkpoint(1742445247, 2860000),
|
||||
_Checkpoint(1743198927, 2870000),
|
||||
_Checkpoint(1743952947, 2880000),
|
||||
_Checkpoint(1744706370, 2890000),
|
||||
_Checkpoint(1745461377, 2900000),
|
||||
_Checkpoint(1746214735, 2910000),
|
||||
_Checkpoint(1746967908, 2920000),
|
||||
_Checkpoint(1747722271, 2930000),
|
||||
_Checkpoint(1748474582, 2940000),
|
||||
_Checkpoint(1749228582, 2950000),
|
||||
_Checkpoint(1749983372, 2960000),
|
||||
_Checkpoint(1750737278, 2970000),
|
||||
_Checkpoint(1751490762, 2980000),
|
||||
_Checkpoint(1752243931, 2990000),
|
||||
_Checkpoint(1752997873, 3000000),
|
||||
_Checkpoint(1753751756, 3010000),
|
||||
_Checkpoint(1754504935, 3020000),
|
||||
_Checkpoint(1755259449, 3030000),
|
||||
_Checkpoint(1756012904, 3040000),
|
||||
_Checkpoint(1756765335, 3050000),
|
||||
_Checkpoint(1757519206, 3060000),
|
||||
_Checkpoint(1758272484, 3070000),
|
||||
_Checkpoint(1759025597, 3080000),
|
||||
_Checkpoint(1759778957, 3090000),
|
||||
_Checkpoint(1760532780, 3100000),
|
||||
_Checkpoint(1761286210, 3110000),
|
||||
_Checkpoint(1762039783, 3120000),
|
||||
_Checkpoint(1762796627, 3130000),
|
||||
_Checkpoint(1763550532, 3140000),
|
||||
_Checkpoint(1764303860, 3150000),
|
||||
_Checkpoint(1765057381, 3160000),
|
||||
_Checkpoint(1765811222, 3170000),
|
||||
_Checkpoint(1766564356, 3180000),
|
||||
_Checkpoint(1767318326, 3190000),
|
||||
_Checkpoint(1768071956, 3200000),
|
||||
_Checkpoint(1768824957, 3210000),
|
||||
_Checkpoint(1769578781, 3220000),
|
||||
_Checkpoint(1770332206, 3230000),
|
||||
_Checkpoint(1771086265, 3240000),
|
||||
_Checkpoint(1771840180, 3250000),
|
||||
_Checkpoint(1772592783, 3260000),
|
||||
_Checkpoint(1773343947, 3270000),
|
||||
_Checkpoint(1774097085, 3280000),
|
||||
_Checkpoint(1774850161, 3290000),
|
||||
_Checkpoint(1775603525, 3300000),
|
||||
_Checkpoint(1776358117, 3310000),
|
||||
];
|
||||
|
||||
static int getBlockHeightByTime(DateTime time) {
|
||||
final t = time.toUtc().millisecondsSinceEpoch;
|
||||
|
||||
if (_points.isEmpty) return 0;
|
||||
|
||||
if (t <= _points.first.timeMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _points.length - 1; i++) {
|
||||
final p0 = _points[i];
|
||||
final p1 = _points[i + 1];
|
||||
|
||||
if (t >= p0.timeMs && t < p1.timeMs) {
|
||||
return _interpolate(
|
||||
p0.height,
|
||||
p1.height,
|
||||
p0.timeMs,
|
||||
p1.timeMs,
|
||||
t,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final last = _points[_points.length - 1];
|
||||
final prev = _points[_points.length - 2];
|
||||
|
||||
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
|
||||
|
||||
final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor();
|
||||
|
||||
return last.height + blocksSinceLast;
|
||||
}
|
||||
|
||||
static DateTime getTimeByBlockHeight(int height) {
|
||||
if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||
if (height <= 1) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _points.length - 1; i++) {
|
||||
final p0 = _points[i];
|
||||
final p1 = _points[i + 1];
|
||||
if (height >= p0.height && height < p1.height) {
|
||||
final t = _interpolate(
|
||||
p0.timeMs,
|
||||
p1.timeMs,
|
||||
p0.height,
|
||||
p1.height,
|
||||
height,
|
||||
);
|
||||
return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true);
|
||||
}
|
||||
}
|
||||
|
||||
final last = _points[_points.length - 1];
|
||||
final prev = _points[_points.length - 2];
|
||||
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
|
||||
final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor();
|
||||
return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true);
|
||||
}
|
||||
|
||||
static int _interpolate(
|
||||
int h0,
|
||||
int h1,
|
||||
int t0,
|
||||
int t1,
|
||||
int t,
|
||||
) {
|
||||
final ratio = (t - t0) / (t1 - t0);
|
||||
return h0 + ((h1 - h0) * ratio).floor();
|
||||
}
|
||||
}
|
||||
|
||||
class _Checkpoint {
|
||||
final int timeMs;
|
||||
final int height;
|
||||
|
||||
_Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ enum DeviceConnectionType {
|
||||
break;
|
||||
case HardwareWalletType.trezor:
|
||||
isSupported = [
|
||||
// WalletType.monero,
|
||||
WalletType.monero,
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.ethereum,
|
||||
|
||||
@@ -4,7 +4,8 @@ import 'dart:typed_data';
|
||||
import 'package:cw_core/hardware/hardware_account_data.dart';
|
||||
|
||||
abstract class HardwareWalletService {
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5});
|
||||
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
Future<Uint8List> signTransaction({required String transaction}) => throw UnimplementedError();
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class ImportedNFT {
|
||||
ImportedNFT({
|
||||
required this.walletName,
|
||||
required this.chain,
|
||||
required this.identifier,
|
||||
this.name,
|
||||
this.symbol,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
this.isOwned,
|
||||
this.id = 0,
|
||||
});
|
||||
|
||||
ImportedNFT.copyWith(ImportedNFT other, {String? walletName})
|
||||
: id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
chain = other.chain,
|
||||
identifier = other.identifier,
|
||||
name = other.name,
|
||||
symbol = other.symbol,
|
||||
description = other.description,
|
||||
imageUrl = other.imageUrl,
|
||||
isOwned = other.isOwned;
|
||||
|
||||
factory ImportedNFT.fromMap(Map<String, Object?> map) => ImportedNFT(
|
||||
id: map[selfIdColumn] as int? ?? 0,
|
||||
walletName: map["walletName"]! as String,
|
||||
chain: map["chain"]! as String,
|
||||
identifier: map["identifier"]! as String,
|
||||
name: map["name"] as String?,
|
||||
symbol: map["symbol"] as String?,
|
||||
description: map["description"] as String?,
|
||||
imageUrl: map["imageUrl"] as String?,
|
||||
isOwned: map["isOwned"] == null ? null : map["isOwned"] == 1,
|
||||
);
|
||||
|
||||
int id;
|
||||
final String walletName;
|
||||
final String chain;
|
||||
final String identifier;
|
||||
final String? name;
|
||||
final String? symbol;
|
||||
final String? description;
|
||||
final String? imageUrl;
|
||||
final bool? isOwned;
|
||||
|
||||
static String get tableName => "ImportedNFT";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
static const solanaChain = "solana";
|
||||
|
||||
Map<String, Object?> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"chain": chain,
|
||||
"identifier": identifier,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"description": description,
|
||||
"imageUrl": imageUrl,
|
||||
"isOwned": isOwned == null ? null : (isOwned! ? 1 : 0),
|
||||
};
|
||||
|
||||
Future<int> save() async {
|
||||
final row = toMap();
|
||||
|
||||
if (row[selfIdColumn] == 0) {
|
||||
row[selfIdColumn] = null;
|
||||
}
|
||||
|
||||
id = await db!.insert(tableName, row, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<int> updateMetadata(ImportedNFT nft) => db!.update(
|
||||
tableName,
|
||||
nft.toMap()..remove(selfIdColumn),
|
||||
where: "walletName = ? AND chain = ? AND identifier = ?",
|
||||
whereArgs: [nft.walletName, nft.chain, nft.identifier],
|
||||
);
|
||||
|
||||
static Future<List<ImportedNFT>> getAllForWallet(String walletName, [String? chain]) async {
|
||||
final rows = await db!.query(
|
||||
tableName,
|
||||
where: chain == null ? "walletName = ?" : "walletName = ? AND chain = ?",
|
||||
whereArgs: chain == null ? [walletName] : [walletName, chain],
|
||||
orderBy: selfIdColumn,
|
||||
);
|
||||
|
||||
return rows.map(ImportedNFT.fromMap).toList();
|
||||
}
|
||||
|
||||
static Future<int> deleteOne(String walletName, String chain, String identifier) => db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND chain = ? AND identifier = ?",
|
||||
whereArgs: [walletName, chain, identifier],
|
||||
);
|
||||
|
||||
static String _scope(List<String>? chains) =>
|
||||
chains == null ? "" : " AND chain IN (${List.filled(chains.length, "?").join(", ")})";
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName, {List<String>? chains}) async {
|
||||
if (chains != null && chains.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ?${_scope(chains)}",
|
||||
whereArgs: [walletName, ...?chains],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> renameWallet(
|
||||
String oldName,
|
||||
String newName, {
|
||||
List<String>? chains,
|
||||
}) async {
|
||||
if (chains != null && chains.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ?${_scope(chains)}",
|
||||
whereArgs: [newName, ...?chains],
|
||||
);
|
||||
await db!.update(
|
||||
tableName,
|
||||
{"walletName": newName},
|
||||
where: "walletName = ?${_scope(chains)}",
|
||||
whereArgs: [oldName, ...?chains],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,8 @@
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
const ivEncodedStringLength = 12;
|
||||
|
||||
String generateKey() {
|
||||
final key = encrypt.Key.fromSecureRandom(512);
|
||||
final iv = encrypt.IV.fromSecureRandom(8);
|
||||
|
||||
return key.base64 + iv.base64;
|
||||
}
|
||||
|
||||
List<String> extractKeys(String key) {
|
||||
final _key = key.substring(0, key.length - ivEncodedStringLength);
|
||||
final iv = key.substring(key.length - ivEncodedStringLength);
|
||||
|
||||
return [_key, iv];
|
||||
}
|
||||
|
||||
Future<String> encode(
|
||||
{required encrypt.Key key, required encrypt.IV iv, required String data}) async {
|
||||
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
|
||||
final encrypted = encrypter.encrypt(data, iv: iv);
|
||||
|
||||
return encrypted.base64;
|
||||
}
|
||||
|
||||
Future<String> decode({required String password, required String data}) async {
|
||||
final keys = extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
|
||||
final encrypted = encrypter.decrypt64(data, iv: iv);
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
+150
-41
@@ -1,96 +1,180 @@
|
||||
import 'dart:convert';
|
||||
import "dart:convert";
|
||||
|
||||
import 'package:bech32/bech32.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import "package:bech32/bech32.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/utils/proxy_wrapper.dart";
|
||||
|
||||
const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"];
|
||||
const _LUD17_PREFIXES = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
|
||||
const _LUD17_PREFIXES = ["lnurlw", "lnurlc", "lnurlp", "keyauth"];
|
||||
|
||||
bool isBolt11ZeroInvoice(String invoice) {
|
||||
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
try {
|
||||
final request =
|
||||
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
|
||||
final prefix =
|
||||
_BOLT_PREFIXES.firstWhere((prefix) => request.hrp.startsWith(prefix), orElse: () => "");
|
||||
final prefix =
|
||||
_BOLT_PREFIXES.firstWhere(request.hrp.startsWith, orElse: () => "");
|
||||
|
||||
return request.hrp.length == prefix.length;
|
||||
return request.hrp.length == prefix.length;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the amount of a Bolt 11 Invoice in
|
||||
int? _getAmountBolt11Msat(String invoice) {
|
||||
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
final request =
|
||||
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
|
||||
|
||||
final prefix = _BOLT_PREFIXES.firstWhere(
|
||||
(prefix) => request.hrp.startsWith(prefix),
|
||||
orElse: () => throw FormatException('Invalid BOLT11 invoice: unknown HRP prefix.'),
|
||||
request.hrp.startsWith,
|
||||
orElse: () => throw const FormatException("Invalid BOLT11 invoice: unknown HRP prefix."),
|
||||
);
|
||||
|
||||
final amountPart = request.hrp.substring(prefix.length);
|
||||
if (amountPart.isEmpty) return null; // zero-amount invoice
|
||||
if (amountPart.isEmpty) {
|
||||
return null; // zero-amount invoice
|
||||
}
|
||||
|
||||
final hasMultiplier = RegExp(r'[munp]$').hasMatch(amountPart);
|
||||
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : '';
|
||||
final hasMultiplier = RegExp(r"[munp]$").hasMatch(amountPart);
|
||||
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : "";
|
||||
final numberStr = hasMultiplier ? amountPart.substring(0, amountPart.length - 1) : amountPart;
|
||||
|
||||
if (numberStr.isEmpty || !RegExp(r'^\d+$').hasMatch(numberStr)) {
|
||||
throw FormatException('Invalid BOLT11 invoice: invalid amount number in HRP.');
|
||||
if (numberStr.isEmpty || !RegExp(r"^\d+$").hasMatch(numberStr)) {
|
||||
throw const FormatException("Invalid BOLT11 invoice: invalid amount number in HRP.");
|
||||
}
|
||||
|
||||
final amount = int.parse(numberStr);
|
||||
|
||||
switch (multiplier) {
|
||||
case '': // bitcoin
|
||||
case "": // bitcoin
|
||||
return amount * 100000000000;
|
||||
case 'm': // milli-bitcoin
|
||||
case "m": // milli-bitcoin
|
||||
return amount * 100000000;
|
||||
case 'u': // micro-bitcoin
|
||||
case "u": // micro-bitcoin
|
||||
return amount * 100000;
|
||||
case 'n': // nano-bitcoin
|
||||
case "n": // nano-bitcoin
|
||||
return amount * 100;
|
||||
case 'p':
|
||||
case "p":
|
||||
if (amount % 10 != 0) {
|
||||
throw FormatException('Invalid BOLT11 invoice: amount not representable in whole msat.');
|
||||
throw const FormatException(
|
||||
"Invalid BOLT11 invoice: amount not representable in whole msat.",
|
||||
);
|
||||
}
|
||||
return amount ~/ 10;
|
||||
default:
|
||||
throw FormatException('Invalid BOLT11 invoice: unknown amount multiplier.');
|
||||
throw const FormatException("Invalid BOLT11 invoice: unknown amount multiplier.");
|
||||
}
|
||||
}
|
||||
|
||||
int? getBolt11Amount(String invoice) {
|
||||
Money? getBolt11Amount(String invoice) {
|
||||
final msat = _getAmountBolt11Msat(invoice);
|
||||
if (msat == null || msat % 1000 != 0) return null;
|
||||
return msat ~/ 1000;
|
||||
if (msat == null || msat % 1000 != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
|
||||
}
|
||||
|
||||
class LNURL {
|
||||
static Future<int?> getPayRequestAmount(String lnurl) async {
|
||||
static Future<Money?> getPayRequestAmount(String lnurl) async {
|
||||
final url = decode(lnurl);
|
||||
final response = await ProxyWrapper().get(clearnetUri: url);
|
||||
|
||||
if (response.statusCode != 200) return null;
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final body = jsonDecode(response.body) as Map;
|
||||
final tag = body["tag"] as String?;
|
||||
|
||||
if (tag != "payRequest") return null;
|
||||
if (tag != "payRequest") {
|
||||
return null;
|
||||
}
|
||||
|
||||
final msat = body["minSendable"] as int?;
|
||||
final maxSendable = body["maxSendable"] as int?;
|
||||
|
||||
// if minSendable and maxSendable are the same we assume a specific payment request
|
||||
if (msat != maxSendable) return null;
|
||||
if (msat != maxSendable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (msat == null || msat % 1000 != 0) return null;
|
||||
return msat ~/ 1000;
|
||||
if (msat == null || msat % 1000 != 0) {
|
||||
return null;
|
||||
}
|
||||
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LNURLWithdrawRequest?> getWithdrawRequest(String lnurl) async {
|
||||
final url = decode(lnurl);
|
||||
final response = await ProxyWrapper().get(clearnetUri: url);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final body = jsonDecode(response.body) as Map;
|
||||
final tag = body["tag"] as String?;
|
||||
|
||||
if (tag != "withdrawRequest") {
|
||||
return null;
|
||||
}
|
||||
|
||||
final minWithdrawable = body["minWithdrawable"] as int?;
|
||||
final maxWithdrawable = body["maxWithdrawable"] as int?;
|
||||
|
||||
if ((minWithdrawable == null || minWithdrawable % 1000 != 0) ||
|
||||
(maxWithdrawable == null || maxWithdrawable % 1000 != 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return LNURLWithdrawRequest(
|
||||
callback: body["callback"] as String,
|
||||
k1: body["k1"] as String,
|
||||
description: body["defaultDescription"] as String,
|
||||
minAmount: Money.fromInt(minWithdrawable ~/ 1000, CryptoCurrency.btcln),
|
||||
maxAmount: Money.fromInt(maxWithdrawable ~/ 1000, CryptoCurrency.btcln),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> commitWithdrawRequest(LNURLWithdrawRequest request, String invoice) async {
|
||||
try {
|
||||
final url = Uri.parse(request.callback);
|
||||
url.queryParameters["k1"] = request.k1;
|
||||
url.queryParameters["pr"] = invoice;
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: url);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final body = jsonDecode(response.body) as Map;
|
||||
final status = body["status"] as String?;
|
||||
|
||||
if (status == "OK") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static String encode(String url) {
|
||||
final raw = _convert(utf8.encode(url), 8, 5, true);
|
||||
return const Bech32Codec().encode(Bech32('lnurl', raw), 999);
|
||||
return const Bech32Codec().encode(Bech32("lnurl", raw), 999);
|
||||
}
|
||||
|
||||
static Uri decode(String encodedUrl) {
|
||||
@@ -108,9 +192,10 @@ class LNURL {
|
||||
if (_LUD17_PREFIXES.contains(decodedUri.scheme)) {
|
||||
/// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.'
|
||||
decodedUri = decodedUri.replace(
|
||||
scheme: decodedUri.host.endsWith('onion') || decodedUri.host.endsWith('onion.')
|
||||
? 'http'
|
||||
: 'https');
|
||||
scheme: decodedUri.host.endsWith("onion") || decodedUri.host.endsWith("onion.")
|
||||
? "http"
|
||||
: "https",
|
||||
);
|
||||
} else {
|
||||
/// Try to parse the input as a lnUrl. Will throw an error if it fails.
|
||||
final lnUrl = _findLnUrl(encodedUrl);
|
||||
@@ -123,12 +208,30 @@ class LNURL {
|
||||
}
|
||||
}
|
||||
|
||||
class LNURLWithdrawRequest {
|
||||
const LNURLWithdrawRequest({
|
||||
required this.callback,
|
||||
required this.k1,
|
||||
required this.description,
|
||||
required this.minAmount,
|
||||
required this.maxAmount,
|
||||
});
|
||||
|
||||
final String callback;
|
||||
final String k1;
|
||||
final String description;
|
||||
final Money minAmount;
|
||||
final Money maxAmount;
|
||||
}
|
||||
|
||||
/// Parse and return a given lnurl string if it's valid. Will remove
|
||||
/// `lightning:` from the beginning of it if present.
|
||||
String _findLnUrl(String input) {
|
||||
final res = RegExp(r',*?((lnurl)([0-9]+[a-z0-9]+))').allMatches(input.toLowerCase());
|
||||
final res = RegExp(r",*?((lnurl)([0-9]+[a-z0-9]+))").allMatches(input.toLowerCase());
|
||||
|
||||
if (res.length != 1) throw ArgumentError('Not a valid lnurl string');
|
||||
if (res.length != 1) {
|
||||
throw ArgumentError("Not a valid lnurl string");
|
||||
}
|
||||
return res.first.group(0)!;
|
||||
}
|
||||
|
||||
@@ -150,10 +253,16 @@ List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits > 0) result.add((value << (outBits - bits)) & maxV);
|
||||
if (bits > 0) {
|
||||
result.add((value << (outBits - bits)) & maxV);
|
||||
}
|
||||
} else {
|
||||
if (bits >= inBits) throw Exception('[BECH32] Excess padding');
|
||||
if ((value << (outBits - bits)) & maxV > 0) throw Exception('[BECH32] Non-zero padding');
|
||||
if (bits >= inBits) {
|
||||
throw Exception("[BECH32] Excess padding");
|
||||
}
|
||||
if ((value << (outBits - bits)) & maxV > 0) {
|
||||
throw Exception("[BECH32] Non-zero padding");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/monero_amount_format.dart';
|
||||
|
||||
class MoneroBalance extends Balance {
|
||||
MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0})
|
||||
: formattedFrozenBalance = moneroAmountToString(amount: frozenBalance),
|
||||
super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance);
|
||||
|
||||
final int fullBalance;
|
||||
final int unlockedBalance;
|
||||
final int frozenBalance;
|
||||
final String formattedFrozenBalance;
|
||||
|
||||
@override
|
||||
String get formattedUnAvailableBalance =>
|
||||
formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'mweb_utxo.g.dart';
|
||||
part 'mweb_utxo.part.dart';
|
||||
|
||||
@HiveType(typeId: MWEB_UTXO_TYPE_ID)
|
||||
// @HiveType(typeId: MWEB_UTXO_TYPE_ID)
|
||||
class MwebUtxo extends HiveObject {
|
||||
MwebUtxo({
|
||||
required this.height,
|
||||
@@ -17,21 +17,21 @@ class MwebUtxo extends HiveObject {
|
||||
static const typeId = MWEB_UTXO_TYPE_ID;
|
||||
static const boxName = 'MwebUtxo';
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
int height;
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
int value;
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
String address;
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
String outputId;
|
||||
|
||||
@HiveField(4)
|
||||
// @HiveField(4)
|
||||
int blockTime;
|
||||
|
||||
@HiveField(5, defaultValue: false)
|
||||
// @HiveField(5, defaultValue: false)
|
||||
bool spent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'mweb_utxo.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class MwebUtxoAdapter extends TypeAdapter<MwebUtxo> {
|
||||
@override
|
||||
final int typeId = 20;
|
||||
|
||||
@override
|
||||
MwebUtxo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return MwebUtxo(
|
||||
height: fields[0] as int,
|
||||
value: fields[1] as int,
|
||||
address: fields[2] as String,
|
||||
outputId: fields[3] as String,
|
||||
blockTime: fields[4] as int,
|
||||
spent: fields[5] == null ? false : fields[5] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MwebUtxo obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(0)
|
||||
..write(obj.height)
|
||||
..writeByte(1)
|
||||
..write(obj.value)
|
||||
..writeByte(2)
|
||||
..write(obj.address)
|
||||
..writeByte(3)
|
||||
..write(obj.outputId)
|
||||
..writeByte(4)
|
||||
..write(obj.blockTime)
|
||||
..writeByte(5)
|
||||
..write(obj.spent);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MwebUtxoAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'nano_account.g.dart';
|
||||
part 'nano_account.part.dart';
|
||||
|
||||
@HiveType(typeId: NanoAccount.typeId)
|
||||
// @HiveType(typeId: NanoAccount.typeId)
|
||||
class NanoAccount extends HiveObject {
|
||||
NanoAccount({required this.label, required this.id, this.balance, this.isSelected = false});
|
||||
|
||||
static const typeId = NANO_ACCOUNT_TYPE_ID;
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
String label;
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
final int id;
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
bool isSelected;
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
String? balance;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'nano_account.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class NanoAccountAdapter extends TypeAdapter<NanoAccount> {
|
||||
@override
|
||||
final int typeId = 13;
|
||||
|
||||
@override
|
||||
NanoAccount read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return NanoAccount(
|
||||
label: fields[0] as String,
|
||||
id: fields[1] as int,
|
||||
balance: fields[3] as String?,
|
||||
isSelected: fields[2] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, NanoAccount obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.label)
|
||||
..writeByte(1)
|
||||
..write(obj.id)
|
||||
..writeByte(2)
|
||||
..write(obj.isSelected)
|
||||
..writeByte(3)
|
||||
..write(obj.balance);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is NanoAccountAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
+211
-57
@@ -1,33 +1,60 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:blockchain_utils/hex/hex.dart';
|
||||
import 'package:cw_core/node_list.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_socket/abstract.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
part 'node.g.dart';
|
||||
import 'db/sqlite.dart';
|
||||
|
||||
Uri createUriFromElectrumAddress(String address, String path) =>
|
||||
Uri.tryParse('tcp://$address$path')!;
|
||||
|
||||
@HiveType(typeId: Node.typeId)
|
||||
class Node extends HiveObject with Keyable {
|
||||
Future<void> validateBuiltinNodes() async {
|
||||
// ensures nodes stored as builtin in the db correspond to the nodes in the .yml files
|
||||
|
||||
final builtinFromDb = await Node.getAllBuiltin();
|
||||
final builtinFromList = await loadAllDefaultNodes();
|
||||
|
||||
for (final listNode in builtinFromList) {
|
||||
// preserve proxy settings from user
|
||||
try {
|
||||
final matchedDbNode = builtinFromDb.firstWhere((dbNode) => dbNode.uri == listNode.uri);
|
||||
listNode.socksProxyAddress = matchedDbNode.socksProxyAddress;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
final dbSet = builtinFromDb.toSet();
|
||||
final listSet = builtinFromList.toSet();
|
||||
|
||||
final nodesToDelete = dbSet.difference(listSet);
|
||||
final nodesToAdd = listSet.difference(dbSet);
|
||||
|
||||
for (final node in nodesToDelete) await node.delete();
|
||||
for (final node in nodesToAdd) await node.save();
|
||||
}
|
||||
|
||||
class Node {
|
||||
Node({
|
||||
this.id = 0,
|
||||
this.label,
|
||||
this.login,
|
||||
this.password,
|
||||
this.useSSL,
|
||||
this.isPow = false,
|
||||
this.trusted = false,
|
||||
this.socksProxyAddress,
|
||||
this.path = '',
|
||||
this.isEnabledForAutoSwitching = false,
|
||||
this.isOfficial = false,
|
||||
this.isBuiltin = false,
|
||||
this.isDefault = false,
|
||||
String? uri,
|
||||
WalletType? type,
|
||||
}) {
|
||||
@@ -50,67 +77,204 @@ class Node extends HiveObject with Keyable {
|
||||
trusted: $trusted,
|
||||
socksProxyAddress: $socksProxyAddress,
|
||||
isEnabledForAutoSwitching: $isEnabledForAutoSwitching,
|
||||
isOfficial: $isOfficial,
|
||||
isBuiltin: $isBuiltin,
|
||||
isDefault: $isDefault
|
||||
})""";
|
||||
}
|
||||
|
||||
Node.fromMap(Map<String, Object?> map)
|
||||
: uriRaw = map['uri'] as String? ?? '',
|
||||
: id = (map[selfIdColumn] ?? 0) as int,
|
||||
uriRaw = map['uri'] as String? ?? '',
|
||||
path = map['path'] as String? ?? '',
|
||||
login = map['login'] as String?,
|
||||
label = map['label'] as String?,
|
||||
password = map['password'] as String?,
|
||||
useSSL = map['useSSL'] as bool?,
|
||||
trusted = map['trusted'] as bool? ?? false,
|
||||
socksProxyAddress = map['socksProxyPort'] as String?,
|
||||
isEnabledForAutoSwitching = map['isEnabledForAutoSwitching'] as bool? ?? false;
|
||||
isPow = _getBoolFromDB(map['isPow']),
|
||||
useSSL = _getBoolFromDB(map['useSSL']),
|
||||
typeRaw = (map["typeRaw"] ?? 0) as int,
|
||||
trusted = _getBoolFromDB(map['trusted']),
|
||||
socksProxyAddress = map['socksProxyAddress'] as String?,
|
||||
isEnabledForAutoSwitching = _getBoolFromDB(map['isEnabledForAutoSwitching']),
|
||||
isOfficial = _getBoolFromDB(map['isOfficial']),
|
||||
isBuiltin = _getBoolFromDB(map['isBuiltin']),
|
||||
isDefault = _getBoolFromDB(map['isDefault']);
|
||||
|
||||
static const typeId = NODE_TYPE_ID;
|
||||
static const boxName = 'Nodes';
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@HiveField(0, defaultValue: '')
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
selfIdColumn: id,
|
||||
'uri': uriRaw,
|
||||
'path': path,
|
||||
'login': login,
|
||||
"label": label,
|
||||
'password': password,
|
||||
"isPow": isPow ? 1 : 0,
|
||||
'useSSL': (useSSL ?? false) ? 1 : 0,
|
||||
"typeRaw": typeRaw,
|
||||
'trusted': trusted ? 1 : 0,
|
||||
'socksProxyAddress': socksProxyAddress,
|
||||
'isEnabledForAutoSwitching': isEnabledForAutoSwitching ? 1 : 0,
|
||||
"isOfficial": isOfficial ? 1 : 0,
|
||||
"isBuiltin": isBuiltin ? 1 : 0,
|
||||
"isDefault": isDefault ? 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
Node.fromUri(Uri uri, WalletType type)
|
||||
: id = 0,
|
||||
uriRaw =
|
||||
"${uri.host}${uri.hasPort ? ':${uri.port}' : (uri.queryParameters['port']?.isNotEmpty == true ? ':${uri.queryParameters['port']}' : '')}",
|
||||
path = uri.path,
|
||||
login = uri.userInfo.contains(':')
|
||||
? uri.userInfo.split(':')[0]
|
||||
: uri.queryParameters['username'],
|
||||
password = uri.userInfo.contains(':')
|
||||
? uri.userInfo.split(':')[1]
|
||||
: uri.queryParameters['password'],
|
||||
useSSL = uri.queryParameters['protocol'] == 'https',
|
||||
trusted = uri.queryParameters['trusted'] == 'true',
|
||||
isPow = false,
|
||||
isEnabledForAutoSwitching = false,
|
||||
isOfficial = false,
|
||||
isBuiltin = false,
|
||||
isDefault = false,
|
||||
typeRaw = serializeToInt(type);
|
||||
|
||||
Future<int> delete() async {
|
||||
return await db!.delete(tableName, where: '${selfIdColumn} = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
static Future<int> deleteAll() async {
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [0]);
|
||||
}
|
||||
|
||||
static Future<int> deleteAllPow() async {
|
||||
return await db!.delete(tableName, where: "isPow = ?", whereArgs: [1]);
|
||||
}
|
||||
|
||||
Future<int> save() async {
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<Node>> selectList(String where, List<dynamic> whereArgs,
|
||||
{String? orderBy}) async {
|
||||
if (orderBy == null) {
|
||||
orderBy = selfIdColumn;
|
||||
}
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => Node.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<Node?> select(String where, List<dynamic> whereArgs, {String? orderBy}) async {
|
||||
if (orderBy == null) {
|
||||
orderBy = selfIdColumn;
|
||||
}
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return list.isEmpty ? null : Node.fromMap(list.first);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAll() async {
|
||||
return selectList("isPow = ?", [0]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllBuiltin() async {
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [0, 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllPowBuiltin() async {
|
||||
return selectList("isPow = ? AND isBuiltin = ?", [1, 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllForWalletType(WalletType type) async {
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 0]);
|
||||
}
|
||||
|
||||
static Future<Node?> getDefaultForWalletType(WalletType type) async {
|
||||
return (await selectList(
|
||||
"typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), 0, 1]))
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
static Future<Node?> getDefaultPowForWalletType(WalletType type) async {
|
||||
return (await selectList(
|
||||
"typeRaw = ? AND isPow = ? AND isDefault = ?", [serializeToInt(type), 1, 1]))
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllForWalletTypePow(WalletType type) async {
|
||||
return selectList("typeRaw = ? AND isPow = ?", [serializeToInt(type), 1]);
|
||||
}
|
||||
|
||||
static Future<List<Node>> getAllPow() async {
|
||||
return selectList("isPow = ?", [1]);
|
||||
}
|
||||
|
||||
static Future<Node?> get(int id) async {
|
||||
return select("${selfIdColumn} = ?", [id]);
|
||||
}
|
||||
|
||||
int id;
|
||||
late String uriRaw;
|
||||
|
||||
@HiveField(1)
|
||||
String? login;
|
||||
|
||||
@HiveField(2)
|
||||
String? password;
|
||||
|
||||
@HiveField(3, defaultValue: 0)
|
||||
late int typeRaw;
|
||||
|
||||
@HiveField(4)
|
||||
bool? useSSL;
|
||||
|
||||
@HiveField(5, defaultValue: false)
|
||||
bool trusted;
|
||||
|
||||
@HiveField(6)
|
||||
bool isPow;
|
||||
String? socksProxyAddress;
|
||||
|
||||
@HiveField(7, defaultValue: '')
|
||||
String? path;
|
||||
|
||||
@HiveField(8)
|
||||
bool? isElectrs;
|
||||
|
||||
@HiveField(9)
|
||||
bool? supportsSilentPayments;
|
||||
|
||||
@HiveField(10)
|
||||
bool? supportsMweb;
|
||||
|
||||
@HiveField(11, defaultValue: false)
|
||||
bool isEnabledForAutoSwitching;
|
||||
bool isOfficial;
|
||||
bool isBuiltin;
|
||||
bool isDefault;
|
||||
|
||||
@HiveField(12, defaultValue: '')
|
||||
String? label;
|
||||
|
||||
static String get tableName => "Node";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
bool get isSSL => useSSL ?? false;
|
||||
|
||||
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
|
||||
|
||||
Uri get uri {
|
||||
try {
|
||||
return _uri;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
return Uri();
|
||||
}
|
||||
}
|
||||
|
||||
Uri get _uri {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
case WalletType.wownero:
|
||||
@@ -161,18 +325,10 @@ class Node extends HiveObject with Keyable {
|
||||
socksProxyAddress.hashCode ^
|
||||
path.hashCode;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex {
|
||||
_keyIndex ??= key;
|
||||
return _keyIndex;
|
||||
}
|
||||
|
||||
WalletType get type => deserializeFromInt(typeRaw);
|
||||
|
||||
set type(WalletType type) => typeRaw = serializeToInt(type);
|
||||
|
||||
dynamic _keyIndex;
|
||||
|
||||
Future<bool> requestNode() async {
|
||||
try {
|
||||
switch (type) {
|
||||
@@ -210,15 +366,13 @@ class Node extends HiveObject with Keyable {
|
||||
final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
|
||||
|
||||
try {
|
||||
final client = ProxyWrapper().getHttpIOClient();
|
||||
|
||||
final jsonBody = json.encode(body);
|
||||
|
||||
final response = await client.post(
|
||||
rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonBody,
|
||||
);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonBody,
|
||||
allowMitmMoneroBypassSSLCheck: true);
|
||||
// Check if we received a 401 Unauthorized response
|
||||
if (response.statusCode == 401) {
|
||||
final daemonRpc = DaemonRpc(
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
import 'package:cw_core/node_list.dart';
|
||||
import 'package:cw_core/utils/proxy_socket/abstract.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
import 'cake_hive.dart';
|
||||
import 'node.dart' as node_new;
|
||||
|
||||
part 'node_legacy.part.dart';
|
||||
|
||||
Future<void> performNodeHiveMigration() async {
|
||||
if (!CakeHive.isAdapterRegistered(Node.typeId)) {
|
||||
CakeHive.registerAdapter(NodeAdapter());
|
||||
}
|
||||
|
||||
final nodeBox = await CakeHive.openBox<Node>(Node.boxName);
|
||||
final powNodeBox = await CakeHive.openBox<Node>(Node.boxName + "pow");
|
||||
final builtinNodes = await loadAllDefaultNodes();
|
||||
final builtinPowNodes = await loadDefaultNanoPowNodes();
|
||||
await Node.migrateAllToSqlite(nodeBox, powNodeBox, builtinNodes, builtinPowNodes);
|
||||
}
|
||||
|
||||
Uri createUriFromElectrumAddress(String address, String path) =>
|
||||
Uri.tryParse('tcp://$address$path')!;
|
||||
|
||||
@HiveType(typeId: Node.typeId)
|
||||
class Node extends HiveObject with Keyable {
|
||||
Node({
|
||||
this.label,
|
||||
this.login,
|
||||
this.password,
|
||||
this.useSSL,
|
||||
this.trusted = false,
|
||||
this.socksProxyAddress,
|
||||
this.path = '',
|
||||
this.isEnabledForAutoSwitching = false,
|
||||
String? uri,
|
||||
WalletType? type,
|
||||
}) {
|
||||
if (uri != null) {
|
||||
uriRaw = uri;
|
||||
}
|
||||
if (type != null) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> migrateAllToSqlite(Box<Node> nodeBox, Box<Node> powNodeBox,
|
||||
List<node_new.Node> defaultNodes, List<node_new.Node> defaultPowNodes) async {
|
||||
final list = nodeBox.values.toList();
|
||||
final powList = powNodeBox.values.toList();
|
||||
for (final node in list) {
|
||||
if (defaultNodes.any((item) => item.uri == node.uri)) {
|
||||
// default nodes will be added by validateBuiltinNodes(), no need to migrate them.
|
||||
await node.delete();
|
||||
continue;
|
||||
}
|
||||
await node.migrateToSqlite(isPow: false, isBuiltin: false, isOfficial: false);
|
||||
await node.delete();
|
||||
}
|
||||
for (final node in powList) {
|
||||
if (defaultPowNodes.any((item) => item.uri == node.uri)) {
|
||||
await node.delete();
|
||||
continue;
|
||||
}
|
||||
await node.migrateToSqlite(isPow: true, isBuiltin: false, isOfficial: false);
|
||||
await node.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite(
|
||||
{required bool isPow, required bool isBuiltin, required bool isOfficial}) async {
|
||||
final newNode = node_new.Node(
|
||||
id: key as int,
|
||||
login: login,
|
||||
label: label,
|
||||
password: password,
|
||||
type: type,
|
||||
useSSL: useSSL,
|
||||
trusted: trusted,
|
||||
socksProxyAddress: socksProxyAddress,
|
||||
isPow: isPow,
|
||||
path: path,
|
||||
uri: uriRaw,
|
||||
isEnabledForAutoSwitching: isEnabledForAutoSwitching,
|
||||
isOfficial: isOfficial,
|
||||
isBuiltin: isBuiltin,
|
||||
);
|
||||
await newNode.save();
|
||||
}
|
||||
|
||||
Node.fromMap(Map<String, Object?> map)
|
||||
: uriRaw = map['uri'] as String? ?? '',
|
||||
path = map['path'] as String? ?? '',
|
||||
login = map['login'] as String?,
|
||||
label = map['label'] as String?,
|
||||
password = map['password'] as String?,
|
||||
useSSL = map['useSSL'] as bool?,
|
||||
trusted = map['trusted'] as bool? ?? false,
|
||||
socksProxyAddress = map['socksProxyPort'] as String?,
|
||||
isEnabledForAutoSwitching = map['isEnabledForAutoSwitching'] as bool? ?? false;
|
||||
|
||||
static const typeId = NODE_TYPE_ID;
|
||||
static const boxName = 'Nodes';
|
||||
|
||||
@HiveField(0, defaultValue: '')
|
||||
late String uriRaw;
|
||||
|
||||
@HiveField(1)
|
||||
String? login;
|
||||
|
||||
@HiveField(2)
|
||||
String? password;
|
||||
|
||||
@HiveField(3, defaultValue: 0)
|
||||
late int typeRaw;
|
||||
|
||||
@HiveField(4)
|
||||
bool? useSSL;
|
||||
|
||||
@HiveField(5, defaultValue: false)
|
||||
bool trusted;
|
||||
|
||||
@HiveField(6)
|
||||
String? socksProxyAddress;
|
||||
|
||||
@HiveField(7, defaultValue: '')
|
||||
String? path;
|
||||
|
||||
@HiveField(8)
|
||||
bool? isElectrs;
|
||||
|
||||
@HiveField(9)
|
||||
bool? supportsSilentPayments;
|
||||
|
||||
@HiveField(10)
|
||||
bool? supportsMweb;
|
||||
|
||||
@HiveField(11, defaultValue: false)
|
||||
bool isEnabledForAutoSwitching;
|
||||
|
||||
@HiveField(12, defaultValue: '')
|
||||
String? label;
|
||||
|
||||
bool get isSSL => useSSL ?? false;
|
||||
|
||||
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
|
||||
|
||||
Uri get uri {
|
||||
try {
|
||||
return _uri;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
return Uri();
|
||||
}
|
||||
}
|
||||
|
||||
Uri get _uri {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
case WalletType.wownero:
|
||||
return Uri.http(uriRaw, '');
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.dogecoin:
|
||||
return createUriFromElectrumAddress(uriRaw, path!);
|
||||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.base:
|
||||
case WalletType.bsc:
|
||||
case WalletType.arbitrum:
|
||||
return Uri.parse(
|
||||
"http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}");
|
||||
case WalletType.none:
|
||||
throw Exception('Unexpected type ${type.toString()} for Node uri');
|
||||
}
|
||||
}
|
||||
|
||||
bool get isValidProxyAddress => socksProxyAddress?.contains(':') ?? false;
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is Node &&
|
||||
(other.uriRaw == uriRaw &&
|
||||
other.login == login &&
|
||||
other.label == label &&
|
||||
other.password == password &&
|
||||
other.typeRaw == typeRaw &&
|
||||
other.useSSL == useSSL &&
|
||||
other.trusted == trusted &&
|
||||
other.socksProxyAddress == socksProxyAddress &&
|
||||
other.path == path);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
uriRaw.hashCode ^
|
||||
login.hashCode ^
|
||||
label.hashCode ^
|
||||
password.hashCode ^
|
||||
typeRaw.hashCode ^
|
||||
useSSL.hashCode ^
|
||||
trusted.hashCode ^
|
||||
socksProxyAddress.hashCode ^
|
||||
path.hashCode;
|
||||
|
||||
@override
|
||||
dynamic get keyIndex {
|
||||
_keyIndex ??= key;
|
||||
return _keyIndex;
|
||||
}
|
||||
|
||||
WalletType get type => deserializeFromInt(typeRaw);
|
||||
|
||||
set type(WalletType type) => typeRaw = serializeToInt(type);
|
||||
|
||||
dynamic _keyIndex;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'node_legacy.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class NodeAdapter extends TypeAdapter<Node> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
Node read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Node(
|
||||
label: fields[12] == null ? '' : fields[12] as String?,
|
||||
login: fields[1] as String?,
|
||||
password: fields[2] as String?,
|
||||
useSSL: fields[4] as bool?,
|
||||
trusted: fields[5] == null ? false : fields[5] as bool,
|
||||
socksProxyAddress: fields[6] as String?,
|
||||
path: fields[7] == null ? '' : fields[7] as String?,
|
||||
isEnabledForAutoSwitching: fields[11] == null ? false : fields[11] as bool,
|
||||
)
|
||||
..uriRaw = fields[0] == null ? '' : fields[0] as String
|
||||
..typeRaw = fields[3] == null ? 0 : fields[3] as int
|
||||
..isElectrs = fields[8] as bool?
|
||||
..supportsSilentPayments = fields[9] as bool?
|
||||
..supportsMweb = fields[10] as bool?;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Node obj) {
|
||||
writer
|
||||
..writeByte(13)
|
||||
..writeByte(0)
|
||||
..write(obj.uriRaw)
|
||||
..writeByte(1)
|
||||
..write(obj.login)
|
||||
..writeByte(2)
|
||||
..write(obj.password)
|
||||
..writeByte(3)
|
||||
..write(obj.typeRaw)
|
||||
..writeByte(4)
|
||||
..write(obj.useSSL)
|
||||
..writeByte(5)
|
||||
..write(obj.trusted)
|
||||
..writeByte(6)
|
||||
..write(obj.socksProxyAddress)
|
||||
..writeByte(7)
|
||||
..write(obj.path)
|
||||
..writeByte(8)
|
||||
..write(obj.isElectrs)
|
||||
..writeByte(9)
|
||||
..write(obj.supportsSilentPayments)
|
||||
..writeByte(10)
|
||||
..write(obj.supportsMweb)
|
||||
..writeByte(11)
|
||||
..write(obj.isEnabledForAutoSwitching)
|
||||
..writeByte(12)
|
||||
..write(obj.label);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is NodeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import "package:yaml/yaml.dart";
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
|
||||
Future<List<Node>> loadDefaultNodes(WalletType type) async {
|
||||
String path;
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
path = 'assets/node_list.yml';
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
path = 'assets/bitcoin_electrum_server_list.yml';
|
||||
break;
|
||||
case WalletType.litecoin:
|
||||
path = 'assets/litecoin_electrum_server_list.yml';
|
||||
break;
|
||||
// TODO: (refactoring) each wallet would have its path, so `wallet.nodePath` would be decided based on chain id in Evm wallet
|
||||
case WalletType.ethereum:
|
||||
path = 'assets/ethereum_server_list.yml';
|
||||
break;
|
||||
case WalletType.nano:
|
||||
path = 'assets/nano_node_list.yml';
|
||||
break;
|
||||
case WalletType.bitcoinCash:
|
||||
path = 'assets/bitcoin_cash_electrum_server_list.yml';
|
||||
break;
|
||||
case WalletType.polygon:
|
||||
path = 'assets/polygon_node_list.yml';
|
||||
break;
|
||||
case WalletType.wownero:
|
||||
path = 'assets/wownero_node_list.yml';
|
||||
break;
|
||||
case WalletType.dogecoin:
|
||||
path = 'assets/dogecoin_electrum_server_list.yml';
|
||||
break;
|
||||
case WalletType.base:
|
||||
path = 'assets/base_node_list.yml';
|
||||
break;
|
||||
case WalletType.arbitrum:
|
||||
path = 'assets/arbitrum_node_list.yml';
|
||||
break;
|
||||
case WalletType.bsc:
|
||||
path = 'assets/bsc_node_list.yml';
|
||||
break;
|
||||
case WalletType.banano:
|
||||
case WalletType.none:
|
||||
path = '';
|
||||
break;
|
||||
}
|
||||
|
||||
final nodesRaw = await rootBundle.loadString(path);
|
||||
final loadedNodes = loadYaml(nodesRaw) as YamlList;
|
||||
final nodes = <Node>[];
|
||||
|
||||
for (final raw in loadedNodes) {
|
||||
if (raw is Map) {
|
||||
final map = Map.from(raw);
|
||||
map["isBuiltin"] = 1;
|
||||
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
|
||||
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
|
||||
|
||||
final node = Node.fromMap(Map<String, Object>.from(map));
|
||||
node.type = type;
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
Future<List<Node>> loadDefaultNanoPowNodes() async {
|
||||
final powNodesRaw = await rootBundle.loadString('assets/nano_pow_node_list.yml');
|
||||
final loadedPowNodes = loadYaml(powNodesRaw) as YamlList;
|
||||
final nodes = <Node>[];
|
||||
|
||||
for (final raw in loadedPowNodes) {
|
||||
if (raw is Map) {
|
||||
final map = Map.from(raw);
|
||||
map["isBuiltin"] = 1;
|
||||
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
|
||||
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
|
||||
final node = Node.fromMap(Map<String, Object>.from(map));
|
||||
node.isPow = true;
|
||||
node.type = WalletType.nano;
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
Future<List<Node>> loadAllDefaultNodes() async {
|
||||
final wowneroNodes = await loadDefaultNodes(WalletType.wownero);
|
||||
final moneroNodes = await loadDefaultNodes(WalletType.monero);
|
||||
final bitcoinElectrumServerList = await loadDefaultNodes(WalletType.bitcoin);
|
||||
final litecoinElectrumServerList = await loadDefaultNodes(WalletType.litecoin);
|
||||
final bitcoinCashElectrumServerList = await loadDefaultNodes(WalletType.bitcoinCash);
|
||||
final ethereumNodes = await loadDefaultNodes(WalletType.ethereum);
|
||||
final nanoNodes = await loadDefaultNodes(WalletType.nano);
|
||||
final polygonNodes = await loadDefaultNodes(WalletType.polygon);
|
||||
final dogecoinElectrumServerList = await loadDefaultNodes(WalletType.dogecoin);
|
||||
final baseNodes = await loadDefaultNodes(WalletType.base);
|
||||
final arbitrumNodes = await loadDefaultNodes(WalletType.arbitrum);
|
||||
final bscNodes = await loadDefaultNodes(WalletType.bsc);
|
||||
|
||||
return wowneroNodes +
|
||||
moneroNodes +
|
||||
bitcoinElectrumServerList +
|
||||
litecoinElectrumServerList +
|
||||
ethereumNodes +
|
||||
bitcoinCashElectrumServerList +
|
||||
nanoNodes +
|
||||
polygonNodes +
|
||||
dogecoinElectrumServerList +
|
||||
baseNodes +
|
||||
arbitrumNodes +
|
||||
bscNodes;
|
||||
}
|
||||
|
||||
Future<void> resetToDefault() async {
|
||||
final nodes = await loadAllDefaultNodes();
|
||||
|
||||
final currentNodes = await Node.getAll();
|
||||
|
||||
// after reset, nodes should retain the id to correspond with the ones in shared prefs.
|
||||
// alternative would be nuking the shared prefs on reset
|
||||
for (final node in nodes) {
|
||||
try {
|
||||
final curr = currentNodes.firstWhere(
|
||||
(item) => item.uri == node.uri && item.typeRaw == node.typeRaw,
|
||||
);
|
||||
node.id = curr.id;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
await Node.deleteAll();
|
||||
for (final node in nodes) {
|
||||
await node.save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetPowToDefault() async {
|
||||
final nanoPowNodes = await loadDefaultNanoPowNodes();
|
||||
await Node.deleteAllPow();
|
||||
for (final node in nanoPowNodes) {
|
||||
node.save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Node> getDefaultNodeFromFiles(WalletType type, {bool isPow = false}) async {
|
||||
final nodes = isPow ? await loadDefaultNanoPowNodes() : await loadDefaultNodes(type);
|
||||
try {
|
||||
return nodes.firstWhere((item) => item.isDefault);
|
||||
} catch (e) {
|
||||
// ideally the yamls would always have a default, but this is better than nothing
|
||||
return nodes.first;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
class OutputInfo {
|
||||
const OutputInfo(
|
||||
{required this.address,
|
||||
required this.sendAll,
|
||||
required this.isParsedAddress,
|
||||
this.cryptoAmount,
|
||||
this.formattedCryptoAmount,
|
||||
this.fiatAmount,
|
||||
this.note,
|
||||
this.extractedAddress,
|
||||
this.memo,
|
||||
this.extra = const {}});
|
||||
const OutputInfo({
|
||||
required this.address,
|
||||
required this.sendAll,
|
||||
required this.isParsedAddress,
|
||||
required this.cryptoAmount,
|
||||
this.fiatAmount,
|
||||
this.note,
|
||||
this.extractedAddress,
|
||||
this.memo,
|
||||
this.extra = const {},
|
||||
});
|
||||
|
||||
final String? fiatAmount;
|
||||
final String? cryptoAmount;
|
||||
final Money cryptoAmount;
|
||||
final String address;
|
||||
final String? note;
|
||||
final String? extractedAddress;
|
||||
final bool sendAll;
|
||||
final bool isParsedAddress;
|
||||
final int? formattedCryptoAmount;
|
||||
final String? memo;
|
||||
final Map<String, dynamic> extra;
|
||||
}
|
||||
|
||||
@@ -29,32 +29,49 @@ BigInt? tryParseFixed(String value, int decimals) {
|
||||
BigInt parseFixed(String value, int decimals) {
|
||||
final multiplier = getMultiplier(decimals);
|
||||
|
||||
/// handle weird cases where users enter spaces and currency after the amount
|
||||
/// This should be handled from UI field to prevent non numerical values
|
||||
/// but will be in the refactoring
|
||||
if (value.contains(" ")) {
|
||||
value = value.split(" ").first;
|
||||
}
|
||||
|
||||
final negative = value.startsWith("-");
|
||||
if (negative) value = value.substring(1);
|
||||
if (negative) {
|
||||
value = value.substring(1);
|
||||
}
|
||||
|
||||
if (value == ".") throw FormatException("missing value, value, $value");
|
||||
if (value == ".") {
|
||||
throw FormatException("missing value, value, $value");
|
||||
}
|
||||
|
||||
if (value.startsWith(".")) value = "0$value";
|
||||
if (value.startsWith(".")) {
|
||||
value = "0$value";
|
||||
}
|
||||
|
||||
final comps = value.split(".");
|
||||
if (comps.length > 2) {
|
||||
throw FormatException("too many decimal points, value, $value");
|
||||
}
|
||||
|
||||
var whole = comps.isNotEmpty ? comps[0] : "0";
|
||||
var fraction = (comps.length == 2 ? comps[1] : "0").padRight(decimals, "0");
|
||||
final whole = comps.isNotEmpty ? comps[0] : "0";
|
||||
final fraction = (comps.length == 2 ? comps[1] : "").padRight(decimals, "0");
|
||||
|
||||
if (fraction.length > multiplier.length - 1) {
|
||||
throw FormatException("fractional component exceeds decimals, underflow, parseFixed");
|
||||
throw FormatException(
|
||||
"fractional component(${fraction.length}) exceeds decimals(${decimals}), underflow, parseFixed",
|
||||
);
|
||||
}
|
||||
|
||||
final wholeValue = BigInt.parse(whole);
|
||||
final fractionValue = BigInt.parse(fraction);
|
||||
final fractionValue = fraction.isEmpty ? BigInt.zero : BigInt.parse(fraction);
|
||||
final multiplierValue = BigInt.parse(multiplier);
|
||||
|
||||
var wei = (wholeValue * multiplierValue) + fractionValue;
|
||||
|
||||
if (negative) wei *= BigInt.from(-1);
|
||||
if (negative) {
|
||||
wei *= BigInt.from(-1);
|
||||
}
|
||||
|
||||
return wei;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
Future<String> pathForWalletTypeDir({required WalletType type}) async {
|
||||
final root = await getAppDir();
|
||||
@@ -35,3 +36,43 @@ Future<String> outdatedAndroidPathForWalletDir({required String name}) async {
|
||||
|
||||
return pathDir;
|
||||
}
|
||||
|
||||
Future<void> copyWalletFilesTo({
|
||||
required String fromName,
|
||||
required String toName,
|
||||
required WalletType type,
|
||||
}) async {
|
||||
if (fromName == toName) return;
|
||||
|
||||
final typeRoot = await pathForWalletTypeDir(type: type);
|
||||
final sourceDir = Directory(p.join(typeRoot, fromName));
|
||||
if (!sourceDir.existsSync()) {
|
||||
throw "Source wallet not found: $fromName $type";
|
||||
}
|
||||
|
||||
if (Directory(p.join(typeRoot, toName)).existsSync()) {
|
||||
throw Exception('Cannot rename wallet: "$toName" already exists');
|
||||
}
|
||||
|
||||
final destinationDir = Directory(p.join(typeRoot, toName));
|
||||
await _copyDirectory(sourceDir, destinationDir);
|
||||
|
||||
for (final suffix in const ['', '.keys', '.keys.backup']) {
|
||||
final file = File(p.join(destinationDir.path, '$fromName$suffix'));
|
||||
if (file.existsSync()) {
|
||||
await file.rename(p.join(destinationDir.path, '$toName$suffix'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _copyDirectory(Directory source, Directory destination) async {
|
||||
await destination.create(recursive: true);
|
||||
await for (final entity in source.list(followLinks: false)) {
|
||||
final name = p.basename(entity.path);
|
||||
if (entity is File) {
|
||||
await entity.copy(p.join(destination.path, name));
|
||||
} else if (entity is Directory) {
|
||||
await _copyDirectory(entity, Directory(p.join(destination.path, name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'payjoin_session.g.dart';
|
||||
part 'payjoin_session.part.dart';
|
||||
|
||||
@HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
|
||||
// @HiveType(typeId: PAYJOIN_SESSION_TYPE_ID)
|
||||
class PayjoinSession extends HiveObject {
|
||||
PayjoinSession({
|
||||
required this.walletId,
|
||||
@@ -25,31 +25,31 @@ class PayjoinSession extends HiveObject {
|
||||
static const typeId = PAYJOIN_SESSION_TYPE_ID;
|
||||
static const boxName = 'PayjoinSessions';
|
||||
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
final String walletId;
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
final String? sender;
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
final String? receiver;
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
final String? pjUri;
|
||||
|
||||
@HiveField(4)
|
||||
// @HiveField(4)
|
||||
String status;
|
||||
|
||||
@HiveField(5)
|
||||
// @HiveField(5)
|
||||
DateTime? inProgressSince;
|
||||
|
||||
@HiveField(6)
|
||||
// @HiveField(6)
|
||||
String? txId;
|
||||
|
||||
@HiveField(7)
|
||||
// @HiveField(7)
|
||||
String? rawAmount;
|
||||
|
||||
@HiveField(8)
|
||||
// @HiveField(8)
|
||||
String? error;
|
||||
|
||||
bool get isSenderSession => sender != null;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'payjoin_session.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class PayjoinSessionAdapter extends TypeAdapter<PayjoinSession> {
|
||||
@override
|
||||
final int typeId = 23;
|
||||
|
||||
@override
|
||||
PayjoinSession read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PayjoinSession(
|
||||
walletId: fields[0] as String,
|
||||
receiver: fields[2] as String?,
|
||||
sender: fields[1] as String?,
|
||||
pjUri: fields[3] as String?,
|
||||
status: fields[4] as String,
|
||||
inProgressSince: fields[5] as DateTime?,
|
||||
rawAmount: fields[7] as String?,
|
||||
)
|
||||
..txId = fields[6] as String?
|
||||
..error = fields[8] as String?;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PayjoinSession obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.walletId)
|
||||
..writeByte(1)
|
||||
..write(obj.sender)
|
||||
..writeByte(2)
|
||||
..write(obj.receiver)
|
||||
..writeByte(3)
|
||||
..write(obj.pjUri)
|
||||
..writeByte(4)
|
||||
..write(obj.status)
|
||||
..writeByte(5)
|
||||
..write(obj.inProgressSince)
|
||||
..writeByte(6)
|
||||
..write(obj.txId)
|
||||
..writeByte(7)
|
||||
..write(obj.rawAmount)
|
||||
..writeByte(8)
|
||||
..write(obj.error);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PayjoinSessionAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
+174
-191
@@ -1,4 +1,4 @@
|
||||
import 'package:cw_core/format_fixed.dart';
|
||||
import "package:cw_core/format_fixed.dart";
|
||||
|
||||
abstract class PaymentURI {
|
||||
PaymentURI({required this.amount, required this.address});
|
||||
@@ -7,12 +7,19 @@ abstract class PaymentURI {
|
||||
final String address;
|
||||
}
|
||||
|
||||
class ExternalAddressURI extends PaymentURI {
|
||||
ExternalAddressURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() => address;
|
||||
}
|
||||
|
||||
class MoneroURI extends PaymentURI {
|
||||
MoneroURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'monero:$address';
|
||||
var base = "monero:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -27,7 +34,7 @@ class HavenURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'haven:$address';
|
||||
var base = "haven:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -38,7 +45,7 @@ class HavenURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class BitcoinURI extends PaymentURI {
|
||||
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
|
||||
BitcoinURI({required super.amount, required super.address, this.pjUri = ""});
|
||||
|
||||
final String pjUri;
|
||||
|
||||
@@ -46,13 +53,16 @@ class BitcoinURI extends PaymentURI {
|
||||
String toString() {
|
||||
final qp = <String, String>{};
|
||||
|
||||
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
|
||||
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
|
||||
qp['pjos'] = '0';
|
||||
qp['pj'] = pjUri;
|
||||
if (amount.isNotEmpty) {
|
||||
qp["amount"] = amount.replaceAll(",", ".");
|
||||
}
|
||||
|
||||
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
|
||||
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
|
||||
qp["pjos"] = "0";
|
||||
qp["pj"] = pjUri;
|
||||
}
|
||||
|
||||
return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +71,7 @@ class LitecoinURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'litecoin:$address';
|
||||
var base = "litecoin:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -76,52 +86,7 @@ class EthereumURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'ethereum:$address';
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
class BaseURI extends PaymentURI {
|
||||
BaseURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'base:$address';
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
class ArbitrumURI extends PaymentURI {
|
||||
ArbitrumURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'arbitrum:$address';
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
class BSCURI extends PaymentURI {
|
||||
BSCURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'bsc:$address';
|
||||
var base = "ethereum:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -151,22 +116,7 @@ class NanoURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'nano:$address';
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
class PolygonURI extends PaymentURI {
|
||||
PolygonURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'polygon:$address';
|
||||
|
||||
var base = "nano:$address";
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
@@ -176,14 +126,23 @@ class PolygonURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class SolanaURI extends PaymentURI {
|
||||
SolanaURI({required super.amount, required super.address});
|
||||
SolanaURI({required super.amount, required super.address, this.contractAddress});
|
||||
|
||||
final String? contractAddress;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'solana:$address';
|
||||
var base = "solana:$address";
|
||||
final params = <String>[];
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
params.add('amount=${amount.replaceAll(',', '.')}');
|
||||
}
|
||||
if (contractAddress != null && contractAddress!.isNotEmpty) {
|
||||
params.add("spl-token=$contractAddress");
|
||||
}
|
||||
if (params.isNotEmpty) {
|
||||
base += '?${params.join('&')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -191,14 +150,23 @@ class SolanaURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class TronURI extends PaymentURI {
|
||||
TronURI({required super.amount, required super.address});
|
||||
TronURI({required super.amount, required super.address, this.contractAddress});
|
||||
|
||||
final String? contractAddress;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'tron:$address';
|
||||
var base = "tron:$address";
|
||||
final params = <String>[];
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
params.add('amount=${amount.replaceAll(',', '.')}');
|
||||
}
|
||||
if (contractAddress != null && contractAddress!.isNotEmpty) {
|
||||
params.add("token=$contractAddress");
|
||||
}
|
||||
if (params.isNotEmpty) {
|
||||
base += '?${params.join('&')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -210,7 +178,7 @@ class WowneroURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'wownero:$address';
|
||||
var base = "wownero:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?tx_amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -221,12 +189,11 @@ class WowneroURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class ZanoURI extends PaymentURI {
|
||||
ZanoURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
ZanoURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'zano:' + address;
|
||||
var base = "zano:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -237,12 +204,11 @@ class ZanoURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class DecredURI extends PaymentURI {
|
||||
DecredURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
DecredURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'decred:' + address;
|
||||
var base = "decred:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -253,12 +219,11 @@ class DecredURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class DogeURI extends PaymentURI {
|
||||
DogeURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
DogeURI({required super.amount, required super.address});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'doge:' + address;
|
||||
var base = "doge:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -273,7 +238,7 @@ class ZcashURI extends PaymentURI {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'zcash:$address';
|
||||
var base = "zcash:$address";
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
@@ -284,93 +249,45 @@ class ZcashURI extends PaymentURI {
|
||||
}
|
||||
|
||||
class ERC681URI extends PaymentURI {
|
||||
final int chainId;
|
||||
final String? contractAddress;
|
||||
|
||||
ERC681URI({
|
||||
required this.chainId,
|
||||
required super.address,
|
||||
required super.amount,
|
||||
required this.contractAddress,
|
||||
this.tokenDecimals = 18,
|
||||
this.rawTokenAmount,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var uri = 'ethereum:';
|
||||
|
||||
final targetAddress = contractAddress ?? address;
|
||||
uri += targetAddress;
|
||||
|
||||
if (chainId != 1) {
|
||||
uri += '@$chainId';
|
||||
}
|
||||
|
||||
if (contractAddress != null) {
|
||||
uri += '/transfer';
|
||||
}
|
||||
|
||||
final params = <String, String>{};
|
||||
|
||||
if (contractAddress != null) {
|
||||
params['address'] = address;
|
||||
if (amount.isNotEmpty) {
|
||||
params['uint256'] = _formatAmountForERC20(amount);
|
||||
}
|
||||
} else {
|
||||
if (amount.isNotEmpty) {
|
||||
params['value'] = _formatAmountForNative(amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.isNotEmpty) {
|
||||
uri += '?';
|
||||
uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/// Formats amount for ERC-20 token transfers (in atomic units)
|
||||
String _formatAmountForERC20(String amount) {
|
||||
try {
|
||||
// Convert decimal amount to BigInt (assuming 18 decimals)
|
||||
final amountDouble = double.parse(amount.replaceAll(',', '.'));
|
||||
final amountBigInt = BigInt.from(amountDouble * 1e18);
|
||||
return amountBigInt.toString();
|
||||
} catch (e) {
|
||||
// Fallback to original amount if parsing fails
|
||||
return amount.replaceAll(',', '.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats amount for native ETH payments (in wei using scientific notation)
|
||||
String _formatAmountForNative(String amount) {
|
||||
try {
|
||||
// Convert decimal amount to double for scientific notation
|
||||
final amountDouble = double.parse(amount.replaceAll(',', '.'));
|
||||
|
||||
// Use scientific notation as recommended by ERC-681
|
||||
return '${amountDouble}e18';
|
||||
} catch (e) {
|
||||
// Fallback to original amount if parsing fails
|
||||
return amount.replaceAll(',', '.');
|
||||
}
|
||||
}
|
||||
|
||||
factory ERC681URI.fromUri(Uri uri) {
|
||||
final (isContract, targetAddress) = _getTargetAddress(uri.path);
|
||||
final chainId = _getChainID(uri.path);
|
||||
|
||||
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
|
||||
final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
|
||||
final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress;
|
||||
|
||||
var formatedAmount = "";
|
||||
String? rawTokenAmount;
|
||||
|
||||
if (amountParam != null) {
|
||||
final normalized = _normalizeToIntegerWei(amountParam);
|
||||
formatedAmount = formatFixed(BigInt.parse(normalized), 18);
|
||||
} else {
|
||||
if (isContract) {
|
||||
formatedAmount = uri.queryParameters["amount"] ?? "";
|
||||
final uint256Param = uri.queryParameters["uint256"];
|
||||
if (uint256Param != null) {
|
||||
final raw = uint256Param.replaceAll(",", ".").trim();
|
||||
if (!_scientificPattern.hasMatch(raw) && raw.contains(".")) {
|
||||
if (formatedAmount.isEmpty) {
|
||||
formatedAmount = raw;
|
||||
}
|
||||
} else {
|
||||
rawTokenAmount = _normalizeToIntegerWei(raw);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final valueParam = uri.queryParameters["value"];
|
||||
if (valueParam != null) {
|
||||
final normalized = BigInt.tryParse(_normalizeToIntegerWei(valueParam));
|
||||
formatedAmount = normalized != null ? formatFixed(normalized, 18) : "";
|
||||
} else {
|
||||
formatedAmount = uri.queryParameters["amount"] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
return ERC681URI(
|
||||
@@ -378,20 +295,85 @@ class ERC681URI extends PaymentURI {
|
||||
address: address,
|
||||
amount: formatedAmount,
|
||||
contractAddress: isContract ? targetAddress : null,
|
||||
rawTokenAmount: rawTokenAmount,
|
||||
);
|
||||
}
|
||||
|
||||
static int _getChainID(String path) {
|
||||
return int.parse(RegExp(
|
||||
r'@\d*',
|
||||
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
|
||||
"1");
|
||||
final int chainId;
|
||||
final String? contractAddress;
|
||||
final int tokenDecimals;
|
||||
final String? rawTokenAmount;
|
||||
|
||||
static final RegExp _scientificPattern = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$");
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var uri = "ethereum:";
|
||||
|
||||
final targetAddress = contractAddress ?? address;
|
||||
uri += targetAddress;
|
||||
|
||||
uri += "@$chainId";
|
||||
|
||||
if (contractAddress != null) {
|
||||
uri += "/transfer";
|
||||
}
|
||||
|
||||
final params = <String, String>{};
|
||||
|
||||
if (contractAddress != null) {
|
||||
params["address"] = address;
|
||||
// only emit an atomic uint256 when it is unambiguous, either stored raw or 18 decimals,
|
||||
// old app versions divide any uint256 by 1e18 so other tokens rely on the amount param
|
||||
final uint256 = rawTokenAmount ??
|
||||
(tokenDecimals == 18 && amount.isNotEmpty
|
||||
? _expandDecimal(amount.replaceAll(",", "."), 18)
|
||||
: null);
|
||||
if (uint256 != null && uint256.isNotEmpty) {
|
||||
params["uint256"] = uint256;
|
||||
}
|
||||
if (amount.isNotEmpty) {
|
||||
params["amount"] = amount.replaceAll(",", ".");
|
||||
}
|
||||
} else {
|
||||
if (amount.isNotEmpty) {
|
||||
params["value"] = _formatAmountForNative(amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.isNotEmpty) {
|
||||
uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}";
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/// Formats amount for native ETH payments (in wei using scientific notation)
|
||||
String _formatAmountForNative(String amount) {
|
||||
try {
|
||||
// Convert decimal amount to double for scientific notation
|
||||
final amountDouble = double.parse(amount.replaceAll(",", "."));
|
||||
|
||||
// Use scientific notation as recommended by ERC-681
|
||||
return "${amountDouble}e18";
|
||||
} catch (e) {
|
||||
// Fallback to original amount if parsing fails
|
||||
return amount.replaceAll(",", ".");
|
||||
}
|
||||
}
|
||||
|
||||
static int _getChainID(String path) =>
|
||||
int.tryParse(
|
||||
RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1",
|
||||
) ??
|
||||
1;
|
||||
|
||||
static (bool, String) _getTargetAddress(String path) {
|
||||
final targetAddress =
|
||||
RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
|
||||
return (path.contains("/"), targetAddress);
|
||||
// I saw in the schema (thanks Konsti) that EIP-681 allows an optional "pay-" prefix before the target address, so adding a check for it here
|
||||
final cleaned = path.startsWith("pay-") ? path.substring(4) : path;
|
||||
final match = RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(cleaned);
|
||||
final targetAddress = match?.group(0) ?? cleaned.split("@").first.split("/").first;
|
||||
return (cleaned.contains("/"), targetAddress);
|
||||
}
|
||||
|
||||
/// Normalize an input amount into an integer wei string.
|
||||
@@ -401,25 +383,26 @@ class ERC681URI extends PaymentURI {
|
||||
/// - Scientific notation: "0.123e18", "1e6" → expanded to integer
|
||||
/// - Decimal ETH: "0.123456" → shifted by 18 decimals
|
||||
static String _normalizeToIntegerWei(String input) {
|
||||
final raw = input.replaceAll(',', '.').trim();
|
||||
final raw = input.replaceAll(",", ".").trim();
|
||||
|
||||
// First we check if it's already a plain integer (basically just a number with no dot, no exponent)
|
||||
try {
|
||||
final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
|
||||
!raw.contains('.') &&
|
||||
!raw.toLowerCase().contains('e');
|
||||
if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
|
||||
final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) &&
|
||||
!raw.contains(".") &&
|
||||
!raw.toLowerCase().contains("e");
|
||||
if (isPlainInteger) {
|
||||
return raw.replaceFirst(RegExp(r"^\+"), "");
|
||||
}
|
||||
|
||||
// Then we check if it's a scientific notation
|
||||
final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
|
||||
if (sci.hasMatch(raw)) {
|
||||
final mantissaStr = raw.toLowerCase().split('e')[0];
|
||||
final exp = int.parse(raw.toLowerCase().split('e')[1]);
|
||||
if (_scientificPattern.hasMatch(raw)) {
|
||||
final mantissaStr = raw.toLowerCase().split("e")[0];
|
||||
final exp = int.parse(raw.toLowerCase().split("e")[1]);
|
||||
return _expandDecimal(mantissaStr, exp);
|
||||
}
|
||||
|
||||
// Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
|
||||
if (raw.contains('.')) {
|
||||
if (raw.contains(".")) {
|
||||
return _expandDecimal(raw, 18);
|
||||
}
|
||||
return raw;
|
||||
@@ -437,34 +420,34 @@ class ERC681URI extends PaymentURI {
|
||||
/// _expandDecimal('1.2', 3) -> '1200'
|
||||
static String _expandDecimal(String decimalStr, int expShift) {
|
||||
var s = decimalStr.trim();
|
||||
var sign = '';
|
||||
if (s.startsWith('-') || s.startsWith('+')) {
|
||||
sign = s[0] == '-' ? '-' : '';
|
||||
var sign = "";
|
||||
if (s.startsWith("-") || s.startsWith("+")) {
|
||||
sign = s[0] == "-" ? "-" : "";
|
||||
s = s.substring(1);
|
||||
}
|
||||
|
||||
// First we split the integer and fractional parts
|
||||
final parts = s.split('.');
|
||||
final intPart = parts[0].isEmpty ? '0' : parts[0];
|
||||
final fracPart = parts.length > 1 ? parts[1] : '';
|
||||
final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
|
||||
final parts = s.split(".");
|
||||
final intPart = parts[0].isEmpty ? "0" : parts[0];
|
||||
final fracPart = parts.length > 1 ? parts[1] : "";
|
||||
final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), "");
|
||||
final fracLen = fracPart.length;
|
||||
|
||||
// Then we calculate the effective shift = desired shift minus existing fractional digits
|
||||
final shift = expShift - fracLen;
|
||||
if (shift >= 0) {
|
||||
final head = digits.isEmpty ? '0' : digits;
|
||||
final zeros = List.filled(shift, '0').join();
|
||||
final head = digits.isEmpty ? "0" : digits;
|
||||
final zeros = List.filled(shift, "0").join();
|
||||
final res = head + zeros;
|
||||
return sign + (res.isEmpty ? '0' : res);
|
||||
return sign + (res.isEmpty ? "0" : res);
|
||||
} else {
|
||||
// Need to insert a decimal point within digits; return integer by truncating
|
||||
final cut = digits.length + shift;
|
||||
if (cut <= 0) {
|
||||
return '0';
|
||||
return "0";
|
||||
}
|
||||
final res = digits.substring(0, cut);
|
||||
return sign + (res.isEmpty ? '0' : res);
|
||||
return sign + (res.isEmpty ? "0" : res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
|
||||
class PendingChange {
|
||||
final String address;
|
||||
final BigInt amount;
|
||||
@@ -7,22 +9,20 @@ class PendingChange {
|
||||
|
||||
mixin PendingTransaction {
|
||||
String get id;
|
||||
|
||||
Money get amount;
|
||||
Money get fee;
|
||||
Money? get additionalCost => null;
|
||||
|
||||
String get amountFormatted;
|
||||
String get feeFormatted;
|
||||
String get feeFormattedValue;
|
||||
String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8);
|
||||
String get feeFormattedValue => fee.toStringWithPrecision(fractionalDigits: 8);
|
||||
String? feeRate;
|
||||
String get hex;
|
||||
String? get evmTxHashFromRawHex => null;
|
||||
int? get outputCount => null;
|
||||
PendingChange? change;
|
||||
|
||||
String get roundedAmountFormatted {
|
||||
if (amountFormatted.split(".").last.length <= 8) {
|
||||
return amountFormatted;
|
||||
}
|
||||
return double.parse(amountFormatted).toStringAsFixed(8);
|
||||
}
|
||||
|
||||
bool shouldCommitUR() => false;
|
||||
|
||||
Future<void> commit();
|
||||
|
||||
@@ -3,8 +3,6 @@ import 'package:cw_core/enumerate.dart';
|
||||
class ReceivePageOption implements Enumerate {
|
||||
static const mainnet = ReceivePageOption._('mainnet');
|
||||
static const testnet = ReceivePageOption._('testnet');
|
||||
static const anonPayInvoice = ReceivePageOption._('anonPayInvoice');
|
||||
static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink');
|
||||
|
||||
const ReceivePageOption._(this.value,
|
||||
{this.iconPath, this.description, this.isCommon = false, this.addAddressWord = false});
|
||||
@@ -22,7 +20,4 @@ class ReceivePageOption implements Enumerate {
|
||||
|
||||
const ReceivePageOptions = [
|
||||
ReceivePageOption.mainnet,
|
||||
// FIXME anonpay disabed for new ui beta. restore after screens are ready
|
||||
// ReceivePageOption.anonPayInvoice,
|
||||
// ReceivePageOption.anonPayDonationLink
|
||||
];
|
||||
|
||||
+146
-59
@@ -1,42 +1,8 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'spl_token.g.dart';
|
||||
|
||||
@HiveType(typeId: SPLToken.typeId)
|
||||
class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
@override
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
@HiveField(2)
|
||||
final String mintAddress;
|
||||
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
@HiveField(5)
|
||||
final String mint;
|
||||
|
||||
@override
|
||||
@HiveField(6)
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
@HiveField(7)
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
@HiveField(8, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class SPLToken extends CryptoCurrency {
|
||||
SPLToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
@@ -44,9 +10,12 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
required this.decimal,
|
||||
required this.mint,
|
||||
this.iconPath,
|
||||
this.tag = 'SOL',
|
||||
this.tag = "SOL",
|
||||
bool enabled = true,
|
||||
this.isPotentialScam = false,
|
||||
super.groups,
|
||||
this.id = 0,
|
||||
this.walletName,
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: mint.toLowerCase(),
|
||||
@@ -63,27 +32,21 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
required String mint,
|
||||
required String symbol,
|
||||
required String mintAddress,
|
||||
required int decimal,
|
||||
String? iconPath,
|
||||
bool isPotentialScam = false,
|
||||
}) {
|
||||
return SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: 0,
|
||||
mint: mint,
|
||||
iconPath: iconPath,
|
||||
isPotentialScam: isPotentialScam,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: decimal,
|
||||
mint: mint,
|
||||
iconPath: iconPath,
|
||||
isPotentialScam: isPotentialScam,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled})
|
||||
SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName})
|
||||
: name = other.name,
|
||||
symbol = other.symbol,
|
||||
mintAddress = other.mintAddress,
|
||||
@@ -93,6 +56,8 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
tag = tag ?? other.tag,
|
||||
iconPath = icon ?? other.iconPath,
|
||||
isPotentialScam = other.isPotentialScam,
|
||||
id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
super(
|
||||
title: other.symbol.toUpperCase(),
|
||||
name: other.symbol.toLowerCase(),
|
||||
@@ -101,10 +66,129 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
tag: other.tag,
|
||||
iconPath: icon,
|
||||
isPotentialScam: other.isPotentialScam,
|
||||
groups: other.groups,
|
||||
);
|
||||
|
||||
static const typeId = SPL_TOKEN_TYPE_ID;
|
||||
static const boxName = 'SPLTokens';
|
||||
SPLToken.fromMap(Map<String, Object?> map)
|
||||
: this(
|
||||
name: map["name"] as String? ?? "",
|
||||
symbol: map["symbol"] as String? ?? "",
|
||||
mintAddress: map["mintAddress"] as String? ?? "",
|
||||
decimal: (map["decimal"] ?? 0) as int,
|
||||
mint: map["mint"] as String? ?? "",
|
||||
enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
|
||||
iconPath: map["iconPath"] as String?,
|
||||
tag: map["tag"] as String?,
|
||||
isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
|
||||
id: (map[selfIdColumn] ?? 0) as int,
|
||||
walletName: map["walletName"] as String?,
|
||||
);
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
final String mintAddress;
|
||||
|
||||
final int decimal;
|
||||
|
||||
bool _enabled;
|
||||
|
||||
final String mint;
|
||||
|
||||
@override
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
bool isPotentialScam;
|
||||
|
||||
int id;
|
||||
String? walletName;
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"mintAddress": mintAddress,
|
||||
"decimal": decimal,
|
||||
"mint": mint,
|
||||
"enabled": _enabled ? 1 : 0,
|
||||
"iconPath": iconPath,
|
||||
"tag": tag,
|
||||
"isPotentialScam": isPotentialScam ? 1 : 0,
|
||||
};
|
||||
|
||||
static String get tableName => "SPLToken";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
Future<int> save() async {
|
||||
if (walletName == null) {
|
||||
throw StateError("SPLToken.save() requires walletName to be set");
|
||||
}
|
||||
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<SPLToken>> selectList(String where, List<dynamic> whereArgs,
|
||||
{String? orderBy}) async {
|
||||
orderBy ??= selfIdColumn;
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => SPLToken.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<List<SPLToken>> getAllForWallet(String walletName) async =>
|
||||
selectList("walletName = ?", [walletName]);
|
||||
|
||||
static Future<SPLToken?> getByMint(String walletName, String mintAddress) async {
|
||||
final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
static Future<int> deleteForWallet(String walletName, String mintAddress) => db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND mintAddress = ?",
|
||||
whereArgs: [walletName, mintAddress],
|
||||
);
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName) =>
|
||||
db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
|
||||
|
||||
static Future<void> renameWallet(String oldName, String newName) async {
|
||||
await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
|
||||
await db!
|
||||
.update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
@@ -113,4 +197,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
|
||||
|
||||
@override
|
||||
int get hashCode => mintAddress.hashCode;
|
||||
|
||||
@override
|
||||
String get apiString => "sol.$mintAddress";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import "package:cw_core/cake_hive.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/hive_type_ids.dart";
|
||||
import "package:cw_core/spl_token.dart" as spl_new;
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:hive/hive.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
part "spl_token_legacy.part.dart";
|
||||
|
||||
Future<void> performSplTokenHiveMigration() async {
|
||||
try {
|
||||
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
|
||||
CakeHive.registerAdapter(SPLTokenAdapter());
|
||||
}
|
||||
|
||||
final wallets = await WalletInfo.getAll();
|
||||
await SPLToken.migrateAllToSqlite(wallets);
|
||||
} catch (e) {
|
||||
printV("Error performing SPLToken Hive migration: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
// @HiveType(typeId: SPLToken.typeId)
|
||||
class SPLToken extends HiveObject {
|
||||
SPLToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.mintAddress,
|
||||
required this.decimal,
|
||||
required this.mint,
|
||||
this.iconPath,
|
||||
this.tag = "SOL",
|
||||
bool enabled = true,
|
||||
this.isPotentialScam = false,
|
||||
}) : _enabled = enabled;
|
||||
// @HiveField(0)
|
||||
final String name;
|
||||
|
||||
// @HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
// @HiveField(2)
|
||||
final String mintAddress;
|
||||
|
||||
// @HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
// @HiveField(5)
|
||||
final String mint;
|
||||
|
||||
// @HiveField(6)
|
||||
final String? iconPath;
|
||||
|
||||
// @HiveField(7)
|
||||
final String? tag;
|
||||
|
||||
// @HiveField(8, defaultValue: false)
|
||||
bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static const typeId = SPL_TOKEN_TYPE_ID;
|
||||
static const boxName = "SPLTokens";
|
||||
|
||||
static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
|
||||
final sanitizedToRawNames = <String, Set<String>>{};
|
||||
for (final wallet in wallets) {
|
||||
// Hash Bags: solana is removed, so no wallet has tokens to migrate.
|
||||
continue;
|
||||
|
||||
sanitizedToRawNames
|
||||
.putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
|
||||
.add(wallet.name);
|
||||
}
|
||||
|
||||
for (final entry in sanitizedToRawNames.entries) {
|
||||
final tokenBoxName = "${entry.key}_$boxName";
|
||||
try {
|
||||
if (!await CakeHive.boxExists(tokenBoxName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<SPLToken>(tokenBoxName);
|
||||
|
||||
for (final key in box.keys.toList()) {
|
||||
final token = box.get(key);
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final rawName in entry.value) {
|
||||
await token.migrateToSqlite(walletName: rawName);
|
||||
}
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating spl token box $tokenBoxName: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite({required String walletName}) async {
|
||||
final row = spl_new.SPLToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
mintAddress: mintAddress,
|
||||
decimal: decimal,
|
||||
mint: mint,
|
||||
enabled: _enabled,
|
||||
iconPath: iconPath,
|
||||
tag: tag,
|
||||
isPotentialScam: isPotentialScam,
|
||||
walletName: walletName,
|
||||
).toMap();
|
||||
row[spl_new.SPLToken.selfIdColumn] = null;
|
||||
|
||||
await db!.insert(
|
||||
spl_new.SPLToken.tableName,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of "spl_token_legacy.dart";
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class SPLTokenAdapter extends TypeAdapter<SPLToken> {
|
||||
@override
|
||||
final int typeId = 16;
|
||||
|
||||
@override
|
||||
SPLToken read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return SPLToken(
|
||||
name: fields[0] as String,
|
||||
symbol: fields[1] as String,
|
||||
mintAddress: fields[2] as String,
|
||||
decimal: fields[3] as int,
|
||||
mint: fields[5] as String,
|
||||
iconPath: fields[6] as String?,
|
||||
tag: fields[7] as String?,
|
||||
isPotentialScam: fields[8] == null ? false : fields[8] as bool,
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SPLToken obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.symbol)
|
||||
..writeByte(2)
|
||||
..write(obj.mintAddress)
|
||||
..writeByte(3)
|
||||
..write(obj.decimal)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.mint)
|
||||
..writeByte(6)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(7)
|
||||
..write(obj.tag)
|
||||
..writeByte(8)
|
||||
..write(obj.isPotentialScam);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SPLTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/keyable.dart';
|
||||
|
||||
abstract class TransactionInfo extends Object with Keyable {
|
||||
late String id;
|
||||
late String txHash = id;
|
||||
late int amount;
|
||||
int? fee;
|
||||
late Money amount;
|
||||
Money? fee;
|
||||
late TransactionDirection direction;
|
||||
late bool isPending;
|
||||
late DateTime date;
|
||||
int? height;
|
||||
int confirmations = 0;
|
||||
String amountFormatted();
|
||||
String fiatAmount();
|
||||
String? feeFormatted();
|
||||
void changeFiatAmount(String amount);
|
||||
String? to;
|
||||
String? from;
|
||||
String? evmSignatureName;
|
||||
@@ -26,4 +24,8 @@ abstract class TransactionInfo extends Object with Keyable {
|
||||
dynamic get keyIndex => id;
|
||||
|
||||
Map<String, dynamic> additionalInfo = {};
|
||||
|
||||
String? _fiatAmount;
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
}
|
||||
|
||||
+135
-42
@@ -1,41 +1,8 @@
|
||||
// ignore_for_file: annotate_overrides, overridden_fields
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'tron_token.g.dart';
|
||||
|
||||
@HiveType(typeId: TronToken.typeId)
|
||||
class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
@HiveField(2)
|
||||
final String contractAddress;
|
||||
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
@HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
@HiveField(6)
|
||||
final String? tag;
|
||||
|
||||
@HiveField(7, defaultValue: false)
|
||||
final bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class TronToken extends CryptoCurrency {
|
||||
TronToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
@@ -43,8 +10,10 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
this.tag = 'TRX',
|
||||
this.tag = "TRX",
|
||||
this.isPotentialScam = false,
|
||||
this.id = 0,
|
||||
this.walletName,
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: symbol.toLowerCase(),
|
||||
@@ -55,9 +24,27 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
decimals: decimal,
|
||||
isPotentialScam: isPotentialScam,
|
||||
);
|
||||
TronToken.fromMap(Map<String, Object?> map)
|
||||
: this(
|
||||
name: map["name"] as String? ?? "",
|
||||
symbol: map["symbol"] as String? ?? "",
|
||||
contractAddress: map["contractAddress"] as String? ?? "",
|
||||
decimal: (map["decimal"] ?? 0) as int,
|
||||
enabled: _getBoolFromDB(map["enabled"], defaultValue: true),
|
||||
iconPath: map["iconPath"] as String?,
|
||||
tag: map["tag"] as String?,
|
||||
isPotentialScam: _getBoolFromDB(map["isPotentialScam"]),
|
||||
id: (map[selfIdColumn] ?? 0) as int,
|
||||
walletName: map["walletName"] as String?,
|
||||
);
|
||||
|
||||
TronToken.copyWith(TronToken other, {String? icon, String? tag, bool? enabled})
|
||||
: name = other.name,
|
||||
TronToken.copyWith(
|
||||
TronToken other, {
|
||||
String? icon,
|
||||
String? tag,
|
||||
bool? enabled,
|
||||
String? walletName,
|
||||
}) : name = other.name,
|
||||
symbol = other.symbol,
|
||||
contractAddress = other.contractAddress,
|
||||
decimal = other.decimal,
|
||||
@@ -65,6 +52,8 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
tag = tag ?? other.tag,
|
||||
iconPath = icon ?? other.iconPath,
|
||||
isPotentialScam = other.isPotentialScam,
|
||||
id = 0,
|
||||
walletName = walletName ?? other.walletName,
|
||||
super(
|
||||
name: other.name,
|
||||
title: other.symbol.toUpperCase(),
|
||||
@@ -74,9 +63,110 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
decimals: other.decimal,
|
||||
isPotentialScam: other.isPotentialScam,
|
||||
);
|
||||
@override
|
||||
final String name;
|
||||
|
||||
static const typeId = TRON_TOKEN_TYPE_ID;
|
||||
static const boxName = 'TronTokens';
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
final String? iconPath;
|
||||
|
||||
@override
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
final bool isPotentialScam;
|
||||
|
||||
@override
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@override
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
int id;
|
||||
bool _enabled;
|
||||
final int decimal;
|
||||
String? walletName;
|
||||
final String contractAddress;
|
||||
|
||||
static bool _getBoolFromDB(value, {bool? defaultValue}) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else if (value is int) {
|
||||
return value == 1;
|
||||
} else {
|
||||
return defaultValue ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
selfIdColumn: id,
|
||||
"walletName": walletName,
|
||||
"name": name,
|
||||
"symbol": symbol,
|
||||
"contractAddress": contractAddress,
|
||||
"decimal": decimal,
|
||||
"enabled": _enabled ? 1 : 0,
|
||||
"iconPath": iconPath,
|
||||
"tag": tag,
|
||||
"isPotentialScam": isPotentialScam ? 1 : 0,
|
||||
};
|
||||
|
||||
static String get tableName => "TronToken";
|
||||
static String get selfIdColumn => "${tableName}Id";
|
||||
|
||||
Future<int> save() async {
|
||||
if (walletName == null) {
|
||||
throw StateError("TronToken.save() requires walletName to be set");
|
||||
}
|
||||
|
||||
final json = toMap();
|
||||
if (json[selfIdColumn] == 0) {
|
||||
json[selfIdColumn] = null;
|
||||
}
|
||||
id = await db!.insert(tableName, json, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
return id;
|
||||
}
|
||||
|
||||
static Future<List<TronToken>> selectList(
|
||||
String where,
|
||||
List<dynamic> whereArgs, {
|
||||
String? orderBy,
|
||||
}) async {
|
||||
orderBy ??= selfIdColumn;
|
||||
final list = await db!.query(
|
||||
tableName,
|
||||
where: where.isNotEmpty ? where : "1 = 1",
|
||||
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||
orderBy: orderBy,
|
||||
);
|
||||
return List.generate(list.length, (index) => TronToken.fromMap(list[index]));
|
||||
}
|
||||
|
||||
static Future<List<TronToken>> getAllForWallet(String walletName) =>
|
||||
selectList("walletName = ?", [walletName]);
|
||||
|
||||
static Future<TronToken?> getByContract(String walletName, String contractAddress) async {
|
||||
final list =
|
||||
await selectList("walletName = ? AND contractAddress = ?", [walletName, contractAddress]);
|
||||
return list.isEmpty ? null : list.first;
|
||||
}
|
||||
|
||||
static Future<int> deleteForWallet(String walletName, String contractAddress) => db!.delete(
|
||||
tableName,
|
||||
where: "walletName = ? AND contractAddress = ?",
|
||||
whereArgs: [walletName, contractAddress],
|
||||
);
|
||||
|
||||
static Future<int> deleteAllForWallet(String walletName) =>
|
||||
db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
|
||||
|
||||
static Future<void> renameWallet(String oldName, String newName) async {
|
||||
await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
|
||||
await db!
|
||||
.update(tableName, {"walletName": newName}, where: "walletName = ?", whereArgs: [oldName]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
@@ -85,4 +175,7 @@ class TronToken extends CryptoCurrency with HiveObjectMixin {
|
||||
|
||||
@override
|
||||
int get hashCode => contractAddress.hashCode;
|
||||
|
||||
@override
|
||||
String get apiString => "trx.$contractAddress";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import "package:cw_core/cake_hive.dart";
|
||||
import "package:cw_core/db/sqlite.dart";
|
||||
import "package:cw_core/hive_type_ids.dart";
|
||||
import "package:cw_core/tron_token.dart" as tron_new;
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:hive/hive.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
part "tron_token_legacy.part.dart";
|
||||
|
||||
Future<void> performTronTokenHiveMigration() async {
|
||||
try {
|
||||
if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
|
||||
CakeHive.registerAdapter(TronTokenAdapter());
|
||||
}
|
||||
|
||||
final wallets = await WalletInfo.getAll();
|
||||
await TronToken.migrateAllToSqlite(wallets);
|
||||
} catch (e) {
|
||||
printV("Error performing TronToken Hive migration: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
// @HiveType(typeId: TronToken.typeId)
|
||||
class TronToken extends HiveObject {
|
||||
TronToken({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.contractAddress,
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
this.tag = "TRX",
|
||||
this.isPotentialScam = false,
|
||||
}) : _enabled = enabled;
|
||||
// @HiveField(0)
|
||||
final String name;
|
||||
|
||||
// @HiveField(1)
|
||||
final String symbol;
|
||||
|
||||
// @HiveField(2)
|
||||
final String contractAddress;
|
||||
|
||||
// @HiveField(3)
|
||||
final int decimal;
|
||||
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
|
||||
// @HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
// @HiveField(6)
|
||||
final String? tag;
|
||||
|
||||
// @HiveField(7, defaultValue: false)
|
||||
final bool isPotentialScam;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
static const typeId = TRON_TOKEN_TYPE_ID;
|
||||
static const boxName = "TronTokens";
|
||||
|
||||
static Future<void> migrateAllToSqlite(List<WalletInfo> wallets) async {
|
||||
final sanitizedToRawNames = <String, Set<String>>{};
|
||||
for (final wallet in wallets) {
|
||||
// Hash Bags: tron is removed, so no wallet has tokens to migrate.
|
||||
continue;
|
||||
|
||||
sanitizedToRawNames
|
||||
.putIfAbsent(wallet.name.replaceAll(" ", "_"), () => <String>{})
|
||||
.add(wallet.name);
|
||||
}
|
||||
|
||||
for (final entry in sanitizedToRawNames.entries) {
|
||||
final tokenBoxName = "${entry.key}_$boxName";
|
||||
try {
|
||||
if (!await CakeHive.boxExists(tokenBoxName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final box = await CakeHive.openBox<TronToken>(tokenBoxName);
|
||||
|
||||
for (final key in box.keys.toList()) {
|
||||
final token = box.get(key);
|
||||
if (token == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final rawName in entry.value) {
|
||||
await token.migrateToSqlite(walletName: rawName);
|
||||
}
|
||||
await box.delete(key);
|
||||
}
|
||||
|
||||
await box.deleteFromDisk();
|
||||
} catch (e) {
|
||||
printV("Error migrating tron token box $tokenBoxName: $e, continuing anyway");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> migrateToSqlite({required String walletName}) async {
|
||||
final row = tron_new.TronToken(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal,
|
||||
enabled: _enabled,
|
||||
iconPath: iconPath,
|
||||
tag: tag,
|
||||
isPotentialScam: isPotentialScam,
|
||||
walletName: walletName,
|
||||
).toMap();
|
||||
row[tron_new.TronToken.selfIdColumn] = null;
|
||||
|
||||
await db!.insert(
|
||||
tron_new.TronToken.tableName,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of "tron_token_legacy.dart";
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class TronTokenAdapter extends TypeAdapter<TronToken> {
|
||||
@override
|
||||
final int typeId = 18;
|
||||
|
||||
@override
|
||||
TronToken read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return TronToken(
|
||||
name: fields[0] as String,
|
||||
symbol: fields[1] as String,
|
||||
contractAddress: fields[2] as String,
|
||||
decimal: fields[3] as int,
|
||||
iconPath: fields[5] as String?,
|
||||
tag: fields[6] as String?,
|
||||
isPotentialScam: fields[7] == null ? false : fields[7] as bool,
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, TronToken obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.symbol)
|
||||
..writeByte(2)
|
||||
..write(obj.contractAddress)
|
||||
..writeByte(3)
|
||||
..write(obj.decimal)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(6)
|
||||
..write(obj.tag)
|
||||
..writeByte(7)
|
||||
..write(obj.isPotentialScam);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TronTokenAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/unspent_comparable_mixin.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'unspent_coins_info.g.dart';
|
||||
part 'unspent_coins_info.part.dart';
|
||||
|
||||
@HiveType(typeId: UnspentCoinsInfo.typeId)
|
||||
// @HiveType(typeId: UnspentCoinsInfo.typeId)
|
||||
class UnspentCoinsInfo extends HiveObject with UnspentComparable {
|
||||
UnspentCoinsInfo({
|
||||
required this.walletId,
|
||||
@@ -25,40 +25,40 @@ class UnspentCoinsInfo extends HiveObject with UnspentComparable {
|
||||
static const boxName = 'Unspent';
|
||||
static const boxKey = 'unspentBoxKey';
|
||||
|
||||
@HiveField(0, defaultValue: '')
|
||||
// @HiveField(0, defaultValue: '')
|
||||
String walletId;
|
||||
|
||||
@HiveField(1, defaultValue: '')
|
||||
// @HiveField(1, defaultValue: '')
|
||||
String hash;
|
||||
|
||||
@HiveField(2, defaultValue: false)
|
||||
// @HiveField(2, defaultValue: false)
|
||||
bool isFrozen;
|
||||
|
||||
@HiveField(3, defaultValue: false)
|
||||
// @HiveField(3, defaultValue: false)
|
||||
bool isSending;
|
||||
|
||||
@HiveField(4)
|
||||
// @HiveField(4)
|
||||
String? noteRaw;
|
||||
|
||||
@HiveField(5, defaultValue: '')
|
||||
// @HiveField(5, defaultValue: '')
|
||||
String address;
|
||||
|
||||
@HiveField(6, defaultValue: 0)
|
||||
// @HiveField(6, defaultValue: 0)
|
||||
int value;
|
||||
|
||||
@HiveField(7, defaultValue: 0)
|
||||
// @HiveField(7, defaultValue: 0)
|
||||
int vout;
|
||||
|
||||
@HiveField(8, defaultValue: null)
|
||||
// @HiveField(8, defaultValue: null)
|
||||
String? keyImage;
|
||||
|
||||
@HiveField(9, defaultValue: false)
|
||||
// @HiveField(9, defaultValue: false)
|
||||
bool isChange;
|
||||
|
||||
@HiveField(10, defaultValue: 0)
|
||||
// @HiveField(10, defaultValue: 0)
|
||||
int accountIndex;
|
||||
|
||||
@HiveField(11, defaultValue: false)
|
||||
// @HiveField(11, defaultValue: false)
|
||||
bool? isSilentPayment;
|
||||
|
||||
String get note => noteRaw ?? '';
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'unspent_coins_info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class UnspentCoinsInfoAdapter extends TypeAdapter<UnspentCoinsInfo> {
|
||||
@override
|
||||
final int typeId = 9;
|
||||
|
||||
@override
|
||||
UnspentCoinsInfo read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return UnspentCoinsInfo(
|
||||
walletId: fields[0] == null ? '' : fields[0] as String,
|
||||
hash: fields[1] == null ? '' : fields[1] as String,
|
||||
isFrozen: fields[2] == null ? false : fields[2] as bool,
|
||||
isSending: fields[3] == null ? false : fields[3] as bool,
|
||||
noteRaw: fields[4] as String?,
|
||||
address: fields[5] == null ? '' : fields[5] as String,
|
||||
vout: fields[7] == null ? 0 : fields[7] as int,
|
||||
value: fields[6] == null ? 0 : fields[6] as int,
|
||||
keyImage: fields[8] as String?,
|
||||
isChange: fields[9] == null ? false : fields[9] as bool,
|
||||
accountIndex: fields[10] == null ? 0 : fields[10] as int,
|
||||
isSilentPayment: fields[11] == null ? false : fields[11] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, UnspentCoinsInfo obj) {
|
||||
writer
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.walletId)
|
||||
..writeByte(1)
|
||||
..write(obj.hash)
|
||||
..writeByte(2)
|
||||
..write(obj.isFrozen)
|
||||
..writeByte(3)
|
||||
..write(obj.isSending)
|
||||
..writeByte(4)
|
||||
..write(obj.noteRaw)
|
||||
..writeByte(5)
|
||||
..write(obj.address)
|
||||
..writeByte(6)
|
||||
..write(obj.value)
|
||||
..writeByte(7)
|
||||
..write(obj.vout)
|
||||
..writeByte(8)
|
||||
..write(obj.keyImage)
|
||||
..writeByte(9)
|
||||
..write(obj.isChange)
|
||||
..writeByte(10)
|
||||
..write(obj.accountIndex)
|
||||
..writeByte(11)
|
||||
..write(obj.isSilentPayment);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is UnspentCoinsInfoAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
+128
-13
@@ -1,21 +1,84 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cw_core/key.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cake_backup/backup.dart' as cwb;
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
Future<void> write({required String path, required String password, required String data}) async =>
|
||||
writeData(path: path, password: password, data: data);
|
||||
const _ivEncodedStringLength = 12;
|
||||
|
||||
Future<void> writeData(
|
||||
{required String path, required String password, required String data}) async {
|
||||
final keys = extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypted = await encode(key: key, iv: iv, data: data);
|
||||
final f = File(path);
|
||||
f.writeAsStringSync(encrypted);
|
||||
Future<void> write({
|
||||
required String path,
|
||||
required String password,
|
||||
required String data,
|
||||
bool? highEntropyPassphrase,
|
||||
}) async {
|
||||
await _writeXChaCha20(
|
||||
path: path,
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: highEntropyPassphrase ?? !Platform.isLinux,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> read({required String path, required String password}) async {
|
||||
Future<String> read({
|
||||
required String path,
|
||||
required String password,
|
||||
bool? highEntropyPassphrase,
|
||||
}) async {
|
||||
final useHighEntropy = highEntropyPassphrase ?? !Platform.isLinux;
|
||||
try {
|
||||
return await _readXChaCha20(path: path, password: password);
|
||||
} catch (e) {
|
||||
final encrypted = await File(path).readAsBytes();
|
||||
if (encrypted.isNotEmpty &&
|
||||
(encrypted[0] == cwb.lowEntropyVersion || encrypted[0] == cwb.highEntropyVersion)) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final String data;
|
||||
try {
|
||||
data = await _readLegacy(path: path, password: password);
|
||||
} catch (_) {
|
||||
throw Exception('Failed to decrypt legacy file: invalid password or corrupted data');
|
||||
}
|
||||
if (data.isEmpty) {
|
||||
throw Exception('Failed to read data');
|
||||
}
|
||||
|
||||
// Salsa20 is unauthenticated, so a wrong password decrypts to garbage instead of
|
||||
// failing. Every payload written here is a JSON object with at least one key, so a
|
||||
// correctly decrypted file always starts with '{"' and anything else is a bad
|
||||
// password.
|
||||
// There's 1 in 2^16 chance that this will be a false positive, but check later
|
||||
// prevents any damage to the file caused by re-encryption.
|
||||
if (!data.startsWith('{"')) {
|
||||
throw Exception('Failed to decrypt legacy file: invalid password or corrupted data');
|
||||
}
|
||||
|
||||
if (_isJson(data)) {
|
||||
await _writeXChaCha20(
|
||||
path: path,
|
||||
password: password,
|
||||
data: data,
|
||||
highEntropyPassphrase: useHighEntropy,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isJson(String data) {
|
||||
try {
|
||||
json.decode(data);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _readLegacy({required String path, required String password}) async {
|
||||
final file = File(path);
|
||||
|
||||
if (!file.existsSync()) {
|
||||
@@ -24,5 +87,57 @@ Future<String> read({required String path, required String password}) async {
|
||||
|
||||
final encrypted = file.readAsStringSync();
|
||||
|
||||
return decode(password: password, data: encrypted);
|
||||
return _decode(password: password, data: encrypted);
|
||||
}
|
||||
|
||||
Future<void> _writeXChaCha20({
|
||||
required String path,
|
||||
required String password,
|
||||
required String data,
|
||||
required bool highEntropyPassphrase,
|
||||
}) async {
|
||||
final encrypted = await cwb.encrypt(
|
||||
password,
|
||||
Uint8List.fromList(utf8.encode(data)),
|
||||
highEntropyPassphrase: highEntropyPassphrase,
|
||||
);
|
||||
|
||||
final tmpFile = File('$path.tmp');
|
||||
tmpFile.writeAsBytesSync(encrypted, flush: true);
|
||||
tmpFile.renameSync(path);
|
||||
}
|
||||
|
||||
Future<String> _readXChaCha20({
|
||||
required String path,
|
||||
required String password,
|
||||
}) async {
|
||||
final encrypted = await File(path).readAsBytes();
|
||||
final bytes = await cwb.decrypt(password, encrypted);
|
||||
return _decodeUtf8(bytes);
|
||||
}
|
||||
|
||||
// Linux wallets written by the old XChaCha20EncryptionFileUtils stored one byte per
|
||||
// UTF-16 code unit, so bytes above 0x7F in those files are Latin-1 rather than UTF-8.
|
||||
String _decodeUtf8(Uint8List bytes) {
|
||||
try {
|
||||
return utf8.decode(bytes);
|
||||
} on FormatException {
|
||||
return String.fromCharCodes(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _extractKeys(String key) {
|
||||
final k = key.substring(0, key.length - _ivEncodedStringLength);
|
||||
final iv = key.substring(key.length - _ivEncodedStringLength);
|
||||
|
||||
return [k, iv];
|
||||
}
|
||||
|
||||
Future<String> _decode({required String password, required String data}) async {
|
||||
final keys = _extractKeys(password);
|
||||
final key = encrypt.Key.fromBase64(keys.first);
|
||||
final iv = encrypt.IV.fromBase64(keys.last);
|
||||
final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
|
||||
|
||||
return encrypter.decrypt64(data, iv: iv);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const _ipfsGateway = "https://ipfs.io/ipfs/";
|
||||
const _ipfsScheme = "ipfs://";
|
||||
|
||||
String? tryNormalizeIpfsUrl(String? url) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url.toLowerCase().startsWith(_ipfsScheme)) {
|
||||
return url.toLowerCase().startsWith("https://") ? url : null;
|
||||
}
|
||||
|
||||
var cid = url.substring(_ipfsScheme.length);
|
||||
|
||||
// Some minters write ipfs://ipfs/<cid>, which would double the gateway path.
|
||||
if (cid.toLowerCase().startsWith("ipfs/")) {
|
||||
cid = cid.substring(5);
|
||||
}
|
||||
|
||||
if (cid.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "$_ipfsGateway$cid";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Control characters and bidi overrides let a mint's own metadata fake extra
|
||||
// lines or reverse the text around it wherever the name is shown.
|
||||
final _unsafeDisplayCharacters = RegExp(
|
||||
"[\u0000-\u001F\u007F\u00AD\u034F\u061C\u115F\u1160\u180E\u200B\u200E\u200F"
|
||||
"\u2028\u2029\u202A-\u202E\u2060-\u2064\u2066-\u2069\u3164\uFEFF\uFFA0]"
|
||||
"|[\u{E0000}-\u{E007F}]",
|
||||
unicode: true,
|
||||
);
|
||||
|
||||
const _maxDisplayTextLength = 512;
|
||||
|
||||
String? sanitizeNFTText(String? text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final cleaned = text.replaceAll(_unsafeDisplayCharacters, " ").trim();
|
||||
final runes = cleaned.runes;
|
||||
|
||||
if (runes.length <= _maxDisplayTextLength) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
return "${String.fromCharCodes(runes.take(_maxDisplayTextLength))}\u2026";
|
||||
}
|
||||
@@ -71,11 +71,12 @@ abstract class WalletAddresses {
|
||||
Future<void> saveAddressesInBox() async {
|
||||
try {
|
||||
walletInfo.address = address;
|
||||
walletInfo.setAddresses(addressesMap);
|
||||
walletInfo.setAddressInfos(addressInfos);
|
||||
walletInfo.setUsedAddresses(usedAddresses.toList());
|
||||
walletInfo.setHiddenAddresses(hiddenAddresses.toList());
|
||||
walletInfo.setManualAddresses(manualAddresses.toList());
|
||||
// TODO: check if it will affect the performance of each wallet
|
||||
await walletInfo.setAddresses(addressesMap);
|
||||
await walletInfo.setAddressInfos(addressInfos);
|
||||
await walletInfo.setUsedAddresses(usedAddresses.toList());
|
||||
await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
|
||||
await walletInfo.setManualAddresses(manualAddresses.toList());
|
||||
|
||||
await walletInfo.save();
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "package:cw_core/receive_page_option.dart";
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
@@ -12,6 +13,7 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
|
||||
abstract class WalletBase<BalanceType extends Balance, HistoryType extends TransactionHistoryBase,
|
||||
TransactionType extends TransactionInfo> {
|
||||
@@ -43,8 +45,6 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
|
||||
ObservableMap<CryptoCurrency, BalanceType> get balance;
|
||||
|
||||
String formatCryptoAmount(String amount) => amount;
|
||||
|
||||
SyncStatus get syncStatus;
|
||||
|
||||
set syncStatus(SyncStatus status);
|
||||
@@ -117,7 +117,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
|
||||
|
||||
Future<void> renameWalletFiles(String newWalletName);
|
||||
Future<void> renameWalletFiles(String newWalletName) =>
|
||||
copyWalletFilesTo(fromName: walletInfo.name, toName: newWalletName, type: type);
|
||||
|
||||
Future<String> signMessage(String message, {String? address = null});
|
||||
|
||||
@@ -136,4 +137,19 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
|
||||
/// Each wallet implementation should override this to make a single, efficient call
|
||||
/// Returns true if the node is healthy, false otherwise
|
||||
Future<bool> checkNodeHealth();
|
||||
|
||||
bool get hasPayjoinSupport => false;
|
||||
bool get hasLightningSupport => false;
|
||||
bool get hasSilentPaymentsScanning => false;
|
||||
|
||||
bool get hasCoinControl => false;
|
||||
bool get hasAccountsSupport => false;
|
||||
bool get hasNativeAccounts => false;
|
||||
bool get canToggleMultiAccounts => hasAccountsSupport && !hasNativeAccounts;
|
||||
|
||||
// hardware wallet - bitbox, ledger, trezor.
|
||||
// we also have airgap wallets but those can't sign messages
|
||||
bool get canSignMessages => walletInfo.hardwareWalletType == null || walletInfo.isHardwareWallet;
|
||||
|
||||
bool receiveOptionAvailable(ReceivePageOption option) => true;
|
||||
}
|
||||
|
||||
+235
-57
@@ -44,7 +44,7 @@ enum HardwareWalletType {
|
||||
coldcard,
|
||||
seedsigner,
|
||||
keystone,
|
||||
trezor,
|
||||
trezor;
|
||||
}
|
||||
|
||||
enum WalletInfoAddressType {
|
||||
@@ -244,6 +244,68 @@ class WalletInfoAddress {
|
||||
}
|
||||
}
|
||||
|
||||
class WalletInfoAccount {
|
||||
WalletInfoAccount({
|
||||
required this.walletInfoId,
|
||||
required this.accountIndex,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
factory WalletInfoAccount.fromJson(Map<String, dynamic> json) => WalletInfoAccount(
|
||||
walletInfoId: json["walletInfoId"] as int,
|
||||
accountIndex: json["accountIndex"] as int,
|
||||
label: json["label"] as String,
|
||||
);
|
||||
|
||||
int walletInfoId;
|
||||
int accountIndex;
|
||||
String label;
|
||||
|
||||
static String get tableName => "walletInfoAccount";
|
||||
|
||||
static Future<List<WalletInfoAccount>> selectList(int walletInfoId) async {
|
||||
final query = await db!.query(
|
||||
tableName,
|
||||
where: "walletInfoId = ?",
|
||||
whereArgs: [walletInfoId],
|
||||
orderBy: "accountIndex ASC",
|
||||
);
|
||||
|
||||
return List.generate(query.length, (index) => WalletInfoAccount.fromJson(query[index]));
|
||||
}
|
||||
|
||||
static Future<int> deleteByWalletInfoId(int walletInfoId) async =>
|
||||
await db!.delete(tableName, where: "walletInfoId = ?", whereArgs: [walletInfoId]);
|
||||
|
||||
static Future<int> insertOrUpdate({
|
||||
required int walletInfoId,
|
||||
required int accountIndex,
|
||||
required String label,
|
||||
}) async =>
|
||||
await db!.transaction((txn) async {
|
||||
final updated = await txn.update(
|
||||
tableName,
|
||||
{"label": label},
|
||||
where: "walletInfoId = ? AND accountIndex = ?",
|
||||
whereArgs: [walletInfoId, accountIndex],
|
||||
);
|
||||
|
||||
if (updated > 0) return updated;
|
||||
|
||||
return await txn.insert(tableName, {
|
||||
"walletInfoId": walletInfoId,
|
||||
"accountIndex": accountIndex,
|
||||
"label": label,
|
||||
});
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"walletInfoId": walletInfoId,
|
||||
"accountIndex": accountIndex,
|
||||
"label": label,
|
||||
};
|
||||
}
|
||||
|
||||
class DerivationInfo {
|
||||
DerivationInfo({
|
||||
this.id = 0,
|
||||
@@ -346,58 +408,73 @@ class WalletInfo {
|
||||
this.hashedWalletIdentifier,
|
||||
this.isNonSeedWallet,
|
||||
this.sortOrder,
|
||||
this.currentAccountIndex,
|
||||
this.addressPageType,
|
||||
this.receiveInfoboxDismissed,
|
||||
this.showCombinedBalance,
|
||||
this.favoriteTokenAddress)
|
||||
this.favoriteTokenAddress,
|
||||
this.showSeedBackupReminder)
|
||||
: _yatLastUsedAddressController = StreamController<String>.broadcast();
|
||||
|
||||
factory WalletInfo.external(
|
||||
{required String id,
|
||||
required String name,
|
||||
required WalletType type,
|
||||
required bool isRecovery,
|
||||
required int restoreHeight,
|
||||
required DateTime date,
|
||||
required String dirPath,
|
||||
required String path,
|
||||
required String address,
|
||||
bool? showIntroCakePayCard,
|
||||
String yatEid = '',
|
||||
String yatLastUsedAddressRaw = '',
|
||||
int? derivationInfoId,
|
||||
HardwareWalletType? hardwareWalletType,
|
||||
String? parentAddress,
|
||||
String? hashedWalletIdentifier,
|
||||
bool? isNonSeedWallet,
|
||||
int? sortOrder,
|
||||
bool? receiveInfoboxDismissed,
|
||||
bool? showCombinedBalance,
|
||||
String? favoriteTokenAddress}) {
|
||||
return WalletInfo(
|
||||
0,
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
isRecovery,
|
||||
restoreHeight,
|
||||
date.millisecondsSinceEpoch,
|
||||
dirPath,
|
||||
path,
|
||||
address,
|
||||
yatEid,
|
||||
yatLastUsedAddressRaw,
|
||||
showIntroCakePayCard,
|
||||
derivationInfoId ?? -1,
|
||||
hardwareWalletType,
|
||||
parentAddress,
|
||||
hashedWalletIdentifier,
|
||||
isNonSeedWallet ?? false,
|
||||
sortOrder ?? 0,
|
||||
null,
|
||||
receiveInfoboxDismissed ?? false,
|
||||
showCombinedBalance ?? true,
|
||||
favoriteTokenAddress);
|
||||
factory WalletInfo.external({
|
||||
required String id,
|
||||
required String name,
|
||||
required WalletType type,
|
||||
required bool isRecovery,
|
||||
required int restoreHeight,
|
||||
required DateTime date,
|
||||
required String dirPath,
|
||||
required String path,
|
||||
required String address,
|
||||
bool? showIntroCakePayCard,
|
||||
String yatEid = "",
|
||||
String yatLastUsedAddressRaw = "",
|
||||
int? derivationInfoId,
|
||||
HardwareWalletType? hardwareWalletType,
|
||||
String? parentAddress,
|
||||
String? hashedWalletIdentifier,
|
||||
bool? isNonSeedWallet,
|
||||
int? sortOrder,
|
||||
int currentAccountIndex = 0,
|
||||
bool? receiveInfoboxDismissed,
|
||||
bool? showCombinedBalance,
|
||||
String? favoriteTokenAddress,
|
||||
}) {
|
||||
final wi = WalletInfo(
|
||||
0,
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
isRecovery,
|
||||
restoreHeight,
|
||||
date.millisecondsSinceEpoch,
|
||||
dirPath,
|
||||
path,
|
||||
address,
|
||||
yatEid.isNotEmpty ? yatEid : null,
|
||||
yatLastUsedAddressRaw.isNotEmpty ? yatLastUsedAddressRaw : null,
|
||||
showIntroCakePayCard ?? false,
|
||||
derivationInfoId ?? -1,
|
||||
hardwareWalletType,
|
||||
parentAddress,
|
||||
hashedWalletIdentifier,
|
||||
isNonSeedWallet ?? false,
|
||||
sortOrder ?? 0,
|
||||
currentAccountIndex,
|
||||
null,
|
||||
receiveInfoboxDismissed ?? false,
|
||||
showCombinedBalance ?? true,
|
||||
favoriteTokenAddress?.isNotEmpty == true ? favoriteTokenAddress : null,
|
||||
false,
|
||||
);
|
||||
|
||||
if (type == WalletType.bitcoin) {
|
||||
wi.isMultiAccountsEnabled = false;
|
||||
} else if (type == WalletType.monero || type == WalletType.wownero) {
|
||||
wi.isMultiAccountsEnabled = true;
|
||||
}
|
||||
|
||||
return wi;
|
||||
}
|
||||
|
||||
static String get tableName => 'walletInfo';
|
||||
@@ -406,6 +483,8 @@ class WalletInfo {
|
||||
|
||||
int internalId;
|
||||
|
||||
int currentAccountIndex;
|
||||
|
||||
String id;
|
||||
String name;
|
||||
WalletType type;
|
||||
@@ -418,6 +497,7 @@ class WalletInfo {
|
||||
bool receiveInfoboxDismissed;
|
||||
bool showCombinedBalance;
|
||||
String? favoriteTokenAddress;
|
||||
bool showSeedBackupReminder;
|
||||
|
||||
Future<Map<String, String>> getAddresses() async {
|
||||
final list = await WalletInfoAddressMap.selectList(internalId);
|
||||
@@ -426,11 +506,9 @@ class WalletInfo {
|
||||
|
||||
Future<void> setAddresses(Map<String, String> addresses) async {
|
||||
await WalletInfoAddressMap.deleteByWalletInfoId(internalId);
|
||||
final keys = addresses.keys.toList();
|
||||
final values = addresses.values.toList();
|
||||
// ToDo: check why the addresses list gets changed half way through
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
await WalletInfoAddressMap.insert(internalId, keys[i], values[i]);
|
||||
final entries = addresses.entries.toList();
|
||||
for (final entry in entries) {
|
||||
await WalletInfoAddressMap.insert(internalId, entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,8 +583,76 @@ class WalletInfo {
|
||||
await WalletInfoAddress.insert(internalId, type, address);
|
||||
}
|
||||
|
||||
Future<List<WalletInfoAccount>> getAccounts() async {
|
||||
var accounts = await WalletInfoAccount.selectList(internalId);
|
||||
|
||||
if (accounts.isEmpty) {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: 0,
|
||||
label: "Primary account",
|
||||
);
|
||||
|
||||
accounts = await WalletInfoAccount.selectList(internalId);
|
||||
}
|
||||
|
||||
if (!accounts.any((a) => a.accountIndex == currentAccountIndex)) {
|
||||
currentAccountIndex = accounts.first.accountIndex;
|
||||
await save();
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
Future<void> setSelectedAccount(int accountIndex) async {
|
||||
currentAccountIndex = accountIndex;
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> setAccounts(List<WalletInfoAccount> accounts) async {
|
||||
await WalletInfoAccount.deleteByWalletInfoId(internalId);
|
||||
|
||||
for (final account in accounts) {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: account.accountIndex,
|
||||
label: account.label,
|
||||
);
|
||||
}
|
||||
|
||||
if (accounts.isNotEmpty && !accounts.any((a) => a.accountIndex == currentAccountIndex)) {
|
||||
currentAccountIndex = accounts.first.accountIndex;
|
||||
await save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addAccount({
|
||||
required int accountIndex,
|
||||
required String label,
|
||||
}) async {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: accountIndex,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> renameAccount({
|
||||
required int accountIndex,
|
||||
required String label,
|
||||
}) async {
|
||||
await WalletInfoAccount.insertOrUpdate(
|
||||
walletInfoId: internalId,
|
||||
accountIndex: accountIndex,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
String? addressPageType;
|
||||
String? network;
|
||||
int? accountDiscoveryLimit;
|
||||
bool? isMultiAccountsEnabled;
|
||||
|
||||
int derivationInfoId;
|
||||
DerivationInfo? _derivationInfo;
|
||||
|
||||
@@ -582,14 +728,20 @@ class WalletInfo {
|
||||
"hashedWalletIdentifier": hashedWalletIdentifier,
|
||||
"isNonSeedWallet": isNonSeedWallet ? 1 : 0,
|
||||
"sortOrder": sortOrder,
|
||||
"currentAccountIndex": currentAccountIndex,
|
||||
"addressPageType": addressPageType,
|
||||
"receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
|
||||
"showCombinedBalance": showCombinedBalance ? 1 : 0,
|
||||
"favoriteTokenAddress": favoriteTokenAddress
|
||||
"favoriteTokenAddress": favoriteTokenAddress,
|
||||
"showSeedBackupReminder": showSeedBackupReminder ? 1 : 0,
|
||||
"network": network,
|
||||
"accountDiscoveryLimit": accountDiscoveryLimit,
|
||||
"isMultiAccountsEnabled":
|
||||
isMultiAccountsEnabled == null ? null : (isMultiAccountsEnabled! ? 1 : 0),
|
||||
};
|
||||
|
||||
factory WalletInfo.fromJson(Map<String, dynamic> json) {
|
||||
return WalletInfo(
|
||||
final info = WalletInfo(
|
||||
json[selfIdColumn] as int,
|
||||
json['id'] as String,
|
||||
json['name'] as String,
|
||||
@@ -611,10 +763,18 @@ class WalletInfo {
|
||||
json['hashedWalletIdentifier'] as String?,
|
||||
(json['isNonSeedWallet'] as int) == 1,
|
||||
json['sortOrder'] as int? ?? 0,
|
||||
json['currentAccountIndex'] as int? ?? 0,
|
||||
json['addressPageType'] as String? ?? null,
|
||||
json['receiveInfoboxDismissed'] != 0,
|
||||
json["showCombinedBalance"] != 0,
|
||||
json["favoriteTokenAddress"] as String? ?? null);
|
||||
json["favoriteTokenAddress"] as String? ?? null,
|
||||
json["showSeedBackupReminder"] == 1);
|
||||
info.network = json['network'] as String?;
|
||||
info.accountDiscoveryLimit = json['accountDiscoveryLimit'] as int?;
|
||||
final rawIsMultiAccountsEnabled = json['isMultiAccountsEnabled'];
|
||||
info.isMultiAccountsEnabled =
|
||||
rawIsMultiAccountsEnabled == null ? null : (rawIsMultiAccountsEnabled as int) == 1;
|
||||
return info;
|
||||
}
|
||||
|
||||
Future<int> save() async {
|
||||
@@ -630,7 +790,8 @@ class WalletInfo {
|
||||
}
|
||||
|
||||
static Future<int> delete(WalletInfo walletInfo) async {
|
||||
return await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
|
||||
final deleted = await db!.delete(tableName, where: 'id = ?', whereArgs: [walletInfo.id]);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
static Future<List<WalletInfo>> selectList(String where, List<dynamic> whereArgs,
|
||||
@@ -660,4 +821,21 @@ class WalletInfo {
|
||||
restoreHeight = height;
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> setAccountDiscoveryLimit(int limit) async {
|
||||
accountDiscoveryLimit = limit;
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> clearSeedBackupReminder() async {
|
||||
final previous = showSeedBackupReminder;
|
||||
showSeedBackupReminder = false;
|
||||
|
||||
try {
|
||||
await save();
|
||||
} catch (e) {
|
||||
showSeedBackupReminder = previous;
|
||||
printV("Failed to save the seed backup reminder flag: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,10 +268,13 @@ class WalletInfo extends HiveObject {
|
||||
hashedWalletIdentifier,
|
||||
isNonSeedWallet,
|
||||
0,
|
||||
0,
|
||||
addressPageType,
|
||||
false,
|
||||
true,
|
||||
null);
|
||||
null,
|
||||
false)
|
||||
..network = network;
|
||||
final wiId = await walletInfo.save();
|
||||
for (final address in usedAddresses ?? <String>[]) {
|
||||
await newWi.WalletInfoAddress.insert(wiId, newWi.WalletInfoAddressType.used, address);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cw_core/encryption_file_utils.dart';
|
||||
import 'package:cw_core/imported_nft.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/utils/file.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_keys_file.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
|
||||
RFK extends WalletCredentials, RFH extends WalletCredentials> {
|
||||
@@ -25,7 +30,38 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
||||
|
||||
Future<void> remove(String wallet);
|
||||
|
||||
Future<void> rename(String currentName, String password, String newName);
|
||||
Future<void> rename(String currentName, String password, String newName) async {
|
||||
if (currentName == newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentWalletInfo = await WalletInfo.get(currentName, getType());
|
||||
if (currentWalletInfo == null) {
|
||||
throw Exception("Wallet not found");
|
||||
}
|
||||
|
||||
await copyWalletFilesTo(fromName: currentName, toName: newName, type: getType());
|
||||
await saveBackup(newName);
|
||||
|
||||
currentWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||
currentWalletInfo.name = newName;
|
||||
await currentWalletInfo.save();
|
||||
|
||||
await _renameTokenRows(currentName, newName);
|
||||
|
||||
final oldDir = Directory(p.join(await pathForWalletTypeDir(type: getType()), currentName));
|
||||
if (oldDir.existsSync()) {
|
||||
try {
|
||||
await oldDir.delete(recursive: true);
|
||||
} catch (e) {
|
||||
printV('rename: failed to delete old wallet dir "$currentName": $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _renameTokenRows(String currentName, String newName) async {
|
||||
|
||||
}
|
||||
|
||||
Future<void> restoreWalletFilesFromBackup(String name) async {
|
||||
final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: getType());
|
||||
@@ -47,18 +83,25 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
|
||||
|
||||
Future<String> getSeeds(String name, String password, WalletType type) async {
|
||||
try {
|
||||
final encryption = encryptionFileUtilsFor(Platform.isLinux);
|
||||
|
||||
if (await WalletKeysFile.hasKeysFile(name, type)) {
|
||||
final keysData = await WalletKeysFile.readKeysFile(name, type, password, encryption);
|
||||
return keysData.mnemonic ?? keysData.altMnemonic ?? keysData.privateKey ?? '';
|
||||
}
|
||||
|
||||
final path = await pathForWallet(name: name, type: type);
|
||||
final jsonSource = await read(path: path, password: password);
|
||||
final jsonSource = await encryption.read(path: path, password: password);
|
||||
try {
|
||||
final data = json.decode(jsonSource) as Map;
|
||||
return data['mnemonic'] as String? ?? '';
|
||||
return data["mnemonic"] as String? ?? "";
|
||||
} catch (_) {
|
||||
// if not a valid json
|
||||
return jsonSource.substring(0, 200);
|
||||
}
|
||||
} catch (_) {
|
||||
// if the file couldn't be opened or read
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'wallet_type.g.dart';
|
||||
part 'wallet_type.part.dart';
|
||||
|
||||
const walletTypes = [
|
||||
WalletType.monero,
|
||||
@@ -35,52 +36,56 @@ const evmWalletTypes = [
|
||||
WalletType.bsc
|
||||
];
|
||||
|
||||
// HiveField IDs are pinned — never renumber. Existing user wallets persist
|
||||
// type via the int ID, so changing assignments here would re-interpret saved
|
||||
// wallet types on next launch. To remove a type, drop its enum entry; do not
|
||||
// re-use its old ID.
|
||||
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
|
||||
// Two encodings are persisted; never change either.
|
||||
// - The HiveField IDs are the bytes WalletTypeAdapter (wallet_type.part.dart) reads when legacy
|
||||
// Hive data is migrated. To remove a type, drop it; never reuse its ID.
|
||||
// - WalletType.index is what the SQLite wallet table stores (wallet_info.dart), so the enum
|
||||
// ORDER is persisted as well: only ever append. Our order differs from upstream's, so a merge
|
||||
// must never take upstream's enum.
|
||||
// hive_generator no longer runs (upstream froze the adapters into *.part.dart); the annotations
|
||||
// below are kept as the record of the IDs.
|
||||
// @HiveType(typeId: WALLET_TYPE_TYPE_ID)
|
||||
enum WalletType {
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
monero,
|
||||
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
none,
|
||||
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
bitcoin,
|
||||
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
litecoin,
|
||||
|
||||
@HiveField(5)
|
||||
// @HiveField(5)
|
||||
ethereum,
|
||||
|
||||
@HiveField(6)
|
||||
// @HiveField(6)
|
||||
nano,
|
||||
|
||||
@HiveField(7)
|
||||
// @HiveField(7)
|
||||
banano,
|
||||
|
||||
@HiveField(8)
|
||||
// @HiveField(8)
|
||||
bitcoinCash,
|
||||
|
||||
@HiveField(9)
|
||||
// @HiveField(9)
|
||||
polygon,
|
||||
|
||||
@HiveField(12)
|
||||
// @HiveField(12)
|
||||
wownero,
|
||||
|
||||
@HiveField(15)
|
||||
// @HiveField(15)
|
||||
dogecoin,
|
||||
|
||||
@HiveField(16)
|
||||
// @HiveField(16)
|
||||
base,
|
||||
|
||||
@HiveField(17)
|
||||
// @HiveField(17)
|
||||
arbitrum,
|
||||
|
||||
@HiveField(19)
|
||||
// @HiveField(19)
|
||||
bsc,
|
||||
}
|
||||
|
||||
@@ -183,38 +188,39 @@ String walletTypeToString(WalletType type) {
|
||||
}
|
||||
}
|
||||
|
||||
String walletTypeToDisplayName(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return 'Monero (XMR)';
|
||||
case WalletType.bitcoin:
|
||||
return 'Bitcoin (BTC)';
|
||||
case WalletType.litecoin:
|
||||
return 'Litecoin (LTC)';
|
||||
case WalletType.ethereum:
|
||||
return 'Ethereum (ETH)';
|
||||
case WalletType.bitcoinCash:
|
||||
return 'Bitcoin Cash (BCH)';
|
||||
case WalletType.nano:
|
||||
return 'Nano (XNO)';
|
||||
case WalletType.banano:
|
||||
return 'Banano (BAN)';
|
||||
case WalletType.polygon:
|
||||
return 'Polygon (POL)';
|
||||
case WalletType.wownero:
|
||||
return 'Wownero (WOW)';
|
||||
case WalletType.dogecoin:
|
||||
return 'Dogecoin (DOGE)';
|
||||
case WalletType.base:
|
||||
return 'Base';
|
||||
case WalletType.arbitrum:
|
||||
return 'Arbitrum (ARB)';
|
||||
case WalletType.bsc:
|
||||
return 'BNB Smart Chain (BNB)';
|
||||
case WalletType.none:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
String walletTypeToDisplayName(WalletType type) => switch (type) {
|
||||
WalletType.monero => 'Monero',
|
||||
WalletType.bitcoin => 'Bitcoin',
|
||||
WalletType.litecoin => 'Litecoin',
|
||||
WalletType.ethereum => 'Ethereum',
|
||||
WalletType.bitcoinCash => 'Bitcoin Cash',
|
||||
WalletType.nano => 'Nano',
|
||||
WalletType.banano => 'Banano',
|
||||
WalletType.polygon => 'Polygon',
|
||||
WalletType.wownero => 'Wownero',
|
||||
WalletType.dogecoin => 'Dogecoin',
|
||||
WalletType.base => 'Base',
|
||||
WalletType.arbitrum => 'Arbitrum',
|
||||
WalletType.bsc => 'BNB Smart Chain',
|
||||
WalletType.none => ''
|
||||
};
|
||||
|
||||
String walletTypeToDisplayTicker(WalletType type) => switch (type) {
|
||||
WalletType.monero => 'XMR',
|
||||
WalletType.bitcoin => 'BTC',
|
||||
WalletType.litecoin => 'LTC',
|
||||
WalletType.ethereum => 'ETH',
|
||||
WalletType.bitcoinCash => 'BCH',
|
||||
WalletType.nano => 'XNO',
|
||||
WalletType.banano => 'BAN',
|
||||
WalletType.polygon => 'POL',
|
||||
WalletType.wownero => 'WOW',
|
||||
WalletType.dogecoin => 'DOGE',
|
||||
WalletType.base => '',
|
||||
WalletType.arbitrum => 'ARB',
|
||||
WalletType.bsc => 'BNB',
|
||||
WalletType.none => ''
|
||||
};
|
||||
|
||||
WalletType? _cryptoCurrencyToWalletType(CryptoCurrency type) {
|
||||
switch (type) {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'wallet_type.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class WalletTypeAdapter extends TypeAdapter<WalletType> {
|
||||
@override
|
||||
final int typeId = 5;
|
||||
|
||||
@override
|
||||
WalletType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return WalletType.monero;
|
||||
case 1:
|
||||
return WalletType.none;
|
||||
case 2:
|
||||
return WalletType.bitcoin;
|
||||
case 3:
|
||||
return WalletType.litecoin;
|
||||
case 5:
|
||||
return WalletType.ethereum;
|
||||
case 6:
|
||||
return WalletType.nano;
|
||||
case 7:
|
||||
return WalletType.banano;
|
||||
case 8:
|
||||
return WalletType.bitcoinCash;
|
||||
case 9:
|
||||
return WalletType.polygon;
|
||||
case 12:
|
||||
return WalletType.wownero;
|
||||
case 15:
|
||||
return WalletType.dogecoin;
|
||||
case 16:
|
||||
return WalletType.base;
|
||||
case 17:
|
||||
return WalletType.arbitrum;
|
||||
case 19:
|
||||
return WalletType.bsc;
|
||||
default:
|
||||
return WalletType.monero;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, WalletType obj) {
|
||||
switch (obj) {
|
||||
case WalletType.monero:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case WalletType.none:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
case WalletType.litecoin:
|
||||
writer.writeByte(3);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
writer.writeByte(5);
|
||||
break;
|
||||
case WalletType.nano:
|
||||
writer.writeByte(6);
|
||||
break;
|
||||
case WalletType.banano:
|
||||
writer.writeByte(7);
|
||||
break;
|
||||
case WalletType.bitcoinCash:
|
||||
writer.writeByte(8);
|
||||
break;
|
||||
case WalletType.polygon:
|
||||
writer.writeByte(9);
|
||||
break;
|
||||
case WalletType.wownero:
|
||||
writer.writeByte(12);
|
||||
break;
|
||||
case WalletType.dogecoin:
|
||||
writer.writeByte(15);
|
||||
break;
|
||||
case WalletType.base:
|
||||
writer.writeByte(16);
|
||||
break;
|
||||
case WalletType.arbitrum:
|
||||
writer.writeByte(17);
|
||||
break;
|
||||
case WalletType.bsc:
|
||||
writer.writeByte(19);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is WalletTypeAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/wownero_amount_format.dart';
|
||||
|
||||
class WowneroBalance extends Balance {
|
||||
WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0})
|
||||
: formattedFrozenBalance = wowneroAmountToString(amount: frozenBalance),
|
||||
super.fromInt(unlockedBalance, fullBalance - unlockedBalance, frozen: frozenBalance);
|
||||
|
||||
final int fullBalance;
|
||||
final int unlockedBalance;
|
||||
final int frozenBalance;
|
||||
final String formattedFrozenBalance;
|
||||
|
||||
@override
|
||||
String get formattedUnAvailableBalance =>
|
||||
formattedFrozenBalance == '0.0' ? '' : formattedFrozenBalance;
|
||||
}
|
||||
+15
-15
@@ -4,38 +4,38 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/hive_type_ids.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'zano_asset.g.dart';
|
||||
part 'zano_asset.part.dart';
|
||||
|
||||
@HiveType(typeId: ZanoAsset.typeId)
|
||||
// @HiveType(typeId: ZanoAsset.typeId)
|
||||
class ZanoAsset extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
// @HiveField(0)
|
||||
final String fullName;
|
||||
@HiveField(1)
|
||||
// @HiveField(1)
|
||||
final String ticker;
|
||||
@HiveField(2)
|
||||
// @HiveField(2)
|
||||
final String assetId;
|
||||
@HiveField(3)
|
||||
// @HiveField(3)
|
||||
final int decimalPoint;
|
||||
@HiveField(4, defaultValue: true)
|
||||
// @HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
@HiveField(5)
|
||||
// @HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
// @HiveField(6)
|
||||
// final String? tag;
|
||||
@HiveField(6)
|
||||
// @HiveField(6)
|
||||
final String owner;
|
||||
@HiveField(7)
|
||||
// @HiveField(7)
|
||||
final String metaInfo;
|
||||
@HiveField(8)
|
||||
// @HiveField(8)
|
||||
final BigInt currentSupply;
|
||||
@HiveField(9)
|
||||
// @HiveField(9)
|
||||
final bool hiddenSupply;
|
||||
@HiveField(10)
|
||||
// @HiveField(10)
|
||||
final BigInt totalMaxSupply;
|
||||
@HiveField(11)
|
||||
// @HiveField(11)
|
||||
final bool isInGlobalWhitelist;
|
||||
@HiveField(12, defaultValue: null)
|
||||
// @HiveField(12, defaultValue: null)
|
||||
final Map<String, dynamic>? info;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'zano_asset.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ZanoAssetAdapter extends TypeAdapter<ZanoAsset> {
|
||||
@override
|
||||
final int typeId = 22;
|
||||
|
||||
@override
|
||||
ZanoAsset read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ZanoAsset(
|
||||
fullName: fields[0] as String,
|
||||
ticker: fields[1] as String,
|
||||
assetId: fields[2] as String,
|
||||
decimalPoint: fields[3] as int,
|
||||
iconPath: fields[5] as String?,
|
||||
owner: fields[6] as String,
|
||||
metaInfo: fields[7] as String,
|
||||
currentSupply: fields[8] as BigInt,
|
||||
hiddenSupply: fields[9] as bool,
|
||||
totalMaxSupply: fields[10] as BigInt,
|
||||
isInGlobalWhitelist: fields[11] as bool,
|
||||
info: (fields[12] as Map?)?.cast<String, dynamic>(),
|
||||
).._enabled = fields[4] == null ? true : fields[4] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ZanoAsset obj) {
|
||||
writer
|
||||
..writeByte(13)
|
||||
..writeByte(0)
|
||||
..write(obj.fullName)
|
||||
..writeByte(1)
|
||||
..write(obj.ticker)
|
||||
..writeByte(2)
|
||||
..write(obj.assetId)
|
||||
..writeByte(3)
|
||||
..write(obj.decimalPoint)
|
||||
..writeByte(4)
|
||||
..write(obj._enabled)
|
||||
..writeByte(5)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(6)
|
||||
..write(obj.owner)
|
||||
..writeByte(7)
|
||||
..write(obj.metaInfo)
|
||||
..writeByte(8)
|
||||
..write(obj.currentSupply)
|
||||
..writeByte(9)
|
||||
..write(obj.hiddenSupply)
|
||||
..writeByte(10)
|
||||
..write(obj.totalMaxSupply)
|
||||
..writeByte(11)
|
||||
..write(obj.isInGlobalWhitelist)
|
||||
..writeByte(12)
|
||||
..write(obj.info);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ZanoAssetAdapter && runtimeType == other.runtimeType && typeId == other.typeId;
|
||||
}
|
||||
Reference in New Issue
Block a user