mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
commit
58b8509b41
15 changed files with 893 additions and 11 deletions
|
@ -134,7 +134,8 @@ enum AddressType {
|
|||
unknown,
|
||||
nonWallet,
|
||||
ethereum,
|
||||
nano;
|
||||
nano,
|
||||
banano;
|
||||
|
||||
String get readableName {
|
||||
switch (this) {
|
||||
|
@ -156,6 +157,8 @@ enum AddressType {
|
|||
return "Ethereum";
|
||||
case AddressType.nano:
|
||||
return "Nano";
|
||||
case AddressType.banano:
|
||||
return "Banano";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -262,6 +262,7 @@ const _AddresstypeEnumValueMap = {
|
|||
'nonWallet': 6,
|
||||
'ethereum': 7,
|
||||
'nano': 8,
|
||||
'banano': 9,
|
||||
};
|
||||
const _AddresstypeValueEnumMap = {
|
||||
0: AddressType.p2pkh,
|
||||
|
@ -273,6 +274,7 @@ const _AddresstypeValueEnumMap = {
|
|||
6: AddressType.nonWallet,
|
||||
7: AddressType.ethereum,
|
||||
8: AddressType.nano,
|
||||
9: AddressType.banano,
|
||||
};
|
||||
|
||||
Id _addressGetId(Address object) {
|
||||
|
|
|
@ -729,6 +729,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
|
|||
case Coin.dogecoinTestNet:
|
||||
case Coin.epicCash:
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
case Coin.eCash:
|
||||
return false;
|
||||
|
||||
|
|
|
@ -1430,7 +1430,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
if (!([Coin.nano, Coin.epicCash].contains(coin)))
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin)))
|
||||
Text(
|
||||
"Transaction fee (${coin == Coin.ethereum ? "max" : "estimated"})",
|
||||
style: STextStyles.desktopTextExtraSmall(context).copyWith(
|
||||
|
@ -1440,11 +1440,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
if (!([Coin.nano, Coin.epicCash].contains(coin)))
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin)))
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (!([Coin.nano, Coin.epicCash].contains(coin)))
|
||||
if (!([Coin.nano, Coin.banano, Coin.epicCash].contains(coin)))
|
||||
DesktopFeeDropDown(
|
||||
walletId: walletId,
|
||||
),
|
||||
|
|
|
@ -24,6 +24,7 @@ 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/banano_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/wownero/wownero_wallet.dart';
|
||||
|
@ -246,6 +247,15 @@ abstract class CoinServiceAPI {
|
|||
secureStore: secureStorageInterface
|
||||
);
|
||||
|
||||
case Coin.banano:
|
||||
return BananoWallet(
|
||||
walletId: walletId,
|
||||
walletName: walletName,
|
||||
coin: coin,
|
||||
tracker: tracker,
|
||||
secureStore: secureStorageInterface
|
||||
);
|
||||
|
||||
case Coin.dogecoinTestNet:
|
||||
return DogecoinWallet(
|
||||
walletId: walletId,
|
||||
|
|
812
lib/services/coins/nano/banano_wallet.dart
Normal file
812
lib/services/coins/nano/banano_wallet.dart
Normal file
|
@ -0,0 +1,812 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:stackwallet/models/balance.dart';
|
||||
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
|
||||
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
||||
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
||||
import 'package:stackwallet/services/coins/coin_service.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';
|
||||
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
|
||||
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
||||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/log_level_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../db/isar/main_db.dart';
|
||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../../models/node_model.dart';
|
||||
import '../../../utilities/default_nodes.dart';
|
||||
import '../../../utilities/flutter_secure_storage_interface.dart';
|
||||
import '../../../utilities/prefs.dart';
|
||||
import '../../node_service.dart';
|
||||
import '../../transaction_notification_tracker.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||
|
||||
const int MINIMUM_CONFIRMATIONS = 1;
|
||||
const String DEFAULT_REPRESENTATIVE =
|
||||
"ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo";
|
||||
|
||||
class BananoWallet extends CoinServiceAPI
|
||||
with WalletCache, WalletDB, CoinControlInterface {
|
||||
BananoWallet({
|
||||
required String walletId,
|
||||
required String walletName,
|
||||
required Coin coin,
|
||||
required TransactionNotificationTracker tracker,
|
||||
required SecureStorageInterface secureStore,
|
||||
MainDB? mockableOverride,
|
||||
}) {
|
||||
txTracker = tracker;
|
||||
_walletId = walletId;
|
||||
_walletName = walletName;
|
||||
_coin = coin;
|
||||
_secureStore = secureStore;
|
||||
initCache(walletId, coin);
|
||||
initWalletDB(mockableOverride: mockableOverride);
|
||||
}
|
||||
|
||||
NodeModel? _xnoNode;
|
||||
|
||||
@override
|
||||
Future<String?> get mnemonicPassphrase => _secureStore.read(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
);
|
||||
|
||||
@override
|
||||
Future<String?> get mnemonicString =>
|
||||
_secureStore.read(key: '${_walletId}_mnemonic');
|
||||
|
||||
Future<String> getSeedFromMnemonic() async {
|
||||
var mnemonic = await mnemonicString;
|
||||
return NanoMnemomics.mnemonicListToSeed(mnemonic!.split(" "));
|
||||
}
|
||||
|
||||
Future<String> getPrivateKeyFromMnemonic() async {
|
||||
var mnemonic = await mnemonicString;
|
||||
var seed = NanoMnemomics.mnemonicListToSeed(mnemonic!.split(" "));
|
||||
return NanoKeys.seedToPrivate(seed, 0);
|
||||
}
|
||||
|
||||
Future<String> getAddressFromMnemonic() async {
|
||||
var mnemonic = await mnemonicString;
|
||||
var seed = NanoMnemomics.mnemonicListToSeed(mnemonic!.split(' '));
|
||||
var address = NanoAccounts.createAccount(NanoAccountType.BANANO,
|
||||
NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0)));
|
||||
return address;
|
||||
}
|
||||
|
||||
Future<String> getPublicKeyFromMnemonic() async {
|
||||
var mnemonic = await mnemonicString;
|
||||
if (mnemonic == null) {
|
||||
return "";
|
||||
} else {
|
||||
var seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" "));
|
||||
return NanoKeys.createPublicKey(NanoKeys.seedToPrivate(seed, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get walletId => _walletId;
|
||||
late String _walletId;
|
||||
|
||||
@override
|
||||
String get walletName => _walletName;
|
||||
late String _walletName;
|
||||
|
||||
@override
|
||||
set walletName(String name) => _walletName = name;
|
||||
|
||||
@override
|
||||
set isFavorite(bool markFavorite) {
|
||||
_isFavorite = markFavorite;
|
||||
updateCachedIsFavorite(markFavorite);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
|
||||
bool? _isFavorite;
|
||||
|
||||
@override
|
||||
Coin get coin => _coin;
|
||||
late Coin _coin;
|
||||
|
||||
late SecureStorageInterface _secureStore;
|
||||
late final TransactionNotificationTracker txTracker;
|
||||
final _prefs = Prefs.instance;
|
||||
|
||||
bool _shouldAutoSync = false;
|
||||
|
||||
@override
|
||||
bool get shouldAutoSync => _shouldAutoSync;
|
||||
|
||||
@override
|
||||
set shouldAutoSync(bool shouldAutoSync) => _shouldAutoSync = shouldAutoSync;
|
||||
|
||||
@override
|
||||
Balance get balance => _balance ??= getCachedBalance();
|
||||
Balance? _balance;
|
||||
|
||||
Future<String?> requestWork(String hash) async {
|
||||
return http
|
||||
.post(
|
||||
Uri.parse("https://rpc.nano.to"), // this should be a
|
||||
headers: {'Content-type': 'application/json'},
|
||||
body: json.encode(
|
||||
{
|
||||
"action": "work_generate",
|
||||
"hash": hash,
|
||||
},
|
||||
),
|
||||
)
|
||||
.then((http.Response response) {
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> decoded =
|
||||
json.decode(response.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 ${response.statusCode}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
||||
try {
|
||||
// our address:
|
||||
final String publicAddress = await getAddressFromMnemonic();
|
||||
|
||||
// first get the account balance:
|
||||
final balanceBody = jsonEncode({
|
||||
"action": "account_balance",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final balanceResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
);
|
||||
final balanceData = jsonDecode(balanceResponse.body);
|
||||
|
||||
final BigInt currentBalance =
|
||||
BigInt.parse(balanceData["balance"].toString());
|
||||
final BigInt txAmount = txData["recipientAmt"].raw as BigInt;
|
||||
final BigInt balanceAfterTx = currentBalance - txAmount;
|
||||
|
||||
// get the account info (we need the frontier and representative):
|
||||
final infoBody = jsonEncode({
|
||||
"action": "account_info",
|
||||
"representative": "true",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
);
|
||||
|
||||
final String frontier =
|
||||
jsonDecode(infoResponse.body)["frontier"].toString();
|
||||
final String representative =
|
||||
jsonDecode(infoResponse.body)["representative"].toString();
|
||||
// link = destination address:
|
||||
final String link =
|
||||
NanoAccounts.extractPublicKey(txData["address"].toString());
|
||||
final String linkAsAccount = txData["address"].toString();
|
||||
|
||||
// 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 http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
);
|
||||
|
||||
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 decoded["hash"].toString();
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Error sending transaction $e - $s", level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> get currentReceivingAddress => getAddressFromMnemonic();
|
||||
|
||||
@override
|
||||
Future<Amount> estimateFeeFor(Amount amount, int feeRate) {
|
||||
// fees are always 0 :)
|
||||
return Future.value(
|
||||
Amount(rawValue: BigInt.from(0), fractionDigits: coin.decimals));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> exit() async {
|
||||
_hasCalledExit = true;
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement fees
|
||||
Future<FeeObject> get fees => throw UnimplementedError();
|
||||
|
||||
Future<void> updateBalance() async {
|
||||
final body = jsonEncode({
|
||||
"action": "account_balance",
|
||||
"account": await getAddressFromMnemonic(),
|
||||
});
|
||||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final response = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: headers, body: body);
|
||||
final data = jsonDecode(response.body);
|
||||
_balance = Balance(
|
||||
total: Amount(
|
||||
rawValue: (BigInt.parse(data["balance"].toString()) +
|
||||
BigInt.parse(data["receivable"].toString())),
|
||||
fractionDigits: coin.decimals),
|
||||
spendable: Amount(
|
||||
rawValue: BigInt.parse(data["balance"].toString()),
|
||||
fractionDigits: coin.decimals),
|
||||
blockedTotal:
|
||||
Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals),
|
||||
pendingSpendable: Amount(
|
||||
rawValue: BigInt.parse(data["receivable"].toString()),
|
||||
fractionDigits: coin.decimals),
|
||||
);
|
||||
await updateCachedBalance(_balance!);
|
||||
}
|
||||
|
||||
Future<void> receiveBlock(
|
||||
String blockHash, String source, String amountRaw) async {
|
||||
// TODO: the opening block of an account is a special case
|
||||
bool openBlock = false;
|
||||
|
||||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// our address:
|
||||
final String publicAddress = await getAddressFromMnemonic();
|
||||
|
||||
// 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 http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
);
|
||||
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 http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
);
|
||||
|
||||
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 = DEFAULT_REPRESENTATIVE;
|
||||
}
|
||||
|
||||
// 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 http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
);
|
||||
|
||||
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() async {
|
||||
final receivableResponse = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "receivable",
|
||||
"source": "true",
|
||||
"account": await getAddressFromMnemonic(),
|
||||
"count": "-1",
|
||||
}));
|
||||
|
||||
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);
|
||||
// a bit of a hack:
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTransactions() async {
|
||||
await confirmAllReceivable();
|
||||
final String publicAddress = await getAddressFromMnemonic();
|
||||
final response = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "account_history",
|
||||
"account": publicAddress,
|
||||
"count": "-1",
|
||||
}));
|
||||
final data = await jsonDecode(response.body);
|
||||
final transactions = 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: coin.decimals,
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
Address address = Address(
|
||||
walletId: walletId,
|
||||
publicKey: [],
|
||||
value: transactionType == TransactionType.incoming
|
||||
? publicAddress
|
||||
: tx["account"].toString(),
|
||||
derivationIndex: 0,
|
||||
derivationPath: null,
|
||||
type: transactionType == TransactionType.incoming
|
||||
? AddressType.nonWallet
|
||||
: AddressType.banano,
|
||||
subType: transactionType == TransactionType.incoming
|
||||
? AddressSubType.receiving
|
||||
: AddressSubType.nonWallet,
|
||||
);
|
||||
Tuple2<Transaction, Address> tuple = Tuple2(transaction, address);
|
||||
transactionList.add(tuple);
|
||||
}
|
||||
|
||||
await db.addNewTransactionData(transactionList, walletId);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> fullRescan(
|
||||
int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async {
|
||||
await _prefs.init();
|
||||
await updateTransactions();
|
||||
await updateBalance();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> generateNewAddress() {
|
||||
// TODO: implement generateNewAddress
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasCalledExit => _hasCalledExit;
|
||||
bool _hasCalledExit = false;
|
||||
|
||||
@override
|
||||
Future<void> initializeExisting() async {
|
||||
await _prefs.init();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
|
||||
throw Exception(
|
||||
"Attempted to overwrite mnemonic on generate new wallet!");
|
||||
}
|
||||
|
||||
await _prefs.init();
|
||||
|
||||
String seed = NanoSeeds.generateSeed();
|
||||
final mnemonic = NanoMnemomics.seedToMnemonic(seed);
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: mnemonic.join(' '),
|
||||
);
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: "",
|
||||
);
|
||||
String privateKey = NanoKeys.seedToPrivate(seed, 0);
|
||||
String publicKey = NanoKeys.createPublicKey(privateKey);
|
||||
String publicAddress =
|
||||
NanoAccounts.createAccount(NanoAccountType.BANANO, publicKey);
|
||||
|
||||
final address = Address(
|
||||
walletId: walletId,
|
||||
value: publicAddress,
|
||||
publicKey: [], // TODO: add public key
|
||||
derivationIndex: 0,
|
||||
derivationPath: null,
|
||||
type: AddressType.unknown,
|
||||
subType: AddressSubType.receiving,
|
||||
);
|
||||
|
||||
await db.putAddress(address);
|
||||
|
||||
await Future.wait(
|
||||
[updateCachedId(walletId), updateCachedIsFavorite(false)]);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
bool _isConnected = false;
|
||||
|
||||
@override
|
||||
bool get isRefreshing => refreshMutex;
|
||||
|
||||
bool refreshMutex = false;
|
||||
|
||||
@override
|
||||
Future<int> get maxFee => Future.value(0);
|
||||
|
||||
@override
|
||||
Future<List<String>> get mnemonic => _getMnemonicList();
|
||||
|
||||
Future<List<String>> _getMnemonicList() async {
|
||||
final _mnemonicString = await mnemonicString;
|
||||
if (_mnemonicString == null) {
|
||||
return [];
|
||||
}
|
||||
final List<String> data = _mnemonicString.split(' ');
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> prepareSend({
|
||||
required String address,
|
||||
required Amount amount,
|
||||
Map<String, dynamic>? args,
|
||||
}) async {
|
||||
try {
|
||||
int satAmount = amount.raw.toInt();
|
||||
int realfee = 0;
|
||||
|
||||
if (balance.spendable == amount) {
|
||||
satAmount = balance.spendable.raw.toInt() - realfee;
|
||||
}
|
||||
|
||||
Map<String, dynamic> txData = {
|
||||
"fee": realfee,
|
||||
"addresss": address,
|
||||
"recipientAmt": Amount(
|
||||
rawValue: BigInt.from(satAmount),
|
||||
fractionDigits: coin.decimals,
|
||||
),
|
||||
};
|
||||
|
||||
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
||||
return txData;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> recoverFromMnemonic(
|
||||
{required String mnemonic,
|
||||
String? mnemonicPassphrase,
|
||||
required int maxUnusedAddressGap,
|
||||
required int maxNumberOfIndexesToCheck,
|
||||
required int height}) async {
|
||||
try {
|
||||
if ((await mnemonicString) != null ||
|
||||
(await this.mnemonicPassphrase) != null) {
|
||||
throw Exception("Attempted to overwrite mnemonic on restore!");
|
||||
}
|
||||
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonic', value: mnemonic.trim());
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
String seed = NanoMnemomics.mnemonicListToSeed(mnemonic.split(" "));
|
||||
String privateKey = NanoKeys.seedToPrivate(seed, 0);
|
||||
String publicKey = NanoKeys.createPublicKey(privateKey);
|
||||
String publicAddress =
|
||||
NanoAccounts.createAccount(NanoAccountType.BANANO, publicKey);
|
||||
|
||||
final address = Address(
|
||||
walletId: walletId,
|
||||
value: publicAddress,
|
||||
publicKey: [], // TODO: add public key
|
||||
derivationIndex: 0,
|
||||
derivationPath: null,
|
||||
type: AddressType.unknown,
|
||||
subType: AddressSubType.receiving,
|
||||
);
|
||||
|
||||
await db.putAddress(address);
|
||||
|
||||
await Future.wait(
|
||||
[updateCachedId(walletId), updateCachedIsFavorite(false)]);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refresh() async {
|
||||
await _prefs.init();
|
||||
|
||||
GlobalEventBus.instance.fire(
|
||||
WalletSyncStatusChangedEvent(
|
||||
WalletSyncStatus.syncing,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await updateChainHeight();
|
||||
await updateTransactions();
|
||||
await updateBalance();
|
||||
|
||||
GlobalEventBus.instance.fire(
|
||||
WalletSyncStatusChangedEvent(
|
||||
WalletSyncStatus.synced,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
GlobalEventBus.instance.fire(
|
||||
WalletSyncStatusChangedEvent(
|
||||
WalletSyncStatus.unableToSync,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get storedChainHeight => getCachedChainHeight();
|
||||
|
||||
NodeModel getCurrentNode() {
|
||||
return _xnoNode ??
|
||||
NodeService(secureStorageInterface: _secureStore)
|
||||
.getPrimaryNodeFor(coin: coin) ??
|
||||
DefaultNodes.getNodeFor(coin);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> testNetworkConnection() {
|
||||
http
|
||||
.get(Uri.parse("${getCurrentNode().host}?action=version"))
|
||||
.then((response) {
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return Future.value(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Transaction>> get transactions =>
|
||||
db.getTransactions(walletId).findAll();
|
||||
|
||||
@override
|
||||
Future<void> updateNode(bool shouldRefresh) async {
|
||||
_xnoNode = NodeService(secureStorageInterface: _secureStore)
|
||||
.getPrimaryNodeFor(coin: coin) ??
|
||||
DefaultNodes.getNodeFor(coin);
|
||||
|
||||
if (shouldRefresh) {
|
||||
unawaited(refresh());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) {
|
||||
// TODO: implement updateSentCachedTxData
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement utxos
|
||||
Future<List<UTXO>> get utxos => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
bool validateAddress(String address) {
|
||||
return NanoAccounts.isValid(NanoAccountType.BANANO, address);
|
||||
}
|
||||
|
||||
Future<void> updateChainHeight() async {
|
||||
final String publicAddress = await getAddressFromMnemonic();
|
||||
// first get the account balance:
|
||||
final infoBody = jsonEncode({
|
||||
"action": "account_info",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
);
|
||||
final infoData = jsonDecode(infoResponse.body);
|
||||
|
||||
final int height = int.parse(infoData["confirmation_height"].toString());
|
||||
await updateCachedChainHeight(height);
|
||||
}
|
||||
}
|
|
@ -100,7 +100,7 @@ class PriceAPI {
|
|||
Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency"
|
||||
"=${baseCurrency.toLowerCase()}"
|
||||
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
|
||||
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano"
|
||||
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,ban"
|
||||
"&order=market_cap_desc&per_page=50&page=1&sparkline=false");
|
||||
|
||||
final coinGeckoResponse = await client.get(
|
||||
|
|
|
@ -29,6 +29,7 @@ class CoinThemeColorDefault {
|
|||
Color get wownero => const Color(0xFFED80C1);
|
||||
Color get particl => const Color(0xFF8175BD);
|
||||
Color get nano => const Color(0xFF209CE9);
|
||||
Color get banano => const Color(0xFFFBDD11);
|
||||
|
||||
Color forCoin(Coin coin) {
|
||||
switch (coin) {
|
||||
|
@ -63,6 +64,8 @@ class CoinThemeColorDefault {
|
|||
return particl;
|
||||
case Coin.nano:
|
||||
return nano;
|
||||
case Coin.banano:
|
||||
return banano;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1709,6 +1709,8 @@ class StackColors extends ThemeExtension<StackColors> {
|
|||
return _coin.particl;
|
||||
case Coin.nano:
|
||||
return _coin.nano;
|
||||
case Coin.banano:
|
||||
return _coin.banano;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,8 @@ class AddressUtils {
|
|||
return Address.validateAddress(address, particl);
|
||||
case Coin.nano:
|
||||
return NanoAccounts.isValid(NanoAccountType.NANO, address);
|
||||
case Coin.banano:
|
||||
return NanoAccounts.isValid(NanoAccountType.BANANO, address);
|
||||
case Coin.bitcoinTestNet:
|
||||
return Address.validateAddress(address, testnet);
|
||||
case Coin.litecoinTestNet:
|
||||
|
|
|
@ -56,6 +56,8 @@ Uri getDefaultBlockExplorerUrlFor({
|
|||
return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm");
|
||||
case Coin.nano:
|
||||
return Uri.parse("https://www.nanolooker.com/block/$txid");
|
||||
case Coin.banano:
|
||||
return Uri.parse("https://www.bananolooker.com/block/$txid");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,10 +40,13 @@ abstract class Constants {
|
|||
static final BigInt _satsPerCoinMonero = BigInt.from(1000000000000);
|
||||
static final BigInt _satsPerCoinWownero = BigInt.from(100000000000);
|
||||
static final BigInt _satsPerCoinNano =
|
||||
BigInt.parse("1000000000000000000000000000000");
|
||||
BigInt.parse("1000000000000000000000000000000");// 1*10^30
|
||||
static final BigInt _satsPerCoinBanano =
|
||||
BigInt.parse("100000000000000000000000000000");// 1*10^29
|
||||
static final BigInt _satsPerCoin = BigInt.from(100000000);
|
||||
static const int _decimalPlaces = 8;
|
||||
static const int _decimalPlacesNano = 30;
|
||||
static const int _decimalPlacesBanano = 29;
|
||||
static const int _decimalPlacesWownero = 11;
|
||||
static const int _decimalPlacesMonero = 12;
|
||||
static const int _decimalPlacesEthereum = 18;
|
||||
|
@ -79,6 +82,9 @@ abstract class Constants {
|
|||
case Coin.nano:
|
||||
return _satsPerCoinNano;
|
||||
|
||||
case Coin.banano:
|
||||
return _satsPerCoinBanano;
|
||||
|
||||
case Coin.wownero:
|
||||
return _satsPerCoinWownero;
|
||||
|
||||
|
@ -113,6 +119,9 @@ abstract class Constants {
|
|||
case Coin.nano:
|
||||
return _decimalPlacesNano;
|
||||
|
||||
case Coin.banano:
|
||||
return _decimalPlacesBanano;
|
||||
|
||||
case Coin.wownero:
|
||||
return _decimalPlacesWownero;
|
||||
|
||||
|
@ -146,7 +155,10 @@ abstract class Constants {
|
|||
case Coin.namecoin:
|
||||
case Coin.particl:
|
||||
case Coin.nano:
|
||||
values.addAll([24, 21, 18, 15, 12]);
|
||||
values.addAll([24, 12]);
|
||||
break;
|
||||
case Coin.banano:
|
||||
values.addAll([24, 12]);
|
||||
break;
|
||||
|
||||
case Coin.monero:
|
||||
|
@ -200,7 +212,8 @@ abstract class Constants {
|
|||
return 600;
|
||||
|
||||
case Coin.nano: // TODO: Verify this
|
||||
return 3;
|
||||
case Coin.banano: // TODO: Verify this
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -190,8 +190,18 @@ abstract class DefaultNodes {
|
|||
enabled: true,
|
||||
coinName: Coin.nano.name,
|
||||
isFailover: true,
|
||||
isDown: false
|
||||
);
|
||||
isDown: false);
|
||||
|
||||
static NodeModel get banano => NodeModel(
|
||||
host: "https://kaliumapi.appditto.com/api",
|
||||
port: 443,
|
||||
name: defaultName,
|
||||
id: _nodeId(Coin.banano),
|
||||
useSSL: true,
|
||||
enabled: true,
|
||||
coinName: Coin.banano.name,
|
||||
isFailover: true,
|
||||
isDown: false);
|
||||
|
||||
static NodeModel get bitcoinTestnet => NodeModel(
|
||||
host: "bitcoin-testnet.stackwallet.com",
|
||||
|
@ -294,6 +304,9 @@ abstract class DefaultNodes {
|
|||
case Coin.nano:
|
||||
return nano;
|
||||
|
||||
case Coin.banano:
|
||||
return banano;
|
||||
|
||||
case Coin.bitcoinTestNet:
|
||||
return bitcoinTestnet;
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ enum Coin {
|
|||
particl,
|
||||
wownero,
|
||||
nano,
|
||||
banano,
|
||||
|
||||
///
|
||||
|
||||
|
@ -89,6 +90,8 @@ extension CoinExt on Coin {
|
|||
return "Namecoin";
|
||||
case Coin.nano:
|
||||
return "Nano";
|
||||
case Coin.banano:
|
||||
return "Banano";
|
||||
case Coin.bitcoinTestNet:
|
||||
return "tBitcoin";
|
||||
case Coin.litecoinTestNet:
|
||||
|
@ -130,6 +133,8 @@ extension CoinExt on Coin {
|
|||
return "NMC";
|
||||
case Coin.nano:
|
||||
return "XNO";
|
||||
case Coin.banano:
|
||||
return "BAN";
|
||||
case Coin.bitcoinTestNet:
|
||||
return "tBTC";
|
||||
case Coin.litecoinTestNet:
|
||||
|
@ -172,6 +177,8 @@ extension CoinExt on Coin {
|
|||
return "namecoin";
|
||||
case Coin.nano:
|
||||
return "nano";
|
||||
case Coin.banano:
|
||||
return "ban";
|
||||
case Coin.bitcoinTestNet:
|
||||
return "bitcoin";
|
||||
case Coin.litecoinTestNet:
|
||||
|
@ -206,7 +213,8 @@ extension CoinExt on Coin {
|
|||
case Coin.ethereum:
|
||||
case Coin.monero:
|
||||
case Coin.wownero:
|
||||
case Coin.nano: // TODO: Check this
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -233,6 +241,7 @@ extension CoinExt on Coin {
|
|||
case Coin.bitcoincashTestnet:
|
||||
case Coin.firoTestNet:
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -251,6 +260,7 @@ extension CoinExt on Coin {
|
|||
case Coin.monero:
|
||||
case Coin.wownero:
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
case Coin.eCash:
|
||||
return false;
|
||||
|
||||
|
@ -277,6 +287,7 @@ extension CoinExt on Coin {
|
|||
case Coin.monero:
|
||||
case Coin.wownero:
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
case Coin.eCash:
|
||||
return this;
|
||||
|
||||
|
@ -341,6 +352,7 @@ extension CoinExt on Coin {
|
|||
return nmc.MINIMUM_CONFIRMATIONS;
|
||||
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
return nano.MINIMUM_CONFIRMATIONS;
|
||||
}
|
||||
}
|
||||
|
@ -432,6 +444,10 @@ Coin coinFromPrettyName(String name) {
|
|||
case "nano":
|
||||
return Coin.nano;
|
||||
|
||||
case "Banano":
|
||||
case "banano":
|
||||
return Coin.banano;
|
||||
|
||||
default:
|
||||
throw ArgumentError.value(
|
||||
name,
|
||||
|
@ -479,6 +495,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) {
|
|||
return Coin.wownero;
|
||||
case "xno":
|
||||
return Coin.nano;
|
||||
case "ban":
|
||||
return Coin.banano;
|
||||
default:
|
||||
throw ArgumentError.value(
|
||||
ticker, "name", "No Coin enum value with that ticker");
|
||||
|
|
|
@ -48,6 +48,7 @@ extension DerivePathTypeExt on DerivePathType {
|
|||
case Coin.monero:
|
||||
case Coin.wownero:
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
throw UnsupportedError(
|
||||
"$coin does not use bitcoin style derivation paths");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue