From fb64c00d9cfb31972b996c21353aed47a224c425 Mon Sep 17 00:00:00 2001 From: Jonald Fyookball Date: Sat, 26 Aug 2023 19:19:25 -0400 Subject: [PATCH] Add cashtokens.dart for initial class implementation. --- .../coins/bitcoincash/cashtokens.dart | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 lib/services/coins/bitcoincash/cashtokens.dart diff --git a/lib/services/coins/bitcoincash/cashtokens.dart b/lib/services/coins/bitcoincash/cashtokens.dart new file mode 100644 index 000000000..aaddb8c8a --- /dev/null +++ b/lib/services/coins/bitcoincash/cashtokens.dart @@ -0,0 +1,345 @@ +import 'dart:typed_data'; +import 'package:hex/hex.dart'; +import 'package:convert/convert.dart'; + +// The Structure enum +enum Structure { + HasAmount, + HasNFT, + HasCommitmentLength, +} + +// The Capability enum +enum Capability { + NoCapability, + Mutable, + Minting, +} + +// Used as a "custom tuple" +class CompactSizeResult { + final int amount; + final int bytesRead; + + CompactSizeResult({required this.amount, required this.bytesRead}); +} + + +class ParsedOutput { + List? script_pub_key; + TokenOutputData? token_data; + ParsedOutput({this.script_pub_key, this.token_data}); +} + +class TokenOutputData { + Uint8List? id; + int? amount; + Uint8List? commitment; + Uint8List? bitfield; // A byte (Uint8List of length 1) + + // Constructor + TokenOutputData({ + this.id, + this.amount, + this.commitment, + this.bitfield, + }); + + int getCapability() { + if (bitfield != null) { + return bitfield![0] & 0x0f; + } + return 0; + } + + bool hasCommitmentLength() { + if (bitfield != null) { + return (bitfield![0] & 0x40) != 0; + } + return false; + } + + bool hasAmount() { + if (bitfield != null) { + return (bitfield![0] & 0x10) != 0; + } + return false; + } + + bool hasNFT() { + if (bitfield != null) { + return (bitfield![0] & 0x20) != 0; + } + return false; + } + + bool isMintingNFT() { + return hasNFT() && getCapability() == Capability.Minting.index; + } + + bool isMutableNFT() { + return hasNFT() && getCapability() == Capability.Mutable.index; + } + + bool isImmutableNFT() { + return hasNFT() && getCapability() == Capability.NoCapability.index; + } + + bool isValidBitfield() { + if (bitfield == null) { + return false; + } + + int s = bitfield![0] & 0xf0; + if (s >= 0x80 || s == 0x00) { + return false; + } + if (bitfield![0] & 0x0f > 2) { + return false; + } + if (!hasNFT() && !hasAmount()) { + return false; + } + if (!hasNFT() && (bitfield![0] & 0x0f) != 0) { + return false; + } + if (!hasNFT() && hasCommitmentLength()) { + return false; + } + return true; + } + + + + int deserialize(Uint8List buffer, {int cursor = 0, bool strict = false}) { + try { + + this.id = buffer.sublist(cursor, cursor + 32); + cursor += 32; + + + this.bitfield = Uint8List.fromList([buffer[cursor]]); + cursor += 1; + + if (this.hasCommitmentLength()) { + + // Read the first byte to determine the length of the commitment data + int commitmentLength = buffer[cursor]; + + // Move cursor to the next byte + cursor += 1; + + // Read 'commitmentLength' bytes for the commitment data + this.commitment = buffer.sublist(cursor, cursor + commitmentLength); + + // Adjust the cursor by the length of the commitment data + cursor += commitmentLength; + } else { + this.commitment = null; + } + + + if (this.hasAmount()) { + // Use readCompactSize that returns CompactSizeResult + CompactSizeResult result = readCompactSize(buffer, cursor, strict: strict); + this.amount = result.amount; + cursor += result.bytesRead; + } else { + this.amount = 0; + } + + + if (!this.isValidBitfield() || + (this.hasAmount() && this.amount == 0) || + (this.amount! < 0 || this.amount! > (1 << 63) - 1) || + (this.hasCommitmentLength() && this.commitment!.isEmpty) || + (this.amount! == 0 && !this.hasNFT()) + ) { + throw Exception('Unable to parse token data or token data is invalid'); + } + + return cursor; // Return the number of bytes read + + } catch (e) { + throw Exception('Deserialization failed: $e'); + } + } + + + // Serialize method + Uint8List serialize() { + var buffer = BytesBuilder(); + + // write ID and bitfield + buffer.add(this.id!); + buffer.addByte(this.bitfield![0]); + + // Write optional fields + if (this.hasCommitmentLength()) { + buffer.add(this.commitment!); + } + + if (this.hasAmount()) { + List compactSizeBytes = writeCompactSize(this.amount!); + buffer.add(compactSizeBytes); + } + + return buffer.toBytes(); + } + +} //END OF OUTPUTDATA CLASS + +final List PREFIX_BYTE = [0xef]; + +ParsedOutput wrap_spk(TokenOutputData? token_data, Uint8List script_pub_key) { + ParsedOutput parsedOutput = ParsedOutput(); + + if (token_data == null) { + parsedOutput.script_pub_key = script_pub_key; + return parsedOutput; + } + + final buf = BytesBuilder(); + + buf.add(PREFIX_BYTE); + buf.add(token_data.serialize()); + buf.add(script_pub_key); + + parsedOutput.script_pub_key = buf.toBytes(); + parsedOutput.token_data = token_data; + + return parsedOutput; +} + + + + +ParsedOutput unwrap_spk(Uint8List wrapped_spk) { + ParsedOutput parsedOutput = ParsedOutput(); + + + if (wrapped_spk.isEmpty || wrapped_spk[0] != PREFIX_BYTE[0]) { + parsedOutput.script_pub_key = wrapped_spk; + return parsedOutput; + } + + + int read_cursor = 1; // Start after the PREFIX_BYTE + TokenOutputData token_data = TokenOutputData(); + + Uint8List wrapped_spk_without_prefix_byte; + try { + // Deserialize updates read_cursor by the number of bytes read + + wrapped_spk_without_prefix_byte= wrapped_spk.sublist(read_cursor); + int bytesRead = token_data.deserialize(wrapped_spk_without_prefix_byte); + + + read_cursor += bytesRead; + parsedOutput.token_data = token_data; + parsedOutput.script_pub_key = wrapped_spk.sublist(read_cursor); + + } catch (e) { + // If unable to deserialize, return all bytes as the full scriptPubKey + parsedOutput.script_pub_key = wrapped_spk; + } + + return parsedOutput; +} + + +void testUnwrapSPK() { + + + // Example Hex format string + String var1 = "YOUR-SCRIPT-PUBKEY-AS-HEX-STRING-FOR-TESTING-GOES-HERE"; + // Convert the Hex string to Uint8List + Uint8List wrapped_spk = Uint8List.fromList(HEX.decode(var1)); + + // Call unwrap_spk + ParsedOutput parsedOutput = unwrap_spk(wrapped_spk); + + print("Parsed Output: $parsedOutput"); + + // Access token_data inside parsedOutput + TokenOutputData? tokenData = parsedOutput.token_data; + + // Check if tokenData is null + if (tokenData != null) { + // Print specific fields + if (tokenData.id != null) { + print("ID: ${hex.encode(tokenData.id!)}"); // hex is imported + } else { + print("ID: null"); + } + print ("amount of tokens"); + print (tokenData.amount); + print("Is it an NFT?: ${tokenData.hasNFT()}"); + } else { + print("Token data is null."); + } + +} //end function + + +// HELPER FUNCTIONS +CompactSizeResult readCompactSize(Uint8List buffer, int cursor, {bool strict = false}) { + + + int bytesRead = 0; // Variable to count bytes read + int val; + try { + val = buffer[cursor]; + cursor += 1; + bytesRead += 1; + int minVal; + if (val == 253) { + val = buffer.buffer.asByteData().getUint16(cursor, Endian.little); + cursor += 2; + bytesRead += 2; + minVal = 253; + } else if (val == 254) { + val = buffer.buffer.asByteData().getUint32(cursor, Endian.little); + cursor += 4; + bytesRead += 4; + minVal = 1 << 16; + } else if (val == 255) { + val = buffer.buffer.asByteData().getInt64(cursor, Endian.little); + cursor += 8; + bytesRead += 8; + minVal = 1 << 32; + } else { + minVal = 0; + } + if (strict && val < minVal) { + throw Exception("CompactSize is not minimally encoded"); + } + + return CompactSizeResult(amount: val, bytesRead: bytesRead); + } catch (e) { + throw Exception("attempt to read past end of buffer"); + } +} +Uint8List writeCompactSize(int size) { + var buffer = ByteData(9); // Maximum needed size for compact size is 9 bytes + if (size < 0) { + throw Exception("attempt to write size < 0"); + } else if (size < 253) { + return Uint8List.fromList([size]); + } else if (size < (1 << 16)) { + buffer.setUint8(0, 253); + buffer.setUint16(1, size, Endian.little); + return buffer.buffer.asUint8List(0, 3); + } else if (size < (1 << 32)) { + buffer.setUint8(0, 254); + buffer.setUint32(1, size, Endian.little); + return buffer.buffer.asUint8List(0, 5); + } else if (size < (1 << 64)) { + buffer.setUint8(0, 255); + buffer.setInt64(1, size, Endian.little); + return buffer.buffer.asUint8List(0, 9); + } else { + throw Exception("Size too large to represent as CompactSize"); + } +} +