diff --git a/src/main/java/com/lmax/solana4j/Solana.java b/src/main/java/com/lmax/solana4j/Solana.java index 19f3fc1..649855c 100644 --- a/src/main/java/com/lmax/solana4j/Solana.java +++ b/src/main/java/com/lmax/solana4j/Solana.java @@ -1,7 +1,6 @@ package com.lmax.solana4j; import com.lmax.solana4j.api.AddressLookupTable; -import com.lmax.solana4j.api.AssociatedTokenAddress; import com.lmax.solana4j.api.Blockhash; import com.lmax.solana4j.api.InnerTransactionBuilder; import com.lmax.solana4j.api.Message; @@ -121,19 +120,6 @@ public static ProgramDerivedAddress programDerivedAddress(final List see return SolanaEncoding.deriveProgramAddress(seeds, programId); } - /** - * Derives an associated token address for the given owner, mint, and token program account. - * - * @param owner the public key of the owner - * @param mint the public key of the mint - * @param tokenProgramAccount the public key of the token program account - * @return a new instance of {@link AssociatedTokenAddress} - */ - public static AssociatedTokenAddress associatedTokenAddress(final PublicKey owner, final PublicKey mint, final PublicKey tokenProgramAccount) - { - return SolanaEncoding.deriveAssociatedTokenAddress(owner, mint, tokenProgramAccount); - } - /** * Creates a new blockhash from the given byte array. * diff --git a/src/main/java/com/lmax/solana4j/api/AssociatedTokenAddress.java b/src/main/java/com/lmax/solana4j/api/AssociatedTokenAddress.java deleted file mode 100644 index 607a59b..0000000 --- a/src/main/java/com/lmax/solana4j/api/AssociatedTokenAddress.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.lmax.solana4j.api; - -/** - * Interface representing an associated token address in the Solana blockchain. - *

- * This interface extends {@link ProgramDerivedAddress} and provides additional methods - * to retrieve the mint and owner public keys associated with the token address. - *

- */ -public interface AssociatedTokenAddress extends ProgramDerivedAddress -{ - - /** - * Returns the public key of the mint associated with this token address. - * - * @return the {@link PublicKey} of the mint - */ - PublicKey mint(); - - /** - * Returns the public key of the owner associated with this token address. - * - * @return the {@link PublicKey} of the owner - */ - PublicKey owner(); -} - diff --git a/src/main/java/com/lmax/solana4j/encoding/SolanaAssociatedTokenAddress.java b/src/main/java/com/lmax/solana4j/encoding/SolanaAssociatedTokenAddress.java deleted file mode 100644 index b65fc96..0000000 --- a/src/main/java/com/lmax/solana4j/encoding/SolanaAssociatedTokenAddress.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.lmax.solana4j.encoding; - -import com.lmax.solana4j.api.AssociatedTokenAddress; -import com.lmax.solana4j.api.ProgramDerivedAddress; -import com.lmax.solana4j.api.PublicKey; - -import java.util.List; - -import static com.lmax.solana4j.programs.AssociatedTokenProgram.ASSOCIATED_TOKEN_PROGRAM_ACCOUNT; -import static java.util.Objects.requireNonNull; - -final class SolanaAssociatedTokenAddress extends SolanaProgramDerivedAddress implements AssociatedTokenAddress -{ - private final PublicKey mint; - private final PublicKey owner; - - static SolanaAssociatedTokenAddress deriveAssociatedTokenAddress(final PublicKey owner, final PublicKey mint, final PublicKey tokenProgramAccount) - { - requireNonNull(owner, "The owner public key must be specified, but was null"); - requireNonNull(mint, "The mint public key must be specified, but was null"); - requireNonNull(owner, "The owner public key must be specified, but was null"); - - final ProgramDerivedAddress programDerivedAddress = SolanaProgramDerivedAddress.deriveProgramAddress( - List.of(owner.bytes(), tokenProgramAccount.bytes(), mint.bytes()), - ASSOCIATED_TOKEN_PROGRAM_ACCOUNT - ); - - return new SolanaAssociatedTokenAddress( - new SolanaAccount(programDerivedAddress.address().bytes()), - owner, - mint, - tokenProgramAccount, - programDerivedAddress.nonce() - ); - } - - private SolanaAssociatedTokenAddress(final PublicKey address, final PublicKey owner, final PublicKey mint, final PublicKey programAccount, final int nonce) - { - super(address, programAccount, nonce); - this.mint = mint; - this.owner = owner; - } - - @Override - public PublicKey mint() - { - return mint; - } - - @Override - public PublicKey owner() - { - return owner; - } - - @Override - public String toString() - { - return "SolanaAssociatedTokenAddress{" + - "mint=" + mint + - ", address=" + address + - ", owner=" + owner + - ", programId=" + programAccount + - ", nonce=" + nonce + - '}'; - } -} diff --git a/src/main/java/com/lmax/solana4j/encoding/SolanaEncoding.java b/src/main/java/com/lmax/solana4j/encoding/SolanaEncoding.java index e7c5375..08886fc 100644 --- a/src/main/java/com/lmax/solana4j/encoding/SolanaEncoding.java +++ b/src/main/java/com/lmax/solana4j/encoding/SolanaEncoding.java @@ -2,7 +2,6 @@ import com.lmax.solana4j.api.AddressLookupTable; -import com.lmax.solana4j.api.AssociatedTokenAddress; import com.lmax.solana4j.api.Blockhash; import com.lmax.solana4j.api.Destination; import com.lmax.solana4j.api.InnerTransactionBuilder; @@ -98,19 +97,6 @@ public static ProgramDerivedAddress deriveProgramAddress(final List seed return SolanaProgramDerivedAddress.deriveProgramAddress(seeds, programId); } - /** - * Derives an associated token address for the given owner, mint, and token program account. - * - * @param owner the public key of the owner - * @param mint the public key of the mint - * @param tokenProgramAccount the public key of the token program account - * @return a new instance of {@link AssociatedTokenAddress} - */ - public static AssociatedTokenAddress deriveAssociatedTokenAddress(final PublicKey owner, final PublicKey mint, final PublicKey tokenProgramAccount) - { - return SolanaAssociatedTokenAddress.deriveAssociatedTokenAddress(owner, mint, tokenProgramAccount); - } - /** * Creates a new blockhash from the given byte array. * diff --git a/src/main/java/com/lmax/solana4j/programs/AssociatedTokenProgram.java b/src/main/java/com/lmax/solana4j/programs/AssociatedTokenProgram.java index d05a939..dcbe744 100644 --- a/src/main/java/com/lmax/solana4j/programs/AssociatedTokenProgram.java +++ b/src/main/java/com/lmax/solana4j/programs/AssociatedTokenProgram.java @@ -4,12 +4,15 @@ import com.lmax.solana4j.api.ProgramDerivedAddress; import com.lmax.solana4j.api.PublicKey; import com.lmax.solana4j.api.TransactionBuilder; +import com.lmax.solana4j.encoding.SolanaEncoding; import org.bitcoinj.core.Base58; import java.nio.ByteOrder; +import java.util.List; import static com.lmax.solana4j.encoding.SysVar.RENT; import static com.lmax.solana4j.programs.SystemProgram.SYSTEM_PROGRAM_ACCOUNT; +import static java.util.Objects.requireNonNull; /** * Program for managing associated token accounts on the Solana blockchain. @@ -97,4 +100,25 @@ public AssociatedTokenProgram createAssociatedToken( return this; } + + /** + * Derives a program address for a given owner, token program account, and mint. + * This method generates a program-derived address based on the combination of the owner's public key, + * the token program account's public key, and the mint's public key, using the associated token program account. + * + * @param owner the owner's public key; must not be null + * @param tokenProgramAccount the token program account's public key; must not be null + * @param mint the mint's public key; must not be null + * @return the derived program address based on the provided owner, token program account, and mint public keys + * @throws NullPointerException if the owner, tokenProgramAccount, or mint is null + */ + public static ProgramDerivedAddress deriveAddress(final PublicKey owner, final PublicKey tokenProgramAccount, final PublicKey mint) + { + requireNonNull(owner, "The owner public key must be specified, but was null"); + requireNonNull(tokenProgramAccount, "The token program public key must be specified, but was null"); + requireNonNull(mint, "The mint public key must be specified, but was null"); + + return SolanaEncoding.deriveProgramAddress(List.of(owner.bytes(), tokenProgramAccount.bytes(), mint.bytes()), ASSOCIATED_TOKEN_PROGRAM_ACCOUNT); + } + } diff --git a/src/test-support/java/com/lmax/solana4j/SolanaNodeDsl.java b/src/test-support/java/com/lmax/solana4j/SolanaNodeDsl.java index 848b61a..718bb2f 100644 --- a/src/test-support/java/com/lmax/solana4j/SolanaNodeDsl.java +++ b/src/test-support/java/com/lmax/solana4j/SolanaNodeDsl.java @@ -6,7 +6,6 @@ import com.lmax.simpledsl.api.RepeatingGroup; import com.lmax.simpledsl.api.RequiredArg; import com.lmax.solana4j.api.AddressLookupTable; -import com.lmax.solana4j.api.AssociatedTokenAddress; import com.lmax.solana4j.api.ProgramDerivedAddress; import com.lmax.solana4j.api.PublicKey; import com.lmax.solana4j.assertion.Condition; @@ -17,6 +16,7 @@ import com.lmax.solana4j.domain.TokenProgram; import com.lmax.solana4j.encoding.SolanaEncoding; import com.lmax.solana4j.programs.AddressLookupTableProgram; +import com.lmax.solana4j.programs.AssociatedTokenProgram; import com.lmax.solana4j.programs.BpfLoaderUpgradeableProgram; import com.lmax.solana4j.programs.SystemProgram; import com.lmax.solana4j.solanaclient.api.AccountInfo; @@ -36,7 +36,6 @@ import static com.lmax.solana4j.assertion.Condition.isEqualTo; import static com.lmax.solana4j.assertion.Condition.isNotNull; import static com.lmax.solana4j.assertion.Condition.isTrue; -import static com.lmax.solana4j.encoding.SolanaEncoding.deriveAssociatedTokenAddress; import static com.lmax.solana4j.programs.SystemProgram.MINT_ACCOUNT_LENGTH; import static com.lmax.solana4j.programs.SystemProgram.NONCE_ACCOUNT_LENGTH; import static com.lmax.solana4j.programs.TokenProgram.ACCOUNT_LAYOUT_SPAN; @@ -581,7 +580,7 @@ public void createAssociatedTokenAccount(final String... args) final TokenProgram tokenProgram = TokenProgram.fromName(params.value("tokenProgram")); final TestPublicKey owner = testContext.data(TestDataType.TEST_PUBLIC_KEY).lookup(params.value("owner")); - final AssociatedTokenAddress associatedTokenAddress = deriveAssociatedTokenAddress(owner.getSolana4jPublicKey(), tokenMint.getSolana4jPublicKey(), tokenProgram.getProgram()); + final ProgramDerivedAddress associatedTokenAddress = AssociatedTokenProgram.deriveAddress(owner.getSolana4jPublicKey(), tokenProgram.getProgram(), tokenMint.getSolana4jPublicKey()); testContext.data(TestDataType.TEST_PUBLIC_KEY).store(params.value("associatedTokenAddress"), new TestPublicKey(associatedTokenAddress.address().bytes())); final TestKeyPair payer = testContext.data(TestDataType.TEST_KEY_PAIR).lookup(params.value("payer")); diff --git a/src/test/java/com/lmax/solana4j/encoding/SolanaAssociatedTokenAddressTest.java b/src/test/java/com/lmax/solana4j/encoding/SolanaAssociatedTokenAddressTest.java deleted file mode 100644 index 9c7b586..0000000 --- a/src/test/java/com/lmax/solana4j/encoding/SolanaAssociatedTokenAddressTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.lmax.solana4j.encoding; - -import com.lmax.solana4j.Solana; -import org.junit.jupiter.api.Test; - -import static com.lmax.solana4j.Solana4jTestHelper.ACCOUNT1; -import static com.lmax.solana4j.Solana4jTestHelper.ACCOUNT2; -import static com.lmax.solana4j.Solana4jTestHelper.ACCOUNT3; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -class SolanaAssociatedTokenAddressTest -{ - @Test - void throwsOnNullOwnerKey() - { - assertThatThrownBy(() -> - SolanaAssociatedTokenAddress.deriveAssociatedTokenAddress(null, Solana.account(ACCOUNT1), Solana.account(ACCOUNT2))) - .isInstanceOf(NullPointerException.class); - } - - @Test - void throwsOnNullMintKey() - { - assertThatThrownBy(() -> - SolanaAssociatedTokenAddress.deriveAssociatedTokenAddress(Solana.account(ACCOUNT1), null, Solana.account(ACCOUNT2))) - .isInstanceOf(NullPointerException.class); - } - - @Test - void throwsOnNullTokenProgramKey() - { - assertThatThrownBy(() -> - SolanaAssociatedTokenAddress.deriveAssociatedTokenAddress(Solana.account(ACCOUNT1), null, Solana.account(ACCOUNT2))) - .isInstanceOf(NullPointerException.class); - } - - @Test - void shouldCreateAssociatedTokenAddress() - { - final SolanaAssociatedTokenAddress associatedTokenAddress = SolanaAssociatedTokenAddress.deriveAssociatedTokenAddress( - Solana.account(ACCOUNT1), - Solana.account(ACCOUNT2), - Solana.account(ACCOUNT3)); - - assertThat(associatedTokenAddress.owner()).isEqualTo(Solana.account(ACCOUNT1)); - assertThat(associatedTokenAddress.mint()).isEqualTo(Solana.account(ACCOUNT2)); - assertThat(associatedTokenAddress.nonce()).isEqualTo(254); - assertThat(associatedTokenAddress.programAccount.base58()).isEqualTo("US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx"); - assertThat(associatedTokenAddress.address.base58()).isEqualTo("5rvict4a6xzcf1kwsQTRDY4NhjgWjgfZtquSPwLsRoPM"); - } -} \ No newline at end of file diff --git a/src/test/java/com/lmax/solana4j/programs/AssociatedTokenProgramTest.java b/src/test/java/com/lmax/solana4j/programs/AssociatedTokenProgramTest.java new file mode 100644 index 0000000..d258c07 --- /dev/null +++ b/src/test/java/com/lmax/solana4j/programs/AssociatedTokenProgramTest.java @@ -0,0 +1,70 @@ +package com.lmax.solana4j.programs; + +import com.lmax.solana4j.Solana; +import com.lmax.solana4j.api.ProgramDerivedAddress; +import com.lmax.solana4j.api.PublicKey; +import org.junit.jupiter.api.Test; + +import static com.lmax.solana4j.Solana4jTestHelper.ACCOUNT1; +import static com.lmax.solana4j.Solana4jTestHelper.ACCOUNT2; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +public class AssociatedTokenProgramTest +{ + @Test + void throwsOnNullOwnerKey() + { + assertThatThrownBy(() -> + AssociatedTokenProgram.deriveAddress(null, Solana.account(ACCOUNT1), Solana.account(ACCOUNT2))) + .isInstanceOf(NullPointerException.class); + } + + @Test + void throwsOnNullMintKey() + { + assertThatThrownBy(() -> + AssociatedTokenProgram.deriveAddress(Solana.account(ACCOUNT1), null, Solana.account(ACCOUNT2))) + .isInstanceOf(NullPointerException.class); + } + + @Test + void throwsOnNullTokenProgramKey() + { + assertThatThrownBy(() -> + AssociatedTokenProgram.deriveAddress(Solana.account(ACCOUNT1), null, Solana.account(ACCOUNT2))) + .isInstanceOf(NullPointerException.class); + } + + @Test + void shouldCreateAssociatedTokenAddress() + { + final PublicKey owner = Solana.account(ACCOUNT1); + final PublicKey mint = Solana.account(ACCOUNT2); + + final ProgramDerivedAddress associatedTokenAddress = AssociatedTokenProgram.deriveAddress( + owner, + TokenProgram.PROGRAM_ACCOUNT, + mint); + + assertThat(associatedTokenAddress.programId()).isEqualTo(AssociatedTokenProgram.ASSOCIATED_TOKEN_PROGRAM_ACCOUNT); + assertThat(associatedTokenAddress.address().base58()).isEqualTo("BtHaGVQ4uRjgxGSoADNkPLxQUVBFhv3GaLbrU5jQnbmh"); + assertThat(associatedTokenAddress.nonce()).isEqualTo(255); + } + + @Test + void shouldCreateAssociatedToken2022Address() + { + final PublicKey owner = Solana.account(ACCOUNT1); + final PublicKey mint = Solana.account(ACCOUNT2); + + final ProgramDerivedAddress associatedTokenAddress = AssociatedTokenProgram.deriveAddress( + owner, + Token2022Program.PROGRAM_ACCOUNT, + mint); + + assertThat(associatedTokenAddress.programId()).isEqualTo(AssociatedTokenProgram.ASSOCIATED_TOKEN_PROGRAM_ACCOUNT); + assertThat(associatedTokenAddress.address().base58()).isEqualTo("6TZpgbVr5gCryZfwJEEgTjp88DU4LWDkVA68ACUdC9gK"); + assertThat(associatedTokenAddress.nonce()).isEqualTo(253); + } +}