From 467f23c6af3e1ae338a744d808d2a056e2938cb0 Mon Sep 17 00:00:00 2001 From: penn Date: Mon, 30 Mar 2026 22:14:48 +0800 Subject: [PATCH 1/2] feat: implement missing Token2022Program extensions - Add support for MetadataPointerExtension - Add support for TransferHookExtension - Implement initializeTokenMetadataInstruction per spl-token-metadata-interface --- .../programs/token/Token2022Program.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java b/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java index e18ce5f..5a10304 100644 --- a/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java +++ b/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java @@ -8,6 +8,9 @@ import software.sava.core.programs.Discriminator; import software.sava.core.tx.Instruction; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -1378,6 +1381,117 @@ public static Instruction withdrawExcessLamports(final SolanaAccounts solanaAcco ); } + public static Instruction initializeMetadataPointer(final AccountMeta invokedTokenProgram, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey metadataAddress) { + final var keys = List.of(createWrite(mintAccount)); + byte[] data = new byte[1+1+32+32]; + data[0] = (byte)TokenInstruction.MetadataPointerExtension.ordinal(); + data[1] = (byte)0; + authority.write(data, 2); + + metadataAddress.write(data, 34); + + return createInstruction(invokedTokenProgram, keys, data); + } + + public static Instruction updateMetadataPointer(final AccountMeta invokedTokenProgram, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey programAccount) { + + byte[] data = new byte[1+1+32]; + data[0] = (byte)TokenInstruction.MetadataPointerExtension.ordinal(); + data[1] = (byte)1; + + programAccount.write(data, 2); + + return Instruction.createInstruction( + invokedTokenProgram, + List.of( + AccountMeta.createWrite(mintAccount), + AccountMeta.createReadOnlySigner(authority) + ), + data + ); + } + + public static Instruction initializeTokenMetadataInstruction( + final SolanaAccounts solanaAccounts, + final PublicKey metadata, + final PublicKey updateAuthority, + final PublicKey mintAuthority, + final PublicKey mint, + final String name, + final String symbol, + final String uri + ) { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); + byte[] symbolBytes = symbol.getBytes(StandardCharsets.UTF_8); + byte[] uriBytes = uri.getBytes(StandardCharsets.UTF_8); + byte[] discriminator = new byte[] { + (byte) 0xD2, (byte) 0xE1, (byte) 0x1E, (byte) 0xA2, (byte) 0x58, (byte) 0xB8, (byte) 0x4D, (byte) 0x8D + }; + + int payloadSize = discriminator.length + 4 + nameBytes.length + 4 + symbolBytes.length + 4 + uriBytes.length; + + ByteBuffer buf = ByteBuffer.allocate(payloadSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(discriminator); + buf.putInt(nameBytes.length); + buf.put(nameBytes); + buf.putInt(symbolBytes.length); + buf.put(symbolBytes); + buf.putInt(uriBytes.length); + buf.put(uriBytes); + + return Instruction.createInstruction( + solanaAccounts.token2022Program(), + List.of( + AccountMeta.createWrite(metadata), + AccountMeta.createMeta(updateAuthority, false, false), + AccountMeta.createMeta(mint, false, false), + AccountMeta.createMeta(mintAuthority, false, true) + ), + buf.array() + ); + + } + public static Instruction initializeTransferHook(final AccountMeta invokedTokenProgram, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey programAccount) { + List keys = List.of(AccountMeta.createWrite(mintAccount)); + byte[] data = new byte[1+1+32+32]; + data[0] = (byte)Token2022Program.TokenInstruction.TransferHookExtension.ordinal(); + data[1] = (byte)0; + + authority.write(data, 2); + programAccount.write(data,34); + return Instruction.createInstruction(invokedTokenProgram, keys, data); + } + public static Instruction updateTransferHook( + final AccountMeta invokedTokenProgram, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey programAccount) { + + byte[] data = new byte[1+1+32]; + data[0] = (byte)Token2022Program.TokenInstruction.TransferHookExtension.ordinal(); + data[1] = (byte)1; + + programAccount.write(data, 2); + + return Instruction.createInstruction( + invokedTokenProgram, + List.of( + AccountMeta.createWrite(mintAccount), + AccountMeta.createReadOnlySigner(authority) + ), + data + ); + } private Token2022Program() { } } From be68b3a63ee366517f3de6c73402befa7a9475c7 Mon Sep 17 00:00:00 2001 From: penn Date: Tue, 31 Mar 2026 17:24:36 +0800 Subject: [PATCH 2/2] refactor: replace ByteBuffer with ByteUtil --- .../programs/token/Token2022Program.java | 147 +++++++++++----- .../programs/system/TokenProgramTests.java | 163 ++++++++++++++++++ 2 files changed, 265 insertions(+), 45 deletions(-) diff --git a/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java b/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java index 5a10304..537e3a7 100644 --- a/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java +++ b/solana-programs/src/main/java/software/sava/solana/programs/token/Token2022Program.java @@ -1380,96 +1380,150 @@ public static Instruction withdrawExcessLamports(final SolanaAccounts solanaAcco signerAccounts ); } - + public static Instruction initializeMetadataPointer(final SolanaAccounts solanaAccounts, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey metadataAccount) { + return initializeMetadataPointer(solanaAccounts.invokedToken2022Program(), mintAccount,authority, metadataAccount); + } public static Instruction initializeMetadataPointer(final AccountMeta invokedTokenProgram, final PublicKey mintAccount, final PublicKey authority, - final PublicKey metadataAddress) { + final PublicKey metadataAccount) { final var keys = List.of(createWrite(mintAccount)); byte[] data = new byte[1+1+32+32]; data[0] = (byte)TokenInstruction.MetadataPointerExtension.ordinal(); data[1] = (byte)0; + authority.write(data, 2); - metadataAddress.write(data, 34); + metadataAccount.write(data, 34); return createInstruction(invokedTokenProgram, keys, data); } + public static Instruction updateMetadataPointer(final SolanaAccounts solanaAccounts, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey metadataAccount){ + return updateMetadataPointer(solanaAccounts.invokedToken2022Program(),mintAccount,authority,metadataAccount); + } public static Instruction updateMetadataPointer(final AccountMeta invokedTokenProgram, final PublicKey mintAccount, final PublicKey authority, - final PublicKey programAccount) { + final PublicKey metadataAccount) { + + final var keys = List.of( + AccountMeta.createWrite(mintAccount), + AccountMeta.createReadOnlySigner(authority) + ); byte[] data = new byte[1+1+32]; data[0] = (byte)TokenInstruction.MetadataPointerExtension.ordinal(); data[1] = (byte)1; - programAccount.write(data, 2); + metadataAccount.write(data, 2); - return Instruction.createInstruction( + return createInstruction( invokedTokenProgram, - List.of( - AccountMeta.createWrite(mintAccount), - AccountMeta.createReadOnlySigner(authority) - ), + keys, data ); } public static Instruction initializeTokenMetadataInstruction( final SolanaAccounts solanaAccounts, - final PublicKey metadata, + final PublicKey metadataAccount, final PublicKey updateAuthority, final PublicKey mintAuthority, - final PublicKey mint, + final PublicKey mintAccount, final String name, final String symbol, final String uri ) { + final var keys = List.of( + AccountMeta.createWrite(metadataAccount), + AccountMeta.createMeta(updateAuthority, false, false), + AccountMeta.createMeta(mintAccount, false, false), + AccountMeta.createMeta(mintAuthority, false, true) + ); + + byte[] data = buildInitializeTokenMetadataData(name, symbol, uri); + + return createInstruction( + solanaAccounts.invokedToken2022Program(), + keys, + data + ); + } + + private static byte[] buildInitializeTokenMetadataData( + String name, + String symbol, + String uri) { + byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); byte[] symbolBytes = symbol.getBytes(StandardCharsets.UTF_8); byte[] uriBytes = uri.getBytes(StandardCharsets.UTF_8); - byte[] discriminator = new byte[] { - (byte) 0xD2, (byte) 0xE1, (byte) 0x1E, (byte) 0xA2, (byte) 0x58, (byte) 0xB8, (byte) 0x4D, (byte) 0x8D + + byte[] discriminator = new byte[]{ + (byte) 0xD2, (byte) 0xE1, (byte) 0x1E, (byte) 0xA2, + (byte) 0x58, (byte) 0xB8, (byte) 0x4D, (byte) 0x8D }; - int payloadSize = discriminator.length + 4 + nameBytes.length + 4 + symbolBytes.length + 4 + uriBytes.length; - - ByteBuffer buf = ByteBuffer.allocate(payloadSize); - buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put(discriminator); - buf.putInt(nameBytes.length); - buf.put(nameBytes); - buf.putInt(symbolBytes.length); - buf.put(symbolBytes); - buf.putInt(uriBytes.length); - buf.put(uriBytes); - - return Instruction.createInstruction( - solanaAccounts.token2022Program(), - List.of( - AccountMeta.createWrite(metadata), - AccountMeta.createMeta(updateAuthority, false, false), - AccountMeta.createMeta(mint, false, false), - AccountMeta.createMeta(mintAuthority, false, true) - ), - buf.array() - ); + int dataSize = discriminator.length + + Integer.BYTES + nameBytes.length + + Integer.BYTES + symbolBytes.length + + Integer.BYTES + uriBytes.length; + + byte[] data = new byte[dataSize]; + int offset = 0; + System.arraycopy(discriminator, 0, data, offset, discriminator.length); + offset += discriminator.length; + + ByteUtil.putInt32LE(data, offset, nameBytes.length); + offset += Integer.BYTES; + System.arraycopy(nameBytes, 0, data, offset, nameBytes.length); + offset += nameBytes.length; + + ByteUtil.putInt32LE(data, offset, symbolBytes.length); + offset += Integer.BYTES; + System.arraycopy(symbolBytes, 0, data, offset, symbolBytes.length); + offset += symbolBytes.length; + + ByteUtil.putInt32LE(data, offset, uriBytes.length); + offset += Integer.BYTES; + System.arraycopy(uriBytes, 0, data, offset, uriBytes.length); + + return data; + } + + public static Instruction initializeTransferHook(final SolanaAccounts solanaAccounts, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey programAccount) { + return initializeTransferHook(solanaAccounts.invokedToken2022Program(), mintAccount,authority, programAccount); } public static Instruction initializeTransferHook(final AccountMeta invokedTokenProgram, final PublicKey mintAccount, final PublicKey authority, final PublicKey programAccount) { - List keys = List.of(AccountMeta.createWrite(mintAccount)); + final var keys = List.of(AccountMeta.createWrite(mintAccount)); byte[] data = new byte[1+1+32+32]; - data[0] = (byte)Token2022Program.TokenInstruction.TransferHookExtension.ordinal(); + data[0] = (byte)TokenInstruction.TransferHookExtension.ordinal(); data[1] = (byte)0; authority.write(data, 2); programAccount.write(data,34); - return Instruction.createInstruction(invokedTokenProgram, keys, data); + return createInstruction(invokedTokenProgram, keys, data); + } + + public static Instruction updateTransferHook(final SolanaAccounts solanaAccounts, + final PublicKey mintAccount, + final PublicKey authority, + final PublicKey programAccount){ + return updateTransferHook(solanaAccounts.invokedToken2022Program(),mintAccount,authority,programAccount); } public static Instruction updateTransferHook( final AccountMeta invokedTokenProgram, @@ -1477,18 +1531,21 @@ public static Instruction updateTransferHook( final PublicKey authority, final PublicKey programAccount) { + + final var keys = List.of( + AccountMeta.createWrite(mintAccount), + AccountMeta.createReadOnlySigner(authority) + ); + byte[] data = new byte[1+1+32]; - data[0] = (byte)Token2022Program.TokenInstruction.TransferHookExtension.ordinal(); + data[0] = (byte)TokenInstruction.TransferHookExtension.ordinal(); data[1] = (byte)1; programAccount.write(data, 2); - return Instruction.createInstruction( + return createInstruction( invokedTokenProgram, - List.of( - AccountMeta.createWrite(mintAccount), - AccountMeta.createReadOnlySigner(authority) - ), + keys, data ); } diff --git a/solana-programs/src/test/java/software/sava/solana/programs/system/TokenProgramTests.java b/solana-programs/src/test/java/software/sava/solana/programs/system/TokenProgramTests.java index f36ea3b..1f40032 100644 --- a/solana-programs/src/test/java/software/sava/solana/programs/system/TokenProgramTests.java +++ b/solana-programs/src/test/java/software/sava/solana/programs/system/TokenProgramTests.java @@ -4,6 +4,7 @@ import software.sava.core.accounts.PublicKey; import software.sava.core.accounts.SolanaAccounts; import software.sava.core.accounts.meta.AccountMeta; +import software.sava.core.encoding.Base58; import software.sava.solana.programs.token.Token2022Program; import software.sava.solana.programs.token.TokenProgram; @@ -94,4 +95,166 @@ void initializeMint() { assertArrayEquals(expectedData, initMintIx.data()); } + + @Test + void createMintWithTransferHook() { + // devnet 3b7rYDCdxymqBXR3FLFgUtUoQyoYGg2ZF2bbKviuSQToSWyZeJmfb2wpjpTsZRf8FCWVMtuNetTAz2EAvmRSZLUi + + final var mintAccount = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f"); + final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV"); + final var programAccount = PublicKey.fromBase58Encoded("2o6gvxp17hkML8Rz3cvqzbSTFStES287fYeDPeHhF7Vj"); + + final byte[] expectedData = Base58.decode(""" + F2LRfuZ8F9SkUvoY2DcGDFXNksBcb4d4UbpQyYcyRZjde31xioLJauJKYwRxjEjAuzMNPepJSHH3njMhSzFgvdM4Gy""".stripTrailing()); + + + final var solAccounts = SolanaAccounts.MAIN_NET; + var initializeTransferHookIx = Token2022Program.initializeTransferHook( + solAccounts, + mintAccount, + mintAuthority, + programAccount + ); + + assertEquals(solAccounts.invokedToken2022Program(), initializeTransferHookIx.programId()); + + var accounts = initializeTransferHookIx.accounts(); + assertEquals(1, accounts.size()); + assertEquals(AccountMeta.createWrite(mintAccount), accounts.getFirst()); + + + assertArrayEquals(expectedData, initializeTransferHookIx.data()); + + } + + @Test + void createMintWithMetadataPointer() { + // devnet 3b7rYDCdxymqBXR3FLFgUtUoQyoYGg2ZF2bbKviuSQToSWyZeJmfb2wpjpTsZRf8FCWVMtuNetTAz2EAvmRSZLUi + + final var mint = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f"); + final var authority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV"); + + final byte[] expectedData = Base58.decode(""" + GC7FSeyRsRSWqdePvGFp5oZSbvCin5dinmBb7X5fn9DzNcfCdmyXiTV9iEzEZRrkmv3ixyvggyPXnUNyTekHbNx3Ph""".stripTrailing()); + + + final var solAccounts = SolanaAccounts.MAIN_NET; + var initMetadataPointerIx = Token2022Program.initializeMetadataPointer( + solAccounts, + mint, + authority, + mint + ); + + assertEquals(solAccounts.invokedToken2022Program(), initMetadataPointerIx.programId()); + + var accounts = initMetadataPointerIx.accounts(); + assertEquals(1, accounts.size()); + assertEquals(AccountMeta.createWrite(mint), accounts.getFirst()); + + assertArrayEquals(expectedData, initMetadataPointerIx.data()); + + } + @Test + void createMintWithInitializingMetadata() { + // devnet 3b7rYDCdxymqBXR3FLFgUtUoQyoYGg2ZF2bbKviuSQToSWyZeJmfb2wpjpTsZRf8FCWVMtuNetTAz2EAvmRSZLUi + + final var name = "SimpleTestCoin"; + final var symbol = "STC"; + final var uri = "https://example.com/metadata.json"; + final var mintAccount = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f"); + final var metadataAccount = PublicKey.fromBase58Encoded("88WLQK58mbqNjaUBxYjEvhvdsWGQde4s1EqyagvEng2f"); + final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV"); + final var updateAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV"); + + final byte[] expectedData = Base58.decode(""" + AGUhRKBLRk1Ueut5CpnmUkTSsn2Fpg3v3un8sZ52wUqQh5ZvW8ots8FCc3MtpVSzANADodfMKGeGjhkTv59ziTJPF3XZ9LnH""".stripTrailing()); + + + final var solAccounts = SolanaAccounts.MAIN_NET; + var initializeTokenMetadataIx = Token2022Program.initializeTokenMetadataInstruction( + solAccounts, + metadataAccount, + mintAuthority, + updateAuthority, + mintAccount, + name, + symbol, + uri + ); + + assertEquals(solAccounts.invokedToken2022Program(), initializeTokenMetadataIx.programId()); + + var accounts = initializeTokenMetadataIx.accounts(); + assertEquals(4, accounts.size()); + assertEquals(AccountMeta.createWrite(metadataAccount), accounts.getFirst()); + assertEquals(AccountMeta.createRead(updateAuthority), accounts.get(1)); + assertEquals(AccountMeta.createRead(mintAccount), accounts.get(2)); + assertEquals(AccountMeta.createReadOnlySigner(mintAuthority), accounts.get(3)); + + assertArrayEquals(expectedData, initializeTokenMetadataIx.data()); + + } + + @Test + void updateTransferHookAccount() { + // devnet THZ3HTPAQZaEj6ggHSaLSxSS5CeGYp88VDa6NyXxY7pV9khHk1xJk1yHqP4jWByHjBUz34UuWLPffWQfeCzjNyi + + final var mintAccount = PublicKey.fromBase58Encoded("HCRDkSQ6vM9QxDkJMGNUmVKjWqPYudkEsZRDwoJvyzQE"); + final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV"); + final var newTransferHookProgramId = PublicKey.fromBase58Encoded("7cjXTZvHYGuFarmmsYqjXsyYZY5TMyeNmvidxPJfvQ1Q"); + + final byte[] expectedData = Base58.decode(""" + pD8q5bQ9YX5HQ6qxGodu61fJtfqPFHjAKoTLes7gCwnme2""".stripTrailing()); + + + final var solAccounts = SolanaAccounts.MAIN_NET; + var updateTransferHookIx = Token2022Program.updateTransferHook( + solAccounts, + mintAccount, + mintAuthority, + newTransferHookProgramId + ); + + assertEquals(solAccounts.invokedToken2022Program(), updateTransferHookIx.programId()); + + var accounts = updateTransferHookIx.accounts(); + assertEquals(2, accounts.size()); + assertEquals(AccountMeta.createWrite(mintAccount), accounts.getFirst()); + assertEquals(AccountMeta.createReadOnlySigner(mintAuthority), accounts.getLast()); + + assertArrayEquals(expectedData, updateTransferHookIx.data()); + + } + @Test + void updateMintMetadataAccount() { + // devnet 3iKA2XCusAq2uCxuGyWhw8oBkdYQMQMq87t5sJTXJpCcD169NDzDjBE3fcuTv6Dg8QpjC4QNmwxZXFhSB8DLZkj2 + + final var mintAccount = PublicKey.fromBase58Encoded("HCRDkSQ6vM9QxDkJMGNUmVKjWqPYudkEsZRDwoJvyzQE"); + final var mintAuthority = PublicKey.fromBase58Encoded("CvUqgjP892h66aYPC9E8gKTXnTebY8qaU5ehGrgEQSwV"); + final var newMetadataAddress = PublicKey.fromBase58Encoded("AsFagyk29GvS8dtibZ6vjtbfwjnMzn9xHcEzoAnRusCB"); + + final byte[] expectedData = Base58.decode(""" + t9LQrHqNgyQXjTT4nYpfgsSqXM8sAQVo3zQ8kxyueAPmrf""".stripTrailing()); + + + final var solAccounts = SolanaAccounts.MAIN_NET; + var updateMetadataPointerIx = Token2022Program.updateMetadataPointer( + solAccounts, + mintAccount, + mintAuthority, + newMetadataAddress + ); + + assertEquals(solAccounts.invokedToken2022Program(), updateMetadataPointerIx.programId()); + + var accounts = updateMetadataPointerIx.accounts(); + assertEquals(2, accounts.size()); + assertEquals(AccountMeta.createWrite(mintAccount), accounts.getFirst()); + assertEquals(AccountMeta.createReadOnlySigner(mintAuthority), accounts.getLast()); + + assertArrayEquals(expectedData, updateMetadataPointerIx.data()); + + } + }