import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { AccountMeta, KeyedAccountInfo, Keypair, PublicKey, SystemProgram, } from '@solana/web3.js'; import { BN } from '@staratlas/anchor'; import { Account, AccountStatic, AsyncSigner, DecodedAccountData, InstructionReturn, arrayDeepEquals, decodeAccount, staticImplements, } from '@staratlas/data-source'; import { CraftingIDL, CraftingIDLProgram, CraftingProcessAccount, CreateCraftingProcessInput, } from './constants'; export enum CraftingProcessStatus { Initialized, Started, Completed, } /** * Checks equality between 2 Crafting Process Accounts * @param data1 - First Crafting Process Account * @param data2 - Second Crafting Process Account * @returns boolean */ export function craftingProcessDataEquals( data1: CraftingProcessAccount, data2: CraftingProcessAccount, ): boolean { return ( data1.version === data2.version && data1.craftingId.eq(data2.craftingId) && data1.authority.equals(data2.authority) && data1.recipe.equals(data2.recipe) && data1.craftingFacility.equals(data2.craftingFacility) && arrayDeepEquals( data1.inputsChecksum, data2.inputsChecksum, (a, b) => a === b, ) && arrayDeepEquals( data1.outputsChecksum, data2.outputsChecksum, (a, b) => a === b, ) && data1.quantity.eq(data2.quantity) && data1.status === data2.status && data1.startTime.eq(data2.startTime) && data1.endTime.eq(data2.endTime) && data1.denyPermissionlessClaiming === data2.denyPermissionlessClaiming && data1.useLocalTime === data2.useLocalTime && data1.bump === data2.bump ); } export interface IngredientInput { /** the index of the consumable recipe ingredient in the recipe input/outputs array */ ingredientIndex: number; } export interface LocalTimeInput { /** the current time as supplied by the location; required if `useLocalTime` is set on the `CraftingProcess` */ localTime?: BN; } type IngredientAndTimeInput = IngredientInput & LocalTimeInput; export interface RecipeIngredientInput extends IngredientInput { /** the amount */ amount: BN; } export interface StartCraftingProcessInput extends LocalTimeInput { /** the recipe duration (overrides the duration set on the recipe account) */ recipeDurationOverride?: BN; } @staticImplements>() export class CraftingProcess implements Account { static readonly ACCOUNT_NAME: NonNullable< CraftingIDL['accounts'] >[number]['name'] = 'craftingProcess'; static readonly MIN_DATA_SIZE: number = 8 + // discriminator 1 + // version 8 + // craftingId 32 + // authority 32 + // recipe 32 + // craftingFacility 32 + // cargoPod 16 + // inputsChecksum 16 + // outputsChecksum 8 + // quantity 1 + // status 8 + // startTime 8 + // endTime 1; // bump constructor(private _data: CraftingProcessAccount, private _key: PublicKey) {} get data(): Readonly { return this._data; } get key(): PublicKey { return this._key; } static getCargoPodSeeds() { return Keypair.generate().publicKey.toBuffer(); } /** * Find the `CraftingProcess` address * @param program - Crafting program * @param craftingFacility - the crafting facility * @param recipe - the crafting recipe * @param craftingId - the crafting process id * @returns PDA and bump */ static findAddress( program: CraftingIDLProgram, craftingFacility: PublicKey, recipe: PublicKey, craftingId: BN, ): [PublicKey, number] { return PublicKey.findProgramAddressSync( [ Buffer.from('CraftingProcess'), craftingFacility.toBuffer(), recipe.toBuffer(), craftingId.toArrayLike(Buffer, 'le', 8), ], program.programId, ); } /** * Create or Initialize a `CraftingProcess` Account * @param program - Crafting program * @param location - the crafting facility's location * @param authority - the authority for the crafting process account * @param craftingFacility - the crafting facility * @param recipe - the crafting recipe * @param input - the input parameters * @returns InstructionReturn */ static createCraftingProcess( program: CraftingIDLProgram, location: AsyncSigner, authority: AsyncSigner, craftingFacility: PublicKey, recipe: PublicKey, input: CreateCraftingProcessInput, ): InstructionReturn { const craftingProcessKey = this.findAddress( program, craftingFacility, recipe, input.craftingId, )[0]; return async (funder) => [ { instruction: await program.methods .createCraftingProcess(input) .accountsStrict({ location: location.publicKey(), authority: authority.publicKey(), funder: funder.publicKey(), craftingProcess: craftingProcessKey, recipe, craftingFacility, systemProgram: SystemProgram.programId, }) .instruction(), signers: [location, authority, funder], }, ]; } /** * Add an ingredient to a crafting process account * @param program - Crafting program * @param location - the crafting facility's location * @param authority - the authority for the crafting process account * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param craftingFacility - the crafting facility * @param tokenFrom - the source token account * @param tokenTo - the destination token account (must be owned by the crafting process account) * @param input - the input parameters * @returns InstructionReturn */ static addRecipeIngredient( program: CraftingIDLProgram, location: AsyncSigner, authority: AsyncSigner, craftingProcess: PublicKey, recipe: PublicKey, craftingFacility: PublicKey, tokenFrom: PublicKey, tokenTo: PublicKey, input: RecipeIngredientInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .addRecipeIngredient(input) .accountsStrict({ location: location.publicKey(), authority: authority.publicKey(), craftingProcess, recipe, craftingFacility, tokenFrom, tokenTo, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [location, authority], }, ]; } /** * Legitimize an ingredient * The token accounts that hold ingredients can receive tokens from anyone. However, only tokens received through the crafting * program are recognized as valid ingredients. This instruction can be used in cases where one wants to "legitimize" such token account balances * @param program - Crafting program * @param location - the crafting facility's location * @param authority - the authority for the crafting process account * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param craftingFacility - the crafting facility * @param craftingTokenAccount - the token account owned by the crafting process which holds the ingredient in escrow * @param input - the input parameters * @returns InstructionReturn */ static legitimizeRecipeIngredient( program: CraftingIDLProgram, location: AsyncSigner, authority: AsyncSigner, craftingProcess: PublicKey, recipe: PublicKey, craftingFacility: PublicKey, craftingTokenAccount: PublicKey, input: RecipeIngredientInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .legitimizeRecipeIngredient(input) .accountsStrict({ location: location.publicKey(), authority: authority.publicKey(), craftingProcess, recipe, craftingFacility, tokenTo: craftingTokenAccount, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [location, authority], }, ]; } /** * Remove an ingredient from a crafting process account * @param program - Crafting program * @param location - the crafting facility's location * @param authority - the authority for the crafting process account * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param craftingFacility - the crafting facility * @param tokenFrom - the source token account (must be owned by the crafting process) * @param tokenTo - the destination token account * @param mint - the token mint * @param input - the input parameters * @returns InstructionReturn */ static removeRecipeIngredient( program: CraftingIDLProgram, location: AsyncSigner, authority: AsyncSigner, craftingProcess: PublicKey, recipe: PublicKey, craftingFacility: PublicKey, tokenFrom: PublicKey, tokenTo: PublicKey, mint: PublicKey, input: RecipeIngredientInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .removeRecipeIngredient(input) .accountsStrict({ location: location.publicKey(), authority: authority.publicKey(), craftingProcess, recipe, craftingFacility, tokenFrom, tokenTo, mint, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [location, authority], }, ]; } /** * Start the crafting process * @param program - Crafting program * @param location - the crafting facility's location * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param craftingFacility - the crafting facility * @param input - the input parameters * @param recipeFeeRecipient - the recipient of crafting fees as defined in the Recipe account * @param tokenFromAuthority - the transfer authority of `tokenFrom` * @param tokenFrom - the source token account for crafting fees * @param tokenTo - the destination token account for crafting fees, should be ATA owned by `craftingProcess` * @returns InstructionReturn */ static startCraftingProcess( program: CraftingIDLProgram, location: AsyncSigner, craftingProcess: PublicKey, recipe: PublicKey, craftingFacility: PublicKey, input?: StartCraftingProcessInput, recipeFeeRecipient?: PublicKey, tokenFromAuthority?: AsyncSigner, tokenFrom?: PublicKey, tokenTo?: PublicKey, ): InstructionReturn { const signers = [location]; const remainingAccounts: AccountMeta[] = []; if (recipeFeeRecipient && tokenFromAuthority && tokenFrom && tokenTo) { if ( !signers .map((it) => it.publicKey().toBase58()) .includes(tokenFromAuthority.publicKey().toBase58()) ) { signers.push(tokenFromAuthority); } remainingAccounts.push( ...[ { pubkey: recipeFeeRecipient, isSigner: false, isWritable: false, }, { pubkey: tokenFromAuthority.publicKey(), isSigner: true, isWritable: true, }, { pubkey: tokenFrom, isSigner: false, isWritable: true, }, { pubkey: tokenTo, isSigner: false, isWritable: true, }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false, }, ], ); } return async () => [ { instruction: await program.methods .startCraftingProcess({ recipeDurationOverride: input?.recipeDurationOverride ?? null, localTime: input?.localTime ?? null, }) .accountsStrict({ location: location.publicKey(), craftingProcess, recipe, craftingFacility, }) .remainingAccounts(remainingAccounts) .instruction(), signers, }, ]; } /** * Stop the crafting process * * Meant to be used to stop already started process which are not yet complete * @param program - Crafting program * @param location - the crafting facility's location * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param craftingFacility - the crafting facility * @param input - the input parameters * @param tokenFrom - the source token account for crafting fees, should be ATA owned by `craftingProcess` * @param tokenTo - the token account that the refund of crafting fees should be sent to * @returns InstructionReturn */ static stopCraftingProcess( program: CraftingIDLProgram, location: AsyncSigner, craftingProcess: PublicKey, recipe: PublicKey, craftingFacility: PublicKey, input?: LocalTimeInput, tokenFrom?: PublicKey, tokenTo?: PublicKey, ): InstructionReturn { const signers = [location]; const remainingAccounts: AccountMeta[] = []; if (tokenFrom && tokenTo) { remainingAccounts.push( ...[ { pubkey: tokenFrom, isSigner: false, isWritable: true, }, { pubkey: tokenTo, isSigner: false, isWritable: true, }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false, }, ], ); } return async () => [ { instruction: await program.methods .stopCraftingProcess({ localTime: input?.localTime ?? null, }) .accountsStrict({ location: location.publicKey(), craftingProcess, recipe, craftingFacility, }) .remainingAccounts(remainingAccounts) .instruction(), signers, }, ]; } /** * Cancel the crafting process and close the crafting process account * can only be done if the crafting process was not started * @param program - Crafting program * @param location - the crafting facility's location * @param authority - the authority for the crafting process account * @param fundsTo - receives rent refund * @param craftingProcess - the crafting process * @param craftingFacility - the crafting facility * @returns InstructionReturn */ static cancelCraftingProcess( program: CraftingIDLProgram, location: AsyncSigner, authority: AsyncSigner, fundsTo: PublicKey | 'funder', craftingProcess: PublicKey, craftingFacility: PublicKey, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .cancelCraftingProcess() .accountsStrict({ location: location.publicKey(), authority: authority.publicKey(), fundsTo: fundsTo === 'funder' ? funder.publicKey() : fundsTo, craftingProcess, craftingFacility, }) .instruction(), signers: [location, authority], }, ]; } /** * Burn a consumable ingredient from a crafting process account * @param program - Crafting program * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param tokenFrom - the source token account (owned by crafting process) * @param mint - the token mint * @param input - the input parameters * @returns InstructionReturn */ static burnConsumableIngredient( program: CraftingIDLProgram, craftingProcess: PublicKey, recipe: PublicKey, tokenFrom: PublicKey, mint: PublicKey, input: IngredientAndTimeInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .burnConsumableIngredient({ ...input, localTime: input.localTime ?? null, }) .accountsStrict({ craftingProcess, recipe, tokenFrom, mint, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [], }, ]; } /** * Claim a non-consumable ingredient from a crafting process account * @param program - Crafting program * @param craftingProcess - the crafting process * @param authority - the authority for the crafting process account * @param recipe - the crafting recipe * @param tokenFrom - the source token account (owned by crafting process) * @param tokenTo - the destination token account * @param mint - the token mint * @param input - the input parameters * @returns InstructionReturn */ static claimNonConsumableIngredient( program: CraftingIDLProgram, craftingProcess: PublicKey, authority: PublicKey, recipe: PublicKey, tokenFrom: PublicKey, tokenTo: PublicKey, mint: PublicKey, input: IngredientAndTimeInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .claimNonConsumableIngredient({ ...input, localTime: input.localTime ?? null, }) .accountsStrict({ authority, craftingProcess, recipe, tokenFrom, tokenTo, mint, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [], }, ]; } /** * Claim an output from a crafting process account * @param program - Crafting program * @param craftingProcess - the crafting process * @param authority - the authority for the crafting process account * @param recipe - the crafting recipe * @param tokenFrom - the source token account (owned by the `craftableItem`) * @param tokenTo - the destination token account * @param craftableItem - the craftable item * @param input - the input parameters * @returns InstructionReturn */ static claimRecipeOutput( program: CraftingIDLProgram, craftingProcess: PublicKey, authority: PublicKey, recipe: PublicKey, tokenFrom: PublicKey, tokenTo: PublicKey, craftableItem: PublicKey, input: IngredientAndTimeInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .claimRecipeOutput({ ...input, localTime: input.localTime ?? null, }) .accountsStrict({ authority, craftingProcess, recipe, tokenFrom, tokenTo, craftableItem, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [], }, ]; } /** * Close the crafting process account * @param program - Crafting program * @param authority - the authority for the crafting process account * @param fundsTo - receives rent refund * @param craftingProcess - the crafting process * @param recipe - the crafting recipe * @param craftingFacility - the crafting facility * @param tokenFrom - the source token account for crafting fees, should be ATA owned by `craftingProcess` * @param tokenTo - the recipient of crafting fees as defined in the Recipe account * @returns InstructionReturn */ static closeCraftingProcess( program: CraftingIDLProgram, authority: AsyncSigner, fundsTo: PublicKey | 'funder', craftingProcess: PublicKey, recipe: PublicKey, craftingFacility: PublicKey, tokenFrom?: PublicKey, tokenTo?: PublicKey, ): InstructionReturn { const remainingAccounts: AccountMeta[] = []; if (tokenFrom && tokenTo) { remainingAccounts.push( ...[ { pubkey: tokenFrom, isSigner: false, isWritable: true, }, { pubkey: tokenTo, isSigner: false, isWritable: true, }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false, }, ], ); } return async (funder) => [ { instruction: await program.methods .closeCraftingProcess() .accountsStrict({ authority: authority.publicKey(), fundsTo: fundsTo === 'funder' ? funder.publicKey() : fundsTo, craftingProcess, recipe, craftingFacility, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [authority], }, ]; } static decodeData( account: KeyedAccountInfo, program: CraftingIDLProgram, ): DecodedAccountData { return decodeAccount(account, program, CraftingProcess); } }