From ca52f39cc2166465fa4718c2c21a9ccd53aaffa8 Mon Sep 17 00:00:00 2001 From: Reuben Yap Date: Sat, 4 Jul 2026 00:24:03 +0800 Subject: [PATCH 1/5] Fix Spark Name registration format --- .../spark_interface.dart | 27 ++++++++++++++++++- pubspec.lock | 6 ++--- .../templates/pubspec.template.yaml | 4 +-- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index fcf753fd6a..eaf82f4d25 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -4,6 +4,7 @@ import 'dart:isolate'; import 'dart:math'; import 'package:bitcoindart/bitcoindart.dart' as btc; +import 'package:bitcoindart/src/utils/script.dart' as bscript; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; @@ -50,6 +51,8 @@ const SPARK_OUT_LIMIT_PER_TX = 16; const OP_SPARKMINT = 0xd1; const OP_SPARKSMINT = 0xd2; const OP_SPARKSPEND = 0xd3; +const OP_SPARKNAMEID = 0xe1; +const OP_DROP = 0x75; /// top level function for use with [compute] String _hashTag(String tag) { @@ -61,6 +64,21 @@ String _hashTag(String tag) { return hash; } +Uint8List _sparkNameFeeScript({ + required Uint8List baseScript, + required String name, + required String sparkAddress, +}) => Uint8List.fromList([ + ...baseScript, + ...bscript.compile([ + OP_SPARKNAMEID, + Uint8List.fromList(utf8.encode(name)), + OP_DROP, + Uint8List.fromList(utf8.encode(sparkAddress)), + OP_DROP, + ]), +]); + void initSparkLogging(Level level) => libSpark.initSparkLogging(level); abstract class _SparkIsolate { @@ -708,10 +726,17 @@ mixin SparkInterface ), ); - final scriptPubKey = btc.Address.addressToOutputScript( + var scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, _bitcoinDartNetwork, ); + if (txData.sparkNameInfo != null) { + scriptPubKey = _sparkNameFeeScript( + baseScript: scriptPubKey, + name: txData.sparkNameInfo!.name, + sparkAddress: txData.sparkNameInfo!.sparkAddress.value, + ); + } txb.addOutput( scriptPubKey, recipientsWithFeeSubtracted[i].amount.raw.toInt(), diff --git a/pubspec.lock b/pubspec.lock index 091d58a6f0..1baec63d48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1028,9 +1028,9 @@ packages: dependency: "direct main" description: path: "." - ref: "4bd84c88e1b2a817a2604ec53030634cc3304bc7" - resolved-ref: "4bd84c88e1b2a817a2604ec53030634cc3304bc7" - url: "https://github.com/cypherstack/flutter_libsparkmobile.git" + ref: "171bc186663e3c7a573a6240f28f430e8d6b7d50" + resolved-ref: "171bc186663e3c7a573a6240f28f430e8d6b7d50" + url: "https://github.com/firoorg/flutter_libsparkmobile.git" source: git version: "0.1.0" flutter_lints: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 6764428fac..7ebc0b24c0 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -43,8 +43,8 @@ dependencies: # %%ENABLE_FIRO%% # flutter_libsparkmobile: # git: -# url: https://github.com/cypherstack/flutter_libsparkmobile.git -# ref: 4bd84c88e1b2a817a2604ec53030634cc3304bc7 +# url: https://github.com/firoorg/flutter_libsparkmobile.git +# ref: 171bc186663e3c7a573a6240f28f430e8d6b7d50 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% From 4f3534de2ce7a1b907da2b2a465804d4b7a32465 Mon Sep 17 00:00:00 2001 From: Reuben Yap Date: Thu, 16 Jul 2026 19:06:54 +0800 Subject: [PATCH 2/5] Fix Spark Name registration fee sizing --- .../spark_interface.dart | 21 ++++++++--- test/wallets/spark_name_fee_test.dart | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 test/wallets/spark_name_fee_test.dart diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index eaf82f4d25..bf3e62700a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -64,7 +64,8 @@ String _hashTag(String tag) { return hash; } -Uint8List _sparkNameFeeScript({ +@visibleForTesting +Uint8List sparkNameFeeScript({ required Uint8List baseScript, required String name, required String sparkAddress, @@ -79,6 +80,12 @@ Uint8List _sparkNameFeeScript({ ]), ]); +@visibleForTesting +bool shouldSubtractSparkFeeFromAmount({ + required bool isSparkNameRegistration, + required bool spendsAll, +}) => !isSparkNameRegistration && spendsAll; + void initSparkLogging(Level level) => libSpark.initSparkLogging(level); abstract class _SparkIsolate { @@ -586,7 +593,10 @@ mixin SparkInterface throw Exception("Insufficient Spark balance"); } - final bool isSendAll = available == txAmount; + final bool isSendAll = shouldSubtractSparkFeeFromAmount( + isSparkNameRegistration: txData.sparkNameInfo != null, + spendsAll: available == txAmount, + ); // prepare coin data for ffi final serializedCoins = coins @@ -711,6 +721,7 @@ mixin SparkInterface final List tempInputs = []; final List tempOutputs = []; + var sparkNameFeeScriptSizeDelta = 0; for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; @@ -731,11 +742,13 @@ mixin SparkInterface _bitcoinDartNetwork, ); if (txData.sparkNameInfo != null) { - scriptPubKey = _sparkNameFeeScript( + final baseScript = scriptPubKey; + scriptPubKey = sparkNameFeeScript( baseScript: scriptPubKey, name: txData.sparkNameInfo!.name, sparkAddress: txData.sparkNameInfo!.sparkAddress.value, ); + sparkNameFeeScriptSizeDelta += scriptPubKey.length - baseScript.length; } txb.addOutput( scriptPubKey, @@ -841,7 +854,7 @@ mixin SparkInterface txHash: extractedTx.getHash(), additionalTxSize: txData.sparkNameInfo == null ? 0 - : noProofNameTxData!.size, + : noProofNameTxData!.size + sparkNameFeeScriptSizeDelta, )); for (final outputScript in spend.outputScripts) { diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart new file mode 100644 index 0000000000..442dd3d19f --- /dev/null +++ b/test/wallets/spark_name_fee_test.dart @@ -0,0 +1,35 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; + +void main() { + test('Spark Name fee size includes the tagged output bytes', () { + final baseScript = Uint8List(25); + final feeScript = sparkNameFeeScript( + baseScript: baseScript, + name: 'alice', + sparkAddress: List.filled(144, 'a').join(), + ); + + expect(feeScript.length - baseScript.length, 155); + expect(feeScript.length, 180); + }); + + test('Spark Name payments never have the miner fee subtracted', () { + expect( + shouldSubtractSparkFeeFromAmount( + isSparkNameRegistration: true, + spendsAll: true, + ), + isFalse, + ); + expect( + shouldSubtractSparkFeeFromAmount( + isSparkNameRegistration: false, + spendsAll: true, + ), + isTrue, + ); + }); +} From f2d474d967653ddceec77c2ccf63a80ba242aad8 Mon Sep 17 00:00:00 2001 From: Reuben Yap Date: Thu, 16 Jul 2026 20:38:35 +0800 Subject: [PATCH 3/5] Reject invalid Spark Name characters --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 5 +---- pubspec.lock | 4 ++-- scripts/app_config/templates/pubspec.template.yaml | 2 +- test/wallets/spark_name_fee_test.dart | 6 ++++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index bf3e62700a..fd5236260d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -721,7 +721,6 @@ mixin SparkInterface final List tempInputs = []; final List tempOutputs = []; - var sparkNameFeeScriptSizeDelta = 0; for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; @@ -742,13 +741,11 @@ mixin SparkInterface _bitcoinDartNetwork, ); if (txData.sparkNameInfo != null) { - final baseScript = scriptPubKey; scriptPubKey = sparkNameFeeScript( baseScript: scriptPubKey, name: txData.sparkNameInfo!.name, sparkAddress: txData.sparkNameInfo!.sparkAddress.value, ); - sparkNameFeeScriptSizeDelta += scriptPubKey.length - baseScript.length; } txb.addOutput( scriptPubKey, @@ -854,7 +851,7 @@ mixin SparkInterface txHash: extractedTx.getHash(), additionalTxSize: txData.sparkNameInfo == null ? 0 - : noProofNameTxData!.size + sparkNameFeeScriptSizeDelta, + : noProofNameTxData!.size, )); for (final outputScript in spend.outputScripts) { diff --git a/pubspec.lock b/pubspec.lock index 1baec63d48..75185759f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1028,8 +1028,8 @@ packages: dependency: "direct main" description: path: "." - ref: "171bc186663e3c7a573a6240f28f430e8d6b7d50" - resolved-ref: "171bc186663e3c7a573a6240f28f430e8d6b7d50" + ref: "53db5a06a7b7f3df68fe6263f1453f77513bec06" + resolved-ref: "53db5a06a7b7f3df68fe6263f1453f77513bec06" url: "https://github.com/firoorg/flutter_libsparkmobile.git" source: git version: "0.1.0" diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 7ebc0b24c0..ead1294072 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -44,7 +44,7 @@ dependencies: # flutter_libsparkmobile: # git: # url: https://github.com/firoorg/flutter_libsparkmobile.git -# ref: 171bc186663e3c7a573a6240f28f430e8d6b7d50 +# ref: 53db5a06a7b7f3df68fe6263f1453f77513bec06 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart index 442dd3d19f..745ceff165 100644 --- a/test/wallets/spark_name_fee_test.dart +++ b/test/wallets/spark_name_fee_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; void main() { - test('Spark Name fee size includes the tagged output bytes', () { + test('Spark Name fee output includes the name and address tag', () { final baseScript = Uint8List(25); final feeScript = sparkNameFeeScript( baseScript: baseScript, @@ -12,8 +12,10 @@ void main() { sparkAddress: List.filled(144, 'a').join(), ); - expect(feeScript.length - baseScript.length, 155); expect(feeScript.length, 180); + expect(feeScript[25], OP_SPARKNAMEID); + expect(feeScript[32], OP_DROP); + expect(feeScript.last, OP_DROP); }); test('Spark Name payments never have the miner fee subtracted', () { From 2bff96e56ba6a2529dbb909b06ecd76239fe832a Mon Sep 17 00:00:00 2001 From: Reuben Yap Date: Thu, 16 Jul 2026 20:40:41 +0800 Subject: [PATCH 4/5] Test Spark Name character validation --- test/wallets/spark_name_fee_test.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart index 745ceff165..a15ed3fdd1 100644 --- a/test/wallets/spark_name_fee_test.dart +++ b/test/wallets/spark_name_fee_test.dart @@ -1,9 +1,16 @@ import 'dart:typed_data'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; void main() { + test('Spark Name validation rejects underscores before construction', () { + final pattern = RegExp(kNameRegexString); + expect(pattern.hasMatch('NAME-FOR.TESTING'), isTrue); + expect(pattern.hasMatch('NAME_FOR_TESTING'), isFalse); + }); + test('Spark Name fee output includes the name and address tag', () { final baseScript = Uint8List(25); final feeScript = sparkNameFeeScript( From 1579f8ee1eb11f59d155c25f938a145e39c18d25 Mon Sep 17 00:00:00 2001 From: Reuben Yap Date: Thu, 16 Jul 2026 21:10:56 +0800 Subject: [PATCH 5/5] Restore Spark Name fee script sizing --- .../wallet/wallet_mixin_interfaces/spark_interface.dart | 5 ++++- test/wallets/spark_name_fee_test.dart | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index fd5236260d..bf3e62700a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -721,6 +721,7 @@ mixin SparkInterface final List tempInputs = []; final List tempOutputs = []; + var sparkNameFeeScriptSizeDelta = 0; for (int i = 0; i < (txData.recipients?.length ?? 0); i++) { if (txData.recipients![i].amount.raw == BigInt.zero) { continue; @@ -741,11 +742,13 @@ mixin SparkInterface _bitcoinDartNetwork, ); if (txData.sparkNameInfo != null) { + final baseScript = scriptPubKey; scriptPubKey = sparkNameFeeScript( baseScript: scriptPubKey, name: txData.sparkNameInfo!.name, sparkAddress: txData.sparkNameInfo!.sparkAddress.value, ); + sparkNameFeeScriptSizeDelta += scriptPubKey.length - baseScript.length; } txb.addOutput( scriptPubKey, @@ -851,7 +854,7 @@ mixin SparkInterface txHash: extractedTx.getHash(), additionalTxSize: txData.sparkNameInfo == null ? 0 - : noProofNameTxData!.size, + : noProofNameTxData!.size + sparkNameFeeScriptSizeDelta, )); for (final outputScript in spend.outputScripts) { diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart index a15ed3fdd1..0608fa9274 100644 --- a/test/wallets/spark_name_fee_test.dart +++ b/test/wallets/spark_name_fee_test.dart @@ -19,6 +19,7 @@ void main() { sparkAddress: List.filled(144, 'a').join(), ); + expect(feeScript.length - baseScript.length, 155); expect(feeScript.length, 180); expect(feeScript[25], OP_SPARKNAMEID); expect(feeScript[32], OP_DROP);