import { getAssociatedTokenAddress } from '@solana/spl-token'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { BN, Program } from '@staratlas/anchor'; import { AsyncSigner, buildSendAndCheck, createAssociatedTokenAccount, createMint, keypairToAsyncSigner, mintToTokenAccount, stringToByteArray, } from '@staratlas/data-source'; import { PermissionType, PlayerProfile, PlayerProfileIDL, ProfilePermissions, } from '@staratlas/player-profile'; import assert from 'assert'; import { CraftableItem, CraftingFacility, CraftingIDLProgram, Recipe, RecipeCategory, RecipeStatus, } from '../src'; export type CreateProfileKeysInput = { key: PublicKey | AsyncSigner; expireTime: BN | null; permissions: PermissionType; scope: PublicKey; }[]; export const checkTokenBalance = async ( tokenKey: PublicKey, expectedAmount: BN, connection: Connection, ) => { const tokenAccount = await connection.getTokenAccountBalance( tokenKey, 'confirmed', ); if (tokenAccount.value.uiAmount == null) { throw 'token account should be defined'; } else { assert(new BN(tokenAccount.value.amount).eq(expectedAmount)); } }; export const createManyMints = async ( keypairs: Keypair[], mintAuthority: PublicKey, walletSigner: AsyncSigner, connection: Connection, ) => { const ixs = keypairs.map((it) => { return createMint(keypairToAsyncSigner(it), 9, mintAuthority, null); }); await buildSendAndCheck(ixs, walletSigner, connection); }; /** * Creates and funds associated token accounts for the owner and mints * @param mintAuthority - SPL Token mint authority for {@param mints} * @param mints - array of mints for the token accounts that will be created * @param owner - owner of the token accounts to be created * @param funder - token accounts rent funder * @param walletSigner - transaction fee payer * @param connection - Solana Connection object */ export const createAndFundTokenAccounts = async ( mintAuthority: AsyncSigner, mints: Array<[PublicKey, number]>, owner: PublicKey, funder: AsyncSigner, walletSigner: AsyncSigner, connection: Connection, ) => { // create token accounts await buildSendAndCheck( mints.map( (it) => createAssociatedTokenAccount(it[0], owner, true).instructions, ), walletSigner, connection, ); // fund token accounts await buildSendAndCheck( ( await Promise.all( mints.map(async (it) => mintToTokenAccount( mintAuthority, it[0], await getAssociatedTokenAddress(it[0], owner, true), it[1], ), ), ) ).flat(), walletSigner, connection, ); }; export const createProfile = async ( program: Program, profileId: AsyncSigner, funder: AsyncSigner, keys: CreateProfileKeysInput, connection: Connection, authAuthority?: AsyncSigner, walletSigner?: AsyncSigner, keyThreshold = 1, ) => { if (authAuthority) { keys.unshift({ key: authAuthority, expireTime: null, permissions: ProfilePermissions.all(), scope: program.programId, }); } const createix = PlayerProfile.createProfile( program, profileId, keys, keyThreshold, ); await buildSendAndCheck([createix], walletSigner || funder, connection); }; /** Create a new recipe and recipe category * @param facility - The CraftingFacility to register the recipe category to * @param recipe - The new Recipe to be created * @param recipeCategory - The RecipeCategory to be created * @param walletSigner - The transaction fee payer and signer * @param duration - Recipe duration in seconds * @param minDuration - Recipe minimum duration in seconds * @param domain - Domain account key * @param profileSigners - Profile signers for {@param profile} in the order of [facilitySigner, recipeCatSigner, recipeSigner] * @param profile - The profile with the required permissions * @param profileKeyIndices - Profile key indices for {@param profileSigners} in {@param profile} in the order of [facilityKeyIndex, recipeCatKeyIndex, recipeKeyIndex] * @param connection - Solana Connection object * @param program - CraftingIDLProgram * @param recipeName - The name of the recipe * @param recipeCategoryName - The name of the recipe category * @param recipeLimit - The maximum number of times that this recipe can be used */ export const createRecipeAndRecipeCategory = async ( facility: PublicKey, recipe: AsyncSigner, recipeCategory: AsyncSigner, walletSigner: AsyncSigner, duration: BN, minDuration: BN, domain: PublicKey, profileSigners: AsyncSigner[], profile: PublicKey, profileKeyIndices: number[], connection: Connection, program: CraftingIDLProgram, recipeName = '🎰', recipeCategoryName = '🐸', recipeLimit?: BN, ) => { const recipeSigner = profileSigners[2]; const recipeKeyIndex = profileKeyIndices[2]; const recipeCatSigner = profileSigners[1]; const recipeCatKeyIndex = profileKeyIndices[1]; const facilitySigner = profileSigners[0]; const facilityKeyIndex = profileKeyIndices[0]; if ( facilityKeyIndex && facilitySigner && recipeCatKeyIndex && recipeCatSigner && recipeKeyIndex && recipeSigner ) { await buildSendAndCheck( ( await Promise.all( [ RecipeCategory.registerRecipeCategory( program, recipeCategory, recipeCatSigner, profile, domain, { namespace: stringToByteArray(recipeCategoryName, 32), keyIndex: recipeCatKeyIndex, }, ), Recipe.registerRecipe( program, recipe, recipeSigner, profile, domain, recipeCategory.publicKey(), { namespace: stringToByteArray(recipeName, 32), duration, minDuration, usageLimit: recipeLimit, keyIndex: recipeKeyIndex, }, ), CraftingFacility.addCraftingFacilityRecipeCategory( program, facility, facilitySigner, profile, domain, recipeCategory.publicKey(), { keyIndex: facilityKeyIndex }, ), ].flat(), ) ).flat(), walletSigner, connection, ); } }; /** * Register craftable items using mints that are already valid cargo types * @param inputs - Array of mint and mint authority pairs which will be used to create craftable items. * They must be valid cargo types already * @param walletSigner - The transaction fee payer and signer * @param domain - Domain account key * @param profileSigner - Profile signer for {@param profile} * @param profile - The profile with the required permissions * @param profileKeyIndex - Profile key index for {@param profileSigner} in {@param profile} * @param connection - Solana Connection object * @param program - CraftingIDLProgram * @returns Array of craftable item addresses */ export const registerCraftableItems = async ( inputs: Array<{ mint: PublicKey; mintAuthority: AsyncSigner; namespace: string; }>, walletSigner: AsyncSigner, domain: PublicKey, profileSigner: AsyncSigner, profile: PublicKey, profileKeyIndex: number, connection: Connection, program: CraftingIDLProgram, ) => { await buildSendAndCheck( inputs.map((it) => CraftableItem.registerCraftableItem( program, profileSigner, profile, domain, it.mint, { namespace: stringToByteArray(it.namespace, 32), keyIndex: profileKeyIndex, }, ), ), walletSigner, connection, ); return inputs.map( (it) => CraftableItem.findAddress(program, domain, it.mint)[0], ); }; /** * Register Recipe Input/Outputs * Note that all the inputs/outputs must be valid Cargo Types * @param recipe - The Recipe account to register the inputs/outputs to * @param consumableInputs - Array of [Mint, amount] pairs for {@param recipe}. The mints must be valid Cargo Types * @param nonConsumableInputs - Array of [Mint, amount] pairs {@param recipe}. The mints must be valid Cargo Types * @param outputs - Array of [Mint, amount] pairs {@param recipe}. The mints must be valid Cargo Types * @param walletSigner - The transaction fee payer and signer * @param domain - Domain account key * @param profileSigner - Profile signer for {@param profile} * @param profile - The profile with the required permissions * @param profileKeyIndex - Profile key index for {@param profileSigner} in {@param profile} * @param connection - Solana Connection object * @param program - CraftingIDLProgram */ export const registerRecipeInputOutputs = async ( recipe: PublicKey, consumableInputs: Array<[PublicKey, number]>, nonConsumableInputs: Array<[PublicKey, number]>, outputs: Array<[PublicKey, number]>, walletSigner: AsyncSigner, domain: PublicKey, profileSigner: AsyncSigner, profile: PublicKey, profileKeyIndex: number, connection: Connection, program: CraftingIDLProgram, ) => { await buildSendAndCheck( ( await Promise.all( [ consumableInputs .map((input) => Recipe.addConsumableInputToRecipe( program, recipe, profileSigner, profile, domain, input[0], { amount: new BN(input[1]), mint: input[0], keyIndex: profileKeyIndex, }, ), ) .flat(), nonConsumableInputs .map((input) => Recipe.addNonConsumableInputToRecipe( program, recipe, profileSigner, profile, domain, input[0], { amount: new BN(input[1]), mint: input[0], keyIndex: profileKeyIndex, }, ), ) .flat(), outputs .map((output) => Recipe.addOutputToRecipe( program, recipe, profileSigner, profile, domain, CraftableItem.findAddress(program, domain, output[0])[0], { amount: new BN(output[1]), mint: output[0], keyIndex: profileKeyIndex, }, ), ) .flat(), ].flat(), ) ).flat(), walletSigner, connection, ); }; /** De-registers a recipe and recipe category * @param facility - crafting facility key * @param recipe - recipe key * @param recipeCategory - recipe category key * @param recipeCategoryIndex - recipe category index * @param funder - key that gets rent refunded * @param walletSigner - transaction signer and fee payer * @param domain - Domain account key * @param profileSigners - Profile signers for {@param profile} in the order of: [facilitySigner, recipeCategorySigner, recipeSigner] * @param profile - Profile with the required permissions * @param profileKeyIndices - Profile key indices for {@param profileSigners} in {@param profile} in the order of: [facilityKeyIndex, recipeCategoryKeyIndex, recipeKeyIndex] * @param connection - Solana Connection object * @param program - CraftingIDLProgram */ export const cleanupRecipeAndRecipeCategory = async ( facility: PublicKey, recipe: PublicKey, recipeCategory: PublicKey, recipeCategoryIndex: number, funder: AsyncSigner, walletSigner: AsyncSigner, domain: PublicKey, profileSigners: AsyncSigner[], profile: PublicKey, profileKeyIndices: number[], connection: Connection, program: CraftingIDLProgram, ) => { const recipeSigner = profileSigners[2]; const recipeKeyIndex = profileKeyIndices[2]; const recipeCatSigner = profileSigners[1]; const recipeCatKeyIndex = profileKeyIndices[1]; const facilitySigner = profileSigners[0]; const facilityKeyIndex = profileKeyIndices[0]; if ( facilityKeyIndex && facilitySigner && recipeCatKeyIndex && recipeCatSigner && recipeKeyIndex && recipeSigner ) { await buildSendAndCheck( ( await Promise.all( [ CraftingFacility.removeCraftingFacilityRecipeCategory( program, facility, facilitySigner, profile, domain, recipeCategory, { recipeCategoryIndex: recipeCategoryIndex, keyIndex: facilityKeyIndex, }, ), Recipe.updateRecipe( program, recipe, recipeSigner, profile, domain, { status: RecipeStatus.Deactivated, keyIndex: recipeKeyIndex, }, ), ].flat(), ) ).flat(), walletSigner, connection, ); } }; /** * Converts a number to a little endian byte array * @param num - number to convert * @param length - number of bytes in the array. Defaults to 16 bytes (u128) * @returns Uint8Array */ export const numberToLEByteArray = (num: number, length = 16): Uint8Array => { const byteArray = new Uint8Array(length); for (let i = 0; i < length; i++) { byteArray[i] = num & 0xff; num = num >>> 8; } return byteArray; };