import { getAccount, getAssociatedTokenAddress, getAssociatedTokenAddressSync, } from '@solana/spl-token'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import * as anchor from '@staratlas/anchor'; import { AnchorProvider, BN, Program, Wallet } from '@staratlas/anchor'; import { arrayDeepEquals, buildSendAndCheck, byteArrayToString, createAssociatedTokenAccount, createMint, getNumberFromEnum, keypairToAsyncSigner, readFromRPCOrError, stringToByteArray, transferToTokenAccount, } from '@staratlas/data-source'; import { PLAYER_PROFILE_IDL } from '@staratlas/player-profile'; import { CRAFTING_IDL, CraftableItem, CraftingDomain, CraftingFacility, CraftingFacilityLocationType, CraftingPermissions, CraftingProcess, CraftingProcessStatus, Recipe, RecipeCategory, RecipeInputsOutputs, RecipeStatus, craftableItemDataEquals, craftingDomainDataEquals, craftingFacilityDataEquals, craftingProcessDataEquals, recipeCategoryDataEquals, recipeDataEquals, } from '../src'; import { checkTokenBalance, cleanupRecipeAndRecipeCategory, createAndFundTokenAccounts, createManyMints, createProfile, createRecipeAndRecipeCategory, numberToLEByteArray, registerCraftableItems, registerRecipeInputOutputs, } from './helpers'; describe('Crafting2', () => { const u64Max = new BN(2).pow(new BN(64)).sub(new BN(1)); const connection = new Connection('http://localhost:8899', 'confirmed'); const walletKeypair = Keypair.generate(); const walletSigner = keypairToAsyncSigner(walletKeypair); const provider = new AnchorProvider( connection, new Wallet(walletKeypair), AnchorProvider.defaultOptions(), ); const program = new Program( CRAFTING_IDL, new PublicKey('CRAFtUSjCW74gQtCS6LyJH33rhhVhdPhZxbPegE4Qwfq'), provider, ); const playerProfileProgram = new Program( PLAYER_PROFILE_IDL, new PublicKey('PprofUW1pURCnMW2si88GWPXEEK3Bvh9Tksy8WtnoYJ'), provider, ); const admin = keypairToAsyncSigner(Keypair.generate()); const funder = keypairToAsyncSigner(Keypair.generate()); const randomSigner = keypairToAsyncSigner(Keypair.generate()); const facilityLocationSigner = keypairToAsyncSigner(Keypair.generate()); const craftingProcessAuthSigner = keypairToAsyncSigner(Keypair.generate()); const craftingProfile = keypairToAsyncSigner(Keypair.generate()); const randomProfile = keypairToAsyncSigner(Keypair.generate()); const domainManagerKey = keypairToAsyncSigner(Keypair.generate()); const craftableItemManagerKey = keypairToAsyncSigner(Keypair.generate()); const craftingFacilityManagerKey = keypairToAsyncSigner(Keypair.generate()); const recipeCategoryManagerKey = keypairToAsyncSigner(Keypair.generate()); const recipeManagerKey = keypairToAsyncSigner(Keypair.generate()); // these correspond to keys array in _createCraftingProfile const DOMAIN_MANAGER = 1; const CRAFTABLE_ITEM_MANAGER = 2; const CRAFTING_FACILITY_MANAGER = 3; const RECIPE_CATEGORY_MANAGER = 4; const RECIPE_MANAGER = 5; const _domainName = 'crafting domain 🎮'; const _domainNamespace = stringToByteArray(_domainName, 32); const _domainSigner = keypairToAsyncSigner(Keypair.generate()); const _recipeCategorySigner = keypairToAsyncSigner(Keypair.generate()); const _recipeSigner = keypairToAsyncSigner(Keypair.generate()); const _recipeCategorySigner2 = keypairToAsyncSigner(Keypair.generate()); const _recipeSigner2 = keypairToAsyncSigner(Keypair.generate()); const _numCraftableItems = 4; const _craftableItemKeys = [...Array(_numCraftableItems).keys()].map(() => Keypair.generate(), ); const _facilitySigner = keypairToAsyncSigner(Keypair.generate()); const _setupCraftingProcess = ( craftingId = new BN(1), recipeSetup = _recipeSigner2, ) => { const addressResult = CraftingProcess.findAddress( program, _facilitySigner.publicKey(), recipeSetup.publicKey(), craftingId, ); return { addressResult, }; }; const _createCraftingProfile = async (profileId = craftingProfile) => { await createProfile( playerProfileProgram, profileId, funder, [ { key: domainManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: CraftingPermissions.manageDomain(), }, { key: craftableItemManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: CraftingPermissions.craftableItemPermissions(), }, { key: craftingFacilityManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: CraftingPermissions.manageCraftingFacility(), }, { key: recipeCategoryManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: CraftingPermissions.recipeCategoryPermissions(), }, { key: recipeManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: CraftingPermissions.recipePermissions(), }, ], provider.connection, admin, walletSigner, ); }; beforeAll(async () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( walletSigner.publicKey(), 100_000_000_000, ), ); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( admin.publicKey(), 100_000_000_000, ), ); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( funder.publicKey(), 100_000_000_000, ), ); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( randomSigner.publicKey(), 100_000_000_000, ), ); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( facilityLocationSigner.publicKey(), 100_000_000_000, ), ); await createManyMints( _craftableItemKeys, admin.publicKey(), walletSigner, provider.connection, ); await _createCraftingProfile(); await _createCraftingProfile(randomProfile); await createAndFundTokenAccounts( admin, _craftableItemKeys.map((it) => [it.publicKey, 100_000_000]), admin.publicKey(), funder, walletSigner, provider.connection, ); }); it('can register a domain', async function () { const ix = CraftingDomain.initializeDomain( program, _domainSigner, admin, craftingProfile.publicKey(), _domainNamespace, ); await buildSendAndCheck(ix, walletSigner, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _domainSigner.publicKey(), CraftingDomain, 'confirmed', ); expect(account.key.equals(_domainSigner.publicKey())).toBe(true); expect( craftingDomainDataEquals(account.data, { profile: craftingProfile.publicKey(), namespace: _domainNamespace, version: 0, }), ).toBe(true); expect(byteArrayToString(account.data.namespace)).toBe(_domainName); }); it('can update a domain', async function () { const ix1 = CraftingDomain.updateDomain( program, _domainSigner.publicKey(), domainManagerKey, craftingProfile.publicKey(), randomProfile.publicKey(), { keyIndex: DOMAIN_MANAGER }, ); await buildSendAndCheck(ix1, admin, provider.connection); const account1 = await readFromRPCOrError( provider.connection, program, _domainSigner.publicKey(), CraftingDomain, 'confirmed', ); expect( craftingDomainDataEquals(account1.data, { profile: randomProfile.publicKey(), namespace: _domainNamespace, version: 0, }), ).toBe(true); const ix2 = CraftingDomain.updateDomain( program, _domainSigner.publicKey(), domainManagerKey, randomProfile.publicKey(), craftingProfile.publicKey(), { keyIndex: DOMAIN_MANAGER }, ); await buildSendAndCheck(ix2, funder, provider.connection); const account2 = await readFromRPCOrError( provider.connection, program, _domainSigner.publicKey(), CraftingDomain, 'confirmed', ); expect( craftingDomainDataEquals(account2.data, { ...account1.data, profile: craftingProfile.publicKey(), }), ).toBe(true); }); it('wrong key cannot manage a domain', async function () { // try update const updateIx = CraftingDomain.updateDomain( program, _domainSigner.publicKey(), domainManagerKey, craftingProfile.publicKey(), randomProfile.publicKey(), { keyIndex: 0 }, ); let success1 = true; try { await buildSendAndCheck(updateIx, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success1 = false; } expect(success1).toBe(false); }); it('can register craftable items', async function () { const itemsInput = _craftableItemKeys.map((it, index) => { return { mint: it.publicKey, mintAuthority: admin, namespace: `craftableItem${index}`, }; }); await registerCraftableItems( itemsInput, walletSigner, _domainSigner.publicKey(), craftableItemManagerKey, craftingProfile.publicKey(), CRAFTABLE_ITEM_MANAGER, provider.connection, program, ); const addressResults = _craftableItemKeys.map((it) => CraftableItem.findAddress( program, _domainSigner.publicKey(), it.publicKey, ), ); const accounts = await Promise.all( addressResults.map((it) => readFromRPCOrError( provider.connection, program, it[0], CraftableItem, 'confirmed', ), ), ); for (let index = 0; index < accounts.length; index++) { const account = accounts[index]; const accountKp = _craftableItemKeys[index]; const addressResult = addressResults[index]; if (account && accountKp && addressResult) { expect(account.key.equals(addressResult[0])).toBe(true); expect( craftableItemDataEquals(account.data, { domain: _domainSigner.publicKey(), creator: PublicKey.default, mint: accountKp.publicKey, namespace: stringToByteArray(itemsInput[index].namespace, 32), version: 0, bump: addressResult[1], }), ).toBe(true); } else { throw 'element should exist in array'; } } }); it('can fund craftable item banks', async function () { for (let index = 0; index < _craftableItemKeys.length; index++) { const mint = _craftableItemKeys[index]; if (!mint) { throw 'mint should exist'; } const mineItemKey = CraftableItem.findAddress( program, _domainSigner.publicKey(), mint.publicKey, )[0]; const createIx = createAssociatedTokenAccount( mint.publicKey, mineItemKey, true, ); const fundIx = CraftableItem.fundCraftableItemBank( mineItemKey, mint.publicKey, admin, await getAssociatedTokenAddress( mint.publicKey, admin.publicKey(), true, ), 100_000, ); const ixs = [createIx.instructions, fundIx]; await buildSendAndCheck(ixs.flat(), walletSigner, provider.connection); await checkTokenBalance( await getAssociatedTokenAddress(mint.publicKey, mineItemKey, true), new BN(100_000), provider.connection, ); } }); it('can register a recipe category', async function () { const name = 'recipe category 🔠'; const namespace = stringToByteArray(name, 32); const ix = RecipeCategory.registerRecipeCategory( program, _recipeCategorySigner, recipeCategoryManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { namespace, keyIndex: RECIPE_CATEGORY_MANAGER }, ); await buildSendAndCheck(ix, walletSigner, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeCategorySigner.publicKey(), RecipeCategory, 'confirmed', ); expect(account.key.equals(_recipeCategorySigner.publicKey())).toBe(true); expect( recipeCategoryDataEquals(account.data, { domain: _domainSigner.publicKey(), creator: PublicKey.default, namespace: namespace, recipeCount: 0, version: 0, }), ).toBe(true); expect(byteArrayToString(account.data.namespace)).toBe(name); }); it('non-admin cannot manage a recipe category', async function () { // try deregister const ix = RecipeCategory.deregisterRecipeCategory( program, _facilitySigner.publicKey(), recipeCategoryManagerKey, craftingProfile.publicKey(), funder.publicKey(), _domainSigner.publicKey(), { keyIndex: CRAFTING_FACILITY_MANAGER }, ); let success2 = true; try { await buildSendAndCheck(ix, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success2 = false; } expect(success2).toBe(false); }); it('can register a recipe', async function () { const name = 'recipe 👩‍🍳'; const namespace = stringToByteArray(name, 32); const duration = new BN(10000); const minDuration = new BN(1000); const feeAmount = new BN(1337); const value = new BN(12); const feeMint = _craftableItemKeys[0]; if (!feeMint) { throw 'mint should exist'; } const mineItemKey = CraftableItem.findAddress( program, _domainSigner.publicKey(), feeMint.publicKey, )[0]; const feeRecipient = getAssociatedTokenAddressSync( feeMint.publicKey, mineItemKey, true, ); // Register recipe const ix = Recipe.registerRecipe( program, _recipeSigner, recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner.publicKey(), { namespace, duration, minDuration, feeAmount, value, keyIndex: RECIPE_MANAGER, }, feeRecipient, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: namespace, duration: duration, minDuration: minDuration, version: 0, category: _recipeCategorySigner.publicKey(), creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 0, outputsCount: 0, totalCount: 0, status: RecipeStatus.Initializing, feeAmount, feeRecipient: { key: feeRecipient }, usageCount: new BN(0), value, usageLimit: u64Max, }), ).toBe(true); expect(byteArrayToString(account.data.namespace)).toBe(name); }); it('can update the recipe fee', async function () { const accountBefore = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); const feeMint = _craftableItemKeys[1]; if (!feeMint) { throw 'mint should exist'; } const mineItemKey = CraftableItem.findAddress( program, _domainSigner.publicKey(), feeMint.publicKey, )[0]; const feeRecipient = getAssociatedTokenAddressSync( feeMint.publicKey, mineItemKey, true, ); const updateRecipeInput = { feeAmount: new BN(200), value: new BN(1000), keyIndex: RECIPE_MANAGER, }; const ix = Recipe.updateRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), updateRecipeInput, feeRecipient, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: accountBefore.data.namespace, duration: accountBefore.data.duration, minDuration: accountBefore.data.minDuration, version: 0, category: accountBefore.data.category, creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 0, outputsCount: 0, totalCount: 0, status: accountBefore.data.status, feeAmount: updateRecipeInput.feeAmount, value: updateRecipeInput.value, feeRecipient: { key: feeRecipient }, usageCount: new BN(0), usageLimit: u64Max, }), ).toBe(true); }); it('can remove the recipe fee', async function () { const accountBefore = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); const updateRecipeInput = { feeAmount: new BN(0), keyIndex: RECIPE_MANAGER, }; const ix = Recipe.updateRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), updateRecipeInput, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: accountBefore.data.namespace, duration: accountBefore.data.duration, minDuration: accountBefore.data.minDuration, value: accountBefore.data.value, version: 0, category: accountBefore.data.category, creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 0, outputsCount: 0, totalCount: 0, status: accountBefore.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, usageCount: new BN(0), usageLimit: u64Max, }), ).toBe(true); }); it('non-admin cannot manage a recipe', async function () { const name = 'new recipe category 🔠'; const newNamespace = stringToByteArray(name, 32); const newRecipeCategorySigner = keypairToAsyncSigner(Keypair.generate()); const ixNewRecipeCategory = RecipeCategory.registerRecipeCategory( program, newRecipeCategorySigner, recipeCategoryManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { namespace: newNamespace, keyIndex: RECIPE_CATEGORY_MANAGER }, ); await buildSendAndCheck( ixNewRecipeCategory, walletSigner, provider.connection, ); const ix = Recipe.updateRecipeCategory( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner.publicKey(), newRecipeCategorySigner.publicKey(), { keyIndex: RECIPE_CATEGORY_MANAGER }, ); let success = true; try { await buildSendAndCheck(ix, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); }); it('can update the recipe category of a recipe', async function () { const newNamespace = stringToByteArray('new 2 recipe category 🔠', 32); const ixNewRecipeCategory = RecipeCategory.registerRecipeCategory( program, _recipeCategorySigner2, recipeCategoryManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { namespace: newNamespace, keyIndex: RECIPE_CATEGORY_MANAGER }, ); await buildSendAndCheck( ixNewRecipeCategory, walletSigner, provider.connection, ); const ix = Recipe.updateRecipeCategory( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner.publicKey(), _recipeCategorySigner2.publicKey(), { keyIndex: RECIPE_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: account.data.namespace, duration: account.data.duration, minDuration: account.data.minDuration, version: 0, category: _recipeCategorySigner2.publicKey(), creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 0, outputsCount: 0, totalCount: 0, status: account.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, usageCount: new BN(0), value: account.data.value, usageLimit: u64Max, }), ).toBe(true); const recipeCategoryAccount = await readFromRPCOrError( provider.connection, program, _recipeCategorySigner2.publicKey(), RecipeCategory, 'confirmed', ); expect( recipeCategoryAccount.key.equals(_recipeCategorySigner2.publicKey()), ).toBe(true); expect(recipeCategoryAccount.data.recipeCount).toBe(1); }); it('non-admin cannot add inputs / outputs to the recipe', async function () { const consumableMint = Keypair.generate(); const ixConsumable = createMint( keypairToAsyncSigner(consumableMint), 9, admin.publicKey(), null, ); await buildSendAndCheck(ixConsumable, walletSigner, provider.connection); const nonConsumableMint = Keypair.generate(); const ixNonConsumable = createMint( keypairToAsyncSigner(nonConsumableMint), 9, admin.publicKey(), null, ); await buildSendAndCheck(ixNonConsumable, walletSigner, provider.connection); const consumableInputInfo: RecipeInputsOutputs = { amount: new anchor.BN(10), mint: consumableMint.publicKey, keyIndex: RECIPE_CATEGORY_MANAGER, }; const ix = Recipe.addConsumableInputToRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), consumableMint.publicKey, consumableInputInfo, ); let success = true; try { await buildSendAndCheck(ix, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); const nonConsumableInputInfo: RecipeInputsOutputs = { amount: new anchor.BN(10), mint: nonConsumableMint.publicKey, keyIndex: RECIPE_CATEGORY_MANAGER, }; const ix2 = Recipe.addNonConsumableInputToRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), nonConsumableMint.publicKey, nonConsumableInputInfo, ); success = true; try { await buildSendAndCheck(ix2, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); const outputMint = Keypair.generate(); const ixOutput = createMint( keypairToAsyncSigner(outputMint), 9, admin.publicKey(), null, ); await buildSendAndCheck(ixOutput, walletSigner, provider.connection); const ixCraftableItem = CraftableItem.registerCraftableItem( program, craftableItemManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), outputMint.publicKey, { keyIndex: CRAFTABLE_ITEM_MANAGER, namespace: stringToByteArray('item1', 32), }, ); await buildSendAndCheck(ixCraftableItem, admin, provider.connection); const outputInfo: RecipeInputsOutputs = { amount: new anchor.BN(1), mint: outputMint.publicKey, keyIndex: RECIPE_CATEGORY_MANAGER, }; const ix3 = Recipe.addOutputToRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), CraftableItem.findAddress( program, _domainSigner.publicKey(), outputMint.publicKey, )[0], outputInfo, ); success = true; try { await buildSendAndCheck(ix3, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); }); it('can add consumable / non consumable inputs to the recipe', async function () { const consumableMint = Keypair.generate(); const ixConsumable = createMint( keypairToAsyncSigner(consumableMint), 9, admin.publicKey(), null, ); await buildSendAndCheck(ixConsumable, walletSigner, provider.connection); const nonConsumableMint = Keypair.generate(); const ixNonConsumable = createMint( keypairToAsyncSigner(nonConsumableMint), 9, admin.publicKey(), null, ); await buildSendAndCheck(ixNonConsumable, walletSigner, provider.connection); const consumableInputInfo: RecipeInputsOutputs = { amount: new anchor.BN(10), mint: consumableMint.publicKey, keyIndex: RECIPE_MANAGER, }; const ix = Recipe.addConsumableInputToRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), consumableMint.publicKey, consumableInputInfo, ); await buildSendAndCheck(ix, admin, provider.connection); const nonConsumableInputInfo: RecipeInputsOutputs = { amount: new anchor.BN(10), mint: nonConsumableMint.publicKey, keyIndex: RECIPE_MANAGER, }; const ix2 = Recipe.addNonConsumableInputToRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), nonConsumableMint.publicKey, nonConsumableInputInfo, ); await buildSendAndCheck(ix2, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: account.data.namespace, duration: account.data.duration, minDuration: account.data.minDuration, version: 0, category: account.data.category, creator: PublicKey.default, consumablesCount: 1, nonConsumablesCount: 1, outputsCount: 0, totalCount: 2, status: account.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, usageCount: new BN(0), value: account.data.value, usageLimit: u64Max, }), ).toBe(true); expect(account.data.consumablesCount).toBe(1); if (account.ingredientInputsOutputs[0]) { expect(account.ingredientInputsOutputs[0].amount.toNumber()).toBe(10); expect( account.ingredientInputsOutputs[0].mint.equals( consumableMint.publicKey, ), ).toBe(true); } else { throw 'Consumable recipe ingredient should exist'; } if (account.ingredientInputsOutputs[1]) { expect(account.ingredientInputsOutputs[1].amount.toNumber()).toBe(10); expect( account.ingredientInputsOutputs[1].mint.equals( nonConsumableMint.publicKey, ), ).toBe(true); } else { throw 'Non-consumable recipe ingredient should exist'; } }); it('can add an output to the recipe', async function () { const outputMint = Keypair.generate(); const ixOutput = createMint( keypairToAsyncSigner(outputMint), 9, admin.publicKey(), null, ); await buildSendAndCheck(ixOutput, walletSigner, provider.connection); const ixCraftableItem = CraftableItem.registerCraftableItem( program, craftableItemManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), outputMint.publicKey, { keyIndex: CRAFTABLE_ITEM_MANAGER, namespace: stringToByteArray('item2', 32), }, ); await buildSendAndCheck(ixCraftableItem, admin, provider.connection); const craftableItemAddress = CraftableItem.findAddress( program, _domainSigner.publicKey(), outputMint.publicKey, )[0]; const craftableItemAccount = await readFromRPCOrError( provider.connection, program, craftableItemAddress, CraftableItem, 'confirmed', ); expect('item2').toEqual( byteArrayToString(craftableItemAccount.data.namespace), ); const outputInfo: RecipeInputsOutputs = { amount: new anchor.BN(1), mint: outputMint.publicKey, keyIndex: RECIPE_MANAGER, }; const ix = Recipe.addOutputToRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), craftableItemAddress, outputInfo, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: account.data.namespace, duration: account.data.duration, minDuration: account.data.minDuration, version: 0, category: account.data.category, creator: PublicKey.default, consumablesCount: 1, nonConsumablesCount: 1, outputsCount: 1, totalCount: 3, status: account.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, usageCount: new BN(0), value: account.data.value, usageLimit: u64Max, }), ).toBe(true); expect(account.data.outputsCount).toBe(1); if (account.ingredientInputsOutputs[2]) { expect(account.ingredientInputsOutputs[2].amount.toNumber()).toBe(1); expect( account.ingredientInputsOutputs[2].mint.equals(outputMint.publicKey), ).toBe(true); } else { throw 'Output should exist'; } }); it('non-admin cannot remove inputs / outputs from the recipe', async function () { const ix = Recipe.removeConsumableInputFromRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { ingredientIndex: 0, keyIndex: RECIPE_CATEGORY_MANAGER }, ); let success = true; try { await buildSendAndCheck(ix, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); const ix2 = Recipe.removeNonConsumableInputFromRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { ingredientIndex: 1, keyIndex: RECIPE_CATEGORY_MANAGER }, ); success = true; try { await buildSendAndCheck(ix2, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); const ix3 = Recipe.removeNonConsumableInputFromRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { ingredientIndex: 2, keyIndex: RECIPE_CATEGORY_MANAGER }, ); success = true; try { await buildSendAndCheck(ix3, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); }); it('can remove a consumable input from the recipe', async function () { const ix = Recipe.removeConsumableInputFromRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { ingredientIndex: 0, keyIndex: RECIPE_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: account.data.namespace, duration: account.data.duration, minDuration: account.data.minDuration, version: 0, category: account.data.category, creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 1, outputsCount: 1, totalCount: 2, status: account.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, value: account.data.value, usageCount: new BN(0), usageLimit: u64Max, }), ).toBe(true); expect(account.data.outputsCount).toBe(1); if (account.ingredientInputsOutputs) { expect(account.ingredientInputsOutputs.length).toBe(2); } }); it('can remove a non-consumable input from the recipe', async function () { const ix = Recipe.removeNonConsumableInputFromRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { ingredientIndex: 0, keyIndex: RECIPE_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: account.data.namespace, duration: account.data.duration, minDuration: account.data.minDuration, version: 0, category: account.data.category, creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 0, outputsCount: 1, totalCount: 1, status: account.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, usageCount: new BN(0), value: account.data.value, usageLimit: u64Max, }), ).toBe(true); expect(account.data.outputsCount).toBe(1); if (account.ingredientInputsOutputs) { expect(account.ingredientInputsOutputs.length).toBe(1); } }); it('can remove an output from the recipe', async function () { const ix = await Recipe.removeOutputFromRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { ingredientIndex: 0, keyIndex: RECIPE_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.key.equals(_recipeSigner.publicKey())).toBe(true); expect( recipeDataEquals(account.data, { domain: _domainSigner.publicKey(), namespace: account.data.namespace, duration: account.data.duration, minDuration: account.data.minDuration, version: 0, category: account.data.category, creator: PublicKey.default, consumablesCount: 0, nonConsumablesCount: 0, outputsCount: 0, totalCount: 0, status: account.data.status, feeAmount: new BN(0), feeRecipient: { key: PublicKey.default }, usageCount: new BN(0), value: account.data.value, usageLimit: u64Max, }), ).toBe(true); expect(account.data.outputsCount).toBe(0); if (account.ingredientInputsOutputs) { expect(account.ingredientInputsOutputs.length).toBe(0); } }); it('non-admin cannot update the recipe duration', async function () { const updateRecipeInput = { duration: new BN(1500), minDuration: new BN(250), keyIndex: RECIPE_CATEGORY_MANAGER, }; const ix = Recipe.updateRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), updateRecipeInput, ); let success = true; try { await buildSendAndCheck(ix, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success = false; } expect(success).toBe(false); }); it('can update the recipe duration', async function () { const updateRecipeInput = { duration: new BN(1500), minDuration: new BN(250), keyIndex: RECIPE_MANAGER, }; const ix = Recipe.updateRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), updateRecipeInput, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.data.duration.eq(updateRecipeInput.duration)).toBe(true); expect(account.data.minDuration.eq(updateRecipeInput.minDuration)).toBe( true, ); }); it('can update the recipe limit', async function () { const updateRecipeInput = { usageLimit: new BN(2), keyIndex: RECIPE_MANAGER, }; const ix = Recipe.updateRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), updateRecipeInput, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(account.data.usageLimit.eq(updateRecipeInput.usageLimit)).toBe(true); }); it('can register a crafting facility', async function () { const input = { locationType: CraftingFacilityLocationType.Starbase, efficiency: 10, maxConcurrentProcesses: 500, keyIndex: CRAFTING_FACILITY_MANAGER, }; const ix = CraftingFacility.registerCraftingFacility( program, _facilitySigner, craftingFacilityManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), facilityLocationSigner.publicKey(), input, ); await buildSendAndCheck(ix, walletSigner, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); expect(account.key.equals(_facilitySigner.publicKey())).toBe(true); expect(account.recipeCategories.length).toBe(0); expect(account.data.locationType).toBe( getNumberFromEnum( CraftingFacilityLocationType, CraftingFacilityLocationType.Starbase, ), ); expect( craftingFacilityDataEquals(account.data, { domain: _domainSigner.publicKey(), location: facilityLocationSigner.publicKey(), locationType: getNumberFromEnum( CraftingFacilityLocationType, CraftingFacilityLocationType.Starbase, ), maxConcurrentProcesses: 500, numConcurrentProcesses: 0, efficiency: 10, numRecipeCategories: 0, version: 0, }), ).toBe(true); }); it('can update a crafting facility', async function () { const input = { efficiency: 10000, maxConcurrentProcesses: 5, keyIndex: CRAFTING_FACILITY_MANAGER, }; const ix = CraftingFacility.updateCraftingFacility( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), input, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); expect( craftingFacilityDataEquals(account.data, { domain: _domainSigner.publicKey(), location: facilityLocationSigner.publicKey(), locationType: getNumberFromEnum( CraftingFacilityLocationType, CraftingFacilityLocationType.Starbase, ), maxConcurrentProcesses: 5, numConcurrentProcesses: 0, efficiency: 10000, numRecipeCategories: 0, version: 0, }), ).toBe(true); }); it('can add a recipe category to a crafting facility', async function () { const ix = CraftingFacility.addCraftingFacilityRecipeCategory( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner2.publicKey(), { keyIndex: CRAFTING_FACILITY_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); expect(account.recipeCategories.length).toBe(1); expect( account.recipeCategories[0]?.equals(_recipeCategorySigner2.publicKey()), ).toBe(true); }); it('can activate a recipe', async function () { // non-admin cannot activate the recipe let success = true; try { await buildSendAndCheck( Recipe.updateRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: RECIPE_CATEGORY_MANAGER, }, ), randomSigner, provider.connection, { suppressLogging: true, }, ); } catch (error) { success = false; } expect(success).toBe(false); // Active recipe await buildSendAndCheck( Recipe.updateRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: RECIPE_MANAGER, }, ), admin, provider.connection, ); const updateRecipeAccount = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(updateRecipeAccount.data.status).toBe(RecipeStatus.Active); }); it('can create a crafting process account', async function () { const { addressResult } = _setupCraftingProcess(undefined, _recipeSigner); const input = { craftingId: new BN(1), recipeCategoryIndex: 0, quantity: new BN(2), denyPermissionlessClaiming: false, useLocalTime: false, }; const ix = CraftingProcess.createCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, _facilitySigner.publicKey(), _recipeSigner.publicKey(), input, ); await buildSendAndCheck(ix, walletSigner, provider.connection); const account = await readFromRPCOrError( provider.connection, program, addressResult[0], CraftingProcess, 'confirmed', ); expect(account.key.equals(addressResult[0])).toBe(true); expect( craftingProcessDataEquals(account.data, { authority: craftingProcessAuthSigner.publicKey(), craftingFacility: _facilitySigner.publicKey(), recipe: _recipeSigner.publicKey(), craftingId: input.craftingId, quantity: input.quantity, status: getNumberFromEnum( CraftingProcessStatus, CraftingProcessStatus.Initialized, ), inputsChecksum: Array(16).fill(0), outputsChecksum: Array(16).fill(0), startTime: new BN(0), endTime: new BN(0), denyPermissionlessClaiming: 0, useLocalTime: 0, version: 0, bump: addressResult[1], }), ).toBe(true); }); it('can cancel a crafting process account', async function () { const { addressResult } = _setupCraftingProcess(undefined, _recipeSigner); const ix = CraftingProcess.cancelCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, admin.publicKey(), addressResult[0], _facilitySigner.publicKey(), ); await buildSendAndCheck(ix, walletSigner, provider.connection); let success = true; try { await readFromRPCOrError( provider.connection, program, addressResult[0], CraftingProcess, 'confirmed', ); } catch (error) { success = false; expect((error as Error).message).toBe('account does not exist'); } expect(success).toBe(false); }); it('can cancel a crafting process account with partial deposits', async function () { const ingredientKeys = [...Array(3).keys()].map(() => Keypair.generate()); // create mints for ingredients (mints for outputs already exist) await createManyMints( ingredientKeys, admin.publicKey(), walletSigner, provider.connection, ); // setup recipe, and recipe category const thisRecipeCategorySigner = keypairToAsyncSigner(Keypair.generate()); const thisRecipeSigner = keypairToAsyncSigner(Keypair.generate()); await createRecipeAndRecipeCategory( _facilitySigner.publicKey(), thisRecipeSigner, thisRecipeCategorySigner, walletSigner, new BN(1), new BN(1), _domainSigner.publicKey(), [craftingFacilityManagerKey, recipeCategoryManagerKey, recipeManagerKey], craftingProfile.publicKey(), [CRAFTING_FACILITY_MANAGER, RECIPE_CATEGORY_MANAGER, RECIPE_MANAGER], provider.connection, program, ); // add recipe inputs and outputs await registerRecipeInputOutputs( thisRecipeSigner.publicKey(), ingredientKeys.map((it) => [it.publicKey, 10]) /** consumable */, [] /** non-consumable */, [] /** output */, walletSigner, _domainSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), RECIPE_MANAGER, provider.connection, program, ); // activate recipe await buildSendAndCheck( Recipe.updateRecipe( program, thisRecipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: RECIPE_MANAGER, }, ), admin, provider.connection, ); // create crafting process const craftingProcessInput = { craftingId: new BN(11), recipeCategoryIndex: 1, quantity: new BN(1), denyPermissionlessClaiming: false, useLocalTime: false, }; const craftingProcessSetup = _setupCraftingProcess( craftingProcessInput.craftingId, thisRecipeSigner, ); await buildSendAndCheck( CraftingProcess.createCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, _facilitySigner.publicKey(), thisRecipeSigner.publicKey(), craftingProcessInput, ), walletSigner, provider.connection, ); // create and fund ingredient token accounts const initialTokenAmount = 1000; await Promise.all([ createAndFundTokenAccounts( admin, ingredientKeys.map((it) => [it.publicKey, initialTokenAmount]), craftingProcessAuthSigner.publicKey(), funder, walletSigner, provider.connection, ) /** used to deposit into crafting process */, createAndFundTokenAccounts( admin, ingredientKeys.map((it) => [it.publicKey, 0]), craftingProcessSetup.addressResult[0], funder, walletSigner, provider.connection, ) /** used to receive deposits into crafting process */, ]); const craftingProcessIngredientTokens = await Promise.all( ingredientKeys.map((it) => getAssociatedTokenAddress( it.publicKey, craftingProcessSetup.addressResult[0], true, ), ), ); const craftingAuthorityInputTokens = await Promise.all( ingredientKeys.map((it) => getAssociatedTokenAddress( it.publicKey, craftingProcessAuthSigner.publicKey(), true, ), ), ); // add ingredients to crafting process const addConsumablesIx = ingredientKeys.slice(0, 3).map((_, index) => { const tokenFrom = craftingAuthorityInputTokens[index]; const tokenTo = craftingProcessIngredientTokens[index]; if (tokenFrom && tokenTo) { return CraftingProcess.addRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), tokenFrom, tokenTo, { amount: new BN(3), ingredientIndex: index, }, ); } else { throw 'element should exist in array'; } }); await buildSendAndCheck( addConsumablesIx, walletSigner, provider.connection, ); const processAccount = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); const outputsChecksum = 2 ** ingredientKeys.slice(0, 3).length - 1; const outputsChecksumArray = Array.from( numberToLEByteArray(outputsChecksum), ); expect( arrayDeepEquals( processAccount.data.inputsChecksum, Array(16).fill(0), (a, b) => a === b, ), ).toBe(true); expect( arrayDeepEquals( Array.from(processAccount.data.outputsChecksum), outputsChecksumArray, (a, b) => a == b, ), ).toBe(true); // can't cancel with partial deposits let success = true; try { await buildSendAndCheck( CraftingProcess.cancelCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, admin.publicKey(), craftingProcessSetup.addressResult[0], _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); } catch (error) { success = false; expect(JSON.stringify(error as Error)).toContain( 'IncorrectOutputsChecksum', ); } expect(success).toBe(false); // withdraw one ingredient partially await buildSendAndCheck( CraftingProcess.removeRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), craftingProcessIngredientTokens[1], craftingAuthorityInputTokens[1], ingredientKeys.slice(0, 3)[1].publicKey, { amount: new BN(2), ingredientIndex: 1, }, ), walletSigner, provider.connection, ); // checksum is the same const processAccount2 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( Array.from(processAccount2.data.outputsChecksum), outputsChecksumArray, (a, b) => a == b, ), ).toBe(true); // withdraw all ingredients fully const removeConsumablesIx = ingredientKeys.slice(0, 3).map((it, index) => { const tokenTo = craftingAuthorityInputTokens[index]; const tokenFrom = craftingProcessIngredientTokens[index]; if (tokenFrom && tokenTo) { return CraftingProcess.removeRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), tokenFrom, tokenTo, it.publicKey, { amount: index === 1 ? new BN(1) : new BN(3), ingredientIndex: index, }, ); } else { throw 'element should exist in array'; } }); await buildSendAndCheck( removeConsumablesIx, walletSigner, provider.connection, ); // checksum now 0 const processAccount3 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( processAccount3.data.outputsChecksum, Array(16).fill(0), (a, b) => a === b, ), ).toBe(true); // can now cancel okay await buildSendAndCheck( CraftingProcess.cancelCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, admin.publicKey(), craftingProcessSetup.addressResult[0], _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); let success1 = true; try { await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); } catch (error) { success1 = false; expect((error as Error).message).toBe('account does not exist'); } expect(success1).toBe(false); // clean up recipe and recipe category const facilityAccount = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); const recipeCategoryIndexInFacility = facilityAccount.recipeCategories.findIndex((element: PublicKey) => element.equals(thisRecipeCategorySigner.publicKey()), ); await cleanupRecipeAndRecipeCategory( _facilitySigner.publicKey(), thisRecipeSigner.publicKey(), thisRecipeCategorySigner.publicKey(), recipeCategoryIndexInFacility, funder, walletSigner, _domainSigner.publicKey(), [craftingFacilityManagerKey, recipeCategoryManagerKey, recipeManagerKey], craftingProfile.publicKey(), [CRAFTING_FACILITY_MANAGER, RECIPE_CATEGORY_MANAGER, RECIPE_MANAGER], provider.connection, program, ); }); it('can successfully go through the crafting process', async function () { const ingredientKeys = [...Array(3).keys()].map(() => Keypair.generate()); const outputKeys = _craftableItemKeys.slice(-2); // create mints for ingredients (mints for outputs already exist) await createManyMints( ingredientKeys, admin.publicKey(), walletSigner, provider.connection, ); // setup recipe, and recipe category const thisRecipeCategorySigner = keypairToAsyncSigner(Keypair.generate()); const thisRecipeSigner = keypairToAsyncSigner(Keypair.generate()); const thisRecipeUsageLimit = new BN(1337); await createRecipeAndRecipeCategory( _facilitySigner.publicKey(), thisRecipeSigner, thisRecipeCategorySigner, walletSigner, new BN(1), new BN(1), _domainSigner.publicKey(), [craftingFacilityManagerKey, recipeCategoryManagerKey, recipeManagerKey], craftingProfile.publicKey(), [CRAFTING_FACILITY_MANAGER, RECIPE_CATEGORY_MANAGER, RECIPE_MANAGER], provider.connection, program, undefined /** recipeName */, undefined /** recipeCategoryName */, thisRecipeUsageLimit /** recipeLimit */, ); // add recipe inputs and outputs const consumablesQty = 2; const nonConsumablesQty = 1; const outputsQty = 1337; await registerRecipeInputOutputs( thisRecipeSigner.publicKey(), ingredientKeys .slice(0, 2) .map((it) => [it.publicKey, consumablesQty]) /** consumable */, ingredientKeys .slice(2, 3) .map((it) => [it.publicKey, nonConsumablesQty]) /** non-consumable */, outputKeys.map((it) => [it.publicKey, outputsQty]) /** output */, walletSigner, _domainSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), RECIPE_MANAGER, provider.connection, program, ); // activate recipe await buildSendAndCheck( Recipe.updateRecipe( program, thisRecipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: RECIPE_MANAGER, }, ), admin, provider.connection, ); const updateRecipeAccount = await readFromRPCOrError( provider.connection, program, thisRecipeSigner.publicKey(), Recipe, 'confirmed', ); expect(updateRecipeAccount.data.status).toBe(RecipeStatus.Active); expect(updateRecipeAccount.data.usageLimit.eq(thisRecipeUsageLimit)).toBe( true, ); // create crafting process const craftingProcessInput = { craftingId: new BN(12), recipeCategoryIndex: 1, quantity: new BN(2), denyPermissionlessClaiming: false, useLocalTime: false, }; const craftingProcessSetup = _setupCraftingProcess( craftingProcessInput.craftingId, thisRecipeSigner, ); const createIx = CraftingProcess.createCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, _facilitySigner.publicKey(), thisRecipeSigner.publicKey(), craftingProcessInput, ); await buildSendAndCheck(createIx, walletSigner, provider.connection); // create and fund ingredient token accounts const initialTokenAmount = 1000; await Promise.all([ createAndFundTokenAccounts( admin, ingredientKeys.map((it) => [it.publicKey, initialTokenAmount]), craftingProcessAuthSigner.publicKey(), funder, walletSigner, provider.connection, ) /** used to deposit into crafting process */, createAndFundTokenAccounts( admin, ingredientKeys.map((it) => [it.publicKey, 0]), craftingProcessSetup.addressResult[0], funder, walletSigner, provider.connection, ) /** used to receive deposits into crafting process */, ]); // create output token accounts owned by the crafting authority await buildSendAndCheck( outputKeys.map( (it) => createAssociatedTokenAccount( it.publicKey, craftingProcessAuthSigner.publicKey(), ).instructions, ), walletSigner, provider.connection, ); const craftingProcessIngredientTokens = await Promise.all( ingredientKeys.map((it) => getAssociatedTokenAddress( it.publicKey, craftingProcessSetup.addressResult[0], true, ), ), ); const craftingAuthorityInputTokens = await Promise.all( ingredientKeys.map((it) => getAssociatedTokenAddress( it.publicKey, craftingProcessAuthSigner.publicKey(), true, ), ), ); const craftingAuthorityOutputTokens = await Promise.all( outputKeys.map((it) => getAssociatedTokenAddress( it.publicKey, craftingProcessAuthSigner.publicKey(), true, ), ), ); const processAccount = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( processAccount.data.inputsChecksum, Array(16).fill(0), (a, b) => a === b, ), ).toBe(true); expect( arrayDeepEquals( processAccount.data.outputsChecksum, Array(16).fill(0), (a, b) => a === b, ), ).toBe(true); // add ingredients to crafting process const addConsumablesIx = ingredientKeys.slice(0, 3).map((_, index) => { const tokenFrom = craftingAuthorityInputTokens[index]; const tokenTo = craftingProcessIngredientTokens[index]; if (tokenFrom && tokenTo) { return CraftingProcess.addRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), tokenFrom, tokenTo, { amount: new BN(index < 2 ? consumablesQty : nonConsumablesQty).mul( craftingProcessInput.quantity, ), ingredientIndex: index, }, ); } else { throw 'element should exist in array'; } }); await buildSendAndCheck( (await Promise.all(addConsumablesIx.flat())).flat(), walletSigner, provider.connection, ); const processAccount1 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); const inputsChecksum1 = 2 ** ingredientKeys.length - 1; const inputsChecksum1Array = Array.from( numberToLEByteArray(inputsChecksum1), ); expect( arrayDeepEquals( Array.from(processAccount1.data.inputsChecksum), inputsChecksum1Array, (a, b) => a == b, ), ).toBe(true); // remove and add back one ingredient via the legitimize ix const indexToChange = 1; const changingTokenFrom = craftingProcessIngredientTokens[indexToChange]; const changingTokenTo = craftingAuthorityInputTokens[indexToChange]; const changingTokenMint = ingredientKeys[indexToChange]; if (changingTokenFrom && changingTokenTo && changingTokenMint) { const amountToChange = nonConsumablesQty; await buildSendAndCheck( CraftingProcess.removeRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), changingTokenFrom, changingTokenTo, changingTokenMint.publicKey, { amount: new BN(amountToChange), ingredientIndex: indexToChange, }, ), walletSigner, provider.connection, ); const processAccount2 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); const inputsChecksum2 = inputsChecksum1 - (1 << indexToChange); const inputsChecksum2Array = Array.from( numberToLEByteArray(inputsChecksum2), ); expect( arrayDeepEquals( processAccount2.data.inputsChecksum, inputsChecksum2Array, (a, b) => a === b, ), ).toBe(true); // manually transfer to crafting process token await buildSendAndCheck( await transferToTokenAccount( craftingProcessAuthSigner, changingTokenTo, changingTokenFrom, amountToChange, ), walletSigner, provider.connection, ); // legitimize manually transferred amount await buildSendAndCheck( CraftingProcess.legitimizeRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), changingTokenFrom, { amount: new BN(amountToChange), ingredientIndex: indexToChange, }, ), walletSigner, provider.connection, ); const processAccount3 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( processAccount3.data.inputsChecksum, processAccount1.data.inputsChecksum, (a, b) => a === b, ), ).toBe(true); } else { throw 'element should exist in array'; } // start the process await buildSendAndCheck( CraftingProcess.startCraftingProcess( program, facilityLocationSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); const recipeAccount = await readFromRPCOrError( provider.connection, program, thisRecipeSigner.publicKey(), Recipe, 'confirmed', ); const facilityAccount = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); const processAccount4 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect(processAccount4.data.status).toBe( getNumberFromEnum(CraftingProcessStatus, CraftingProcessStatus.Started), ); let expectedDuration = new BN(recipeAccount.data.duration) .mul(new BN(10000)) .mul(processAccount4.data.quantity) .div(new BN(facilityAccount.data.efficiency)); expectedDuration = BN.max( expectedDuration, new BN(recipeAccount.data.minDuration), ); expect(processAccount4.data.startTime.gt(new BN(0))).toBe(true); expect( processAccount4.data.endTime.eq( processAccount4.data.startTime.add(expectedDuration), ), ).toBe(true); const updateRecipeAccount2 = await readFromRPCOrError( provider.connection, program, thisRecipeSigner.publicKey(), Recipe, 'confirmed', ); expect(updateRecipeAccount.data.status).toBe(RecipeStatus.Active); expect(updateRecipeAccount2.data.usageCount.toNumber()).toBe( updateRecipeAccount.data.usageCount .add(processAccount4.data.quantity) .toNumber(), ); // stop it and start again await buildSendAndCheck( CraftingProcess.stopCraftingProcess( program, facilityLocationSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); const processAccount4b = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect(processAccount4b.data.status).toBe( getNumberFromEnum( CraftingProcessStatus, CraftingProcessStatus.Initialized, ), ); expect(processAccount4b.data.startTime.toNumber()).toBe(0); expect(processAccount4b.data.endTime.toNumber()).toBe(0); // restart it await buildSendAndCheck( CraftingProcess.startCraftingProcess( program, facilityLocationSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); // sleep for `expectedDuration` sec for crafting process duration to end await new Promise((r) => setTimeout(r, (expectedDuration.toNumber() + 1) * 1000), ); // burn consumables let checksumReduction = 0; const burnConsumablesIx = ingredientKeys.slice(0, 2).map((it, index) => { const craftingToken = craftingProcessIngredientTokens[index]; if (craftingToken) { checksumReduction += 1 << index; return CraftingProcess.burnConsumableIngredient( program, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), craftingToken, it.publicKey, { ingredientIndex: index, }, ); } else { throw 'element should exist in array'; } }); await buildSendAndCheck( (await Promise.all(burnConsumablesIx.flat())).flat(), walletSigner, provider.connection, ); const inputsChecksum5 = inputsChecksum1 - checksumReduction; const inputsChecksum5Array = Array.from( numberToLEByteArray(inputsChecksum5), ); const processAccount5 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( processAccount5.data.inputsChecksum, inputsChecksum5Array, (a, b) => a === b, ), ).toBe(true); ingredientKeys.slice(0, 2).map(async (_, index) => { const craftingToken = craftingProcessIngredientTokens[index]; if (craftingToken) { const thisTokenAccount = await connection.getAccountInfo( craftingToken, 'confirmed', ); expect(thisTokenAccount).toBeNull; } else { throw 'element should exist in array'; } }); // withdraw non-consumables const nonConsumableIndex = 2; const nonConsumableMint = ingredientKeys[nonConsumableIndex]; const nonConsumableTokenFrom = craftingProcessIngredientTokens[nonConsumableIndex]; const nonConsumableTokenTo = craftingAuthorityInputTokens[nonConsumableIndex]; if (nonConsumableMint && nonConsumableTokenFrom && nonConsumableTokenTo) { const withdrawNonConsumablesIx = CraftingProcess.claimNonConsumableIngredient( program, craftingProcessSetup.addressResult[0], craftingProcessAuthSigner.publicKey(), thisRecipeSigner.publicKey(), nonConsumableTokenFrom, nonConsumableTokenTo, nonConsumableMint.publicKey, { ingredientIndex: nonConsumableIndex, }, ); await buildSendAndCheck( withdrawNonConsumablesIx, walletSigner, provider.connection, ); } else { throw 'element should exist in array'; } const inputsChecksum5b = 0; const inputsChecksum5bArray = Array.from( numberToLEByteArray(inputsChecksum5b), ); const processAccount5b = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( processAccount5b.data.inputsChecksum, inputsChecksum5bArray, (a, b) => a === b, ), ).toBe(true); await checkTokenBalance( nonConsumableTokenTo, new BN(initialTokenAmount), provider.connection, ); // claim outputs const claimOutputsIx = outputKeys.map(async (it, index) => { const craftableItemKey = CraftableItem.findAddress( program, _domainSigner.publicKey(), it.publicKey, )[0]; const outputTokenFrom = await getAssociatedTokenAddress( it.publicKey, craftableItemKey, true, ); const outputTokenTo = craftingAuthorityOutputTokens[index]; // ingredientIndex = recipe_inputs_count + index const ingredientIndex: number = recipeAccount.data.consumablesCount + recipeAccount.data.nonConsumablesCount + index; if (outputTokenTo) { return CraftingProcess.claimRecipeOutput( program, craftingProcessSetup.addressResult[0], craftingProcessAuthSigner.publicKey(), thisRecipeSigner.publicKey(), outputTokenFrom, outputTokenTo, craftableItemKey, { ingredientIndex, }, ); } else { throw 'element should exist in array'; } }); await buildSendAndCheck( (await Promise.all(claimOutputsIx.flat())).flat(), walletSigner, provider.connection, ); const processAccount6 = await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); expect( arrayDeepEquals( processAccount6.data.inputsChecksum, processAccount5b.data.inputsChecksum, (a, b) => a === b, ), ).toBe(true); const outputsChecksum = 2 ** recipeAccount.data.outputsCount - 1; const outputsChecksumArray = Array.from( numberToLEByteArray(outputsChecksum), ); expect( arrayDeepEquals( processAccount6.data.outputsChecksum, outputsChecksumArray, (a, b) => a === b, ), ).toBe(true); outputKeys.map(async (_, index) => { const outputToken = craftingAuthorityOutputTokens[index]; if (outputToken) { await checkTokenBalance( outputToken, new BN(outputsQty).mul(craftingProcessInput.quantity), provider.connection, ); } else { throw 'element should exist in array'; } }); // close crafting process await buildSendAndCheck( CraftingProcess.closeCraftingProcess( program, craftingProcessAuthSigner, funder.publicKey(), craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); let success = true; try { await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); } catch (error) { success = false; expect((error as Error).message).toBe('account does not exist'); } expect(success).toBe(false); // clean up recipe and recipe category const recipeCategoryIndexInFacility = facilityAccount.recipeCategories.findIndex((element: PublicKey) => element.equals(thisRecipeCategorySigner.publicKey()), ); await cleanupRecipeAndRecipeCategory( _facilitySigner.publicKey(), thisRecipeSigner.publicKey(), thisRecipeCategorySigner.publicKey(), recipeCategoryIndexInFacility, funder, walletSigner, _domainSigner.publicKey(), [craftingFacilityManagerKey, recipeCategoryManagerKey, recipeManagerKey], craftingProfile.publicKey(), [CRAFTING_FACILITY_MANAGER, RECIPE_CATEGORY_MANAGER, RECIPE_MANAGER], provider.connection, program, ); }); it('can successfully go through the crafting process with crafting fees', async function () { const quantity = 3; const fee = Keypair.generate(); const ingredient = Keypair.generate(); const output = _craftableItemKeys[0]; // create mints for fee & ingredient (mint for output already exists) await createManyMints( [fee, ingredient], admin.publicKey(), walletSigner, provider.connection, ); // setup recipe, and recipe category const thisRecipeCategorySigner = keypairToAsyncSigner(Keypair.generate()); const thisRecipeSigner = keypairToAsyncSigner(Keypair.generate()); await createRecipeAndRecipeCategory( _facilitySigner.publicKey(), thisRecipeSigner, thisRecipeCategorySigner, walletSigner, new BN(1), new BN(1), _domainSigner.publicKey(), [craftingFacilityManagerKey, recipeCategoryManagerKey, recipeManagerKey], craftingProfile.publicKey(), [CRAFTING_FACILITY_MANAGER, RECIPE_CATEGORY_MANAGER, RECIPE_MANAGER], provider.connection, program, ); // add recipe inputs and outputs await registerRecipeInputOutputs( thisRecipeSigner.publicKey(), [[ingredient.publicKey, quantity]] /** consumable */, [] /** non-consumable */, [[output.publicKey, quantity]] /** output */, walletSigner, _domainSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), RECIPE_MANAGER, provider.connection, program, ); // add recipe fee & activate recipe const feeRecipientResult = createAssociatedTokenAccount( fee.publicKey, randomSigner.publicKey(), ); await buildSendAndCheck( [ feeRecipientResult.instructions, Recipe.updateRecipe( program, thisRecipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { feeAmount: new BN(quantity), keyIndex: RECIPE_MANAGER, }, feeRecipientResult.address, ), Recipe.updateRecipe( program, thisRecipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: RECIPE_MANAGER, }, ), ], admin, provider.connection, ); // create crafting process const craftingProcessInput = { craftingId: new BN(13), recipeCategoryIndex: 1, quantity: new BN(1), denyPermissionlessClaiming: false, useLocalTime: false, }; const craftingProcessSetup = _setupCraftingProcess( craftingProcessInput.craftingId, thisRecipeSigner, ); const createIx = CraftingProcess.createCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, _facilitySigner.publicKey(), thisRecipeSigner.publicKey(), craftingProcessInput, ); await buildSendAndCheck(createIx, walletSigner, provider.connection); // create and fund ingredient token accounts const initialTokenAmount = 1000; await Promise.all([ createAndFundTokenAccounts( admin, [[ingredient.publicKey, initialTokenAmount]], craftingProcessAuthSigner.publicKey(), funder, walletSigner, provider.connection, ) /** used to deposit into crafting process */, createAndFundTokenAccounts( admin, [[fee.publicKey, initialTokenAmount]], admin.publicKey(), funder, walletSigner, provider.connection, ) /** used to pay crafting fees */, createAndFundTokenAccounts( admin, [[ingredient.publicKey, 0]], craftingProcessSetup.addressResult[0], funder, walletSigner, provider.connection, ) /** used to receive deposits into crafting process */, ]); // create more token accounts await buildSendAndCheck( [ createAssociatedTokenAccount( output.publicKey, craftingProcessAuthSigner.publicKey(), ) .instructions /** create output token accounts owned by the crafting authority */, createAssociatedTokenAccount( fee.publicKey, craftingProcessSetup.addressResult[0], ).instructions /** crafting fee escrow token account */, ], walletSigner, provider.connection, ); const craftingProcessIngredientToken = getAssociatedTokenAddressSync( ingredient.publicKey, craftingProcessSetup.addressResult[0], true, ); const craftingAuthorityInputToken = getAssociatedTokenAddressSync( ingredient.publicKey, craftingProcessAuthSigner.publicKey(), true, ); const craftingAuthorityOutputToken = getAssociatedTokenAddressSync( output.publicKey, craftingProcessAuthSigner.publicKey(), true, ); const craftingFeeEscrowToken = getAssociatedTokenAddressSync( fee.publicKey, craftingProcessSetup.addressResult[0], true, ); const craftingFeePayerToken = getAssociatedTokenAddressSync( fee.publicKey, admin.publicKey(), false, ); const craftingFeePayerTokenBalance0 = await connection.getTokenAccountBalance( craftingFeePayerToken, 'confirmed', ); // add ingredients & start the process await buildSendAndCheck( [ CraftingProcess.addRecipeIngredient( program, facilityLocationSigner, craftingProcessAuthSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), craftingAuthorityInputToken, craftingProcessIngredientToken, { amount: new BN(quantity), ingredientIndex: 0, }, ), CraftingProcess.startCraftingProcess( program, facilityLocationSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), { recipeDurationOverride: new BN( 3, ) /** force duration to 3 seconds */, }, feeRecipientResult.address, admin, craftingFeePayerToken, craftingFeeEscrowToken, ), ], walletSigner, provider.connection, ); // fee was paid into escrow const craftingFeeEscrowTokenBalance0 = await connection.getTokenAccountBalance( craftingFeeEscrowToken, 'confirmed', ); const craftingFeePayerTokenBalance1 = await connection.getTokenAccountBalance( craftingFeePayerToken, 'confirmed', ); expect( Number(craftingFeePayerTokenBalance0.value.amount) - Number(craftingFeePayerTokenBalance1.value.amount), ).toBe(quantity); expect(Number(craftingFeeEscrowTokenBalance0.value.amount)).toBe(quantity); // stop it and start again await buildSendAndCheck( CraftingProcess.stopCraftingProcess( program, facilityLocationSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), undefined /** no localTime */, craftingFeeEscrowToken, craftingFeePayerToken, ), walletSigner, provider.connection, ); // refund was received const craftingFeeEscrowTokenAfter = await connection.getAccountInfo( craftingFeeEscrowToken, 'confirmed', ); expect(craftingFeeEscrowTokenAfter).toBeNull(); const craftingFeePayerTokenBalance2 = await connection.getTokenAccountBalance( craftingFeePayerToken, 'confirmed', ); expect( Number(craftingFeePayerTokenBalance2.value.amount) - Number(craftingFeePayerTokenBalance1.value.amount), ).toBe(quantity); expect(Number(craftingFeePayerTokenBalance2.value.amount)).toBe( Number(craftingFeePayerTokenBalance0.value.amount), ); // restart it await buildSendAndCheck( [ createAssociatedTokenAccount( fee.publicKey, craftingProcessSetup.addressResult[0], ).instructions, CraftingProcess.startCraftingProcess( program, facilityLocationSigner, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), undefined /** no override */, feeRecipientResult.address, admin, craftingFeePayerToken, craftingFeeEscrowToken, ), ], walletSigner, provider.connection, ); // sleep for 2 sec for crafting process duration to end await new Promise((r) => setTimeout(r, 2000)); // burn consumable & claim output & close process const craftableItemKey = CraftableItem.findAddress( program, _domainSigner.publicKey(), output.publicKey, )[0]; const outputTokenFrom = await getAssociatedTokenAddress( output.publicKey, craftableItemKey, true, ); await buildSendAndCheck( [ CraftingProcess.burnConsumableIngredient( program, craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), craftingProcessIngredientToken, ingredient.publicKey, { ingredientIndex: 0, }, ), CraftingProcess.claimRecipeOutput( program, craftingProcessSetup.addressResult[0], craftingProcessAuthSigner.publicKey(), thisRecipeSigner.publicKey(), outputTokenFrom, craftingAuthorityOutputToken, craftableItemKey, { ingredientIndex: 1, }, ), CraftingProcess.closeCraftingProcess( program, craftingProcessAuthSigner, funder.publicKey(), craftingProcessSetup.addressResult[0], thisRecipeSigner.publicKey(), _facilitySigner.publicKey(), craftingFeeEscrowToken, feeRecipientResult.address, ), ], walletSigner, provider.connection, ); // confirm process was closed let success = true; try { await readFromRPCOrError( provider.connection, program, craftingProcessSetup.addressResult[0], CraftingProcess, 'confirmed', ); } catch (error) { success = false; expect((error as Error).message).toBe('account does not exist'); } expect(success).toBe(false); // fee recipient was paid crafting fees const feeRecipientBalance = await connection.getTokenAccountBalance( feeRecipientResult.address, 'confirmed', ); expect(Number(feeRecipientBalance.value.amount)).toBe(quantity); // clean up recipe and recipe category const facilityAccount = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); const recipeCategoryIndexInFacility = facilityAccount.recipeCategories.findIndex((element: PublicKey) => element.equals(thisRecipeCategorySigner.publicKey()), ); await cleanupRecipeAndRecipeCategory( _facilitySigner.publicKey(), thisRecipeSigner.publicKey(), thisRecipeCategorySigner.publicKey(), recipeCategoryIndexInFacility, funder, walletSigner, _domainSigner.publicKey(), [craftingFacilityManagerKey, recipeCategoryManagerKey, recipeManagerKey], craftingProfile.publicKey(), [CRAFTING_FACILITY_MANAGER, RECIPE_CATEGORY_MANAGER, RECIPE_MANAGER], provider.connection, program, ); }); it('can deactivate a recipe', async function () { // non-admin cannot deactivate the recipe let success = true; try { await buildSendAndCheck( Recipe.updateRecipe( program, _recipeCategorySigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Deactivated, keyIndex: RECIPE_CATEGORY_MANAGER, }, ), randomSigner, provider.connection, { suppressLogging: true, }, ); } catch (error) { success = false; } expect(success).toBe(false); // deactivate recipe await buildSendAndCheck( Recipe.updateRecipe( program, _recipeSigner.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Deactivated, keyIndex: RECIPE_MANAGER, }, ), admin, provider.connection, ); const updateRecipeAccount = await readFromRPCOrError( provider.connection, program, _recipeSigner.publicKey(), Recipe, 'confirmed', ); expect(updateRecipeAccount.data.status).toBe(RecipeStatus.Deactivated); }); it('cannot exceed recipe usage limit', async function () { // Register recipe with no inputs or outputs const emptyRecipe = keypairToAsyncSigner(Keypair.generate()); await buildSendAndCheck( [ Recipe.registerRecipe( program, emptyRecipe, recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner2.publicKey(), { namespace: stringToByteArray('Oo', 32), duration: new BN(1), minDuration: new BN(1), usageLimit: new BN(1), keyIndex: RECIPE_MANAGER, }, ), Recipe.updateRecipe( program, emptyRecipe.publicKey(), recipeManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: RECIPE_MANAGER, }, ), ], walletSigner, provider.connection, ); const facilityAccount = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); const recipeCategoryIndex = facilityAccount.recipeCategories.findIndex( (element: PublicKey) => element.equals(_recipeCategorySigner2.publicKey()), ); for (let index = 0; index < 2; index++) { const craftingId = new BN(33 * index); const { addressResult } = _setupCraftingProcess(craftingId, emptyRecipe); const createIx = CraftingProcess.createCraftingProcess( program, facilityLocationSigner, craftingProcessAuthSigner, _facilitySigner.publicKey(), emptyRecipe.publicKey(), { craftingId, recipeCategoryIndex, quantity: new BN(1), denyPermissionlessClaiming: false, useLocalTime: false, }, ); const startIx = CraftingProcess.startCraftingProcess( program, facilityLocationSigner, addressResult[0], emptyRecipe.publicKey(), _facilitySigner.publicKey(), ); let success = true; try { await buildSendAndCheck( [createIx, startIx], walletSigner, provider.connection, ); } catch (error) { success = false; } await new Promise((r) => setTimeout(r, 2000)); if (index == 0) { expect(success).toBe(true); await buildSendAndCheck( CraftingProcess.closeCraftingProcess( program, craftingProcessAuthSigner, 'funder', addressResult[0], emptyRecipe.publicKey(), _facilitySigner.publicKey(), ), walletSigner, provider.connection, ); } else { expect(success).toBe(false); } } }); it('non-admins cannot manage crafting facility recipe categories', async function () { // non-admin cannot add recipe category const failingIx1 = CraftingFacility.addCraftingFacilityRecipeCategory( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner2.publicKey(), { keyIndex: CRAFTABLE_ITEM_MANAGER }, ); let success1 = true; try { await buildSendAndCheck(failingIx1, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success1 = false; } expect(success1).toBe(false); // non-admin cannot remove recipe category const failingIx2 = CraftingFacility.removeCraftingFacilityRecipeCategory( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner2.publicKey(), { recipeCategoryIndex: 0, keyIndex: CRAFTABLE_ITEM_MANAGER }, ); let success2 = true; try { await buildSendAndCheck(failingIx2, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success2 = false; } expect(success2).toBe(false); }); it('can remove a recipe category from a crafting facility', async function () { const ix = CraftingFacility.removeCraftingFacilityRecipeCategory( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), _recipeCategorySigner2.publicKey(), { recipeCategoryIndex: 0, keyIndex: CRAFTING_FACILITY_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); const account = await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), CraftingFacility, 'confirmed', ); expect(account.recipeCategories.length).toBe(0); }); it('non-admin cannot manage a crafting facility', async function () { // try deregister const ix = CraftingFacility.deregisterCraftingFacility( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), funder.publicKey(), _domainSigner.publicKey(), { keyIndex: CRAFTABLE_ITEM_MANAGER }, ); let success2 = true; try { await buildSendAndCheck(ix, randomSigner, provider.connection, { suppressLogging: true, }); } catch (error) { success2 = false; } expect(success2).toBe(false); }); it('can deregister a recipe category', async function () { const recipeCategory = keypairToAsyncSigner(Keypair.generate()); const createIx = RecipeCategory.registerRecipeCategory( program, recipeCategory, recipeCategoryManagerKey, craftingProfile.publicKey(), _domainSigner.publicKey(), { namespace: stringToByteArray('Oo', 32), keyIndex: RECIPE_CATEGORY_MANAGER, }, ); const deregisterIx = RecipeCategory.deregisterRecipeCategory( program, recipeCategory.publicKey(), recipeCategoryManagerKey, craftingProfile.publicKey(), funder.publicKey(), _domainSigner.publicKey(), { keyIndex: RECIPE_CATEGORY_MANAGER }, ); await buildSendAndCheck( [createIx, deregisterIx], admin, provider.connection, ); let success = true; try { await readFromRPCOrError( provider.connection, program, recipeCategory.publicKey(), RecipeCategory, 'confirmed', ); } catch (error) { success = false; expect((error as Error).message).toBe('account does not exist'); } expect(success).toBe(false); }); it('can deregister a crafting facility', async function () { const ix = CraftingFacility.deregisterCraftingFacility( program, _facilitySigner.publicKey(), craftingFacilityManagerKey, craftingProfile.publicKey(), funder.publicKey(), _domainSigner.publicKey(), { keyIndex: CRAFTING_FACILITY_MANAGER }, ); await buildSendAndCheck(ix, admin, provider.connection); let success = true; try { await readFromRPCOrError( provider.connection, program, _facilitySigner.publicKey(), RecipeCategory, 'confirmed', ); } catch (error) { success = false; expect((error as Error).message).toBe('account does not exist'); } expect(success).toBe(false); }); it('can drain craftable item banks', async function () { for (let index = 0; index < _craftableItemKeys.length; index++) { const mint = _craftableItemKeys[index]; if (!mint) { throw 'mint should exist'; } const craftableItemKey = CraftableItem.findAddress( program, _domainSigner.publicKey(), mint.publicKey, )[0]; const bank = getAssociatedTokenAddressSync( mint.publicKey, craftableItemKey, true, ); expect(await connection.getAccountInfo(bank, 'processed')).toBeTruthy; const createTokenToIx = createAssociatedTokenAccount( mint.publicKey, _domainSigner.publicKey(), true, ); const drainIx = CraftableItem.drainCraftableItemBank( program, craftableItemKey, craftableItemManagerKey, craftingProfile.publicKey(), funder.publicKey(), _domainSigner.publicKey(), bank, createTokenToIx.address, { keyIndex: CRAFTABLE_ITEM_MANAGER }, ); const ixs = [createTokenToIx.instructions, drainIx]; await buildSendAndCheck(ixs.flat(), walletSigner, provider.connection); const tokenFromBefore = await getAccount( program.provider.connection, bank, 'processed', ); expect(Number(tokenFromBefore.amount)).toBe(0); } }); });