Skip to content

Commit 653d11a

Browse files
committed
Address Spark mint review feedback
1 parent 51d4357 commit 653d11a

1 file changed

Lines changed: 58 additions & 42 deletions

File tree

lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
25582574
class MutableSparkRecipient {
25592575
String address;
25602576
BigInt value;

0 commit comments

Comments
 (0)