mirror of
https://github.com/MAGICGrants/flutter_libsparkmobile.git
synced 2026-01-09 21:17:56 -05:00
WIP spark spend ffi binding
This commit is contained in:
@@ -223,18 +223,140 @@ abstract final class LibSpark {
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempt to create and sign a spark spend transaction.
|
||||
/// Attempt to create a spark spend.
|
||||
///
|
||||
/// Returns the raw transaction hex if successful, otherwise null.
|
||||
/// Returns the serialized spark spend.
|
||||
///
|
||||
static String? createSparkSendTransaction({
|
||||
static ({
|
||||
String serializedSpendPayload,
|
||||
List<Uint8List> outputScripts,
|
||||
int fee,
|
||||
}) createSparkSendTransaction({
|
||||
required String privateKeyHex,
|
||||
// TODO what do we need?
|
||||
int index = 1,
|
||||
required List<({String address, int amount, bool subtractFeeFromAmount})>
|
||||
recipients,
|
||||
required List<
|
||||
({
|
||||
String sparkAddress,
|
||||
int amount,
|
||||
bool subtractFeeFromAmount,
|
||||
String memo
|
||||
})>
|
||||
privateRecipients,
|
||||
required List<Uint8List> serializedMintMetas,
|
||||
required List<
|
||||
({
|
||||
int setId,
|
||||
String setHash,
|
||||
List<({String serializedCoin, String txHash})> set
|
||||
})>
|
||||
allAnonymitySets,
|
||||
}) {
|
||||
// TODO allocate/create data structures required by the generated bindings
|
||||
final privateKeyPtr =
|
||||
privateKeyHex.to32BytesFromHex().unsignedCharPointer();
|
||||
|
||||
// some kind of failure
|
||||
return null;
|
||||
final recipientsPtr =
|
||||
malloc.allocate<CRecip>(sizeOf<CRecip>() * recipients.length);
|
||||
for (int i = 0; i < recipients.length; i++) {
|
||||
recipientsPtr[i].amount = recipients[i].amount;
|
||||
recipientsPtr[i].subtractFee =
|
||||
recipients[i].subtractFeeFromAmount ? 1 : 0;
|
||||
}
|
||||
|
||||
final privateRecipientsPtr = malloc.allocate<COutputRecipient>(
|
||||
sizeOf<COutputRecipient>() * recipients.length);
|
||||
for (int i = 0; i < recipients.length; i++) {
|
||||
privateRecipientsPtr[i].subtractFee =
|
||||
recipients[i].subtractFeeFromAmount ? 1 : 0;
|
||||
|
||||
privateRecipientsPtr[i].output =
|
||||
malloc.allocate<COutputCoinData>(sizeOf<COutputCoinData>());
|
||||
privateRecipientsPtr[i].output.ref.value = privateRecipients[i].amount;
|
||||
privateRecipientsPtr[i].output.ref.memo =
|
||||
privateRecipients[i].memo.toNativeUtf8().cast<Char>();
|
||||
privateRecipientsPtr[i].output.ref.address =
|
||||
privateRecipients[i].sparkAddress.toNativeUtf8().cast<Char>();
|
||||
}
|
||||
|
||||
final serializedMintMetasPtr = malloc.allocate<CCDataStream>(
|
||||
sizeOf<CCDataStream>() * serializedMintMetas.length);
|
||||
for (int i = 0; i < serializedMintMetas.length; i++) {
|
||||
serializedMintMetasPtr[i].data =
|
||||
serializedMintMetas[i].unsignedCharPointer();
|
||||
serializedMintMetasPtr[i].length = serializedMintMetas[i].length;
|
||||
}
|
||||
|
||||
final coverSetDataAllPtr = malloc.allocate<CCoverSetData>(
|
||||
sizeOf<CCoverSetData>() * allAnonymitySets.length);
|
||||
for (int i = 0; i < allAnonymitySets.length; i++) {
|
||||
coverSetDataAllPtr[i].setId = allAnonymitySets[i].setId;
|
||||
|
||||
coverSetDataAllPtr[i].cover_set = malloc.allocate<CCDataStream>(
|
||||
sizeOf<CCDataStream>() * allAnonymitySets[i].set.length);
|
||||
coverSetDataAllPtr[i].cover_setLength = allAnonymitySets[i].set.length;
|
||||
|
||||
for (int j = 0; j < allAnonymitySets[i].set.length; j++) {
|
||||
final b64CoinDecoded =
|
||||
base64Decode(allAnonymitySets[i].set[j].serializedCoin);
|
||||
coverSetDataAllPtr[i].cover_set[j].length = b64CoinDecoded.length;
|
||||
coverSetDataAllPtr[i].cover_set[j].data =
|
||||
b64CoinDecoded.unsignedCharPointer();
|
||||
}
|
||||
|
||||
final setHash = base64Decode(allAnonymitySets[i].setHash);
|
||||
coverSetDataAllPtr[i].cover_set_representation =
|
||||
setHash.unsignedCharPointer();
|
||||
coverSetDataAllPtr[i].cover_set_representationLength = setHash.length;
|
||||
}
|
||||
|
||||
final result = _bindings.cCreateSparkSpendTransaction(
|
||||
privateKeyPtr,
|
||||
index,
|
||||
recipientsPtr,
|
||||
recipients.length,
|
||||
privateRecipientsPtr,
|
||||
privateRecipients.length,
|
||||
serializedMintMetasPtr,
|
||||
serializedMintMetas.length,
|
||||
coverSetDataAllPtr,
|
||||
allAnonymitySets.length,
|
||||
);
|
||||
|
||||
// todo: more comprehensive frees
|
||||
malloc.free(privateKeyPtr);
|
||||
malloc.free(recipientsPtr);
|
||||
malloc.free(privateRecipientsPtr);
|
||||
malloc.free(serializedMintMetasPtr);
|
||||
malloc.free(coverSetDataAllPtr);
|
||||
|
||||
if (result.address == nullptr.address) {
|
||||
throw Exception(
|
||||
"createSparkSendTransaction() failed for an unknown reason",
|
||||
);
|
||||
}
|
||||
|
||||
final messageBytes = result.ref.data.toUint8List(result.ref.dataLength);
|
||||
final message = utf8.decode(messageBytes);
|
||||
malloc.free(result.ref.data);
|
||||
|
||||
if (result.ref.isError > 0) {
|
||||
throw Exception(message);
|
||||
}
|
||||
|
||||
final fee = result.ref.fee;
|
||||
|
||||
final List<Uint8List> scripts = [];
|
||||
for (int i = 0; i < result.ref.outputScriptsLength; i++) {
|
||||
final script = result.ref.outputScripts[i].bytes
|
||||
.toUint8List(result.ref.outputScripts[i].length);
|
||||
malloc.free(result.ref.outputScripts[i].bytes);
|
||||
scripts.add(script);
|
||||
}
|
||||
|
||||
malloc.free(result.ref.outputScripts);
|
||||
|
||||
return (serializedSpendPayload: message, fee: fee, outputScripts: scripts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,22 +136,17 @@ class FlutterLibsparkmobileBindings {
|
||||
/// FFI-friendly wrapper for spark::createSparkSpendTransaction.
|
||||
///
|
||||
/// createSparkSpendTransaction: https://github.com/firoorg/sparkmobile/blob/23099b0d9010a970ad75b9cfe05d568d634088f3/src/spark.cpp#L190
|
||||
ffi.Pointer<ffi.UnsignedChar> cCreateSparkSpendTransaction(
|
||||
ffi.Pointer<SparkSpendTransactionResult> cCreateSparkSpendTransaction(
|
||||
ffi.Pointer<ffi.UnsignedChar> keyData,
|
||||
int index,
|
||||
ffi.Pointer<CRecip> recipients,
|
||||
int recipientsLength,
|
||||
ffi.Pointer<COutputRecipient> privateRecipients,
|
||||
int privateRecipientsLength,
|
||||
ffi.Pointer<CCSparkMintMeta> coins,
|
||||
int coinsLength,
|
||||
ffi.Pointer<CCoverSets> cover_set_data_all,
|
||||
ffi.Pointer<CCDataStream> serializedMintMetas,
|
||||
int serializedMintMetasLength,
|
||||
ffi.Pointer<CCoverSetData> cover_set_data_all,
|
||||
int cover_set_data_allLength,
|
||||
ffi.Pointer<ffi.Char> txHashSig,
|
||||
int txHashSigLength,
|
||||
int fee,
|
||||
ffi.Pointer<OutputScript> outputScripts,
|
||||
int outputScriptsLength,
|
||||
) {
|
||||
return _cCreateSparkSpendTransaction(
|
||||
keyData,
|
||||
@@ -160,53 +155,38 @@ class FlutterLibsparkmobileBindings {
|
||||
recipientsLength,
|
||||
privateRecipients,
|
||||
privateRecipientsLength,
|
||||
coins,
|
||||
coinsLength,
|
||||
serializedMintMetas,
|
||||
serializedMintMetasLength,
|
||||
cover_set_data_all,
|
||||
cover_set_data_allLength,
|
||||
txHashSig,
|
||||
txHashSigLength,
|
||||
fee,
|
||||
outputScripts,
|
||||
outputScriptsLength,
|
||||
);
|
||||
}
|
||||
|
||||
late final _cCreateSparkSpendTransactionPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.UnsignedChar> Function(
|
||||
ffi.Pointer<SparkSpendTransactionResult> Function(
|
||||
ffi.Pointer<ffi.UnsignedChar>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<CRecip>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<COutputRecipient>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<CCSparkMintMeta>,
|
||||
ffi.Pointer<CCDataStream>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<CCoverSets>,
|
||||
ffi.Int,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Int,
|
||||
ffi.Uint64,
|
||||
ffi.Pointer<OutputScript>,
|
||||
ffi.Pointer<CCoverSetData>,
|
||||
ffi.Int)>>('cCreateSparkSpendTransaction');
|
||||
late final _cCreateSparkSpendTransaction =
|
||||
_cCreateSparkSpendTransactionPtr.asFunction<
|
||||
ffi.Pointer<ffi.UnsignedChar> Function(
|
||||
ffi.Pointer<SparkSpendTransactionResult> Function(
|
||||
ffi.Pointer<ffi.UnsignedChar>,
|
||||
int,
|
||||
ffi.Pointer<CRecip>,
|
||||
int,
|
||||
ffi.Pointer<COutputRecipient>,
|
||||
int,
|
||||
ffi.Pointer<CCSparkMintMeta>,
|
||||
ffi.Pointer<CCDataStream>,
|
||||
int,
|
||||
ffi.Pointer<CCoverSets>,
|
||||
int,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
int,
|
||||
int,
|
||||
ffi.Pointer<OutputScript>,
|
||||
ffi.Pointer<CCoverSetData>,
|
||||
int)>();
|
||||
}
|
||||
|
||||
@@ -337,75 +317,25 @@ final class COutputCoinData extends ffi.Struct {
|
||||
///
|
||||
/// See https://github.com/firoorg/sparkmobile/blob/23099b0d9010a970ad75b9cfe05d568d634088f3/src/spark.cpp#L195
|
||||
final class COutputRecipient extends ffi.Struct {
|
||||
external COutputCoinData output;
|
||||
external ffi.Pointer<COutputCoinData> output;
|
||||
|
||||
@ffi.Int()
|
||||
external int subtractFee;
|
||||
}
|
||||
|
||||
final class CCDataStream extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Char> data;
|
||||
external ffi.Pointer<ffi.UnsignedChar> data;
|
||||
|
||||
@ffi.Int()
|
||||
external int length;
|
||||
}
|
||||
|
||||
/// FFI-friendly wrapper for a spark::CSparkMintMeta.
|
||||
///
|
||||
/// CSparkMintMeta: https://github.com/firoorg/sparkmobile/blob/8bf17cd3deba6c3b0d10e89282e02936d7e71cdd/src/primitives.h#L9
|
||||
final class CCSparkMintMeta extends ffi.Struct {
|
||||
@ffi.Uint64()
|
||||
external int height;
|
||||
|
||||
external ffi.Pointer<ffi.Char> id;
|
||||
|
||||
@ffi.Int()
|
||||
external int isUsed;
|
||||
|
||||
external ffi.Pointer<ffi.Char> txid;
|
||||
|
||||
/// Diversifier.
|
||||
@ffi.Uint64()
|
||||
external int i;
|
||||
|
||||
/// Encrypted diversifier.
|
||||
external ffi.Pointer<ffi.UnsignedChar> d;
|
||||
|
||||
@ffi.Int()
|
||||
external int dLength;
|
||||
|
||||
/// Value.
|
||||
@ffi.Uint64()
|
||||
external int v;
|
||||
|
||||
/// Nonce.
|
||||
external ffi.Pointer<ffi.UnsignedChar> k;
|
||||
|
||||
@ffi.Int()
|
||||
external int kLength;
|
||||
|
||||
external ffi.Pointer<ffi.Char> memo;
|
||||
|
||||
@ffi.Int()
|
||||
external int memoLength;
|
||||
|
||||
external ffi.Pointer<ffi.UnsignedChar> serial_context;
|
||||
|
||||
@ffi.Int()
|
||||
external int serial_contextLength;
|
||||
|
||||
@ffi.Char()
|
||||
external int type;
|
||||
|
||||
external CCDataStream coin;
|
||||
}
|
||||
|
||||
/// FFI-friendly wrapper for a spark::CoverSetData.
|
||||
///
|
||||
/// CoverSetData: https://github.com/firoorg/sparkmobile/blob/8bf17cd3deba6c3b0d10e89282e02936d7e71cdd/src/spend_transaction.h#L28
|
||||
final class CCoverSetData extends ffi.Struct {
|
||||
/// vs. struct CCoin* cover_set;
|
||||
external ffi.Pointer<ffi.Pointer<CCDataStream>> cover_set;
|
||||
external ffi.Pointer<CCDataStream> cover_set;
|
||||
|
||||
@ffi.Int()
|
||||
external int cover_setLength;
|
||||
@@ -414,16 +344,9 @@ final class CCoverSetData extends ffi.Struct {
|
||||
|
||||
@ffi.Int()
|
||||
external int cover_set_representationLength;
|
||||
}
|
||||
|
||||
/// FFI-friendly wrapper for a std::unordered_map<uint64_t, spark::CoverSetData>.
|
||||
///
|
||||
/// See https://github.com/firoorg/sparkmobile/blob/23099b0d9010a970ad75b9cfe05d568d634088f3/src/spark.cpp#L197
|
||||
final class CCoverSets extends ffi.Struct {
|
||||
external ffi.Pointer<CCoverSetData> cover_sets;
|
||||
|
||||
@ffi.Int()
|
||||
external int cover_setsLength;
|
||||
external int setId;
|
||||
}
|
||||
|
||||
final class OutputScript extends ffi.Struct {
|
||||
@@ -465,3 +388,24 @@ final class AggregateCoinData extends ffi.Struct {
|
||||
@ffi.Int()
|
||||
external int nonceLength;
|
||||
}
|
||||
|
||||
/// Aggregate data structure to handle passing spark spend data across FFI.
|
||||
///
|
||||
/// Contains the serialized transaction or the error message if isError is true.
|
||||
final class SparkSpendTransactionResult extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.UnsignedChar> data;
|
||||
|
||||
@ffi.Int()
|
||||
external int dataLength;
|
||||
|
||||
external ffi.Pointer<OutputScript> outputScripts;
|
||||
|
||||
@ffi.Int()
|
||||
external int outputScriptsLength;
|
||||
|
||||
@ffi.Int()
|
||||
external int fee;
|
||||
|
||||
@ffi.Int()
|
||||
external int isError;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user