@@ -4,6 +4,7 @@ import 'dart:isolate';
44import 'dart:math' ;
55
66import 'package:bitcoindart/bitcoindart.dart' as btc;
7+ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
78import 'package:decimal/decimal.dart' ;
89import 'package:flutter/foundation.dart' ;
910import 'package:isar_community/isar.dart' ;
@@ -1546,10 +1547,64 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
15461547 .map ((e) => MutableSparkRecipient (e.address, e.value, e.memo))
15471548 .toList (); // deep copy
15481549 final feesObject = await fees;
1550+ final minRelayFeeRatePerKB = BigInt .from (1000 );
1551+ final mintFeeRatePerKB = feesObject.medium < minRelayFeeRatePerKB
1552+ ? minRelayFeeRatePerKB
1553+ : feesObject.medium;
15491554 final currentHeight = await chainHeight;
15501555 final random = Random .secure ();
15511556 final List <TxData > results = [];
15521557
1558+ final String ? autoMintSparkAddress = autoMintAll
1559+ ? (await getCurrentReceivingSparkAddress ())? .value
1560+ : null ;
1561+ if (autoMintAll && autoMintSparkAddress == null ) {
1562+ throw Exception ("No current Spark receiving address found." );
1563+ }
1564+
1565+ // Cache signing keys lazily for selected inputs. This mirrors the subset
1566+ // of addSigningKeys used by Firo Spark mints; Firo currently supports only
1567+ // BIP44 transparent inputs, so caching from the wallet root is valid here.
1568+ final root = await getRootHDNode ();
1569+ final Map <String , _SparkMintSigningKey > signingKeyCache = {};
1570+ Future <_SparkMintSigningKey > getCachedSigningKey (String address) async {
1571+ final existing = signingKeyCache[address];
1572+ if (existing != null ) {
1573+ return existing;
1574+ }
1575+
1576+ final derivePathType = cryptoCurrency.addressType (address: address);
1577+ final dbAddress = await mainDB.getAddress (walletId, address);
1578+ if (dbAddress? .derivationPath == null ) {
1579+ throw Exception (
1580+ "Signing key not found for address $address . "
1581+ "Local db may be corrupt. Rescan wallet." ,
1582+ );
1583+ }
1584+
1585+ final key = root.derivePath (dbAddress! .derivationPath! .value);
1586+ final cached = (derivePathType: derivePathType, key: key);
1587+ signingKeyCache[address] = cached;
1588+ return cached;
1589+ }
1590+
1591+ Address ? cachedChangeAddress;
1592+ Future <Address > getMintChangeAddress () async {
1593+ cachedChangeAddress ?? = await getCurrentChangeAddress ();
1594+ if (cachedChangeAddress == null ) {
1595+ throw Exception ("No current change address found." );
1596+ }
1597+ return cachedChangeAddress! ;
1598+ }
1599+
1600+ // Pre-fetch wallet-owned addresses for output ownership checks.
1601+ final walletAddresses = await mainDB.isar.addresses
1602+ .where ()
1603+ .walletIdEqualTo (walletId)
1604+ .valueProperty ()
1605+ .findAll ();
1606+ final walletAddressSet = walletAddresses.toSet ();
1607+
15531608 valueAndUTXOs.shuffle (random);
15541609
15551610 while (valueAndUTXOs.isNotEmpty) {
@@ -1590,7 +1645,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
15901645 }
15911646
15921647 // if (!MoneyRange(mintedValue) || mintedValue == 0) {
1593- if (mintedValue = = BigInt .zero) {
1648+ if (mintedValue < = BigInt .zero) {
15941649 valueAndUTXOs.remove (itr);
15951650 skipCoin = true ;
15961651 break ;
@@ -1609,11 +1664,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
16091664
16101665 if (autoMintAll) {
16111666 singleTxOutputs.add (
1612- MutableSparkRecipient (
1613- (await getCurrentReceivingSparkAddress ())! .value,
1614- mintedValue,
1615- "" ,
1616- ),
1667+ MutableSparkRecipient (autoMintSparkAddress! , mintedValue, "" ),
16171668 );
16181669 } else {
16191670 BigInt remainingMintValue = BigInt .parse (mintedValue.toString ());
@@ -1641,25 +1692,42 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
16411692 }
16421693 }
16431694
1644- if (subtractFeeFromAmount) {
1645- final BigInt singleFee =
1646- nFeeRet ~ / BigInt .from (singleTxOutputs.length);
1647- BigInt remainder = nFeeRet % BigInt .from (singleTxOutputs.length);
1648-
1649- for (int i = 0 ; i < singleTxOutputs.length; ++ i) {
1650- if (singleTxOutputs[i].value <= singleFee) {
1651- singleTxOutputs.removeAt (i);
1652- remainder += singleTxOutputs[i].value - singleFee;
1653- -- i;
1695+ if (subtractFeeFromAmount && nFeeRet > BigInt .zero) {
1696+ var remainingFee = nFeeRet;
1697+ var outputIndex = 0 ;
1698+ while (singleTxOutputs.isNotEmpty && remainingFee > BigInt .zero) {
1699+ if (outputIndex >= singleTxOutputs.length) {
1700+ outputIndex = 0 ;
1701+ }
1702+
1703+ final outputsLeft = BigInt .from (
1704+ singleTxOutputs.length - outputIndex,
1705+ );
1706+ var feeShare = remainingFee ~ / outputsLeft;
1707+ if (remainingFee % outputsLeft != BigInt .zero) {
1708+ feeShare += BigInt .one;
16541709 }
1655- singleTxOutputs[i].value -= singleFee;
1656- if (remainder > BigInt .zero &&
1657- singleTxOutputs[i].value >
1658- nFeeRet % BigInt .from (singleTxOutputs.length)) {
1659- // first receiver pays the remainder not divisible by output count
1660- singleTxOutputs[i].value -= remainder;
1661- remainder = BigInt .zero;
1710+
1711+ if (singleTxOutputs[outputIndex].value <= feeShare) {
1712+ remainingFee -= singleTxOutputs[outputIndex].value;
1713+ singleTxOutputs.removeAt (outputIndex);
1714+ continue ;
16621715 }
1716+
1717+ singleTxOutputs[outputIndex].value -= feeShare;
1718+ remainingFee -= feeShare;
1719+ ++ outputIndex;
1720+ }
1721+
1722+ if (singleTxOutputs.isEmpty) {
1723+ if (autoMintAll) {
1724+ throw Exception (
1725+ "UTXO value is too small to cover Spark mint fee" ,
1726+ );
1727+ }
1728+ valueAndUTXOs.remove (itr);
1729+ skipCoin = true ;
1730+ break ;
16631731 }
16641732 }
16651733
@@ -1694,11 +1762,13 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
16941762 BigInt nValueIn = BigInt .zero;
16951763 for (final utxo in itr) {
16961764 if (nValueToSelect > nValueIn) {
1697- setCoins. add (
1698- ( await addSigningKeys ([
1699- StandardInput ( utxo) ,
1700- ])). whereType < StandardInput >().first ,
1765+ final cached = await getCachedSigningKey (utxo.address ! );
1766+ final input = StandardInput (
1767+ utxo,
1768+ derivePathType : cached.derivePathType ,
17011769 );
1770+ input.key = cached.key;
1771+ setCoins.add (input);
17021772 nValueIn += BigInt .from (utxo.value);
17031773 }
17041774 }
@@ -1720,9 +1790,9 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
17201790 throw Exception ("Change index out of range" );
17211791 }
17221792
1723- final changeAddress = await getCurrentChangeAddress ();
1793+ final changeAddress = await getMintChangeAddress ();
17241794 vout.insert (nChangePosInOut, (
1725- changeAddress! .value,
1795+ changeAddress.value,
17261796 nChange.toInt (),
17271797 null ,
17281798 ));
@@ -1817,13 +1887,19 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
18171887 throw Exception ("Transaction too large" );
18181888 }
18191889
1820- const nBytesBuffer = 10 ;
1890+ // ECDSA DER signatures are not fixed-size. Even with low-S
1891+ // normalization, the encoded signature length can vary across
1892+ // signatures, so the dummy signed transaction used for fee estimation
1893+ // can be smaller than the final signed transaction. Use a per-input
1894+ // safety margin so fee estimation remains an upper bound for many-input
1895+ // Spark mints.
1896+ final nBytesBuffer = 10 + 4 * setCoins.length;
18211897 final nFeeNeeded = BigInt .from (
18221898 estimateTxFee (
18231899 vSize: nBytes + nBytesBuffer,
1824- feeRatePerKB: feesObject.medium ,
1900+ feeRatePerKB: mintFeeRatePerKB ,
18251901 ),
1826- ); // One day we'll do this properly
1902+ );
18271903
18281904 if (nFeeRet >= nFeeNeeded) {
18291905 for (final usedCoin in setCoins) {
@@ -1984,19 +2060,11 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
19842060 addresses: [
19852061 if (addressOrScript is String ) addressOrScript.toString (),
19862062 ],
1987- walletOwns:
1988- (await mainDB.isar.addresses
1989- .where ()
1990- .walletIdEqualTo (walletId)
1991- .filter ()
1992- .valueEqualTo (
1993- addressOrScript is Uint8List
1994- ? output.$3!
1995- : addressOrScript as String ,
1996- )
1997- .valueProperty ()
1998- .findFirst ()) !=
1999- null ,
2063+ walletOwns: walletAddressSet.contains (
2064+ addressOrScript is Uint8List
2065+ ? output.$3!
2066+ : addressOrScript as String ,
2067+ ),
20002068 ),
20012069 );
20022070 }
@@ -2026,6 +2094,18 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
20262094 rethrow ;
20272095 }
20282096 final builtTx = txb.build ();
2097+ final actualFee =
2098+ vin
2099+ .map ((e) => BigInt .from (e.utxo.value))
2100+ .fold (BigInt .zero, (p, e) => p + e) -
2101+ vout.map ((e) => BigInt .from (e.$2)).fold (BigInt .zero, (p, e) => p + e);
2102+ if (actualFee != nFeeRet) {
2103+ Logging .instance.e (
2104+ "Spark mint fee accounting mismatch: "
2105+ "expected=$nFeeRet , actual=$actualFee " ,
2106+ );
2107+ throw Exception ("Spark mint fee accounting mismatch" );
2108+ }
20292109
20302110 // TODO: see todo at top of this function
20312111 assert (outputs.length == 1 );
@@ -2076,11 +2156,14 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
20762156 );
20772157
20782158 Logging .instance.i ("nFeeRet=$nFeeRet , vSize=${data .vSize }" );
2159+ // Sanity check: with the fee rate clamped to at least 1 sat/vbyte, this
2160+ // should only fire if fee accounting or size estimation regresses.
20792161 if (nFeeRet.toInt () < data.vSize! ) {
20802162 Logging .instance.w (
2081- "Spark mint transaction failed: $nFeeRet is less than ${data .vSize }" ,
2163+ "Fee rate below 1 sat/byte minimum relay fee: "
2164+ "fee=$nFeeRet sats, vSize=${data .vSize } bytes" ,
20822165 );
2083- throw Exception ("fee is less than vSize " );
2166+ throw Exception ("Fee rate below 1 sat/byte minimum relay fee " );
20842167 }
20852168
20862169 results.add (data);
@@ -2130,6 +2213,10 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
21302213 throw Exception ("Failed to mint expected amounts" );
21312214 }
21322215
2216+ if (autoMintAll && results.isEmpty) {
2217+ throw Exception ("No Spark mint transactions were created" );
2218+ }
2219+
21332220 return results;
21342221 }
21352222
@@ -2507,6 +2594,11 @@ BigInt _sum(List<UTXO> utxos) => utxos
25072594 .map ((e) => BigInt .from (e.value))
25082595 .fold (BigInt .zero, (previousValue, element) => previousValue + element);
25092596
2597+ typedef _SparkMintSigningKey = ({
2598+ DerivePathType derivePathType,
2599+ coinlib.HDPrivateKey key,
2600+ });
2601+
25102602class MutableSparkRecipient {
25112603 String address;
25122604 BigInt value;
0 commit comments