Merge branch 'main' into mweb-bg-sync-3

This commit is contained in:
Matthew Fosse 2025-02-23 19:37:37 -08:00 committed by GitHub
commit f7b189af24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
311 changed files with 10251 additions and 2064 deletions

View file

@ -190,7 +190,7 @@ jobs:
echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart
echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart
echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart

View file

@ -1,6 +1,6 @@
name: Cake Wallet Android
on: [push]
on: [pull_request]
defaults:
run:
@ -34,10 +34,19 @@ jobs:
- name: Fix github actions messing up $HOME...
run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV'
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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: Get the full commit message
run: |
FULL_MESSAGE="$(git log -1 --pretty=%B)"
echo "message<<EOF" >> $GITHUB_ENV
echo "$FULL_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Add secrets
run: |
touch lib/.secrets.g.dart
@ -126,7 +135,7 @@ jobs:
echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart
echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart
echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
@ -160,6 +169,10 @@ jobs:
echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart
# end of test secrets
echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
- name: prepare monero_c and cache
run: |
export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }')
@ -259,25 +272,23 @@ jobs:
mkdir test-apk
cp app-arm64-v8a-release.apk test-apk/${BRANCH_NAME}.apk
cp app-x86_64-release.apk test-apk/${BRANCH_NAME}_x86.apk
cd test-apk
cp ${BRANCH_NAME}.apk ${BRANCH_NAME}_slack.apk
- name: Find APK file
id: find_apk
run: |
set -x
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/*_slack.apk || exit 1)
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1)
echo "APK_FILE=$apk_file" >> $GITHUB_ENV
- name: Upload artifact to slack
if: ${{ !contains(github.event.head_commit.message, 'skip slack') }}
if: ${{ !contains(env.message, 'skip slack') }}
continue-on-error: true
uses: adrey/slack-file-upload-action@1.0.5
with:
token: ${{ secrets.SLACK_APP_TOKEN }}
path: ${{ env.APK_FILE }}
channel: ${{ secrets.SLACK_APK_CHANNEL }}
initial_comment: ${{ github.event.head_commit.message }}
initial_comment: ${{ env.message }}
- name: cleanup
run: rm -rf build/app/outputs/flutter-apk/test-apk/

View file

@ -1,6 +1,6 @@
name: Cake Wallet Linux
on: [push]
on: [pull_request]
defaults:
run:
@ -30,10 +30,19 @@ jobs:
- name: Fix github actions messing up $HOME...
run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV'
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- 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: Get the full commit message
run: |
FULL_MESSAGE="$(git log -1 --pretty=%B)"
echo "message<<EOF" >> $GITHUB_ENV
echo "$FULL_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Add secrets
run: |
touch lib/.secrets.g.dart
@ -122,7 +131,7 @@ jobs:
echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart
echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart
echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart
echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const swapTradeExchangeMarkup = '${{ secrets.SWAPTRADE_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
@ -155,7 +164,11 @@ jobs:
echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
echo "const wowneroTestWalletReceiveAddress = '${{ secrets.WOWNERO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart
echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart
echo "const moneroTestWalletBlockHeight = '${{ secrets.MONERO_TEST_WALLET_BLOCK_HEIGHT }}';" >> lib/.secrets.g.dart
# end of test secrets
echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart
echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
- name: prepare monero_c and cache
run: |
export MONEROC_HASH=$(cat scripts/prepare_moneroc.sh | grep 'git checkout' | xargs | awk '{ print $3 }')
@ -227,7 +240,7 @@ jobs:
name: cakewallet_linux
- name: Prepare virtual desktop
if: ${{ contains(github.event.head_commit.message, 'run tests') }}
if: ${{ contains(env.message, 'run tests') }}
run: |
nohup Xvfb :99 -screen 0 720x1280x16 &
echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV
@ -243,28 +256,28 @@ jobs:
# isn't much in those wallets anyway, we still wouldn't like to leak it to anyone who is able to access github.
- name: Test [confirm_seeds_flow_test]
if: ${{ contains(github.event.head_commit.message, 'run tests') }}
if: ${{ contains(env.message, 'run tests') }}
timeout-minutes: 20
run: |
xmessage -timeout 30 "confirm_seeds_flow_test" &
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/confirm_seeds_flow_test.dart
- name: Test [create_wallet_flow_test]
if: ${{ contains(github.event.head_commit.message, 'run tests') }}
if: ${{ contains(env.message, 'run tests') }}
timeout-minutes: 20
run: |
xmessage -timeout 30 "create_wallet_flow_test" &
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/create_wallet_flow_test.dart
- name: Test [exchange_flow_test]
if: ${{ contains(github.event.head_commit.message, 'run tests') }}
if: ${{ contains(env.message, 'run tests') }}
timeout-minutes: 20
run: |
xmessage -timeout 30 "exchange_flow_test" &
rm -rf ~/.local/share/com.example.cake_wallet/ ~/Documents/cake_wallet/ ~/cake_wallet
exec timeout --signal=SIGKILL 900 flutter drive --driver=test_driver/integration_test.dart --target=integration_test/test_suites/exchange_flow_test.dart
- name: Test [restore_wallet_through_seeds_flow_test]
if: ${{ contains(github.event.head_commit.message, 'run tests') }}
if: ${{ contains(env.message, 'run tests') }}
timeout-minutes: 20
run: |
xmessage -timeout 30 "restore_wallet_through_seeds_flow_test" &

5
.gitignore vendored
View file

@ -126,7 +126,7 @@ cw_shared_external/ios/External/
cw_haven/ios/External/
cw_haven/android/.externalNativeBuild/
cw_haven/android/.cxx/
cw_zano/ios/External/
lib/bitcoin/bitcoin.dart
lib/monero/monero.dart
lib/haven/haven.dart
@ -137,6 +137,7 @@ lib/polygon/polygon.dart
lib/solana/solana.dart
lib/tron/tron.dart
lib/wownero/wownero.dart
lib/zano/zano.dart
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
@ -179,3 +180,5 @@ scripts/monero_c
# iOS generated framework bin
ios/MoneroWallet.framework/MoneroWallet
ios/WowneroWallet.framework/WowneroWallet
ios/ZanoWallet.framework/ZanoWallet
*_libwallet2_api_c.dylib

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2023 Cake Labs LLC
Copyright (c) 2018-2025 Cake Labs LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -22,6 +22,7 @@ analyzer:
lib/solana/cw_solana.dart,
lib/tron/cw_tron.dart,
lib/wownero/cw_wownero.dart,
lib/zano/cw_zano.dart,
]
language:
strict-casts: true

View file

@ -89,6 +89,9 @@
<data android:scheme="wownero" />
<data android:scheme="wownero-wallet" />
<data android:scheme="wownero_wallet" />
<data android:scheme="zano" />
<data android:scheme="zano-wallet" />
<data android:scheme="zano_wallet" />
</intent-filter>
<!-- nano-gpt link scheme -->
<intent-filter android:autoVerify="true">

View file

@ -0,0 +1 @@
../../../../../../scripts/monero_c/release/zano/aarch64-linux-android_libwallet2_api_c.so

View file

@ -0,0 +1 @@
../../../../../../scripts/monero_c/release/zano/armv7a-linux-androideabi_libwallet2_api_c.so

View file

@ -0,0 +1 @@
../../../../../../scripts/monero_c/release/zano/x86_64-linux-android_libwallet2_api_c.so

BIN
assets/images/chainflip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/images/flip_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

BIN
assets/images/zano_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,2 +1,3 @@
Ledger fixes
UI enhancements
Bug fixes

View file

@ -1,4 +1,5 @@
Bitcoin and Litecoin enhancements
Solana and Nano fixes/improvements
Zano enhancements
Ethereum enhancements
Ledger fixes
UI enhancements
Bug fixes

View file

@ -0,0 +1,7 @@
-
uri: 37.27.100.59:10500
useSSL: false
-
uri: zano.cakewallet.com:11211
is_default: true
useSSL: false

View file

@ -2,6 +2,7 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/bip/bip/bip32/bip32.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
@ -26,7 +27,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
@override
String getAddress(
{required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) {
{required int index,
required Bip32Slip10Secp256k1 hd,
BitcoinAddressType? addressType,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) {
if (addressType == P2pkhAddressType.p2pkh)
return generateP2PKHAddress(hd: hd, index: index, network: network);

View file

@ -865,6 +865,7 @@ abstract class ElectrumWalletBase
final changeAddress = await walletAddresses.getChangeAddress(
inputs: utxoDetails.availableInputs,
outputs: updatedOutputs,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
updatedOutputs.add(BitcoinOutput(

View file

@ -4,6 +4,7 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/wallet_addresses.dart';
@ -47,7 +48,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
List<BitcoinAddressRecord>? initialMwebAddresses,
Bip32Slip10Secp256k1? masterHd,
BitcoinAddressType? initialAddressPageType,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
addressesByReceiveType =
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
@ -187,13 +187,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return;
}
try {
final addressRecord = _addresses.firstWhere(
(addressRecord) => addressRecord.address == addr,
);
final addressRecord = _addresses.firstWhere(
(addressRecord) => addressRecord.address == addr,
);
previousAddressRecord = addressRecord;
receiveAddresses.remove(addressRecord);
receiveAddresses.insert(0, addressRecord);
previousAddressRecord = addressRecord;
receiveAddresses.remove(addressRecord);
receiveAddresses.insert(0, addressRecord);
} catch (e) {
printV("ElectrumWalletAddressBase: set address ($addr): $e");
}
@ -274,7 +274,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
Future<BitcoinAddressRecord> getChangeAddress({List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
Future<BitcoinAddressRecord> getChangeAddress(
{List<BitcoinUnspent>? inputs,
List<BitcoinOutput>? outputs,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
updateChangeAddresses();
if (changeAddresses.isEmpty) {

View file

@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/node.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart';
@ -394,7 +395,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
// if the confirmations haven't changed, skip updating:
if (tx.confirmations == confirmations) continue;
// if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin):
if (confirmations >= 2 &&
tx.direction == TransactionDirection.outgoing &&
@ -967,10 +967,19 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
List<ECPrivateInfo>? inputPrivKeyInfos,
List<Outpoint>? vinOutpoints,
}) async {
final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
final paysToMweb = outputs
bool spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb);
bool paysToMweb = outputs
.any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
if (!spendsMweb && !paysToMweb) {
bool isRegular = !spendsMweb && !paysToMweb;
bool isMweb = spendsMweb || paysToMweb;
if (isMweb && !mwebEnabled) {
throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
// TODO: likely the change address is mweb and just not updated
}
if (isRegular) {
return await super.calcFee(
utxos: utxos,
outputs: outputs,
@ -982,10 +991,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
);
}
if (!mwebEnabled) {
throw Exception("MWEB is not enabled! can't calculate fee without starting the mweb server!");
}
if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) {
outputs = [
BitcoinScriptOutput(
@ -1056,7 +1061,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
if (!mwebEnabled) {
tx.changeAddressOverride =
(await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false))
(await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb))
.address;
return tx;
}
@ -1104,13 +1109,15 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
}
// could probably be simplified but left for clarity:
bool isPegIn = !hasMwebInput && hasMwebOutput;
bool isPegOut = hasMwebInput && hasRegularOutput;
bool isRegular = !hasMwebInput && !hasMwebOutput;
bool shouldNotUseMwebChange = isPegIn || isRegular || !hasMwebInput;
tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses)
.getChangeAddress(isPegIn: isPegIn || isRegular))
.getChangeAddress(coinTypeToSpendFrom: shouldNotUseMwebChange ? UnspentCoinType.nonMweb : UnspentCoinType.any))
.address;
if (!hasMwebInput && !hasMwebOutput) {
if (isRegular) {
tx.isMweb = false;
return tx;
}
@ -1213,7 +1220,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
Future<void> setMwebEnabled(bool enabled) async {
if (mwebEnabled == enabled) {
if (mwebEnabled == enabled &&
alwaysScan == enabled &&
(walletAddresses as LitecoinWalletAddresses).mwebEnabled == enabled) {
return;
}

View file

@ -9,6 +9,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_mweb/cw_mweb.dart';
@ -148,10 +149,12 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
@action
@override
Future<BitcoinAddressRecord> getChangeAddress(
{List<BitcoinUnspent>? inputs, List<BitcoinOutput>? outputs, bool isPegIn = false}) async {
{List<BitcoinUnspent>? inputs,
List<BitcoinOutput>? outputs,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
// use regular change address on peg in, otherwise use mweb for change address:
if (!mwebEnabled || isPegIn) {
if (!mwebEnabled || coinTypeToSpendFrom == UnspentCoinType.nonMweb) {
return super.getChangeAddress();
}
@ -178,19 +181,17 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
});
bool isPegIn = !comesFromMweb && outputsToMweb;
bool isNonMweb = !comesFromMweb && !outputsToMweb;
if (isPegIn && mwebEnabled) {
return super.getChangeAddress();
}
// use regular change address if it's not an mweb tx:
if (!comesFromMweb && !outputsToMweb) {
// use regular change address if it's not an mweb tx or if it's a peg in:
if (isPegIn || isNonMweb) {
return super.getChangeAddress();
}
}
if (mwebEnabled) {
await ensureMwebAddressUpToIndexExists(1);
updateChangeAddresses();
return BitcoinAddressRecord(
mwebAddrs[0],
index: 0,

View file

@ -316,6 +316,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.10"
decimal:
dependency: transitive
description:
name: decimal
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
encrypt:
dependency: transitive
description:
@ -797,6 +805,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.2"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
rxdart:
dependency: "direct main"
description:

View file

@ -1,3 +1,5 @@
import 'package:decimal/decimal.dart';
import 'package:decimal/intl.dart';
import 'package:intl/intl.dart';
import 'package:cw_core/crypto_currency.dart';
@ -43,6 +45,8 @@ class AmountConverter {
case CryptoCurrency.xnzd:
case CryptoCurrency.xusd:
return _moneroAmountToString(amount);
case CryptoCurrency.zano:
return _moneroAmountToStringUsingDecimals(amount);
default:
return '';
}
@ -59,4 +63,10 @@ class AmountConverter {
static String _wowneroAmountToString(int amount) => _wowneroAmountFormat
.format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider));
static Decimal cryptoAmountToDecimal({required int amount, required int divider}) =>
(Decimal.fromInt(amount) / Decimal.fromInt(divider)).toDecimal();
static String _moneroAmountToStringUsingDecimals(int amount) => _moneroAmountFormat.format(
DecimalIntl(cryptoAmountToDecimal(amount: amount, divider: _moneroAmountDivider)));
}

View file

@ -106,7 +106,9 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.usdcTrc20,
CryptoCurrency.tbtc,
CryptoCurrency.wow,
CryptoCurrency.zano,
CryptoCurrency.ton,
CryptoCurrency.flip
];
static const havenCurrencies = [
@ -225,7 +227,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8);
static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11);
static const ton = CryptoCurrency(title: 'TON', fullName: 'Toncoin', raw: 95, name: 'ton', iconPath: 'assets/images/ton_icon.png', decimals: 8);
static const zano = CryptoCurrency(title: 'ZANO', tag: 'ZANO', fullName: 'Zano', raw: 96, name: 'zano', iconPath: 'assets/images/zano_icon.png', decimals: 12);
static const flip = CryptoCurrency(title: 'FLIP', tag: 'ETH', fullName: 'Chainflip', raw: 97, name: 'flip', iconPath: 'assets/images/flip_icon.png', decimals: 18);
static final Map<int, CryptoCurrency> _rawCurrencyMap =
[...all, ...havenCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) {

View file

@ -30,8 +30,11 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) {
return CryptoCurrency.trx;
case WalletType.wownero:
return CryptoCurrency.wow;
case WalletType.zano:
return CryptoCurrency.zano;
case WalletType.none:
throw Exception(
'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
}
}

View file

@ -56,3 +56,13 @@ class CreateAssociatedTokenAccountException implements Exception {
class SignSPLTokenTransactionRentException implements Exception {}
class NoAssociatedTokenAccountException implements Exception {}
/// ==============================================================================
/// ==============================================================================
class RestoreFromSeedException implements Exception {
final String message;
RestoreFromSeedException(this.message);
}

View file

@ -19,4 +19,5 @@ const DERIVATION_INFO_TYPE_ID = 17;
const TRON_TOKEN_TYPE_ID = 18;
const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
const MWEB_UTXO_TYPE_ID = 20;
const HAVEN_SEED_STORE_TYPE_ID = 21;
const HAVEN_SEED_STORE_TYPE_ID = 21;
const ZANO_ASSET_TYPE_ID = 22;

View file

@ -4,11 +4,13 @@ class MoneroWalletKeys {
required this.privateSpendKey,
required this.privateViewKey,
required this.publicSpendKey,
required this.publicViewKey});
required this.publicViewKey,
required this.passphrase});
final String primaryAddress;
final String publicViewKey;
final String privateViewKey;
final String publicSpendKey;
final String privateSpendKey;
final String passphrase;
}

View file

@ -1,13 +1,16 @@
import 'dart:io';
import 'package:cw_core/keyable.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:hive/hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/io_client.dart' as ioc;
import 'dart:math' as math;
import 'package:convert/convert.dart';
// import 'package:tor/tor.dart';
import 'package:crypto/crypto.dart';
part 'node.g.dart';
@ -99,8 +102,9 @@ class Node extends HiveObject with Keyable {
case WalletType.polygon:
case WalletType.solana:
case WalletType.tron:
case WalletType.zano:
return Uri.parse(
"http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") ? path : "/$path"}");
"http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}");
case WalletType.none:
throw Exception('Unexpected type ${type.toString()} for Node uri');
}
@ -161,6 +165,8 @@ class Node extends HiveObject with Keyable {
case WalletType.solana:
case WalletType.tron:
return requestElectrumServer();
case WalletType.zano:
return requestZanoNode();
case WalletType.none:
return false;
}
@ -169,35 +175,71 @@ class Node extends HiveObject with Keyable {
}
}
Future<bool> requestMoneroNode() async {
if (uri.toString().contains(".onion") || useSocksProxy) {
return await requestNodeWithProxy();
}
Future<bool> requestZanoNode() async {
final path = '/json_rpc';
final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path);
final realm = 'monero-rpc';
final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'};
final body = {'jsonrpc': '2.0', 'id': '0', 'method': "getinfo"};
try {
final authenticatingClient = HttpClient();
authenticatingClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
authenticatingClient.addCredentials(
rpcUri,
realm,
HttpClientDigestCredentials(login ?? '', password ?? ''),
);
final http.Client client = ioc.IOClient(authenticatingClient);
final jsonBody = json.encode(body);
final response = await client.post(
rpcUri,
headers: {'Content-Type': 'application/json'},
body: json.encode(body),
body: jsonBody,
);
client.close();
printV("node check response: ${response.body}");
final resBody = json.decode(response.body) as Map<String, dynamic>;
return resBody['result']['height'] != null;
} catch (e) {
printV("error: $e");
return false;
}
}
Future<bool> requestMoneroNode({String methodName = 'get_info'}) async {
if (useSocksProxy) {
return await requestNodeWithProxy();
}
final path = '/json_rpc';
final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path);
final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
try {
final authenticatingClient = HttpClient();
authenticatingClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
final http.Client client = ioc.IOClient(authenticatingClient);
final jsonBody = json.encode(body);
final response = await client.post(
rpcUri,
headers: {'Content-Type': 'application/json'},
body: jsonBody,
);
// Check if we received a 401 Unauthorized response
if (response.statusCode == 401) {
final daemonRpc = DaemonRpc(
rpcUri.toString(),
username: login ?? '',
password: password ?? '',
);
final response = await daemonRpc.call('get_info', {});
return !(response['offline'] as bool);
}
printV("node check response: ${response.body}");
if ((response.body.contains("400 Bad Request") // Some other generic error
||
@ -212,7 +254,7 @@ class Node extends HiveObject with Keyable {
final oldUseSSL = useSSL;
useSSL = true;
try {
final ret = await requestMoneroNode();
final ret = await requestMoneroNode(methodName: methodName);
if (ret == true) {
await save();
return ret;
@ -225,7 +267,8 @@ class Node extends HiveObject with Keyable {
final resBody = json.decode(response.body) as Map<String, dynamic>;
return !(resBody['result']['offline'] as bool);
} catch (_) {
} catch (e) {
printV("error: $e");
return false;
}
}
@ -278,10 +321,7 @@ class Node extends HiveObject with Keyable {
try {
final response = await http.post(
uri,
headers: {
"Content-Type": "application/json",
"nano-app": "cake-wallet"
},
headers: {"Content-Type": "application/json", "nano-app": "cake-wallet"},
body: jsonEncode(
{
"action": "account_balance",
@ -316,3 +356,149 @@ class Node extends HiveObject with Keyable {
}
}
}
/// https://github.com/ManyMath/digest_auth/
/// HTTP Digest authentication.
///
/// Adapted from https://github.com/dart-lang/http/issues/605#issue-963962341.
///
/// Created because http_auth was not working for Monero daemon RPC responses.
class DigestAuth {
final String username;
final String password;
String? realm;
String? nonce;
String? uri;
String? qop = "auth";
int _nonceCount = 0;
DigestAuth(this.username, this.password);
/// Initialize Digest parameters from the `WWW-Authenticate` header.
void initFromAuthorizationHeader(String authInfo) {
final Map<String, String>? values = _splitAuthenticateHeader(authInfo);
if (values != null) {
realm = values['realm'];
// Check if the nonce has changed.
if (nonce != values['nonce']) {
nonce = values['nonce'];
_nonceCount = 0; // Reset nonce count when nonce changes.
}
}
}
/// Generate the Digest Authorization header.
String getAuthString(String method, String uri) {
this.uri = uri;
_nonceCount++;
String cnonce = _computeCnonce();
String nc = _formatNonceCount(_nonceCount);
String ha1 = md5Hash("$username:$realm:$password");
String ha2 = md5Hash("$method:$uri");
String response = md5Hash("$ha1:$nonce:$nc:$cnonce:$qop:$ha2");
return 'Digest username="$username", realm="$realm", nonce="$nonce", uri="$uri", qop=$qop, nc=$nc, cnonce="$cnonce", response="$response"';
}
/// Helper to parse the `WWW-Authenticate` header.
Map<String, String>? _splitAuthenticateHeader(String? header) {
if (header == null || !header.startsWith('Digest ')) {
return null;
}
String token = header.substring(7); // Remove 'Digest '.
final Map<String, String> result = {};
final components = token.split(',').map((token) => token.trim());
for (final component in components) {
final kv = component.split('=');
final key = kv[0];
final value = kv.sublist(1).join('=').replaceAll('"', '');
result[key] = value;
}
return result;
}
/// Helper to compute a random cnonce.
String _computeCnonce() {
final math.Random rnd = math.Random();
final List<int> values = List<int>.generate(16, (i) => rnd.nextInt(256));
return hex.encode(values);
}
/// Helper to format the nonce count.
String _formatNonceCount(int count) => count.toRadixString(16).padLeft(8, '0');
/// Compute the MD5 hash of a string.
String md5Hash(String input) {
return md5.convert(utf8.encode(input)).toString();
}
}
class DaemonRpc {
final String rpcUrl;
final String username;
final String password;
DaemonRpc(this.rpcUrl, {required this.username, required this.password});
/// Perform a JSON-RPC call with Digest Authentication.
Future<Map<String, dynamic>> call(String method, Map<String, dynamic> params) async {
final http.Client client = http.Client();
final DigestAuth digestAuth = DigestAuth(username, password);
// Initial request to get the `WWW-Authenticate` header.
final initialResponse = await client.post(
Uri.parse(rpcUrl),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'jsonrpc': '2.0',
'id': '0',
'method': method,
'params': params,
}),
);
if (initialResponse.statusCode != 401 ||
!initialResponse.headers.containsKey('www-authenticate')) {
throw Exception('Unexpected response: ${initialResponse.body}');
}
// Extract Digest details from `WWW-Authenticate` header.
final String authInfo = initialResponse.headers['www-authenticate']!;
digestAuth.initFromAuthorizationHeader(authInfo);
// Create Authorization header for the second request.
String uri = Uri.parse(rpcUrl).path;
String authHeader = digestAuth.getAuthString('POST', uri);
// Make the authenticated request.
final authenticatedResponse = await client.post(
Uri.parse(rpcUrl),
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader,
},
body: jsonEncode({
'jsonrpc': '2.0',
'id': '0',
'method': method,
'params': params,
}),
);
if (authenticatedResponse.statusCode != 200) {
throw Exception('RPC call failed: ${authenticatedResponse.body}');
}
final Map<String, dynamic> result =
jsonDecode(authenticatedResponse.body) as Map<String, dynamic>;
if (result['error'] != null) {
throw Exception('RPC Error: ${result['error']}');
}
return result['result'] as Map<String, dynamic>;
}
}

View file

@ -16,6 +16,7 @@ const walletTypes = [
WalletType.polygon,
WalletType.solana,
WalletType.tron,
WalletType.zano,
];
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
@ -58,6 +59,10 @@ enum WalletType {
@HiveField(12)
wownero,
@HiveField(13)
zano,
}
int serializeToInt(WalletType type) {
@ -86,6 +91,8 @@ int serializeToInt(WalletType type) {
return 10;
case WalletType.wownero:
return 11;
case WalletType.zano:
return 12;
case WalletType.none:
return -1;
}
@ -117,8 +124,11 @@ WalletType deserializeFromInt(int raw) {
return WalletType.tron;
case 11:
return WalletType.wownero;
case 12:
return WalletType.zano;
default:
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
throw Exception(
'Unexpected token: $raw for WalletType deserializeFromInt');
}
}
@ -148,6 +158,8 @@ String walletTypeToString(WalletType type) {
return 'Tron';
case WalletType.wownero:
return 'Wownero';
case WalletType.zano:
return 'Zano';
case WalletType.none:
return '';
}
@ -179,6 +191,8 @@ String walletTypeToDisplayName(WalletType type) {
return 'Tron (TRX)';
case WalletType.wownero:
return 'Wownero (WOW)';
case WalletType.zano:
return 'Zano (ZANO)';
case WalletType.none:
return '';
}
@ -213,6 +227,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal
return CryptoCurrency.trx;
case WalletType.wownero:
return CryptoCurrency.wow;
case WalletType.zano:
return CryptoCurrency.zano;
case WalletType.none:
throw Exception(
'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');

131
cw_core/lib/zano_asset.dart Normal file
View file

@ -0,0 +1,131 @@
import 'dart:convert';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'zano_asset.g.dart';
@HiveType(typeId: ZanoAsset.typeId)
class ZanoAsset extends CryptoCurrency with HiveObjectMixin {
@HiveField(0)
final String fullName;
@HiveField(1)
final String ticker;
@HiveField(2)
final String assetId;
@HiveField(3)
final int decimalPoint;
@HiveField(4, defaultValue: true)
bool _enabled;
@HiveField(5)
final String? iconPath;
// @HiveField(6)
// final String? tag;
@HiveField(6)
final String owner;
@HiveField(7)
final String metaInfo;
@HiveField(8)
final BigInt currentSupply;
@HiveField(9)
final bool hiddenSupply;
@HiveField(10)
final BigInt totalMaxSupply;
@HiveField(11)
final bool isInGlobalWhitelist;
@HiveField(12, defaultValue: null)
final Map<String, dynamic>? info;
bool get enabled => _enabled;
set enabled(bool value) => _enabled = value;
ZanoAsset({
this.fullName = '',
this.ticker = '',
required this.assetId,
this.decimalPoint = 12,
bool enabled = true,
this.iconPath,
this.owner = defaultOwner,
this.metaInfo = '',
required this.currentSupply,
this.hiddenSupply = false,
required this.totalMaxSupply,
this.isInGlobalWhitelist = false,
this.info,
}) : _enabled = enabled,
super(
name: fullName,
title: ticker.toUpperCase(),
fullName: fullName,
tag: 'ZANO',
iconPath: iconPath,
decimals: decimalPoint,
);
ZanoAsset.copyWith(ZanoAsset other, {String? assetId, bool enabled = true})
: this.fullName = other.fullName,
this.ticker = other.ticker,
this.assetId = assetId ?? other.assetId,
this.decimalPoint = other.decimalPoint,
this._enabled = enabled && other.enabled,
this.iconPath = other.iconPath,
this.currentSupply = other.currentSupply,
this.hiddenSupply = other.hiddenSupply,
this.metaInfo = other.metaInfo,
this.owner = other.owner,
this.totalMaxSupply = other.totalMaxSupply,
this.isInGlobalWhitelist = other.isInGlobalWhitelist,
this.info = other.info,
super(
name: other.name,
title: other.ticker.toUpperCase(),
fullName: other.name,
tag: 'ZANO',
iconPath: other.iconPath,
decimals: other.decimalPoint,
enabled: enabled,
);
factory ZanoAsset.fromJson(Map<String, dynamic> json, {bool isInGlobalWhitelist = false}) {
Map<String, dynamic>? info;
try {
info = jsonDecode((json['meta_info'] as String?) ?? '{}') as Map<String, dynamic>?;
} catch (_) {}
return ZanoAsset(
assetId: json['asset_id'] as String? ?? '',
currentSupply: bigIntFromDynamic(json['current_supply']),
decimalPoint: json['decimal_point'] as int? ?? 12,
fullName: json['full_name'] as String? ?? '',
hiddenSupply: json['hidden_supply'] as bool? ?? false,
metaInfo: json['meta_info'] as String? ?? '',
owner: json['owner'] as String? ?? '',
ticker: json['ticker'] as String? ?? '',
iconPath: info?['logo_url'] as String? ?? '',
totalMaxSupply: bigIntFromDynamic(json['total_max_supply']),
isInGlobalWhitelist: isInGlobalWhitelist,
info: info,
);
}
static const typeId = ZANO_ASSET_TYPE_ID;
static const zanoAssetsBoxName = 'zanoAssetsBox';
static const defaultOwner = '0000000000000000000000000000000000000000000000000000000000000000';
}
BigInt bigIntFromDynamic(dynamic d) {
if (d is int) {
return BigInt.from(d);
} else if (d is BigInt) {
return d;
} else if (d == null) {
return BigInt.zero;
} else {
throw 'cannot cast value of type ${d.runtimeType} to BigInt';
//return BigInt.zero;
}
}

View file

@ -207,6 +207,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.7"
decimal:
dependency: "direct main"
description:
name: decimal
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
encrypt:
dependency: "direct main"
description:
@ -577,6 +585,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shelf:
dependency: transitive
description:

View file

@ -19,6 +19,7 @@ dependencies:
flutter_mobx: ^2.0.6+1
intl: ^0.19.0
encrypt: ^5.0.1
decimal: ^2.3.3
cake_backup:
git:
url: https://github.com/cake-tech/cake_backup.git

View file

@ -290,6 +290,13 @@ class DefaultEthereumErc20Tokens {
decimal: 6,
enabled: false,
),
Erc20Token(
name: "Chainflip",
symbol: "FLIP",
contractAddress: "0x826180541412D574cf1336d22c0C0a287822678A",
decimal: 18,
enabled: false,
),
];
List<Erc20Token> get initialErc20Tokens => _defaultTokens.map((token) {

View file

@ -268,12 +268,18 @@ abstract class EVMChainClient {
Future<EVMChainERC20Balance> fetchERC20Balances(
EthereumAddress userAddress, String contractAddress) async {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final balance = await erc20.balanceOf(userAddress);
try {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final balance = await erc20.balanceOf(userAddress);
int exponent = (await erc20.decimals()).toInt();
return EVMChainERC20Balance(balance, exponent: exponent);
return EVMChainERC20Balance(balance, exponent: exponent);
} on RangeError catch (_) {
throw Exception('Invalid token contract for this network.');
} catch (e) {
throw Exception('Could not fetch balances: ${e.toString()}');
}
}
Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async {

View file

@ -634,14 +634,21 @@ abstract class EVMChainWalletBase
final newToken = createNewErc20TokenObject(token, iconPath);
await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
if (newToken.enabled) {
balance[newToken] = await _client.fetchERC20Balances(
_evmChainPrivateKey.address,
newToken.contractAddress,
);
try {
final erc20Balance = await _client.fetchERC20Balances(
_evmChainPrivateKey.address,
newToken.contractAddress,
);
balance[newToken] = erc20Balance;
await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
} on Exception catch (_) {
rethrow;
}
} else {
await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
balance.remove(newToken);
}
}

View file

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:math';
import 'package:intl/intl.dart';
import 'package:cw_core/balance.dart';
@ -17,8 +18,10 @@ class EVMChainERC20Balance extends Balance {
String get formattedAvailableBalance => _balance();
String _balance() {
final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString();
return formattedBalance.substring(0, min(12, formattedBalance.length));
NumberFormat formatter = NumberFormat('0.00##########', 'en_US');
double numBalance = (balance / BigInt.from(10).pow(exponent)).toDouble();
String formattedBalance = formatter.format(numBalance);
return formattedBalance;
}
String toJSON() => json.encode({

View file

@ -78,7 +78,8 @@ abstract class HavenWalletBase
privateSpendKey: haven_wallet.getSecretSpendKey(),
privateViewKey: haven_wallet.getSecretViewKey(),
publicSpendKey: haven_wallet.getPublicSpendKey(),
publicViewKey: haven_wallet.getPublicViewKey());
publicViewKey: haven_wallet.getPublicViewKey(),
passphrase: "");
haven_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;

View file

@ -209,6 +209,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.4"
decimal:
dependency: transitive
description:
name: decimal
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
encrypt:
dependency: transitive
description:
@ -571,6 +579,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shelf:
dependency: transitive
description:

View file

@ -8,6 +8,7 @@ import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
import 'package:flutter/foundation.dart';
import 'package:monero/monero.dart' as monero;
import 'package:mutex/mutex.dart';
import 'package:polyseed/polyseed.dart';
bool debugMonero = false;
@ -34,20 +35,34 @@ String getFilename() => monero.Wallet_filename(wptr!);
String getSeed() {
// monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
final cakepolyseed = monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
final cakepolyseed =
monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
final cakepassphrase = getPassphrase();
final weirdPolyseed = monero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase);
if (weirdPolyseed != "") return weirdPolyseed;
if (cakepolyseed != "") {
if (cakepassphrase != "") {
try {
final lang = PolyseedLang.getByPhrase(cakepolyseed);
final coin = PolyseedCoin.POLYSEED_MONERO;
final ps = Polyseed.decode(cakepolyseed, lang, coin);
if (ps.isEncrypted || cakepassphrase == "") return ps.encode(lang, coin);
ps.crypt(cakepassphrase);
return ps.encode(lang, coin);
} catch (e) {
printV(e);
}
}
return cakepolyseed;
}
final polyseed = monero.Wallet_getPolyseed(wptr!, passphrase: '');
if (polyseed != "") {
return polyseed;
}
final legacy = getSeedLegacy("English");
final legacy = getSeedLegacy(null);
return legacy;
}
String getSeedLegacy(String? language) {
var legacy = monero.Wallet_seed(wptr!, seedOffset: '');
final cakepassphrase = getPassphrase();
var legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
switch (language) {
case "Chinese (Traditional)":
language = "Chinese (simplified)";
@ -67,7 +82,7 @@ String getSeedLegacy(String? language) {
}
if (monero.Wallet_status(wptr!) != 0) {
monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
legacy = monero.Wallet_seed(wptr!, seedOffset: '');
legacy = monero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
}
if (monero.Wallet_status(wptr!) != 0) {
final err = monero.Wallet_errorString(wptr!);
@ -79,6 +94,10 @@ String getSeedLegacy(String? language) {
return legacy;
}
String getPassphrase() {
return monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase");
}
Map<int, Map<int, Map<int, String>>> addressCache = {};
String getAddress({int accountIndex = 0, int addressIndex = 0}) {

View file

@ -70,6 +70,7 @@ void createWalletSync(
{required String path,
required String password,
required String language,
required String passphrase,
int nettype = 0}) {
txhistory = null;
final newWptr = monero.WalletManager_createWallet(wmPtr,
@ -80,6 +81,7 @@ void createWalletSync(
throw WalletCreationException(message: monero.Wallet_errorString(newWptr));
}
wptr = newWptr;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
monero.Wallet_store(wptr!, path: path);
openedWalletsByPath[path] = wptr!;
_lastOpenedWallet = path;
@ -95,6 +97,7 @@ bool isWalletExistSync({required String path}) {
void restoreWalletFromSeedSync(
{required String path,
required String password,
required String passphrase,
required String seed,
int nettype = 0,
int restoreHeight = 0}) {
@ -105,7 +108,7 @@ void restoreWalletFromSeedSync(
password: password,
mnemonic: seed,
restoreHeight: restoreHeight,
seedOffset: '',
seedOffset: passphrase,
networkType: 0,
);
@ -113,11 +116,22 @@ void restoreWalletFromSeedSync(
if (status != 0) {
final error = monero.Wallet_errorString(newWptr);
if (error.contains('word list failed verification')) {
throw WalletRestoreFromSeedException(
message: "Seed verification failed, please make sure you entered the correct seed with the correct words order",
);
}
throw WalletRestoreFromSeedException(message: error);
}
wptr = newWptr;
setRefreshFromBlockHeight(height: restoreHeight);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
openedWalletsByPath[path] = wptr!;
monero.Wallet_store(wptr!);
_lastOpenedWallet = path;
}
@ -189,6 +203,48 @@ void restoreWalletFromKeysSync(
_lastOpenedWallet = path;
}
// English only, because normalization.
void restoreWalletFromPolyseedWithOffset(
{required String path,
required String password,
required String seed,
required String seedOffset,
required String language,
int nettype = 0}) {
txhistory = null;
final newWptr = monero.WalletManager_createWalletFromPolyseed(
wmPtr,
path: path,
password: password,
networkType: nettype,
mnemonic: seed,
seedOffset: seedOffset,
newWallet: true, // safe to remove
restoreHeight: 0,
kdfRounds: 1,
);
final status = monero.Wallet_status(newWptr);
if (status != 0) {
final err = monero.Wallet_errorString(newWptr);
printV("err: $err");
throw WalletRestoreFromKeysException(message: err);
}
wptr = newWptr;
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset);
monero.Wallet_store(wptr!);
storeSync();
openedWalletsByPath[path] = wptr!;
}
void restoreWalletFromSpendKeySync(
{required String path,
required String password,
@ -341,18 +397,20 @@ void _createWallet(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final language = args['language'] as String;
final passphrase = args['passphrase'] as String;
createWalletSync(path: path, password: password, language: language);
createWalletSync(path: path, password: password, language: language, passphrase: passphrase);
}
void _restoreFromSeed(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final passphrase = args['passphrase'] as String;
final seed = args['seed'] as String;
final restoreHeight = args['restoreHeight'] as int;
restoreWalletFromSeedSync(
path: path, password: password, seed: seed, restoreHeight: restoreHeight);
return restoreWalletFromSeedSync(
path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight);
}
void _restoreFromKeys(Map<String, dynamic> args) {
@ -409,23 +467,27 @@ Future<void> createWallet(
{required String path,
required String password,
required String language,
required String passphrase,
int nettype = 0}) async =>
_createWallet({
'path': path,
'password': password,
'language': language,
'passphrase': passphrase,
'nettype': nettype
});
Future<void> restoreFromSeed(
void restoreFromSeed(
{required String path,
required String password,
required String passphrase,
required String seed,
int nettype = 0,
int restoreHeight = 0}) async =>
int restoreHeight = 0}) =>
_restoreFromSeed({
'path': path,
'password': password,
'passphrase': passphrase,
'seed': seed,
'nettype': nettype,
'restoreHeight': restoreHeight

View file

@ -56,14 +56,12 @@ void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) {
void keepAlive(LedgerConnection connection) {
if (connection.connectionType == ConnectionType.ble) {
_ledgerKeepAlive = Timer.periodic(Duration(seconds: 10), (_) async {
try {
UniversalBle.setNotifiable(
connection.device.id,
connection.device.deviceInfo.serviceId,
connection.device.deviceInfo.notifyCharacteristicKey,
BleInputProperty.notification,
);
} catch (_) {}
UniversalBle.setNotifiable(
connection.device.id,
connection.device.deviceInfo.serviceId,
connection.device.deviceInfo.notifyCharacteristicKey,
BleInputProperty.notification,
).onError((_, __) async {});
});
}
}

View file

@ -1,9 +1,7 @@
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_core/parseBoolFromString.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/format_amount.dart';
import 'package:cw_monero/api/transaction_history.dart';
class MoneroTransactionInfo extends TransactionInfo {
MoneroTransactionInfo(this.txHash, this.height, this.direction, this.date,
@ -11,29 +9,6 @@ class MoneroTransactionInfo extends TransactionInfo {
this.confirmations) :
id = "${txHash}_${amount}_${accountIndex}_${addressIndex}";
MoneroTransactionInfo.fromMap(Map<String, Object?> map)
: id = "${map['hash']}_${map['amount']}_${map['accountIndex']}_${map['addressIndex']}",
txHash = map['hash'] as String,
height = (map['height'] ?? 0) as int,
direction = map['direction'] != null
? parseTransactionDirectionFromNumber(map['direction'] as String)
: TransactionDirection.incoming,
date = DateTime.fromMillisecondsSinceEpoch(
(int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000),
isPending = parseBoolFromString(map['isPending'] as String),
amount = map['amount'] as int,
accountIndex = int.parse(map['accountIndex'] as String),
addressIndex = map['addressIndex'] as int,
confirmations = map['confirmations'] as int,
key = getTxKey((map['hash'] ?? '') as String),
fee = map['fee'] as int? ?? 0 {
additionalInfo = <String, dynamic>{
'key': key,
'accountIndex': accountIndex,
'addressIndex': addressIndex
};
}
final String id;
final String txHash;
final int height;

View file

@ -120,15 +120,20 @@ abstract class MoneroWalletBase
@override
String get password => _password;
@override
String get passphrase => monero_wallet.getPassphrase();
@override
MoneroWalletKeys get keys => MoneroWalletKeys(
primaryAddress: monero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
privateSpendKey: monero_wallet.getSecretSpendKey(),
privateViewKey: monero_wallet.getSecretViewKey(),
publicSpendKey: monero_wallet.getPublicSpendKey(),
publicViewKey: monero_wallet.getPublicViewKey());
publicViewKey: monero_wallet.getPublicViewKey(),
passphrase: monero_wallet.getPassphrase());
int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height;
int? get restoreHeight =>
transactionHistory.transactions.values.firstOrNull?.height ?? monero.Wallet_getRefreshFromBlockHeight(wptr!);
monero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
@ -172,6 +177,23 @@ abstract class MoneroWalletBase
@override
Future<void> close({bool shouldCleanup = false}) async {
if (isHardwareWallet) {
disableLedgerExchange();
final currentWalletDirPath = await pathForWalletDir(name: name, type: type);
if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) {
printV("closing wallet");
final wmaddr = wmPtr.address;
final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address;
await Isolate.run(() {
monero.WalletManager_closeWallet(
Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
});
openedWalletsByPath.remove("$currentWalletDirPath/$name");
wptr = null;
printV("wallet closed");
}
}
_listener?.stop();
_onAccountChangeReaction?.reaction.dispose();
_onTxHistoryChangeReaction?.reaction.dispose();

View file

@ -11,6 +11,8 @@ import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_monero/api/account_list.dart';
import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager;
import 'package:cw_monero/api/wallet_manager.dart';
@ -24,11 +26,12 @@ import 'package:polyseed/polyseed.dart';
class MoneroNewWalletCredentials extends WalletCredentials {
MoneroNewWalletCredentials(
{required String name, required this.language, required this.isPolyseed, String? password})
{required String name, required this.language, required this.isPolyseed, String? password, this.passphrase})
: super(name: name, password: password);
final String language;
final bool isPolyseed;
final String? passphrase;
}
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
@ -42,10 +45,15 @@ class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
MoneroRestoreWalletFromSeedCredentials(
{required String name, required this.mnemonic, 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;
final String passphrase;
}
class MoneroWalletLoadingException implements Exception {
@ -85,7 +93,7 @@ class MoneroWalletService extends WalletService<
@override
WalletType getType() => WalletType.monero;
@override
@override
Future<MoneroWallet> create(MoneroNewWalletCredentials credentials, {bool? isTestnet}) async {
try {
final path = await pathForWallet(name: credentials.name, type: getType());
@ -94,16 +102,18 @@ class MoneroWalletService extends WalletService<
final polyseed = Polyseed.create();
final lang = PolyseedLang.getByEnglishName(credentials.language);
if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!);
final heightOverride =
getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2)));
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
overrideHeight: heightOverride);
overrideHeight: heightOverride, passphrase: credentials.passphrase);
}
await monero_wallet_manager.createWallet(
path: path, password: credentials.password!, language: credentials.language);
path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??"");
final wallet = MoneroWallet(
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
@ -282,16 +292,24 @@ class MoneroWalletService extends WalletService<
Future<MoneroWallet> restoreFromSeed(
MoneroRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
// Restore from Polyseed
if (Polyseed.isValidSeed(credentials.mnemonic)) {
return restoreFromPolyseed(credentials);
if (credentials.mnemonic.split(" ").length == 16) {
// Restore from Polyseed
try {
if (Polyseed.isValidSeed(credentials.mnemonic)) {
return restoreFromPolyseed(credentials);
}
} catch (e) {
printV("Polyseed restore failed: $e");
rethrow;
}
}
try {
final path = await pathForWallet(name: credentials.name, type: getType());
await monero_wallet_manager.restoreFromSeed(
monero_wallet_manager.restoreFromSeed(
path: path,
password: credentials.password!,
passphrase: credentials.passphrase,
seed: credentials.mnemonic,
restoreHeight: credentials.height!);
final wallet = MoneroWallet(
@ -318,7 +336,8 @@ class MoneroWalletService extends WalletService<
Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang);
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
passphrase: credentials.passphrase);
} catch (e) {
// TODO: Implement Exception for wallet list service.
printV('MoneroWalletsManager Error: $e');
@ -326,9 +345,35 @@ class MoneroWalletService extends WalletService<
}
}
Future<MoneroWallet> _restoreFromPolyseed(String path, String password, Polyseed polyseed,
WalletInfo walletInfo, PolyseedLang lang,
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async {
Future<MoneroWallet> _restoreFromPolyseed(
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
{PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO,
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.
monero_wallet_manager.restoreWalletFromPolyseedWithOffset(
path: path,
password: password,
seed: polyseed.encode(lang, coin),
seedOffset: passphrase??'',
language: "English");
final wallet = MoneroWallet(
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
password: password,
);
await wallet.init();
return wallet;
}
if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? '');
final height = overrideHeight ??
getMoneroHeigthByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000));
final spendKey = polyseed.generateKey(coin, 32).toHexString();
@ -345,6 +390,10 @@ class MoneroWalletService extends WalletService<
restoreHeight: height,
spendKey: spendKey);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??'');
final wallet = MoneroWallet(
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,

View file

@ -1 +1 @@
/Users/omarhatem/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
/home/parallels/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/

View file

@ -225,6 +225,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.10"
decimal:
dependency: transitive
description:
name: decimal
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
encrypt:
dependency: "direct main"
description:
@ -503,8 +511,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: af5277f96073917185864d3596e82b67bee54e78
resolved-ref: af5277f96073917185864d3596e82b67bee54e78
ref: "65608c09e9093f1cd42c6afd8e9131016c82574b"
resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b"
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
@ -660,6 +668,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
rxdart:
dependency: transitive
description:

View file

@ -25,8 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: af5277f96073917185864d3596e82b67bee54e78
# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
ref: 65608c09e9093f1cd42c6afd8e9131016c82574b
path: impls/monero.dart
mutex: ^3.1.0
ledger_flutter_plus: ^1.4.1

View file

@ -35,4 +35,6 @@ Icon?
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh
/Flutter/flutter_export_environment.sh
Mwebd.xcframework

View file

@ -16,11 +16,12 @@ A new Flutter plugin project.
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '11.0'
s.libraries = 'resolv'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
s.ios.vendored_frameworks = 'Mwebd.xcframework'
s.vendored_frameworks = 'Mwebd.xcframework'
s.preserve_paths = 'Mwebd.xcframework/**/*'
end

View file

@ -10,7 +10,7 @@ import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dar
import 'package:ffi/ffi.dart';
import 'package:monero/wownero.dart' as wownero;
import 'package:monero/src/generated_bindings_wownero.g.dart' as wownero_gen;
import 'package:mutex/mutex.dart';
String getTxKey(String txId) {
final ret = wownero.Wallet_getTxKey(wptr!, txid: txId);
@ -18,6 +18,7 @@ String getTxKey(String txId) {
return ret;
}
final txHistoryMutex = Mutex();
wownero.TransactionHistory? txhistory;
bool isRefreshingTx = false;
@ -26,22 +27,25 @@ Future<void> refreshTransactions() async {
isRefreshingTx = true;
txhistory ??= wownero.Wallet_history(wptr!);
final ptr = txhistory!.address;
await txHistoryMutex.acquire();
await Isolate.run(() {
wownero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
});
txHistoryMutex.release();
isRefreshingTx = false;
}
int countOfTransactions() => wownero.TransactionHistory_count(txhistory!);
List<Transaction> getAllTransactions() {
Future<List<Transaction>> getAllTransactions() async {
List<Transaction> dummyTxs = [];
await txHistoryMutex.acquire();
txhistory ??= wownero.Wallet_history(wptr!);
wownero.TransactionHistory_refresh(txhistory!);
int size = countOfTransactions();
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++) {
final fullBalance = wownero.Wallet_balance(wptr!, accountIndex: i);

View file

@ -7,6 +7,7 @@ import 'package:cw_wownero/api/account_list.dart';
import 'package:cw_wownero/api/exceptions/setup_wallet_exception.dart';
import 'package:monero/wownero.dart' as wownero;
import 'package:mutex/mutex.dart';
import 'package:polyseed/polyseed.dart';
int getSyncingHeight() {
// final height = wownero.WOWNERO_cw_WalletListener_height(getWlptr());
@ -32,21 +33,36 @@ bool isNewTransactionExist() {
String getFilename() => wownero.Wallet_filename(wptr!);
String getSeed() {
// wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
final cakepolyseed = wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
// monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
final cakepolyseed =
wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed");
final cakepassphrase = getPassphrase();
final weirdPolyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase);
if (weirdPolyseed != "") return weirdPolyseed;
if (cakepolyseed != "") {
if (cakepassphrase != "") {
try {
final lang = PolyseedLang.getByPhrase(cakepolyseed);
final coin = PolyseedCoin.POLYSEED_WOWNERO;
final ps = Polyseed.decode(cakepolyseed, lang, coin);
final passphrase = getPassphrase();
if (ps.isEncrypted || passphrase == "") return ps.encode(lang, coin);
ps.crypt(passphrase);
return ps.encode(lang, coin);
} catch (e) {
printV(e);
}
}
return cakepolyseed;
}
final polyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: '');
if (polyseed != "") {
return polyseed;
}
final legacy = getSeedLegacy(null);
return legacy;
}
String getSeedLegacy(String? language) {
var legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
final cakepassphrase = getPassphrase();
var legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
switch (language) {
case "Chinese (Traditional)":
language = "Chinese (simplified)";
@ -66,7 +82,7 @@ String getSeedLegacy(String? language) {
}
if (wownero.Wallet_status(wptr!) != 0) {
wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English");
legacy = wownero.Wallet_seed(wptr!, seedOffset: '');
legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase);
}
if (wownero.Wallet_status(wptr!) != 0) {
final err = wownero.Wallet_errorString(wptr!);
@ -80,6 +96,10 @@ String getSeedLegacy(String? language) {
Map<int, Map<int, Map<int, String>>> addressCache = {};
String getPassphrase() {
return wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.passphrase");
}
String getAddress({int accountIndex = 0, int addressIndex = 1}) {
while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex) - 1 < addressIndex) {
printV("adding subaddress");
@ -357,6 +377,5 @@ 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);
}

View file

@ -66,6 +66,7 @@ void createWalletSync(
{required String path,
required String password,
required String language,
required String passphrase,
int nettype = 0}) {
txhistory = null;
final newWptr = wownero.WalletManager_createWallet(wmPtr,
@ -76,6 +77,8 @@ void createWalletSync(
throw WalletCreationException(message: wownero.Wallet_errorString(newWptr));
}
wptr = newWptr;
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase);
wownero.Wallet_store(wptr!, path: path);
openedWalletsByPath[path] = wptr!;
@ -90,6 +93,7 @@ bool isWalletExistSync({required String path}) {
void restoreWalletFromSeedSync(
{required String path,
required String password,
required String passphrase,
required String seed,
int nettype = 0,
int restoreHeight = 0}) {
@ -102,10 +106,12 @@ void restoreWalletFromSeedSync(
language: seed, // I KNOW - this is supposed to be called seed
networkType: 0,
);
final oldwptr = wptr;
wptr = newWptr;
setRefreshFromBlockHeight(
height: wownero.WOWNERO_deprecated_14WordSeedHeight(seed: seed),
);
wptr = oldwptr;
} else {
txhistory = null;
newWptr = wownero.WalletManager_recoveryWallet(
@ -114,7 +120,7 @@ void restoreWalletFromSeedSync(
password: password,
mnemonic: seed,
restoreHeight: restoreHeight,
seedOffset: '',
seedOffset: passphrase,
networkType: 0,
);
}
@ -127,8 +133,13 @@ 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();
}
void restoreWalletFromKeysSync(
@ -196,6 +207,48 @@ void restoreWalletFromKeysSync(
openedWalletsByPath[path] = wptr!;
}
// English only, because normalization.
void restoreWalletFromPolyseedWithOffset(
{required String path,
required String password,
required String seed,
required String seedOffset,
required String language,
int nettype = 0}) {
txhistory = null;
final newWptr = wownero.WalletManager_createWalletFromPolyseed(
wmPtr,
path: path,
password: password,
networkType: nettype,
mnemonic: seed,
seedOffset: seedOffset,
newWallet: true, // safe to remove
restoreHeight: 0,
kdfRounds: 1,
);
final status = wownero.Wallet_status(newWptr);
if (status != 0) {
final err = wownero.Wallet_errorString(newWptr);
printV("err: $err");
throw WalletRestoreFromKeysException(message: err);
}
wptr = newWptr;
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: seedOffset);
storeSync();
openedWalletsByPath[path] = wptr!;
}
void restoreWalletFromSpendKeySync(
{required String path,
required String password,
@ -312,18 +365,20 @@ void _createWallet(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final language = args['language'] as String;
final passphrase = args['passphrase'] as String;
createWalletSync(path: path, password: password, language: language);
createWalletSync(path: path, password: password, language: language, passphrase: passphrase);
}
void _restoreFromSeed(Map<String, dynamic> args) {
final path = args['path'] as String;
final password = args['password'] as String;
final passphrase = args['passphrase'] as String;
final seed = args['seed'] as String;
final restoreHeight = args['restoreHeight'] as int;
restoreWalletFromSeedSync(
path: path, password: password, seed: seed, restoreHeight: restoreHeight);
path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight);
}
void _restoreFromKeys(Map<String, dynamic> args) {
@ -380,23 +435,27 @@ Future<void> createWallet(
{required String path,
required String password,
required String language,
required String passphrase,
int nettype = 0}) async =>
_createWallet({
'path': path,
'password': password,
'language': language,
'passphrase': passphrase,
'nettype': nettype
});
Future<void> restoreFromSeed(
{required String path,
required String password,
required String passphrase,
required String seed,
int nettype = 0,
int restoreHeight = 0}) async =>
_restoreFromSeed({
'path': path,
'password': password,
'passphrase': passphrase,
'seed': seed,
'nettype': nettype,
'restoreHeight': restoreHeight

View file

@ -20,6 +20,7 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wownero_amount_format.dart';
import 'package:cw_core/wownero_balance.dart';
import 'package:cw_wownero/api/account_list.dart';
import 'package:cw_wownero/api/coins_info.dart';
import 'package:cw_wownero/api/structs/pending_transaction.dart';
import 'package:cw_wownero/api/transaction_history.dart' as transaction_history;
@ -119,6 +120,9 @@ abstract class WowneroWalletBase
String get password => _password;
@override
String get passphrase => wownero_wallet.getPassphrase();
String _password;
@override
@ -127,7 +131,12 @@ abstract class WowneroWalletBase
privateSpendKey: wownero_wallet.getSecretSpendKey(),
privateViewKey: wownero_wallet.getSecretViewKey(),
publicSpendKey: wownero_wallet.getPublicSpendKey(),
publicViewKey: wownero_wallet.getPublicViewKey());
publicViewKey: wownero_wallet.getPublicViewKey(),
passphrase: wownero_wallet.getPassphrase());
int? get restoreHeight =>
transactionHistory.transactions.values.firstOrNull?.height ?? wownero.Wallet_getRefreshFromBlockHeight(wptr!);
wownero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
@ -571,7 +580,7 @@ abstract class WowneroWalletBase
@override
Future<Map<String, WowneroTransactionInfo>> fetchTransactions() async {
transaction_history.refreshTransactions();
return _getAllTransactionsOfAccount(walletAddresses.account?.id)
return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
.fold<Map<String, WowneroTransactionInfo>>(<String, WowneroTransactionInfo>{},
(Map<String, WowneroTransactionInfo> acc, WowneroTransactionInfo tx) {
acc[tx.id] = tx;
@ -600,9 +609,9 @@ abstract class WowneroWalletBase
String getSubaddressLabel(int accountIndex, int addressIndex) =>
wownero_wallet.getSubaddressLabel(accountIndex, addressIndex);
List<WowneroTransactionInfo> _getAllTransactionsOfAccount(int? accountIndex) =>
transaction_history
.getAllTransactions()
Future<List<WowneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
(await transaction_history
.getAllTransactions())
.map(
(row) => WowneroTransactionInfo(
row.hash,

View file

@ -10,6 +10,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_wownero/api/account_list.dart';
import 'package:cw_wownero/api/exceptions/wallet_opening_exception.dart';
import 'package:cw_wownero/api/wallet_manager.dart' as wownero_wallet_manager;
import 'package:cw_wownero/api/wallet_manager.dart';
@ -21,19 +22,21 @@ import 'package:monero/wownero.dart' as wownero;
class WowneroNewWalletCredentials extends WalletCredentials {
WowneroNewWalletCredentials(
{required String name, required this.language, required this.isPolyseed, String? password})
{required String name, required this.language, required this.isPolyseed, this.passphrase, String? password})
: super(name: name, password: password);
final String language;
final bool isPolyseed;
final String? passphrase;
}
class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials {
WowneroRestoreWalletFromSeedCredentials(
{required String name, required this.mnemonic, 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;
final String passphrase;
}
class WowneroWalletLoadingException implements Exception {
@ -83,16 +86,18 @@ class WowneroWalletService extends WalletService<
final polyseed = Polyseed.create();
final lang = PolyseedLang.getByEnglishName(credentials.language);
if (credentials.passphrase != null) polyseed.crypt(credentials.passphrase!);
final heightOverride =
getWowneroHeightByDate(date: DateTime.now().subtract(Duration(days: 2)));
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang,
overrideHeight: heightOverride);
overrideHeight: heightOverride, passphrase: credentials.passphrase);
}
await wownero_wallet_manager.createWallet(
path: path, password: credentials.password!, language: credentials.language);
path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??'');
final wallet = WowneroWallet(
walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!);
await wallet.init();
@ -266,6 +271,7 @@ class WowneroWalletService extends WalletService<
await wownero_wallet_manager.restoreFromSeed(
path: path,
password: credentials.password!,
passphrase: credentials.passphrase,
seed: credentials.mnemonic,
restoreHeight: credentials.height!);
final wallet = WowneroWallet(
@ -289,7 +295,7 @@ class WowneroWalletService extends WalletService<
final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin);
return _restoreFromPolyseed(
path, credentials.password!, polyseed, credentials.walletInfo!, lang);
path, credentials.password!, polyseed, credentials.walletInfo!, lang, passphrase: credentials.passphrase);
} catch (e) {
// TODO: Implement Exception for wallet list service.
printV('WowneroWalletsManager Error: $e');
@ -299,7 +305,32 @@ class WowneroWalletService extends WalletService<
Future<WowneroWallet> _restoreFromPolyseed(
String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang,
{PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight}) async {
{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");
final wallet = WowneroWallet(
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfoSource,
password: password,
);
await wallet.init();
return wallet;
}
if (polyseed.isEncrypted) polyseed.crypt(passphrase ?? '');
final height = overrideHeight ??
getWowneroHeightByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000));
final spendKey = polyseed.generateKey(coin, 32).toHexString();
@ -316,6 +347,9 @@ class WowneroWalletService extends WalletService<
restoreHeight: height,
spendKey: spendKey);
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??'');
final wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, password: password);
await wallet.init();

View file

@ -209,6 +209,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.4"
decimal:
dependency: transitive
description:
name: decimal
sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
encrypt:
dependency: "direct main"
description:
@ -463,8 +471,8 @@ packages:
dependency: "direct main"
description:
path: "impls/monero.dart"
ref: af5277f96073917185864d3596e82b67bee54e78
resolved-ref: af5277f96073917185864d3596e82b67bee54e78
ref: "65608c09e9093f1cd42c6afd8e9131016c82574b"
resolved-ref: "65608c09e9093f1cd42c6afd8e9131016c82574b"
url: "https://github.com/mrcyjanek/monero_c"
source: git
version: "0.0.0"
@ -612,6 +620,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shelf:
dependency: transitive
description:

View file

@ -25,8 +25,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c
ref: af5277f96073917185864d3596e82b67bee54e78
# ref: 6eb571ea498ed7b854934785f00fabfd0dadf75b # monero_c hash
ref: 65608c09e9093f1cd42c6afd8e9131016c82574b # monero_c hash
path: impls/monero.dart
mutex: ^3.1.0

7
cw_zano/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.DS_Store
.dart_tool/
.packages
.pub/
build/

10
cw_zano/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 4d7946a68d26794349189cf21b3f68cc6fe61dcb
channel: stable
project_type: plugin

3
cw_zano/CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
cw_zano/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

15
cw_zano/README.md Normal file
View file

@ -0,0 +1,15 @@
# cw_zano
A new flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/developing-packages/),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,6 @@
class Consts {
static const errorWrongSeed = 'WRONG_SEED';
static const errorAlreadyExists = 'ALREADY_EXISTS';
static const errorWalletWrongId = 'WALLET_WRONG_ID';
static const errorBusy = 'BUSY';
}

View file

@ -0,0 +1,9 @@
class AssetIdParams {
final String assetId;
AssetIdParams({required this.assetId});
Map<String, dynamic> toJson() => {
'asset_id': assetId,
};
}

View file

@ -0,0 +1,32 @@
import 'package:cw_core/zano_asset.dart';
import 'package:cw_zano/model/zano_asset.dart';
import 'package:cw_zano/zano_formatter.dart';
class Balance {
final ZanoAsset assetInfo;
final BigInt awaitingIn;
final BigInt awaitingOut;
final BigInt total;
final BigInt unlocked;
Balance(
{required this.assetInfo,
required this.awaitingIn,
required this.awaitingOut,
required this.total,
required this.unlocked});
String get assetId => assetInfo.assetId;
@override
String toString() => '$assetInfo: $total/$unlocked';
factory Balance.fromJson(Map<String, dynamic> json) => Balance(
assetInfo:
ZanoAsset.fromJson(json['asset_info'] as Map<String, dynamic>? ?? {}),
awaitingIn: ZanoFormatter.bigIntFromDynamic(json['awaiting_in']),
awaitingOut: ZanoFormatter.bigIntFromDynamic(json['awaiting_out']),
total: ZanoFormatter.bigIntFromDynamic(json['total']),
unlocked: ZanoFormatter.bigIntFromDynamic(json['unlocked']),
);
}

View file

@ -0,0 +1,52 @@
import 'package:cw_zano/api/model/recent_history.dart';
import 'package:cw_zano/api/model/wi.dart';
import 'package:cw_zano/zano_wallet.dart';
class CreateWalletResult {
final String name;
final String pass;
final RecentHistory recentHistory;
final bool recovered;
final int walletFileSize;
final int walletId;
final int walletLocalBcSize;
final Wi wi;
final String privateSpendKey;
final String privateViewKey;
final String publicSpendKey;
final String publicViewKey;
CreateWalletResult(
{required this.name,
required this.pass,
required this.recentHistory,
required this.recovered,
required this.walletFileSize,
required this.walletId,
required this.walletLocalBcSize,
required this.wi,
required this.privateSpendKey,
required this.privateViewKey,
required this.publicSpendKey,
required this.publicViewKey});
factory CreateWalletResult.fromJson(Map<String, dynamic> json) =>
CreateWalletResult(
name: json['name'] as String? ?? '',
pass: json['pass'] as String? ?? '',
recentHistory: RecentHistory.fromJson(
json['recent_history'] as Map<String, dynamic>? ?? {}),
recovered: json['recovered'] as bool? ?? false,
walletFileSize: json['wallet_file_size'] as int? ?? 0,
walletId: json['wallet_id'] as int? ?? 0,
walletLocalBcSize: json['wallet_local_bc_size'] as int? ?? 0,
wi: Wi.fromJson(json['wi'] as Map<String, dynamic>? ?? {}),
privateSpendKey: json['private_spend_key'] as String? ?? '',
privateViewKey: json['private_view_key'] as String? ?? '',
publicSpendKey: json['public_spend_key'] as String? ?? '',
publicViewKey: json['public_view_key'] as String? ?? '',
);
Future<String> seed(ZanoWalletBase api) {
return api.getSeed();
}
}

View file

@ -0,0 +1,20 @@
class Destination {
final BigInt amount; // transfered as string
final String address;
final String assetId;
Destination(
{required this.amount, required this.address, required this.assetId});
factory Destination.fromJson(Map<String, dynamic> json) => Destination(
amount: BigInt.parse(json['amount'] as String? ?? '0'),
address: json['address'] as String? ?? '',
assetId: json['asset_id'] as String? ?? '',
);
Map<String, dynamic> toJson() => {
'amount': amount.toString(),
'address': address,
'asset_id': assetId,
};
}

View file

@ -0,0 +1,18 @@
import 'package:cw_zano/api/model/receive.dart';
class EmployedEntries {
final List<Receive> receive;
final List<Receive> send;
EmployedEntries({required this.receive, required this.send});
factory EmployedEntries.fromJson(Map<String, dynamic> json) =>
EmployedEntries(
receive: json['receive'] == null ? [] : (json['receive'] as List<dynamic>)
.map((e) => Receive.fromJson(e as Map<String, dynamic>))
.toList(),
send: json['spent'] == null ? [] : (json['spent'] as List<dynamic>)
.map((e) => Receive.fromJson(e as Map<String, dynamic>))
.toList(),
);
}

View file

@ -0,0 +1,16 @@
class GetAddressInfoResult {
final bool valid;
final bool auditable;
final bool paymentId;
final bool wrap;
GetAddressInfoResult(
{required this.valid, required this.auditable, required this.paymentId, required this.wrap});
factory GetAddressInfoResult.fromJson(Map<String, dynamic> json) => GetAddressInfoResult(
valid: json['valid'] as bool? ?? false,
auditable: json['auditable'] as bool? ?? false,
paymentId: json['payment_id'] as bool? ?? false,
wrap: json['wrap'] as bool? ?? false,
);
}

View file

@ -0,0 +1,14 @@
class GetRecentTxsAndInfoParams {
final int offset;
final int count;
final bool updateProvisionInfo;
GetRecentTxsAndInfoParams({required this.offset, required this.count, this.updateProvisionInfo = true});
Map<String, dynamic> toJson() => {
'offset': offset,
'count': count,
'update_provision_info': updateProvisionInfo,
'order': 'FROM_BEGIN_TO_END',
};
}

View file

@ -0,0 +1,12 @@
import 'package:cw_zano/api/model/transfer.dart';
class GetRecentTxsAndInfoResult {
final List<Transfer> transfers;
final int lastItemIndex;
final int totalTransfers;
GetRecentTxsAndInfoResult({required this.transfers, required this.lastItemIndex, required this.totalTransfers});
GetRecentTxsAndInfoResult.empty(): this.transfers = [], this.lastItemIndex = 0, this.totalTransfers = 0;
}

View file

@ -0,0 +1,14 @@
import 'package:cw_zano/api/model/wi.dart';
import 'package:cw_zano/api/model/wi_extended.dart';
class GetWalletInfoResult {
final Wi wi;
final WiExtended wiExtended;
GetWalletInfoResult({required this.wi, required this.wiExtended});
factory GetWalletInfoResult.fromJson(Map<String, dynamic> json) => GetWalletInfoResult(
wi: Wi.fromJson(json['wi'] as Map<String, dynamic>? ?? {}),
wiExtended: WiExtended.fromJson(json['wi_extended'] as Map<String, dynamic>? ?? {}),
);
}

View file

@ -0,0 +1,26 @@
class GetWalletStatusResult {
final int currentDaemonHeight;
final int currentWalletHeight;
final bool isDaemonConnected;
final bool isInLongRefresh;
final int progress;
final int walletState;
GetWalletStatusResult(
{required this.currentDaemonHeight,
required this.currentWalletHeight,
required this.isDaemonConnected,
required this.isInLongRefresh,
required this.progress,
required this.walletState});
factory GetWalletStatusResult.fromJson(Map<String, dynamic> json) =>
GetWalletStatusResult(
currentDaemonHeight: json['current_daemon_height'] as int? ?? 0,
currentWalletHeight: json['current_wallet_height'] as int? ?? 0,
isDaemonConnected: json['is_daemon_connected'] as bool? ?? false,
isInLongRefresh: json['is_in_long_refresh'] as bool? ?? false,
progress: json['progress'] as int? ?? 0,
walletState: json['wallet_state'] as int? ?? 0,
);
}

View file

@ -0,0 +1,13 @@
import 'dart:convert';
class ProxyToDaemonParams {
final String body;
final String uri;
ProxyToDaemonParams({required this.body, required this.uri});
Map<String, dynamic> toJson() => {
'base64_body': base64Encode(utf8.encode(body)),
'uri': uri,
};
}

View file

@ -0,0 +1,13 @@
import 'dart:convert';
class ProxyToDaemonResult {
final String body;
final int responseCode;
ProxyToDaemonResult({required this.body, required this.responseCode});
factory ProxyToDaemonResult.fromJson(Map<String, dynamic> json) => ProxyToDaemonResult(
body: utf8.decode(base64Decode(json['base64_body'] as String? ?? '')),
responseCode: json['response_code'] as int? ?? 0,
);
}

View file

@ -0,0 +1,15 @@
import 'package:cw_zano/zano_formatter.dart';
class Receive {
final BigInt amount;
final String assetId;
final int index;
Receive({required this.amount, required this.assetId, required this.index});
factory Receive.fromJson(Map<String, dynamic> json) => Receive(
amount: ZanoFormatter.bigIntFromDynamic(json['amount']),
assetId: json['asset_id'] as String? ?? '',
index: json['index'] as int? ?? 0,
);
}

View file

@ -0,0 +1,20 @@
import 'package:cw_zano/api/model/transfer.dart';
class RecentHistory {
final List<Transfer>? history;
final int lastItemIndex;
final int totalHistoryItems;
RecentHistory(
{required this.history,
required this.lastItemIndex,
required this.totalHistoryItems});
factory RecentHistory.fromJson(Map<String, dynamic> json) => RecentHistory(
history: json['history'] == null ? null : (json['history'] as List<dynamic>)
.map((e) => Transfer.fromJson(e as Map<String, dynamic>))
.toList(),
lastItemIndex: json['last_item_index'] as int? ?? 0,
totalHistoryItems: json['total_history_items'] as int? ?? 0,
);
}

View file

@ -0,0 +1,9 @@
class StoreResult {
final int walletFileSize;
StoreResult({required this.walletFileSize});
factory StoreResult.fromJson(Map<String, dynamic> json) => StoreResult(
walletFileSize: json['wallet_file_size'] as int? ?? 0,
);
}

View file

@ -0,0 +1,16 @@
import 'package:cw_zano/zano_formatter.dart';
class Subtransfer {
final BigInt amount;
final String assetId;
final bool isIncome;
Subtransfer(
{required this.amount, required this.assetId, required this.isIncome});
factory Subtransfer.fromJson(Map<String, dynamic> json) => Subtransfer(
amount: ZanoFormatter.bigIntFromDynamic(json['amount']),
assetId: json['asset_id'] as String? ?? '',
isIncome: json['is_income'] as bool? ?? false,
);
}

View file

@ -0,0 +1,133 @@
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/zano_asset.dart';
import 'package:cw_zano/api/model/employed_entries.dart';
import 'package:cw_zano/api/model/subtransfer.dart';
import 'package:collection/collection.dart';
import 'package:cw_zano/model/zano_transaction_info.dart';
import 'package:cw_zano/zano_wallet.dart';
class Transfer {
final String comment;
final EmployedEntries employedEntries;
final int fee;
final int height;
final bool isMining;
final bool isMixing;
final bool isService;
final String paymentId;
final List<String> remoteAddresses;
final List<String> remoteAliases;
final bool showSender;
final List<Subtransfer> subtransfers;
final int timestamp;
final int transferInternalIndex;
final int txBlobSize;
final String txHash;
final int txType;
final int unlockTime;
Transfer({
required this.comment,
required this.employedEntries,
required this.fee,
required this.height,
required this.isMining,
required this.isMixing,
required this.isService,
required this.paymentId,
required this.remoteAddresses,
required this.remoteAliases,
required this.showSender,
required this.subtransfers,
required this.timestamp,
required this.transferInternalIndex,
required this.txBlobSize,
required this.txHash,
required this.txType,
required this.unlockTime,
});
factory Transfer.fromJson(Map<String, dynamic> json) =>
Transfer(
comment: json['comment'] as String? ?? '',
employedEntries: EmployedEntries.fromJson(
json['employed_entries'] as Map<String, dynamic>? ?? {}),
fee: json['fee'] as int? ?? 0,
height: json['height'] as int? ?? 0,
isMining: json['is_mining'] as bool? ?? false,
isMixing: json['is_mixing'] as bool? ?? false,
isService: json['is_service'] as bool? ?? false,
paymentId: json['payment_id'] as String? ?? '',
remoteAddresses: json['remote_addresses'] == null ? [] : (json['remote_addresses'] as List<
dynamic>).cast<String>(),
remoteAliases: json['remote_aliases'] == null ? [] : (json['remote_aliases'] as List<
dynamic>).cast<String>(),
showSender: json['show_sender'] as bool? ?? false,
subtransfers: (json['subtransfers'] as List<dynamic>? ?? []).map((e) =>
Subtransfer.fromJson(e as Map<String, dynamic>)).toList(),
timestamp: json['timestamp'] as int? ?? 0,
transferInternalIndex: json['transfer_internal_index'] == null
? 0
: json['transfer_internal_index'] is double
? (json['transfer_internal_index'] as double).toInt()
: json['transfer_internal_index'] as int,
txBlobSize: json['tx_blob_size'] as int? ?? 0,
txHash: json['tx_hash'] as String? ?? '',
txType: json['tx_type'] as int? ?? 0,
unlockTime: json['unlock_time'] as int? ?? 0,
);
static Map<String, ZanoTransactionInfo> makeMap(List<Transfer> transfers,
Map<String, ZanoAsset> zanoAssets, int currentDaemonHeight) {
return Map.fromIterable(
transfers,
key: (item) => (item as Transfer).txHash,
value: (transfer) {
transfer as Transfer;
// Simple (only one subtransfer OR two subtransfers and the second is Zano, outgoing and amount equals to fee) or complex?
Subtransfer? single = transfer.subtransfers.singleOrNull;
if (transfer.subtransfers.length == 2) {
final zano = transfer.subtransfers.firstWhereOrNull((element) =>
element.assetId == ZanoWalletBase.zanoAssetId);
if (zano != null && !zano.isIncome && zano.amount == BigInt.from(transfer.fee)) {
single = transfer.subtransfers.firstWhere((element) => element.assetId !=
ZanoWalletBase.zanoAssetId);
}
}
bool isSimple = single != null;
// TODO: for complex transactions we show zano or any other transaction, will fix it later
if (!isSimple) {
single =
transfer.subtransfers.firstWhereOrNull((element) =>
element.assetId == ZanoWalletBase.zanoAssetId) ?? transfer.subtransfers.first;
}
if (single.assetId != ZanoWalletBase.zanoAssetId) {
final asset = zanoAssets[single.assetId];
if (asset == null) {
printV('unknown asset ${single.assetId}');
}
final ticker = asset == null ? '***' : asset.ticker;
final decimalPoint = asset == null ? 0 : asset.decimalPoint;
return ZanoTransactionInfo.fromTransfer(
transfer,
confirmations: currentDaemonHeight - transfer.height,
isIncome: single.isIncome,
assetId: single.assetId,
amount: single.amount,
tokenSymbol: isSimple ? ticker : '*${ticker}',
decimalPoint: decimalPoint,
);
}
final amount = single.isIncome ? single.amount : single.amount - BigInt.from(transfer.fee);
return ZanoTransactionInfo.fromTransfer(
transfer,
confirmations: currentDaemonHeight - transfer.height,
isIncome: single.isIncome,
assetId: single.assetId,
amount: amount,
tokenSymbol: isSimple ? 'ZANO' : '*ZANO',
);
},
);
}
}

View file

@ -0,0 +1,41 @@
import 'package:cw_zano/api/model/destination.dart';
class TransferParams {
final List<Destination> destinations;
final BigInt fee;
final int mixin;
final String paymentId;
final String comment;
final bool pushPayer;
final bool hideReceiver;
TransferParams({
required this.destinations,
required this.fee,
required this.mixin,
required this.paymentId,
required this.comment,
required this.pushPayer,
required this.hideReceiver,
});
Map<String, dynamic> toJson() => {
'destinations': destinations,
'fee': fee.toInt(),
'mixin': mixin,
'payment_id': paymentId,
'comment': comment,
'push_payer': pushPayer,
'hide_receiver': hideReceiver,
};
factory TransferParams.fromJson(Map<String, dynamic> json) => TransferParams(
destinations: (json['destinations'] as List<dynamic>?)?.map((e) => Destination.fromJson(e as Map<String, dynamic>)).toList() ?? [],
fee: BigInt.from(json['fee'] as int? ?? 0),
mixin: json['mixin'] as int? ?? 0,
paymentId: json['payment_id'] as String? ?? '',
comment: json['comment'] as String? ?? '',
pushPayer: json['push_payer'] as bool? ?? false,
hideReceiver: json['hide_receiver'] as bool? ?? false,
);
}

View file

@ -0,0 +1,13 @@
class TransferResult {
final String txHash;
final int txSize;
final String txUnsignedHex;
TransferResult({required this.txHash, required this.txSize, required this.txUnsignedHex});
factory TransferResult.fromJson(Map<String, dynamic> json) => TransferResult(
txHash: json['tx_hash'] as String? ?? '',
txSize: json['tx_size'] as int? ?? 0,
txUnsignedHex: json['tx_unsigned_hex'] as String? ?? '',
);
}

View file

@ -0,0 +1,32 @@
import 'package:cw_zano/api/model/balance.dart';
class Wi {
final String address;
final List<Balance> balances;
final bool isAuditable;
final bool isWatchOnly;
final int minedTotal;
final String path;
final String viewSecKey;
Wi(
{required this.address,
required this.balances,
required this.isAuditable,
required this.isWatchOnly,
required this.minedTotal,
required this.path,
required this.viewSecKey});
factory Wi.fromJson(Map<String, dynamic> json) => Wi(
address: json['address'] as String? ?? '',
balances: (json['balances'] as List<dynamic>? ?? [])
.map((e) => Balance.fromJson(e as Map<String, dynamic>))
.toList(),
isAuditable: json['is_auditable'] as bool? ?? false,
isWatchOnly: json['is_watch_only'] as bool? ?? false,
minedTotal: json['mined_total'] as int? ?? 0,
path: json['path'] as String? ?? '',
viewSecKey: json['view_sec_key'] as String? ?? '',
);
}

View file

@ -0,0 +1,21 @@
import 'package:cw_zano/zano_wallet.dart';
class WiExtended {
final String spendPrivateKey;
final String spendPublicKey;
final String viewPrivateKey;
final String viewPublicKey;
WiExtended({required this.spendPrivateKey, required this.spendPublicKey, required this.viewPrivateKey, required this.viewPublicKey});
factory WiExtended.fromJson(Map<String, dynamic> json) => WiExtended(
spendPrivateKey: json['spend_private_key'] as String? ?? '',
spendPublicKey: json['spend_public_key'] as String? ?? '',
viewPrivateKey: json['view_private_key'] as String? ?? '',
viewPublicKey: json['view_public_key'] as String? ?? '',
);
Future<String> seed(ZanoWalletBase api) {
return api.getSeed();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_zano/api/model/destination.dart';
import 'package:cw_zano/api/model/transfer_result.dart';
import 'package:cw_zano/zano_formatter.dart';
import 'package:cw_zano/zano_wallet.dart';
class PendingZanoTransaction with PendingTransaction {
PendingZanoTransaction({
required this.zanoWallet,
required this.destinations,
required this.fee,
required this.comment,
required this.assetId,
required this.ticker,
this.decimalPoint = ZanoFormatter.defaultDecimalPoint,
required this.amount,
});
final ZanoWalletBase zanoWallet;
final List<Destination> destinations;
final BigInt fee;
final String comment;
final String assetId;
final String ticker;
final int decimalPoint;
final BigInt amount;
@override
String get id => transferResult?.txHash ?? '';
@override
String get hex => '';
@override
String get amountFormatted => ZanoFormatter.bigIntAmountToString(amount, decimalPoint);
@override
String get feeFormatted => ZanoFormatter.bigIntAmountToString(fee);
TransferResult? transferResult;
@override
Future<void> commit() async {
await zanoWallet.transfer(destinations, fee, comment);
zanoWallet.fetchTransactions();
}
@override
Future<String?> commitUR() {
throw UnimplementedError();
}
}

View file

View file

@ -0,0 +1,17 @@
import 'package:cw_core/balance.dart';
import 'package:cw_zano/zano_formatter.dart';
class ZanoBalance extends Balance {
final BigInt total;
final BigInt unlocked;
final int decimalPoint;
ZanoBalance({required this.total, required this.unlocked, this.decimalPoint = ZanoFormatter.defaultDecimalPoint}) : super(unlocked.isValidInt ? unlocked.toInt() : 0, (total - unlocked).isValidInt ? (total - unlocked).toInt() : 0);
ZanoBalance.empty({this.decimalPoint = ZanoFormatter.defaultDecimalPoint}): total = BigInt.zero, unlocked = BigInt.zero, super(0, 0);
@override
String get formattedAdditionalBalance => ZanoFormatter.bigIntAmountToString(total - unlocked, decimalPoint);
@override
String get formattedAvailableBalance => ZanoFormatter.bigIntAmountToString(unlocked, decimalPoint);
}

View file

@ -0,0 +1,8 @@
class ZanoTransactionCreationException implements Exception {
ZanoTransactionCreationException(this.message);
final String message;
@override
String toString() => message;
}

View file

@ -0,0 +1,11 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/output_info.dart';
class ZanoTransactionCredentials {
ZanoTransactionCredentials({required this.outputs, required this.priority, required this.currency});
final List<OutputInfo> outputs;
final MoneroTransactionPriority priority;
final CryptoCurrency currency;
}

View file

@ -0,0 +1,81 @@
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_zano/api/model/transfer.dart';
import 'package:cw_zano/zano_formatter.dart';
class ZanoTransactionInfo extends TransactionInfo {
ZanoTransactionInfo({
required this.id,
required this.height,
required this.direction,
required this.date,
required this.isPending,
required this.zanoAmount,
required this.fee,
required this.confirmations,
required this.tokenSymbol,
required this.decimalPoint,
required String assetId,
}) : amount = zanoAmount.isValidInt ? zanoAmount.toInt() : 0 {
additionalInfo['assetId'] = assetId;
}
ZanoTransactionInfo.fromTransfer(Transfer transfer,
{required int confirmations,
required bool isIncome,
required String assetId,
required BigInt amount,
this.tokenSymbol = 'ZANO',
this.decimalPoint = ZanoFormatter.defaultDecimalPoint})
: id = transfer.txHash,
height = transfer.height,
direction = isIncome ? TransactionDirection.incoming : TransactionDirection.outgoing,
date = DateTime.fromMillisecondsSinceEpoch(transfer.timestamp * 1000),
zanoAmount = amount,
amount = amount.isValidInt ? amount.toInt() : 0,
fee = transfer.fee,
confirmations = confirmations,
isPending = confirmations < 10,
recipientAddress = transfer.remoteAddresses.isNotEmpty
? transfer.remoteAddresses.first
: '' {
additionalInfo = <String, dynamic>{
'comment': transfer.comment,
'assetId': assetId,
};
}
String get assetId => additionalInfo["assetId"] as String;
set assetId(String newId) => additionalInfo["assetId"] = newId;
final String id;
final int height;
final TransactionDirection direction;
final DateTime date;
final bool isPending;
final BigInt zanoAmount;
final int amount;
final int fee;
final int confirmations;
final int decimalPoint;
late String recipientAddress;
final String tokenSymbol;
String? _fiatAmount;
String? key;
@override
String amountFormatted() =>
'${formatAmount(ZanoFormatter.bigIntAmountToString(zanoAmount, decimalPoint))} $tokenSymbol';
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() => '${formatAmount(ZanoFormatter.intAmountToString(fee))} $feeCurrency';
String get feeCurrency => 'ZANO';
}

View file

@ -0,0 +1,12 @@
class ZanoWalletKeys {
const ZanoWalletKeys(
{required this.privateSpendKey,
required this.privateViewKey,
required this.publicSpendKey,
required this.publicViewKey});
final String publicViewKey;
final String privateViewKey;
final String publicSpendKey;
final String privateSpendKey;
}

View file

@ -0,0 +1,78 @@
import 'dart:math';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zano/zano_wallet_api.dart';
import 'package:decimal/decimal.dart';
import 'package:decimal/intl.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
class ZanoFormatter {
static const defaultDecimalPoint = 12;
//static final numberFormat = NumberFormat()
// ..maximumFractionDigits = defaultDecimalPoint
// ..minimumFractionDigits = 1;
static Decimal _bigIntDivision({required BigInt amount, required BigInt divider}) {
return (Decimal.fromBigInt(amount) / Decimal.fromBigInt(divider)).toDecimal();
}
static String intAmountToString(int amount, [int decimalPoint = defaultDecimalPoint]) {
final numberFormat = NumberFormat()..maximumFractionDigits = decimalPoint
..minimumFractionDigits = 1;
return numberFormat.format(
DecimalIntl(
_bigIntDivision(
amount: BigInt.from(amount),
divider: BigInt.from(pow(10, decimalPoint)),
),
),
)
.replaceAll(',', '');
}
static String bigIntAmountToString(BigInt amount, [int decimalPoint = defaultDecimalPoint]) {
if (decimalPoint == 0) {
return '0';
}
final numberFormat = NumberFormat()..maximumFractionDigits = decimalPoint
..minimumFractionDigits = 1;
return numberFormat.format(
DecimalIntl(
_bigIntDivision(
amount: amount,
divider: BigInt.from(pow(10, decimalPoint)),
),
),
)
.replaceAll(',', '');
}
static double intAmountToDouble(int amount, [int decimalPoint = defaultDecimalPoint]) => _bigIntDivision(
amount: BigInt.from(amount),
divider: BigInt.from(pow(10, decimalPoint)),
).toDouble();
static int parseAmount(String amount, [int decimalPoint = defaultDecimalPoint]) {
final resultBigInt = (Decimal.parse(amount) * Decimal.fromBigInt(BigInt.from(10).pow(decimalPoint))).toBigInt();
if (!resultBigInt.isValidInt) {
Fluttertoast.showToast(msg: 'Cannot transfer $amount. Maximum is ${intAmountToString(resultBigInt.toInt(), decimalPoint)}.');
}
return resultBigInt.toInt();
}
static BigInt bigIntFromDynamic(dynamic d) {
if (d is int) {
return BigInt.from(d);
} else if (d is BigInt) {
return d;
} else if (d == null) {
return BigInt.zero;
} else {
printV(('cannot cast value of type ${d.runtimeType} to BigInt'));
throw 'cannot cast value of type ${d.runtimeType} to BigInt';
//return BigInt.zero;
}
}
}

View file

@ -0,0 +1,27 @@
import 'dart:core';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_zano/model/zano_transaction_info.dart';
part 'zano_transaction_history.g.dart';
class ZanoTransactionHistory = ZanoTransactionHistoryBase
with _$ZanoTransactionHistory;
abstract class ZanoTransactionHistoryBase
extends TransactionHistoryBase<ZanoTransactionInfo> with Store {
ZanoTransactionHistoryBase() {
transactions = ObservableMap<String, ZanoTransactionInfo>();
}
@override
Future<void> save() async {}
@override
void addOne(ZanoTransactionInfo transaction) =>
transactions[transaction.id] = transaction;
@override
void addMany(Map<String, ZanoTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
}

View file

@ -0,0 +1,17 @@
import 'dart:convert';
import 'package:monero/zano.dart' as zano;
import 'package:cw_zano/api/model/get_address_info_result.dart';
class ZanoUtils {
static bool validateAddress(String address) {
try {
final result = GetAddressInfoResult.fromJson(
jsonDecode(zano.PlainWallet_getAddressInfo(address)) as Map<String, dynamic>,
);
return result.valid;
} catch (err) {
return false;
}
}
}

View file

@ -0,0 +1,577 @@
import 'dart:async';
import 'dart:core';
import 'dart:io';
import 'dart:math';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/zano_asset.dart';
import 'package:cw_zano/api/model/create_wallet_result.dart';
import 'package:cw_zano/api/model/destination.dart';
import 'package:cw_zano/api/model/get_recent_txs_and_info_result.dart';
import 'package:cw_zano/api/model/get_wallet_status_result.dart';
import 'package:cw_zano/api/model/transfer.dart';
import 'package:cw_zano/model/pending_zano_transaction.dart';
import 'package:cw_zano/model/zano_balance.dart';
import 'package:cw_zano/model/zano_transaction_creation_exception.dart';
import 'package:cw_zano/model/zano_transaction_credentials.dart';
import 'package:cw_zano/model/zano_transaction_info.dart';
import 'package:cw_zano/model/zano_wallet_keys.dart';
import 'package:cw_zano/zano_formatter.dart';
import 'package:cw_zano/zano_transaction_history.dart';
import 'package:cw_zano/zano_wallet_addresses.dart';
import 'package:cw_zano/zano_wallet_api.dart';
import 'package:cw_zano/zano_wallet_exceptions.dart';
import 'package:cw_zano/zano_wallet_service.dart';
import 'package:cw_zano/api/model/balance.dart';
import 'package:mobx/mobx.dart';
part 'zano_wallet.g.dart';
class ZanoWallet = ZanoWalletBase with _$ZanoWallet;
abstract class ZanoWalletBase
extends WalletBase<ZanoBalance, ZanoTransactionHistory, ZanoTransactionInfo>
with Store, ZanoWalletApi {
static const int _autoSaveIntervalSeconds = 30;
static const int _pollIntervalMilliseconds = 5000;
static const int _maxLoadAssetsRetries = 5;
@override
void setPassword(String password) {
_password = password;
super.setPassword(password);
}
String _password;
@override
String get password => _password;
@override
Future<String> signMessage(String message, {String? address = null}) {
throw UnimplementedError();
}
@override
Future<bool> verifyMessage(String message, String signature, {String? address = null}) {
throw UnimplementedError();
}
@override
ZanoWalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
ObservableMap<CryptoCurrency, ZanoBalance> balance;
@override
String seed = '';
@override
String? passphrase = '';
@override
ZanoWalletKeys keys = ZanoWalletKeys(
privateSpendKey: '', privateViewKey: '', publicSpendKey: '', publicViewKey: '');
static const String zanoAssetId =
'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a';
Map<String, ZanoAsset> zanoAssets = {};
Timer? _updateSyncInfoTimer;
int _lastKnownBlockHeight = 0;
int _initialSyncHeight = 0;
int currentDaemonHeight = 0;
bool _isTransactionUpdating;
bool _hasSyncAfterStartup;
Timer? _autoSaveTimer;
/// number of transactions in each request
static final int _txChunkSize = (pow(2, 32) - 1).toInt();
ZanoWalletBase(WalletInfo walletInfo, String password)
: balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty()}),
_isTransactionUpdating = false,
_hasSyncAfterStartup = false,
walletAddresses = ZanoWalletAddresses(walletInfo),
syncStatus = NotConnectedSyncStatus(),
_password = password,
super(walletInfo) {
transactionHistory = ZanoTransactionHistory();
if (!CakeHive.isAdapterRegistered(ZanoAsset.typeId)) {
CakeHive.registerAdapter(ZanoAssetAdapter());
}
}
@override
int calculateEstimatedFee(TransactionPriority priority, [int? amount = null]) =>
getCurrentTxFee(priority);
@override
Future<void> changePassword(String password) async {
setPassword(password);
}
static Future<ZanoWallet> create({required WalletCredentials credentials}) async {
final wallet = ZanoWallet(credentials.walletInfo!, credentials.password!);
await wallet.initWallet();
final path = await pathForWallet(name: credentials.name, type: credentials.walletInfo!.type);
final createWalletResult = await wallet.createWallet(path, credentials.password!);
await wallet.initWallet();
await wallet.parseCreateWalletResult(createWalletResult);
if (credentials.passphrase != null) {
await wallet.setPassphrase(credentials.passphrase!);
wallet.seed = await createWalletResult.seed(wallet);
wallet.passphrase = await wallet.getPassphrase();
}
await wallet.init(createWalletResult.wi.address);
return wallet;
}
static Future<ZanoWallet> restore(
{required ZanoRestoreWalletFromSeedCredentials credentials}) async {
final wallet = ZanoWallet(credentials.walletInfo!, credentials.password!);
await wallet.initWallet();
final path = await pathForWallet(name: credentials.name, type: credentials.walletInfo!.type);
final createWalletResult = await wallet.restoreWalletFromSeed(
path, credentials.password!, credentials.mnemonic, credentials.passphrase);
await wallet.initWallet();
await wallet.parseCreateWalletResult(createWalletResult);
if (credentials.passphrase != null) {
await wallet.setPassphrase(credentials.passphrase!);
wallet.seed = await createWalletResult.seed(wallet);
wallet.passphrase = await wallet.getPassphrase();
}
await wallet.init(createWalletResult.wi.address);
return wallet;
}
static Future<ZanoWallet> open(
{required String name, required String password, required WalletInfo walletInfo}) async {
final path = await pathForWallet(name: name, type: walletInfo.type);
if (ZanoWalletApi.openWalletCache[path] != null) {
final wallet = ZanoWallet(walletInfo, password);
await wallet.parseCreateWalletResult(ZanoWalletApi.openWalletCache[path]!).then((_) {
unawaited(wallet.init(ZanoWalletApi.openWalletCache[path]!.wi.address));
});
return wallet;
} else {
final wallet = ZanoWallet(walletInfo, password);
await wallet.initWallet();
final createWalletResult = await wallet.loadWallet(path, password);
await wallet.parseCreateWalletResult(createWalletResult).then((_) {
unawaited(wallet.init(createWalletResult.wi.address));
});
return wallet;
}
}
Future<void> parseCreateWalletResult(CreateWalletResult result) async {
hWallet = result.walletId;
seed = await result.seed(this);
keys = ZanoWalletKeys(
privateSpendKey: result.privateSpendKey,
privateViewKey: result.privateViewKey,
publicSpendKey: result.publicSpendKey,
publicViewKey: result.publicViewKey,
);
passphrase = await getPassphrase();
printV('setting hWallet = ${result.walletId}');
walletAddresses.address = result.wi.address;
await loadAssets(result.wi.balances, maxRetries: _maxLoadAssetsRetries);
for (final item in result.wi.balances) {
if (item.assetInfo.assetId == zanoAssetId) {
balance[CryptoCurrency.zano] = ZanoBalance(
total: item.total,
unlocked: item.unlocked,
);
}
}
if (result.recentHistory.history != null) {
final transfers = result.recentHistory.history!;
final transactions = Transfer.makeMap(transfers, zanoAssets, currentDaemonHeight);
transactionHistory.addMany(transactions);
await transactionHistory.save();
}
}
@override
Future<void> close({bool shouldCleanup = true}) async {
closeWallet(null);
_updateSyncInfoTimer?.cancel();
_autoSaveTimer?.cancel();
}
@override
Future<void> connectToNode({required Node node}) async {
syncStatus = ConnectingSyncStatus();
await setupNode(node.uriRaw);
syncStatus = ConnectedSyncStatus();
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
credentials as ZanoTransactionCredentials;
final isZano = credentials.currency == CryptoCurrency.zano;
final outputs = credentials.outputs;
final hasMultiDestination = outputs.length > 1;
final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? BigInt.zero;
final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? BigInt.zero;
final fee = BigInt.from(calculateEstimatedFee(credentials.priority));
late BigInt totalAmount;
void checkForEnoughBalances() {
if (isZano) {
if (totalAmount + fee > unlockedBalanceZano) {
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount + fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO).");
}
} else {
if (fee > unlockedBalanceZano) {
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO).");
}
if (totalAmount > unlockedBalanceCurrency) {
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount, credentials.currency.decimals)} ${credentials.currency.title}, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceCurrency, credentials.currency.decimals)} ${credentials.currency.title}).");
}
}
}
final assetId = isZano ? zanoAssetId : (credentials.currency as ZanoAsset).assetId;
late List<Destination> destinations;
if (hasMultiDestination) {
if (outputs.any((output) => output.sendAll || (output.formattedCryptoAmount ?? 0) <= 0)) {
throw ZanoTransactionCreationException("You don't have enough coins.");
}
totalAmount = outputs.fold(
BigInt.zero, (acc, value) => acc + BigInt.from(value.formattedCryptoAmount ?? 0));
checkForEnoughBalances();
destinations = outputs
.map((output) => Destination(
amount: BigInt.from(output.formattedCryptoAmount ?? 0),
address: output.isParsedAddress ? output.extractedAddress! : output.address,
assetId: assetId,
))
.toList();
} else {
final output = outputs.first;
if (output.sendAll) {
if (isZano) {
totalAmount = unlockedBalanceZano - fee;
} else {
totalAmount = unlockedBalanceCurrency;
}
} else {
totalAmount = BigInt.from(output.formattedCryptoAmount!);
}
checkForEnoughBalances();
destinations = [
Destination(
amount: totalAmount,
address: output.isParsedAddress ? output.extractedAddress! : output.address,
assetId: assetId,
)
];
}
return PendingZanoTransaction(
zanoWallet: this,
destinations: destinations,
fee: fee,
comment: outputs.first.note ?? '',
assetId: assetId,
ticker: credentials.currency.title,
decimalPoint: credentials.currency.decimals,
amount: totalAmount,
);
}
@override
Future<Map<String, ZanoTransactionInfo>> fetchTransactions() async {
try {
final transfers = <Transfer>[];
late GetRecentTxsAndInfoResult result;
do {
result = await getRecentTxsAndInfo(offset: 0, count: _txChunkSize);
// _lastTxIndex += result.transfers.length;
transfers.addAll(result.transfers);
} while (result.lastItemIndex + 1 < result.totalTransfers);
return Transfer.makeMap(transfers, zanoAssets, currentDaemonHeight);
} catch (e) {
printV((e.toString()));
return {};
}
}
Future<void> init(String address) async {
await walletAddresses.init();
await walletAddresses.updateAddress(address);
await updateTransactions();
_autoSaveTimer = Timer.periodic(Duration(seconds: _autoSaveIntervalSeconds), (_) async {
await save();
});
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: name, type: type);
final currentCacheFile = File(currentWalletPath);
final currentKeysFile = File('$currentWalletPath.keys');
final currentAddressListFile = File('$currentWalletPath.address.txt');
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
// Copies current wallet files into new wallet name's dir and files
if (currentCacheFile.existsSync()) {
await currentCacheFile.copy(newWalletPath);
}
if (currentKeysFile.existsSync()) {
await currentKeysFile.copy('$newWalletPath.keys');
}
if (currentAddressListFile.existsSync()) {
await currentAddressListFile.copy('$newWalletPath.address.txt');
}
// Delete old name's dir and files
await Directory(currentWalletPath).delete(recursive: true);
}
@override
Future<void> rescan({required int height}) => throw UnimplementedError();
@override
Future<void> save() async {
try {
await store();
await walletAddresses.updateAddressesInBox();
} catch (e) {
printV(('Error while saving Zano wallet file ${e.toString()}'));
}
}
Future<void> loadAssets(List<Balance> balances, {int maxRetries = 1}) async {
List<ZanoAsset> assets = [];
int retryCount = 0;
while (retryCount < maxRetries) {
try {
assets = await getAssetsWhitelist();
break;
} on ZanoWalletBusyException {
if (retryCount < maxRetries - 1) {
retryCount++;
await Future.delayed(Duration(seconds: 1));
} else {
printV(('failed to load assets after $retryCount retries'));
break;
}
}
}
zanoAssets = {};
for (final asset in assets) {
final newAsset = ZanoAsset.copyWith(
asset,
enabled: balances.any((element) => element.assetId == asset.assetId),
);
zanoAssets.putIfAbsent(asset.assetId, () => newAsset);
}
}
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
_lastKnownBlockHeight = 0;
_initialSyncHeight = 0;
_updateSyncInfoTimer ??=
Timer.periodic(Duration(milliseconds: _pollIntervalMilliseconds), (_) => _updateSyncInfo());
} catch (e) {
syncStatus = FailedSyncStatus();
printV((e.toString()));
}
}
@override
Future<void>? updateBalance() => null;
Future<void> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.clear();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (e) {
printV("e: $e");
printV((e.toString()));
_isTransactionUpdating = false;
}
}
Future<CryptoCurrency> addZanoAssetById(String assetId) async {
if (zanoAssets.containsKey(assetId)) {
throw ZanoWalletException('zano asset with id $assetId already added');
}
final assetDescriptor = await addAssetsWhitelist(assetId);
if (assetDescriptor == null) {
throw ZanoWalletException("there's no zano asset with id $assetId");
}
final asset = ZanoAsset.copyWith(
assetDescriptor,
assetId: assetId,
enabled: true,
);
zanoAssets[asset.assetId] = asset;
balance[asset] = ZanoBalance.empty(decimalPoint: asset.decimalPoint);
return asset;
}
Future<void> changeZanoAssetAvailability(ZanoAsset asset) async {
if (asset.enabled) {
final assetDescriptor = await addAssetsWhitelist(asset.assetId);
if (assetDescriptor == null) {
printV(('Error adding zano asset'));
}
} else {
final result = await removeAssetsWhitelist(asset.assetId);
if (result == false) {
printV(('Error removing zano asset'));
}
}
}
Future<void> deleteZanoAsset(ZanoAsset asset) async {
final _ = await removeAssetsWhitelist(asset.assetId);
}
Future<ZanoAsset?> getZanoAsset(String assetId) async {
// wallet api is not available while the wallet is syncing so only call it if it's synced
if (syncStatus is SyncedSyncStatus) {
return await getAssetInfo(assetId);
}
return null;
}
Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
void _onNewBlock(int height, int blocksLeft, double ptc) async {
try {
if (blocksLeft < 1000) {
await _askForUpdateTransactionHistory();
syncStatus = SyncedSyncStatus();
if (!_hasSyncAfterStartup) {
_hasSyncAfterStartup = true;
await save();
}
} else {
syncStatus = SyncingSyncStatus(blocksLeft, ptc);
}
} catch (e) {
printV((e.toString()));
}
}
void _updateSyncProgress(GetWalletStatusResult walletStatus) {
final syncHeight = walletStatus.currentWalletHeight;
if (_initialSyncHeight <= 0) {
_initialSyncHeight = syncHeight;
}
final bchHeight = walletStatus.currentDaemonHeight;
if (_lastKnownBlockHeight == syncHeight) {
return;
}
_lastKnownBlockHeight = syncHeight;
final track = bchHeight - _initialSyncHeight;
final diff = track - (bchHeight - syncHeight);
final ptc = diff <= 0 ? 0.0 : diff / track;
final left = bchHeight - syncHeight;
if (syncHeight < 0 || left < 0) {
return;
}
// 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents;
_onNewBlock.call(syncHeight, left, ptc);
}
void _updateSyncInfo() async {
GetWalletStatusResult walletStatus;
// ignoring get wallet status exception (in case of wrong wallet id)
try {
walletStatus = await getWalletStatus();
} on ZanoWalletException {
return;
}
currentDaemonHeight = walletStatus.currentDaemonHeight;
_updateSyncProgress(walletStatus);
// we can call getWalletInfo ONLY if getWalletStatus returns NOT is in long refresh and wallet state is 2 (ready)
if (!walletStatus.isInLongRefresh && walletStatus.walletState == 2) {
final walletInfo = await getWalletInfo();
seed = await walletInfo.wiExtended.seed(this);
keys = ZanoWalletKeys(
privateSpendKey: walletInfo.wiExtended.spendPrivateKey,
privateViewKey: walletInfo.wiExtended.viewPrivateKey,
publicSpendKey: walletInfo.wiExtended.spendPublicKey,
publicViewKey: walletInfo.wiExtended.viewPublicKey,
);
loadAssets(walletInfo.wi.balances);
// matching balances and whitelists
// 1. show only balances available in whitelists
// 2. set whitelists available in balances as 'enabled' ('disabled' by default)
for (final b in walletInfo.wi.balances) {
if (b.assetId == zanoAssetId) {
balance[CryptoCurrency.zano] = ZanoBalance(total: b.total, unlocked: b.unlocked);
} else {
final asset = zanoAssets[b.assetId];
if (asset == null) {
printV('balance for an unknown asset ${b.assetInfo.assetId}');
continue;
}
if (balance.keys.any(
(element) => element is ZanoAsset && element.assetId == b.assetInfo.assetId)) {
balance[balance.keys.firstWhere((element) =>
element is ZanoAsset && element.assetId == b.assetInfo.assetId)] =
ZanoBalance(
total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
} else {
balance[asset] = ZanoBalance(
total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
}
}
}
await updateTransactions();
// removing balances for assets missing in wallet info balances
balance.removeWhere(
(key, _) =>
key != CryptoCurrency.zano &&
!walletInfo.wi.balances
.any((element) => element.assetId == (key as ZanoAsset).assetId),
);
}
}
}

View file

@ -0,0 +1,41 @@
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_zano/zano_wallet_api.dart';
import 'package:mobx/mobx.dart';
part 'zano_wallet_addresses.g.dart';
class ZanoWalletAddresses = ZanoWalletAddressesBase with _$ZanoWalletAddresses;
abstract class ZanoWalletAddressesBase extends WalletAddresses with Store {
ZanoWalletAddressesBase(WalletInfo walletInfo)
: address = '',
super(walletInfo);
@override
@observable
String address;
@override
Future<void> init() async {
address = walletInfo.address;
await updateAddressesInBox();
}
Future<void> updateAddress(String address) async {
this.address = address;
await updateAddressesInBox();
}
@override
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
await saveAddressesInBox();
} catch (e) {
printV(e.toString());
}
}
}

View file

@ -0,0 +1,511 @@
import 'dart:convert' as convert;
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/zano_asset.dart';
import 'package:cw_zano/api/consts.dart';
import 'package:cw_zano/api/model/asset_id_params.dart';
import 'package:cw_zano/api/model/create_wallet_result.dart';
import 'package:cw_zano/api/model/destination.dart';
import 'package:cw_zano/api/model/get_address_info_result.dart';
import 'package:cw_zano/api/model/get_recent_txs_and_info_params.dart';
import 'package:cw_zano/api/model/get_recent_txs_and_info_result.dart';
import 'package:cw_zano/api/model/get_wallet_info_result.dart';
import 'package:cw_zano/api/model/get_wallet_status_result.dart';
import 'package:cw_zano/api/model/proxy_to_daemon_params.dart';
import 'package:cw_zano/api/model/proxy_to_daemon_result.dart';
import 'package:cw_zano/api/model/store_result.dart';
import 'package:cw_zano/api/model/transfer.dart';
import 'package:cw_zano/api/model/transfer_params.dart';
import 'package:cw_zano/api/model/transfer_result.dart';
import 'package:cw_zano/zano_wallet_exceptions.dart';
import 'package:ffi/ffi.dart';
import 'package:json_bigint/json_bigint.dart';
import 'package:monero/zano.dart' as zano;
import 'package:monero/src/generated_bindings_zano.g.dart' as zanoapi;
import 'package:path/path.dart' as p;
mixin ZanoWalletApi {
static const _maxReopenAttempts = 5;
static const _logInfo = false;
static const int _zanoMixinValue = 10;
int _hWallet = 0;
int get hWallet => _hWallet;
set hWallet(int value) {
_hWallet = value;
}
int getCurrentTxFee(TransactionPriority priority) => zano.PlainWallet_getCurrentTxFee(priority.raw);
void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password);
void closeWallet(int? walletToClose, {bool force = false}) async {
printV('close_wallet ${walletToClose ?? hWallet}: $force');
if (Platform.isWindows || force) {
final result = await _closeWallet(walletToClose ?? hWallet);
printV('close_wallet result $result');
openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet));
}
}
static bool isInit = false;
Future<bool> initWallet() async {
if (isInit) return true;
final result = zano.PlainWallet_init("", "", 0);
isInit = true;
return result == "OK";
}
Future<bool> setupNode(String nodeUrl) async {
await _setupNode(hWallet, nodeUrl);
return true;
}
Future<Directory> getWalletDir() async {
final walletInfoResult = await getWalletInfo();
return Directory(p.dirname(walletInfoResult.wi.path));
}
Future<File> _getWalletSecretsFile() async {
final dir = await getWalletDir();
final file = File(p.join(dir.path, "zano-secrets.json.bin"));
return file;
}
Future<Map<String, dynamic>> _getSecrets() async {
final file = await _getWalletSecretsFile();
if (!file.existsSync()) {
return {};
}
final data = file.readAsBytesSync();
final b64 = convert.base64.encode(data);
final respStr = await invokeMethod("decrypt_data", {"buff": "$b64"});
final resp = convert.json.decode(respStr);
final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
final dataStr = convert.utf8.decode(dataBytes);
final dataObject = convert.json.decode(dataStr);
return dataObject as Map<String, dynamic>;
}
Future<void> _setSecrets(Map<String, dynamic> data) async {
final dataStr = convert.json.encode(data);
final b64 = convert.base64.encode(convert.utf8.encode(dataStr));
final respStr = await invokeMethod("encrypt_data", {"buff": "$b64"});
final resp = convert.json.decode(respStr);
final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
final file = await _getWalletSecretsFile();
file.writeAsBytesSync(dataBytes);
}
Future<String?> _getWalletSecret(String key) async {
final secrets = await _getSecrets();
return secrets[key] as String?;
}
Future<void> _setWalletSecret(String key, String value) async {
final secrets = await _getSecrets();
secrets[key] = value;
await _setSecrets(secrets);
}
Future<String?> getPassphrase() async {
return await _getWalletSecret("passphrase");
}
Future<void> setPassphrase(String passphrase) {
return _setWalletSecret("passphrase", passphrase);
}
Future<String> getSeed() async {
final passphrase = await getPassphrase();
final respStr = await invokeMethod("get_restore_info", {"seed_password": passphrase??""});
final resp = convert.json.decode(respStr);
return resp["result"]["seed_phrase"] as String;
}
Future<GetWalletInfoResult> getWalletInfo() async {
final json = await _getWalletInfo(hWallet);
final result = GetWalletInfoResult.fromJson(jsonDecode(json));
printV('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances}');
return result;
}
Future<GetWalletStatusResult> getWalletStatus() async {
final json = await _getWalletStatus(hWallet);
if (json == Consts.errorWalletWrongId) {
printV('wrong wallet id');
throw ZanoWalletException('Wrong wallet id');
}
final status = GetWalletStatusResult.fromJson(jsonDecode(json));
if (_logInfo)
printV(
'get_wallet_status connected: ${status.isDaemonConnected} in refresh: ${status.isInLongRefresh} progress: ${status.progress} wallet state: ${status.walletState} sync: ${status.currentWalletHeight}/${status.currentDaemonHeight} ${(status.currentWalletHeight/status.currentDaemonHeight*100).toStringAsFixed(2)}%');
return status;
}
Future<String> invokeMethod(String methodName, Object params) async {
final request = jsonEncode({
"method": methodName,
"params": params,
});
final invokeResult = await callSyncMethod('invoke', hWallet, request);
try {
jsonDecode(invokeResult);
} catch (e) {
if (invokeResult.contains(Consts.errorWalletWrongId)) throw ZanoWalletException('Wrong wallet id');
printV('exception in parsing json in invokeMethod: $invokeResult');
rethrow;
}
return invokeResult;
}
Future<List<ZanoAsset>> getAssetsWhitelist() async {
try {
final json = await invokeMethod('assets_whitelist_get', '{}');
final map = jsonDecode(json) as Map<String, dynamic>?;
_checkForErrors(map);
List<ZanoAsset> assets(String type, bool isGlobalWhitelist) =>
(map?['result']?[type] as List<dynamic>?)
?.map((e) => ZanoAsset.fromJson(e as Map<String, dynamic>, isInGlobalWhitelist: isGlobalWhitelist))
.toList() ??
[];
final localWhitelist = assets('local_whitelist', false);
final globalWhitelist = assets('global_whitelist', true);
final ownAssets = assets('own_assets', false);
if (_logInfo)
printV('assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); '
'global whitelist: ${globalWhitelist.length} ($globalWhitelist); '
'own assets: ${ownAssets.length} ($ownAssets)');
return [...globalWhitelist, ...localWhitelist, ...ownAssets];
} catch (e) {
printV('assets_whitelist_get $e');
return [];
// rethrow;
}
}
Future<ZanoAsset?> addAssetsWhitelist(String assetId) async {
try {
final json = await invokeMethod('assets_whitelist_add', AssetIdParams(assetId: assetId));
final map = jsonDecode(json) as Map<String, dynamic>?;
_checkForErrors(map);
if (map!['result']!['status']! == 'OK') {
final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
printV('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
return assetDescriptor;
} else {
printV('assets_whitelist_add status ${map['result']!['status']!}');
return null;
}
} catch (e) {
printV('assets_whitelist_add $e');
return null;
}
}
Future<bool> removeAssetsWhitelist(String assetId) async {
try {
final json = await invokeMethod('assets_whitelist_remove', AssetIdParams(assetId: assetId));
final map = jsonDecode(json) as Map<String, dynamic>?;
_checkForErrors(map);
printV('assets_whitelist_remove status ${map!['result']!['status']!}');
return (map['result']!['status']! == 'OK');
} catch (e) {
printV('assets_whitelist_remove $e');
return false;
}
}
Future<ProxyToDaemonResult?> _proxyToDaemon(String uri, String body) async {
final json = await invokeMethod('proxy_to_daemon', ProxyToDaemonParams(body: body, uri: uri));
final map = jsonDecode(json) as Map<String, dynamic>?;
_checkForErrors(map);
return ProxyToDaemonResult.fromJson(map!['result'] as Map<String, dynamic>);
}
Future<ZanoAsset?> getAssetInfo(String assetId) async {
final methodName = 'get_asset_info';
final params = AssetIdParams(assetId: assetId);
final result = await _proxyToDaemon('/json_rpc', '{"method": "$methodName","params": ${jsonEncode(params)}}');
if (result == null) {
printV('get_asset_info empty result');
return null;
}
final map = jsonDecode(result.body) as Map<String, dynamic>?;
if (map!['error'] != null) {
printV('get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}');
return null;
} else if (map['result']!['status']! == 'OK') {
final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
printV('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
return assetDescriptor;
} else {
printV('get_asset_info $assetId status ${map['result']!['status']!}');
return null;
}
}
Future<StoreResult?> store() async {
try {
final json = await invokeMethod('store', {});
final map = jsonDecode(json) as Map<String, dynamic>?;
_checkForErrors(map);
return StoreResult.fromJson(map!['result'] as Map<String, dynamic>);
} catch (e) {
printV('store $e');
return null;
}
}
Future<GetRecentTxsAndInfoResult> getRecentTxsAndInfo({required int offset, required int count}) async {
printV('get_recent_txs_and_info $offset $count');
try {
final json = await invokeMethod('get_recent_txs_and_info', GetRecentTxsAndInfoParams(offset: offset, count: count));
final map = jsonDecode(json) as Map<String, dynamic>?;
_checkForErrors(map);
final lastItemIndex = map?['result']?['last_item_index'] as int?;
final totalTransfers = map?['result']?['total_transfers'] as int?;
final transfers = map?['result']?['transfers'] as List<dynamic>?;
if (transfers == null || lastItemIndex == null || totalTransfers == null) {
printV('get_recent_txs_and_info empty transfers');
return GetRecentTxsAndInfoResult.empty();
}
printV('get_recent_txs_and_info transfers.length: ${transfers.length}');
return GetRecentTxsAndInfoResult(
transfers: transfers.map((e) => Transfer.fromJson(e as Map<String, dynamic>)).toList(),
lastItemIndex: lastItemIndex,
totalTransfers: totalTransfers,
);
} catch (e) {
printV('get_recent_txs_and_info $e');
return GetRecentTxsAndInfoResult.empty();
}
}
GetAddressInfoResult getAddressInfo(String address) => GetAddressInfoResult.fromJson(
jsonDecode(zano.PlainWallet_getAddressInfo(address)),
);
String _shorten(String s) => s.length > 10 ? '${s.substring(0, 4)}...${s.substring(s.length - 4)}' : s;
Future<CreateWalletResult> createWallet(String path, String password) async {
printV('create_wallet path $path password ${_shorten(password)}');
final json = zano.PlainWallet_generate(path, password);
final map = jsonDecode(json) as Map<String, dynamic>?;
if (map?['error'] != null) {
final code = map!['error']?['code'] ?? '';
final message = map['error']?['message'] ?? '';
throw ZanoWalletException('Error creating wallet file, $message ($code)');
}
if (map?['result'] == null) {
throw ZanoWalletException('Error creating wallet file, empty response');
}
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
openWalletCache[path] = result;
printV('create_wallet ${result.name}');
return result;
}
Future<CreateWalletResult> restoreWalletFromSeed(String path, String password, String seed, String? passphrase) async {
printV('restore_wallet path $path');
final json = zano.PlainWallet_restore(seed, path, password, passphrase??'');
final map = jsonDecode(json) as Map<String, dynamic>?;
if (map?['error'] != null) {
final code = map!['error']!['code'] ?? '';
final message = map['error']!['message'] ?? '';
if (code == Consts.errorWrongSeed) {
throw RestoreFromSeedsException('Error restoring wallet\nPlease check the seed words are correct. Additionally, if you created this wallet with a passphrase please add it under the Advanced Settings page.');
} else if (code == Consts.errorAlreadyExists) {
throw RestoreFromSeedsException('Error restoring wallet, already exists');
}
throw RestoreFromSeedsException('Error restoring wallet, $message ($code)');
}
if (map?['result'] == null) {
throw RestoreFromSeedsException('Error restoring wallet, empty response');
}
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
openWalletCache[path] = result;
printV('restore_wallet ${result.name} ${result.wi.address}');
return result;
}
Future<CreateWalletResult> loadWallet(String path, String password, [int attempt = 0]) async {
printV('load_wallet1 path $path');
final String json;
try {
json = zano.PlainWallet_open(path, password);
} catch (e) {
printV('error in loadingWallet $e');
rethrow;
}
final map = jsonDecode(json) as Map<String, dynamic>?;
if (map?['error'] != null) {
final code = map?['error']!['code'] ?? '';
final message = map?['error']!['message'] ?? '';
if (code == Consts.errorAlreadyExists && attempt <= _maxReopenAttempts) {
// already connected to this wallet. closing and trying to reopen
printV('already connected. closing and reopen wallet (attempt $attempt)');
closeWallet(attempt, force: true);
await Future.delayed(const Duration(milliseconds: 500));
return await loadWallet(path, password, attempt + 1);
}
throw ZanoWalletException('Error loading wallet, $message ($code)');
}
if (map?['result'] == null) {
throw ZanoWalletException('Error loading wallet, empty response');
}
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
printV('load_wallet3 ${result.name} ${result.wi.address}');
openWalletCache[path] = result;
return result;
}
static Map<String, CreateWalletResult> openWalletCache = {};
Future<TransferResult> transfer(List<Destination> destinations, BigInt fee, String comment) async {
final params = TransferParams(
destinations: destinations,
fee: fee,
mixin: _zanoMixinValue,
paymentId: '',
comment: comment,
pushPayer: false,
hideReceiver: true,
);
final json = await invokeMethod('transfer', params);
final map = jsonDecode(json);
final resultMap = map as Map<String, dynamic>?;
if (resultMap != null) {
final transferResultMap = resultMap['result'] as Map<String, dynamic>?;
if (transferResultMap != null) {
final transferResult = TransferResult.fromJson(transferResultMap);
printV('transfer success hash ${transferResult.txHash}');
return transferResult;
} else {
final errorCode = resultMap['error']?['code'];
final code = errorCode is int ? errorCode.toString() : errorCode as String? ?? '';
final message = resultMap['error']?['message'] as String? ?? '';
printV('transfer error $code $message');
throw TransferException('Transfer error, $message ($code)');
}
}
printV('transfer error empty result');
throw TransferException('Transfer error, empty result');
}
void _checkForErrors(Map<String, dynamic>? map) {
if (map == null) {
throw ZanoWalletException('Empty response');
}
final result = map['result'];
if (result == null) {
throw ZanoWalletException('Empty response');
}
if (result['error'] != null) {
final code = result['error']!['code'] ?? '';
final message = result['error']!['message'] ?? '';
if (code == -1 && message == Consts.errorBusy) {
throw ZanoWalletBusyException();
}
throw ZanoWalletException('Error, $message ($code)');
}
}
}
Future<String> callSyncMethod(String methodName, int hWallet, String params) async {
final params_ = params.toNativeUtf8().address;
final method_name_ = methodName.toNativeUtf8().address;
final invokeResult = await Isolate.run(() async {
final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
final txid = lib.ZANO_PlainWallet_syncCall(
Pointer.fromAddress(method_name_).cast(),
hWallet,
Pointer.fromAddress(params_).cast()
);
try {
final strPtr = txid.cast<Utf8>();
final str = strPtr.toDartString();
lib.ZANO_free(strPtr.cast());
return str;
} catch (e) {
return "";
}
});
calloc.free(Pointer.fromAddress(method_name_));
calloc.free(Pointer.fromAddress(params_));
return invokeResult;
}
Map<String, dynamic> jsonDecode(String json) {
try {
return decodeJson(json.replaceAll("\\/", "/")) as Map<String, dynamic>;
} catch (e) {
return convert.jsonDecode(json) as Map<String, dynamic>;
}
}
String jsonEncode(Object? object) {
return convert.jsonEncode(object);
}
Future<String> _getWalletStatus(int hWallet) async {
final jsonPtr = await Isolate.run(() async {
final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
final status = lib.ZANO_PlainWallet_getWalletStatus(
hWallet,
);
return status.address;
});
String json = "";
try {
final strPtr = Pointer.fromAddress(jsonPtr).cast<Utf8>();
final str = strPtr.toDartString();
zano.ZANO_free(strPtr.cast());
json = str;
} catch (e) {
json = "";
}
return json;
}
Future<String> _getWalletInfo(int hWallet) async {
final jsonPtr = await Isolate.run(() async {
final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
final status = lib.ZANO_PlainWallet_getWalletInfo(
hWallet,
);
return status.address;
});
String json = "";
try {
final strPtr = Pointer.fromAddress(jsonPtr).cast<Utf8>();
final str = strPtr.toDartString();
zano.ZANO_free(strPtr.cast());
json = str;
} catch (e) {
json = "";
}
return json;
}
Future<String> _setupNode(int hWallet, String nodeUrl) async {
await callSyncMethod("reset_connection_url", hWallet, nodeUrl);
await callSyncMethod("run_wallet", hWallet, "");
return "OK";
}
Future<String> _closeWallet(int hWallet) async {
final str = await Isolate.run(() async {
return zano.PlainWallet_closeWallet(hWallet);
});
printV("Closing wallet: $str");
return str;
}

View file

@ -0,0 +1,21 @@
import 'package:cw_core/exceptions.dart';
class ZanoWalletException implements Exception {
final String message;
ZanoWalletException(this.message);
@override
String toString() => '${this.runtimeType} (message: $message)';
}
class RestoreFromSeedsException extends RestoreFromSeedException {
RestoreFromSeedsException(String message) : super(message);
}
class TransferException extends ZanoWalletException {
TransferException(String message): super(message);
}
class ZanoWalletBusyException extends ZanoWalletException {
ZanoWalletBusyException(): super('');
}

Some files were not shown because too many files have changed in this diff Show more