mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
Merge branch 'main' into mweb-bg-sync-3
This commit is contained in:
commit
f7b189af24
311 changed files with 10251 additions and 2064 deletions
|
@ -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
|
||||
|
|
25
.github/workflows/pr_test_build_android.yml
vendored
25
.github/workflows/pr_test_build_android.yml
vendored
|
@ -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/
|
||||
|
|
29
.github/workflows/pr_test_build_linux.yml
vendored
29
.github/workflows/pr_test_build_linux.yml
vendored
|
@ -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
5
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../../../../../../scripts/monero_c/release/zano/aarch64-linux-android_libwallet2_api_c.so
|
|
@ -0,0 +1 @@
|
|||
../../../../../../scripts/monero_c/release/zano/armv7a-linux-androideabi_libwallet2_api_c.so
|
1
android/app/src/main/jniLibs/x86_64/libzano_libwallet2_api_c.so
Symbolic link
1
android/app/src/main/jniLibs/x86_64/libzano_libwallet2_api_c.so
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../../../../scripts/monero_c/release/zano/x86_64-linux-android_libwallet2_api_c.so
|
BIN
assets/images/chainflip.png
Normal file
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
BIN
assets/images/flip_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
BIN
assets/images/zano_icon.png
Normal file
BIN
assets/images/zano_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -1,2 +1,3 @@
|
|||
Ledger fixes
|
||||
UI enhancements
|
||||
Bug fixes
|
|
@ -1,4 +1,5 @@
|
|||
Bitcoin and Litecoin enhancements
|
||||
Solana and Nano fixes/improvements
|
||||
Zano enhancements
|
||||
Ethereum enhancements
|
||||
Ledger fixes
|
||||
UI enhancements
|
||||
Bug fixes
|
7
assets/zano_node_list.yml
Normal file
7
assets/zano_node_list.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
-
|
||||
uri: 37.27.100.59:10500
|
||||
useSSL: false
|
||||
-
|
||||
uri: zano.cakewallet.com:11211
|
||||
is_default: true
|
||||
useSSL: false
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
131
cw_core/lib/zano_asset.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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/
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
4
cw_mweb/ios/.gitignore
vendored
4
cw_mweb/ios/.gitignore
vendored
|
@ -35,4 +35,6 @@ Icon?
|
|||
|
||||
/Flutter/Generated.xcconfig
|
||||
/Flutter/ephemeral/
|
||||
/Flutter/flutter_export_environment.sh
|
||||
/Flutter/flutter_export_environment.sh
|
||||
|
||||
Mwebd.xcframework
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
7
cw_zano/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
.dart_tool/
|
||||
|
||||
.packages
|
||||
.pub/
|
||||
|
||||
build/
|
10
cw_zano/.metadata
Normal file
10
cw_zano/.metadata
Normal 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
3
cw_zano/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
cw_zano/LICENSE
Normal file
1
cw_zano/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
15
cw_zano/README.md
Normal file
15
cw_zano/README.md
Normal 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.
|
||||
|
6
cw_zano/lib/api/consts.dart
Normal file
6
cw_zano/lib/api/consts.dart
Normal 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';
|
||||
}
|
9
cw_zano/lib/api/model/asset_id_params.dart
Normal file
9
cw_zano/lib/api/model/asset_id_params.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
class AssetIdParams {
|
||||
final String assetId;
|
||||
|
||||
AssetIdParams({required this.assetId});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'asset_id': assetId,
|
||||
};
|
||||
}
|
32
cw_zano/lib/api/model/balance.dart
Normal file
32
cw_zano/lib/api/model/balance.dart
Normal 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']),
|
||||
);
|
||||
}
|
52
cw_zano/lib/api/model/create_wallet_result.dart
Normal file
52
cw_zano/lib/api/model/create_wallet_result.dart
Normal 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();
|
||||
}
|
||||
}
|
20
cw_zano/lib/api/model/destination.dart
Normal file
20
cw_zano/lib/api/model/destination.dart
Normal 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,
|
||||
};
|
||||
}
|
18
cw_zano/lib/api/model/employed_entries.dart
Normal file
18
cw_zano/lib/api/model/employed_entries.dart
Normal 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(),
|
||||
);
|
||||
}
|
16
cw_zano/lib/api/model/get_address_info_result.dart
Normal file
16
cw_zano/lib/api/model/get_address_info_result.dart
Normal 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,
|
||||
);
|
||||
}
|
14
cw_zano/lib/api/model/get_recent_txs_and_info_params.dart
Normal file
14
cw_zano/lib/api/model/get_recent_txs_and_info_params.dart
Normal 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',
|
||||
};
|
||||
}
|
12
cw_zano/lib/api/model/get_recent_txs_and_info_result.dart
Normal file
12
cw_zano/lib/api/model/get_recent_txs_and_info_result.dart
Normal 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;
|
||||
|
||||
}
|
14
cw_zano/lib/api/model/get_wallet_info_result.dart
Normal file
14
cw_zano/lib/api/model/get_wallet_info_result.dart
Normal 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>? ?? {}),
|
||||
);
|
||||
}
|
26
cw_zano/lib/api/model/get_wallet_status_result.dart
Normal file
26
cw_zano/lib/api/model/get_wallet_status_result.dart
Normal 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,
|
||||
);
|
||||
}
|
13
cw_zano/lib/api/model/proxy_to_daemon_params.dart
Normal file
13
cw_zano/lib/api/model/proxy_to_daemon_params.dart
Normal 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,
|
||||
};
|
||||
}
|
13
cw_zano/lib/api/model/proxy_to_daemon_result.dart
Normal file
13
cw_zano/lib/api/model/proxy_to_daemon_result.dart
Normal 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,
|
||||
);
|
||||
}
|
15
cw_zano/lib/api/model/receive.dart
Normal file
15
cw_zano/lib/api/model/receive.dart
Normal 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,
|
||||
);
|
||||
}
|
20
cw_zano/lib/api/model/recent_history.dart
Normal file
20
cw_zano/lib/api/model/recent_history.dart
Normal 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,
|
||||
);
|
||||
}
|
9
cw_zano/lib/api/model/store_result.dart
Normal file
9
cw_zano/lib/api/model/store_result.dart
Normal 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,
|
||||
);
|
||||
}
|
16
cw_zano/lib/api/model/subtransfer.dart
Normal file
16
cw_zano/lib/api/model/subtransfer.dart
Normal 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,
|
||||
);
|
||||
}
|
133
cw_zano/lib/api/model/transfer.dart
Normal file
133
cw_zano/lib/api/model/transfer.dart
Normal 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',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
41
cw_zano/lib/api/model/transfer_params.dart
Normal file
41
cw_zano/lib/api/model/transfer_params.dart
Normal 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,
|
||||
);
|
||||
}
|
13
cw_zano/lib/api/model/transfer_result.dart
Normal file
13
cw_zano/lib/api/model/transfer_result.dart
Normal 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? ?? '',
|
||||
);
|
||||
}
|
32
cw_zano/lib/api/model/wi.dart
Normal file
32
cw_zano/lib/api/model/wi.dart
Normal 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? ?? '',
|
||||
);
|
||||
}
|
21
cw_zano/lib/api/model/wi_extended.dart
Normal file
21
cw_zano/lib/api/model/wi_extended.dart
Normal 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();
|
||||
}
|
||||
}
|
1630
cw_zano/lib/mnemonics/english.dart
Normal file
1630
cw_zano/lib/mnemonics/english.dart
Normal file
File diff suppressed because it is too large
Load diff
52
cw_zano/lib/model/pending_zano_transaction.dart
Normal file
52
cw_zano/lib/model/pending_zano_transaction.dart
Normal 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();
|
||||
}
|
||||
}
|
0
cw_zano/lib/model/zano_asset.dart
Normal file
0
cw_zano/lib/model/zano_asset.dart
Normal file
17
cw_zano/lib/model/zano_balance.dart
Normal file
17
cw_zano/lib/model/zano_balance.dart
Normal 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);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
class ZanoTransactionCreationException implements Exception {
|
||||
ZanoTransactionCreationException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
11
cw_zano/lib/model/zano_transaction_credentials.dart
Normal file
11
cw_zano/lib/model/zano_transaction_credentials.dart
Normal 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;
|
||||
}
|
81
cw_zano/lib/model/zano_transaction_info.dart
Normal file
81
cw_zano/lib/model/zano_transaction_info.dart
Normal 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';
|
||||
}
|
12
cw_zano/lib/model/zano_wallet_keys.dart
Normal file
12
cw_zano/lib/model/zano_wallet_keys.dart
Normal 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;
|
||||
}
|
78
cw_zano/lib/zano_formatter.dart
Normal file
78
cw_zano/lib/zano_formatter.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
27
cw_zano/lib/zano_transaction_history.dart
Normal file
27
cw_zano/lib/zano_transaction_history.dart
Normal 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);
|
||||
}
|
17
cw_zano/lib/zano_utils.dart
Normal file
17
cw_zano/lib/zano_utils.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
577
cw_zano/lib/zano_wallet.dart
Normal file
577
cw_zano/lib/zano_wallet.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
41
cw_zano/lib/zano_wallet_addresses.dart
Normal file
41
cw_zano/lib/zano_wallet_addresses.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
511
cw_zano/lib/zano_wallet_api.dart
Normal file
511
cw_zano/lib/zano_wallet_api.dart
Normal 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;
|
||||
}
|
21
cw_zano/lib/zano_wallet_exceptions.dart
Normal file
21
cw_zano/lib/zano_wallet_exceptions.dart
Normal 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
Loading…
Add table
Add a link
Reference in a new issue