mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-26 21:39:21 +00:00
557 lines
17 KiB
Dart
557 lines
17 KiB
Dart
import 'package:bip32/bip32.dart';
|
|
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:lelantus/lelantus.dart' as lelantus;
|
|
|
|
import '../../models/isar/models/isar_models.dart' as isar_models;
|
|
import '../../models/isar/models/isar_models.dart';
|
|
import '../../models/lelantus_fee_data.dart';
|
|
import '../../utilities/amount/amount.dart';
|
|
import '../../utilities/extensions/impl/string.dart';
|
|
import '../../utilities/extensions/impl/uint8_list.dart';
|
|
import '../../utilities/format.dart';
|
|
import '../../utilities/logger.dart';
|
|
import '../crypto_currency/crypto_currency.dart';
|
|
import '../crypto_currency/intermediate/bip39_hd_currency.dart';
|
|
import '../models/tx_data.dart';
|
|
|
|
abstract final class LelantusFfiWrapper {
|
|
static const MINT_LIMIT = 5001 * 100000000;
|
|
static const MINT_LIMIT_TESTNET = 1001 * 100000000;
|
|
|
|
static const JMINT_INDEX = 5;
|
|
static const MINT_INDEX = 2;
|
|
static const TRANSACTION_LELANTUS = 8;
|
|
static const ANONYMITY_SET_EMPTY_ID = 0;
|
|
|
|
// partialDerivationPath should be something like "m/$purpose'/$coinType'/$account'/"
|
|
static Future<({List<String> spendTxIds, List<LelantusCoin> lelantusCoins})>
|
|
restore({
|
|
required final String hexRootPrivateKey,
|
|
required final Uint8List chaincode,
|
|
required final Bip39HDCurrency cryptoCurrency,
|
|
required final int latestSetId,
|
|
required final Map<dynamic, dynamic> setDataMap,
|
|
required final Set<String> usedSerialNumbers,
|
|
required final String walletId,
|
|
required final String partialDerivationPath,
|
|
}) async {
|
|
final args = (
|
|
hexRootPrivateKey: hexRootPrivateKey,
|
|
chaincode: chaincode,
|
|
cryptoCurrency: cryptoCurrency,
|
|
latestSetId: latestSetId,
|
|
setDataMap: setDataMap,
|
|
usedSerialNumbers: usedSerialNumbers,
|
|
walletId: walletId,
|
|
partialDerivationPath: partialDerivationPath,
|
|
);
|
|
try {
|
|
return await compute(_restore, args);
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Exception rethrown from _restore(): $e\n$s",
|
|
level: LogLevel.Info,
|
|
);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// partialDerivationPath should be something like "m/$purpose'/$coinType'/$account'/"
|
|
static Future<({List<String> spendTxIds, List<LelantusCoin> lelantusCoins})>
|
|
_restore(
|
|
({
|
|
String hexRootPrivateKey,
|
|
Uint8List chaincode,
|
|
Bip39HDCurrency cryptoCurrency,
|
|
int latestSetId,
|
|
Map<dynamic, dynamic> setDataMap,
|
|
Set<String> usedSerialNumbers,
|
|
String walletId,
|
|
String partialDerivationPath,
|
|
}) args,
|
|
) async {
|
|
final List<int> jindexes = [];
|
|
final List<isar_models.LelantusCoin> lelantusCoins = [];
|
|
|
|
final List<String> spendTxIds = [];
|
|
int lastFoundIndex = 0;
|
|
int currentIndex = 0;
|
|
|
|
final root = BIP32.fromPrivateKey(
|
|
args.hexRootPrivateKey.toUint8ListFromHex,
|
|
args.chaincode,
|
|
);
|
|
|
|
while (currentIndex < lastFoundIndex + 50) {
|
|
final mintKeyPair = root.derivePath(
|
|
"${args.partialDerivationPath}$MINT_INDEX/$currentIndex",
|
|
);
|
|
|
|
final String mintTag = lelantus.CreateTag(
|
|
mintKeyPair.privateKey!.toHex,
|
|
currentIndex,
|
|
mintKeyPair.identifier.toHex,
|
|
isTestnet: args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
);
|
|
|
|
for (int setId = 1; setId <= args.latestSetId; setId++) {
|
|
final setData = args.setDataMap[setId] as Map;
|
|
final foundCoin = (setData["coins"] as List).firstWhere(
|
|
(e) => e[1] == mintTag,
|
|
orElse: () => <Object>[],
|
|
);
|
|
|
|
if (foundCoin.length == 4) {
|
|
lastFoundIndex = currentIndex;
|
|
|
|
final String publicCoin = foundCoin[0] as String;
|
|
final String txId = foundCoin[3] as String;
|
|
|
|
// this value will either be an int or a String
|
|
final dynamic thirdValue = foundCoin[2];
|
|
|
|
if (thirdValue is int) {
|
|
final int amount = thirdValue;
|
|
final String serialNumber = lelantus.GetSerialNumber(
|
|
amount,
|
|
mintKeyPair.privateKey!.toHex,
|
|
currentIndex,
|
|
isTestnet:
|
|
args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
);
|
|
final bool isUsed = args.usedSerialNumbers.contains(serialNumber);
|
|
|
|
lelantusCoins.removeWhere(
|
|
(e) =>
|
|
e.txid == txId &&
|
|
e.mintIndex == currentIndex &&
|
|
e.anonymitySetId != setId,
|
|
);
|
|
|
|
lelantusCoins.add(
|
|
isar_models.LelantusCoin(
|
|
walletId: args.walletId,
|
|
mintIndex: currentIndex,
|
|
value: amount.toString(),
|
|
txid: txId,
|
|
anonymitySetId: setId,
|
|
isUsed: isUsed,
|
|
isJMint: false,
|
|
otherData:
|
|
publicCoin, // not really needed but saved just in case
|
|
),
|
|
);
|
|
debugPrint("serial=$serialNumber amount=$amount used=$isUsed");
|
|
} else if (thirdValue is String) {
|
|
final int keyPath = lelantus.GetAesKeyPath(publicCoin);
|
|
|
|
final aesKeyPair = root.derivePath(
|
|
"${args.partialDerivationPath}$JMINT_INDEX/$keyPath",
|
|
);
|
|
|
|
try {
|
|
final String aesPrivateKey = aesKeyPair.privateKey!.toHex;
|
|
|
|
final int amount = lelantus.decryptMintAmount(
|
|
aesPrivateKey,
|
|
thirdValue,
|
|
);
|
|
|
|
final String serialNumber = lelantus.GetSerialNumber(
|
|
amount,
|
|
aesPrivateKey,
|
|
currentIndex,
|
|
isTestnet:
|
|
args.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
);
|
|
final bool isUsed = args.usedSerialNumbers.contains(serialNumber);
|
|
|
|
lelantusCoins.removeWhere(
|
|
(e) =>
|
|
e.txid == txId &&
|
|
e.mintIndex == currentIndex &&
|
|
e.anonymitySetId != setId,
|
|
);
|
|
|
|
lelantusCoins.add(
|
|
isar_models.LelantusCoin(
|
|
walletId: args.walletId,
|
|
mintIndex: currentIndex,
|
|
value: amount.toString(),
|
|
txid: txId,
|
|
anonymitySetId: setId,
|
|
isUsed: isUsed,
|
|
isJMint: true,
|
|
otherData:
|
|
publicCoin, // not really needed but saved just in case
|
|
),
|
|
);
|
|
jindexes.add(currentIndex);
|
|
|
|
spendTxIds.add(txId);
|
|
} catch (_) {
|
|
debugPrint("AES keypair derivation issue for key path: $keyPath");
|
|
}
|
|
} else {
|
|
debugPrint("Unexpected coin found: $foundCoin");
|
|
}
|
|
}
|
|
}
|
|
|
|
currentIndex++;
|
|
}
|
|
|
|
return (spendTxIds: spendTxIds, lelantusCoins: lelantusCoins);
|
|
}
|
|
|
|
static Future<LelantusFeeData> estimateJoinSplitFee({
|
|
required Amount spendAmount,
|
|
required bool subtractFeeFromAmount,
|
|
required List<lelantus.DartLelantusEntry> lelantusEntries,
|
|
required bool isTestNet,
|
|
}) async {
|
|
return await compute(
|
|
LelantusFfiWrapper._estimateJoinSplitFee,
|
|
(
|
|
spendAmount: spendAmount.raw.toInt(),
|
|
subtractFeeFromAmount: subtractFeeFromAmount,
|
|
lelantusEntries: lelantusEntries,
|
|
isTestNet: isTestNet,
|
|
),
|
|
);
|
|
}
|
|
|
|
static Future<LelantusFeeData> _estimateJoinSplitFee(
|
|
({
|
|
int spendAmount,
|
|
bool subtractFeeFromAmount,
|
|
List<lelantus.DartLelantusEntry> lelantusEntries,
|
|
bool isTestNet,
|
|
}) data,
|
|
) async {
|
|
debugPrint("estimateJoinSplit fee");
|
|
// for (int i = 0; i < lelantusEntries.length; i++) {
|
|
// Logging.instance.log(lelantusEntries[i], addToDebugMessagesDB: false);
|
|
// }
|
|
debugPrint(
|
|
"${data.spendAmount} ${data.subtractFeeFromAmount}",
|
|
);
|
|
|
|
final List<int> changeToMint = List.empty(growable: true);
|
|
final List<int> spendCoinIndexes = List.empty(growable: true);
|
|
// Logging.instance.log(lelantusEntries, addToDebugMessagesDB: false);
|
|
final fee = lelantus.estimateFee(
|
|
data.spendAmount,
|
|
data.subtractFeeFromAmount,
|
|
data.lelantusEntries,
|
|
changeToMint,
|
|
spendCoinIndexes,
|
|
isTestnet: data.isTestNet,
|
|
);
|
|
|
|
final estimateFeeData = LelantusFeeData(
|
|
changeToMint[0],
|
|
fee,
|
|
spendCoinIndexes,
|
|
);
|
|
debugPrint(
|
|
"estimateFeeData ${estimateFeeData.changeToMint}"
|
|
" ${estimateFeeData.fee}"
|
|
" ${estimateFeeData.spendCoinIndexes}",
|
|
);
|
|
return estimateFeeData;
|
|
}
|
|
|
|
static Future<TxData> createJoinSplitTransaction({
|
|
required TxData txData,
|
|
required bool subtractFeeFromAmount,
|
|
required int nextFreeMintIndex,
|
|
required int locktime, // set to current chain height
|
|
required List<lelantus.DartLelantusEntry> lelantusEntries,
|
|
required List<Map<String, dynamic>> anonymitySets,
|
|
required Bip39HDCurrency cryptoCurrency,
|
|
required String partialDerivationPath,
|
|
required String hexRootPrivateKey,
|
|
required Uint8List chaincode,
|
|
}) async {
|
|
final arg = (
|
|
txData: txData,
|
|
subtractFeeFromAmount: subtractFeeFromAmount,
|
|
index: nextFreeMintIndex,
|
|
lelantusEntries: lelantusEntries,
|
|
locktime: locktime,
|
|
cryptoCurrency: cryptoCurrency,
|
|
anonymitySetsArg: anonymitySets,
|
|
partialDerivationPath: partialDerivationPath,
|
|
hexRootPrivateKey: hexRootPrivateKey,
|
|
chaincode: chaincode,
|
|
);
|
|
|
|
return await compute(_createJoinSplitTransaction, arg);
|
|
}
|
|
|
|
static Future<TxData> _createJoinSplitTransaction(
|
|
({
|
|
TxData txData,
|
|
bool subtractFeeFromAmount,
|
|
int index,
|
|
List<lelantus.DartLelantusEntry> lelantusEntries,
|
|
int locktime,
|
|
Bip39HDCurrency cryptoCurrency,
|
|
List<Map<dynamic, dynamic>> anonymitySetsArg,
|
|
String partialDerivationPath,
|
|
String hexRootPrivateKey,
|
|
Uint8List chaincode,
|
|
}) arg,
|
|
) async {
|
|
final spendAmount = arg.txData.recipients!.first.amount.raw.toInt();
|
|
final address = arg.txData.recipients!.first.address;
|
|
final isChange = arg.txData.recipients!.first.isChange;
|
|
|
|
final estimateJoinSplitFee = await _estimateJoinSplitFee(
|
|
(
|
|
spendAmount: spendAmount,
|
|
subtractFeeFromAmount: arg.subtractFeeFromAmount,
|
|
lelantusEntries: arg.lelantusEntries,
|
|
isTestNet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
),
|
|
);
|
|
final changeToMint = estimateJoinSplitFee.changeToMint;
|
|
final fee = estimateJoinSplitFee.fee;
|
|
final spendCoinIndexes = estimateJoinSplitFee.spendCoinIndexes;
|
|
debugPrint("$changeToMint $fee $spendCoinIndexes");
|
|
if (spendCoinIndexes.isEmpty) {
|
|
throw Exception("Error, Not enough funds.");
|
|
}
|
|
|
|
final params = arg.cryptoCurrency.networkParams;
|
|
final _network = bitcoindart.NetworkType(
|
|
messagePrefix: params.messagePrefix,
|
|
bech32: params.bech32Hrp,
|
|
bip32: bitcoindart.Bip32Type(
|
|
public: params.pubHDPrefix,
|
|
private: params.privHDPrefix,
|
|
),
|
|
pubKeyHash: params.p2pkhPrefix,
|
|
scriptHash: params.p2shPrefix,
|
|
wif: params.wifPrefix,
|
|
);
|
|
|
|
final tx = bitcoindart.TransactionBuilder(network: _network);
|
|
tx.setLockTime(arg.locktime);
|
|
|
|
tx.setVersion(3 | (TRANSACTION_LELANTUS << 16));
|
|
|
|
tx.addInput(
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
4294967295,
|
|
4294967295,
|
|
Uint8List(0),
|
|
);
|
|
final derivePath = "${arg.partialDerivationPath}$MINT_INDEX/${arg.index}";
|
|
|
|
final root = BIP32.fromPrivateKey(
|
|
arg.hexRootPrivateKey.toUint8ListFromHex,
|
|
arg.chaincode,
|
|
);
|
|
|
|
final jmintKeyPair = root.derivePath(derivePath);
|
|
|
|
final String jmintprivatekey = jmintKeyPair.privateKey!.toHex;
|
|
|
|
final keyPath = lelantus.getMintKeyPath(
|
|
changeToMint,
|
|
jmintprivatekey,
|
|
arg.index,
|
|
isTestnet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
);
|
|
|
|
final _derivePath = "${arg.partialDerivationPath}$JMINT_INDEX/$keyPath";
|
|
|
|
final aesKeyPair = root.derivePath(_derivePath);
|
|
final aesPrivateKey = aesKeyPair.privateKey!.toHex;
|
|
|
|
final jmintData = lelantus.createJMintScript(
|
|
changeToMint,
|
|
jmintprivatekey,
|
|
arg.index,
|
|
Format.uint8listToString(jmintKeyPair.identifier),
|
|
aesPrivateKey,
|
|
isTestnet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
);
|
|
|
|
tx.addOutput(
|
|
Format.stringToUint8List(jmintData),
|
|
0,
|
|
);
|
|
|
|
int amount = spendAmount;
|
|
if (arg.subtractFeeFromAmount) {
|
|
amount -= fee;
|
|
}
|
|
tx.addOutput(
|
|
address,
|
|
amount,
|
|
);
|
|
|
|
final extractedTx = tx.buildIncomplete();
|
|
extractedTx.setPayload(Uint8List(0));
|
|
final txHash = extractedTx.getId();
|
|
|
|
final List<int> setIds = [];
|
|
final List<List<String>> anonymitySets = [];
|
|
final List<String> anonymitySetHashes = [];
|
|
final List<String> groupBlockHashes = [];
|
|
for (var i = 0; i < arg.lelantusEntries.length; i++) {
|
|
final anonymitySetId = arg.lelantusEntries[i].anonymitySetId;
|
|
if (!setIds.contains(anonymitySetId)) {
|
|
setIds.add(anonymitySetId);
|
|
final anonymitySet = arg.anonymitySetsArg.firstWhere(
|
|
(element) => element["setId"] == anonymitySetId,
|
|
orElse: () => <String, dynamic>{},
|
|
);
|
|
if (anonymitySet.isNotEmpty) {
|
|
anonymitySetHashes.add(anonymitySet['setHash'] as String);
|
|
groupBlockHashes.add(anonymitySet['blockHash'] as String);
|
|
final List<String> list = [];
|
|
for (int i = 0; i < (anonymitySet['coins'] as List).length; i++) {
|
|
list.add(anonymitySet['coins'][i][0] as String);
|
|
}
|
|
anonymitySets.add(list);
|
|
}
|
|
}
|
|
}
|
|
|
|
final String spendScript = lelantus.createJoinSplitScript(
|
|
txHash,
|
|
spendAmount,
|
|
arg.subtractFeeFromAmount,
|
|
jmintprivatekey,
|
|
arg.index,
|
|
arg.lelantusEntries,
|
|
setIds,
|
|
anonymitySets,
|
|
anonymitySetHashes,
|
|
groupBlockHashes,
|
|
isTestnet: arg.cryptoCurrency.network == CryptoCurrencyNetwork.test,
|
|
);
|
|
|
|
final finalTx = bitcoindart.TransactionBuilder(network: _network);
|
|
finalTx.setLockTime(arg.locktime);
|
|
|
|
finalTx.setVersion(3 | (TRANSACTION_LELANTUS << 16));
|
|
|
|
finalTx.addOutput(
|
|
Format.stringToUint8List(jmintData),
|
|
0,
|
|
);
|
|
|
|
finalTx.addOutput(
|
|
address,
|
|
amount,
|
|
);
|
|
|
|
final extTx = finalTx.buildIncomplete();
|
|
extTx.addInput(
|
|
Format.stringToUint8List(
|
|
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
),
|
|
4294967295,
|
|
4294967295,
|
|
Format.stringToUint8List("c9"),
|
|
);
|
|
// debugPrint("spendscript: $spendScript");
|
|
extTx.setPayload(Format.stringToUint8List(spendScript));
|
|
|
|
final txHex = extTx.toHex();
|
|
final txId = extTx.getId();
|
|
|
|
final amountAmount = Amount(
|
|
rawValue: BigInt.from(amount),
|
|
fractionDigits: arg.cryptoCurrency.fractionDigits,
|
|
);
|
|
|
|
return arg.txData.copyWith(
|
|
txid: txId,
|
|
raw: txHex,
|
|
recipients: [
|
|
(address: address, amount: amountAmount, isChange: isChange),
|
|
],
|
|
fee: Amount(
|
|
rawValue: BigInt.from(fee),
|
|
fractionDigits: arg.cryptoCurrency.fractionDigits,
|
|
),
|
|
vSize: extTx.virtualSize(),
|
|
jMintValue: changeToMint,
|
|
spendCoinIndexes: spendCoinIndexes,
|
|
height: arg.locktime,
|
|
txType: TransactionType.outgoing,
|
|
txSubType: TransactionSubType.join,
|
|
// "confirmed_status": false,
|
|
// "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
);
|
|
|
|
// return {
|
|
// "txid": txId,
|
|
// "txHex": txHex,
|
|
// "value": amount,
|
|
// "fees": Amount(
|
|
// rawValue: BigInt.from(fee),
|
|
// fractionDigits: arg.cryptoCurrency.fractionDigits,
|
|
// ).decimal.toDouble(),
|
|
// "fee": fee,
|
|
// "vSize": extTx.virtualSize(),
|
|
// "jmintValue": changeToMint,
|
|
// "spendCoinIndexes": spendCoinIndexes,
|
|
// "height": arg.locktime,
|
|
// "txType": "Sent",
|
|
// "confirmed_status": false,
|
|
// "amount": amountAmount.decimal.toDouble(),
|
|
// "recipientAmt": amountAmount,
|
|
// "address": arg.address,
|
|
// "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
// "subType": "join",
|
|
// };
|
|
}
|
|
|
|
// ===========================================================================
|
|
|
|
static Future<String> _getMintScriptWrapper(
|
|
({
|
|
int amount,
|
|
String privateKeyHex,
|
|
int index,
|
|
String seedId,
|
|
bool isTestNet
|
|
}) data,
|
|
) async {
|
|
final String mintHex = lelantus.getMintScript(
|
|
data.amount,
|
|
data.privateKeyHex,
|
|
data.index,
|
|
data.seedId,
|
|
isTestnet: data.isTestNet,
|
|
);
|
|
return mintHex;
|
|
}
|
|
|
|
static Future<String> getMintScript({
|
|
required Amount amount,
|
|
required String privateKeyHex,
|
|
required int index,
|
|
required String seedId,
|
|
required bool isTestNet,
|
|
}) async {
|
|
return await compute(
|
|
LelantusFfiWrapper._getMintScriptWrapper,
|
|
(
|
|
amount: amount.raw.toInt(),
|
|
privateKeyHex: privateKeyHex,
|
|
index: index,
|
|
seedId: seedId,
|
|
isTestNet: isTestNet
|
|
),
|
|
);
|
|
}
|
|
}
|