refactored ba/nano wallets

This commit is contained in:
julian 2023-11-15 15:59:01 -06:00
parent 8ba998af8f
commit c381326dd5
16 changed files with 851 additions and 2097 deletions

View file

@ -486,34 +486,41 @@ class _NewWalletRecoveryPhraseWarningViewState
String? mnemonic;
String? privateKey;
// TODO: [prio=high] finish fleshing this out
if (coin.hasMnemonicPassphraseSupport) {
wordCount =
Constants.defaultSeedPhraseLengthFor(
coin: info.coin,
);
if (wordCount > 0) {
if (ref
.read(pNewWalletOptions.state)
.state !=
null) {
if (coin.hasMnemonicPassphraseSupport) {
mnemonicPassphrase = ref
.read(pNewWalletOptions.state)
.state!
.mnemonicPassphrase;
} else {}
wordCount = ref
.read(pNewWalletOptions.state)
.state!
.mnemonicWordsCount;
} else {
wordCount = 12;
mnemonicPassphrase = "";
}
final int strength;
if (wordCount == 12) {
strength = 128;
} else if (wordCount == 24) {
strength = 256;
} else {
if (wordCount < 12 ||
24 < wordCount ||
wordCount % 3 != 0) {
throw Exception("Invalid word count");
}
final strength = (wordCount ~/ 3) * 32;
mnemonic = bip39.generateMnemonic(
strength: strength);
strength: strength,
);
}
final wallet = await Wallet.create(

View file

@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/monkey_service.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
@ -17,6 +16,7 @@ import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';

View file

@ -16,16 +16,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
@ -51,10 +49,12 @@ class ChangeRepresentativeView extends ConsumerStatefulWidget {
static const String routeName = "/changeRepresentative";
@override
ConsumerState<ChangeRepresentativeView> createState() => _XPubViewState();
ConsumerState<ChangeRepresentativeView> createState() =>
_ChangeRepresentativeViewState();
}
class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
class _ChangeRepresentativeViewState
extends ConsumerState<ChangeRepresentativeView> {
final _textController = TextEditingController();
final _textFocusNode = FocusNode();
final bool isDesktop = Util.isDesktop;
@ -65,23 +65,18 @@ class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
Future<String> loadRepresentative() async {
final wallet = ref.read(pWallets).getWallet(widget.walletId);
final coin = wallet.info.coin;
if (coin == Coin.nano) {
return (wallet as NanoWallet).getCurrentRepresentative();
} else if (coin == Coin.banano) {
return (wallet as BananoWallet).getCurrentRepresentative();
}
if (wallet is NanoBased) {
return wallet.getCurrentRepresentative();
} else {
throw Exception("Unsupported wallet attempted to show representative!");
}
}
Future<void> _save() async {
final wallet = ref.read(pWallets).getWallet(widget.walletId);
final coin = wallet.info.coin;
final wallet = ref.read(pWallets).getWallet(widget.walletId) as NanoBased;
final changeFuture = coin == Coin.nano
? (wallet as NanoWallet).changeRepresentative
: (wallet as BananoWallet).changeRepresentative;
final changeFuture = wallet.changeRepresentative;
final result = await showLoading(
whileFuture: changeFuture(_textController.text),

View file

@ -33,7 +33,6 @@ import 'package:stackwallet/providers/global/active_wallet_provider.dart';
import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
@ -43,6 +42,7 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
@ -193,9 +193,7 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
final walletInfo = wallet.info;
final monke = wallet.info.coin == Coin.banano
? (wallet as BananoWallet).getMonkeyImageBytes()
: null;
final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null;
return ConditionalParent(
condition: _rescanningOnOpen,

File diff suppressed because it is too large Load diff

View file

@ -14,14 +14,12 @@ import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart';
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
import 'package:stackwallet/services/coins/particl/particl_wallet.dart';
import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart';
import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart';
@ -217,20 +215,10 @@ abstract class CoinServiceAPI {
);
case Coin.nano:
return NanoWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
tracker: tracker,
secureStore: secureStorageInterface);
throw UnimplementedError("moved");
case Coin.banano:
return BananoWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
tracker: tracker,
secureStore: secureStorageInterface);
throw UnimplementedError("moved");
case Coin.dogecoinTestNet:
throw UnimplementedError("moved");

File diff suppressed because it is too large Load diff

View file

@ -277,6 +277,9 @@ abstract class Constants {
case Coin.monero:
return 25;
//
// default:
// -1;
}
}

View file

@ -0,0 +1,25 @@
import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
class Banano extends NanoCurrency {
Banano(super.network) {
switch (network) {
case CryptoCurrencyNetwork.main:
coin = Coin.banano;
default:
throw Exception("Unsupported network: $network");
}
}
@override
int get minConfirms => 1;
@override
String get defaultRepresentative =>
"ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo";
@override
int get nanoAccountType => NanoAccountType.BANANO;
}

View file

@ -0,0 +1,25 @@
import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
class Nano extends NanoCurrency {
Nano(super.network) {
switch (network) {
case CryptoCurrencyNetwork.main:
coin = Coin.nano;
default:
throw Exception("Unsupported network: $network");
}
}
@override
int get minConfirms => 1;
@override
String get defaultRepresentative =>
"nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579";
@override
int get nanoAccountType => NanoAccountType.NANO;
}

View file

@ -0,0 +1,21 @@
import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart';
abstract class NanoCurrency extends Bip39Currency {
NanoCurrency(super.network);
String get defaultRepresentative;
int get nanoAccountType;
@override
bool validateAddress(String address) => NanoAccounts.isValid(
nanoAccountType,
address,
);
@override
String get genesisHash => throw UnimplementedError(
"Not used in nano based coins",
);
}

View file

@ -215,6 +215,26 @@ class WalletInfo implements IsarId {
}
}
/// update [otherData] with the map entries in [newEntries]
Future<void> updateOtherData({
required Map<String, dynamic> newEntries,
required Isar isar,
}) async {
final Map<String, dynamic> newMap = {};
newMap.addAll(otherData);
newMap.addAll(newEntries);
final encodedNew = jsonEncode(newMap);
// only update if there were changes
if (_otherDataJsonString != encodedNew) {
_otherDataJsonString = encodedNew;
await isar.writeTxn(() async {
await isar.walletInfo.deleteByWalletId(walletId);
await isar.walletInfo.put(this);
});
}
}
/// copies this with a new name and updates the db
Future<void> setMnemonicVerified({
required Isar isar,
@ -317,4 +337,5 @@ abstract class WalletInfoKeys {
static const String tokenContractAddresses = "tokenContractAddressesKey";
static const String cachedSecondaryBalance = "cachedSecondaryBalanceKey";
static const String epiccashData = "epiccashDataKey";
static const String bananoMonkeyImageBytes = "monkeyImageBytesKey";
}

View file

@ -0,0 +1,27 @@
import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart';
class BananoWallet extends Bip39Wallet<NanoCurrency> with NanoBased {
BananoWallet(CryptoCurrencyNetwork network) : super(Banano(network));
Future<void> updateMonkeyImageBytes(List<int> bytes) async {
await info.updateOtherData(
newEntries: {
WalletInfoKeys.bananoMonkeyImageBytes: bytes,
},
isar: mainDB.isar,
);
}
List<int>? getMonkeyImageBytes() {
final list = info.otherData[WalletInfoKeys.bananoMonkeyImageBytes] as List?;
if (list == null) {
return null;
}
return List<int>.from(list);
}
}

View file

@ -0,0 +1,9 @@
import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/nano_based.dart';
class NanoWallet extends Bip39Wallet<NanoCurrency> with NanoBased {
NanoWallet(CryptoCurrencyNetwork network) : super(Nano(network));
}

View file

@ -0,0 +1,675 @@
import 'dart:async';
import 'dart:convert';
import 'package:isar/isar.dart';
import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/extensions/impl/string.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/nano_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
import 'package:tuple/tuple.dart';
const _kWorkServer = "https://rpc.nano.to";
mixin NanoBased<T extends NanoCurrency> on Bip39Wallet<T> {
// since nano based coins only have a single address/account we can cache
// the address instead of fetching from db every time we need it in certain
// cases
Address? _cachedAddress;
NodeModel? _cachedNode;
final _httpClient = HTTP();
Future<String?> _requestWork(String hash) async {
return _httpClient
.post(
url: Uri.parse(_kWorkServer), // this should be a
headers: {'Content-type': 'application/json'},
body: json.encode(
{
"action": "work_generate",
"hash": hash,
},
),
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
)
.then((_httpClient) {
if (_httpClient.code == 200) {
final Map<String, dynamic> decoded =
json.decode(_httpClient.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
return decoded["work"] as String?;
} else {
throw Exception("Received error ${_httpClient.code}");
}
});
}
Future<String> _getPrivateKeyFromMnemonic() async {
final mnemonicList = await getMnemonicAsWords();
final seed = NanoMnemomics.mnemonicListToSeed(mnemonicList);
return NanoKeys.seedToPrivate(seed, 0);
}
Future<Address> _getAddressFromMnemonic() async {
final publicKey = NanoKeys.createPublicKey(
await _getPrivateKeyFromMnemonic(),
);
final addressString =
NanoAccounts.createAccount(cryptoCurrency.nanoAccountType, publicKey);
return Address(
walletId: walletId,
value: addressString,
publicKey: publicKey.toUint8ListFromHex,
derivationIndex: 0,
derivationPath: null,
type: cryptoCurrency.coin.primaryAddressType,
subType: AddressSubType.receiving,
);
}
Future<void> _receiveBlock(
String blockHash,
String source,
String amountRaw,
String publicAddress,
) async {
// TODO: the opening block of an account is a special case
bool openBlock = false;
final headers = {
"Content-Type": "application/json",
};
// first check if the account is open:
// get the account info (we need the frontier and representative):
final infoBody = jsonEncode({
"action": "account_info",
"representative": "true",
"account": publicAddress,
});
final infoResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: infoBody,
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final infoData = jsonDecode(infoResponse.body);
if (infoData["error"] != null) {
// account is not open yet, we need to create an open block:
openBlock = true;
}
// first get the account balance:
final balanceBody = jsonEncode({
"action": "account_balance",
"account": publicAddress,
});
final balanceResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: balanceBody,
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final balanceData = jsonDecode(balanceResponse.body);
final BigInt currentBalance =
BigInt.parse(balanceData["balance"].toString());
final BigInt txAmount = BigInt.parse(amountRaw);
final BigInt balanceAfterTx = currentBalance + txAmount;
String frontier = infoData["frontier"].toString();
String representative = infoData["representative"].toString();
if (openBlock) {
// we don't have a representative set yet:
representative = cryptoCurrency.defaultRepresentative;
}
// link = send block hash:
final String link = blockHash;
// this "linkAsAccount" is meaningless:
final String linkAsAccount =
NanoAccounts.createAccount(NanoAccountType.BANANO, blockHash);
// construct the receive block:
Map<String, String> receiveBlock = {
"type": "state",
"account": publicAddress,
"previous": openBlock
? "0000000000000000000000000000000000000000000000000000000000000000"
: frontier,
"representative": representative,
"balance": balanceAfterTx.toString(),
"link": link,
"link_as_account": linkAsAccount,
};
// sign the receive block:
final String hash = NanoBlocks.computeStateHash(
NanoAccountType.BANANO,
receiveBlock["account"]!,
receiveBlock["previous"]!,
receiveBlock["representative"]!,
BigInt.parse(receiveBlock["balance"]!),
receiveBlock["link"]!,
);
final String privateKey = await _getPrivateKeyFromMnemonic();
final String signature = NanoSignatures.signBlock(hash, privateKey);
// get PoW for the receive block:
String? work;
if (openBlock) {
work = await _requestWork(NanoAccounts.extractPublicKey(publicAddress));
} else {
work = await _requestWork(frontier);
}
if (work == null) {
throw Exception("Failed to get PoW for receive block");
}
receiveBlock["link_as_account"] = linkAsAccount;
receiveBlock["signature"] = signature;
receiveBlock["work"] = work;
// process the receive block:
final processBody = jsonEncode({
"action": "process",
"json_block": "true",
"subtype": "receive",
"block": receiveBlock,
});
final processResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: processBody,
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final Map<String, dynamic> decoded =
json.decode(processResponse.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
}
Future<void> _confirmAllReceivable(String accountAddress) async {
final receivableResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: {"Content-Type": "application/json"},
body: jsonEncode({
"action": "receivable",
"source": "true",
"account": accountAddress,
"count": "-1",
}),
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final receivableData = await jsonDecode(receivableResponse.body);
if (receivableData["blocks"] == "") {
return;
}
final blocks = receivableData["blocks"] as Map<String, dynamic>;
// confirm all receivable blocks:
for (final blockHash in blocks.keys) {
final block = blocks[blockHash];
final String amountRaw = block["amount"] as String;
final String source = block["source"] as String;
await _receiveBlock(blockHash, source, amountRaw, accountAddress);
// a bit of a hack:
await Future<void>.delayed(const Duration(seconds: 1));
}
}
//========= public ===========================================================
Future<String> getCurrentRepresentative() async {
final serverURI = Uri.parse(getCurrentNode().host);
final address =
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
return response.accountInfo?.representative ??
cryptoCurrency.defaultRepresentative;
}
Future<bool> changeRepresentative(String newRepresentative) async {
try {
final serverURI = Uri.parse(getCurrentNode().host);
await updateBalance();
final balance = info.cachedBalance.spendable.raw.toString();
final String privateKey = await _getPrivateKeyFromMnemonic();
final address =
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
if (response.accountInfo == null) {
throw response.exception ?? Exception("Failed to get account info");
}
final work = await _requestWork(response.accountInfo!.frontier);
return await NanoAPI.changeRepresentative(
server: serverURI,
accountType: NanoAccountType.BANANO,
account: address,
newRepresentative: newRepresentative,
previousBlock: response.accountInfo!.frontier,
balance: balance,
privateKey: privateKey,
work: work!,
);
} catch (_) {
rethrow;
}
}
//========= overrides ========================================================
@override
Future<void> updateNode() async {
_cachedNode = NodeService(secureStorageInterface: secureStorageInterface)
.getPrimaryNodeFor(coin: info.coin) ??
DefaultNodes.getNodeFor(info.coin);
unawaited(refresh());
}
@override
NodeModel getCurrentNode() {
return _cachedNode ??
NodeService(secureStorageInterface: secureStorageInterface)
.getPrimaryNodeFor(coin: info.coin) ??
DefaultNodes.getNodeFor(info.coin);
}
@override
Future<void> init() async {
_cachedAddress = await getCurrentReceivingAddress();
if (_cachedAddress == null) {
_cachedAddress = await _getAddressFromMnemonic();
await mainDB.putAddress(_cachedAddress!);
}
return super.init();
}
@override
Future<bool> pingCheck() async {
final uri = Uri.parse(getCurrentNode().host);
final response = await _httpClient.post(
url: uri,
headers: {"Content-Type": "application/json"},
body: jsonEncode(
{
"action": "version",
},
),
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
return response.code == 200;
}
@override
Future<TxData> prepareSend({required TxData txData}) async {
if (txData.recipients!.length != 1) {
throw ArgumentError(
"${cryptoCurrency.runtimeType} currently only "
"supports one recipient per transaction",
);
}
return txData.copyWith(
fee: Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
),
);
}
@override
Future<TxData> confirmSend({required TxData txData}) async {
try {
// our address:
final String publicAddress =
(_cachedAddress ?? await getCurrentReceivingAddress())!.value;
// first update to get latest account balance:
final currentBalance = info.cachedBalance.spendable;
final txAmount = txData.amount!;
final BigInt balanceAfterTx = (currentBalance - txAmount).raw;
// get the account info (we need the frontier and representative):
final infoBody = jsonEncode({
"action": "account_info",
"representative": "true",
"account": publicAddress,
});
final headers = {
"Content-Type": "application/json",
};
final infoResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: infoBody,
proxyInfo:
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final String frontier =
jsonDecode(infoResponse.body)["frontier"].toString();
final String representative =
jsonDecode(infoResponse.body)["representative"].toString();
// link = destination address:
final String linkAsAccount = txData.recipients!.first.address;
final String link = NanoAccounts.extractPublicKey(linkAsAccount);
// construct the send block:
Map<String, String> sendBlock = {
"type": "state",
"account": publicAddress,
"previous": frontier,
"representative": representative,
"balance": balanceAfterTx.toString(),
"link": link,
};
// sign the send block:
final String hash = NanoBlocks.computeStateHash(
NanoAccountType.BANANO,
sendBlock["account"]!,
sendBlock["previous"]!,
sendBlock["representative"]!,
BigInt.parse(sendBlock["balance"]!),
sendBlock["link"]!,
);
final String privateKey = await _getPrivateKeyFromMnemonic();
final String signature = NanoSignatures.signBlock(hash, privateKey);
// get PoW for the send block:
final String? work = await _requestWork(frontier);
if (work == null) {
throw Exception("Failed to get PoW for send block");
}
sendBlock["link_as_account"] = linkAsAccount;
sendBlock["signature"] = signature;
sendBlock["work"] = work;
final processBody = jsonEncode({
"action": "process",
"json_block": "true",
"subtype": "send",
"block": sendBlock,
});
final processResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: processBody,
proxyInfo:
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final Map<String, dynamic> decoded =
json.decode(processResponse.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
// return the hash of the transaction:
return txData.copyWith(
txid: decoded["hash"].toString(),
);
} catch (e, s) {
Logging.instance
.log("Error sending transaction $e - $s", level: LogLevel.Error);
rethrow;
}
}
@override
Future<void> recover({required bool isRescan}) async {
try {
await refreshMutex.protect(() async {
if (isRescan) {
await mainDB.deleteWalletBlockchainData(walletId);
}
_cachedAddress = await _getAddressFromMnemonic();
await mainDB.putAddress(_cachedAddress!);
});
await refresh();
} catch (e) {
rethrow;
}
}
@override
Future<void> updateTransactions() async {
final receivingAddress =
(_cachedAddress ?? await getCurrentReceivingAddress())!;
final String publicAddress = receivingAddress.value;
await _confirmAllReceivable(publicAddress);
final response = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: {"Content-Type": "application/json"},
body: jsonEncode({
"action": "account_history",
"account": publicAddress,
"count": "-1",
}),
proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final data = await jsonDecode(response.body);
final transactions =
data["history"] is List ? data["history"] as List<dynamic> : [];
if (transactions.isEmpty) {
return;
} else {
List<Tuple2<Transaction, Address?>> transactionList = [];
for (var tx in transactions) {
var typeString = tx["type"].toString();
TransactionType transactionType = TransactionType.unknown;
if (typeString == "send") {
transactionType = TransactionType.outgoing;
} else if (typeString == "receive") {
transactionType = TransactionType.incoming;
}
final amount = Amount(
rawValue: BigInt.parse(tx["amount"].toString()),
fractionDigits: cryptoCurrency.fractionDigits,
);
var transaction = Transaction(
walletId: walletId,
txid: tx["hash"].toString(),
timestamp: int.parse(tx["local_timestamp"].toString()),
type: transactionType,
subType: TransactionSubType.none,
amount: 0,
amountString: amount.toJsonString(),
fee: 0,
height: int.parse(tx["height"].toString()),
isCancelled: false,
isLelantus: false,
slateId: "",
otherData: "",
inputs: [],
outputs: [],
nonce: 0,
numberOfMessages: null,
);
Address address = transactionType == TransactionType.incoming
? receivingAddress
: Address(
walletId: walletId,
publicKey: [],
value: tx["account"].toString(),
derivationIndex: 0,
derivationPath: null,
type: info.coin.primaryAddressType,
subType: AddressSubType.nonWallet,
);
Tuple2<Transaction, Address> tuple = Tuple2(transaction, address);
transactionList.add(tuple);
}
await mainDB.addNewTransactionData(transactionList, walletId);
}
}
@override
Future<void> updateBalance() async {
try {
final addressString =
(_cachedAddress ??= (await getCurrentReceivingAddress())!).value;
final body = jsonEncode({
"action": "account_balance",
"account": addressString,
});
final headers = {
"Content-Type": "application/json",
};
final response = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: body,
proxyInfo:
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final data = jsonDecode(response.body);
final balance = Balance(
total: Amount(
rawValue: (BigInt.parse(data["balance"].toString()) +
BigInt.parse(data["receivable"].toString())),
fractionDigits: cryptoCurrency.fractionDigits,
),
spendable: Amount(
rawValue: BigInt.parse(data["balance"].toString()),
fractionDigits: cryptoCurrency.fractionDigits,
),
blockedTotal: Amount(
rawValue: BigInt.parse("0"),
fractionDigits: cryptoCurrency.fractionDigits,
),
pendingSpendable: Amount(
rawValue: BigInt.parse(data["receivable"].toString()),
fractionDigits: cryptoCurrency.fractionDigits,
),
);
await info.updateBalance(newBalance: balance, isar: mainDB.isar);
} catch (e, s) {
Logging.instance.log(
"Failed to update ${cryptoCurrency.runtimeType} balance: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
Future<void> updateChainHeight() async {
try {
final String publicAddress =
(_cachedAddress ??= (await getCurrentReceivingAddress())!).value;
final infoBody = jsonEncode({
"action": "account_info",
"account": publicAddress,
});
final headers = {
"Content-Type": "application/json",
};
final infoResponse = await _httpClient.post(
url: Uri.parse(getCurrentNode().host),
headers: headers,
body: infoBody,
proxyInfo:
prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null,
);
final infoData = jsonDecode(infoResponse.body);
final height = int.tryParse(
infoData["confirmation_height"].toString(),
) ??
0;
await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar);
} catch (e, s) {
Logging.instance.log(
"Failed to update ${cryptoCurrency.runtimeType} chain height: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
FilterOperation? get changeAddressFilterOperation =>
FilterGroup.and(standardChangeAddressFilters);
@override
FilterOperation? get receivingAddressFilterOperation =>
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<void> updateUTXOs() async {
// do nothing for nano based coins
}
@override
// nano has no fees
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async => Amount(
rawValue: BigInt.from(0),
fractionDigits: cryptoCurrency.fractionDigits,
);
@override
// nano has no fees
Future<FeeObject> get fees async => FeeObject(
numberOfBlocksFast: 1,
numberOfBlocksAverage: 1,
numberOfBlocksSlow: 1,
fast: 0,
medium: 0,
slow: 0,
);
}

View file

@ -22,11 +22,13 @@ import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/electrumx.dart';
import 'package:stackwallet/wallets/wallet/mixins/mnemonic_based_wallet.dart';
@ -238,6 +240,9 @@ abstract class Wallet<T extends CryptoCurrency> {
required WalletInfo walletInfo,
}) {
switch (walletInfo.coin) {
case Coin.banano:
return BananoWallet(CryptoCurrencyNetwork.main);
case Coin.bitcoin:
return BitcoinWallet(CryptoCurrencyNetwork.main);
case Coin.bitcoinTestNet:
@ -259,6 +264,9 @@ abstract class Wallet<T extends CryptoCurrency> {
case Coin.epicCash:
return EpiccashWallet(CryptoCurrencyNetwork.main);
case Coin.nano:
return NanoWallet(CryptoCurrencyNetwork.main);
case Coin.wownero:
return WowneroWallet(CryptoCurrencyNetwork.main);