mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'dev' into cw-1440-new-buy-sell-flow
# Conflicts: # cw_core/lib/format_fixed.dart # cw_solana/lib/solana_client.dart # lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart # lib/di.dart # lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart # lib/new-ui/widgets/receive_page/receive_top_bar.dart # lib/solana/cw_solana.dart # lib/src/screens/buy/buy_sell_page.dart # lib/src/screens/connect_device/connect_device_page.dart # lib/view_model/buy/buy_sell_view_model.dart # lib/view_model/hardware_wallet/hardware_wallet_view_model.dart # lib/view_model/hardware_wallet/ledger_view_model.dart # lib/view_model/send/send_view_model.dart
This commit is contained in:
+107
-107
@@ -6,110 +6,110 @@ defaults:
|
||||
run:
|
||||
shell: bash
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: [Linux, amd64, android]
|
||||
container:
|
||||
image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.41.9-ndkr28-go1.24.1-ruststablenightly
|
||||
env:
|
||||
STORE_PASS: test@cake_wallet
|
||||
KEY_PASS: test@cake_wallet
|
||||
MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
|
||||
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
ANDROID_AVD_HOME: /root/.android/avd
|
||||
volumes:
|
||||
- /opt/cw_cache_android/root/.cache:/root/.cache
|
||||
- /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
|
||||
- /opt/cw_cache_android/root/.ccache:/root/.ccache
|
||||
- /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
|
||||
- /opt/cw_cache_android/root/.gradle/:/root/.gradle
|
||||
- /opt/cw_cache_android/root/.android/:/root/.android
|
||||
- /opt/cw_cache_android/root/go/pkg:/root/go/pkg
|
||||
- /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
|
||||
- /dev/kvm:/dev/kvm
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [ 29 ]
|
||||
|
||||
steps:
|
||||
- name: Fix github actions messing up $HOME...
|
||||
run: "echo HOME=/root | sudo tee -a $GITHUB_ENV"
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: configure git
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
git config --global user.email "ci@cakewallet.com"
|
||||
git config --global user.name "CakeWallet CI"
|
||||
|
||||
- name: Fetch prebuilt Torch
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts
|
||||
wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz
|
||||
rm -rf torch_dart
|
||||
mkdir torch_dart
|
||||
pushd torch_dart
|
||||
tar -xzf ../torch_dart.tar.gz
|
||||
popd
|
||||
rm ./torch_dart.tar.gz
|
||||
popd
|
||||
|
||||
- name: Fetch prebuilt Reown
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts
|
||||
# cleaning
|
||||
rm -rf reown_flutter
|
||||
rm -f reown_flutter.tar.gz
|
||||
|
||||
wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz
|
||||
mkdir reown_flutter
|
||||
pushd reown_flutter
|
||||
tar -xzf ../reown_flutter.tar.gz
|
||||
popd
|
||||
rm ./reown_flutter.tar.gz
|
||||
popd
|
||||
|
||||
- name: Build Bitbox Flutter
|
||||
run: |
|
||||
set -x -e
|
||||
pushd scripts
|
||||
./build_bitbox_flutter.sh
|
||||
popd
|
||||
|
||||
- name: Prepare Zcash
|
||||
run: |
|
||||
set -x -e
|
||||
scripts/prepare_zcash.sh
|
||||
|
||||
- name: Execute Build and Setup Commands
|
||||
run: |
|
||||
pushd scripts/android
|
||||
source ./app_env.sh cakewallet
|
||||
export GITHUB_HUH=yeah
|
||||
./app_config.sh
|
||||
popd
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Build generated code
|
||||
run: |
|
||||
flutter --version
|
||||
flutter clean
|
||||
rm -rf .dart_tool
|
||||
rm pubspec.lock
|
||||
flutter pub get
|
||||
./model_generator.sh async
|
||||
|
||||
- name: Generate localization
|
||||
run: |
|
||||
dart run tool/generate_localization.dart
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
ABORT_ON_CHANGE=yeah scripts/lint.sh
|
||||
# lint:
|
||||
# runs-on: [Linux, amd64, android]
|
||||
# container:
|
||||
# image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.41.9-ndkr28-go1.24.1-ruststablenightly
|
||||
# env:
|
||||
# STORE_PASS: test@cake_wallet
|
||||
# KEY_PASS: test@cake_wallet
|
||||
# MONEROC_CACHE_DIR_ROOT: /opt/generic_cache
|
||||
# BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
# ANDROID_AVD_HOME: /root/.android/avd
|
||||
# volumes:
|
||||
# - /opt/cw_cache_android/root/.cache:/root/.cache
|
||||
# - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd
|
||||
# - /opt/cw_cache_android/root/.ccache:/root/.ccache
|
||||
# - /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache
|
||||
# - /opt/cw_cache_android/root/.gradle/:/root/.gradle
|
||||
# - /opt/cw_cache_android/root/.android/:/root/.android
|
||||
# - /opt/cw_cache_android/root/go/pkg:/root/go/pkg
|
||||
# - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache
|
||||
# - /dev/kvm:/dev/kvm
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# strategy:
|
||||
# matrix:
|
||||
# api-level: [ 29 ]
|
||||
#
|
||||
# steps:
|
||||
# - name: Fix github actions messing up $HOME...
|
||||
# run: "echo HOME=/root | sudo tee -a $GITHUB_ENV"
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# ref: ${{ inputs.ref }}
|
||||
#
|
||||
# - name: configure git
|
||||
# run: |
|
||||
# git config --global --add safe.directory '*'
|
||||
# git config --global user.email "ci@cakewallet.com"
|
||||
# git config --global user.name "CakeWallet CI"
|
||||
#
|
||||
# - name: Fetch prebuilt Torch
|
||||
# run: |
|
||||
# set -x -e
|
||||
# pushd scripts
|
||||
# wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz
|
||||
# rm -rf torch_dart
|
||||
# mkdir torch_dart
|
||||
# pushd torch_dart
|
||||
# tar -xzf ../torch_dart.tar.gz
|
||||
# popd
|
||||
# rm ./torch_dart.tar.gz
|
||||
# popd
|
||||
#
|
||||
# - name: Fetch prebuilt Reown
|
||||
# run: |
|
||||
# set -x -e
|
||||
# pushd scripts
|
||||
# # cleaning
|
||||
# rm -rf reown_flutter
|
||||
# rm -f reown_flutter.tar.gz
|
||||
#
|
||||
# wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz
|
||||
# mkdir reown_flutter
|
||||
# pushd reown_flutter
|
||||
# tar -xzf ../reown_flutter.tar.gz
|
||||
# popd
|
||||
# rm ./reown_flutter.tar.gz
|
||||
# popd
|
||||
#
|
||||
# - name: Build Bitbox Flutter
|
||||
# run: |
|
||||
# set -x -e
|
||||
# pushd scripts
|
||||
# ./build_bitbox_flutter.sh
|
||||
# popd
|
||||
#
|
||||
# - name: Prepare Zcash
|
||||
# run: |
|
||||
# set -x -e
|
||||
# scripts/prepare_zcash.sh
|
||||
#
|
||||
# - name: Execute Build and Setup Commands
|
||||
# run: |
|
||||
# pushd scripts/android
|
||||
# source ./app_env.sh cakewallet
|
||||
# export GITHUB_HUH=yeah
|
||||
# ./app_config.sh
|
||||
# popd
|
||||
#
|
||||
# - name: Install Flutter dependencies
|
||||
# run: |
|
||||
# flutter pub get
|
||||
#
|
||||
# - name: Build generated code
|
||||
# run: |
|
||||
# flutter --version
|
||||
# flutter clean
|
||||
# rm -rf .dart_tool
|
||||
# rm pubspec.lock
|
||||
# flutter pub get
|
||||
# ./model_generator.sh async
|
||||
#
|
||||
# - name: Generate localization
|
||||
# run: |
|
||||
# dart run tool/generate_localization.dart
|
||||
#
|
||||
# - name: Lint
|
||||
# run: |
|
||||
# ABORT_ON_CHANGE=yeah scripts/lint.sh
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
|
||||
class ExchangeRate {
|
||||
/// 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;
|
||||
|
||||
const ExchangeRate({required this.base, required this.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;
|
||||
|
||||
final scale = BigInt.from(10).pow(base.decimals);
|
||||
|
||||
if (amount.currency == base) {
|
||||
if (quote.isZero) return Money.zero(quote.currency);
|
||||
|
||||
return Money(amount.amount * quote.amount ~/ scale, quote.currency);
|
||||
}
|
||||
|
||||
if (amount.currency == quote.currency) {
|
||||
if (quote.isZero) return Money.zero(base);
|
||||
|
||||
return Money(amount.amount * scale ~/ quote.amount, base);
|
||||
}
|
||||
|
||||
throw ArgumentError(
|
||||
"Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair");
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,10 @@ class Money implements Comparable<Money> {
|
||||
@override
|
||||
int get hashCode => amount.hashCode ^ currency.hashCode;
|
||||
|
||||
// Added this to reduce the hops we do to convert Money to double
|
||||
// for fiat conversion and display
|
||||
double toDouble() => amount / multiplierOf(currency.decimals);
|
||||
|
||||
@override
|
||||
String toString() => formatFixed(amount, currency.decimals);
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri
|
||||
decimals ??= 0;
|
||||
fractionalDigits ??= decimals;
|
||||
|
||||
var multiplier = getMultiplier(decimals);
|
||||
// Make sure wei is a big number (convert as necessary)
|
||||
final multiplier = multiplierOf(decimals);
|
||||
var negative = value.isNegative;
|
||||
if (negative) value = value * BigInt.from(-1);
|
||||
if (negative) value = -value;
|
||||
|
||||
var fraction =
|
||||
value.modPow(BigInt.one, BigInt.parse(multiplier)).toString().padLeft(decimals, "0");
|
||||
var fraction = (value % multiplier).toString().padLeft(decimals, "0");
|
||||
|
||||
if (fractionalDigits < 0) fractionalDigits = 0;
|
||||
if (fractionalDigits > decimals) fractionalDigits = decimals;
|
||||
@@ -20,7 +18,7 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri
|
||||
fraction = removeTrailing("0", fraction);
|
||||
}
|
||||
|
||||
final whole = (value ~/ BigInt.parse(multiplier));
|
||||
final whole = value ~/ multiplier;
|
||||
|
||||
final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction";
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ BigInt parseFixed(String value, int decimals) {
|
||||
|
||||
final wholeValue = BigInt.parse(whole);
|
||||
final fractionValue = BigInt.parse(fraction);
|
||||
final multiplierValue = BigInt.parse(multiplier);
|
||||
final multiplierValue = multiplierOf(decimals);
|
||||
|
||||
var wei = (wholeValue * multiplierValue) + fractionValue;
|
||||
|
||||
@@ -69,3 +69,8 @@ BigInt parseFixed(String value, int decimals) {
|
||||
|
||||
// Returns a string "1" followed by decimal "0"s
|
||||
String getMultiplier(int decimals) => "1".padRight(decimals + 1, "0");
|
||||
|
||||
final _multipliers = <int, BigInt>{};
|
||||
|
||||
// this is more direct and faster than having it as string then parsing everytime to get the number
|
||||
BigInt multiplierOf(int decimals) => _multipliers[decimals] ??= BigInt.from(10).pow(decimals);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'package:cw_core/amount/exchange_rate.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ExchangeRate', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000", EUR),
|
||||
);
|
||||
|
||||
test('convert base currency to quote currency: 1 BTC to 60.000 EUR', () {
|
||||
final result = rate.convert(Money.parse('1', CryptoCurrency.btc));
|
||||
expect(result, Money.parse("60000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert base currency to quote currency: 0.5 BTC to 30.000 EUR', () {
|
||||
final result = rate.convert(Money.parse('0.5', CryptoCurrency.btc));
|
||||
expect(result, Money.parse("30000.00", EUR));
|
||||
});
|
||||
|
||||
test('convert quote currency to base currency: 60.000 EUR to 1 BTC', () {
|
||||
final result = rate.convert(Money.parse("60000.00", EUR));
|
||||
expect(result, Money.parse('1', CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('convert quote currency to base currency: 30 EUR to 0.0005 BTC', () {
|
||||
final result = rate.convert(Money.parse("30.00", EUR));
|
||||
expect(result, Money.parse('0.0005', CryptoCurrency.btc));
|
||||
});
|
||||
});
|
||||
|
||||
group('Edge Cases', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money.parse("60000", EUR),
|
||||
);
|
||||
|
||||
test('Converting zero turns into zero', () {
|
||||
expect(rate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR));
|
||||
expect(rate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('truncate to smallest unit', () {
|
||||
final result = rate.convert(Money(BigInt.one, CryptoCurrency.btc)); // 1 sat
|
||||
expect(result, Money.zero(EUR));
|
||||
});
|
||||
|
||||
test('Truncate base unit of base currency', () {
|
||||
final result = rate.convert(Money(BigInt.one, EUR));
|
||||
expect(result, Money.fromInt(16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('No int overflow with large numbers', () {
|
||||
final allBtc = Money.parse('21000000', CryptoCurrency.btc);
|
||||
final result = rate.convert(allBtc);
|
||||
expect(result, Money(BigInt.from(126000000000000), EUR));
|
||||
});
|
||||
|
||||
test('truncation for negative amounts', () {
|
||||
final result = rate.convert(Money.fromInt(-1, EUR));
|
||||
expect(result, Money.fromInt(-16, CryptoCurrency.btc));
|
||||
});
|
||||
|
||||
test('Base currency without decimals places: JPY', () {
|
||||
final rate = ExchangeRate(
|
||||
base: JPY,
|
||||
quote: Money.fromInt(1, EUR),
|
||||
);
|
||||
|
||||
expect(rate.convert(Money.fromInt(500, JPY)), Money.fromInt(500, EUR));
|
||||
expect(rate.convert(Money.fromInt(500, EUR)), Money.fromInt(500, JPY));
|
||||
});
|
||||
|
||||
test('Base currency with 18 decimals places: ETH', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.eth,
|
||||
quote: Money.fromInt(300000, EUR),
|
||||
);
|
||||
|
||||
expect(rate.convert(Money.parse('1', CryptoCurrency.eth)), Money.fromInt(300000, EUR));
|
||||
expect(rate.convert(Money.fromInt(3000, EUR)), Money.parse('0.01', CryptoCurrency.eth));
|
||||
});
|
||||
|
||||
test('Smalles base unit is less than smalles base unit of quote', () {
|
||||
final rate = ExchangeRate(
|
||||
base: CryptoCurrency.eth,
|
||||
quote: Money.fromInt(300000, EUR),
|
||||
);
|
||||
expect(rate.convert(Money(BigInt.one, CryptoCurrency.eth)), Money(BigInt.zero, EUR));
|
||||
});
|
||||
|
||||
test('Cannot convert currency outside of pair', () {
|
||||
expect(() => rate.convert(Money.parse("60000", USD)), throwsArgumentError);
|
||||
});
|
||||
|
||||
test('handle quote being 0 correctly', () {
|
||||
final zeroRate = ExchangeRate(
|
||||
base: CryptoCurrency.btc,
|
||||
quote: Money(BigInt.zero, EUR),
|
||||
);
|
||||
expect(zeroRate.convert(Money.fromInt(100, CryptoCurrency.btc)), Money(BigInt.zero, EUR));
|
||||
expect(zeroRate.convert(Money.fromInt(100, EUR)), Money(BigInt.zero, CryptoCurrency.btc));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class FiatCurrency implements Currency {
|
||||
const FiatCurrency({
|
||||
required this.symbol,
|
||||
required this.countryCode,
|
||||
required this.fullName,
|
||||
this.decimals = 2,
|
||||
});
|
||||
|
||||
final String countryCode;
|
||||
|
||||
@override
|
||||
final String fullName;
|
||||
|
||||
@override
|
||||
final int decimals;
|
||||
|
||||
@override
|
||||
final String symbol;
|
||||
|
||||
@override
|
||||
String? get iconPath => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get name => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String? get tag => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Money parseAmount(String value) => Money.parse(value, this);
|
||||
|
||||
@override
|
||||
Money? tryParseAmount(String value) => Money.tryParse(value, this);
|
||||
}
|
||||
|
||||
const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro");
|
||||
const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar");
|
||||
const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0);
|
||||
@@ -51,8 +51,7 @@ class _Visitor extends SimpleAstVisitor<void> {
|
||||
|
||||
if (uriString.startsWith("package:http")) {
|
||||
final prefixNode = node.prefix;
|
||||
if (prefixNode != null &&
|
||||
prefixNode.name == "very_insecure_http_do_not_use") {
|
||||
if (prefixNode != null && prefixNode.name == "very_insecure_http_do_not_use") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ class ReplaceWithPrintV extends ResolvedCorrectionProducer {
|
||||
);
|
||||
|
||||
@override
|
||||
CorrectionApplicability get applicability =>
|
||||
CorrectionApplicability.singleLocation;
|
||||
CorrectionApplicability get applicability => CorrectionApplicability.singleLocation;
|
||||
|
||||
@override
|
||||
FixKind get fixKind => _fixKind;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CwSharedExternal {
|
||||
static const MethodChannel _channel =
|
||||
const MethodChannel('cw_shared_external');
|
||||
static const MethodChannel _channel = const MethodChannel('cw_shared_external');
|
||||
|
||||
static Future<String> get platformVersion async {
|
||||
final String version = (await _channel.invokeMethod('getPlatformVersion')).toString();
|
||||
|
||||
@@ -32,14 +32,29 @@ class TransactionFetchResult {
|
||||
});
|
||||
}
|
||||
|
||||
class TransactionSyncResult {
|
||||
final List<SolanaTransactionModel> transactions;
|
||||
final String? newestSignature;
|
||||
|
||||
TransactionSyncResult({
|
||||
required this.transactions,
|
||||
this.newestSignature,
|
||||
});
|
||||
}
|
||||
|
||||
class SolanaWalletClient {
|
||||
// Minimum amount in SOL to consider a transaction valid (to filter spam)
|
||||
static Money minValidAmount = Money.parse("0.00000003", CryptoCurrency.sol);
|
||||
|
||||
static const int _signaturePageSize = 1000;
|
||||
|
||||
late final client = ProxyWrapper().getHttpIOClient();
|
||||
SolanaRPC? _provider;
|
||||
bool _isStopped = false;
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
_isStopped = false;
|
||||
String formattedUrl;
|
||||
String protocolUsed = node.isSSL ? "https" : "http";
|
||||
|
||||
@@ -625,8 +640,6 @@ class SolanaWalletClient {
|
||||
incomingAmount = diff.toDouble();
|
||||
incomingMintAddress = mint;
|
||||
final token = await getTokenInfo(mint);
|
||||
printV(token?.symbol);
|
||||
printV(token?.decimals);
|
||||
incomingToken =
|
||||
token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
|
||||
incomingTo = walletAddress;
|
||||
@@ -951,27 +964,67 @@ class SolanaWalletClient {
|
||||
return mints.toList();
|
||||
}
|
||||
|
||||
/// Load the Address's transactions into the account
|
||||
Future<List<SolanaTransactionModel>> fetchTransactions(
|
||||
Future<List<Map<String, dynamic>>> _getAllSignaturesSinceLastFetch(
|
||||
SolAddress address,
|
||||
String? until,
|
||||
Commitment? commitment,
|
||||
) async {
|
||||
final signatures = <Map<String, dynamic>>[];
|
||||
String? before;
|
||||
|
||||
while (true) {
|
||||
final currentPageSignatureResults = await _provider!.request(
|
||||
SolanaRPCGetSignaturesForAddress(
|
||||
account: address,
|
||||
commitment: commitment,
|
||||
until: until,
|
||||
before: before,
|
||||
limit: _signaturePageSize,
|
||||
),
|
||||
);
|
||||
|
||||
if (currentPageSignatureResults.isEmpty) break;
|
||||
|
||||
signatures.addAll(currentPageSignatureResults);
|
||||
|
||||
if (currentPageSignatureResults.length < _signaturePageSize) break;
|
||||
|
||||
if (until == null) break;
|
||||
|
||||
final lastSignatureOnPage = currentPageSignatureResults.last['signature'] as String;
|
||||
|
||||
if (lastSignatureOnPage == before) break;
|
||||
|
||||
before = lastSignatureOnPage;
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
Future<TransactionSyncResult> fetchTransactions(
|
||||
SolAddress address, {
|
||||
SPLToken? splToken,
|
||||
Commitment? commitment,
|
||||
SolAddress? walletAddress,
|
||||
String? untilSignature,
|
||||
required void Function(List<SolanaTransactionModel>) onUpdate,
|
||||
}) async {
|
||||
List<SolanaTransactionModel> transactions = [];
|
||||
final transactions = <SolanaTransactionModel>[];
|
||||
|
||||
try {
|
||||
final signatures = await _provider!.request(
|
||||
SolanaRPCGetSignaturesForAddress(
|
||||
account: address,
|
||||
commitment: commitment,
|
||||
),
|
||||
);
|
||||
final signatures =
|
||||
await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment);
|
||||
|
||||
if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions);
|
||||
|
||||
// The maximum concurrent batch size.
|
||||
const int batchSize = 10;
|
||||
|
||||
bool hasFailures = false;
|
||||
|
||||
for (int i = 0; i < signatures.length; i += batchSize) {
|
||||
if (_isStopped) return TransactionSyncResult(transactions: transactions);
|
||||
|
||||
final batch = signatures.skip(i).take(batchSize).toList();
|
||||
|
||||
final batchResponses = await Future.wait(batch.map((signature) async {
|
||||
@@ -985,6 +1038,7 @@ class SolanaWalletClient {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
hasFailures = true;
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
@@ -999,16 +1053,16 @@ class SolanaWalletClient {
|
||||
|
||||
final parsedTransactionsLists = await Future.wait(parsedTransactionsFutures);
|
||||
|
||||
// We flatten the list of lists into a single list
|
||||
final batchTransactions = <SolanaTransactionModel>[];
|
||||
for (final parsedList in parsedTransactionsLists) {
|
||||
if (parsedList != null) {
|
||||
transactions.addAll(parsedList);
|
||||
batchTransactions.addAll(parsedList);
|
||||
}
|
||||
}
|
||||
|
||||
// Only update UI if we have new valid transactions
|
||||
if (transactions.isNotEmpty) {
|
||||
onUpdate(List<SolanaTransactionModel>.from(transactions));
|
||||
if (batchTransactions.isNotEmpty) {
|
||||
transactions.addAll(batchTransactions);
|
||||
onUpdate(batchTransactions);
|
||||
}
|
||||
|
||||
if (i + batchSize < signatures.length) {
|
||||
@@ -1016,38 +1070,53 @@ class SolanaWalletClient {
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
return TransactionSyncResult(
|
||||
transactions: transactions,
|
||||
newestSignature: hasFailures ? null : signatures.first['signature'] as String,
|
||||
);
|
||||
} catch (err, s) {
|
||||
printV('Error fetching transactions: $err \n$s');
|
||||
return [];
|
||||
return TransactionSyncResult(transactions: transactions);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SolanaTransactionModel>> getSPLTokenTransfers({
|
||||
final Map<String, ProgramDerivedAddress> associatedTokenAccountCache = {};
|
||||
|
||||
Future<TransactionSyncResult> getSPLTokenTransfers({
|
||||
required String mintAddress,
|
||||
required SPLToken splToken,
|
||||
required SolanaPrivateKey privateKey,
|
||||
String? untilSignature,
|
||||
required void Function(List<SolanaTransactionModel>) onUpdate,
|
||||
}) async {
|
||||
ProgramDerivedAddress? associatedTokenAccount;
|
||||
final ownerWalletAddress = privateKey.publicKey().toAddress();
|
||||
try {
|
||||
associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
|
||||
payerPrivateKey: privateKey,
|
||||
mintAddress: SolAddress(mintAddress),
|
||||
ownerAddress: ownerWalletAddress,
|
||||
shouldCreateATA: false,
|
||||
);
|
||||
} catch (e, s) {
|
||||
printV('$e \n $s');
|
||||
}
|
||||
|
||||
if (associatedTokenAccount == null) return [];
|
||||
var associatedTokenAccount = associatedTokenAccountCache[mintAddress];
|
||||
|
||||
if (associatedTokenAccount == null) {
|
||||
try {
|
||||
associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
|
||||
payerPrivateKey: privateKey,
|
||||
mintAddress: SolAddress(mintAddress),
|
||||
ownerAddress: ownerWalletAddress,
|
||||
shouldCreateATA: false,
|
||||
);
|
||||
} catch (e, s) {
|
||||
printV('$e \n $s');
|
||||
}
|
||||
|
||||
if (associatedTokenAccount == null) {
|
||||
return TransactionSyncResult(transactions: <SolanaTransactionModel>[]);
|
||||
}
|
||||
|
||||
associatedTokenAccountCache[mintAddress] = associatedTokenAccount;
|
||||
}
|
||||
|
||||
return fetchTransactions(
|
||||
associatedTokenAccount.address,
|
||||
splToken: splToken,
|
||||
walletAddress: ownerWalletAddress,
|
||||
untilSignature: untilSignature,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
}
|
||||
@@ -1055,16 +1124,9 @@ class SolanaWalletClient {
|
||||
final Map<String, SPLToken?> tokenInfoCache = {};
|
||||
|
||||
Future<SPLToken?> getTokenInfo(String mintAddress) async {
|
||||
if (tokenInfoCache.containsKey(mintAddress)) {
|
||||
printV("Cached");
|
||||
return tokenInfoCache[mintAddress];
|
||||
} else {
|
||||
final token = await fetchSPLTokenInfo(mintAddress);
|
||||
if (token != null) {
|
||||
tokenInfoCache[mintAddress] = token;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress];
|
||||
|
||||
return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress);
|
||||
}
|
||||
|
||||
Future<SPLToken?> fetchSPLTokenInfo(String mintAddress) async {
|
||||
@@ -1139,7 +1201,7 @@ class SolanaWalletClient {
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {}
|
||||
void stop() => _isStopped = true;
|
||||
|
||||
SolanaRPC? get getSolanaProvider => _provider;
|
||||
|
||||
|
||||
@@ -31,18 +31,36 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<Solan
|
||||
await _load();
|
||||
}
|
||||
|
||||
Future<void> _saveQueue = Future.value();
|
||||
|
||||
@override
|
||||
Future<void> save() async {
|
||||
try {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
|
||||
final data = json.encode({'transactions': transactionMaps});
|
||||
await encryptionFileUtils.write(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
printV('Error while saving solana transaction history: ${e.toString()}');
|
||||
printV(s);
|
||||
}
|
||||
Future<void> save() => saveAndConfirm();
|
||||
|
||||
Future<bool> saveAndConfirm() {
|
||||
final write = _saveQueue.then((_) async {
|
||||
try {
|
||||
await _write();
|
||||
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
printV('Error while saving solana transaction history: ${e.toString()}');
|
||||
printV(s);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
_saveQueue = write;
|
||||
|
||||
return write;
|
||||
}
|
||||
|
||||
Future<void> _write() async {
|
||||
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
|
||||
final path = '$dirPath/$transactionsHistoryFileName';
|
||||
final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
|
||||
final data = json.encode({'transactions': transactionMaps});
|
||||
await encryptionFileUtils.write(path: path, password: _password, data: data);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -88,6 +88,8 @@ abstract class SolanaWalletBase
|
||||
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
Future<void>? _currentRefresh;
|
||||
|
||||
late final Box<SPLToken> splTokensBox;
|
||||
|
||||
@override
|
||||
@@ -321,6 +323,33 @@ abstract class SolanaWalletBase
|
||||
]);
|
||||
}
|
||||
|
||||
static const _nativeSource = 'native';
|
||||
|
||||
String _lastSyncedSignatureKey(String source) =>
|
||||
'solana_last_synced_signature_${walletInfo.name}_$source';
|
||||
|
||||
Future<String?> _lastSyncedSignature(String source) async {
|
||||
if (transactionHistory.transactions.isEmpty) return null;
|
||||
|
||||
final prefs = await _sharedPrefs.future;
|
||||
|
||||
return prefs.getString(_lastSyncedSignatureKey(source));
|
||||
}
|
||||
|
||||
Future<void> _saveLastSyncedSignature(String source, String? signature) async {
|
||||
if (signature == null) return;
|
||||
|
||||
final prefs = await _sharedPrefs.future;
|
||||
|
||||
await prefs.setString(_lastSyncedSignatureKey(source), signature);
|
||||
}
|
||||
|
||||
Future<void> _clearLastSyncedSignature(String source) async {
|
||||
final prefs = await _sharedPrefs.future;
|
||||
|
||||
await prefs.remove(_lastSyncedSignatureKey(source));
|
||||
}
|
||||
|
||||
/// Polls for a specific transaction by signature with exponential backoff
|
||||
/// I'm using this in case we make the call to fetch the transaction and it has not finished its confirmations on the solana network and been indexed by the node networks we use.
|
||||
Future<void> pollForTransaction({
|
||||
@@ -365,20 +394,34 @@ abstract class SolanaWalletBase
|
||||
await updateTransactionsHistory();
|
||||
}
|
||||
|
||||
void updateTransactions(List<SolanaTransactionModel> updatedTx) {
|
||||
addTransactionsToTransactionHistory(updatedTx);
|
||||
}
|
||||
void updateTransactions(List<SolanaTransactionModel> updatedTx) => _addTransactions(updatedTx);
|
||||
|
||||
/// Fetches the native SOL transactions linked to the wallet Public Key
|
||||
Future<void> _updateNativeSOLTransactions() async {
|
||||
final transactions =
|
||||
await _client.fetchTransactions(_solanaPublicKey.toAddress(), onUpdate: updateTransactions);
|
||||
final result = await _client.fetchTransactions(
|
||||
_solanaPublicKey.toAddress(),
|
||||
untilSignature: await _lastSyncedSignature(_nativeSource),
|
||||
onUpdate: updateTransactions,
|
||||
);
|
||||
|
||||
await addTransactionsToTransactionHistory(transactions);
|
||||
await _updateStateWhenSyncForTheSourceEnds(_nativeSource, result);
|
||||
}
|
||||
|
||||
Future<void> _updateStateWhenSyncForTheSourceEnds(
|
||||
String source,
|
||||
TransactionSyncResult result,
|
||||
) async {
|
||||
if (result.transactions.isNotEmpty) {
|
||||
final isSaved = await transactionHistory.saveAndConfirm();
|
||||
|
||||
if (!isSaved) return;
|
||||
}
|
||||
|
||||
await _saveLastSyncedSignature(source, result.newestSignature);
|
||||
}
|
||||
|
||||
Future<void> updateSPLTokenTransactions({List<String>? specificMints}) async {
|
||||
final allTokens = balance.keys.whereType<SPLToken>().toList(growable: false);
|
||||
final allTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false);
|
||||
|
||||
// Filter to specific mints if provided
|
||||
final tokens = specificMints != null
|
||||
@@ -394,30 +437,28 @@ abstract class SolanaWalletBase
|
||||
i,
|
||||
i + batchSize > tokens.length ? tokens.length : i + batchSize,
|
||||
);
|
||||
final results = await Future.wait(
|
||||
|
||||
await Future.wait(
|
||||
batch.map((token) async {
|
||||
try {
|
||||
return await _client.getSPLTokenTransfers(
|
||||
final result = await _client.getSPLTokenTransfers(
|
||||
mintAddress: token.mintAddress,
|
||||
splToken: token,
|
||||
privateKey: _solanaPrivateKey,
|
||||
untilSignature: await _lastSyncedSignature(token.mintAddress),
|
||||
onUpdate: updateTransactions,
|
||||
);
|
||||
} catch (_) {
|
||||
return <SolanaTransactionModel>[];
|
||||
|
||||
await _updateStateWhenSyncForTheSourceEnds(token.mintAddress, result);
|
||||
} catch (e) {
|
||||
printV('Error fetching spl token (${token.symbol}) transfers ${e.toString()}');
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
for (final list in results) {
|
||||
await addTransactionsToTransactionHistory(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addTransactionsToTransactionHistory(
|
||||
List<SolanaTransactionModel> transactions,
|
||||
) async {
|
||||
void _addTransactions(List<SolanaTransactionModel> transactions) {
|
||||
final Map<String, SolanaTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
@@ -436,6 +477,12 @@ abstract class SolanaWalletBase
|
||||
}
|
||||
|
||||
transactionHistory.addMany(result);
|
||||
}
|
||||
|
||||
Future<void> addTransactionsToTransactionHistory(
|
||||
List<SolanaTransactionModel> transactions,
|
||||
) async {
|
||||
_addTransactions(transactions);
|
||||
|
||||
await transactionHistory.save();
|
||||
}
|
||||
@@ -456,6 +503,17 @@ abstract class SolanaWalletBase
|
||||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
// we want to handle the case where multiple refresh triggers (our users can swipe down
|
||||
// multiple times), so we track the currrent refresh and join it instead of starting
|
||||
// another one
|
||||
Future<void> _refresh() {
|
||||
return _currentRefresh ??= Future.wait([
|
||||
updateTokenBalance(),
|
||||
updateTransactionsHistory(),
|
||||
_getEstimatedFees(),
|
||||
]).whenComplete(() => _currentRefresh = null);
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
@@ -469,12 +527,7 @@ abstract class SolanaWalletBase
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
updateTokenBalance(),
|
||||
_updateNativeSOLTransactions(),
|
||||
updateSPLTokenTransactions(),
|
||||
_getEstimatedFees(),
|
||||
]);
|
||||
await _refresh();
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
@@ -626,6 +679,14 @@ abstract class SolanaWalletBase
|
||||
|
||||
List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
|
||||
|
||||
SPLToken? splTokenBySymbol(String symbol) {
|
||||
for (final token in splTokensBox.values) {
|
||||
if (token.symbol == symbol) return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void addInitialTokens() {
|
||||
final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
|
||||
|
||||
@@ -802,12 +863,27 @@ abstract class SolanaWalletBase
|
||||
}
|
||||
|
||||
Future<void> deleteSPLToken(SPLToken token) async {
|
||||
final sources = <String>{token.mintAddress};
|
||||
|
||||
if (token.symbol == CryptoCurrency.sol.symbol) {
|
||||
sources.add(_nativeSource);
|
||||
}
|
||||
|
||||
if (splTokensBox.isOpen) {
|
||||
sources.addAll(splTokensBox.values
|
||||
.where((t) => t.symbol == token.symbol)
|
||||
.map((t) => t.mintAddress));
|
||||
|
||||
await splTokensBox.delete(token.mintAddress);
|
||||
}
|
||||
|
||||
balance.remove(token);
|
||||
await _removeTokenTransactionsInHistory(token);
|
||||
|
||||
for (final source in sources) {
|
||||
await _clearLastSyncedSignature(source);
|
||||
}
|
||||
|
||||
updateTokenBalance();
|
||||
}
|
||||
|
||||
@@ -831,11 +907,12 @@ abstract class SolanaWalletBase
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
updateTokenBalance();
|
||||
_updateNativeSOLTransactions();
|
||||
updateSPLTokenTransactions();
|
||||
_getEstimatedFees();
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) async {
|
||||
try {
|
||||
await _refresh();
|
||||
} catch (e) {
|
||||
printV('Error on periodic solana refresh: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cw_solana/solana_mnemonics.dart';
|
||||
import 'package:cw_solana/solana_wallet.dart';
|
||||
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SolanaWalletService extends WalletService<
|
||||
SolanaNewWalletCredentials,
|
||||
@@ -96,6 +97,11 @@ class SolanaWalletService extends WalletService<
|
||||
throw Exception('Wallet not found');
|
||||
}
|
||||
await WalletInfo.delete(walletInfo);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
for (final key in prefs.getKeys().where(
|
||||
(k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) {
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -13,7 +13,6 @@ wownero.WalletListener getWlptr() {
|
||||
return _wlptr!;
|
||||
}
|
||||
|
||||
|
||||
wownero.SubaddressAccount? subaddressAccount;
|
||||
|
||||
bool isUpdating = false;
|
||||
@@ -69,6 +68,6 @@ Future<void> addAccount({required String label}) async {
|
||||
}
|
||||
|
||||
Future<void> setLabelForAccount({required int accountIndex, required String label}) async {
|
||||
_setLabelForAccount({'accountIndex': accountIndex, 'label': label});
|
||||
await store();
|
||||
}
|
||||
_setLabelForAccount({'accountIndex': accountIndex, 'label': label});
|
||||
await store();
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ class ConnectionToNodeException implements Exception {
|
||||
ConnectionToNodeException({required this.message});
|
||||
|
||||
final String message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class CreationTransactionException implements Exception {
|
||||
CreationTransactionException({required this.message});
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class SetupWalletException implements Exception {
|
||||
SetupWalletException({required this.message});
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ class WalletCreationException implements Exception {
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ class WalletOpeningException implements Exception {
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class WalletRestoreFromKeysException implements Exception {
|
||||
WalletRestoreFromKeysException({required this.message});
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class WalletRestoreFromSeedException implements Exception {
|
||||
WalletRestoreFromSeedException({required this.message});
|
||||
|
||||
|
||||
final String message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:ffi/ffi.dart';
|
||||
class AccountRow extends Struct {
|
||||
@Int64()
|
||||
external int id;
|
||||
|
||||
|
||||
external Pointer<Utf8> label;
|
||||
|
||||
String getLabel() => label.toDartString();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
|
||||
class PendingTransactionDescription {
|
||||
PendingTransactionDescription({
|
||||
required this.amount,
|
||||
required this.fee,
|
||||
required this.hash,
|
||||
required this.hex,
|
||||
required this.txKey,
|
||||
required this.pointerAddress});
|
||||
PendingTransactionDescription(
|
||||
{required this.amount,
|
||||
required this.fee,
|
||||
required this.hash,
|
||||
required this.hex,
|
||||
required this.txKey,
|
||||
required this.pointerAddress});
|
||||
|
||||
final int amount;
|
||||
final int fee;
|
||||
@@ -14,4 +13,4 @@ class PendingTransactionDescription {
|
||||
final String hex;
|
||||
final String txKey;
|
||||
final int pointerAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import 'package:ffi/ffi.dart';
|
||||
class SubaddressRow extends Struct {
|
||||
@Int64()
|
||||
external int id;
|
||||
|
||||
|
||||
external Pointer<Utf8> address;
|
||||
|
||||
|
||||
external Pointer<Utf8> label;
|
||||
|
||||
String getLabel() => label.toDartString();
|
||||
String getAddress() => address.toDartString();
|
||||
int getId() => id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ class Subaddress {
|
||||
);
|
||||
final int addressIndex;
|
||||
final int accountIndex;
|
||||
String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
String get label => wownero.Wallet_getSubaddressLabel(wptr!,
|
||||
accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
final int txCount;
|
||||
final int received;
|
||||
}
|
||||
@@ -68,7 +69,10 @@ List<Subaddress> getAllSubaddresses() {
|
||||
final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(",");
|
||||
final account = wownero.TransactionInfo_subaddrAccount(tx);
|
||||
newttDetails.add(TinyTransactionDetails(
|
||||
address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)),
|
||||
address: List.generate(
|
||||
subaddrs.length,
|
||||
(index) => getAddress(
|
||||
accountIndex: account, addressIndex: int.tryParse(subaddrs[index]) ?? 0)),
|
||||
amount: wownero.TransactionInfo_amount(tx),
|
||||
));
|
||||
}
|
||||
@@ -79,7 +83,7 @@ List<Subaddress> getAllSubaddresses() {
|
||||
final list = List.generate(size, (index) {
|
||||
final ttDetailsLocal = ttDetails.where((element) {
|
||||
final address = getAddress(
|
||||
accountIndex: subaddress!.accountIndex,
|
||||
accountIndex: subaddress!.accountIndex,
|
||||
addressIndex: index,
|
||||
);
|
||||
if (address == element.address) return true;
|
||||
@@ -97,7 +101,8 @@ List<Subaddress> getAllSubaddresses() {
|
||||
);
|
||||
}).reversed.toList();
|
||||
if (list.isEmpty) {
|
||||
list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0));
|
||||
list.add(Subaddress(
|
||||
addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -113,7 +118,8 @@ int numSubaddresses(int subaccountIndex) {
|
||||
|
||||
void setLabelForSubaddressSync(
|
||||
{required int accountIndex, required int addressIndex, required String label}) {
|
||||
wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label);
|
||||
wownero.Wallet_setSubaddressLabel(wptr!,
|
||||
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
|
||||
}
|
||||
|
||||
void _addSubaddress(Map<String, dynamic> args) {
|
||||
@@ -128,8 +134,7 @@ void _setLabelForSubaddress(Map<String, dynamic> args) {
|
||||
final accountIndex = args['accountIndex'] as int;
|
||||
final addressIndex = args['addressIndex'] as int;
|
||||
|
||||
setLabelForSubaddressSync(
|
||||
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
|
||||
setLabelForSubaddressSync(accountIndex: accountIndex, addressIndex: addressIndex, label: label);
|
||||
}
|
||||
|
||||
Future<void> addSubaddress({required int accountIndex, required String label}) async {
|
||||
@@ -138,11 +143,8 @@ Future<void> addSubaddress({required int accountIndex, required String label}) a
|
||||
}
|
||||
|
||||
Future<void> setLabelForSubaddress(
|
||||
{required int accountIndex, required int addressIndex, required String label}) async {
|
||||
_setLabelForSubaddress({
|
||||
'accountIndex': accountIndex,
|
||||
'addressIndex': addressIndex,
|
||||
'label': label
|
||||
});
|
||||
{required int accountIndex, required int addressIndex, required String label}) async {
|
||||
_setLabelForSubaddress(
|
||||
{'accountIndex': accountIndex, 'addressIndex': addressIndex, 'label': label});
|
||||
await store();
|
||||
}
|
||||
|
||||
@@ -43,33 +43,36 @@ Future<List<Transaction>> getAllTransactions() async {
|
||||
await txHistoryMutex.acquire();
|
||||
txhistory ??= wownero.Wallet_history(wptr!);
|
||||
int size = countOfTransactions();
|
||||
final list = List.generate(size, (index) => Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index)));
|
||||
final list = List.generate(
|
||||
size,
|
||||
(index) =>
|
||||
Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index)));
|
||||
txHistoryMutex.release();
|
||||
|
||||
|
||||
final accts = wownero.Wallet_numSubaddressAccounts(wptr!);
|
||||
for (var i = 0; i < accts; i++) {
|
||||
for (var i = 0; i < accts; i++) {
|
||||
final fullBalance = wownero.Wallet_balance(wptr!, accountIndex: i);
|
||||
final availBalance = wownero.Wallet_unlockedBalance(wptr!, accountIndex: i);
|
||||
if (fullBalance > availBalance) {
|
||||
if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isEmpty) {
|
||||
dummyTxs.add(
|
||||
Transaction.dummy(
|
||||
displayLabel: "",
|
||||
description: "",
|
||||
fee: 0,
|
||||
confirmations: 0,
|
||||
blockheight: 0,
|
||||
accountIndex: i,
|
||||
addressIndex: 0,
|
||||
addressIndexList: [0],
|
||||
paymentId: "",
|
||||
amount: fullBalance - availBalance,
|
||||
isSpend: false,
|
||||
hash: "pending",
|
||||
key: "pending",
|
||||
txInfo: Pointer.fromAddress(0),
|
||||
)..timeStamp = DateTime.now()
|
||||
);
|
||||
if (list
|
||||
.where((element) => element.accountIndex == i && element.isConfirmed == false)
|
||||
.isEmpty) {
|
||||
dummyTxs.add(Transaction.dummy(
|
||||
displayLabel: "",
|
||||
description: "",
|
||||
fee: 0,
|
||||
confirmations: 0,
|
||||
blockheight: 0,
|
||||
accountIndex: i,
|
||||
addressIndex: 0,
|
||||
addressIndexList: [0],
|
||||
paymentId: "",
|
||||
amount: fullBalance - availBalance,
|
||||
isSpend: false,
|
||||
hash: "pending",
|
||||
key: "pending",
|
||||
txInfo: Pointer.fromAddress(0),
|
||||
)..timeStamp = DateTime.now());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,13 +92,13 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||
String? amount,
|
||||
int accountIndex = 0,
|
||||
List<String> preferredInputs = const []}) async {
|
||||
|
||||
final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount);
|
||||
|
||||
final address_ = address.toNativeUtf8();
|
||||
|
||||
final address_ = address.toNativeUtf8();
|
||||
final paymentId_ = paymentId.toNativeUtf8();
|
||||
if (preferredInputs.isEmpty) {
|
||||
throw WowneroTransactionCreationException("No inputs provided, transaction cannot be constructed");
|
||||
throw WowneroTransactionCreationException(
|
||||
"No inputs provided, transaction cannot be constructed");
|
||||
}
|
||||
final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8();
|
||||
|
||||
@@ -105,7 +108,8 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||
final preferredInputsAddr = preferredInputs_.address;
|
||||
final spaddr = wownero.defaultSeparator.address;
|
||||
final pendingTx = Pointer<Void>.fromAddress(await Isolate.run(() {
|
||||
final tx = wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction(
|
||||
final tx =
|
||||
wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction(
|
||||
Pointer.fromAddress(waddr),
|
||||
Pointer.fromAddress(addraddr).cast(),
|
||||
Pointer.fromAddress(paymentIdAddr).cast(),
|
||||
@@ -140,13 +144,13 @@ Future<PendingTransactionDescription> createTransactionSync(
|
||||
final rTxKey = rHash;
|
||||
|
||||
return PendingTransactionDescription(
|
||||
amount: rAmt,
|
||||
fee: rFee,
|
||||
hash: rHash,
|
||||
hex: '',
|
||||
txKey: rTxKey,
|
||||
pointerAddress: pendingTx.address,
|
||||
);
|
||||
amount: rAmt,
|
||||
fee: rFee,
|
||||
hash: rHash,
|
||||
hex: '',
|
||||
txKey: rTxKey,
|
||||
pointerAddress: pendingTx.address,
|
||||
);
|
||||
}
|
||||
|
||||
PendingTransactionDescription createTransactionMultDestSync(
|
||||
@@ -155,7 +159,6 @@ PendingTransactionDescription createTransactionMultDestSync(
|
||||
required int priorityRaw,
|
||||
int accountIndex = 0,
|
||||
List<String> preferredInputs = const []}) {
|
||||
|
||||
final txptr = wownero.Wallet_createTransactionMultDest(
|
||||
wptr!,
|
||||
dstAddr: outputs.map((e) => e.address).toList(),
|
||||
@@ -182,8 +185,8 @@ void commitTransactionFromPointerAddress({required int address}) =>
|
||||
commitTransaction(transactionPointer: wownero.PendingTransaction.fromAddress(address));
|
||||
|
||||
void commitTransaction({required wownero.PendingTransaction transactionPointer}) {
|
||||
|
||||
final txCommit = wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
|
||||
final txCommit =
|
||||
wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false);
|
||||
|
||||
String? error = (() {
|
||||
final status = wownero.PendingTransaction_status(transactionPointer.cast());
|
||||
@@ -200,9 +203,8 @@ void commitTransaction({required wownero.PendingTransaction transactionPointer})
|
||||
}
|
||||
return wownero.Wallet_errorString(wptr!);
|
||||
})();
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (error != null) {
|
||||
throw CreationTransactionException(message: error);
|
||||
}
|
||||
@@ -270,19 +272,20 @@ Future<PendingTransactionDescription> createTransactionMultDest(
|
||||
'preferredInputs': preferredInputs
|
||||
});
|
||||
|
||||
|
||||
class Transaction {
|
||||
final String displayLabel;
|
||||
late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!,
|
||||
accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
late final String address = getAddress(
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndex,
|
||||
);
|
||||
late final List<String> addressList = List.generate(addressIndexList.length, (index) =>
|
||||
getAddress(
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndexList[index],
|
||||
));
|
||||
late final List<String> addressList = List.generate(
|
||||
addressIndexList.length,
|
||||
(index) => getAddress(
|
||||
accountIndex: accountIndex,
|
||||
addressIndex: addressIndexList[index],
|
||||
));
|
||||
final String description;
|
||||
final int fee;
|
||||
final int confirmations;
|
||||
@@ -331,33 +334,36 @@ class Transaction {
|
||||
timeStamp = DateTime.fromMillisecondsSinceEpoch(
|
||||
wownero.TransactionInfo_timestamp(txInfo) * 1000,
|
||||
),
|
||||
isSpend = wownero.TransactionInfo_direction(txInfo) ==
|
||||
wownero.TransactionInfo_Direction.Out,
|
||||
isSpend =
|
||||
wownero.TransactionInfo_direction(txInfo) == wownero.TransactionInfo_Direction.Out,
|
||||
amount = wownero.TransactionInfo_amount(txInfo),
|
||||
paymentId = wownero.TransactionInfo_paymentId(txInfo),
|
||||
accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo),
|
||||
addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
|
||||
addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
|
||||
addressIndex =
|
||||
int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0,
|
||||
addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo)
|
||||
.split(", ")
|
||||
.map((e) => int.tryParse(e) ?? 0)
|
||||
.toList(),
|
||||
blockheight = wownero.TransactionInfo_blockHeight(txInfo),
|
||||
confirmations = wownero.TransactionInfo_confirmations(txInfo),
|
||||
fee = wownero.TransactionInfo_fee(txInfo),
|
||||
description = wownero.TransactionInfo_description(txInfo),
|
||||
key = wownero.Wallet_getTxKey(wptr!, txid: wownero.TransactionInfo_hash(txInfo));
|
||||
|
||||
Transaction.dummy({
|
||||
required this.displayLabel,
|
||||
required this.description,
|
||||
required this.fee,
|
||||
required this.confirmations,
|
||||
required this.blockheight,
|
||||
required this.accountIndex,
|
||||
required this.addressIndex,
|
||||
required this.addressIndexList,
|
||||
required this.paymentId,
|
||||
required this.amount,
|
||||
required this.isSpend,
|
||||
required this.hash,
|
||||
required this.key,
|
||||
required this.txInfo
|
||||
});
|
||||
Transaction.dummy(
|
||||
{required this.displayLabel,
|
||||
required this.description,
|
||||
required this.fee,
|
||||
required this.confirmations,
|
||||
required this.blockheight,
|
||||
required this.accountIndex,
|
||||
required this.addressIndex,
|
||||
required this.addressIndexList,
|
||||
required this.paymentId,
|
||||
required this.amount,
|
||||
required this.isSpend,
|
||||
required this.hash,
|
||||
required this.key,
|
||||
required this.txInfo});
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@ String getFilename() => wownero.Wallet_filename(wptr!);
|
||||
|
||||
String getSeed() {
|
||||
// monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
|
||||
final cakepolyseed =
|
||||
wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
|
||||
final cakepolyseed = wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
|
||||
final cakepassphrase = getPassphrase();
|
||||
|
||||
final weirdPolyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase);
|
||||
@@ -65,11 +64,21 @@ String getSeedLegacy(String? language) {
|
||||
final cakepassphrase = getPassphrase();
|
||||
var legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
|
||||
switch (language) {
|
||||
case "Chinese (Traditional)": language = "Chinese (simplified)"; break;
|
||||
case "Chinese (Simplified)": language = "Chinese (simplified)"; break;
|
||||
case "Korean": language = "English"; break;
|
||||
case "Czech": language = "English"; break;
|
||||
case "Japanese": language = "English"; break;
|
||||
case "Chinese (Traditional)":
|
||||
language = "Chinese (simplified)";
|
||||
break;
|
||||
case "Chinese (Simplified)":
|
||||
language = "Chinese (simplified)";
|
||||
break;
|
||||
case "Korean":
|
||||
language = "English";
|
||||
break;
|
||||
case "Czech":
|
||||
language = "English";
|
||||
break;
|
||||
case "Japanese":
|
||||
language = "English";
|
||||
break;
|
||||
}
|
||||
if (wownero.Wallet_status(wptr!) != 0) {
|
||||
wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
|
||||
@@ -84,6 +93,7 @@ String getSeedLegacy(String? language) {
|
||||
}
|
||||
return legacy;
|
||||
}
|
||||
|
||||
Map<int, Map<int, Map<int, String>>> addressCache = {};
|
||||
|
||||
String getPassphrase() {
|
||||
@@ -91,14 +101,14 @@ String getPassphrase() {
|
||||
}
|
||||
|
||||
String getAddress({int accountIndex = 0, int addressIndex = 1}) {
|
||||
while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) {
|
||||
while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex) - 1 < addressIndex) {
|
||||
printV("adding subaddress");
|
||||
wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex);
|
||||
}
|
||||
addressCache[wptr!.address] ??= {};
|
||||
addressCache[wptr!.address]![accountIndex] ??= {};
|
||||
addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!,
|
||||
accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
addressCache[wptr!.address]![accountIndex]![addressIndex] ??=
|
||||
wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
return addressCache[wptr!.address]![accountIndex]![addressIndex]!;
|
||||
}
|
||||
|
||||
@@ -106,10 +116,12 @@ int getFullBalance({int accountIndex = 0}) {
|
||||
if (wptr == null) return 0;
|
||||
return wownero.Wallet_balance(wptr!, accountIndex: accountIndex);
|
||||
}
|
||||
|
||||
int getUnlockedBalance({int accountIndex = 0}) {
|
||||
if (wptr == null) return 0;
|
||||
return wownero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex);
|
||||
}
|
||||
|
||||
int getCurrentHeight() {
|
||||
if (wptr == null) return 0;
|
||||
return wownero.Wallet_blockChainHeight(wptr!);
|
||||
@@ -181,8 +193,7 @@ Future<bool> connectToNode() async {
|
||||
}
|
||||
|
||||
void setRefreshFromBlockHeight({required int height}) =>
|
||||
wownero.Wallet_setRefreshFromBlockHeight(wptr!,
|
||||
refresh_from_block_height: height);
|
||||
wownero.Wallet_setRefreshFromBlockHeight(wptr!, refresh_from_block_height: height);
|
||||
|
||||
void setRecoveringFromSeed({required bool isRecovery}) =>
|
||||
wownero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery);
|
||||
@@ -257,8 +268,7 @@ class SyncListener {
|
||||
_cachedBlockchainHeight = 0;
|
||||
_lastKnownBlockHeight = 0;
|
||||
_initialSyncHeight = 0;
|
||||
_updateSyncInfoTimer ??=
|
||||
Timer.periodic(Duration(milliseconds: 1200), (_) async {
|
||||
_updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async {
|
||||
if (isNewTransactionExist()) {
|
||||
onNewTransaction();
|
||||
}
|
||||
@@ -297,8 +307,8 @@ class SyncListener {
|
||||
void stop() => _updateSyncInfoTimer?.cancel();
|
||||
}
|
||||
|
||||
SyncListener setListeners(void Function(int, int, double) onNewBlock,
|
||||
void Function() onNewTransaction) {
|
||||
SyncListener setListeners(
|
||||
void Function(int, int, double) onNewBlock, void Function() onNewTransaction) {
|
||||
final listener = SyncListener(onNewBlock, onNewTransaction);
|
||||
// setListenerNative();
|
||||
return listener;
|
||||
@@ -360,8 +370,7 @@ String getSubaddressLabel(int accountIndex, int addressIndex) {
|
||||
accountIndex: accountIndex, addressIndex: addressIndex);
|
||||
}
|
||||
|
||||
Future setTrustedDaemon(bool trusted) async =>
|
||||
wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted);
|
||||
Future setTrustedDaemon(bool trusted) async => wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted);
|
||||
|
||||
Future<bool> trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!);
|
||||
|
||||
@@ -370,7 +379,8 @@ String signMessage(String message, {String address = ""}) {
|
||||
}
|
||||
|
||||
bool verifyMessage(String message, String address, String signature) {
|
||||
return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature);
|
||||
return wownero.Wallet_verifySignedMessage(wptr!,
|
||||
message: message, address: address, signature: signature);
|
||||
}
|
||||
|
||||
Map<String, List<int>> debugCallLength() => wownero.debugCallLength;
|
||||
Map<String, List<int>> debugCallLength() => wownero.debugCallLength;
|
||||
|
||||
@@ -34,15 +34,18 @@ void checkIfMoneroCIsFine() {
|
||||
final dartCsExp = wownero.wallet2_api_c_exp_sha256;
|
||||
|
||||
if (cppCsCpp != dartCsCpp) {
|
||||
throw MoneroCException("monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'");
|
||||
throw MoneroCException(
|
||||
"monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'");
|
||||
}
|
||||
|
||||
if (cppCsH != dartCsH) {
|
||||
throw MoneroCException("monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'");
|
||||
throw MoneroCException(
|
||||
"monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'");
|
||||
}
|
||||
|
||||
if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) {
|
||||
throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'");
|
||||
throw MoneroCException(
|
||||
"monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,10 +136,10 @@ void restoreWalletFromSeedSync(
|
||||
}
|
||||
|
||||
wptr = newWptr;
|
||||
|
||||
|
||||
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
|
||||
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
|
||||
|
||||
|
||||
openedWalletsByPath[path] = wptr!;
|
||||
|
||||
store();
|
||||
@@ -153,29 +156,27 @@ void restoreWalletFromKeysSync(
|
||||
int restoreHeight = 0}) {
|
||||
txhistory = null;
|
||||
var newWptr = (spendKey != "")
|
||||
? wownero.WalletManager_createDeterministicWalletFromSpendKey(
|
||||
wmPtr,
|
||||
path: path,
|
||||
password: password,
|
||||
language: language,
|
||||
spendKeyString: spendKey,
|
||||
newWallet: true, // TODO(mrcyjanek): safe to remove
|
||||
restoreHeight: restoreHeight)
|
||||
: wownero.WalletManager_createWalletFromKeys(
|
||||
wmPtr,
|
||||
path: path,
|
||||
password: password,
|
||||
restoreHeight: restoreHeight,
|
||||
addressString: address,
|
||||
viewKeyString: viewKey,
|
||||
spendKeyString: spendKey,
|
||||
nettype: 0,
|
||||
);
|
||||
? wownero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr,
|
||||
path: path,
|
||||
password: password,
|
||||
language: language,
|
||||
spendKeyString: spendKey,
|
||||
newWallet: true, // TODO(mrcyjanek): safe to remove
|
||||
restoreHeight: restoreHeight)
|
||||
: wownero.WalletManager_createWalletFromKeys(
|
||||
wmPtr,
|
||||
path: path,
|
||||
password: password,
|
||||
restoreHeight: restoreHeight,
|
||||
addressString: address,
|
||||
viewKeyString: viewKey,
|
||||
spendKeyString: spendKey,
|
||||
nettype: 0,
|
||||
);
|
||||
|
||||
final status = wownero.Wallet_status(newWptr);
|
||||
if (status != 0) {
|
||||
throw WalletRestoreFromKeysException(
|
||||
message: wownero.Wallet_errorString(newWptr));
|
||||
throw WalletRestoreFromKeysException(message: wownero.Wallet_errorString(newWptr));
|
||||
}
|
||||
// CW-712 - Try to restore deterministic wallet first, if the view key doesn't
|
||||
// match the view key provided
|
||||
@@ -184,7 +185,7 @@ void restoreWalletFromKeysSync(
|
||||
if (viewKey != viewKeyRestored && viewKey != "") {
|
||||
wownero.WalletManager_closeWallet(wmPtr, newWptr, false);
|
||||
File(path).deleteSync();
|
||||
File(path+".keys").deleteSync();
|
||||
File(path + ".keys").deleteSync();
|
||||
newWptr = wownero.WalletManager_createWalletFromKeys(
|
||||
wmPtr,
|
||||
path: path,
|
||||
@@ -197,8 +198,7 @@ void restoreWalletFromKeysSync(
|
||||
);
|
||||
final status = wownero.Wallet_status(newWptr);
|
||||
if (status != 0) {
|
||||
throw WalletRestoreFromKeysException(
|
||||
message: wownero.Wallet_errorString(newWptr));
|
||||
throw WalletRestoreFromKeysException(message: wownero.Wallet_errorString(newWptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,8 +207,6 @@ void restoreWalletFromKeysSync(
|
||||
openedWalletsByPath[path] = wptr!;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// English only, because normalization.
|
||||
void restoreWalletFromPolyseedWithOffset(
|
||||
{required String path,
|
||||
@@ -217,7 +215,6 @@ void restoreWalletFromPolyseedWithOffset(
|
||||
required String seedOffset,
|
||||
required String language,
|
||||
int nettype = 0}) {
|
||||
|
||||
txhistory = null;
|
||||
final newWptr = wownero.WalletManager_createWalletFromPolyseed(
|
||||
wmPtr,
|
||||
@@ -332,8 +329,7 @@ String _lastOpenedWallet = "";
|
||||
|
||||
Map<String, wownero.wallet> openedWalletsByPath = {};
|
||||
|
||||
void loadWallet(
|
||||
{required String path, required String password, int nettype = 0}) {
|
||||
void loadWallet({required String path, required String password, int nettype = 0}) {
|
||||
if (openedWalletsByPath[path] != null) {
|
||||
txhistory = null;
|
||||
wptr = openedWalletsByPath[path]!;
|
||||
@@ -347,8 +343,7 @@ void loadWallet(
|
||||
});
|
||||
}
|
||||
txhistory = null;
|
||||
final newWptr = wownero.WalletManager_openWallet(wmPtr,
|
||||
path: path, password: password);
|
||||
final newWptr = wownero.WalletManager_openWallet(wmPtr, path: path, password: password);
|
||||
|
||||
_lastOpenedWallet = path;
|
||||
final status = wownero.Wallet_status(newWptr);
|
||||
@@ -379,7 +374,11 @@ void _restoreFromSeed(Map<String, dynamic> args) {
|
||||
final restoreHeight = args['restoreHeight'] as int;
|
||||
|
||||
restoreWalletFromSeedSync(
|
||||
path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight);
|
||||
path: path,
|
||||
password: password,
|
||||
passphrase: passphrase,
|
||||
seed: seed,
|
||||
restoreHeight: restoreHeight);
|
||||
}
|
||||
|
||||
void _restoreFromKeys(Map<String, dynamic> args) {
|
||||
@@ -418,19 +417,15 @@ void _restoreFromSpendKey(Map<String, dynamic> args) {
|
||||
spendKey: spendKey);
|
||||
}
|
||||
|
||||
Future<void> _openWallet(Map<String, String> args) async => loadWallet(
|
||||
path: args['path'] as String, password: args['password'] as String);
|
||||
Future<void> _openWallet(Map<String, String> args) async =>
|
||||
loadWallet(path: args['path'] as String, password: args['password'] as String);
|
||||
|
||||
Future<bool> _isWalletExist(String path) async => isWalletExistSync(path: path);
|
||||
|
||||
void openWallet(
|
||||
{required String path,
|
||||
required String password,
|
||||
int nettype = 0}) async =>
|
||||
void openWallet({required String path, required String password, int nettype = 0}) async =>
|
||||
loadWallet(path: path, password: password, nettype: nettype);
|
||||
|
||||
Future<void> openWalletAsync(Map<String, String> args) async =>
|
||||
_openWallet(args);
|
||||
Future<void> openWalletAsync(Map<String, String> args) async => _openWallet(args);
|
||||
|
||||
Future<void> createWallet(
|
||||
{required String path,
|
||||
|
||||
@@ -3,4 +3,4 @@ class WowneroOutput {
|
||||
|
||||
final String address;
|
||||
final String amount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'cw_wownero_platform_interface.dart';
|
||||
|
||||
class CwWownero {
|
||||
|
||||
@@ -5,4 +5,4 @@ class WowneroTransactionCreationException implements Exception {
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ class WowneroTransactionNoInputsException implements Exception {
|
||||
int inputsSize;
|
||||
|
||||
@override
|
||||
String toString() => 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control';
|
||||
String toString() =>
|
||||
'Not enough inputs ($inputsSize) selected. Please select more under Coin Control';
|
||||
}
|
||||
|
||||
@@ -1627,4 +1627,4 @@ class ChineseSimplifiedMnemonics {
|
||||
"浙",
|
||||
"貌"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1627,4 +1627,4 @@ class DutchMnemonics {
|
||||
"zwijmel",
|
||||
"zworen"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1629
-1629
File diff suppressed because it is too large
Load Diff
@@ -1627,4 +1627,4 @@ class GermanMnemonics {
|
||||
"Zweck",
|
||||
"Zyklop"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1629
-1629
File diff suppressed because it is too large
Load Diff
@@ -1627,4 +1627,4 @@ class JapaneseMnemonics {
|
||||
"びじゅつかん",
|
||||
"ひしょ"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1627,4 +1627,4 @@ class PortugueseMnemonics {
|
||||
"zenite",
|
||||
"zumbi"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1627,4 +1627,4 @@ class RussianMnemonics {
|
||||
"ячейка",
|
||||
"ящик"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1627,4 +1627,4 @@ class SpanishMnemonics {
|
||||
"ritmo",
|
||||
"rito"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +31,9 @@ String mnemonicDecode(String seed) {
|
||||
wlist.removeLast();
|
||||
|
||||
for (var i = 0; i < wlist.length; i += 3) {
|
||||
final w1 =
|
||||
tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength));
|
||||
final w2 = tructWords(englistWordSet)
|
||||
.indexOf(wlist[i + 1].substring(0, prefixLength));
|
||||
final w3 = tructWords(englistWordSet)
|
||||
.indexOf(wlist[i + 2].substring(0, prefixLength));
|
||||
final w1 = tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength));
|
||||
final w2 = tructWords(englistWordSet).indexOf(wlist[i + 1].substring(0, prefixLength));
|
||||
final w3 = tructWords(englistWordSet).indexOf(wlist[i + 2].substring(0, prefixLength));
|
||||
|
||||
if (w1 == -1 || w2 == -1 || w3 == -1) {
|
||||
printV("invalid word in mnemonic");
|
||||
|
||||
@@ -48,7 +48,7 @@ class PendingWowneroTransaction with PendingTransaction {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> commitUR() {
|
||||
throw UnimplementedError();
|
||||
|
||||
@@ -56,14 +56,13 @@ abstract class WowneroSubaddressListBase with Store {
|
||||
final address = s.address;
|
||||
final label = s.label;
|
||||
final id = s.addressIndex;
|
||||
final hasDefaultAddressName =
|
||||
label.toLowerCase() == 'Primary account'.toLowerCase() ||
|
||||
label.toLowerCase() == 'Untitled account'.toLowerCase();
|
||||
final hasDefaultAddressName = label.toLowerCase() == 'Primary account'.toLowerCase() ||
|
||||
label.toLowerCase() == 'Untitled account'.toLowerCase();
|
||||
final isPrimaryAddress = id == 0 && hasDefaultAddressName;
|
||||
return Subaddress(
|
||||
id: id,
|
||||
address: address,
|
||||
balance: (s.received/1e12).toStringAsFixed(6),
|
||||
balance: (s.received / 1e12).toStringAsFixed(6),
|
||||
txCount: s.txCount,
|
||||
label: isPrimaryAddress
|
||||
? 'Primary address'
|
||||
@@ -139,33 +138,27 @@ abstract class WowneroSubaddressListBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
return allAddresses
|
||||
.map((s) {
|
||||
final id = s.addressIndex;
|
||||
final address = s.address;
|
||||
final label = s.label;
|
||||
return Subaddress(
|
||||
id: id,
|
||||
address: address,
|
||||
balance: (s.received/1e12).toStringAsFixed(6),
|
||||
txCount: s.txCount,
|
||||
label: id == 0 &&
|
||||
label.toLowerCase() == 'Primary account'.toLowerCase()
|
||||
? 'Primary address'
|
||||
: label);
|
||||
})
|
||||
.toList();
|
||||
return allAddresses.map((s) {
|
||||
final id = s.addressIndex;
|
||||
final address = s.address;
|
||||
final label = s.label;
|
||||
return Subaddress(
|
||||
id: id,
|
||||
address: address,
|
||||
balance: (s.received / 1e12).toStringAsFixed(6),
|
||||
txCount: s.txCount,
|
||||
label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase()
|
||||
? 'Primary address'
|
||||
: label);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<bool> _newSubaddress({required int accountIndex, required String label}) async {
|
||||
await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
|
||||
|
||||
return subaddress_list
|
||||
.getAllSubaddresses()
|
||||
.where((s) {
|
||||
final address = s.address;
|
||||
return !_usedAddresses.contains(address);
|
||||
})
|
||||
.isNotEmpty;
|
||||
return subaddress_list.getAllSubaddresses().where((s) {
|
||||
final address = s.address;
|
||||
return !_usedAddresses.contains(address);
|
||||
}).isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import 'package:cw_wownero/wownero_transaction_info.dart';
|
||||
|
||||
part 'wownero_transaction_history.g.dart';
|
||||
|
||||
class WowneroTransactionHistory = WowneroTransactionHistoryBase
|
||||
with _$WowneroTransactionHistory;
|
||||
class WowneroTransactionHistory = WowneroTransactionHistoryBase with _$WowneroTransactionHistory;
|
||||
|
||||
abstract class WowneroTransactionHistoryBase
|
||||
extends TransactionHistoryBase<WowneroTransactionInfo> with Store {
|
||||
abstract class WowneroTransactionHistoryBase extends TransactionHistoryBase<WowneroTransactionInfo>
|
||||
with Store {
|
||||
WowneroTransactionHistoryBase() {
|
||||
transactions = ObservableMap<String, WowneroTransactionInfo>();
|
||||
}
|
||||
@@ -18,11 +17,9 @@ abstract class WowneroTransactionHistoryBase
|
||||
Future<void> save() async {}
|
||||
|
||||
@override
|
||||
void addOne(WowneroTransactionInfo transaction) =>
|
||||
transactions[transaction.id] = transaction;
|
||||
void addOne(WowneroTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, WowneroTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_wownero/api/transaction_history.dart';
|
||||
|
||||
class WowneroTransactionInfo extends TransactionInfo {
|
||||
WowneroTransactionInfo(this.txHash, this.height, this.direction, this.date,
|
||||
this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee,
|
||||
this.confirmations) :
|
||||
id = "${txHash}_${amount}_${accountIndex}_${addressIndex}";
|
||||
WowneroTransactionInfo(this.txHash, this.height, this.direction, this.date, this.isPending,
|
||||
this.amount, this.accountIndex, this.addressIndex, this.fee, this.confirmations)
|
||||
: id = "${txHash}_${amount}_${accountIndex}_${addressIndex}";
|
||||
|
||||
WowneroTransactionInfo.fromMap(Map<String, Object?> map)
|
||||
: id = "${map['hash']}_${map['amount']}_${map['accountIndex']}_${map['addressIndex']}",
|
||||
|
||||
@@ -54,7 +54,10 @@ abstract class WowneroWalletBase
|
||||
extends WalletBase<WowneroBalance, WowneroTransactionHistory, WowneroTransactionInfo>
|
||||
with Store {
|
||||
WowneroWalletBase(
|
||||
{required WalletInfo walletInfo, required DerivationInfo derivationInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo, required String password})
|
||||
{required WalletInfo walletInfo,
|
||||
required DerivationInfo derivationInfo,
|
||||
required Box<UnspentCoinsInfo> unspentCoinsInfo,
|
||||
required String password})
|
||||
: balance = ObservableMap<CryptoCurrency, WowneroBalance>.of({
|
||||
CryptoCurrency.wow: WowneroBalance(
|
||||
fullBalance:
|
||||
@@ -144,8 +147,8 @@ abstract class WowneroWalletBase
|
||||
passphrase: wownero_wallet.getPassphrase());
|
||||
|
||||
int? get restoreHeight =>
|
||||
transactionHistory.transactions.values.firstOrNull?.height ?? wownero.Wallet_getRefreshFromBlockHeight(wptr!);
|
||||
|
||||
transactionHistory.transactions.values.firstOrNull?.height ??
|
||||
wownero.Wallet_getRefreshFromBlockHeight(wptr!);
|
||||
|
||||
wownero_wallet.SyncListener? _listener;
|
||||
ReactionDisposer? _onAccountChangeReaction;
|
||||
@@ -635,8 +638,7 @@ abstract class WowneroWalletBase
|
||||
wownero_wallet.getSubaddressLabel(accountIndex, addressIndex);
|
||||
|
||||
Future<List<WowneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
|
||||
(await transaction_history
|
||||
.getAllTransactions())
|
||||
(await transaction_history.getAllTransactions())
|
||||
.map(
|
||||
(row) => WowneroTransactionInfo(
|
||||
row.hash,
|
||||
|
||||
@@ -34,25 +34,26 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
|
||||
|
||||
@override
|
||||
String get latestAddress {
|
||||
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
|
||||
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
var addressIndex = subaddress_list.numSubaddresses(account?.id ?? 0) - 1;
|
||||
var address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
|
||||
while (hiddenAddresses.contains(address)) {
|
||||
addressIndex++;
|
||||
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
@override
|
||||
String get addressForExchange {
|
||||
var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1;
|
||||
var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
var addressIndex = subaddress_list.numSubaddresses(account?.id ?? 0) - 1;
|
||||
var address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
|
||||
while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) {
|
||||
addressIndex++;
|
||||
address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex);
|
||||
address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
@observable
|
||||
Account? account;
|
||||
|
||||
@@ -69,7 +70,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
|
||||
@override
|
||||
Future<void> init() async {
|
||||
accountList.update();
|
||||
account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first;
|
||||
account = accountList.accounts.isEmpty
|
||||
? Account(id: 0, label: "Primary address")
|
||||
: accountList.accounts.first;
|
||||
updateSubaddressList(accountIndex: account?.id ?? 0);
|
||||
await updateAddressesInBox();
|
||||
}
|
||||
@@ -143,7 +146,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
|
||||
accountIndex: accountIndex,
|
||||
defaultLabel: defaultLabel,
|
||||
usedAddresses: usedAddresses.toList());
|
||||
subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last;
|
||||
subaddress = (subaddressList.subaddresses.isEmpty)
|
||||
? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0)
|
||||
: subaddressList.subaddresses.last;
|
||||
address = subaddress!.address;
|
||||
}
|
||||
|
||||
@@ -152,6 +157,5 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store {
|
||||
addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false;
|
||||
|
||||
@override
|
||||
PaymentURI getPaymentUri(String amount) =>
|
||||
WowneroURI(address: address, amount: amount);
|
||||
PaymentURI getPaymentUri(String amount) => WowneroURI(address: address, amount: amount);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ import 'package:monero/wownero.dart' as wownero;
|
||||
|
||||
class WowneroNewWalletCredentials extends WalletCredentials {
|
||||
WowneroNewWalletCredentials(
|
||||
{required String name, required this.language, required this.isPolyseed, this.passphrase, String? password})
|
||||
{required String name,
|
||||
required this.language,
|
||||
required this.isPolyseed,
|
||||
this.passphrase,
|
||||
String? password})
|
||||
: super(name: name, password: password);
|
||||
|
||||
final String language;
|
||||
@@ -33,7 +37,11 @@ class WowneroNewWalletCredentials extends WalletCredentials {
|
||||
|
||||
class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
WowneroRestoreWalletFromSeedCredentials(
|
||||
{required String name, required this.mnemonic, required this.passphrase, int height = 0, String? password})
|
||||
{required String name,
|
||||
required this.mnemonic,
|
||||
required this.passphrase,
|
||||
int height = 0,
|
||||
String? password})
|
||||
: super(name: name, password: password, height: height);
|
||||
|
||||
final String mnemonic;
|
||||
@@ -97,9 +105,15 @@ class WowneroWalletService extends WalletService<
|
||||
}
|
||||
|
||||
await wownero_wallet_manager.createWallet(
|
||||
path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??'');
|
||||
path: path,
|
||||
password: credentials.password!,
|
||||
language: credentials.language,
|
||||
passphrase: credentials.passphrase ?? '');
|
||||
final wallet = WowneroWallet(
|
||||
walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
|
||||
walletInfo: credentials.walletInfo!,
|
||||
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: credentials.password!);
|
||||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
@@ -138,7 +152,11 @@ class WowneroWalletService extends WalletService<
|
||||
throw Exception('Wallet not found');
|
||||
}
|
||||
|
||||
wallet = WowneroWallet(walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password);
|
||||
wallet = WowneroWallet(
|
||||
walletInfo: walletInfo,
|
||||
derivationInfo: await walletInfo.getDerivationInfo(),
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: password);
|
||||
throw WalletDeprecationException(seed: wallet.seed, curr: wallet.currency);
|
||||
|
||||
final isValid = wallet.walletAddresses.validate();
|
||||
@@ -224,7 +242,11 @@ class WowneroWalletService extends WalletService<
|
||||
if (currentWalletInfo == null) {
|
||||
throw Exception('Wallet not found');
|
||||
}
|
||||
final currentWallet = WowneroWallet(walletInfo: currentWalletInfo, derivationInfo: await currentWalletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password);
|
||||
final currentWallet = WowneroWallet(
|
||||
walletInfo: currentWalletInfo,
|
||||
derivationInfo: await currentWalletInfo.getDerivationInfo(),
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: password);
|
||||
|
||||
await currentWallet.renameWalletFiles(newName);
|
||||
|
||||
@@ -249,7 +271,10 @@ class WowneroWalletService extends WalletService<
|
||||
viewKey: credentials.viewKey,
|
||||
spendKey: credentials.spendKey);
|
||||
final wallet = WowneroWallet(
|
||||
walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
|
||||
walletInfo: credentials.walletInfo!,
|
||||
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: credentials.password!);
|
||||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
@@ -283,7 +308,10 @@ class WowneroWalletService extends WalletService<
|
||||
seed: credentials.mnemonic,
|
||||
restoreHeight: credentials.height!);
|
||||
final wallet = WowneroWallet(
|
||||
walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
|
||||
walletInfo: credentials.walletInfo!,
|
||||
derivationInfo: await credentials.walletInfo!.getDerivationInfo(),
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: credentials.password!);
|
||||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
@@ -303,7 +331,8 @@ class WowneroWalletService extends WalletService<
|
||||
final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
|
||||
|
||||
return _restoreFromPolyseed(
|
||||
path, credentials.password!, polyseed, credentials.walletInfo!, lang, passphrase: credentials.passphrase);
|
||||
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
|
||||
passphrase: credentials.passphrase);
|
||||
} catch (e) {
|
||||
// TODO: Implement Exception for wallet list service.
|
||||
printV('WowneroWalletsManager Error: $e');
|
||||
@@ -313,20 +342,19 @@ class WowneroWalletService extends WalletService<
|
||||
|
||||
Future<WowneroWallet> _restoreFromPolyseed(
|
||||
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
|
||||
{PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight, String? passphrase}) async {
|
||||
|
||||
|
||||
if (polyseed.isEncrypted == false &&
|
||||
(passphrase??'') != "") {
|
||||
{PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO,
|
||||
int? overrideHeight,
|
||||
String? passphrase}) async {
|
||||
if (polyseed.isEncrypted == false && (passphrase ?? '') != "") {
|
||||
// Fallback to the different passphrase offset method, when a passphrase
|
||||
// was provided but the polyseed is not encrypted.
|
||||
wownero_wallet_manager.restoreWalletFromPolyseedWithOffset(
|
||||
path: path,
|
||||
password: password,
|
||||
seed: polyseed.encode(lang, coin),
|
||||
seedOffset: passphrase??'',
|
||||
language: "English");
|
||||
|
||||
path: path,
|
||||
password: password,
|
||||
seed: polyseed.encode(lang, coin),
|
||||
seedOffset: passphrase ?? '',
|
||||
language: "English");
|
||||
|
||||
final wallet = WowneroWallet(
|
||||
walletInfo: walletInfo,
|
||||
derivationInfo: await walletInfo.getDerivationInfo(),
|
||||
@@ -357,9 +385,13 @@ class WowneroWalletService extends WalletService<
|
||||
spendKey: spendKey);
|
||||
|
||||
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
|
||||
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??'');
|
||||
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase ?? '');
|
||||
|
||||
final wallet = WowneroWallet(walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password);
|
||||
final wallet = WowneroWallet(
|
||||
walletInfo: walletInfo,
|
||||
derivationInfo: await walletInfo.getDerivationInfo(),
|
||||
unspentCoinsInfo: unspentCoinsInfoSource,
|
||||
password: password);
|
||||
await wallet.init();
|
||||
|
||||
return wallet;
|
||||
|
||||
@@ -8,18 +8,20 @@ import 'package:cake_wallet/view_model/link_view_model.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockAppStore extends Mock implements AppStore{}
|
||||
class MockAuthService extends Mock implements AuthService{}
|
||||
class MockSettingsStore extends Mock implements SettingsStore {}
|
||||
class MockAuthenticationStore extends Mock implements AuthenticationStore{}
|
||||
class MockWalletListStore extends Mock implements WalletListStore{}
|
||||
class MockAppStore extends Mock implements AppStore {}
|
||||
|
||||
class MockAuthService extends Mock implements AuthService {}
|
||||
|
||||
class MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
class MockLinkViewModel extends Mock implements LinkViewModel {}
|
||||
class MockAuthenticationStore extends Mock implements AuthenticationStore {}
|
||||
|
||||
class MockWalletListStore extends Mock implements WalletListStore {}
|
||||
|
||||
class MockLinkViewModel extends Mock implements LinkViewModel {}
|
||||
|
||||
class MockHiveInterface extends Mock implements HiveInterface {}
|
||||
|
||||
class MockHiveBox extends Mock implements Box<dynamic> {}
|
||||
|
||||
class MockSecureStorage extends Mock implements SecureStorage{}
|
||||
class MockSecureStorage extends Mock implements SecureStorage {}
|
||||
|
||||
@@ -34,7 +34,7 @@ class DisclaimerPageRobot {
|
||||
|
||||
Future<void> tapAcceptButton() async {
|
||||
await commonTestCases.tapItemByKey('disclaimer_accept_button_key');
|
||||
|
||||
|
||||
await commonTestCases.defaultSleepTime();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class RestoreFromSeedOrKeysPageRobot {
|
||||
|
||||
Future<void> enterSeedPhraseForWalletRestore(String text) async {
|
||||
ValidatableAnnotatedEditableTextState seedTextState =
|
||||
await tester.state(find.byType(ValidatableAnnotatedEditableText));
|
||||
await tester.state(find.byType(ValidatableAnnotatedEditableText));
|
||||
|
||||
seedTextState.widget.controller.text = text;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -21,7 +21,7 @@ class SecurityAndBackupPageRobot {
|
||||
|
||||
Future<void> navigateToShowKeysPage() async {
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
|
||||
|
||||
await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key');
|
||||
await tester.pumpAndSettle(Duration(milliseconds: 1000));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ void main() {
|
||||
(tester) async {
|
||||
// Store the original FlutterError.onError handler
|
||||
final originalOnError = FlutterError.onError;
|
||||
|
||||
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
debugPrint('FlutterError caught: ${details.exception}');
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ void main() {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
debugPrint('FlutterError caught: ${details.exception}');
|
||||
};
|
||||
|
||||
|
||||
commonTestFlows = CommonTestFlows(tester);
|
||||
exchangePageRobot = ExchangePageRobot(tester);
|
||||
dashboardPageRobot = DashboardPageRobot(tester);
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dar
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart';
|
||||
import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
|
||||
@@ -147,24 +148,38 @@ class CakePayBuyCardPage extends BasePage {
|
||||
),
|
||||
bottomCardChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: card.denominationItems.isNotEmpty
|
||||
? DenominationsAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
denominations: card.denominationItems,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
quantityFieldFocus: _quantityFieldFocus,
|
||||
quantityController: _quantityController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel)
|
||||
: EnterAmountWidget(
|
||||
minValue: card.minValue ?? '-',
|
||||
maxValue: card.maxValue ?? '-',
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
|
||||
child: Column(children: [
|
||||
if (card.prepaidRange.isNotEmpty)
|
||||
PrepaidRangeAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
prepaidRanges: card.prepaidRange,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
)
|
||||
else if (card.denominationItems.isNotEmpty)
|
||||
DenominationsAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
denominations: card.denominationItems,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
quantityFieldFocus: _quantityFieldFocus,
|
||||
quantityController: _quantityController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
|
||||
)
|
||||
else
|
||||
EnterAmountWidget(
|
||||
minValue: card.minValue ?? '-',
|
||||
maxValue: card.maxValue ?? '-',
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
),
|
||||
]))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import 'package:cake_wallet/core/utilities.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
|
||||
enum CakePayCardType {
|
||||
prepaid('Prepaid Cards'),
|
||||
gift('gift'),
|
||||
onDemand('on_demand'),
|
||||
custom('custom');
|
||||
|
||||
const CakePayCardType(this.apiValue);
|
||||
|
||||
final String apiValue;
|
||||
}
|
||||
|
||||
class CakePayCard {
|
||||
final int id;
|
||||
final String name;
|
||||
final CakePayCardType? type;
|
||||
final String? description;
|
||||
final String? termsAndConditions;
|
||||
final String? howToUse;
|
||||
@@ -15,26 +28,77 @@ class CakePayCard {
|
||||
final String? minValue;
|
||||
final String? maxValue;
|
||||
final List<Denomination> denominationItems;
|
||||
final List<PrepaidRange> prepaidRange;
|
||||
|
||||
CakePayCard({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.termsAndConditions,
|
||||
this.howToUse,
|
||||
this.expiryAndValidity,
|
||||
this.cardImageUrl,
|
||||
this.country,
|
||||
required this.fiatCurrency,
|
||||
this.minValueUsd,
|
||||
this.maxValueUsd,
|
||||
this.minValue,
|
||||
this.maxValue,
|
||||
CakePayCard(
|
||||
{required this.id,
|
||||
required this.name,
|
||||
this.type,
|
||||
this.description,
|
||||
this.termsAndConditions,
|
||||
this.howToUse,
|
||||
this.expiryAndValidity,
|
||||
this.cardImageUrl,
|
||||
this.country,
|
||||
required this.fiatCurrency,
|
||||
this.minValueUsd,
|
||||
this.maxValueUsd,
|
||||
this.minValue,
|
||||
this.maxValue,
|
||||
List<Denomination>? denominationItems,
|
||||
List<PrepaidRange>? prepaidRange})
|
||||
: denominationItems = denominationItems ?? const [],
|
||||
prepaidRange = prepaidRange ?? const [];
|
||||
|
||||
CakePayCard copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
CakePayCardType? type,
|
||||
String? description,
|
||||
String? termsAndConditions,
|
||||
String? howToUse,
|
||||
String? expiryAndValidity,
|
||||
String? cardImageUrl,
|
||||
String? country,
|
||||
FiatCurrency? fiatCurrency,
|
||||
String? minValueUsd,
|
||||
String? maxValueUsd,
|
||||
String? minValue,
|
||||
String? maxValue,
|
||||
List<Denomination>? denominationItems,
|
||||
}) : denominationItems = denominationItems ?? const [];
|
||||
List<PrepaidRange>? prepaidRange,
|
||||
}) {
|
||||
return CakePayCard(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
description: description ?? this.description,
|
||||
termsAndConditions: termsAndConditions ?? this.termsAndConditions,
|
||||
howToUse: howToUse ?? this.howToUse,
|
||||
expiryAndValidity: expiryAndValidity ?? this.expiryAndValidity,
|
||||
cardImageUrl: cardImageUrl ?? this.cardImageUrl,
|
||||
country: country ?? this.country,
|
||||
fiatCurrency: fiatCurrency ?? this.fiatCurrency,
|
||||
minValueUsd: minValueUsd ?? this.minValueUsd,
|
||||
maxValueUsd: maxValueUsd ?? this.maxValueUsd,
|
||||
minValue: minValue ?? this.minValue,
|
||||
maxValue: maxValue ?? this.maxValue,
|
||||
denominationItems: denominationItems ?? this.denominationItems,
|
||||
prepaidRange: prepaidRange ?? this.prepaidRange,
|
||||
);
|
||||
}
|
||||
|
||||
static CakePayCardType? cakePayCardTypeFromApi(String? value) {
|
||||
if (value == null) return null;
|
||||
final normalized = value.trim().toLowerCase();
|
||||
return CakePayCardType.values.firstWhereOrNull(
|
||||
(e) => e.apiValue.toLowerCase() == normalized,
|
||||
);
|
||||
}
|
||||
|
||||
factory CakePayCard.fromJson(Map<String, dynamic> json) {
|
||||
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
|
||||
final typeString = json['type'] as String? ?? '';
|
||||
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
|
||||
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
|
||||
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
|
||||
@@ -63,9 +127,12 @@ class CakePayCard {
|
||||
}
|
||||
}
|
||||
|
||||
final cakePayCardType = cakePayCardTypeFromApi(typeString);
|
||||
|
||||
return CakePayCard(
|
||||
id: json['id'] as int? ?? 0,
|
||||
name: name,
|
||||
type: cakePayCardType,
|
||||
description: description,
|
||||
termsAndConditions: termsAndConditions,
|
||||
howToUse: howToUse,
|
||||
@@ -103,12 +170,6 @@ class Denomination {
|
||||
this.usdValue,
|
||||
});
|
||||
|
||||
static double? _toDouble(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString());
|
||||
}
|
||||
|
||||
factory Denomination.fromJson(Map<String, dynamic> json) {
|
||||
return Denomination(
|
||||
value: _toDouble(json['value']) ?? 0,
|
||||
@@ -117,3 +178,29 @@ class Denomination {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PrepaidRange {
|
||||
final double minValue;
|
||||
final double maxValue;
|
||||
final String rangeId;
|
||||
|
||||
PrepaidRange({
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.rangeId,
|
||||
});
|
||||
|
||||
factory PrepaidRange.fromCard(CakePayCard card) {
|
||||
return PrepaidRange(
|
||||
minValue: _toDouble(card.minValue) ?? 0,
|
||||
maxValue: _toDouble(card.maxValue) ?? 0,
|
||||
rangeId: card.id.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double? _toDouble(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString());
|
||||
}
|
||||
|
||||
@@ -31,9 +31,26 @@ class CakePayVendor {
|
||||
|
||||
if (cardsJson != null && cardsJson.isNotEmpty) {
|
||||
try {
|
||||
cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
|
||||
return Country.normalizeName(card['country'] as String) == country;
|
||||
}) as Map<String, dynamic>);
|
||||
final cards = cardsJson
|
||||
.map((cardJson) => CakePayCard.fromJson(cardJson as Map<String, dynamic>))
|
||||
.toList();
|
||||
final cardsForSelectedCountry =
|
||||
cards.where((card) => Country.normalizeName(card.country ?? '') == country).toList();
|
||||
|
||||
if (cardsForSelectedCountry.isNotEmpty) {
|
||||
final isPrepaid =
|
||||
cardsForSelectedCountry.every((card) => card.type == CakePayCardType.prepaid);
|
||||
|
||||
if (isPrepaid) {
|
||||
final firstCard = cardsForSelectedCountry.first;
|
||||
final prepaidRanges = cardsForSelectedCountry.map(PrepaidRange.fromCard).toList()
|
||||
..sort((a, b) => a.minValue.compareTo(b.minValue));
|
||||
|
||||
cardForVendor = firstCard.copyWith(prepaidRange: prepaidRanges);
|
||||
} else {
|
||||
cardForVendor = cardsForSelectedCountry.first;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error parsing card for vendor: $e');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
class CakePayApi {
|
||||
static const testBaseUri = FeatureFlag.hasDevOptions;
|
||||
static const testBaseUri = false; //FeatureFlag.hasDevOptions;
|
||||
|
||||
static const baseTestCakePayUri = 'api-stg.cakepay.com';
|
||||
static const baseProdCakePayUri = 'api-prod.cakepay.com';
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enter_amount_widget.dart';
|
||||
|
||||
class PrepaidRangeAmountWidget extends StatefulWidget {
|
||||
const PrepaidRangeAmountWidget({
|
||||
required this.fiatCurrency,
|
||||
required this.prepaidRanges,
|
||||
required this.amountFieldFocus,
|
||||
required this.amountController,
|
||||
required this.cakePayBuyCardViewModel,
|
||||
required this.onAmountChanged,
|
||||
});
|
||||
|
||||
final String fiatCurrency;
|
||||
final List<PrepaidRange> prepaidRanges;
|
||||
final FocusNode amountFieldFocus;
|
||||
final TextEditingController amountController;
|
||||
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
|
||||
final Function(String) onAmountChanged;
|
||||
|
||||
@override
|
||||
State<PrepaidRangeAmountWidget> createState() => _PrepaidRangeAmountWidgetState();
|
||||
}
|
||||
|
||||
class _PrepaidRangeAmountWidgetState extends State<PrepaidRangeAmountWidget> {
|
||||
late PrepaidRange _selectedRange;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedRange = widget.prepaidRanges.first;
|
||||
_applySelectedRange(forceAmountUpdate: widget.amountController.text.isEmpty);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Select range:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<PrepaidRange>(
|
||||
value: _selectedRange,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(40),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
items: widget.prepaidRanges
|
||||
.map(
|
||||
(range) => DropdownMenuItem<PrepaidRange>(
|
||||
value: range,
|
||||
child: Text(_rangeLabel(range)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (range) {
|
||||
if (range == null) return;
|
||||
setState(() => _selectedRange = range);
|
||||
widget.cakePayBuyCardViewModel.onPrepaidRangeChanged(range);
|
||||
_applySelectedRange(forceAmountUpdate: true);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
EnterAmountWidget(
|
||||
minValue: _formatAmount(_selectedRange.minValue),
|
||||
maxValue: _formatAmount(_selectedRange.maxValue),
|
||||
fiatCurrency: widget.fiatCurrency,
|
||||
amountFieldFocus: widget.amountFieldFocus,
|
||||
amountController: widget.amountController,
|
||||
onAmountChanged: (value) {
|
||||
widget.onAmountChanged(value);
|
||||
widget.cakePayBuyCardViewModel.selectedDenomination = (
|
||||
value.replaceAll(',', '.'),
|
||||
int.tryParse(_selectedRange.rangeId),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _applySelectedRange({required bool forceAmountUpdate}) {
|
||||
final currentAmount = double.tryParse(widget.amountController.text);
|
||||
final isCurrentAmountInRange = currentAmount != null &&
|
||||
currentAmount >= _selectedRange.minValue &&
|
||||
currentAmount <= _selectedRange.maxValue;
|
||||
|
||||
final nextAmount = forceAmountUpdate || !isCurrentAmountInRange
|
||||
? _formatAmount(_selectedRange.minValue)
|
||||
: widget.amountController.text;
|
||||
|
||||
widget.amountController.text = nextAmount;
|
||||
widget.onAmountChanged(nextAmount);
|
||||
widget.cakePayBuyCardViewModel.selectedDenomination = (
|
||||
nextAmount.replaceAll(',', '.'),
|
||||
int.tryParse(_selectedRange.rangeId),
|
||||
);
|
||||
}
|
||||
|
||||
String _rangeLabel(PrepaidRange range) {
|
||||
return '${widget.fiatCurrency} ${_formatAmount(range.minValue)} - ${_formatAmount(range.maxValue)}';
|
||||
}
|
||||
|
||||
String _formatAmount(double value) {
|
||||
return value % 1 == 0 ? value.toInt().toString() : value.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
@@ -12,32 +12,46 @@ class RoundedOverlayCards extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
child: Container(
|
||||
height: screenHeight * 0.50,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
borderRadius: constBorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
Flexible(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25.0),
|
||||
bottomRight: Radius.circular(25.0),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
height: screenHeight * 0.38,
|
||||
width: double.infinity,
|
||||
child: topCardChild)),
|
||||
bottomCardChild,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: screenHeight * 0.38,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: topCardChild,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: bottomCardChild,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+7
-4
@@ -423,16 +423,19 @@ Future<void> setup({
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
throw Exception(
|
||||
"This should not have happened, because airgapped Wallets don't need View Models");
|
||||
"This should not have happened, because airgapped Wallets don't need View Models",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
getIt.registerLazySingleton(() => LedgerViewModel());
|
||||
getIt.registerLazySingleton(() => LedgerViewModel(getIt<AppStore>()));
|
||||
|
||||
getIt.registerLazySingleton(() => BitboxViewModel());
|
||||
getIt.registerLazySingleton(BitboxViewModel.new);
|
||||
|
||||
getIt.registerLazySingleton(
|
||||
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"));
|
||||
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
|
||||
);
|
||||
|
||||
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
|
||||
|
||||
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
|
||||
|
||||
@@ -166,12 +166,15 @@ class CWEVM extends EVM {
|
||||
amount: amount,
|
||||
fee: fee,
|
||||
tokenSymbol: tokenSymbol,
|
||||
exponent: exponent,
|
||||
direction: direction,
|
||||
isPending: isPending,
|
||||
date: date,
|
||||
confirmations: confirmations,
|
||||
to: to,
|
||||
from: from,
|
||||
evmSignatureName: evmSignatureName,
|
||||
contractAddress: contractAddress,
|
||||
chainId: chainId);
|
||||
|
||||
@override
|
||||
|
||||
@@ -433,6 +433,9 @@ class CWMonero extends Monero {
|
||||
@override
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection;
|
||||
|
||||
@override
|
||||
bool hasGlobalLedgerConnection() => gLedger != null;
|
||||
|
||||
@override
|
||||
String? getLastLedgerCommand() => latestLedgerCommand;
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
final oldTabLength = tabs.length;
|
||||
final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled ||
|
||||
(widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled);
|
||||
final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled;
|
||||
final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type,
|
||||
chainId: widget.dashboardViewModel.wallet.chainId);
|
||||
tabs = [
|
||||
@@ -58,13 +59,14 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
|
||||
AssetsSection(
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
),
|
||||
hasAssetsButton ?
|
||||
AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
|
||||
() {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.homeSettings,
|
||||
arguments: widget.dashboardViewModel.balanceViewModel,
|
||||
);
|
||||
})),
|
||||
}) : null),
|
||||
AssetsHistorySectionTab(
|
||||
S.current.history,
|
||||
HistorySection(
|
||||
|
||||
@@ -12,7 +12,6 @@ class ModalTopBar extends StatelessWidget {
|
||||
this.onTrailingPressed = nothing,
|
||||
this.leadingIcon,
|
||||
this.trailingIcon,
|
||||
this.bottomText,
|
||||
this.padding,
|
||||
this.leadingWidget,
|
||||
this.trailingWidget}) {
|
||||
|
||||
@@ -75,6 +75,23 @@ void startAuthenticationStateChange(
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ledgerVM.suppressMoneroReconnectPrompt = true;
|
||||
|
||||
if (!monero!.hasGlobalLedgerConnection()) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "Unexpected Error",
|
||||
alertContent:
|
||||
"Unexpected Error while trying to connect to the device (has no global LedgerConnection)",
|
||||
buttonText: S.of(context).try_again,
|
||||
buttonAction: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
@@ -85,14 +102,17 @@ void startAuthenticationStateChange(
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
bool tryOpening = true;
|
||||
|
||||
var isConnected = false;
|
||||
var tryOpening = true;
|
||||
while (tryOpening) {
|
||||
try {
|
||||
await loadCurrentWallet();
|
||||
isConnected = true;
|
||||
tryOpening = false;
|
||||
} on Exception catch (e) {
|
||||
final ledgerErrorMessage = ledgerVM.interpretErrorCode(e.toString());
|
||||
if (ledgerErrorMessage != null) {
|
||||
if (ledgerErrorMessage != null && context.mounted) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
@@ -116,16 +136,17 @@ void startAuthenticationStateChange(
|
||||
}
|
||||
|
||||
getIt.get<BottomSheetService>().showNext();
|
||||
await navigatorKey.currentState!
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
if (isConnected) {
|
||||
ledgerVM.suppressMoneroReconnectPrompt = false;
|
||||
await navigatorKey.currentState!
|
||||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
},
|
||||
allowChangeWallet: true,
|
||||
),
|
||||
);
|
||||
|
||||
// await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice));
|
||||
} else {
|
||||
navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final linkViewModel = getIt.get<LinkViewModel>();
|
||||
|
||||
@@ -122,9 +122,13 @@ class CWSolana extends Solana {
|
||||
return CryptoCurrency.sol;
|
||||
}
|
||||
|
||||
return (wallet as SolanaWallet).splTokenCurrencies.firstWhere(
|
||||
(element) => transaction.amount.currency.symbol == element.symbol,
|
||||
);
|
||||
final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol);
|
||||
|
||||
if (token == null) {
|
||||
throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -401,6 +405,15 @@ class CWSolana extends Solana {
|
||||
}
|
||||
|
||||
await Future.wait(tokenChecks);
|
||||
|
||||
final discoveredMints = result.newTokens
|
||||
.where((item) => item.token.enabled)
|
||||
.map((item) => item.token.mintAddress)
|
||||
.toList();
|
||||
|
||||
if (discoveredMints.isNotEmpty) {
|
||||
await wallet.updateSPLTokenTransactions(specificMints: discoveredMints);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.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:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/main.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/base_page.dart";
|
||||
import "package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
|
||||
import "package:cake_wallet/src/widgets/primary_button.dart";
|
||||
import "package:cake_wallet/themes/core/material_base_theme.dart";
|
||||
import "package:cake_wallet/utils/responsive_layout_util.dart";
|
||||
import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.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:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel);
|
||||
|
||||
class ConnectDevicePageParams {
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
|
||||
ConnectDevicePageParams({
|
||||
const ConnectDevicePageParams({
|
||||
required this.walletType,
|
||||
required this.hardwareWalletType,
|
||||
required this.onConnectDevice,
|
||||
this.allowChangeWallet = false,
|
||||
this.isReconnect = true,
|
||||
});
|
||||
}
|
||||
|
||||
class ConnectDevicePage extends BasePage {
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
}
|
||||
|
||||
class ConnectDevicePage extends BasePage {
|
||||
ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
|
||||
: walletType = params.walletType,
|
||||
onConnectDevice = params.onConnectDevice,
|
||||
allowChangeWallet = params.allowChangeWallet,
|
||||
isReconnect = params.isReconnect;
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
|
||||
@override
|
||||
String get title => isReconnect
|
||||
@@ -60,31 +60,32 @@ class ConnectDevicePage extends BasePage {
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => PopScope(
|
||||
canPop: !isReconnect,
|
||||
child: ConnectDevicePageBody(
|
||||
walletType,
|
||||
onConnectDevice,
|
||||
allowChangeWallet,
|
||||
hardwareWalletVM,
|
||||
currentTheme,
|
||||
));
|
||||
canPop: !isReconnect,
|
||||
child: ConnectDevicePageBody(
|
||||
walletType,
|
||||
onConnectDevice,
|
||||
hardwareWalletVM,
|
||||
currentTheme,
|
||||
allowChangeWallet: allowChangeWallet,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ConnectDevicePageBody extends StatefulWidget {
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.hardwareWalletVM,
|
||||
this.currentTheme, {
|
||||
this.allowChangeWallet = false,
|
||||
});
|
||||
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
final MaterialThemeBase currentTheme;
|
||||
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.allowChangeWallet,
|
||||
this.hardwareWalletVM,
|
||||
this.currentTheme,
|
||||
);
|
||||
|
||||
@override
|
||||
ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState();
|
||||
}
|
||||
@@ -95,10 +96,10 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
|
||||
List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
|
||||
|
||||
late Timer? _usbRefreshTimer = null;
|
||||
late Timer? _bleRefreshTimer = null;
|
||||
late Timer? _bleStateTimer = null;
|
||||
late StreamSubscription<HardwareWalletDevice>? _bleRefresh = null;
|
||||
Timer? _usbRefreshTimer;
|
||||
Timer? _bleRefreshTimer;
|
||||
Timer? _bleStateTimer;
|
||||
StreamSubscription<HardwareWalletDevice>? _bleRefresh;
|
||||
|
||||
bool longWait = false;
|
||||
Timer? _longWaitTimer;
|
||||
@@ -107,19 +108,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_bleStateTimer =
|
||||
Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState());
|
||||
_bleStateTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => widget.hardwareWalletVM.updateBleState(),
|
||||
);
|
||||
|
||||
_bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices());
|
||||
_bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices());
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
_usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
|
||||
_usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices());
|
||||
}
|
||||
|
||||
if (widget.hardwareWalletVM.hasBluetooth) {
|
||||
_longWaitTimer = Timer(Duration(seconds: 10), () {
|
||||
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty)
|
||||
_longWaitTimer = Timer(const Duration(seconds: 10), () {
|
||||
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) {
|
||||
setState(() => longWait = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -134,22 +138,29 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
_longWaitTimer?.cancel();
|
||||
|
||||
widget.hardwareWalletVM.stopScanning();
|
||||
_isConnectPressed = false;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _refreshUsbDevices() async {
|
||||
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
|
||||
if (usbDevices.length != dev.length) setState(() => usbDevices = dev);
|
||||
|
||||
if (usbDevices.length != dev.length) {
|
||||
setState(() => usbDevices = dev);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshBleDevices() async {
|
||||
try {
|
||||
if (widget.hardwareWalletVM.isBleEnabled) {
|
||||
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() {
|
||||
bleDevices.add(device);
|
||||
if (longWait) longWait = false;
|
||||
}))
|
||||
..onError((Object e) {
|
||||
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen(
|
||||
(device) => setState(() {
|
||||
bleDevices.add(device);
|
||||
if (longWait) {
|
||||
longWait = false;
|
||||
}
|
||||
}),
|
||||
)..onError((e) {
|
||||
printV(e);
|
||||
});
|
||||
_bleRefreshTimer?.cancel();
|
||||
@@ -160,48 +171,67 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
}
|
||||
}
|
||||
|
||||
var _isConnectPressed = false;
|
||||
|
||||
Future<void> _connectToDevice(HardwareWalletDevice device) async {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
if (_isConnectPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isConnectPressed = true;
|
||||
try {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
_isConnectPressed = false;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isConnected)
|
||||
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
|
||||
});
|
||||
{
|
||||
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
_isConnectPressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
|
||||
printV(deviceType);
|
||||
switch (deviceType) {
|
||||
case HardwareWalletDeviceType.ledgerNanoX:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletDeviceType.ledgerNanoGen5:
|
||||
return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg";
|
||||
case HardwareWalletDeviceType.ledgerStax:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_stax.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_stax.svg";
|
||||
case HardwareWalletDeviceType.ledgerFlex:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_flex.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_flex.svg";
|
||||
case HardwareWalletDeviceType.BitBox02:
|
||||
case HardwareWalletDeviceType.BitBox02Nova:
|
||||
return 'assets/new-ui/hardware_wallets/device_bitbox.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletDeviceType.trezorModelOne:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg";
|
||||
case HardwareWalletDeviceType.trezorModelT:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe3:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe5:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe7:
|
||||
return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
|
||||
|
||||
default:
|
||||
return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble;
|
||||
if (!Platform.isAndroid) {
|
||||
return S.of(context).connect_your_hardware_wallet_ble;
|
||||
}
|
||||
|
||||
if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet;
|
||||
if (widget.hardwareWalletVM.hasBluetooth) {
|
||||
return S.of(context).connect_your_hardware_wallet;
|
||||
}
|
||||
|
||||
return S.of(context).connect_your_hardware_wallet_usb;
|
||||
}
|
||||
@@ -210,7 +240,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Container(
|
||||
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -218,9 +248,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/hardware_wallet.svg",
|
||||
),
|
||||
const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
@@ -231,7 +259,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
offstage: widget.hardwareWalletVM.isBleEnabled ||
|
||||
!widget.hardwareWalletVM.hasBluetooth,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).ledger_please_enable_bluetooth,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
@@ -242,67 +270,89 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = allDevices[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _connectToDevice(item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: _getDeviceTileLeading(item.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
Text(item.name)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.connectionType ==
|
||||
HardwareWalletConnectionType.ble)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/bluetooth.svg",
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = allDevices[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _connectToDevice(item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (widget.hardwareWalletVM.isConnecting) {
|
||||
return CupertinoActivityIndicator(
|
||||
animating: true,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
radius: 12,
|
||||
);
|
||||
}
|
||||
|
||||
return CakeImageWidget(
|
||||
imageUrl: _getDeviceTileLeading(item.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(item.name),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.connectionType ==
|
||||
HardwareWalletConnectionType.ble)
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn))
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
imageUrl: "assets/new-ui/bluetooth.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
itemCount: allDevices.length),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
itemCount: allDevices.length,
|
||||
),
|
||||
),
|
||||
if (widget.allowChangeWallet) ...[
|
||||
PrimaryButton(
|
||||
@@ -310,7 +360,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: _onChangeWallet,
|
||||
)
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -321,7 +371,7 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => _onHowToConnect(context),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -339,17 +389,25 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet(
|
||||
builder: (_) => InfoStepsBottomSheet(
|
||||
titleText: S.of(context).how_to_connect,
|
||||
steps: [
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg',
|
||||
S.of(context).connect_hw_info_step_1),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg',
|
||||
S.of(context).connect_hw_info_step_2),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg',
|
||||
S.of(context).connect_hw_info_step_3),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg',
|
||||
S.of(context).connect_hw_info_step_4),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step1_power.svg",
|
||||
S.of(context).connect_hw_info_step_1,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step2_connect.svg",
|
||||
S.of(context).connect_hw_info_step_2,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step3_unlock.svg",
|
||||
S.of(context).connect_hw_info_step_3,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step4_select.svg",
|
||||
S.of(context).connect_hw_info_step_4,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+24
-23
@@ -1,25 +1,25 @@
|
||||
import 'dart:async';
|
||||
import "dart:async";
|
||||
|
||||
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
|
||||
import 'package:cake_wallet/themes/core/theme_store.dart';
|
||||
import 'package:cake_wallet/utils/exception_handler.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cake_wallet/store/wallet_list_store.dart';
|
||||
import 'package:cake_wallet/store/authentication_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import "package:cake_wallet/core/amount_parsing_proxy.dart";
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/entities/preferences_key.dart";
|
||||
import "package:cake_wallet/reactions/wallet_connect.dart";
|
||||
import "package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart";
|
||||
import "package:cake_wallet/store/authentication_store.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/store/wallet_list_store.dart";
|
||||
import "package:cake_wallet/themes/core/theme_store.dart";
|
||||
import "package:cake_wallet/utils/exception_handler.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
part 'app_store.g.dart';
|
||||
part "app_store.g.dart";
|
||||
|
||||
class AppStore = AppStoreBase with _$AppStore;
|
||||
|
||||
@@ -59,8 +59,9 @@ abstract class AppStoreBase with Store {
|
||||
@action
|
||||
Future<void> changeCurrentWallet(
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet) async {
|
||||
bool changingToSameWalletType = this.wallet?.type == wallet.type;
|
||||
this.wallet?.close(shouldCleanup: !changingToSameWalletType);
|
||||
final changingToSameWalletType = this.wallet?.type == wallet.type;
|
||||
|
||||
await this.wallet?.close(shouldCleanup: !changingToSameWalletType);
|
||||
this.wallet = wallet;
|
||||
this.wallet!.setExceptionHandler(ExceptionHandler.onError);
|
||||
|
||||
@@ -80,7 +81,7 @@ abstract class AppStoreBase with Store {
|
||||
wcService.create();
|
||||
await wcService.init();
|
||||
} catch (e, s) {
|
||||
printV('WalletConnect setup failed: $e\n$s');
|
||||
printV("WalletConnect setup failed: $e\n$s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
: 0,
|
||||
quantity = 1,
|
||||
card = vendor.card!,
|
||||
min = _toDouble(vendor.card!.minValue) ?? 0,
|
||||
max = _toDouble(vendor.card!.maxValue) ?? 0 {
|
||||
min = vendor.card!.prepaidRange.isNotEmpty
|
||||
? vendor.card!.prepaidRange.first.minValue
|
||||
: _toDouble(vendor.card!.minValue) ?? 0,
|
||||
max = vendor.card!.prepaidRange.isNotEmpty
|
||||
? vendor.card!.prepaidRange.first.maxValue
|
||||
: _toDouble(vendor.card!.maxValue) ?? 0 {
|
||||
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
|
||||
}
|
||||
|
||||
@@ -46,8 +50,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
final CakePayVendor vendor;
|
||||
final SendViewModel sendViewModel;
|
||||
final CakePayService _cakePayService;
|
||||
final double max;
|
||||
final double min;
|
||||
final CakePayCard card;
|
||||
final WalletType walletType;
|
||||
final Box<Order> orders;
|
||||
@@ -59,6 +61,8 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
bool confirmsNoVpn = false;
|
||||
bool confirmsVoidedRefund = false;
|
||||
bool confirmsTermsAgreed = false;
|
||||
double max;
|
||||
double min;
|
||||
(String, int?) selectedDenomination = ('', null);
|
||||
|
||||
String simulatedResponse = '';
|
||||
@@ -136,10 +140,24 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
@action
|
||||
void onQuantityChanged(int? input) => quantity = input ?? 1;
|
||||
|
||||
void onPrepaidRangeChanged(PrepaidRange range) {
|
||||
min = range.minValue;
|
||||
max = range.maxValue;
|
||||
selectedDenomination = (range.minValue.toString(), int.tryParse(range.rangeId));
|
||||
|
||||
if (amount < min || amount > max) {
|
||||
amount = min;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void onAmountChanged(String input) {
|
||||
if (input.isEmpty) return;
|
||||
amount = double.parse(input.replaceAll(',', '.'));
|
||||
|
||||
if (card.prepaidRange.isNotEmpty) {
|
||||
selectedDenomination = (input.replaceAll(',', '.'), selectedDenomination.$2);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -153,10 +171,25 @@ abstract class CakePayBuyCardViewModelBase with Store {
|
||||
sendViewModel.state =
|
||||
FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.');
|
||||
}
|
||||
|
||||
final isPrepaidRangeSelected = card.prepaidRange.isNotEmpty;
|
||||
|
||||
int selectedCardId = card.id;
|
||||
String selectedPrice = amount.toString();
|
||||
|
||||
if (isPrepaidRangeSelected) {
|
||||
selectedCardId = selectedDenomination.$2 ?? card.id;
|
||||
selectedPrice =
|
||||
selectedDenomination.$1.isNotEmpty ? selectedDenomination.$1 : amount.toString();
|
||||
} else if (isDenominationSelected) {
|
||||
selectedCardId = selectedDenomination.$2 ?? card.id;
|
||||
selectedPrice = selectedDenomination.$1;
|
||||
}
|
||||
|
||||
try {
|
||||
order = await _cakePayService.createOrder(
|
||||
cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
|
||||
price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
|
||||
cardId: selectedCardId,
|
||||
price: selectedPrice,
|
||||
quantity: quantity,
|
||||
confirmsNoVpn: confirmsNoVpn,
|
||||
confirmsVoidedRefund: confirmsVoidedRefund,
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
@@ -498,7 +497,6 @@ abstract class BalanceViewModelBase with Store {
|
||||
final record = wallet.balance[curr]!;
|
||||
final available = record.available - (record.secondAvailable ?? Money.zero(curr));
|
||||
final price = fiatConversionStore.prices[curr] ?? 0;
|
||||
printV(record.available);
|
||||
ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString())
|
||||
.replaceAll(",", "")) ??
|
||||
0;
|
||||
|
||||
@@ -227,9 +227,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
1;
|
||||
} catch (_) {}
|
||||
} else {
|
||||
confirmations = appStore.wallet!.transactionHistory.transactions.values
|
||||
.map((item) => item.isPending)
|
||||
.fold(0, (val, pending) => pending ? val + 1 : val);
|
||||
final pendingCount = appStore.wallet!.transactionHistory.transactions.values
|
||||
.where((item) => item.isPending)
|
||||
.length;
|
||||
confirmations = pendingCount + 1;
|
||||
}
|
||||
return length * confirmations;
|
||||
}, _transactionDisposerCallback, delay: 300);
|
||||
@@ -1331,9 +1332,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
1;
|
||||
} catch (_) {}
|
||||
} else {
|
||||
confirmations = appStore.wallet!.transactionHistory.transactions.values
|
||||
.map((item) => item.isPending)
|
||||
.fold(0, (val, pending) => pending ? val + 1 : val);
|
||||
final pendingCount = appStore.wallet!.transactionHistory.transactions.values
|
||||
.where((item) => item.isPending)
|
||||
.length;
|
||||
confirmations = pendingCount + 1;
|
||||
}
|
||||
return length * confirmations;
|
||||
}, _transactionDisposerCallback, delay: 300);
|
||||
|
||||
@@ -195,7 +195,7 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
case WalletType.decred:
|
||||
case WalletType.zcash:
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
case WalletType.ethereum:
|
||||
@@ -206,15 +206,15 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
final asset = assetOfTransaction;
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
case WalletType.solana:
|
||||
final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
|
||||
final asset = assetOfTransaction;
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
@@ -222,7 +222,7 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
@@ -234,7 +234,7 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
}
|
||||
final price = balanceViewModel.fiatConversionStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: double.parse(transaction.amount.toString()),
|
||||
cryptoAmount: transaction.amount.toDouble(),
|
||||
price: price,
|
||||
).withLocalSeperator(_appStore.settingsStore.languageCode);
|
||||
break;
|
||||
|
||||
@@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isConnecting = false;
|
||||
|
||||
@override
|
||||
bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth
|
||||
|
||||
@@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
Future<void> stopScanning() async {}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
|
||||
if (!(device is BitboxHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
if (isConnecting) return false;
|
||||
isConnecting = true;
|
||||
|
||||
try {
|
||||
final appDocDir = await getAppDir();
|
||||
@@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
|
||||
printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt');
|
||||
await bitboxManager.channelHashVerify();
|
||||
printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt');
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isConnecting = false;
|
||||
|
||||
@override
|
||||
bool isConnected(WalletType type) => false;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:cw_core/hardware/hardware_wallet_service.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
|
||||
abstract class HardwareWalletViewModel {
|
||||
HardwareWalletType get hardwareWalletType;
|
||||
bool get isBleEnabled;
|
||||
bool get hasBluetooth;
|
||||
bool get isConnecting;
|
||||
|
||||
bool isConnected(WalletType type);
|
||||
|
||||
@@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel {
|
||||
Future<void> initWallet(WalletBase wallet);
|
||||
|
||||
String? interpretErrorCode(String error) => null;
|
||||
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/main.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cake_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/hardware/device_connection_type.dart';
|
||||
import 'package:cw_core/hardware/hardware_wallet_service.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk;
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import "package:cake_wallet/bitcoin/bitcoin.dart";
|
||||
import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:cake_wallet/evm/evm.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/main.dart";
|
||||
import "package:cake_wallet/monero/monero.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
|
||||
import "package:cake_wallet/wallet_type_utils.dart";
|
||||
import "package:cw_core/hardware/device_connection_type.dart";
|
||||
import "package:cw_core/hardware/hardware_wallet_service.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
|
||||
import "package:mobx/mobx.dart";
|
||||
import "package:permission_handler/permission_handler.dart";
|
||||
|
||||
part 'ledger_view_model.g.dart';
|
||||
part "ledger_view_model.g.dart";
|
||||
|
||||
class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
|
||||
|
||||
abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
late final sdk.LedgerInterface ledgerPlusBLE;
|
||||
late final sdk.LedgerInterface ledgerPlusUSB;
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(
|
||||
WalletType.monero, HardwareWalletType.ledger, Platform.isIOS)
|
||||
.isNotEmpty;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
LedgerViewModelBase() {
|
||||
LedgerViewModelBase(AppStore appStore) {
|
||||
if (_doesSupportHardwareWallets) {
|
||||
reaction((_) => isBleEnabled, (_) {
|
||||
if (isBleEnabled) _initBLE();
|
||||
if (isBleEnabled) {
|
||||
_initBLE();
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => appStore.wallet, (wallet) async {
|
||||
if (wallet != null && wallet.type != _connectingWalletType) {
|
||||
await close();
|
||||
}
|
||||
});
|
||||
|
||||
updateBleState();
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
@@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
}
|
||||
}
|
||||
|
||||
late final sdk.LedgerInterface ledgerPlusBLE;
|
||||
late final sdk.LedgerInterface ledgerPlusUSB;
|
||||
|
||||
bool get _doesSupportHardwareWallets {
|
||||
if (isMoneroOnly) {
|
||||
return DeviceConnectionType.supportedConnectionTypes(
|
||||
WalletType.monero,
|
||||
HardwareWalletType.ledger,
|
||||
Platform.isIOS,
|
||||
).isNotEmpty;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
|
||||
|
||||
@@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isConnecting = false;
|
||||
|
||||
@override
|
||||
bool get hasBluetooth => true;
|
||||
|
||||
@@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
if (isBleEnabled && !_bleIsInitialized) {
|
||||
ledgerPlusBLE = sdk.LedgerInterface.ble(
|
||||
onPermissionRequest: (_) async {
|
||||
if (Platform.isMacOS) return true;
|
||||
if (Platform.isMacOS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
final statuses = await [
|
||||
Permission.bluetoothScan,
|
||||
Permission.bluetoothConnect,
|
||||
Permission.bluetoothAdvertise,
|
||||
@@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
|
||||
return statuses.values.where((status) => status.isDenied).isEmpty;
|
||||
},
|
||||
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
|
||||
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
|
||||
);
|
||||
_bleIsInitialized = true;
|
||||
}
|
||||
@@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
|
||||
final newState = bleState == sdk.AvailabilityState.poweredOn;
|
||||
|
||||
if (newState != isBleEnabled) isBleEnabled = newState;
|
||||
if (newState != isBleEnabled) {
|
||||
isBleEnabled = newState;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<HardwareWalletDevice> scanForBleDevices() =>
|
||||
ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d));
|
||||
ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
|
||||
|
||||
@override
|
||||
Future<List<HardwareWalletDevice>> getAllUsbDevices() => ledgerPlusUSB.devices
|
||||
.then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList());
|
||||
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
|
||||
ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
|
||||
|
||||
@override
|
||||
Future<void> stopScanning() async {
|
||||
if (_bleIsInitialized) await ledgerPlusBLE.stopScanning();
|
||||
if (!Platform.isIOS) await ledgerPlusUSB.stopScanning();
|
||||
if (_bleIsInitialized) {
|
||||
await ledgerPlusBLE.stopScanning();
|
||||
}
|
||||
if (!Platform.isIOS) {
|
||||
await ledgerPlusUSB.stopScanning();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
|
||||
if (!(device is LedgerHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
if (device is! LedgerHardwareWalletDevice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isConnecting = true;
|
||||
_connectingWalletType = type;
|
||||
if (isConnected(type)) {
|
||||
try {
|
||||
@@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
final ledger =
|
||||
device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
|
||||
|
||||
if (_connectionChangeSubscription == null) {
|
||||
_connectionChangeSubscription =
|
||||
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
|
||||
}
|
||||
_connectionChangeSubscription ??=
|
||||
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
|
||||
|
||||
try {
|
||||
_connection = await ledger.connect(device.device);
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
|
||||
sdk.LedgerConnection? _connection;
|
||||
bool _isConnecting = false;
|
||||
WalletType? _connectingWalletType;
|
||||
bool suppressMoneroReconnectPrompt = false;
|
||||
|
||||
void _connectionChangeListener(sdk.BleConnectionState event) {
|
||||
printV('Ledger Device State Changed: $event');
|
||||
if (event == sdk.BleConnectionState.disconnected && !_isConnecting) {
|
||||
printV("Ledger Device State Changed: $event");
|
||||
if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
|
||||
_connection = null;
|
||||
if (_connectingWalletType == WalletType.monero) {
|
||||
monero!.resetLedgerConnection();
|
||||
|
||||
if (suppressMoneroReconnectPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(navigatorKey.currentContext!).pushNamed(
|
||||
Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
@@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
hardwareWalletType: HardwareWalletType.ledger,
|
||||
allowChangeWallet: true,
|
||||
isReconnect: true,
|
||||
onConnectDevice: (context, ledgerVM) async {
|
||||
onConnectDevice: (context, ledgerVM) {
|
||||
if (context.mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected);
|
||||
bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
|
||||
|
||||
sdk.LedgerConnection get connection => _connection!;
|
||||
|
||||
@override
|
||||
Future<void> initWallet(WalletBase wallet) async {
|
||||
Future<void> initWallet(WalletBase wallet) {
|
||||
switch (wallet.type) {
|
||||
case WalletType.monero:
|
||||
return monero!.setLedgerConnection(wallet, connection);
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!
|
||||
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!
|
||||
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
|
||||
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
|
||||
throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
|
||||
return error;
|
||||
}
|
||||
|
||||
var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString());
|
||||
final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
|
||||
|
||||
String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
|
||||
final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
|
||||
if (errorCode.contains("6985")) {
|
||||
return S.current.ledger_error_tx_rejected_by_user;
|
||||
} else if (errorCode.contains("5515")) {
|
||||
return S.current.ledger_error_device_locked;
|
||||
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
|
||||
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
|
||||
return S.current.ledger_error_wrong_app;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
try {
|
||||
printV("Resetting ledger VM");
|
||||
await _connectionChangeSubscription?.cancel();
|
||||
await _connection?.disconnect().catchError((_) {});
|
||||
|
||||
_connectingWalletType = null;
|
||||
_connectionChangeSubscription = null;
|
||||
_connection = null;
|
||||
isConnecting = false;
|
||||
await stopScanning();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
@override
|
||||
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isConnecting = false;
|
||||
|
||||
@override
|
||||
@observable
|
||||
bool isBleEnabled = false;
|
||||
@@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
if (!Platform.isIOS) await trezorUSB.stopScanning();
|
||||
}
|
||||
|
||||
bool _isConnecting = false;
|
||||
sdk.TrezorClient? _client;
|
||||
|
||||
Completer<String?>? _pinCompleter;
|
||||
@@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
void setParingPin(String pin) => _pinCompleter?.complete(pin);
|
||||
|
||||
@override
|
||||
@action
|
||||
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type,
|
||||
[bool isRetry = false]) async {
|
||||
if (!(device is TrezorHardwareWalletDevice)) return false;
|
||||
if (_isConnecting) return false;
|
||||
_isConnecting = true;
|
||||
if (isConnecting) return false;
|
||||
isConnecting = true;
|
||||
paringState = TrezorParingState.initial;
|
||||
|
||||
try {
|
||||
@@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
|
||||
printV(e);
|
||||
return false;
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1121,6 +1121,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
// it is required because evm, solana and tron don't actually save the transaction info when you send something.
|
||||
// instead, they rely on the tx to eventually get fetched at sync time, which can take a while
|
||||
if (isEVMWallet) {
|
||||
final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull(
|
||||
(token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(),
|
||||
);
|
||||
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
|
||||
id: pendingTransaction!.evmTxHashFromRawHex!,
|
||||
height: 0,
|
||||
@@ -1132,6 +1135,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
date: DateTime.now(),
|
||||
confirmations: 0,
|
||||
chainId: wallet.chainId ?? 0,
|
||||
contractAddress: selectedToken?.contractAddress,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store {
|
||||
if (walletItem.type == WalletType.haven) {
|
||||
return;
|
||||
}
|
||||
// bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type;
|
||||
// await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType);
|
||||
|
||||
final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name);
|
||||
await _appStore.changeCurrentWallet(wallet);
|
||||
updateList();
|
||||
|
||||
+4
-10
@@ -125,7 +125,7 @@ dependencies:
|
||||
ledger_flutter_plus:
|
||||
git:
|
||||
url: https://github.com/vespr-wallet/ledger-flutter-plus
|
||||
ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9
|
||||
ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28
|
||||
trezor_connect:
|
||||
git:
|
||||
url: https://github.com/cake-tech/trezor_connect
|
||||
@@ -201,14 +201,8 @@ dependency_overrides:
|
||||
ffi: 2.1.0
|
||||
ledger_flutter_plus:
|
||||
git:
|
||||
url: https://github.com/cake-tech/ledger-flutter-plus
|
||||
ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2
|
||||
ledger_usb_plus:
|
||||
git:
|
||||
# url: https://github.com/konstantinullrich/ledger-usb-plus
|
||||
# ref: dad482c847c0d0eedceabbb40d3478e64b65e630
|
||||
url: https://github.com/cake-tech/ledger-usb-plus
|
||||
ref: 734c3d6f8267ba52ad73b6e1e78e570c07298890
|
||||
url: https://github.com/vespr-wallet/ledger-flutter-plus
|
||||
ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28
|
||||
socks5_proxy:
|
||||
git:
|
||||
url: https://github.com/LacticWhale/socks_dart.git
|
||||
@@ -231,7 +225,7 @@ dependency_overrides:
|
||||
universal_ble:
|
||||
git:
|
||||
url: https://github.com/cake-tech/universal_ble.git
|
||||
ref: f7044558ad111a156987614971e08c583c80175c
|
||||
ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981
|
||||
|
||||
flutter_icons:
|
||||
image_path: "assets/images/app_logo.png"
|
||||
|
||||
@@ -48,7 +48,8 @@ void main() {
|
||||
|
||||
group("getCanonicalCryptoAmount", () {
|
||||
test("Amount should not change for Bitcoin", () {
|
||||
final amount = amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btc);
|
||||
final amount =
|
||||
amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btc);
|
||||
expect(amount, "0.000001");
|
||||
});
|
||||
|
||||
@@ -58,7 +59,8 @@ void main() {
|
||||
});
|
||||
|
||||
test("Amount should not change for Bitcoin", () {
|
||||
final amount = amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btcln);
|
||||
final amount =
|
||||
amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btcln);
|
||||
expect(amount, "0.000001");
|
||||
});
|
||||
|
||||
@@ -118,7 +120,8 @@ void main() {
|
||||
});
|
||||
|
||||
test("Amount should be formated to Satoshi for Bitcoin Lightning", () {
|
||||
final amount = amountParsingProxy.getDisplayCryptoAmount("0.000001", CryptoCurrency.btcln);
|
||||
final amount =
|
||||
amountParsingProxy.getDisplayCryptoAmount("0.000001", CryptoCurrency.btcln);
|
||||
expect(amount, "100");
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,8 @@ void main() {
|
||||
});
|
||||
|
||||
test('detects Lightning invoice', () {
|
||||
const invoice = 'lnbc1u1p3zj7j8pp5vkwxc4vxfzgzxcjzk5ksp0l7jgrgzmqxqmexxp8e8gfqk6s0knnqdlc35hsggzypnxw7xxxnrvd3hscqzpgxqrrsssp5txs5g8vkdmdtnm8g5v9m3u094nnxkl0jjf38jj5xn2pptxlqkwn7gpgfkt7q6z0frcj7vjy4mv7659hccln2e0gzt4ufnsukxcnknh7e58kwnr8pk6kgv6vrghcpld2z5n';
|
||||
const invoice =
|
||||
'lnbc1u1p3zj7j8pp5vkwxc4vxfzgzxcjzk5ksp0l7jgrgzmqxqmexxp8e8gfqk6s0knnqdlc35hsggzypnxw7xxxnrvd3hscqzpgxqrrsssp5txs5g8vkdmdtnm8g5v9m3u094nnxkl0jjf38jj5xn2pptxlqkwn7gpgfkt7q6z0frcj7vjy4mv7659hccln2e0gzt4ufnsukxcnknh7e58kwnr8pk6kgv6vrghcpld2z5n';
|
||||
final result = UniversalAddressDetector.detectAddress(invoice);
|
||||
|
||||
expect(result.isValid, true);
|
||||
|
||||
@@ -39,8 +39,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('extracts Ethereum address correctly', () {
|
||||
final raw =
|
||||
'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks';
|
||||
final raw = 'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks';
|
||||
final result = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw,
|
||||
type: CryptoCurrency.eth,
|
||||
@@ -60,25 +59,21 @@ void main() {
|
||||
});
|
||||
|
||||
test('extracts Bitcoin Cash address correctly', () {
|
||||
final raw =
|
||||
'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj';
|
||||
final raw = 'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj';
|
||||
final result = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw,
|
||||
type: CryptoCurrency.bch,
|
||||
);
|
||||
expect(
|
||||
result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj');
|
||||
expect(result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj');
|
||||
});
|
||||
|
||||
test('extracts Nano address correctly', () {
|
||||
final raw =
|
||||
'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd';
|
||||
final raw = 'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd';
|
||||
final result = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw,
|
||||
type: CryptoCurrency.nano,
|
||||
);
|
||||
expect(result,
|
||||
'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd');
|
||||
expect(result, 'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd');
|
||||
});
|
||||
|
||||
test('returns null for unsupported currency', () {
|
||||
@@ -101,9 +96,7 @@ void main() {
|
||||
final raw =
|
||||
'monero_wallet:467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz?spend_key=029c559cd7669f14e91fd835144916009f8697ab5ac5c7f7c06e1ff869c17b0b&view_key=afaf646edbff3d3bcee8efd3383ffe5d20c947040f74e1110b70ca0fbb0ef90d';
|
||||
final result = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw,
|
||||
type: CryptoCurrency.xmr,
|
||||
requireSurroundingWhitespaces: false);
|
||||
raw: raw, type: CryptoCurrency.xmr, requireSurroundingWhitespaces: false);
|
||||
expect(result,
|
||||
'467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz');
|
||||
});
|
||||
@@ -119,16 +112,16 @@ sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucw
|
||||
#LTC MWEB
|
||||
ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc
|
||||
''';
|
||||
final resultXmr = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw, type: CryptoCurrency.xmr);
|
||||
final resultXmr =
|
||||
AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.xmr);
|
||||
expect(resultXmr,
|
||||
'89bH6i3ftaWSWuPJJYSQuuApWJ8xzinCEbbnAXN1Z3mGGUuAFdpBUg82R9MvJDSheJ6kW2dyMQEFUGM4tsZqRb2Q75UXqvc');
|
||||
final resultBtc = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw, type: CryptoCurrency.btc);
|
||||
final resultBtc =
|
||||
AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.btc);
|
||||
expect(resultBtc,
|
||||
'sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucwj5k7nenpw2r86wf42xv6wqdvxuk5rggrul45');
|
||||
final resultLtc = AddressResolverUtils.extractAddressByType(
|
||||
raw: raw, type: CryptoCurrency.ltc);
|
||||
final resultLtc =
|
||||
AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.ltc);
|
||||
expect(resultLtc,
|
||||
'ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc');
|
||||
});
|
||||
|
||||
@@ -23,8 +23,7 @@ void main() {
|
||||
});
|
||||
|
||||
test("extract address and amount from Cake Style Uri", () {
|
||||
final uri =
|
||||
Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00");
|
||||
final uri = Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00");
|
||||
final paymentRequest = PaymentRequest.fromUri(uri);
|
||||
|
||||
expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41");
|
||||
|
||||
@@ -26,11 +26,11 @@ void main(List<String> args) async {
|
||||
}
|
||||
|
||||
print('Alphabetizing all files...');
|
||||
|
||||
|
||||
for (var lang in langs) {
|
||||
final fileName = getArbFileName(lang);
|
||||
alphabetizeArbFile(fileName);
|
||||
}
|
||||
|
||||
print('Done!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +475,7 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir
|
||||
Future<void> setLedgerConnection(Object wallet, ledger.LedgerConnection connection);
|
||||
void resetLedgerConnection();
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection);
|
||||
bool hasGlobalLedgerConnection();
|
||||
String? getLastLedgerCommand();
|
||||
void setHardwareWalletService(Object wallet, HardwareWalletService service);
|
||||
HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection);
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:archive/archive_io.dart';
|
||||
final _dio = Dio();
|
||||
|
||||
final List<String> triplets = [
|
||||
"x86_64-linux-gnu", // linux desktop - majority of users onlinux
|
||||
"x86_64-linux-gnu", // linux desktop - majority of users onlinux
|
||||
// "i686-linux-gnu", // not supported by cake
|
||||
// "i686-meego-linux-gnu", // sailfishos (emulator)- not supported by cake
|
||||
// "aarch64-linux-gnu", // not (yet) supported by cake - (mostly) mobile linux
|
||||
@@ -18,7 +18,7 @@ final List<String> triplets = [
|
||||
"armv7a-linux-androideabi",
|
||||
// "i686-w64-mingw32", // 32bit windows - not supported by monero_c
|
||||
"x86_64-w64-mingw32",
|
||||
// "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake
|
||||
// "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake
|
||||
// "aarch64-apple-darwin11", // apple silicon macbooks (contrib) - not used by cake
|
||||
// "x86_64-host-apple-darwin", // not available on CI (yet)
|
||||
"aarch64-apple-darwin", // apple silicon macbooks
|
||||
@@ -55,10 +55,8 @@ Future<void> main() async {
|
||||
}
|
||||
if (Platform.isMacOS) {
|
||||
print("Generating ios framework");
|
||||
final result = Process.runSync("bash", [
|
||||
"-c",
|
||||
"cd scripts/ios && ./gen_framework.sh && cd ../.."
|
||||
]);
|
||||
print((result.stdout+result.stderr).toString().trim());
|
||||
final result =
|
||||
Process.runSync("bash", ["-c", "cd scripts/ios && ./gen_framework.sh && cd ../.."]);
|
||||
print((result.stdout + result.stderr).toString().trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,21 @@ const srcDir = 'srcDir';
|
||||
const defaultLocale = 'en';
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
final extraInfo = args.isNotEmpty ?
|
||||
args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
|
||||
final parts = arg.split('=');
|
||||
var key = normalizeKeyName(parts[0]);
|
||||
if (key.contains('--')) {
|
||||
key = key.substring(2);
|
||||
}
|
||||
acc[key] = parts.length > 1
|
||||
? parts[1].isNotEmpty
|
||||
? parts[1]
|
||||
: inputPath
|
||||
: inputPath;
|
||||
return acc;
|
||||
})
|
||||
: <String, dynamic> {srcDir : inputPath};
|
||||
final extraInfo = args.isNotEmpty
|
||||
? args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
|
||||
final parts = arg.split('=');
|
||||
var key = normalizeKeyName(parts[0]);
|
||||
if (key.contains('--')) {
|
||||
key = key.substring(2);
|
||||
}
|
||||
acc[key] = parts.length > 1
|
||||
? parts[1].isNotEmpty
|
||||
? parts[1]
|
||||
: inputPath
|
||||
: inputPath;
|
||||
return acc;
|
||||
})
|
||||
: <String, dynamic>{srcDir: inputPath};
|
||||
|
||||
final outputDir = Directory(outputPath);
|
||||
|
||||
@@ -59,10 +59,10 @@ Future<void> main(List<String> args) async {
|
||||
return;
|
||||
}
|
||||
final parts =
|
||||
fileName.substring('strings_'.length, fileName.length - '.arb'.length).split('_');
|
||||
fileName.substring('strings_'.length, fileName.length - '.arb'.length).split('_');
|
||||
final locale = parts.length > 1
|
||||
? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}'
|
||||
: parts.first.toLowerCase();
|
||||
? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}'
|
||||
: parts.first.toLowerCase();
|
||||
localePath[locale] = element.path;
|
||||
});
|
||||
|
||||
@@ -78,8 +78,7 @@ Future<void> main(List<String> args) async {
|
||||
output += part1;
|
||||
output += textDirectionDeclaration;
|
||||
|
||||
var inputContent =
|
||||
File(localePath[defaultLocale].toString()).readAsStringSync();
|
||||
var inputContent = File(localePath[defaultLocale].toString()).readAsStringSync();
|
||||
var config = json.decode(inputContent) as Map<String, dynamic>;
|
||||
|
||||
output += localizedStrings(config: config, hasOverride: false);
|
||||
@@ -161,4 +160,3 @@ String localizedStrings({required Map<String, dynamic> config, required bool has
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,4 @@ Future<void> main(List<String> args) async => generateSecrets(args);
|
||||
Future<void> generateSecrets(List<String> args) async {
|
||||
await generateSecretsConfig(args);
|
||||
await importSecretsConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ Future<void> main(List<String> args) async {
|
||||
final pubspecDescription = File(pubspecDescriptionPath);
|
||||
|
||||
if (!pubspecBase.existsSync() || !pubspecDescription.existsSync()) {
|
||||
throw("$pubspecBasePath or $pubspecDescriptionPath doesn't exists");
|
||||
throw ("$pubspecBasePath or $pubspecDescriptionPath doesn't exists");
|
||||
}
|
||||
|
||||
final pubspecBaseContent = await pubspecBase.readAsString();
|
||||
final pubspecDescriptionContent = await pubspecDescription.readAsString();
|
||||
final pubSpecContent = pubspecDescriptionContent + '\n\n' + pubspecBaseContent;
|
||||
final pubSpecContent = pubspecDescriptionContent + '\n\n' + pubspecBaseContent;
|
||||
final outputPubspec = File(outputPubspecPath);
|
||||
|
||||
if (outputPubspec.existsSync()) {
|
||||
|
||||
@@ -61,9 +61,9 @@ Future<void> generateSecretsConfig(List<String> args) async {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await writeConfig(baseConfigFile, SecretKey.base, existingSecrets: secrets);
|
||||
|
||||
|
||||
await writeConfig(evmChainsConfigFile, SecretKey.evmChainsSecrets);
|
||||
await writeConfig(solanaConfigFile, SecretKey.solanaSecrets);
|
||||
await writeConfig(nanoConfigFile, SecretKey.nanoSecrets);
|
||||
|
||||
@@ -157,4 +157,4 @@ String getLang(Locale l) => l == null
|
||||
: l.countryCode != null && l.countryCode!.isEmpty
|
||||
? l.languageCode
|
||||
: l.toString();
|
||||
""";
|
||||
""";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import 'utils/translation/translation_utils.dart';
|
||||
const changelogsFilePath = "./assets/new-ui/changelog/text";
|
||||
|
||||
void main() async {
|
||||
final List<dynamic> defaultLangChangelog = jsonDecode(
|
||||
await File("$changelogsFilePath/changelog_$defaultLang.json").readAsString())
|
||||
as List<dynamic>;
|
||||
final List<dynamic> defaultLangChangelog =
|
||||
jsonDecode(await File("$changelogsFilePath/changelog_$defaultLang.json").readAsString())
|
||||
as List<dynamic>;
|
||||
|
||||
int currentLang = 0;
|
||||
for (final lang in langs) {
|
||||
@@ -50,7 +50,6 @@ void main() async {
|
||||
"\r$currentLang/${langs.length} langs done, $currentItem/${defaultLangChangelog.length} items done");
|
||||
}
|
||||
currentLang++;
|
||||
await File("$changelogsFilePath/changelog_$lang.json")
|
||||
.writeAsString(jsonEncode(out));
|
||||
await File("$changelogsFilePath/changelog_$lang.json").writeAsString(jsonEncode(out));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,11 +217,20 @@ IMPORTANT: Respond with a JSON object in the exact format, you must think about
|
||||
|
||||
void main(List<String> args) async {
|
||||
final parser = ArgParser()
|
||||
..addOption('source', abbr: 's', help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)', mandatory: true)
|
||||
..addOption('destination', abbr: 'd', help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)', mandatory: true)
|
||||
..addOption('source',
|
||||
abbr: 's',
|
||||
help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)',
|
||||
mandatory: true)
|
||||
..addOption('destination',
|
||||
abbr: 'd',
|
||||
help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)',
|
||||
mandatory: true)
|
||||
..addOption('key', abbr: 'k', help: 'Translate only this specific key (optional)')
|
||||
..addOption('ollama-url', help: 'Ollama server URL (default: http://localhost:11434)', defaultsTo: 'http://localhost:11434')
|
||||
..addOption('model', help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b')
|
||||
..addOption('ollama-url',
|
||||
help: 'Ollama server URL (default: http://localhost:11434)',
|
||||
defaultsTo: 'http://localhost:11434')
|
||||
..addOption('model',
|
||||
help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b')
|
||||
..addFlag('help', abbr: 'h', help: 'Show this help message', negatable: false);
|
||||
|
||||
try {
|
||||
@@ -268,7 +277,6 @@ void main(List<String> args) async {
|
||||
destinationArbPath: destinationPath,
|
||||
specificKey: specificKey,
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
print('Error: $e');
|
||||
print('Run with --help for more information.');
|
||||
|
||||
@@ -8,8 +8,7 @@ import 'utils/translation/translation_utils.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
if (args.length != 1) {
|
||||
throw Exception(
|
||||
'Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`');
|
||||
throw Exception('Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`');
|
||||
}
|
||||
|
||||
final targetLang = args.first;
|
||||
|
||||
@@ -28,8 +28,7 @@ void main(List<String> args) async {
|
||||
|
||||
missingKeys.forEach((key) {
|
||||
print('Missing in "$lang": "$key"');
|
||||
if (doFix)
|
||||
missingDefaults[key] = arbObj[key] as String;
|
||||
if (doFix) missingDefaults[key] = arbObj[key] as String;
|
||||
});
|
||||
|
||||
if (missingDefaults.isNotEmpty) {
|
||||
|
||||
@@ -12,8 +12,7 @@ Future<void> main(List<String> args) async {
|
||||
}
|
||||
|
||||
Future<void> updateSecretsConfig(List<String> args) async {
|
||||
final extraInfo =
|
||||
args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
|
||||
final extraInfo = args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
|
||||
final parts = arg.split('=');
|
||||
final key = normalizeKeyName(parts[0]);
|
||||
acc[key] = parts.length > 1 ? parts[1] : 1;
|
||||
@@ -32,8 +31,7 @@ Future<void> updateSecretsConfig(List<String> args) async {
|
||||
return false;
|
||||
});
|
||||
|
||||
final fileConfig =
|
||||
json.decode(configFile.readAsStringSync()) as Map<String, dynamic>;
|
||||
final fileConfig = json.decode(configFile.readAsStringSync()) as Map<String, dynamic>;
|
||||
fileConfig.forEach((key, dynamic value) {
|
||||
if (secrets[key] == null) {
|
||||
secrets[key] = value;
|
||||
|
||||
Reference in New Issue
Block a user