diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/DecodedTransaction.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/DecodedTransaction.kt index d1862e2..d743d9a 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/DecodedTransaction.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/core/DecodedTransaction.kt @@ -3,7 +3,6 @@ package net.avianlabs.solana.domain.core import net.avianlabs.solana.domain.program.AssociatedTokenProgram import net.avianlabs.solana.domain.program.SystemProgram import net.avianlabs.solana.domain.program.TokenProgram -import net.avianlabs.solana.domain.program.TokenProgramBase import net.avianlabs.solana.methods.TransactionResponse import net.avianlabs.solana.vendor.decodeBase58 import net.avianlabs.solana.vendor.encodeToBase58String @@ -68,7 +67,7 @@ public sealed class DecodedInstruction( val owner: PublicKey, val amount: Long, ) : - TokenProgram(net.avianlabs.solana.domain.program.TokenProgramBase.Instruction.Transfer.index) + TokenProgram(net.avianlabs.solana.domain.program.TokenProgram.Instruction.Transfer.index) public data class TransferChecked( val source: PublicKey, @@ -78,7 +77,7 @@ public sealed class DecodedInstruction( val amount: Long, val decimals: UByte, ) : - TokenProgram(net.avianlabs.solana.domain.program.TokenProgramBase.Instruction.TransferChecked.index) + TokenProgram(net.avianlabs.solana.domain.program.TokenProgram.Instruction.TransferChecked.index) } public sealed class AssociatedTokenProgram : @@ -175,7 +174,7 @@ public fun TransactionResponse.decode(): DecodedTransaction? { TokenProgram.programId -> { val programIndex = buffer.readByte().toUByte() when (programIndex) { - TokenProgramBase.Instruction.Transfer.index -> { + TokenProgram.Instruction.Transfer.index -> { val (source, destination, owner) = accountsMeta!! DecodedInstruction.TokenProgram.Transfer( source = source.publicKey, @@ -185,7 +184,7 @@ public fun TransactionResponse.decode(): DecodedTransaction? { ) } - TokenProgramBase.Instruction.TransferChecked.index -> { + TokenProgram.Instruction.TransferChecked.index -> { val (source, mint, destination, owner) = accountsMeta!! DecodedInstruction.TokenProgram.TransferChecked( source = source.publicKey, diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/AssociatedTokenProgram.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/AssociatedTokenProgram.kt index 79645aa..651f089 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/AssociatedTokenProgram.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/AssociatedTokenProgram.kt @@ -4,15 +4,13 @@ import net.avianlabs.solana.domain.core.AccountMeta import net.avianlabs.solana.domain.core.PublicKey import net.avianlabs.solana.domain.core.TransactionInstruction -private val ASSOCIATED_TOKEN_PROGRAM_PROGRAM_ID = - PublicKey.fromBase58("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") +public object AssociatedTokenProgram : Program { -public object AssociatedTokenProgram : Program( - programId = ASSOCIATED_TOKEN_PROGRAM_PROGRAM_ID -) { + public override val programId: PublicKey = + PublicKey.fromBase58("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") public fun createAssociatedTokenAccountInstruction( - associatedProgramId: PublicKey = ASSOCIATED_TOKEN_PROGRAM_PROGRAM_ID, + associatedProgramId: PublicKey = this.programId, programId: PublicKey, mint: PublicKey, associatedAccount: PublicKey, @@ -29,7 +27,7 @@ public object AssociatedTokenProgram : Program( ) public fun createAssociatedTokenAccountInstructionIdempotent( - associatedProgramId: PublicKey = ASSOCIATED_TOKEN_PROGRAM_PROGRAM_ID, + associatedProgramId: PublicKey = this.programId, programId: PublicKey, mint: PublicKey, associatedAccount: PublicKey, diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/ComputeBudgetProgram.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/ComputeBudgetProgram.kt index ac37157..50769bc 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/ComputeBudgetProgram.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/ComputeBudgetProgram.kt @@ -2,14 +2,14 @@ package net.avianlabs.solana.domain.program import net.avianlabs.solana.domain.core.PublicKey import net.avianlabs.solana.domain.core.TransactionInstruction +import net.avianlabs.solana.domain.program.Program.Companion.createTransactionInstruction import okio.Buffer -private val COMPUTE_BUDGET_PROGRAM_ID = - PublicKey.fromBase58("ComputeBudget111111111111111111111111111111") -public object ComputeBudgetProgram : Program( - programId = COMPUTE_BUDGET_PROGRAM_ID, -) { +public object ComputeBudgetProgram : Program { + + public override val programId: PublicKey = + PublicKey.fromBase58("ComputeBudget111111111111111111111111111111") public enum class Instruction( public val index: UByte, diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Program.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Program.kt index 446b096..7a12f6a 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Program.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Program.kt @@ -7,19 +7,19 @@ import net.avianlabs.solana.domain.core.TransactionInstruction import net.avianlabs.solana.vendor.Sha256 import okio.Buffer -public abstract class Program( - public val programId: PublicKey, -) { +public interface Program { + + public val programId: PublicKey public companion object { - public fun createTransactionInstruction( + internal fun createTransactionInstruction( programId: PublicKey, keys: List, data: ByteArray, ): TransactionInstruction = TransactionInstruction(programId, keys, data) - public fun findProgramAddress( + internal fun findProgramAddress( seeds: List, programId: PublicKey, ): ProgramDerivedAddress { @@ -37,7 +37,7 @@ public abstract class Program( throw Exception("Unable to find a viable program address nonce") } - public fun createProgramAddress(seeds: List, programId: PublicKey): PublicKey { + internal fun createProgramAddress(seeds: List, programId: PublicKey): PublicKey { if (seeds.size > 16) { throw RuntimeException("max seed length exceeded: ${seeds.size}") } @@ -65,5 +65,4 @@ public abstract class Program( return PublicKey(hash) } } - } diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/SystemProgram.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/SystemProgram.kt index 03967f9..f41407a 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/SystemProgram.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/SystemProgram.kt @@ -3,13 +3,13 @@ package net.avianlabs.solana.domain.program import net.avianlabs.solana.domain.core.AccountMeta import net.avianlabs.solana.domain.core.PublicKey import net.avianlabs.solana.domain.core.TransactionInstruction +import net.avianlabs.solana.domain.program.Program.Companion.createTransactionInstruction import okio.Buffer -private val SYSTEM_PROGRAM_ID = PublicKey.fromBase58("11111111111111111111111111111111") +public object SystemProgram : Program { -public object SystemProgram : Program( - programId = SYSTEM_PROGRAM_ID -) { + public override val programId: PublicKey = + PublicKey.fromBase58("11111111111111111111111111111111") public val SYSVAR_RENT_ACCOUNT: PublicKey = PublicKey.fromBase58("SysvarRent111111111111111111111111111111111") diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Token2022Program.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Token2022Program.kt index 80c0f72..1f8d438 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Token2022Program.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/Token2022Program.kt @@ -2,11 +2,11 @@ package net.avianlabs.solana.domain.program import net.avianlabs.solana.domain.core.PublicKey import net.avianlabs.solana.domain.core.TransactionInstruction +import net.avianlabs.solana.domain.program.TokenProgram.Companion.createCloseAccountInstruction +import net.avianlabs.solana.domain.program.TokenProgram.Companion.createTransferCheckedInstruction -private val TOKEN_2022_PROGRAM_ID = - PublicKey.fromBase58("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") -public object Token2022Program : TokenProgramBase(TOKEN_2022_PROGRAM_ID) { +public interface Token2022Program : TokenProgram { public enum class Extensions( public val index: UByte, @@ -27,16 +27,50 @@ public object Token2022Program : TokenProgramBase(TOKEN_2022_PROGRAM_ID) { MetadataPointerExtension(39u), } - public fun createAssociatedTokenAccountInstruction( - mint: PublicKey, - associatedAccount: PublicKey, - owner: PublicKey, - payer: PublicKey, - ): TransactionInstruction = AssociatedTokenProgram.createAssociatedTokenAccountInstruction( - programId = programId, - mint = mint, - associatedAccount = associatedAccount, - owner = owner, - payer = payer, - ) + public companion object : Token2022Program, TokenProgram by TokenProgram.Companion { + + public override val programId: PublicKey = + PublicKey.fromBase58("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") + + @Deprecated( + message = "Token2022Program does not support transfer, use transferChecked instead", + replaceWith = ReplaceWith("transferChecked(source, destination, owner, amount, decimals, mint)"), + level = DeprecationLevel.ERROR, + ) + public override fun transfer( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + ): TransactionInstruction = + error("Token2022Program does not support transfer, use transferChecked instead") + + public override fun closeAccount( + account: PublicKey, + destination: PublicKey, + owner: PublicKey, + ): TransactionInstruction = createCloseAccountInstruction( + account = account, + destination = destination, + owner = owner, + programId = programId, + ) + + public override fun transferChecked( + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + decimals: UByte, + ): TransactionInstruction = createTransferCheckedInstruction( + source = source, + mint = mint, + destination = destination, + owner = owner, + amount = amount, + decimals = decimals, + programId = programId, + ) + } } diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgram.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgram.kt index 1040bb4..c7a9846 100644 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgram.kt +++ b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgram.kt @@ -1,22 +1,169 @@ package net.avianlabs.solana.domain.program +import net.avianlabs.solana.domain.core.AccountMeta import net.avianlabs.solana.domain.core.PublicKey import net.avianlabs.solana.domain.core.TransactionInstruction +import net.avianlabs.solana.domain.program.Program.Companion.createTransactionInstruction +import okio.Buffer -private val TOKEN_PROGRAM_ID = PublicKey.fromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") -public object TokenProgram : TokenProgramBase(TOKEN_PROGRAM_ID) { +public interface TokenProgram : Program { - public fun createAssociatedTokenAccountInstruction( + public enum class Instruction( + public val index: UByte, + ) { + InitializeAccount(1u), + InitializeMultisig(2u), + Transfer(3u), + Approve(4u), + Revoke(5u), + SetAuthority(6u), + MintTo(7u), + Burn(8u), + CloseAccount(9u), + FreezeAccount(10u), + ThawAccount(11u), + TransferChecked(12u), + ApproveChecked(13u), + MintToChecked(14u), + BurnChecked(15u), + InitializeAccount2(16u), + SyncNative(17u), + InitializeAccount3(18u), + InitializeMultisig2(19u), + InitializeMint2(20u), + GetAccountDataSize(21u), + InitializeImmutableOwner(22u), + AmountToUiAmount(23u), + UiAmountToAmount(24u), + InitializeMintCloseAuthority(25u), + ; + } + + public fun transfer( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + ): TransactionInstruction + + public fun closeAccount( + account: PublicKey, + destination: PublicKey, + owner: PublicKey, + ): TransactionInstruction + + public fun transferChecked( + source: PublicKey, mint: PublicKey, - associatedAccount: PublicKey, + destination: PublicKey, owner: PublicKey, - payer: PublicKey, - ): TransactionInstruction = AssociatedTokenProgram.createAssociatedTokenAccountInstruction( - programId = programId, - mint = mint, - associatedAccount = associatedAccount, - owner = owner, - payer = payer, - ) + amount: ULong, + decimals: UByte, + ): TransactionInstruction + + public companion object : TokenProgram { + public override val programId: PublicKey = + PublicKey.fromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + + public override fun transfer( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + ): TransactionInstruction = createTransferInstruction( + source = source, + destination = destination, + owner = owner, + amount = amount, + programId = programId, + ) + + internal fun createTransferInstruction( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + programId: PublicKey + ) = createTransactionInstruction( + programId = programId, + keys = listOf( + AccountMeta(source, isSigner = false, isWritable = true), + AccountMeta(destination, isSigner = false, isWritable = true), + AccountMeta(owner, isSigner = true, isWritable = false), + ), + data = Buffer() + .writeByte(Instruction.Transfer.index.toInt()) + .writeLongLe(amount.toLong()) + .readByteArray(), + ) + + public override fun closeAccount( + account: PublicKey, + destination: PublicKey, + owner: PublicKey, + ): TransactionInstruction = createCloseAccountInstruction( + account = account, + destination = destination, + owner = owner, + programId = programId, + ) + + internal fun createCloseAccountInstruction( + account: PublicKey, + destination: PublicKey, + owner: PublicKey, + programId: PublicKey + ) = createTransactionInstruction( + programId = programId, + keys = listOf( + AccountMeta(account, isSigner = false, isWritable = true), + AccountMeta(destination, isSigner = false, isWritable = true), + AccountMeta(owner, isSigner = false, isWritable = false), + ), + data = Buffer() + .writeByte(Instruction.CloseAccount.index.toInt()) + .readByteArray(), + ) + + public override fun transferChecked( + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + decimals: UByte, + ): TransactionInstruction = createTransferCheckedInstruction( + source = source, + mint = mint, + destination = destination, + owner = owner, + amount = amount, + decimals = decimals, + programId = programId, + ) + + internal fun createTransferCheckedInstruction( + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: ULong, + decimals: UByte, + programId: PublicKey + ) = createTransactionInstruction( + programId = programId, + keys = listOf( + AccountMeta(source, isSigner = false, isWritable = true), + AccountMeta(mint, isSigner = false, isWritable = false), + AccountMeta(destination, isSigner = false, isWritable = true), + AccountMeta(owner, isSigner = true, isWritable = false), + ), + data = Buffer() + .writeByte(Instruction.TransferChecked.index.toInt()) + .writeLongLe(amount.toLong()) + .writeByte(decimals.toInt()) + .readByteArray(), + ) + } } diff --git a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgramBase.kt b/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgramBase.kt deleted file mode 100644 index 448d025..0000000 --- a/solana-kotlin/src/commonMain/kotlin/net/avianlabs/solana/domain/program/TokenProgramBase.kt +++ /dev/null @@ -1,100 +0,0 @@ -package net.avianlabs.solana.domain.program - -import net.avianlabs.solana.domain.core.AccountMeta -import net.avianlabs.solana.domain.core.PublicKey -import net.avianlabs.solana.domain.core.TransactionInstruction -import okio.Buffer - -public abstract class TokenProgramBase internal constructor( - programId: PublicKey, -) : Program( - programId = programId, -) { - - public enum class Instruction( - public val index: UByte, - ) { - InitializeAccount(1u), - InitializeMultisig(2u), - Transfer(3u), - Approve(4u), - Revoke(5u), - SetAuthority(6u), - MintTo(7u), - Burn(8u), - CloseAccount(9u), - FreezeAccount(10u), - ThawAccount(11u), - TransferChecked(12u), - ApproveChecked(13u), - MintToChecked(14u), - BurnChecked(15u), - InitializeAccount2(16u), - SyncNative(17u), - InitializeAccount3(18u), - InitializeMultisig2(19u), - InitializeMint2(20u), - GetAccountDataSize(21u), - InitializeImmutableOwner(22u), - AmountToUiAmount(23u), - UiAmountToAmount(24u), - InitializeMintCloseAuthority(25u), - ; - } - - public fun transfer( - source: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: ULong, - ): TransactionInstruction = createTransactionInstruction( - programId = programId, - keys = listOf( - AccountMeta(source, isSigner = false, isWritable = true), - AccountMeta(destination, isSigner = false, isWritable = true), - AccountMeta(owner, isSigner = true, isWritable = false), - ), - data = Buffer() - .writeByte(Instruction.Transfer.index.toInt()) - .writeLongLe(amount.toLong()) - .readByteArray(), - ) - - public fun closeAccount( - account: PublicKey, - destination: PublicKey, - owner: PublicKey, - ): TransactionInstruction = createTransactionInstruction( - programId = programId, - keys = listOf( - AccountMeta(account, isSigner = false, isWritable = true), - AccountMeta(destination, isSigner = false, isWritable = true), - AccountMeta(owner, isSigner = false, isWritable = false), - ), - data = Buffer() - .writeByte(Instruction.CloseAccount.index.toInt()) - .readByteArray(), - ) - - public fun transferChecked( - source: PublicKey, - mint: PublicKey, - destination: PublicKey, - owner: PublicKey, - amount: ULong, - decimals: UByte, - ): TransactionInstruction = createTransactionInstruction( - programId = programId, - keys = listOf( - AccountMeta(source, isSigner = false, isWritable = true), - AccountMeta(mint, isSigner = false, isWritable = false), - AccountMeta(destination, isSigner = false, isWritable = true), - AccountMeta(owner, isSigner = true, isWritable = false), - ), - data = Buffer() - .writeByte(Instruction.TransferChecked.index.toInt()) - .writeLongLe(amount.toLong()) - .writeByte(decimals.toInt()) - .readByteArray(), - ) -} diff --git a/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/TokenProgramTest.kt b/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/TokenProgramTest.kt new file mode 100644 index 0000000..34b44f2 --- /dev/null +++ b/solana-kotlin/src/commonTest/kotlin/net/avianlabs/solana/domain/program/TokenProgramTest.kt @@ -0,0 +1,42 @@ +package net.avianlabs.solana.domain.program + +import net.avianlabs.solana.domain.core.PublicKey +import kotlin.test.Test +import kotlin.test.assertEquals + +class TokenProgramTest { + + @Test + fun testTokenProgramIds() { + val tokenTransfer = TokenProgram.transferChecked( + source = PublicKey(ByteArray(32)), + destination = PublicKey(ByteArray(32)), + owner = PublicKey(ByteArray(32)), + amount = 0u, + decimals = 0u, + mint = PublicKey(ByteArray(32)), + ) + + assertEquals( + tokenTransfer.programId, + TokenProgram.programId, + ) + } + + @Test + fun testToken2022ProgramIds() { + val token2022Transfer = Token2022Program.transferChecked( + source = PublicKey(ByteArray(32)), + destination = PublicKey(ByteArray(32)), + owner = PublicKey(ByteArray(32)), + amount = 0u, + decimals = 0u, + mint = PublicKey(ByteArray(32)), + ) + + assertEquals( + token2022Transfer.programId, + Token2022Program.programId, + ) + } +}