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/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.isTestNet, ); 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.isTestNet, ); 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.isTestNet, ); 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.isTestNet, ), ); 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.isTestNet, ); 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.isTestNet, ); 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.isTestNet, ); 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 ), ); } }