From 4816e546dd77794ede6e59d020b836072aa9d4b3 Mon Sep 17 00:00:00 2001 From: Marco Martinez Date: Fri, 4 Nov 2022 05:56:09 -0600 Subject: [PATCH] Candy Machine v0.3 - Candy Guards (#76) * new branch w/refactor * use solanakt master branch snapshot * update with solana kt sockets/remove moshi branch * fix up issues from auction house merge, remove deprecations * switch to solana 2.0 release * create guards with groups working * create guard 0.1.1 update * wrap candy guard * remove duplicate code, replace with builder --- .../experimental/jen/candyguard/Accounts.kt | 15 +- .../lib/experimental/jen/candyguard/Errors.kt | 168 ++- .../jen/candyguard/Instructions.kt | 71 +- .../lib/experimental/jen/candyguard/Types.kt | 151 ++- .../lib/experimental/jen/candyguard/idl.kt | 953 ++++++++++++++---- .../lib/extensions/Transaction+Extensions.kt | 9 +- .../candymachines/CandyMachineClient.kt | 57 +- .../CreateCandyGuardTransactionBuilder.kt | 42 + .../CreateCandyMachineTransactionBuilder.kt | 30 +- .../WrapCandyGuardTransactionBuilder.kt | 36 + .../candymachines/models/CandyGuard.kt | 290 ++++++ .../candymachines/models/CandyMachine.kt | 11 +- ...FindCandyGuardByAddressOperationHandler.kt | 40 + ...ndCandyMachineByAddressOperationHandler.kt | 1 + .../candymachine/CandyMachineClientTests.kt | 234 ++++- 15 files changed, 1819 insertions(+), 289 deletions(-) create mode 100644 lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyGuardTransactionBuilder.kt create mode 100644 lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/WrapCandyGuardTransactionBuilder.kt create mode 100644 lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyGuard.kt create mode 100644 lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyGuardByAddressOperationHandler.kt diff --git a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Accounts.kt b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Accounts.kt index 3ec394e..f727997 100644 --- a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Accounts.kt +++ b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Accounts.kt @@ -2,7 +2,7 @@ // Accounts // Metaplex // -// This code was generated locally by Funkatronics on 2022-09-28 +// This code was generated locally by Funkatronics on 2022-11-01 // @file:UseSerializers(PublicKeyAs32ByteSerializer::class) @@ -10,13 +10,22 @@ package com.metaplex.lib.experimental.jen.candyguard import com.metaplex.lib.serialization.serializers.solana.PublicKeyAs32ByteSerializer import com.solana.core.PublicKey +import kotlin.Long import kotlin.UByte -import kotlin.UShort +import kotlin.ULong import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers @Serializable -class MintCounter(val count: UShort) +class FreezeEscrow( + val candyGuard: PublicKey, + val candyMachine: PublicKey, + val frozenCount: ULong, + val firstMintTime: Long?, + val freezePeriod: Long, + val destination: PublicKey, + val authority: PublicKey +) @Serializable class CandyGuard( diff --git a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Errors.kt b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Errors.kt index 53cdaba..0f2f3e0 100644 --- a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Errors.kt +++ b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Errors.kt @@ -2,7 +2,7 @@ // Errors // Metaplex // -// This code was generated locally by Funkatronics on 2022-09-28 +// This code was generated locally by Funkatronics on 2022-11-01 // package com.metaplex.lib.experimental.jen.candyguard @@ -36,7 +36,7 @@ class PublicKeyMismatch : CandyGuardError { class DataIncrementLimitExceeded : CandyGuardError { override val code: Int = 6003 - override val message: String = "Missing expected remaining account" + override val message: String = "Exceeded account increase limit" } class IncorrectOwner : CandyGuardError { @@ -75,94 +75,94 @@ class GroupNotFound : CandyGuardError { override val message: String = "Group not found" } -class LabelExceededLength : CandyGuardError { +class ExceededLength : CandyGuardError { override val code: Int = 6010 - override val message: String = "Group not found" + override val message: String = "Value exceeded maximum length" } -class CollectionKeyMismatch : CandyGuardError { +class CandyMachineEmpty : CandyGuardError { override val code: Int = 6011 - override val message: String = "Collection public key mismatch" + override val message: String = "Candy machine is empty" } -class MissingCollectionAccounts : CandyGuardError { +class InstructionNotFound : CandyGuardError { override val code: Int = 6012 - override val message: String = "Missing collection accounts" + override val message: String = "No instruction was found" } -class CollectionUpdateAuthorityKeyMismatch : CandyGuardError { +class CollectionKeyMismatch : CandyGuardError { override val code: Int = 6013 - override val message: String = "Collection update authority public key mismatch" + override val message: String = "Collection public key mismatch" } -class MintNotLastTransaction : CandyGuardError { +class MissingCollectionAccounts : CandyGuardError { override val code: Int = 6014 - override val message: String = "Mint must be the last instructions of the transaction" + override val message: String = "Missing collection accounts" } -class MintNotLive : CandyGuardError { +class CollectionUpdateAuthorityKeyMismatch : CandyGuardError { override val code: Int = 6015 - override val message: String = "Mint is not live" + override val message: String = "Collection update authority public key mismatch" } -class NotEnoughSOL : CandyGuardError { +class MintNotLastTransaction : CandyGuardError { override val code: Int = 6016 - override val message: String = "Not enough SOL to pay for the mint" + override val message: String = "Mint must be the last instructions of the transaction" } -class TokenTransferFailed : CandyGuardError { +class MintNotLive : CandyGuardError { override val code: Int = 6017 - override val message: String = "Token transfer failed" + override val message: String = "Mint is not live" } -class NotEnoughTokens : CandyGuardError { +class NotEnoughSOL : CandyGuardError { override val code: Int = 6018 - override val message: String = "Not enough tokens to pay for this minting" + override val message: String = "Not enough SOL to pay for the mint" } -class MissingRequiredSignature : CandyGuardError { +class TokenBurnFailed : CandyGuardError { override val code: Int = 6019 - override val message: String = "A signature was required but not found" + override val message: String = "Token burn failed" } -class TokenBurnFailed : CandyGuardError { +class NotEnoughTokens : CandyGuardError { override val code: Int = 6020 - override val message: String = "Token burn failed" + override val message: String = "Not enough tokens on the account" } -class NoWhitelistToken : CandyGuardError { +class TokenTransferFailed : CandyGuardError { override val code: Int = 6021 - override val message: String = "No whitelist token present" + override val message: String = "Token transfer failed" } -class GatewayTokenInvalid : CandyGuardError { +class MissingRequiredSignature : CandyGuardError { override val code: Int = 6022 - override val message: String = "Gateway token is not valid" + override val message: String = "A signature was required but not found" } -class AfterEndSettingsDate : CandyGuardError { +class GatewayTokenInvalid : CandyGuardError { override val code: Int = 6023 - override val message: String = "Current time is after the set end settings date" + override val message: String = "Gateway token is not valid" } -class AfterEndSettingsMintAmount : CandyGuardError { +class AfterEndDate : CandyGuardError { override val code: Int = 6024 - override val message: String = "Current items minted is at the set end settings amount" + override val message: String = "Current time is after the set end date" } class InvalidMintTime : CandyGuardError { @@ -183,14 +183,110 @@ class MissingAllowedListProof : CandyGuardError { override val message: String = "Missing allowed list proof" } -class AllowedMintLimitReached : CandyGuardError { +class AllowedListNotEnabled : CandyGuardError { override val code: Int = 6028 - override val message: String = "The maximum number of allowed mints was reached" + override val message: String = "Allow list guard is not enabled" } -class InvalidNFTCollectionPayment : CandyGuardError { +class AllowedMintLimitReached : CandyGuardError { override val code: Int = 6029 - override val message: String = "Invalid NFT Collection Payment" + override val message: String = "The maximum number of allowed mints was reached" +} + +class InvalidNftCollection : CandyGuardError { + override val code: Int = 6030 + + override val message: String = "Invalid NFT collection" +} + +class MissingNft : CandyGuardError { + override val code: Int = 6031 + + override val message: String = "Missing NFT on the account" +} + +class MaximumRedeemedAmount : CandyGuardError { + override val code: Int = 6032 + + override val message: String = "Current redemeed items is at the set maximum amount" +} + +class AddressNotAuthorized : CandyGuardError { + override val code: Int = 6033 + + override val message: String = "Address not authorized" +} + +class MissingFreezeInstruction : CandyGuardError { + override val code: Int = 6034 + + override val message: String = "Missing freeze instruction data" +} + +class FreezeGuardNotEnabled : CandyGuardError { + override val code: Int = 6035 + + override val message: String = "Freeze guard must be enabled" +} + +class FreezeNotInitialized : CandyGuardError { + override val code: Int = 6036 + + override val message: String = "Freeze must be initialized" +} + +class MissingFreezePeriod : CandyGuardError { + override val code: Int = 6037 + + override val message: String = "Missing freeze period" +} + +class FreezeEscrowAlreadyExists : CandyGuardError { + override val code: Int = 6038 + + override val message: String = "The freeze escrow account already exists" +} + +class ExceededMaximumFreezePeriod : CandyGuardError { + override val code: Int = 6039 + + override val message: String = "Maximum freeze period exceeded" +} + +class ThawNotEnabled : CandyGuardError { + override val code: Int = 6040 + + override val message: String = "Thaw is not enabled" +} + +class UnlockNotEnabled : CandyGuardError { + override val code: Int = 6041 + + override val message: String = "Unlock is not enabled (not all NFTs are thawed)" +} + +class DuplicatedGroupLabel : CandyGuardError { + override val code: Int = 6042 + + override val message: String = "Duplicated group label" +} + +class DuplicatedMintLimitId : CandyGuardError { + override val code: Int = 6043 + + override val message: String = "Duplicated mint limit id" +} + +class UnauthorizedProgramFound : CandyGuardError { + override val code: Int = 6044 + + override val message: String = "An unauthorized program was found in the transaction" +} + +class ExceededProgramListSize : CandyGuardError { + override val code: Int = 6045 + + override val message: String = "Exceeded the maximum number of programs in the additional list" } diff --git a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Instructions.kt b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Instructions.kt index e476ebd..73f4eb2 100644 --- a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Instructions.kt +++ b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Instructions.kt @@ -2,12 +2,13 @@ // Instructions // Metaplex // -// This code was generated locally by Funkatronics on 2022-09-28 +// This code was generated locally by Funkatronics on 2022-11-01 // package com.metaplex.lib.experimental.jen.candyguard import com.metaplex.lib.serialization.format.Borsh import com.metaplex.lib.serialization.serializers.solana.AnchorInstructionSerializer +import com.metaplex.lib.serialization.serializers.solana.PublicKeyAs32ByteSerializer import com.solana.core.AccountMeta import com.solana.core.PublicKey import com.solana.core.TransactionInstruction @@ -22,10 +23,10 @@ object CandyGuardInstructions { authority: PublicKey, payer: PublicKey, systemProgram: PublicKey, - data: CandyGuardData + data: ByteArray ): TransactionInstruction = - TransactionInstruction(PublicKey("CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ"), - listOf(AccountMeta(candyGuard, false, true), AccountMeta(base, true, true), + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), + listOf(AccountMeta(candyGuard, false, true), AccountMeta(base, true, false), AccountMeta(authority, false, false), AccountMeta(payer, true, true), AccountMeta(systemProgram, false, false)), Borsh.encodeToByteArray(AnchorInstructionSerializer("initialize"), @@ -49,13 +50,12 @@ object CandyGuardInstructions { tokenMetadataProgram: PublicKey, tokenProgram: PublicKey, systemProgram: PublicKey, - rent: PublicKey, recentSlothashes: PublicKey, instructionSysvarAccount: PublicKey, mintArgs: ByteArray, label: String? ): TransactionInstruction = - TransactionInstruction(PublicKey("CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ"), + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), listOf(AccountMeta(candyGuard, false, false), AccountMeta(candyMachineProgram, false, false), AccountMeta(candyMachine, false, true), AccountMeta(candyMachineAuthorityPda, false, true), AccountMeta(payer, true, true), AccountMeta(nftMetadata, false, true), @@ -65,22 +65,44 @@ object CandyGuardInstructions { AccountMeta(collectionMetadata, false, true), AccountMeta(collectionMasterEdition, false, false), AccountMeta(collectionUpdateAuthority, false, false), AccountMeta(tokenMetadataProgram, false, false), AccountMeta(tokenProgram, false, - false), AccountMeta(systemProgram, false, false), AccountMeta(rent, false, false), - AccountMeta(recentSlothashes, false, false), AccountMeta(instructionSysvarAccount, - false, false)), Borsh.encodeToByteArray(AnchorInstructionSerializer("mint"), - Args_mint(mintArgs, label))) + false), AccountMeta(systemProgram, false, false), AccountMeta(recentSlothashes, false, + false), AccountMeta(instructionSysvarAccount, false, false)), + Borsh.encodeToByteArray(AnchorInstructionSerializer("mint"), Args_mint(mintArgs, + label))) + + fun route( + candyGuard: PublicKey, + candyMachine: PublicKey, + payer: PublicKey, + args: RouteArgs, + label: String? + ): TransactionInstruction = + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), + listOf(AccountMeta(candyGuard, false, false), AccountMeta(candyMachine, false, true), + AccountMeta(payer, true, true)), + Borsh.encodeToByteArray(AnchorInstructionSerializer("route"), Args_route(args, label))) + + fun setAuthority( + candyGuard: PublicKey, + authority: PublicKey, + newAuthority: PublicKey + ): TransactionInstruction = + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), + listOf(AccountMeta(candyGuard, false, true), AccountMeta(authority, true, false)), + Borsh.encodeToByteArray(AnchorInstructionSerializer("set_authority"), + Args_setAuthority(newAuthority))) fun unwrap( candyGuard: PublicKey, authority: PublicKey, candyMachine: PublicKey, - candyMachineProgram: PublicKey, - candyMachineAuthority: PublicKey + candyMachineAuthority: PublicKey, + candyMachineProgram: PublicKey ): TransactionInstruction = - TransactionInstruction(PublicKey("CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ"), + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), listOf(AccountMeta(candyGuard, false, false), AccountMeta(authority, true, false), - AccountMeta(candyMachine, false, true), AccountMeta(candyMachineProgram, false, false), - AccountMeta(candyMachineAuthority, true, false)), + AccountMeta(candyMachine, false, true), AccountMeta(candyMachineAuthority, true, false), + AccountMeta(candyMachineProgram, false, false)), Borsh.encodeToByteArray(AnchorInstructionSerializer("unwrap"), Args_unwrap())) fun update( @@ -88,15 +110,15 @@ object CandyGuardInstructions { authority: PublicKey, payer: PublicKey, systemProgram: PublicKey, - data: CandyGuardData + data: ByteArray ): TransactionInstruction = - TransactionInstruction(PublicKey("CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ"), + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), listOf(AccountMeta(candyGuard, false, true), AccountMeta(authority, true, false), AccountMeta(payer, true, false), AccountMeta(systemProgram, false, false)), Borsh.encodeToByteArray(AnchorInstructionSerializer("update"), Args_update(data))) fun withdraw(candyGuard: PublicKey, authority: PublicKey): TransactionInstruction = - TransactionInstruction(PublicKey("CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ"), + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), listOf(AccountMeta(candyGuard, false, true), AccountMeta(authority, true, true)), Borsh.encodeToByteArray(AnchorInstructionSerializer("withdraw"), Args_withdraw())) @@ -107,23 +129,30 @@ object CandyGuardInstructions { candyMachineProgram: PublicKey, candyMachineAuthority: PublicKey ): TransactionInstruction = - TransactionInstruction(PublicKey("CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ"), + TransactionInstruction(PublicKey("Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g"), listOf(AccountMeta(candyGuard, false, false), AccountMeta(authority, true, false), AccountMeta(candyMachine, false, true), AccountMeta(candyMachineProgram, false, false), AccountMeta(candyMachineAuthority, true, false)), Borsh.encodeToByteArray(AnchorInstructionSerializer("wrap"), Args_wrap())) @Serializable - class Args_initialize(val data: CandyGuardData) + class Args_initialize(val data: ByteArray) @Serializable class Args_mint(val mintArgs: ByteArray, val label: String?) + @Serializable + class Args_route(val args: RouteArgs, val label: String?) + + @Serializable + class Args_setAuthority(@Serializable(with = PublicKeyAs32ByteSerializer::class) val + newAuthority: PublicKey) + @Serializable class Args_unwrap() @Serializable - class Args_update(val data: CandyGuardData) + class Args_update(val data: ByteArray) @Serializable class Args_withdraw() diff --git a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Types.kt b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Types.kt index 6c9fcff..0a73b5f 100644 --- a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Types.kt +++ b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/Types.kt @@ -2,15 +2,17 @@ // Types // Metaplex // -// This code was generated locally by Funkatronics on 2022-09-28 +// This code was generated locally by Funkatronics on 2022-11-01 // -@file:UseSerializers(PublicKeyAs32ByteSerializer::class) +@file:UseSerializers(PublicKeyAs32ByteSerializer::class, AllowListSerializer::class) package com.metaplex.lib.experimental.jen.candyguard import com.metaplex.lib.serialization.serializers.solana.PublicKeyAs32ByteSerializer import com.solana.core.PublicKey +import kotlinx.serialization.KSerializer import kotlin.Boolean +import kotlin.ByteArray import kotlin.Long import kotlin.String import kotlin.UByte @@ -19,49 +21,98 @@ import kotlin.UShort import kotlin.collections.List import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder @Serializable +data class AddressGate(val address: PublicKey) + data class AllowList(val merkleRoot: List) +object AllowListSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AllowList") + + override fun deserialize(decoder: Decoder): AllowList = + AllowList(List(32) { + decoder.decodeByte().toUByte() + }) + + override fun serialize(encoder: Encoder, value: AllowList) = + value.merkleRoot.take(32).forEach { + encoder.encodeByte(it.toByte()) + } +} + +@Serializable +data class AllowListProof(val timestamp: Long) + @Serializable data class BotTax(val lamports: ULong, val lastInstruction: Boolean) @Serializable -data class EndSettings(val endSettingType: EndSettingType, val number: ULong) +data class EndDate(val date: Long) @Serializable -data class Gatekeeper(val gatekeeperNetwork: PublicKey, val expireOnUse: Boolean) +data class FreezeSolPayment(val lamports: ULong, val destination: PublicKey) @Serializable -data class Lamports(val amount: ULong, val destination: PublicKey) +data class FreezeTokenPayment( + val amount: ULong, + val mint: PublicKey, + val destinationAta: PublicKey +) @Serializable -data class LiveDate(val date: Long?) +data class Gatekeeper(val gatekeeperNetwork: PublicKey, val expireOnUse: Boolean) @Serializable data class MintLimit(val id: UByte, val limit: UShort) @Serializable -data class NftPayment(val burn: Boolean, val requiredCollection: PublicKey) +data class MintCounter(val count: UShort) @Serializable -data class SplToken( - val amount: ULong, - val tokenMint: PublicKey, - val destinationAta: PublicKey -) +data class NftBurn(val requiredCollection: PublicKey) + +@Serializable +data class NftGate(val requiredCollection: PublicKey) + +@Serializable +data class NftPayment(val requiredCollection: PublicKey, val destination: PublicKey) + +@Serializable +data class ProgramGate(val additional: List) + +@Serializable +data class RedeemedAmount(val maximum: ULong) + +@Serializable +data class SolPayment(val lamports: ULong, val destination: PublicKey) + +@Serializable +data class StartDate(val date: Long) @Serializable data class ThirdPartySigner(val signerKey: PublicKey) @Serializable -data class Whitelist( +data class TokenBurn(val amount: ULong, val mint: PublicKey) + +@Serializable +data class TokenGate(val amount: ULong, val mint: PublicKey) + +@Serializable +data class TokenPayment( + val amount: ULong, val mint: PublicKey, - val presale: Boolean, - val discountPrice: ULong?, - val mode: WhitelistTokenMode + val destinationAta: PublicKey ) +@Serializable +data class RouteArgs(val guard: GuardType, val data: ByteArray) + @Serializable data class CandyGuardData(val default: GuardSet, val groups: List?) @@ -71,26 +122,70 @@ data class Group(val label: String, val guards: GuardSet) @Serializable data class GuardSet( val botTax: BotTax?, - val lamports: Lamports?, - val splToken: SplToken?, - val liveDate: LiveDate?, + val solPayment: SolPayment?, + val tokenPayment: TokenPayment?, + val startDate: StartDate?, val thirdPartySigner: ThirdPartySigner?, - val whitelist: Whitelist?, + val tokenGate: TokenGate?, val gatekeeper: Gatekeeper?, - val endSettings: EndSettings?, + val endDate: EndDate?, val allowList: AllowList?, val mintLimit: MintLimit?, - val nftPayment: NftPayment? + val nftPayment: NftPayment?, + val redeemedAmount: RedeemedAmount?, + val addressGate: AddressGate?, + val nftGate: NftGate?, + val nftBurn: NftBurn?, + val tokenBurn: TokenBurn?, + val freezeSolPayment: FreezeSolPayment?, + val freezeTokenPayment: FreezeTokenPayment?, + val programGate: ProgramGate? ) -enum class EndSettingType { - Date, +enum class FreezeInstruction { + Initialize, - Amount + Thaw, + + UnlockFunds } -enum class WhitelistTokenMode { - BurnEveryTime, +enum class GuardType { + BotTax, + + SolPayment, + + TokenPayment, + + StartDate, + + ThirdPartySigner, + + TokenGate, + + Gatekeeper, + + EndDate, + + AllowList, + + MintLimit, + + NftPayment, + + RedeemedAmount, + + AddressGate, + + NftGate, + + NftBurn, + + TokenBurn, + + FreezeSolPayment, + + FreezeTokenPayment, - NeverBurn + ProgramGate } diff --git a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/idl.kt b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/idl.kt index 4862e48..5ad7af5 100644 --- a/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/idl.kt +++ b/lib/src/main/java/com/metaplex/lib/experimental/jen/candyguard/idl.kt @@ -9,7 +9,7 @@ package com.metaplex.lib.experimental.jen.candyguard val candyGuardJson = """ { - "version": "0.0.1", + "version": "0.1.1", "name": "candy_guard", "instructions": [ { @@ -25,7 +25,7 @@ val candyGuardJson = """ }, { "name": "base", - "isMut": true, + "isMut": false, "isSigner": true }, { @@ -47,9 +47,7 @@ val candyGuardJson = """ "args": [ { "name": "data", - "type": { - "defined": "CandyGuardData" - } + "type": "bytes" } ] }, @@ -144,11 +142,6 @@ val candyGuardJson = """ "isMut": false, "isSigner": false }, - { - "name": "rent", - "isMut": false, - "isSigner": false - }, { "name": "recentSlothashes", "isMut": false, @@ -173,6 +166,67 @@ val candyGuardJson = """ } ] }, + { + "name": "route", + "docs": [ + "Route the transaction to a guard instruction." + ], + "accounts": [ + { + "name": "candyGuard", + "isMut": false, + "isSigner": false + }, + { + "name": "candyMachine", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "RouteArgs" + } + }, + { + "name": "label", + "type": { + "option": "string" + } + } + ] + }, + { + "name": "setAuthority", + "docs": [ + "Set a new authority of the candy guard." + ], + "accounts": [ + { + "name": "candyGuard", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "newAuthority", + "type": "publicKey" + } + ] + }, { "name": "unwrap", "docs": [ @@ -196,14 +250,14 @@ val candyGuardJson = """ "isSigner": false }, { - "name": "candyMachineProgram", + "name": "candyMachineAuthority", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "candyMachineAuthority", + "name": "candyMachineProgram", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] @@ -238,9 +292,7 @@ val candyGuardJson = """ "args": [ { "name": "data", - "type": { - "defined": "CandyGuardData" - } + "type": "bytes" } ] }, @@ -301,16 +353,67 @@ val candyGuardJson = """ ], "accounts": [ { - "name": "MintCounter", + "name": "FreezeEscrow", "docs": [ - "PDA to track the number of mints for an individual address." + "PDA to store the frozen funds." ], "type": { "kind": "struct", "fields": [ { - "name": "count", - "type": "u16" + "name": "candyGuard", + "docs": [ + "Candy guard address associated with this escrow." + ], + "type": "publicKey" + }, + { + "name": "candyMachine", + "docs": [ + "Candy machine address associated with this escrow." + ], + "type": "publicKey" + }, + { + "name": "frozenCount", + "docs": [ + "Number of NFTs frozen." + ], + "type": "u64" + }, + { + "name": "firstMintTime", + "docs": [ + "The timestamp of the first (frozen) mint. This is used to calculate", + "when the freeze period is over." + ], + "type": { + "option": "i64" + } + }, + { + "name": "freezePeriod", + "docs": [ + "The amount of time (in seconds) for the freeze. The NFTs will be", + "allowed to thaw after this." + ], + "type": "i64" + }, + { + "name": "destination", + "docs": [ + "The destination address for the frozed fund to go to." + ], + "type": "publicKey" + }, + { + "name": "authority", + "docs": [ + "The authority that initialized the freeze. This will be the only", + "address able to unlock the funds in case the candy guard account is", + "closed." + ], + "type": "publicKey" } ] } @@ -337,10 +440,30 @@ val candyGuardJson = """ } ], "types": [ + { + "name": "AddressGate", + "docs": [ + "Guard that restricts access to a specific address." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "publicKey" + } + ] + } + }, { "name": "AllowList", "docs": [ - "Configurations options for allow list." + "Guard that uses a merkle tree to specify the addresses allowed to mint.", + "", + "List of accounts required:", + "", + "0. `[]` Pda created by the merkle proof instruction (seeds `[\"allow_list\", merke tree root,", + "payer key, candy guard pubkey, candy machine pubkey]`)." ], "type": { "kind": "struct", @@ -360,8 +483,32 @@ val candyGuardJson = """ ] } }, + { + "name": "AllowListProof", + "docs": [ + "PDA to track whether an address has been validated or not." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, { "name": "BotTax", + "docs": [ + "Guard is used to:", + "* charge a penalty for invalid transactions", + "* validate that the mint transaction is the last transaction", + "* verify that only authorized programs have instructions", + "", + "The `bot_tax` is applied to any error that occurs during the", + "validation of the guards." + ], "type": { "kind": "struct", "fields": [ @@ -377,54 +524,61 @@ val candyGuardJson = """ } }, { - "name": "EndSettings", + "name": "EndDate", "docs": [ - "Configurations options for end settings." + "Guard that sets a specific date for the mint to stop." ], "type": { "kind": "struct", "fields": [ { - "name": "endSettingType", - "type": { - "defined": "EndSettingType" - } - }, - { - "name": "number", - "type": "u64" + "name": "date", + "type": "i64" } ] } }, { - "name": "Gatekeeper", + "name": "FreezeSolPayment", "docs": [ - "Configurations options for the gatekeeper." + "Guard that charges an amount in SOL (lamports) for the mint with a freeze period.", + "", + "List of accounts required:", + "", + "0. `[writable]` Freeze PDA to receive the funds (seeds `[\"freeze_escrow\",", + "destination pubkey, candy guard pubkey, candy machine pubkey]`).", + "1. `[]` Associate token account of the NFT (seeds `[payer pubkey, token", + "program pubkey, nft mint pubkey]`)." ], "type": { "kind": "struct", "fields": [ { - "name": "gatekeeperNetwork", - "docs": [ - "The network for the gateway token required" - ], - "type": "publicKey" + "name": "lamports", + "type": "u64" }, { - "name": "expireOnUse", - "docs": [ - "Whether or not the token should expire after minting.", - "The gatekeeper network must support this if true." - ], - "type": "bool" + "name": "destination", + "type": "publicKey" } ] } }, { - "name": "Lamports", + "name": "FreezeTokenPayment", + "docs": [ + "Guard that charges an amount in a specified spl-token as payment for the mint with a freeze period.", + "", + "List of accounts required:", + "", + "0. `[writable]` Freeze PDA to receive the funds (seeds `[\"freeze_escrow\",", + "destination_ata pubkey, candy guard pubkey, candy machine pubkey]`).", + "1. `[]` Associate token account of the NFT (seeds `[payer pubkey, token", + "program pubkey, nft mint pubkey]`).", + "2. `[writable]` Token account holding the required amount.", + "3. `[writable]` Associate token account of the Freeze PDA (seeds `[freeze PDA", + "pubkey, token program pubkey, nft mint pubkey]`)." + ], "type": { "kind": "struct", "fields": [ @@ -433,22 +587,45 @@ val candyGuardJson = """ "type": "u64" }, { - "name": "destination", + "name": "mint", + "type": "publicKey" + }, + { + "name": "destinationAta", "type": "publicKey" } ] } }, { - "name": "LiveDate", + "name": "Gatekeeper", + "docs": [ + "Guard that validates if the payer of the transaction has a token from a specified", + "gateway network — in most cases, a token after completing a captcha challenge.", + "", + "List of accounts required:", + "", + "0. `[writeable]` Gatekeeper token account.", + "1. `[]` Gatekeeper program account.", + "2. `[]` Gatekeeper expire account." + ], "type": { "kind": "struct", "fields": [ { - "name": "date", - "type": { - "option": "i64" - } + "name": "gatekeeperNetwork", + "docs": [ + "The network for the gateway token required" + ], + "type": "publicKey" + }, + { + "name": "expireOnUse", + "docs": [ + "Whether or not the token should expire after minting.", + "The gatekeeper network must support this if true." + ], + "type": "bool" } ] } @@ -456,7 +633,13 @@ val candyGuardJson = """ { "name": "MintLimit", "docs": [ - "Configurations options for mint limit." + "Gaurd to set a limit of mints per wallet.", + "", + "List of accounts required:", + "", + "0. `[writable]` Mint counter PDA. The PDA is derived", + "using the seed `[\"mint_limit\", mint guard id, payer key,", + "candy guard pubkey, candy machine pubkey]`." ], "type": { "kind": "struct", @@ -479,121 +662,326 @@ val candyGuardJson = """ } }, { - "name": "NftPayment", + "name": "MintCounter", + "docs": [ + "PDA to track the number of mints for an individual address." + ], "type": { "kind": "struct", "fields": [ { - "name": "burn", - "type": "bool" - }, - { - "name": "requiredCollection", - "type": "publicKey" + "name": "count", + "type": "u16" } ] } }, { - "name": "SplToken", + "name": "NftBurn", + "docs": [ + "Guard that requires another NFT (token) from a specific collection to be burned.", + "", + "List of accounts required:", + "", + "0. `[writeable]` Token account of the NFT.", + "1. `[writeable]` Metadata account of the NFT.", + "2. `[writeable]` Master Edition account of the NFT.", + "3. `[writeable]` Mint account of the NFT.", + "4. `[writeable]` Collection metadata account of the NFT." + ], "type": { "kind": "struct", "fields": [ { - "name": "amount", - "type": "u64" - }, - { - "name": "tokenMint", - "type": "publicKey" - }, - { - "name": "destinationAta", + "name": "requiredCollection", "type": "publicKey" } ] } }, { - "name": "ThirdPartySigner", + "name": "NftGate", + "docs": [ + "Guard that restricts the transaction to holders of a specified collection.", + "", + "List of accounts required:", + "", + "0. `[]` Token account of the NFT.", + "1. `[]` Metadata account of the NFT." + ], "type": { "kind": "struct", "fields": [ { - "name": "signerKey", + "name": "requiredCollection", "type": "publicKey" } ] } }, { - "name": "Whitelist", + "name": "NftPayment", + "docs": [ + "Guard that charges another NFT (token) from a specific collection as payment", + "for the mint.", + "", + "List of accounts required:", + "", + "0. `[writeable]` Token account of the NFT.", + "1. `[writeable]` Metadata account of the NFT.", + "2. `[]` Mint account of the NFT.", + "3. `[]` Account to receive the NFT.", + "4. `[writeable]` Destination PDA key (seeds [destination pubkey, token program id, nft mint pubkey]).", + "5. `[]` spl-associate-token program ID." + ], "type": { "kind": "struct", "fields": [ { - "name": "mint", + "name": "requiredCollection", "type": "publicKey" }, { - "name": "presale", - "type": "bool" - }, - { - "name": "discountPrice", - "type": { - "option": "u64" - } - }, - { - "name": "mode", - "type": { - "defined": "WhitelistTokenMode" - } + "name": "destination", + "type": "publicKey" } ] } }, { - "name": "CandyGuardData", + "name": "ProgramGate", + "docs": [ + "Guard that restricts the programs that can be in a mint transaction. The guard allows the", + "necessary programs for the mint and any other program specified in the configuration." + ], "type": { "kind": "struct", "fields": [ { - "name": "default", - "type": { - "defined": "GuardSet" - } - }, - { - "name": "groups", + "name": "additional", "type": { - "option": { - "vec": { - "defined": "Group" - } - } + "vec": "publicKey" } } ] } }, { - "name": "Group", + "name": "RedeemedAmount", "docs": [ - "A group represent a specific set of guards. When groups are used, transactions", - "must specify which group should be used during validation." + "Guard that stop the mint once the specified amount of items", + "redeenmed is reached." ], "type": { "kind": "struct", "fields": [ { - "name": "label", - "type": "string" - }, - { - "name": "guards", - "type": { + "name": "maximum", + "type": "u64" + } + ] + } + }, + { + "name": "SolPayment", + "docs": [ + "Guard that charges an amount in SOL (lamports) for the mint.", + "", + "List of accounts required:", + "", + "0. `[]` Account to receive the funds." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "lamports", + "type": "u64" + }, + { + "name": "destination", + "type": "publicKey" + } + ] + } + }, + { + "name": "StartDate", + "docs": [ + "Guard that sets a specific start date for the mint." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "date", + "type": "i64" + } + ] + } + }, + { + "name": "ThirdPartySigner", + "docs": [ + "Guard that requires a specified signer to validate the transaction.", + "", + "List of accounts required:", + "", + "0. `[signer]` Signer of the transaction." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "signerKey", + "type": "publicKey" + } + ] + } + }, + { + "name": "TokenBurn", + "docs": [ + "Guard that requires addresses that hold an amount of a specified spl-token", + "and burns them.", + "", + "List of accounts required:", + "", + "0. `[writable]` Token account holding the required amount.", + "1. `[writable]` Token mint account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "mint", + "type": "publicKey" + } + ] + } + }, + { + "name": "TokenGate", + "docs": [ + "Guard that restricts access to addresses that hold the specified spl-token.", + "", + "List of accounts required:", + "", + "0. `[]` Token account holding the required amount." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "mint", + "type": "publicKey" + } + ] + } + }, + { + "name": "TokenPayment", + "docs": [ + "Guard that charges an amount in a specified spl-token as payment for the mint.", + "", + "List of accounts required:", + "", + "0. `[writable]` Token account holding the required amount.", + "1. `[writable]` Address of the ATA to receive the tokens." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "destinationAta", + "type": "publicKey" + } + ] + } + }, + { + "name": "RouteArgs", + "docs": [ + "Arguments for a route transaction." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "guard", + "docs": [ + "The target guard type." + ], + "type": { + "defined": "GuardType" + } + }, + { + "name": "data", + "docs": [ + "Arguments for the guard instruction." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "CandyGuardData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "default", + "type": { + "defined": "GuardSet" + } + }, + { + "name": "groups", + "type": { + "option": { + "vec": { + "defined": "Group" + } + } + } + } + ] + } + }, + { + "name": "Group", + "docs": [ + "A group represent a specific set of guards. When groups are used, transactions", + "must specify which group should be used during validation." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "label", + "type": "string" + }, + { + "name": "guards", + "type": { "defined": "GuardSet" } } @@ -620,42 +1008,42 @@ val candyGuardJson = """ } }, { - "name": "lamports", + "name": "solPayment", "docs": [ - "Lamports guard (set the price for the mint in lamports)." + "Sol payment guard (set the price for the mint in lamports)." ], "type": { "option": { - "defined": "Lamports" + "defined": "SolPayment" } } }, { - "name": "splToken", + "name": "tokenPayment", "docs": [ - "Spl-token guard (set the price for the mint in spl-token amount)." + "Token payment guard (set the price for the mint in spl-token amount)." ], "type": { "option": { - "defined": "SplToken" + "defined": "TokenPayment" } } }, { - "name": "liveDate", + "name": "startDate", "docs": [ - "Live data guard (controls when minting is allowed)." + "Start data guard (controls when minting is allowed)." ], "type": { "option": { - "defined": "LiveDate" + "defined": "StartDate" } } }, { "name": "thirdPartySigner", "docs": [ - "Third party signer guard." + "Third party signer guard (requires an extra signer for the transaction)." ], "type": { "option": { @@ -664,20 +1052,20 @@ val candyGuardJson = """ } }, { - "name": "whitelist", + "name": "tokenGate", "docs": [ - "Whitelist guard (whitelist mint settings)." + "Token gate guard (restrict access to holders of a specific token)." ], "type": { "option": { - "defined": "Whitelist" + "defined": "TokenGate" } } }, { "name": "gatekeeper", "docs": [ - "Gatekeeper guard" + "Gatekeeper guard (captcha challenge)." ], "type": { "option": { @@ -686,20 +1074,20 @@ val candyGuardJson = """ } }, { - "name": "endSettings", + "name": "endDate", "docs": [ - "End settings guard" + "End date guard (set an end date to stop the mint)." ], "type": { "option": { - "defined": "EndSettings" + "defined": "EndDate" } } }, { "name": "allowList", "docs": [ - "Allow list guard" + "Allow list guard (curated list of allowed addresses)." ], "type": { "option": { @@ -710,7 +1098,7 @@ val candyGuardJson = """ { "name": "mintLimit", "docs": [ - "Mint limit guard" + "Mint limit guard (add a limit on the number of mints per wallet)." ], "type": { "option": { @@ -721,41 +1109,186 @@ val candyGuardJson = """ { "name": "nftPayment", "docs": [ - "NFT Payment" + "NFT Payment (charge an NFT in order to mint)." ], "type": { "option": { "defined": "NftPayment" } } + }, + { + "name": "redeemedAmount", + "docs": [ + "Redeemed amount guard (add a limit on the overall number of items minted)." + ], + "type": { + "option": { + "defined": "RedeemedAmount" + } + } + }, + { + "name": "addressGate", + "docs": [ + "Address gate (check access against a specified address)." + ], + "type": { + "option": { + "defined": "AddressGate" + } + } + }, + { + "name": "nftGate", + "docs": [ + "NFT gate guard (check access based on holding a specified NFT)." + ], + "type": { + "option": { + "defined": "NftGate" + } + } + }, + { + "name": "nftBurn", + "docs": [ + "NFT burn guard (burn a specified NFT)." + ], + "type": { + "option": { + "defined": "NftBurn" + } + } + }, + { + "name": "tokenBurn", + "docs": [ + "Token burn guard (burn a specified amount of spl-token)." + ], + "type": { + "option": { + "defined": "TokenBurn" + } + } + }, + { + "name": "freezeSolPayment", + "docs": [ + "Freeze sol payment guard (set the price for the mint in lamports with a freeze period)." + ], + "type": { + "option": { + "defined": "FreezeSolPayment" + } + } + }, + { + "name": "freezeTokenPayment", + "docs": [ + "Freeze token payment guard (set the price for the mint in spl-token amount with a freeze period)." + ], + "type": { + "option": { + "defined": "FreezeTokenPayment" + } + } + }, + { + "name": "programGate", + "docs": [ + "Program gate guard (restricts the programs that can be in a mint transaction)." + ], + "type": { + "option": { + "defined": "ProgramGate" + } + } } ] } }, { - "name": "EndSettingType", + "name": "FreezeInstruction", "type": { "kind": "enum", "variants": [ { - "name": "Date" + "name": "Initialize" }, { - "name": "Amount" + "name": "Thaw" + }, + { + "name": "UnlockFunds" } ] } }, { - "name": "WhitelistTokenMode", + "name": "GuardType", + "docs": [ + "Available guard types." + ], "type": { "kind": "enum", "variants": [ { - "name": "BurnEveryTime" + "name": "BotTax" + }, + { + "name": "SolPayment" + }, + { + "name": "TokenPayment" + }, + { + "name": "StartDate" + }, + { + "name": "ThirdPartySigner" + }, + { + "name": "TokenGate" + }, + { + "name": "Gatekeeper" + }, + { + "name": "EndDate" + }, + { + "name": "AllowList" + }, + { + "name": "MintLimit" + }, + { + "name": "NftPayment" + }, + { + "name": "RedeemedAmount" + }, + { + "name": "AddressGate" + }, + { + "name": "NftGate" + }, + { + "name": "NftBurn" }, { - "name": "NeverBurn" + "name": "TokenBurn" + }, + { + "name": "FreezeSolPayment" + }, + { + "name": "FreezeTokenPayment" + }, + { + "name": "ProgramGate" } ] } @@ -780,7 +1313,7 @@ val candyGuardJson = """ { "code": 6003, "name": "DataIncrementLimitExceeded", - "msg": "Missing expected remaining account" + "msg": "Exceeded account increase limit" }, { "code": 6004, @@ -814,78 +1347,78 @@ val candyGuardJson = """ }, { "code": 6010, - "name": "LabelExceededLength", - "msg": "Group not found" + "name": "ExceededLength", + "msg": "Value exceeded maximum length" }, { "code": 6011, + "name": "CandyMachineEmpty", + "msg": "Candy machine is empty" + }, + { + "code": 6012, + "name": "InstructionNotFound", + "msg": "No instruction was found" + }, + { + "code": 6013, "name": "CollectionKeyMismatch", "msg": "Collection public key mismatch" }, { - "code": 6012, + "code": 6014, "name": "MissingCollectionAccounts", "msg": "Missing collection accounts" }, { - "code": 6013, + "code": 6015, "name": "CollectionUpdateAuthorityKeyMismatch", "msg": "Collection update authority public key mismatch" }, { - "code": 6014, + "code": 6016, "name": "MintNotLastTransaction", "msg": "Mint must be the last instructions of the transaction" }, { - "code": 6015, + "code": 6017, "name": "MintNotLive", "msg": "Mint is not live" }, { - "code": 6016, + "code": 6018, "name": "NotEnoughSOL", "msg": "Not enough SOL to pay for the mint" }, - { - "code": 6017, - "name": "TokenTransferFailed", - "msg": "Token transfer failed" - }, - { - "code": 6018, - "name": "NotEnoughTokens", - "msg": "Not enough tokens to pay for this minting" - }, { "code": 6019, - "name": "MissingRequiredSignature", - "msg": "A signature was required but not found" + "name": "TokenBurnFailed", + "msg": "Token burn failed" }, { "code": 6020, - "name": "TokenBurnFailed", - "msg": "Token burn failed" + "name": "NotEnoughTokens", + "msg": "Not enough tokens on the account" }, { "code": 6021, - "name": "NoWhitelistToken", - "msg": "No whitelist token present" + "name": "TokenTransferFailed", + "msg": "Token transfer failed" }, { "code": 6022, - "name": "GatewayTokenInvalid", - "msg": "Gateway token is not valid" + "name": "MissingRequiredSignature", + "msg": "A signature was required but not found" }, { "code": 6023, - "name": "AfterEndSettingsDate", - "msg": "Current time is after the set end settings date" + "name": "GatewayTokenInvalid", + "msg": "Gateway token is not valid" }, { "code": 6024, - "name": "AfterEndSettingsMintAmount", - "msg": "Current items minted is at the set end settings amount" + "name": "AfterEndDate", + "msg": "Current time is after the set end date" }, { "code": 6025, @@ -904,17 +1437,97 @@ val candyGuardJson = """ }, { "code": 6028, + "name": "AllowedListNotEnabled", + "msg": "Allow list guard is not enabled" + }, + { + "code": 6029, "name": "AllowedMintLimitReached", "msg": "The maximum number of allowed mints was reached" }, { - "code": 6029, - "name": "InvalidNFTCollectionPayment", - "msg": "Invalid NFT Collection Payment" + "code": 6030, + "name": "InvalidNftCollection", + "msg": "Invalid NFT collection" + }, + { + "code": 6031, + "name": "MissingNft", + "msg": "Missing NFT on the account" + }, + { + "code": 6032, + "name": "MaximumRedeemedAmount", + "msg": "Current redemeed items is at the set maximum amount" + }, + { + "code": 6033, + "name": "AddressNotAuthorized", + "msg": "Address not authorized" + }, + { + "code": 6034, + "name": "MissingFreezeInstruction", + "msg": "Missing freeze instruction data" + }, + { + "code": 6035, + "name": "FreezeGuardNotEnabled", + "msg": "Freeze guard must be enabled" + }, + { + "code": 6036, + "name": "FreezeNotInitialized", + "msg": "Freeze must be initialized" + }, + { + "code": 6037, + "name": "MissingFreezePeriod", + "msg": "Missing freeze period" + }, + { + "code": 6038, + "name": "FreezeEscrowAlreadyExists", + "msg": "The freeze escrow account already exists" + }, + { + "code": 6039, + "name": "ExceededMaximumFreezePeriod", + "msg": "Maximum freeze period exceeded" + }, + { + "code": 6040, + "name": "ThawNotEnabled", + "msg": "Thaw is not enabled" + }, + { + "code": 6041, + "name": "UnlockNotEnabled", + "msg": "Unlock is not enabled (not all NFTs are thawed)" + }, + { + "code": 6042, + "name": "DuplicatedGroupLabel", + "msg": "Duplicated group label" + }, + { + "code": 6043, + "name": "DuplicatedMintLimitId", + "msg": "Duplicated mint limit id" + }, + { + "code": 6044, + "name": "UnauthorizedProgramFound", + "msg": "An unauthorized program was found in the transaction" + }, + { + "code": 6045, + "name": "ExceededProgramListSize", + "msg": "Exceeded the maximum number of programs in the additional list" } ], "metadata": { - "address": "CnDYGRdU51FsSyLnVgSd19MCFxA4YHT5h3nacvCKMPUJ", + "address": "Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g", "origin": "anchor", "binaryVersion": "0.25.0", "libVersion": "0.25.0" diff --git a/lib/src/main/java/com/metaplex/lib/extensions/Transaction+Extensions.kt b/lib/src/main/java/com/metaplex/lib/extensions/Transaction+Extensions.kt index 236ee54..e7f8f3f 100644 --- a/lib/src/main/java/com/metaplex/lib/extensions/Transaction+Extensions.kt +++ b/lib/src/main/java/com/metaplex/lib/extensions/Transaction+Extensions.kt @@ -10,6 +10,7 @@ package com.metaplex.lib.extensions import android.util.Base64 import com.metaplex.lib.drivers.indenty.IdentityDriver import com.metaplex.lib.drivers.solana.* +import com.solana.core.Account import com.solana.core.HotAccount import com.solana.core.Transaction import com.solana.vendor.ShortvecEncoding @@ -24,7 +25,7 @@ import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible suspend fun Transaction.sign(connection: Connection, payer: IdentityDriver, - additionalSigners: List = listOf()): Result { + additionalSigners: List = listOf()): Result { // set block hash setRecentBlockHash(connection.getRecentBlockhash().getOrElse { @@ -51,7 +52,7 @@ suspend fun Transaction.sign(connection: Connection, payer: IdentityDriver, } suspend fun Transaction.signAndSend(connection: Connection, payer: IdentityDriver, - additionalSigners: List = listOf()): Result { + additionalSigners: List = listOf()): Result { val signedTxn: Transaction = sign(connection, payer, additionalSigners).getOrElse { return Result.failure(it) @@ -65,7 +66,7 @@ suspend fun Transaction.signAndSend(connection: Connection, payer: IdentityDrive ) } -suspend fun Transaction.signAndSend(connection: Connection, signers: List = listOf(), +suspend fun Transaction.signAndSend(connection: Connection, signers: List = listOf(), recentBlockhash: String? = null): Result { // set block hash @@ -81,7 +82,7 @@ suspend fun Transaction.signAndSend(connection: Connection, signers: List = listOf(), + connection: Connection, payer: IdentityDriver, additionalSigners: List = listOf(), transactionOptions: TransactionOptions = connection.transactionOptions ): Result = signAndSend(connection, payer, additionalSigners) .confirmTransaction(connection, transactionOptions) diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/CandyMachineClient.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/CandyMachineClient.kt index 6114d6e..086909e 100644 --- a/lib/src/main/java/com/metaplex/lib/modules/candymachines/CandyMachineClient.kt +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/CandyMachineClient.kt @@ -13,19 +13,18 @@ import com.metaplex.lib.drivers.solana.TransactionOptions import com.metaplex.lib.experimental.jen.candymachine.ConfigLine import com.metaplex.lib.experimental.jen.candymachine.ConfigLineSettings import com.metaplex.lib.extensions.signSendAndConfirm -import com.metaplex.lib.modules.candymachines.builders.AddConfigLinesTransactionBuilder -import com.metaplex.lib.modules.candymachines.builders.CreateCandyMachineTransactionBuilder -import com.metaplex.lib.modules.candymachines.models.CandyMachine +import com.metaplex.lib.modules.candymachines.builders.* +import com.metaplex.lib.modules.candymachines.models.* import com.metaplex.lib.modules.candymachines.operations.FindCandyMachineByAddressOperationHandler -import com.metaplex.lib.modules.candymachines.builders.MintNftTransactionBuilder -import com.metaplex.lib.modules.candymachines.builders.SetCollectionTransactionBuilder -import com.metaplex.lib.modules.candymachines.models.CandyMachineItem +import com.metaplex.lib.modules.candymachines.operations.FindCandyGuardByAddressOperationHandler import com.metaplex.lib.modules.nfts.models.NFT import com.metaplex.lib.modules.nfts.operations.FindNftByMintOnChainOperationHandler +import com.solana.core.Account import com.solana.core.HotAccount import com.solana.core.PublicKey import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import java.lang.Exception class CandyMachineClient(val connection: Connection, val signer: IdentityDriver, private val dispatcher: CoroutineDispatcher = Dispatchers.IO, @@ -34,18 +33,25 @@ class CandyMachineClient(val connection: Connection, val signer: IdentityDriver, suspend fun findByAddress(address: PublicKey): Result = FindCandyMachineByAddressOperationHandler(connection, dispatcher).handle(address) + suspend fun findCandyGuardByBaseAddress(base: PublicKey): Result = + FindCandyGuardByAddressOperationHandler(connection, dispatcher) + .handle(CandyGuard.pda(base).address) + suspend fun create( sellerFeeBasisPoints: Int, itemsAvailable: Long, collection: PublicKey, collectionUpdateAuthority: PublicKey, authority: PublicKey = signer.publicKey, + withoutCandyGuard: Boolean = false, transactionOptions: TransactionOptions = txOptions ): Result = runCatching { val candyMachineAccount = HotAccount() val candyMachineAddress = candyMachineAccount.publicKey + val mintAuthority = if (withoutCandyGuard) authority else CandyGuard.pda(signer.publicKey).address CandyMachine( address = candyMachineAddress, authority = authority, + mintAuthority = mintAuthority, sellerFeeBasisPoints = sellerFeeBasisPoints.toUShort(), itemsAvailable = itemsAvailable, collectionMintAddress = collection, @@ -59,15 +65,48 @@ class CandyMachineClient(val connection: Connection, val signer: IdentityDriver, ) ).apply { - CreateCandyMachineTransactionBuilder(this, signer.publicKey, connection, dispatcher) - .build() - .getOrThrow() + CreateCandyMachineTransactionBuilder( + this, withoutCandyGuard, signer.publicKey, connection, dispatcher + ).build().getOrThrow() .signSendAndConfirm(connection, signer, listOf(candyMachineAccount), transactionOptions) return Result.success(this) } } + suspend fun createCandyGuard( + guards: List, groups: Map> = mapOf(), + authority: PublicKey = signer.publicKey, transactionOptions: TransactionOptions = txOptions + ): Result = runCatching { + + val base = HotAccount() + + CandyGuard(base.publicKey, authority, guards, groups).apply { + + CreateCandyGuardTransactionBuilder(this, signer.publicKey, connection, dispatcher) + .build() + .getOrThrow() + .signSendAndConfirm(connection, signer, listOf(base), transactionOptions) + + return Result.success(CandyGuard(base.publicKey, authority, guards)) + } + } + + suspend fun wrapCandyGuard( + candyGuard: CandyGuard, candyMachine: PublicKey, authority: Account? = null, + transactionOptions: TransactionOptions = txOptions + ): Result { + + val authorityAddress = authority?.publicKey ?: signer.publicKey + val additionalSigners = authority?.let { listOf(authority) } ?: listOf() + + return WrapCandyGuardTransactionBuilder( + candyGuard.base, candyMachine, authorityAddress, signer.publicKey, connection, dispatcher + ).build().getOrThrow() + .signSendAndConfirm(connection, signer, additionalSigners, + transactionOptions = transactionOptions) + } + suspend fun setCollection(candyMachine: CandyMachine, collection: PublicKey, transactionOptions: TransactionOptions = txOptions): Result = SetCollectionTransactionBuilder(candyMachine, collection, signer.publicKey, connection, dispatcher) diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyGuardTransactionBuilder.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyGuardTransactionBuilder.kt new file mode 100644 index 0000000..b8982ec --- /dev/null +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyGuardTransactionBuilder.kt @@ -0,0 +1,42 @@ +/* + * CreateCandyGuardTransactionBuilder + * metaplex-android + * + * Created by Funkatronics on 10/13/2022 + */ + +package com.metaplex.lib.modules.candymachines.builders + +import com.metaplex.lib.drivers.solana.Connection +import com.metaplex.lib.experimental.jen.candyguard.* +import com.metaplex.lib.modules.candymachines.CANDY_GUARD_DATA +import com.metaplex.lib.modules.candymachines.models.* +import com.metaplex.lib.modules.candymachines.models.CandyGuard +import com.metaplex.lib.serialization.format.Borsh +import com.metaplex.lib.shared.builders.TransactionBuilder +import com.solana.core.PublicKey +import com.solana.core.Transaction +import com.solana.programs.SystemProgram +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +class CreateCandyGuardTransactionBuilder( + val candyGuard: CandyGuard, payer: PublicKey, + connection: Connection, dispatcher: CoroutineDispatcher = Dispatchers.IO +) : TransactionBuilder(payer, connection, dispatcher) { + + override suspend fun build(): Result = Result.success(Transaction().apply { + + val data = Borsh.encodeToByteArray(CandyGuardSerializer, candyGuard) + .run { slice(CANDY_GUARD_DATA until size) } + + addInstruction(CandyGuardInstructions.initialize( + candyGuard = CandyGuard.pda(candyGuard.base).address, + base = candyGuard.base, + authority = candyGuard.authority, + payer = payer, + systemProgram = SystemProgram.PROGRAM_ID, + data = data.toByteArray() + )) + }) +} \ No newline at end of file diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyMachineTransactionBuilder.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyMachineTransactionBuilder.kt index 1a9e007..79c714f 100644 --- a/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyMachineTransactionBuilder.kt +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/CreateCandyMachineTransactionBuilder.kt @@ -8,9 +8,12 @@ package com.metaplex.lib.modules.candymachines.builders import com.metaplex.lib.drivers.solana.Connection +import com.metaplex.lib.experimental.jen.candyguard.CandyGuardInstructions import com.metaplex.lib.experimental.jen.candymachine.* +import com.metaplex.lib.modules.candymachines.models.CandyGuard import com.metaplex.lib.modules.candymachines.models.CandyMachine import com.metaplex.lib.modules.candymachines.models.authorityPda +import com.metaplex.lib.modules.candymachines.models.pda import com.metaplex.lib.programs.token_metadata.MasterEditionAccount import com.metaplex.lib.programs.token_metadata.TokenMetadataProgram import com.metaplex.lib.programs.token_metadata.accounts.MetadataAccount @@ -24,10 +27,10 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class CreateCandyMachineTransactionBuilder(val candyMachine: CandyMachine, payer: PublicKey, - connection: Connection, - dispatcher: CoroutineDispatcher = Dispatchers.IO) - : TransactionBuilder(payer, connection, dispatcher) { +class CreateCandyMachineTransactionBuilder( + val candyMachine: CandyMachine, val withoutCandyGuard: Boolean = false, payer: PublicKey, + connection: Connection, dispatcher: CoroutineDispatcher = Dispatchers.IO +) : TransactionBuilder(payer, connection, dispatcher) { override suspend fun build(): Result = withContext(dispatcher) { Result.success(Transaction().apply { @@ -41,9 +44,6 @@ class CreateCandyMachineTransactionBuilder(val candyMachine: CandyMachine, payer val collectionAuthorityRecord = collectionAuthorityRecordPda(collectionMintAddress, authorityPda).address - // TODO: need to add candy guard support. - // An additional instruction will need to be added here to create the Candy Guard - // Create an empty account for the candy machine. addInstruction( SystemProgram.createAccount( @@ -79,6 +79,22 @@ class CreateCandyMachineTransactionBuilder(val candyMachine: CandyMachine, payer hiddenSettings = hiddenSettings, ) )) + + if (withoutCandyGuard) return@apply + + val candyGuard = CandyGuard(payer, authority) + + CreateCandyGuardTransactionBuilder(candyGuard, payer, connection, dispatcher) + .build() + .getOrThrow() + .instructions + .forEach { ix -> addInstruction(ix) } + + WrapCandyGuardTransactionBuilder(payer, address, authority, payer, connection, dispatcher) + .build() + .getOrThrow() + .instructions + .forEach { ix -> addInstruction(ix) } } }) } diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/WrapCandyGuardTransactionBuilder.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/WrapCandyGuardTransactionBuilder.kt new file mode 100644 index 0000000..6be91c6 --- /dev/null +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/builders/WrapCandyGuardTransactionBuilder.kt @@ -0,0 +1,36 @@ +/* + * WrapCandyGuardTransactionBuilder + * Metaplex + * + * Created by Funkatronics on 11/1/2022 + */ + +package com.metaplex.lib.modules.candymachines.builders + +import com.metaplex.lib.drivers.solana.Connection +import com.metaplex.lib.experimental.jen.candyguard.CandyGuardInstructions +import com.metaplex.lib.modules.candymachines.models.CandyGuard +import com.metaplex.lib.modules.candymachines.models.CandyMachine +import com.metaplex.lib.modules.candymachines.models.pda +import com.metaplex.lib.shared.builders.TransactionBuilder +import com.solana.core.PublicKey +import com.solana.core.Transaction +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +class WrapCandyGuardTransactionBuilder( + val candyGuardBase: PublicKey, val candyMachine: PublicKey, val authority: PublicKey, + payer: PublicKey, connection: Connection, dispatcher: CoroutineDispatcher = Dispatchers.IO +) : TransactionBuilder(payer, connection, dispatcher) { + + override suspend fun build(): Result = Result.success(Transaction().apply { + addInstruction( + CandyGuardInstructions.wrap( + candyGuard = CandyGuard.pda(candyGuardBase).address, + authority = authority, + candyMachine = candyMachine, + candyMachineProgram = PublicKey(CandyMachine.PROGRAM_ADDRESS), + candyMachineAuthority = authority + )) + }) +} \ No newline at end of file diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyGuard.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyGuard.kt new file mode 100644 index 0000000..6438f8b --- /dev/null +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyGuard.kt @@ -0,0 +1,290 @@ +/* + * CandyGuard + * metaplex-android + * + * Created by Funkatronics on 10/13/2022 + */ + +package com.metaplex.lib.modules.candymachines.models + +import com.metaplex.lib.experimental.jen.candyguard.* +import com.metaplex.lib.modules.candymachines.CANDY_GUARD_LABEL_SIZE +import com.metaplex.lib.serialization.serializers.solana.AnchorAccountSerializer +import com.solana.core.PublicKey +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.nio.charset.StandardCharsets +import kotlin.math.min + +import com.metaplex.lib.experimental.jen.candyguard.CandyGuard as CandyGuardAccount + +class CandyGuard(val base: PublicKey, val authority: PublicKey, + val defaultGuards: List = listOf(), + val groups: Map>? = null) { + + companion object { + const val PROGRAM_NAME = "candy_guard" + const val PROGRAM_ADDRESS = "Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g" + } +} + +fun CandyGuard.Companion.pda(base: PublicKey) = + PublicKey.findProgramAddress(listOf( + PROGRAM_NAME.toByteArray(Charsets.UTF_8), + base.toByteArray() + ), PublicKey(PROGRAM_ADDRESS)) + +sealed interface Guard { + data class AddressGate(val address: PublicKey): Guard + data class AllowList(val merkleRoot: List): Guard + data class BotTax(val lamports: Long, val lastInstruction: Boolean): Guard + data class EndDate(val date: Long): Guard + data class Gatekeeper(val gatekeeperNetwork: PublicKey, val expireOnUse: Boolean): Guard + data class MintLimit(val id: Byte, val limit: Short): Guard + data class NftBurn(val requiredCollection: PublicKey): Guard + data class NftGate(val requiredCollection: PublicKey): Guard + data class NftPayment(val requiredCollection: PublicKey, val destination: PublicKey): Guard + data class RedeemedAmount(val maximum: Long): Guard + data class SolPayment(val lamports: Long, val destination: PublicKey): Guard + data class StartDate(val date: Long): Guard + data class ThirdPartySigner(val signerKey: PublicKey): Guard + data class TokenBurn(val amount: Long, val mint: PublicKey): Guard + data class TokenGate(val amount: Long, val mint: PublicKey): Guard + data class TokenPayment( + val amount: Long, + val mint: PublicKey, + val destinationAta: PublicKey + ): Guard + data class FreezeSolPayment(val lamports: Long, val destination: PublicKey) : Guard + data class FreezeTokenPayment( + val amount: Long, + val mint: PublicKey, + val destinationAta: PublicKey + ) : Guard + data class ProgramGate(val additional: List) : Guard +} + +object CandyGuardSerializer : KSerializer { + override val descriptor: SerialDescriptor = CandyGuardAccount.serializer().descriptor + + override fun serialize(encoder: Encoder, value: CandyGuard) { + // encode account data + encoder.encodeSerializableValue(AnchorAccountSerializer(), + CandyGuardAccount(value.base, CandyGuard.pda(value.base).nonce.toUByte(), value.authority) + ) + + // encode default guards + encoder.encodeSerializableValue(GuardSetSerializer, value.defaultGuards.toList()) + + // encode guard groups + encoder.encodeInt(value.groups?.keys?.size ?: 0) // group count + value.groups?.forEach { group -> + + // encode label + val labelBytes = ByteArray(CANDY_GUARD_LABEL_SIZE) + + group.key.toByteArray(Charsets.UTF_8) + .copyInto(labelBytes, endIndex = min(group.key.length, CANDY_GUARD_LABEL_SIZE)) + + labelBytes.forEach { encoder.encodeByte(it) } + + // encode guard set for group + encoder.encodeSerializableValue(GuardSetSerializer, + group.value.toList().sortedBy { it.idlType.ordinal }) + } + } + + override fun deserialize(decoder: Decoder): CandyGuard { + // decode account data + val account = decoder.decodeSerializableValue(AnchorAccountSerializer()) + + // decode default guards + val defaultGuardSet = GuardSetSerializer.deserialize(decoder) + + // decode guard groups + val groupCount = decoder.decodeInt() + val groups = (0 until groupCount).associate { + // decode fixed size label string + val bytes = ByteArray(CANDY_GUARD_LABEL_SIZE) { decoder.decodeByte() } + val label = String(bytes, StandardCharsets.UTF_8) + .replace("\u0000", "") + + // build guard group + label to GuardSetSerializer.deserialize(decoder) + } + + return CandyGuard(account.base, account.authority, defaultGuardSet, groups) + } +} + +object GuardSetSerializer : KSerializer> { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GuardSet") + + override fun serialize(encoder: Encoder, value: List) { + + // encode feature flags + var flags: Long = 0 + value.forEach { + flags = flags or (1L shl it.idlType.ordinal) + } + encoder.encodeLong(flags) + + // encode guards + value.sortedBy { it.idlType.ordinal }.forEach { + encodeGuard(it, encoder) + } + } + + override fun deserialize(decoder: Decoder): List = + decoder.decodeLong().let { flags -> + GuardType.values().mapNotNull { guard -> + if ((flags shr guard.ordinal and 1L) > 0) decodeGuard(guard, decoder) else null + } + } + + // I hate this, this is an extremely fragile and hard to maintain mapping + // between the IDL GuardType and guard objects, and the library Guard set + private fun decodeGuard(guard: GuardType, decoder: Decoder): Guard = when(guard) { + GuardType.AddressGate -> + decoder.decodeSerializableValue(AddressGate.serializer()).run { + Guard.AddressGate(address) + } + GuardType.AllowList -> + decoder.decodeSerializableValue(AllowListSerializer).run { + Guard.AllowList(merkleRoot.map { it.toByte() }) + } + GuardType.BotTax -> + decoder.decodeSerializableValue(BotTax.serializer()).run { + Guard.BotTax(lamports.toLong(), lastInstruction) + } + GuardType.EndDate -> + decoder.decodeSerializableValue(EndDate.serializer()).run { + Guard.EndDate(date) + } + GuardType.Gatekeeper -> + decoder.decodeSerializableValue(Gatekeeper.serializer()).run { + Guard.Gatekeeper(gatekeeperNetwork, expireOnUse) + } + GuardType.MintLimit -> + decoder.decodeSerializableValue(MintLimit.serializer()).run { + Guard.MintLimit(id.toByte(), limit.toShort()) + } + GuardType.NftBurn -> + decoder.decodeSerializableValue(NftBurn.serializer()).run { + Guard.NftBurn(requiredCollection) + } + GuardType.NftGate -> + decoder.decodeSerializableValue(NftGate.serializer()).run { + Guard.NftGate(requiredCollection) + } + GuardType.NftPayment -> + decoder.decodeSerializableValue(NftPayment.serializer()).run { + Guard.NftPayment(requiredCollection, destination) + } + GuardType.RedeemedAmount -> + decoder.decodeSerializableValue(RedeemedAmount.serializer()).run { + Guard.RedeemedAmount(maximum.toLong()) + } + GuardType.SolPayment -> + decoder.decodeSerializableValue(SolPayment.serializer()).run { + Guard.SolPayment(lamports.toLong(), destination) + } + GuardType.StartDate -> + decoder.decodeSerializableValue(StartDate.serializer()).run { + Guard.StartDate(date) + } + GuardType.ThirdPartySigner -> + decoder.decodeSerializableValue(ThirdPartySigner.serializer()).run { + Guard.ThirdPartySigner(signerKey) + } + GuardType.TokenBurn -> + decoder.decodeSerializableValue(TokenBurn.serializer()).run { + Guard.TokenBurn(amount.toLong(), mint) + } + GuardType.TokenGate -> + decoder.decodeSerializableValue(TokenGate.serializer()).run { + Guard.TokenGate(amount.toLong(), mint) + } + GuardType.TokenPayment -> + decoder.decodeSerializableValue(TokenPayment.serializer()).run { + Guard.TokenPayment(amount.toLong(), mint, destinationAta) + } + GuardType.FreezeSolPayment -> + decoder.decodeSerializableValue(FreezeSolPayment.serializer()).run { + Guard.FreezeSolPayment(lamports.toLong(), destination) + } + GuardType.FreezeTokenPayment -> + decoder.decodeSerializableValue(FreezeTokenPayment.serializer()).run { + Guard.FreezeTokenPayment(amount.toLong(), mint, destinationAta) + } + GuardType.ProgramGate -> + decoder.decodeSerializableValue(ProgramGate.serializer()).run { + Guard.ProgramGate(additional) + } + } + + private fun encodeGuard(guard: Guard, encoder: Encoder) = when(guard) { + is Guard.AddressGate -> + encoder.encodeSerializableValue(AddressGate.serializer(), AddressGate(guard.address)) + is Guard.AllowList -> + encoder.encodeSerializableValue(AllowListSerializer, AllowList(guard.merkleRoot.map { it.toUByte() })) + is Guard.BotTax -> + encoder.encodeSerializableValue(BotTax.serializer(), BotTax(guard.lamports.toULong(), guard.lastInstruction)) + is Guard.EndDate -> + encoder.encodeSerializableValue(EndDate.serializer(), EndDate(guard.date)) + is Guard.Gatekeeper -> + encoder.encodeSerializableValue(Gatekeeper.serializer(), Gatekeeper(guard.gatekeeperNetwork, guard.expireOnUse)) + is Guard.MintLimit -> + encoder.encodeSerializableValue(MintLimit.serializer(), MintLimit(guard.id.toUByte(), guard.limit.toUShort())) + is Guard.NftBurn -> + encoder.encodeSerializableValue(NftBurn.serializer(), NftBurn(guard.requiredCollection)) + is Guard.NftGate -> + encoder.encodeSerializableValue(NftGate.serializer(), NftGate(guard.requiredCollection)) + is Guard.NftPayment -> + encoder.encodeSerializableValue(NftPayment.serializer(), NftPayment(guard.requiredCollection, guard.destination)) + is Guard.RedeemedAmount -> + encoder.encodeSerializableValue(RedeemedAmount.serializer(), RedeemedAmount(guard.maximum.toULong())) + is Guard.SolPayment -> + encoder.encodeSerializableValue(SolPayment.serializer(), SolPayment(guard.lamports.toULong(), guard.destination)) + is Guard.StartDate -> + encoder.encodeSerializableValue(StartDate.serializer(), StartDate(guard.date)) + is Guard.ThirdPartySigner -> + encoder.encodeSerializableValue(ThirdPartySigner.serializer(), ThirdPartySigner(guard.signerKey)) + is Guard.TokenBurn -> + encoder.encodeSerializableValue(TokenBurn.serializer(), TokenBurn(guard.amount.toULong(), guard.mint)) + is Guard.TokenGate -> + encoder.encodeSerializableValue(TokenGate.serializer(), TokenGate(guard.amount.toULong(), guard.mint)) + is Guard.TokenPayment -> + encoder.encodeSerializableValue(TokenPayment.serializer(), TokenPayment(guard.amount.toULong(), guard.mint, guard.destinationAta)) + is Guard.FreezeSolPayment -> + encoder.encodeSerializableValue(FreezeSolPayment.serializer(), FreezeSolPayment(guard.lamports.toULong(), guard.destination)) + is Guard.FreezeTokenPayment -> + encoder.encodeSerializableValue(FreezeTokenPayment.serializer(), FreezeTokenPayment(guard.amount.toULong(), guard.mint, guard.destinationAta)) + is Guard.ProgramGate -> + encoder.encodeSerializableValue(ProgramGate.serializer(), ProgramGate(guard.additional)) + } +} + +internal val Guard.idlType get() = when(this) { + is Guard.AddressGate -> GuardType.AddressGate + is Guard.AllowList -> GuardType.AllowList + is Guard.BotTax -> GuardType.BotTax + is Guard.EndDate -> GuardType.EndDate + is Guard.Gatekeeper -> GuardType.Gatekeeper + is Guard.MintLimit -> GuardType.MintLimit + is Guard.NftBurn -> GuardType.NftBurn + is Guard.NftGate -> GuardType.NftGate + is Guard.NftPayment -> GuardType.NftPayment + is Guard.RedeemedAmount -> GuardType.RedeemedAmount + is Guard.SolPayment -> GuardType.SolPayment + is Guard.StartDate -> GuardType.StartDate + is Guard.ThirdPartySigner -> GuardType.ThirdPartySigner + is Guard.TokenBurn -> GuardType.TokenBurn + is Guard.TokenGate -> GuardType.TokenGate + is Guard.TokenPayment -> GuardType.TokenPayment + is Guard.FreezeSolPayment -> GuardType.FreezeSolPayment + is Guard.FreezeTokenPayment -> GuardType.FreezeTokenPayment + is Guard.ProgramGate -> GuardType.ProgramGate +} \ No newline at end of file diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyMachine.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyMachine.kt index e8c2bc3..d524444 100644 --- a/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyMachine.kt +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/models/CandyMachine.kt @@ -7,7 +7,9 @@ package com.metaplex.lib.modules.candymachines.models -import com.metaplex.lib.experimental.jen.candymachine.* +import com.metaplex.lib.experimental.jen.candymachine.ConfigLineSettings +import com.metaplex.lib.experimental.jen.candymachine.Creator +import com.metaplex.lib.experimental.jen.candymachine.HiddenSettings import com.metaplex.lib.modules.candymachines.CANDY_MACHINE_HIDDEN_SECTION import com.metaplex.lib.modules.candymachines.models.CandyMachine.Companion.PROGRAM_ADDRESS import com.metaplex.lib.modules.candymachinesv2.models.CandyMachineV2 @@ -16,6 +18,7 @@ import com.solana.core.PublicKey class CandyMachine( val address: PublicKey, val authority: PublicKey, + val mintAuthority: PublicKey, val sellerFeeBasisPoints: UShort, val itemsAvailable: Long, val itemsMinted: Long = 0, @@ -61,10 +64,4 @@ val CandyMachine.authorityPda get() = "candy_machine".toByteArray(Charsets.UTF_8), address.toByteArray() ), PublicKey(PROGRAM_ADDRESS)) - -//val CandyMachine.candyGuardPda get() = -// PublicKey.findProgramAddress(listOf( -// "candy_guard".toByteArray(Charsets.UTF_8), -// address.toByteArray() -// ), PublicKey(CandyGuard.PROGRAM_ADDRESS)) //endregion \ No newline at end of file diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyGuardByAddressOperationHandler.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyGuardByAddressOperationHandler.kt new file mode 100644 index 0000000..bc64cb7 --- /dev/null +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyGuardByAddressOperationHandler.kt @@ -0,0 +1,40 @@ +/* + * FindCandyGuardByAddressOperationHandler + * Metaplex + * + * Created by Funkatronics on 10/26/2022 + */ + +package com.metaplex.lib.modules.candymachines.operations + +import com.metaplex.lib.drivers.solana.AccountInfo +import com.metaplex.lib.drivers.solana.AccountRequest +import com.metaplex.lib.drivers.solana.Connection +import com.metaplex.lib.modules.candymachines.models.* +import com.metaplex.lib.modules.candymachines.models.CandyGuard +import com.metaplex.lib.serialization.format.Borsh +import com.metaplex.lib.serialization.serializers.base64.ByteArrayAsBase64JsonArraySerializer +import com.metaplex.lib.serialization.serializers.solana.SolanaResponseSerializer +import com.metaplex.lib.shared.OperationError +import com.metaplex.lib.shared.OperationHandler +import com.solana.core.PublicKey +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class FindCandyGuardByAddressOperationHandler( + override val connection: Connection, + override val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : OperationHandler { + + override suspend fun handle(input: PublicKey): Result = withContext(dispatcher) { + connection.get( + AccountRequest(input.toString(), connection.transactionOptions), + SolanaResponseSerializer(AccountInfo.serializer(ByteArrayAsBase64JsonArraySerializer)) + ).map { + it?.data?.let { + Borsh.decodeFromByteArray(CandyGuardSerializer, it) + } ?: throw OperationError.NilDataOnAccount + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyMachineByAddressOperationHandler.kt b/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyMachineByAddressOperationHandler.kt index 6c3b1d9..2f51535 100644 --- a/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyMachineByAddressOperationHandler.kt +++ b/lib/src/main/java/com/metaplex/lib/modules/candymachines/operations/FindCandyMachineByAddressOperationHandler.kt @@ -45,6 +45,7 @@ class FindCandyMachineByAddressOperationHandler(val connection: Connection, CandyMachine( address = input, authority = cmAccount.authority, + mintAuthority = cmAccount.mintAuthority, sellerFeeBasisPoints = cmAccount.data.sellerFeeBasisPoints, itemsAvailable = cmAccount.data.itemsAvailable.toLong(), itemsMinted = cmAccount.itemsRedeemed.toLong(), diff --git a/lib/src/test/java/com/metaplex/lib/modules/candymachine/CandyMachineClientTests.kt b/lib/src/test/java/com/metaplex/lib/modules/candymachine/CandyMachineClientTests.kt index 2bfc817..4f0f587 100644 --- a/lib/src/test/java/com/metaplex/lib/modules/candymachine/CandyMachineClientTests.kt +++ b/lib/src/test/java/com/metaplex/lib/modules/candymachine/CandyMachineClientTests.kt @@ -15,10 +15,10 @@ import com.metaplex.lib.drivers.indenty.KeypairIdentityDriver import com.metaplex.lib.drivers.indenty.ReadOnlyIdentityDriver import com.metaplex.lib.drivers.solana.Connection import com.metaplex.lib.drivers.solana.SolanaConnectionDriver +import com.metaplex.lib.extensions.epochMillis import com.metaplex.lib.generateConnectionDriver import com.metaplex.lib.modules.candymachines.CandyMachineClient -import com.metaplex.lib.modules.candymachines.models.CandyMachine -import com.metaplex.lib.modules.candymachines.models.CandyMachineItem +import com.metaplex.lib.modules.candymachines.models.* import com.metaplex.lib.modules.candymachines.refresh import com.metaplex.lib.modules.nfts.NftClient import com.metaplex.lib.modules.nfts.models.Metadata @@ -31,6 +31,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Test +import java.time.ZoneId +import java.time.ZonedDateTime class CandyMachineClientTests { @@ -91,7 +93,7 @@ class CandyMachineClientTests { // when val result = client.insertItems( - CandyMachine(signer.publicKey, signer.publicKey, 333.toUShort(), 1L, + CandyMachine(signer.publicKey, signer.publicKey, signer.publicKey, 333.toUShort(), 1L, collectionMintAddress = signer.publicKey, collectionUpdateAuthority = signer.publicKey), listOf(CandyMachineItem("An NFT", "test.com")) ) @@ -111,7 +113,7 @@ class CandyMachineClientTests { // when val result = client.mintNft( - CandyMachine(signer.publicKey, signer.publicKey, 333.toUShort(), 1L, + CandyMachine(signer.publicKey, signer.publicKey, signer.publicKey, 333.toUShort(), 1L, collectionMintAddress = signer.publicKey, collectionUpdateAuthority = signer.publicKey) ) @@ -146,6 +148,7 @@ class CandyMachineClientTests { val connection = MetaplexTestUtils.generateConnectionDriver() val identityDriver = KeypairIdentityDriver(signer, connection) val client = CandyMachineClient(connection, identityDriver) + val expectedMintAuthority = CandyGuard.pda(signer.publicKey).address // when connection.airdrop(signer.publicKey, 10f) @@ -156,6 +159,31 @@ class CandyMachineClientTests { // then Assert.assertNotNull(actualCandyMachine) Assert.assertEquals(candyMachine.configLineSettings, actualCandyMachine?.configLineSettings) + Assert.assertEquals(expectedMintAuthority, candyMachine.mintAuthority) + Assert.assertEquals(expectedMintAuthority, actualCandyMachine?.mintAuthority) + } + + @Test + fun testCandyMachineCreateWithoutCandyGuard() = runTest { + // given + val signer = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + // when + connection.airdrop(signer.publicKey, 10f) + val nft = createCollectionNft(connection, identityDriver).getOrThrow() + val candyMachine = client.create(333, 5000, nft.mint, + signer.publicKey, withoutCandyGuard = true).getOrThrow() + + val actualCandyMachine = client.findByAddress(candyMachine.address).getOrNull() + + // then + Assert.assertNotNull(actualCandyMachine) + Assert.assertEquals(candyMachine.configLineSettings, actualCandyMachine?.configLineSettings) + Assert.assertEquals(candyMachine.authority, candyMachine.mintAuthority) + Assert.assertEquals(candyMachine.mintAuthority, actualCandyMachine?.mintAuthority) } //region SET COLLECTION @@ -260,6 +288,204 @@ class CandyMachineClientTests { // then Assert.assertNotNull(mintResult) } + + //region CANDY GUARDS + @Test + fun testCreateEmptyCandyGuard() = runTest { + // given + val signer = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + val guards = listOf() + + // when + connection.airdrop(signer.publicKey, 0.5f) + val candyGuard = client.createCandyGuard(guards).map { + client.findCandyGuardByBaseAddress(it.base).getOrThrow() + }.getOrThrow() + + // then + Assert.assertNotNull(candyGuard) + Assert.assertEquals(guards, candyGuard.defaultGuards) + } + + @Test + fun testCreateEmptyCandyGuardWithAuthority() = runTest { + // given + val signer = HotAccount() + val authority = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + // when + connection.airdrop(signer.publicKey, 0.5f) + val candyGuard = client.createCandyGuard(listOf(), mapOf(), authority.publicKey).map { + client.findCandyGuardByBaseAddress(it.base).getOrThrow() + }.getOrThrow() + + // then + Assert.assertNotNull(candyGuard) + Assert.assertEquals(authority.publicKey, candyGuard.authority) + } + + @Test + fun testCreateCandyGuardWithAllGuards() = runTest { + // given + val signer = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + val guards = listOf( + Guard.BotTax(1000000, false), + Guard.SolPayment(1000000000, HotAccount().publicKey), + Guard.TokenPayment(5, HotAccount().publicKey, HotAccount().publicKey), + Guard.StartDate(ZonedDateTime.of(2022, 9, 5, 20, 0, 0, 0, ZoneId.of("UTC")).epochMillis()), + Guard.ThirdPartySigner(HotAccount().publicKey), + Guard.TokenGate(5, HotAccount().publicKey), + Guard.Gatekeeper(HotAccount().publicKey, true), + Guard.EndDate(ZonedDateTime.of(2022, 9, 5, 20, 0, 0, 0, ZoneId.of("UTC")).epochMillis()), + Guard.AllowList(List(32) { 42 }), + Guard.MintLimit(1, 5), + Guard.NftPayment(HotAccount().publicKey, HotAccount().publicKey), + Guard.RedeemedAmount(100), + Guard.AddressGate(HotAccount().publicKey), + Guard.NftGate(HotAccount().publicKey), + Guard.NftBurn(HotAccount().publicKey), + Guard.TokenBurn(1, HotAccount().publicKey) + ) + + // when + connection.airdrop(signer.publicKey, 0.5f) + val candyGuard: CandyGuard = client.createCandyGuard(guards).map { + client.findCandyGuardByBaseAddress(it.base).getOrThrow() + }.getOrThrow() + + // then + Assert.assertNotNull(candyGuard) + Assert.assertEquals(guards, candyGuard.defaultGuards) + } + + @Test + fun testCreateCandyGuardWithGuardsIsOrderAgnostic() = runTest { + // given + val signer = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + val guards = listOf( + Guard.NftBurn(HotAccount().publicKey), + Guard.NftGate(HotAccount().publicKey), + Guard.SolPayment(1000000000, HotAccount().publicKey), + Guard.BotTax(1000000, false) + ) + + // when + connection.airdrop(signer.publicKey, 0.5f) + val candyGuard: CandyGuard = client.createCandyGuard(guards).map { + client.findCandyGuardByBaseAddress(it.base).getOrThrow() + }.getOrThrow() + + // then + Assert.assertNotNull(candyGuard) + Assert.assertEquals(guards.sortedBy { it.idlType.ordinal }, candyGuard.defaultGuards) + } + + @Test + fun testCreateCandyGuardWithGuardGroups() = runTest { + // given + val signer = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + val guards = listOf( + Guard.BotTax(1000000, false), + Guard.EndDate(ZonedDateTime.of(2022, 9, 6, 16, 0, 0, 0, ZoneId.of("UTC")).epochMillis()) + ) + + val groups = mapOf( + "VIP" to listOf( + Guard.StartDate(ZonedDateTime.of(2022, 9, 5, 16, 0, 0, 0, ZoneId.of("UTC")).epochMillis()), + Guard.SolPayment(1000000000, signer.publicKey), + Guard.AllowList(List(32) { 42 }) + ), + "WLIST" to listOf( + Guard.StartDate(ZonedDateTime.of(2022, 9, 5, 18, 0, 0, 0, ZoneId.of("UTC")).epochMillis()), + Guard.SolPayment(2000000000, signer.publicKey), + Guard.TokenGate(1, HotAccount().publicKey) + ), + "PUBLIC" to listOf( + Guard.StartDate(ZonedDateTime.of(2022, 9, 5, 20, 0, 0, 0, ZoneId.of("UTC")).epochMillis()), + Guard.SolPayment(3000000000, signer.publicKey), + Guard.Gatekeeper(HotAccount().publicKey, false) + ) + ) + + // when + connection.airdrop(signer.publicKey, 0.5f) + val candyGuard = client.createCandyGuard(guards, groups).map { + client.findCandyGuardByBaseAddress(it.base).getOrThrow() + }.getOrThrow() + + // then + Assert.assertNotNull(candyGuard) + Assert.assertEquals(guards, candyGuard.defaultGuards) + Assert.assertEquals( + groups.mapValues { it.value.sortedBy { it::class.simpleName } }, + candyGuard.groups?.mapValues { it.value.sortedBy { it::class.simpleName } } + ) + } + + @Test + fun testWrapCandyGuard() = runTest { + // given + val signer = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + // when + connection.airdrop(signer.publicKey, 10f) + val nft = createCollectionNft(connection, identityDriver).getOrThrow() + val candyMachine = client.create(333, 5000, nft.mint, signer.publicKey).getOrThrow() + val candyGuard = client.createCandyGuard(listOf(), mapOf()).getOrThrow() + + client.wrapCandyGuard(candyGuard, candyMachine.address) + + val finalCandyMachine = client.refresh(candyMachine) + + //then + Assert.assertNotNull(finalCandyMachine) + } + + @Test + fun testWrapCandyGuardWithAuthority() = runTest { + // given + val signer = HotAccount() + val authority = HotAccount() + val connection = MetaplexTestUtils.generateConnectionDriver() + val identityDriver = KeypairIdentityDriver(signer, connection) + val client = CandyMachineClient(connection, identityDriver) + + // when + connection.airdrop(signer.publicKey, 10f) + val nft = createCollectionNft(connection, identityDriver).getOrThrow() + val candyMachine = client.create(333, 5000, nft.mint, signer.publicKey, authority.publicKey).getOrThrow() + val candyGuard = client.createCandyGuard(listOf(), mapOf(), authority.publicKey).getOrThrow() + + client.wrapCandyGuard(candyGuard, candyMachine.address, authority) + + val finalCandyMachine = client.refresh(candyMachine) + + //then + Assert.assertNotNull(finalCandyMachine) + } + //endregion //endregion private suspend fun createCollectionNft(connection: Connection, identityDriver: IdentityDriver) =