@@ -1547,38 +1547,54 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
15471547 .map ((e) => MutableSparkRecipient (e.address, e.value, e.memo))
15481548 .toList (); // deep copy
15491549 final feesObject = await fees;
1550+ final minRelayFeeRatePerKB = BigInt .from (1000 );
1551+ final mintFeeRatePerKB = feesObject.medium < minRelayFeeRatePerKB
1552+ ? minRelayFeeRatePerKB
1553+ : feesObject.medium;
15501554 final currentHeight = await chainHeight;
15511555 final random = Random .secure ();
15521556 final List <TxData > results = [];
15531557
1554- // Pre-compute signing keys for all UTXOs to avoid repeated calls to
1555- // getRootHDNode() (which re-derives from mnemonic seed each time) and
1556- // individual DB lookups inside the hot loop.
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.
15571568 final root = await getRootHDNode ();
1558- final Map <String , ({DerivePathType derivePathType, coinlib.HDPrivateKey key})>
1559- signingKeyCache = {};
1560- Future <void > cacheSigningKey (String address) async {
1561- if (signingKeyCache.containsKey (address)) return ;
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+
15621576 final derivePathType = cryptoCurrency.addressType (address: address);
15631577 final dbAddress = await mainDB.getAddress (walletId, address);
1564- if (dbAddress? .derivationPath != null ) {
1565- final key = root.derivePath (dbAddress! .derivationPath! .value);
1566- signingKeyCache[address] = (derivePathType: derivePathType, key: key);
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+ );
15671583 }
1568- }
15691584
1570- for (final utxo in availableUtxos) {
1571- await cacheSigningKey (utxo.address! );
1585+ final key = root.derivePath (dbAddress! .derivationPath! .value);
1586+ final cached = (derivePathType: derivePathType, key: key);
1587+ signingKeyCache[address] = cached;
1588+ return cached;
15721589 }
15731590
1574- // Cache addresses used repeatedly inside the loop.
1575- final sparkAddress = (await getCurrentReceivingSparkAddress ())! .value;
1576- final changeAddress = await getCurrentChangeAddress ();
1577-
1578- // Pre-cache the change address signing key so change UTXOs that get
1579- // recycled back into valueAndUTXOs can be signed without re-deriving.
1580- if (changeAddress != null ) {
1581- await cacheSigningKey (changeAddress.value);
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! ;
15821598 }
15831599
15841600 // Pre-fetch wallet-owned addresses for output ownership checks.
@@ -1649,7 +1665,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
16491665 if (autoMintAll) {
16501666 singleTxOutputs.add (
16511667 MutableSparkRecipient (
1652- sparkAddress ,
1668+ autoMintSparkAddress ! ,
16531669 mintedValue,
16541670 "" ,
16551671 ),
@@ -1687,9 +1703,10 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
16871703
16881704 for (int i = 0 ; i < singleTxOutputs.length; ++ i) {
16891705 if (singleTxOutputs[i].value <= singleFee) {
1690- singleTxOutputs.removeAt (i);
1691- remainder += singleTxOutputs[i] .value - singleFee;
1706+ final removed = singleTxOutputs.removeAt (i);
1707+ remainder += removed .value - singleFee;
16921708 -- i;
1709+ continue ;
16931710 }
16941711 singleTxOutputs[i].value -= singleFee;
16951712 if (remainder > BigInt .zero &&
@@ -1733,13 +1750,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
17331750 BigInt nValueIn = BigInt .zero;
17341751 for (final utxo in itr) {
17351752 if (nValueToSelect > nValueIn) {
1736- final cached = signingKeyCache[utxo.address! ];
1737- if (cached == null ) {
1738- throw Exception (
1739- "Signing key not found for address ${utxo .address }. "
1740- "Local db may be corrupt. Rescan wallet." ,
1741- );
1742- }
1753+ final cached = await getCachedSigningKey (utxo.address! );
17431754 final input = StandardInput (
17441755 utxo,
17451756 derivePathType: cached.derivePathType,
@@ -1767,8 +1778,9 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
17671778 throw Exception ("Change index out of range" );
17681779 }
17691780
1781+ final changeAddress = await getMintChangeAddress ();
17701782 vout.insert (nChangePosInOut, (
1771- changeAddress! .value,
1783+ changeAddress.value,
17721784 nChange.toInt (),
17731785 null ,
17741786 ));
@@ -1863,17 +1875,17 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
18631875 throw Exception ("Transaction too large" );
18641876 }
18651877
1866- // ECDSA DER signature lengths vary by up to ~4 bytes per input
1867- // (r randomly flips the 0x80 bit → 32 vs 33 bytes; s varies similarly
1868- // within low-S bounds). The dummy tx above is signed with real keys
1869- // over different data than the final real tx, so their vSizes differ
1870- // by up to ~4 bytes per input. Scale the safety buffer with input
1871- // count so the estimated fee always covers the final signed tx .
1878+ // ECDSA DER signatures are not fixed-size. Even with low-S
1879+ // normalization, the encoded signature length can vary across
1880+ // signatures, so the dummy signed transaction used for fee estimation
1881+ // can be smaller than the final signed transaction. Use a per-input
1882+ // safety margin so fee estimation remains an upper bound for many- input
1883+ // Spark mints .
18721884 final nBytesBuffer = 10 + 4 * setCoins.length;
18731885 final nFeeNeeded = BigInt .from (
18741886 estimateTxFee (
18751887 vSize: nBytes + nBytesBuffer,
1876- feeRatePerKB: feesObject.medium ,
1888+ feeRatePerKB: mintFeeRatePerKB ,
18771889 ),
18781890 );
18791891
@@ -2120,9 +2132,8 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
21202132 );
21212133
21222134 Logging .instance.i ("nFeeRet=$nFeeRet , vSize=${data .vSize }" );
2123- // fee_sats < vSize_bytes ⟺ feeRate < 1 sat/byte (the standard minimum
2124- // relay fee). Firing here means feesObject.medium came back below that
2125- // threshold, not that the buffer underestimated the real tx size.
2135+ // Sanity check: with the fee rate clamped to at least 1 sat/vbyte, this
2136+ // should only fire if fee accounting or size estimation regresses.
21262137 if (nFeeRet.toInt () < data.vSize! ) {
21272138 Logging .instance.w (
21282139 "Fee rate below 1 sat/byte minimum relay fee: "
@@ -2555,6 +2566,11 @@ BigInt _sum(List<UTXO> utxos) => utxos
25552566 .map ((e) => BigInt .from (e.value))
25562567 .fold (BigInt .zero, (previousValue, element) => previousValue + element);
25572568
2569+ typedef _SparkMintSigningKey = ({
2570+ DerivePathType derivePathType,
2571+ coinlib.HDPrivateKey key,
2572+ });
2573+
25582574class MutableSparkRecipient {
25592575 String address;
25602576 BigInt value;
0 commit comments