Merge pull request #710 from cypherstack/spark_refactor

Spark mint
This commit is contained in:
julian-CStack 2023-12-14 08:07:35 -06:00 committed by GitHub
commit 53bdf729cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 274 additions and 15 deletions

View file

@ -23,8 +23,11 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart';
@ -552,6 +555,115 @@ class HiddenSettings extends StatelessWidget {
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
try {
// Run refreshSparkData.
//
// Search wallets for a Firo testnet wallet.
for (final wallet
in ref.read(pWallets).wallets) {
if (!(wallet.info.coin ==
Coin.firoTestNet)) {
continue;
}
// This is a Firo testnet wallet.
final walletId = wallet.info.walletId;
// // Search for `circle chunk...` mnemonic.
// final potentialWallet =
// ref.read(pWallets).getWallet(walletId)
// as MnemonicInterface;
// final mnemonic = await potentialWallet
// .getMnemonicAsWords();
// if (!(mnemonic[0] == "circle" &&
// mnemonic[1] == "chunk)")) {
// // That ain't it. Skip this one.
// return;
// }
// Hardcode key in refreshSparkData instead.
// Get a Spark interface.
final fusionWallet = ref
.read(pWallets)
.getWallet(walletId) as SparkInterface;
// Refresh Spark data.
await fusionWallet.refreshSparkData();
// We only need to run this once.
break;
}
} catch (e, s) {
print("$e\n$s");
}
},
child: RoundedWhiteContainer(
child: Text(
"Refresh Spark wallet",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
try {
// Run prepareSparkMintTransaction.
for (final wallet
in ref.read(pWallets).wallets) {
// Prepare tx with a Firo testnet wallet.
if (!(wallet.info.coin ==
Coin.firoTestNet)) {
continue;
}
final walletId = wallet.info.walletId;
// Get a Spark interface.
final fusionWallet = ref
.read(pWallets)
.getWallet(walletId) as SparkInterface;
// Make a dummy TxData.
TxData txData = TxData(); // TODO
await fusionWallet
.prepareSparkMintTransaction(
txData: txData);
// We only need to run this once.
break;
}
} catch (e, s) {
print("$e\n$s");
}
},
child: RoundedWhiteContainer(
child: Text(
"Prepare Spark mint transaction",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
},
),
// const SizedBox(
// height: 12,
// ),

View file

@ -562,26 +562,144 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
}
}
/// Transparent to Spark (mint) transaction creation
/// Transparent to Spark (mint) transaction creation.
///
/// See https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o
Future<TxData> prepareSparkMintTransaction({required TxData txData}) async {
// https://docs.google.com/document/d/1RG52GoYTZDvKlZz_3G4sQu-PpT6JWSZGHLNswWcrE3o/edit
// "this kind of transaction is generated like a regular transaction, but in
// place of [regular] outputs we put spark outputs... we construct the input
// part of the transaction first then we generate spark related data [and]
// we sign like regular transactions at the end."
// this kind of transaction is generated like a regular transaction, but in
// place of regulart outputs we put spark outputs, so for that we call
// createSparkMintRecipients function, we get spark related data,
// everything else we do like for regular transaction, and we put CRecipient
// object as a tx outputs, we need to keep the order..
// First we pass spark::MintedCoinData>, has following members, Address
// which is any spark address, amount (v) how much we want to send, and
// memo which can be any string with 32 length (any string we want to send
// to receiver), serial_context is a byte array, which should be unique for
// each transaction, and for that we serialize and put all inputs into
// serial_context vector. So we construct the input part of the transaction
// first then we generate spark related data. And we sign like regular
// transactions at the end.
// Validate inputs.
// There should be at least one input.
if (txData.utxos == null || txData.utxos!.isEmpty) {
throw Exception("No inputs provided.");
}
// For now let's limit to one input.
if (txData.utxos!.length > 1) {
throw Exception("Only one input supported.");
// TODO remove and test with multiple inputs.
}
// Validate individual inputs.
for (final utxo in txData.utxos!) {
// Input amount must be greater than zero.
if (utxo.value == 0) {
throw Exception("Input value cannot be zero.");
}
// Input value must be greater than dust limit.
if (BigInt.from(utxo.value) < cryptoCurrency.dustLimit.raw) {
throw Exception("Input value below dust limit.");
}
}
// Validate outputs.
// There should be at least one output.
if (txData.recipients == null || txData.recipients!.isEmpty) {
throw Exception("No recipients provided.");
}
// For now let's limit to one output.
if (txData.recipients!.length > 1) {
throw Exception("Only one recipient supported.");
// TODO remove and test with multiple recipients.
}
// Limit outputs per tx to 16.
//
// See SPARK_OUT_LIMIT_PER_TX at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L16
if (txData.recipients!.length > 16) {
throw Exception("Too many recipients.");
}
// Limit spend value per tx to 1000000000000 satoshis.
//
// See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17
// and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17
// Note that as MAX_MONEY is greater than this limit, we can ignore it. See https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L31
//
// This will be added to and checked as we validate outputs.
Amount totalAmount = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
// Validate individual outputs.
for (final recipient in txData.recipients!) {
// Output amount must be greater than zero.
if (recipient.amount.raw == BigInt.zero) {
throw Exception("Output amount cannot be zero.");
// Could refactor this for loop to use an index and remove this output.
}
// Output amount must be greater than dust limit.
if (recipient.amount < cryptoCurrency.dustLimit) {
throw Exception("Output below dust limit.");
}
// Do not add outputs that would exceed the spend limit.
totalAmount += recipient.amount;
if (totalAmount.raw > BigInt.from(1000000000000)) {
throw Exception(
"Spend limit exceeded (10,000 FIRO per tx).",
);
}
}
// Create a transaction builder and set locktime and version.
final txb = btc.TransactionBuilder(
network: btc.NetworkType(
messagePrefix: cryptoCurrency.networkParams.messagePrefix,
bech32: cryptoCurrency.networkParams.bech32Hrp,
bip32: btc.Bip32Type(
public: cryptoCurrency.networkParams.pubHDPrefix,
private: cryptoCurrency.networkParams.privHDPrefix,
),
pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix,
scriptHash: cryptoCurrency.networkParams.p2shPrefix,
wif: cryptoCurrency.networkParams.wifPrefix,
),
);
txb.setLockTime(await chainHeight);
txb.setVersion(3 | (9 << 16));
// Create a mint script.
final mintScript = bscript.compile([
0xd1, // OP_SPARKMINT.
Uint8List(0),
]);
// Add inputs.
for (final utxo in txData.utxos!) {
txb.addInput(
utxo.txid,
utxo.vout,
0xffffffff,
mintScript,
);
}
// Create the serial context.
//
// "...serial_context is a byte array, which should be unique for each
// transaction, and for that we serialize and put all inputs into
// serial_context vector."
List<int> serialContext = [];
for (final utxo in txData.utxos!) {
serialContext.addAll(
bscript.compile([
utxo.txid,
utxo.vout,
]),
);
}
// Create mint recipients.
final mintRecipients = LibSpark.createSparkMintRecipients(
outputs: txData.recipients!
.map((e) => (
@ -591,10 +709,39 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface {
))
.toList(),
serialContext: Uint8List.fromList(serialContext),
// generate: true // TODO is this needed?
);
// Add mint output(s).
for (final mint in mintRecipients) {
txb.addOutput(
mint.scriptPubKey,
mint.amount,
);
}
// TODO Sign the transaction.
throw UnimplementedError();
}
/// Broadcast a tx and TODO update Spark balance.
Future<TxData> confirmSparkMintTransaction({required TxData txData}) async {
// Broadcast tx.
final txid = await electrumXClient.broadcastTransaction(
rawTx: txData.raw!,
);
// Check txid.
assert(txid == txData.txid!);
// TODO update spark balance.
return txData.copyWith(
txid: txid,
);
}
@override
Future<void> updateBalance() async {
// call to super to update transparent balance (and lelantus balance if