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..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 @@ -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; @@ -1377,7 +1380,175 @@ 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 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); + + 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 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; + + metadataAccount.write(data, 2); + + return createInstruction( + invokedTokenProgram, + keys, + data + ); + } + + public static Instruction initializeTokenMetadataInstruction( + final SolanaAccounts solanaAccounts, + final PublicKey metadataAccount, + final PublicKey updateAuthority, + final PublicKey mintAuthority, + 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 + }; + + 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) { + final var keys = List.of(AccountMeta.createWrite(mintAccount)); + byte[] data = new byte[1+1+32+32]; + data[0] = (byte)TokenInstruction.TransferHookExtension.ordinal(); + data[1] = (byte)0; + + authority.write(data, 2); + programAccount.write(data,34); + 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, + final PublicKey mintAccount, + 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)TokenInstruction.TransferHookExtension.ordinal(); + data[1] = (byte)1; + + programAccount.write(data, 2); + + return createInstruction( + invokedTokenProgram, + keys, + data + ); + } private Token2022Program() { } } 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()); + + } + }