diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index fcf753fd6a..bf3e62700a 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,28 @@ String _hashTag(String tag) { return hash; } +@visibleForTesting +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, + ]), +]); + +@visibleForTesting +bool shouldSubtractSparkFeeFromAmount({ + required bool isSparkNameRegistration, + required bool spendsAll, +}) => !isSparkNameRegistration && spendsAll; + void initSparkLogging(Level level) => libSpark.initSparkLogging(level); abstract class _SparkIsolate { @@ -568,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 @@ -693,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; @@ -708,10 +737,19 @@ mixin SparkInterface ), ); - final scriptPubKey = btc.Address.addressToOutputScript( + var scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, _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, recipientsWithFeeSubtracted[i].amount.raw.toInt(), @@ -816,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/pubspec.lock b/pubspec.lock index 091d58a6f0..75185759f2 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: "53db5a06a7b7f3df68fe6263f1453f77513bec06" + resolved-ref: "53db5a06a7b7f3df68fe6263f1453f77513bec06" + 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..ead1294072 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: 53db5a06a7b7f3df68fe6263f1453f77513bec06 # %%END_ENABLE_FIRO%% # %%ENABLE_EPIC%% diff --git a/test/wallets/spark_name_fee_test.dart b/test/wallets/spark_name_fee_test.dart new file mode 100644 index 0000000000..0608fa9274 --- /dev/null +++ b/test/wallets/spark_name_fee_test.dart @@ -0,0 +1,45 @@ +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( + baseScript: baseScript, + name: 'alice', + 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', () { + expect( + shouldSubtractSparkFeeFromAmount( + isSparkNameRegistration: true, + spendsAll: true, + ), + isFalse, + ); + expect( + shouldSubtractSparkFeeFromAmount( + isSparkNameRegistration: false, + spendsAll: true, + ), + isTrue, + ); + }); +}