import { AccountMeta, KeyedAccountInfo, PublicKey, SystemProgram, } from '@solana/web3.js'; import * as anchor from '@staratlas/anchor'; import { Account, AccountStatic, AsyncSigner, DecodedAccountData, InstructionReturn, decodeAccountWithRemaining, staticImplements, } from '@staratlas/data-source'; import { isEqual } from 'lodash'; import { KeyIndexInput } from './common'; import { CraftingIDL, CraftingIDLProgram, RecipeAccount } from './constants'; export interface RecipeInputsOutputs extends KeyIndexInput { amount: anchor.BN; mint: PublicKey; } export enum RecipeStatus { // The recipe has not yet been activated // Updating the recipe is allowed Initializing = 1, // The recipe is active. // Crafting processes that use this recipe can be created. Updating the recipe is NOT allowed. Active = 2, // The recipe has been deactivated. // Crafting processes that use this recipe CANNOT be created. Updating the recipe is NOT allowed. Deactivated = 3, } export interface UpdateRecipeInput extends KeyIndexInput { duration?: anchor.BN; minDuration?: anchor.BN; status?: RecipeStatus; feeAmount?: anchor.BN; usageLimit?: anchor.BN; value?: anchor.BN; } export interface RegisterRecipeInput extends KeyIndexInput { duration: anchor.BN; minDuration: anchor.BN; namespace: number[]; feeAmount?: anchor.BN; usageLimit?: anchor.BN; value?: anchor.BN; } export interface RemoveRecipeIngredients extends KeyIndexInput { ingredientIndex: number; } export const RECIPE_MIN_DATA_SIZE = 8 + // discriminator 1 + // version 32 + // domain 32 + // category 32 + // creator 8 + // duration 8 + // minDuration 32 + // namespace 1 + // consumablesCount 1 + // nonConsumablesCount 1 + // outputsCount 1 + // status 8 + // feeAmount 8 + // usageCount 8 + // usageLimit 8 + // value 32 + // feeRecipient 2; // totalCount /** * Checks equality between 2 Recipe Accounts * @param data1 - First Recipe Account * @param data2 - Second Recipe Account * @returns boolean */ export function recipeDataEquals( data1: RecipeAccount, data2: RecipeAccount, ): boolean { return ( data1.version === data2.version && data1.domain.equals(data2.domain) && data1.category.equals(data2.category) && data1.duration.eq(data2.duration) && data1.minDuration.eq(data2.minDuration) && isEqual(data1.namespace, data2.namespace) && data1.consumablesCount === data2.consumablesCount && data1.nonConsumablesCount === data2.nonConsumablesCount && data1.outputsCount === data2.outputsCount && data1.totalCount === data2.totalCount && data1.feeAmount.eq(data2.feeAmount) && data1.usageCount.eq(data2.usageCount) && data1.usageLimit.eq(data2.usageLimit) && data1.value.eq(data2.value) && data1.feeRecipient.key.equals(data2.feeRecipient.key) && data1.status === data2.status ); } @staticImplements>() export class Recipe implements Account { static readonly ACCOUNT_NAME: NonNullable< CraftingIDL['accounts'] >[number]['name'] = 'recipe'; static readonly MIN_DATA_SIZE: number = RECIPE_MIN_DATA_SIZE; constructor( private _data: RecipeAccount, private _key: PublicKey, private _ingredientInputsOutputs: RecipeInputsOutputs[], ) {} get data(): Readonly { return this._data; } get key(): Readonly { return this._key; } get ingredientInputsOutputs(): Readonly { return this._ingredientInputsOutputs; } /** * Register a Recipe * @param program - Crafting program * @param recipe - the new crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param recipeCategory - the crafting recipe category * @param input - the input parameters * @param feeRecipient - The token account that receives the Recipe's `feeAmount` * @returns InstructionReturn */ static registerRecipe( program: CraftingIDLProgram, recipe: AsyncSigner, key: AsyncSigner, profile: PublicKey, domain: PublicKey, recipeCategory: PublicKey, input: RegisterRecipeInput, feeRecipient?: PublicKey, ): InstructionReturn { if ( (input.feeAmount && input.feeAmount.gt(new anchor.BN(0)) && !feeRecipient) || (feeRecipient && (!input.feeAmount || input.feeAmount.lte(new anchor.BN(0)))) ) { throw 'feeRecipient must be set if feeAmount is set'; } const remainingAccounts: AccountMeta[] = feeRecipient && input.feeAmount && input.feeAmount.gt(new anchor.BN(0)) ? [ { pubkey: feeRecipient, isSigner: false, isWritable: false, }, ] : []; return async (funder) => [ { instruction: await program.methods .registerRecipe({ ...input, feeAmount: input.feeAmount && input.feeAmount.gt(new anchor.BN(0)) ? input.feeAmount : null, usageLimit: input.usageLimit ?? null, value: input.value ?? null, }) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe: recipe.publicKey(), recipeCategory, domain, systemProgram: SystemProgram.programId, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [key, funder, recipe], }, ]; } /** * Add a consumable input to a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param mint - the ingredient's mint * @param recipeIngredient - the amount and mint for this ingredient * @returns InstructionReturn */ static addConsumableInputToRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, mint: PublicKey, recipeIngredient: RecipeInputsOutputs, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .addConsumableInputToRecipe(recipeIngredient) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe, domain, mint, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Add a non-consumable input to a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param mint - the ingredient's mint * @param recipeIngredient - the amount and mint for this ingredient- the amount and mint for this ingredient * @returns InstructionReturn */ static addNonConsumableInputToRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, mint: PublicKey, recipeIngredient: RecipeInputsOutputs, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .addNonConsumableInputToRecipe(recipeIngredient) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe, domain, mint, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Add an output to a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param craftableItem - the craftable item for this output * @param recipeIngredient - the amount and mint for this ingredient * @returns InstructionReturn */ static addOutputToRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, craftableItem: PublicKey, recipeIngredient: RecipeInputsOutputs, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .addOutputToRecipe(recipeIngredient) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe, domain, craftableItem, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Remove a consumable input from a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param input - the input parameters * @returns InstructionReturn */ static removeConsumableInputFromRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, input: RemoveRecipeIngredients, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .removeConsumableInputFromRecipe(input) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe, domain, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Remove a non-consumable input from a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param input - the input parameters * @returns InstructionReturn */ static removeNonConsumableInputFromRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, input: RemoveRecipeIngredients, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .removeNonConsumableInputFromRecipe(input) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe, domain, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Remove an output from a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param input - the input parameters * @returns InstructionReturn */ static removeOutputFromRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, input: RemoveRecipeIngredients, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .removeOutputFromRecipe(input) .accountsStrict({ key: key.publicKey(), profile, funder: funder.publicKey(), recipe, domain, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Update a recipe's category * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param recipeCategoryOld - the old recipe category * @param recipeCategoryNew - the new recipe category * @param input - the input parameters * @returns InstructionReturn */ static updateRecipeCategory( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, recipeCategoryOld: PublicKey, recipeCategoryNew: PublicKey, input: KeyIndexInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .updateRecipeCategory(input) .accountsStrict({ key: key.publicKey(), profile, recipe, recipeCategoryOld, recipeCategoryNew, domain, }) .instruction(), signers: [key], }, ]; } /** * Update a recipe * @param program - Crafting program * @param recipe - the crafting recipe * @param key - Key authorized to use this instruction * @param profile - Profile Account with required Crafting Permissions * @param domain - the crafting domain * @param updateRecipeInput - input params * @param feeRecipient - The token account that receives the Recipe's `feeAmount` * @returns InstructionReturn */ static updateRecipe( program: CraftingIDLProgram, recipe: PublicKey, key: AsyncSigner, profile: PublicKey, domain: PublicKey, updateRecipeInput: UpdateRecipeInput, feeRecipient?: PublicKey, ): InstructionReturn { if ( (updateRecipeInput.feeAmount && updateRecipeInput.feeAmount.gt(new anchor.BN(0)) && !feeRecipient) || (feeRecipient && (!updateRecipeInput.feeAmount || updateRecipeInput.feeAmount.lte(new anchor.BN(0)))) ) { throw 'feeRecipient must be set if feeAmount is set'; } const remainingAccounts: AccountMeta[] = feeRecipient && updateRecipeInput.feeAmount && updateRecipeInput.feeAmount.gt(new anchor.BN(0)) ? [ { pubkey: feeRecipient, isSigner: false, isWritable: false, }, ] : []; return async () => [ { instruction: await program.methods .updateRecipe({ keyIndex: updateRecipeInput.keyIndex, duration: updateRecipeInput.duration ?? null, minDuration: updateRecipeInput.minDuration ?? null, status: updateRecipeInput.status ?? null, usageLimit: updateRecipeInput.usageLimit ?? null, feeAmount: updateRecipeInput.feeAmount ?? null, value: updateRecipeInput.value ?? null, }) .accountsStrict({ key: key.publicKey(), profile, recipe, domain, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [key], }, ]; } static decodeData( account: KeyedAccountInfo, program: CraftingIDLProgram, ): DecodedAccountData { const INGREDIENT_SIZE = 32 + 8; return decodeAccountWithRemaining( account, program, Recipe, (remainingData, data) => Array(data.totalCount) .fill(0) .map((_, i) => program.coder.types.decode( 'RecipeInputsOutputs', remainingData .subarray(i * INGREDIENT_SIZE) .subarray(0, INGREDIENT_SIZE), ), ), ); } }