diff --git a/src/Solnet.Programs/AccountCompression/AccountCompressionProgram.cs b/src/Solnet.Programs/AccountCompression/AccountCompressionProgram.cs new file mode 100644 index 00000000..494ca694 --- /dev/null +++ b/src/Solnet.Programs/AccountCompression/AccountCompressionProgram.cs @@ -0,0 +1,306 @@ +using Solnet.Programs.Abstract; +using Solnet.Programs.AccountCompression; +using Solnet.Programs.Utilities; +using Solnet.Rpc.Models; +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using static Solnet.Programs.Models.Stake.State; + + +namespace Solnet.Programs +{ + /// + /// Implements the Stake Program methods. + /// + /// For more information see: + /// https://docs.rs/spl-account-compression/latest/spl_account_compression/instruction/index.html + /// https://github.com/solana-program/account-compression/blob/ac-mainnet-tag/account-compression/sdk/src/instructions/index.ts + /// + /// + public static class AccountCompressionProgram + { + /// + /// The public key of the Stake Program. + /// + public static PublicKey ProgramIdKey = new ("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); + + /// + /// The public key of the account compression program. + /// + public static readonly PublicKey ConfigKey = new("ComprConfig11111111111111111111111111111111"); + /// + /// The program's name. + /// + private const string ProgramName = "Account Compression Program"; + + + + /// + /// Creates an instruction to append a leaf to a Merkle tree. + /// + /// + /// + /// + /// + public static TransactionInstruction Append( + PublicKey merkleTree, + PublicKey authority, + byte[] leaf) + { + List keys = new() + { + AccountMeta.Writable(authority, true), // Authority (signer) + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeAppendData(leaf) + }; + } + /// + /// /// Creates an instruction to close an empty Merkle tree and transfer its lamports to a recipient. + /// + /// + /// + /// + /// + public static TransactionInstruction CloseEmptyTree( + PublicKey merkleTree, + PublicKey authority, + PublicKey recipient + ) + { + List keys = new() + { + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + AccountMeta.Writable(authority, true), // Authority (signer) + AccountMeta.ReadOnly(recipient, false) // recipient + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeCloseEmptyTreeData() + }; + } + /// + /// /// Creates an instruction to initialize an empty Merkle tree. + /// + /// + /// + /// + /// + /// + public static TransactionInstruction InitEmptyMerkleTree( + PublicKey merkleTree, + PublicKey authority, + byte maxDepth, byte maxBufferSize + ) + { + List keys = new() + { + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + AccountMeta.Writable(authority, true), // Authority (signer) + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeInitEmptyMerkleTreeData(maxDepth, maxBufferSize) + }; + } + /// + /// /// Creates an instruction to replace a leaf in a Merkle tree. + /// + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction ReplaceLeaf( + PublicKey merkleTree, + PublicKey authority, + byte[] newLeaf, + byte[] previousLeaf, + byte[] root, + uint index + ) + { + List keys = new() + { + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + AccountMeta.Writable(authority, true), // Authority (signer) + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeReplaceLeafData(newLeaf, previousLeaf, root, index) + }; + } + /// + /// /// Creates an instruction to insert or append a leaf to a Merkle tree. + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction InsertOrAppend( + PublicKey merkleTree, + PublicKey authority, + byte[] Leaf, + byte[] root, + uint index + ) + { + List keys = new() + { + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + AccountMeta.Writable(authority, true), // Authority (signer) + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeInsertOrAppendData(Leaf, root, index) + }; + } + /// + /// Creates an instruction to transfer authority of a Merkle tree. + /// + /// + /// + /// + /// + public static TransactionInstruction TransferAuthority( + PublicKey merkleTree, + PublicKey authority, + PublicKey newAuthority + ) + { + List keys = new() + { + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + AccountMeta.Writable(authority, true), // Authority (signer) + AccountMeta.ReadOnly(newAuthority, false), //new Authority + + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeTransferAuthorityData(newAuthority) + }; + } + /// + /// Creates an instruction to verify a leaf in a Merkle tree. + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction VerifyLeaf( + PublicKey merkleTree, + PublicKey authority, + byte[] root, byte[] leaf, uint index + ) + { + List keys = new() + { + AccountMeta.Writable(merkleTree, false), // Merkle tree account (writable) + AccountMeta.Writable(authority, true), // Authority (signer) + + }; + + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = AccountCompressionProgramData.EncodeVerifyLeafData(root, leaf, index) + }; + } + /// + /// Decodes the instruction data for the . + /// + /// + /// + /// + /// + public static DecodedInstruction Decode(ReadOnlySpan data, IList keys, byte[] keyIndices) + { + uint instruction = data.GetU32(AccountCompressionProgramData.MethodOffset); + + if (!Enum.IsDefined(typeof(NameServiceInstructions.Values), instruction)) + { + return new() + { + PublicKey = ProgramIdKey, + InstructionName = "Unknown Instruction", + ProgramName = ProgramName, + Values = new Dictionary(), + InnerInstructions = new List() + }; + } + + AccountCompressionProgramInstructions.Values instructionValue = (AccountCompressionProgramInstructions.Values)instruction; + + DecodedInstruction decodedInstruction = new() + { + PublicKey = ProgramIdKey, + InstructionName = AccountCompressionProgramInstructions.Names[instructionValue], + ProgramName = ProgramName, + Values = new Dictionary() { }, + InnerInstructions = new List() + }; + + switch (instructionValue) + { + case AccountCompressionProgramInstructions.Values.Append: + AccountCompressionProgramData.DecodeAppendLeafData(decodedInstruction, data, keys, keyIndices); + break; + case AccountCompressionProgramInstructions.Values.InsertOrAppend: + AccountCompressionProgramData.DecodeInsertOrAppendData(decodedInstruction, data, keys, keyIndices); + break; + case AccountCompressionProgramInstructions.Values.VerifyLeaf: + AccountCompressionProgramData.DecodeVerifyLeafData(decodedInstruction, data, keys, keyIndices); + break; + case AccountCompressionProgramInstructions.Values.InitEmptyMerkleTree: + AccountCompressionProgramData.DecodeInitEmptyMerkleTreeData(decodedInstruction, data, keys, keyIndices); + break; + case AccountCompressionProgramInstructions.Values.CloseEmptyTree: + AccountCompressionProgramData.DecodeCloseEmptyTreeData(decodedInstruction, data, keys, keyIndices); + break; + case AccountCompressionProgramInstructions.Values.TransferAuthority: + AccountCompressionProgramData.DecodeTransferAuthorityInstruction(decodedInstruction, data, keys, keyIndices); + break; + case AccountCompressionProgramInstructions.Values.ReplaceLeaf: + AccountCompressionProgramData.DecodeReplaceLeafData(decodedInstruction, data, keys, keyIndices); + break; + + } + return decodedInstruction; + } + } +} diff --git a/src/Solnet.Programs/AccountCompression/AccountCompressionProgramData.cs b/src/Solnet.Programs/AccountCompression/AccountCompressionProgramData.cs new file mode 100644 index 00000000..aec1c52c --- /dev/null +++ b/src/Solnet.Programs/AccountCompression/AccountCompressionProgramData.cs @@ -0,0 +1,413 @@ +using Solnet.Programs.Utilities; +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using static Solnet.Programs.Models.Stake.State; + +namespace Solnet.Programs.AccountCompression +{ + /// + /// Represents the instruction data for the . + /// + internal static class AccountCompressionProgramData + { + /// + /// The offset for the instruction discriminator in the instruction data. + /// + internal const int MethodOffset = 0; + /// + /// Encode the Append instruction data + /// + /// 32-byte leaf data + /// Encoded byte array for the instruction data + public static byte[] EncodeAppendData(byte[] leaf) + { + if (leaf == null || leaf.Length != 32) + throw new ArgumentException("Leaf must be 32 bytes"); + + byte[] data = new byte[4 + 32]; // 4 bytes discriminator + 32 bytes leaf + int offset = 0; + + // Write the 4-byte discriminator + BitConverter.GetBytes((uint)AccountCompressionProgramInstructions.Values.Append).CopyTo(data, MethodOffset); + offset += 4; + + // Write the 32-byte leaf data + Buffer.BlockCopy(leaf, 0, data, offset, 32); + + return data; + } + /// + /// Encodes the CloseEmptyTree instruction data. + /// + /// + internal static byte[] EncodeCloseEmptyTreeData() + { + // Only the discriminator (first 4 bytes) is needed + var data = new byte[4]; + + // You should replace this with the actual discriminator used by your Anchor program + // For example: discriminator = Hash("global:close_empty_tree").Take(8) or a known constant + uint discriminator = (uint)AccountCompressionProgramInstructions.Values.CloseEmptyTree; + + data.WriteU32(discriminator, 0); + return data; + } + /// + /// Encodes the InitEmptyMerkleTree instruction data. + /// + /// + /// + /// + internal static byte[] EncodeInitEmptyMerkleTreeData(byte maxDepth, byte maxBufferSize) + { + // Total size = 4 (discriminator) + 1 (maxDepth) + 1 (maxBufferSize) = 6 bytes + byte[] data = new byte[6]; + int offset = 0; + + // 1. Instruction Discriminator (4 bytes) + BitConverter.GetBytes((uint)AccountCompressionProgramInstructions.Values.InitEmptyMerkleTree) + .CopyTo(data, MethodOffset); + offset += 4; + + // 2. Max Depth (1 byte) + data[offset++] = maxDepth; + + // 3. Max Buffer Size (1 byte) + data[offset] = maxBufferSize; + + return data; + } + /// + /// Encodes the ReplaceLeaf instruction data. + /// + /// + /// + /// + /// + /// + /// + internal static byte[] EncodeReplaceLeafData( + byte[] newLeaf, + byte[] previousLeaf, + byte[] root, + uint index + ) + { + // Validate inputs + if (newLeaf == null || newLeaf.Length != 32) throw new ArgumentException("newLeaf must be 32 bytes"); + if (previousLeaf == null || previousLeaf.Length != 32) throw new ArgumentException("previousLeaf must be 32 bytes"); + if (root == null || root.Length != 32) throw new ArgumentException("root must be 32 bytes"); + + // Total size: 4 (discriminator) + 32 (root) + 32 (previousLeaf) + 32 (newLeaf) + 4 (index) = 104 bytes + byte[] data = new byte[104]; + int offset = 0; + + // 1. Instruction Discriminator (4 bytes) + BitConverter.GetBytes((uint)AccountCompressionProgramInstructions.Values.ReplaceLeaf) + .CopyTo(data, MethodOffset); + offset += 4; + + // 2. Root [32 bytes] + Buffer.BlockCopy(root, 0, data, offset, 32); + offset += 32; + + // 3. Previous Leaf [32 bytes] + Buffer.BlockCopy(previousLeaf, 0, data, offset, 32); + offset += 32; + + // 4. New Leaf [32 bytes] + Buffer.BlockCopy(newLeaf, 0, data, offset, 32); + offset += 32; + + // 5. Index [4 bytes] + BitConverter.GetBytes(index).CopyTo(data, offset); + + return data; + } + + /// + /// Encodes the InsertOrAppend instruction data. + /// + /// + /// + /// + /// + /// + internal static byte[] EncodeInsertOrAppendData( + byte[] Leaf, + byte[] root, + uint index + ) + { + // Validate inputs + if (root == null || root.Length != 32) throw new ArgumentException("root must be 32 bytes"); + + // Total size: 4 (discriminator) + 32 (root) + 32 (Leaf) + 4 (index) = 104 bytes + byte[] data = new byte[72]; + int offset = 0; + + // 1. Instruction Discriminator (4 bytes) + BitConverter.GetBytes((uint)AccountCompressionProgramInstructions.Values.InsertOrAppend) + .CopyTo(data, MethodOffset); + offset += 4; + + // 2. Root [32 bytes] + Buffer.BlockCopy(root, 0, data, offset, 32); + offset += 32; + + // 3. Leaf [32 bytes] + Buffer.BlockCopy(Leaf, 0, data, offset, 32); + offset += 32; + + // 4. Index [4 bytes] + BitConverter.GetBytes(index).CopyTo(data, offset); + + return data; + } + + /// + /// Encodes the TransferAuthority instruction data. + /// + /// + /// + /// + internal static byte[] EncodeTransferAuthorityData(PublicKey newAuthority) + { + if (newAuthority is null) throw new ArgumentNullException(nameof(newAuthority)); + + byte[] data = new byte[36]; + + // 1. Add instruction discriminator + data.WriteU32((uint)AccountCompressionProgramInstructions.Values.TransferAuthority,MethodOffset); + + // 2. Add new authority public key (32 bytes) + data.WritePubKey(newAuthority, 4); + + return data; + } + /// + /// Encodes the VerifyLeaf instruction data. + /// + /// + /// + /// + /// + /// + public static byte[] EncodeVerifyLeafData(byte[] root, byte[] leaf, uint index) + { + if (root == null || root.Length != 32) throw new ArgumentException("Root must be 32 bytes", nameof(root)); + if (leaf == null || leaf.Length != 32) throw new ArgumentException("Leaf must be 32 bytes", nameof(leaf)); + + // Total size: 4 (discriminator) + 32 (root) + 32 (leaf) + 4 (index) = 72 bytes + byte[] data = new byte[72]; + int offset = 0; + + // 1. Instruction Discriminator (4 bytes) + BitConverter.GetBytes((uint)AccountCompressionProgramInstructions.Values.VerifyLeaf).CopyTo(data, MethodOffset); + offset += 4; + + // 2. Root (32 bytes) + Buffer.BlockCopy(root, 0, data, offset, 32); + offset += 32; + + // 3. Leaf (32 bytes) + Buffer.BlockCopy(leaf, 0, data, offset, 32); + offset += 32; + + // 4. Index (4 bytes) + BitConverter.GetBytes(index).CopyTo(data, offset); + + return data; + } + /// + /// Decodes the AppendLeaf instruction data. + /// + /// + /// + /// + /// + internal static void DecodeAppendLeafData( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + // Decode accounts + decodedInstruction.Values.Add("authority", keys[keyIndices[0]]); + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[1]]); + + // Instruction data offsets: + // 0..4 = discriminator + // 4..36 = leaf (32 bytes) + var leafBytes = data.GetBytes(4, 32); + decodedInstruction.Values.Add("leaf", leafBytes); + } + /// + /// Decodes the CloseEmptyTree instruction data. + /// + /// + /// + /// + /// + /// + internal static void DecodeCloseEmptyTreeData( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + if (data.Length < 4) throw new ArgumentException("Instruction data too short."); + + uint discriminator = BitConverter.ToUInt32(data.Slice(0, 4)); + + decodedInstruction.Values ??= new Dictionary(); + decodedInstruction.Values.Add("instruction", discriminator); + + // Match accounts: [merkle_tree, authority, recipient] + if (keyIndices.Length < 3) throw new ArgumentException("Insufficient account keys for CloseEmptyTree"); + + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[0]]); + decodedInstruction.Values.Add("authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("recipient", keys[keyIndices[2]]); + } + /// + /// Decodes the InitEmptyMerkleTree instruction data. + /// + /// + /// + /// + /// + internal static void DecodeInitEmptyMerkleTreeData( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + // Accounts used + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[0]]); + decodedInstruction.Values.Add("authority", keys[keyIndices[1]]); + + // Data: [4 bytes discriminator][1 byte maxDepth][1 byte maxBufferSize] + decodedInstruction.Values.Add("max_depth", data[4]); + decodedInstruction.Values.Add("max_buffer_size", data[5]); + } + /// + /// Decodes the ReplaceLeaf instruction data. + /// + /// + /// + /// + /// + internal static void DecodeReplaceLeafData( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + // Decode accounts + + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[0]]); + decodedInstruction.Values.Add("authority", keys[keyIndices[1]]); + + // 0..4 = discriminator + // 4..36 = root + // 36..68 = previousLeaf + // 68..100 = newLeaf + // 100..104 = index (uint32) + + decodedInstruction.Values.Add("root", data.GetBytes(4, 32)); + decodedInstruction.Values.Add("previous_leaf", data.GetBytes(36, 32)); + decodedInstruction.Values.Add("new_leaf", data.GetBytes(68, 32)); + decodedInstruction.Values.Add("index", data.GetU32(100)); + } + /// + /// Decodes the InsertOrAppend instruction data. + /// + /// + /// + /// + /// + internal static void DecodeInsertOrAppendData( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + // Decode accounts + + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[0]]); + decodedInstruction.Values.Add("authority", keys[keyIndices[1]]); + + // 0..4 = discriminator + // 4..36 = root + // 36..68 = previousLeaf + // 68..100 = newLeaf + // 100..104 = index (uint32) + + decodedInstruction.Values.Add("root", data.GetBytes(4,32)); + decodedInstruction.Values.Add("leaf", data.GetBytes(36,32)); + decodedInstruction.Values.Add("index", data.GetU32(68)); + } + /// + /// Decodes the TransferAuthority instruction data. + /// + /// + /// + /// + /// + internal static void DecodeTransferAuthorityInstruction( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + // Decode accounts + + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[0]]); + decodedInstruction.Values.Add("authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("new_authority", keys[keyIndices[2]]); + + // Instruction data offsets: + // 0..4 = discriminator + // 4..36 = newAuthority public key + var newAuthority = data.GetPubKey(4); + decodedInstruction.Values.Add("new_authority", newAuthority); + } + /// + /// Decodes the VerifyLeaf instruction data. + /// + /// + /// + /// + /// + /// + internal static void DecodeVerifyLeafData( + DecodedInstruction decodedInstruction, + ReadOnlySpan data, + IList keys, + byte[] keyIndices) + { + // This instruction typically has only one key: merkleTree + decodedInstruction.Values.Add("merkle_tree", keys[keyIndices[0]]); + + // Decode data + if (data.Length < 72) throw new ArgumentException("Invalid data length for VerifyLeaf"); + + var root = data.GetBytes(4,32); + var leaf = data.GetBytes(36,32); + var index = data.GetU32(68); + + decodedInstruction.Values.Add("root", root); + decodedInstruction.Values.Add("leaf", leaf); + decodedInstruction.Values.Add("index", index); + } + } + + +} diff --git a/src/Solnet.Programs/AccountCompression/AccountCompressionProgramInstructions.cs b/src/Solnet.Programs/AccountCompression/AccountCompressionProgramInstructions.cs new file mode 100644 index 00000000..0958c4a0 --- /dev/null +++ b/src/Solnet.Programs/AccountCompression/AccountCompressionProgramInstructions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs +{ + /// + /// Represents the instruction types for the ??? along with a friendly name so as not to use reflection ???. + /// + /// For more information see: + /// https://docs.rs/spl-account-compression/latest/spl_account_compression/instruction/index.html + /// https://github.com/solana-program/account-compression/blob/ac-mainnet-tag/account-compression/sdk/src/instructions/index.ts + /// + /// + internal static class AccountCompressionProgramInstructions + { + /// + /// Represents the user-friendly names for the instruction types for the . + /// + internal static readonly Dictionary Names = new() + { + { Values.Append, "Append Merkle Tree" }, + { Values.CloseEmptyTree, "Close Empty Tree" }, + { Values.InitEmptyMerkleTree, "Init Empty Merkle Tree" }, + { Values.ReplaceLeaf, "Replace Leaf" }, + { Values.InsertOrAppend, "Insert Or Append Leaf" }, + { Values.TransferAuthority, "Transfer Authority" }, + { Values.VerifyLeaf, "Verify Leaf" } + }; + /// + /// Represents the instruction types for the . + /// + internal enum Values: uint + { + Append = 0, + CloseEmptyTree = 1, + InitEmptyMerkleTree = 2, + ReplaceLeaf = 3, + InsertOrAppend = 4, + TransferAuthority = 5, + VerifyLeaf = 6 + } + } +} diff --git a/src/Solnet.Programs/InstructionDecoder.cs b/src/Solnet.Programs/InstructionDecoder.cs index a6d060f7..2aaa6a58 100644 --- a/src/Solnet.Programs/InstructionDecoder.cs +++ b/src/Solnet.Programs/InstructionDecoder.cs @@ -1,4 +1,5 @@ -using Solnet.Programs.TokenSwap; +using Solnet.Programs.AccountCompression; +using Solnet.Programs.TokenSwap; using Solnet.Rpc.Builders; using Solnet.Rpc.Models; using Solnet.Wallet; @@ -38,6 +39,7 @@ static InstructionDecoder() InstructionDictionary.Add(NameServiceProgram.ProgramIdKey, NameServiceProgram.Decode); InstructionDictionary.Add(SharedMemoryProgram.ProgramIdKey, SharedMemoryProgram.Decode); InstructionDictionary.Add(StakeProgram.ProgramIdKey, StakeProgram.Decode); + InstructionDictionary.Add(AccountCompressionProgram.ProgramIdKey, AccountCompressionProgram.Decode); } /// diff --git a/test/Solnet.Programs.Test/AccountCompressionProgramTest.cs b/test/Solnet.Programs.Test/AccountCompressionProgramTest.cs new file mode 100644 index 00000000..6ddac027 --- /dev/null +++ b/test/Solnet.Programs.Test/AccountCompressionProgramTest.cs @@ -0,0 +1,116 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Solnet.Wallet; +using System.Linq; + +namespace Solnet.Programs.Test +{ + [TestClass] + public class AccountCompressionProgramTest + { + private static readonly PublicKey MerkleTree = new("11111111111111111111111111111111"); + private static readonly PublicKey Authority = new("22222222222222222222222222222222"); + private static readonly PublicKey Recipient = new("33333333333333333333333333333333"); + private static readonly PublicKey NewAuthority = new("44444444444444444444444444444444"); + + [TestMethod] + public void Append_CreatesCorrectInstruction() + { + var leaf = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray(); + var instr = AccountCompressionProgram.Append(MerkleTree, Authority, leaf); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(2, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable && instr.Keys[0].IsSigner); // Authority + Assert.IsTrue(instr.Keys[1].IsWritable && !instr.Keys[1].IsSigner); // MerkleTree + Assert.AreEqual(36, instr.Data.Length); + } + + [TestMethod] + public void CloseEmptyTree_CreatesCorrectInstruction() + { + var instr = AccountCompressionProgram.CloseEmptyTree(MerkleTree, Authority, Recipient); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(3, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable); // MerkleTree + Assert.IsTrue(instr.Keys[1].IsWritable && instr.Keys[1].IsSigner); // Authority + Assert.IsFalse(instr.Keys[2].IsWritable); // Recipient + Assert.AreEqual(4, instr.Data.Length); + } + + [TestMethod] + public void InitEmptyMerkleTree_CreatesCorrectInstruction() + { + byte maxDepth = 10, maxBufferSize = 20; + var instr = AccountCompressionProgram.InitEmptyMerkleTree(MerkleTree, Authority, maxDepth, maxBufferSize); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(2, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable); // MerkleTree + Assert.IsTrue(instr.Keys[1].IsWritable && instr.Keys[1].IsSigner); // Authority + Assert.AreEqual(6, instr.Data.Length); + } + + [TestMethod] + public void ReplaceLeaf_CreatesCorrectInstruction() + { + var newLeaf = Enumerable.Repeat((byte)1, 32).ToArray(); + var prevLeaf = Enumerable.Repeat((byte)2, 32).ToArray(); + var root = Enumerable.Repeat((byte)3, 32).ToArray(); + uint index = 42; + + var instr = AccountCompressionProgram.ReplaceLeaf(MerkleTree, Authority, newLeaf, prevLeaf, root, index); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(2, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable); // MerkleTree + Assert.IsTrue(instr.Keys[1].IsWritable && instr.Keys[1].IsSigner); // Authority + Assert.AreEqual(104, instr.Data.Length); + } + + [TestMethod] + public void InsertOrAppend_CreatesCorrectInstruction() + { + var leaf = Enumerable.Repeat((byte)1, 32).ToArray(); + var root = Enumerable.Repeat((byte)2, 32).ToArray(); + uint index = 99; + + var instr = AccountCompressionProgram.InsertOrAppend(MerkleTree, Authority, leaf, root, index); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(2, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable); // MerkleTree + Assert.IsTrue(instr.Keys[1].IsWritable && instr.Keys[1].IsSigner); // Authority + Assert.AreEqual(72, instr.Data.Length); + } + + [TestMethod] + public void TransferAuthority_CreatesCorrectInstruction() + { + var instr = AccountCompressionProgram.TransferAuthority(MerkleTree, Authority, NewAuthority); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(3, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable); // MerkleTree + Assert.IsTrue(instr.Keys[1].IsWritable && instr.Keys[1].IsSigner); // Authority + Assert.IsFalse(instr.Keys[2].IsWritable); // NewAuthority + Assert.AreEqual(36, instr.Data.Length); + } + + [TestMethod] + public void VerifyLeaf_CreatesCorrectInstruction() + { + var root = Enumerable.Repeat((byte)1, 32).ToArray(); + var leaf = Enumerable.Repeat((byte)2, 32).ToArray(); + uint index = 123; + + var instr = AccountCompressionProgram.VerifyLeaf(MerkleTree, Authority, root, leaf, index); + + CollectionAssert.AreEqual(AccountCompressionProgram.ProgramIdKey.KeyBytes, instr.ProgramId); + Assert.AreEqual(2, instr.Keys.Count); + Assert.IsTrue(instr.Keys[0].IsWritable); // MerkleTree + Assert.IsTrue(instr.Keys[1].IsWritable && instr.Keys[1].IsSigner); // Authority + Assert.AreEqual(72, instr.Data.Length); + } + } +}