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 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/lelantus_fee_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/extensions/impl/string.dart'; import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/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 spendTxIds, List lelantusCoins})> restore({ required final String hexRootPrivateKey, required final Uint8List chaincode, required final Bip39HDCurrency cryptoCurrency, required final int latestSetId, required final Map setDataMap, required final Set 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 spendTxIds, List lelantusCoins})> _restore( ({ String hexRootPrivateKey, Uint8List chaincode, Bip39HDCurrency cryptoCurrency, int latestSetId, Map setDataMap, Set usedSerialNumbers, String walletId, String partialDerivationPath, }) args, ) async { List jindexes = []; List lelantusCoins = []; final List 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: () => [], ); 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 estimateJoinSplitFee({ required Amount spendAmount, required bool subtractFeeFromAmount, required List lelantusEntries, required bool isTestNet, }) async { return await compute( LelantusFfiWrapper._estimateJoinSplitFee, ( spendAmount: spendAmount.raw.toInt(), subtractFeeFromAmount: subtractFeeFromAmount, lelantusEntries: lelantusEntries, isTestNet: isTestNet, ), ); } static Future _estimateJoinSplitFee( ({ int spendAmount, bool subtractFeeFromAmount, List 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}", ); List changeToMint = List.empty(growable: true); List 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 createJoinSplitTransaction({ required TxData txData, required bool subtractFeeFromAmount, required int nextFreeMintIndex, required int locktime, // set to current chain height required List lelantusEntries, required List> 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 _createJoinSplitTransaction( ({ TxData txData, bool subtractFeeFromAmount, int index, List lelantusEntries, int locktime, Bip39HDCurrency cryptoCurrency, List> 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 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 setIds = []; final List> anonymitySets = []; final List anonymitySetHashes = []; final List 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: () => {}); if (anonymitySet.isNotEmpty) { anonymitySetHashes.add(anonymitySet['setHash'] as String); groupBlockHashes.add(anonymitySet['blockHash'] as String); List 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)], 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 _getMintScriptWrapper( ({ int amount, String privateKeyHex, int index, String seedId, bool isTestNet }) data) async { String mintHex = lelantus.getMintScript( data.amount, data.privateKeyHex, data.index, data.seedId, isTestnet: data.isTestNet, ); return mintHex; } static Future 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 ), ); } }