WIP spark spend ffi binding

This commit is contained in:
julian
2023-12-08 12:18:48 -06:00
parent 1c7903a859
commit e2765a2fca
7 changed files with 449 additions and 418 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}