import { getAccount, getAssociatedTokenAddress } from '@solana/spl-token'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { AnchorProvider, BN, Program, Wallet } from '@staratlas/anchor'; import { CARGO_IDL, CargoPermissions, CargoPod, CargoStatsDefinition, CargoType, } from '@staratlas/cargo'; import { CRAFTING_IDL, CraftableItem, CraftingDomain, CraftingFacility, CraftingFacilityLocationType, CraftingPermissions, CraftingProcess, Recipe, RecipeStatus, } from '@staratlas/crafting'; import { CREW_IDL } from '@staratlas/crew'; import { AsyncSigner, FixedSizeArray, buildSendAndCheck, createAssociatedTokenAccount, createAssociatedTokenAccountIdempotent, createMint, createTokenAccount, keypairToAsyncSigner, mintToTokenAccount, readFromRPCNullable, readFromRPCOrError, stringToByteArray, } from '@staratlas/data-source'; import { PLAYER_PROFILE_IDL } from '@staratlas/player-profile'; import { POINTS_IDL, UserPoints } from '@staratlas/points'; import { POINTS_STORE_IDL, UserRedemption } from '@staratlas/points-store'; import { Faction, PROFILE_FACTION_IDL, ProfileFactionAccount, } from '@staratlas/profile-faction'; import { CraftingInstance, CraftingInstanceType, GLOBAL_SCALE_DECIMALS_4, Game, GameState, ProgressionItemType, SAGE_IDL, SagePermissions, SagePlayerProfile, Sector, SectorRing, Ship, ShipStats, SizeClass, Starbase, StarbasePlayer, UpkeepResourceType, craftingInstanceDataEquals, getSizeClassAnchorEnum, shipStatsDefault, } from '../src'; import { CREW_PROGRAM_ID, SAGE_PROGRAM_ID, checkTokenBalance, checkUserPoints, createAndFundTokenAccounts, createCargoTypes, createManyMints, createProfile, createRecipeAndRecipeCategory, createTokenAccounts, mintAndImportCrewToGame, randomWithinRange, registerCraftableItems, registerRecipeInputOutputs, sendManyToChain, setupCrewConfigInstructions, setupPointModifierAccounts, setupPointsAccounts, setupProgression, setupTokenRedemptionInstructions, setupUmi, } from './helpers'; describe('Starbase Crafting', () => { 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(SAGE_IDL, SAGE_PROGRAM_ID, provider); const playerProfileProgram = new Program( PLAYER_PROFILE_IDL, new PublicKey('PprofUW1pURCnMW2si88GWPXEEK3Bvh9Tksy8WtnoYJ'), provider, ); const profileFactionProgram = new Program( PROFILE_FACTION_IDL, new PublicKey('pFACzkX2eSpAjDyEohD6i3VRJvREtH9ynbtM1DwVFsj'), provider, ); const craftingProgram = new Program( CRAFTING_IDL, new PublicKey('CRAFtUSjCW74gQtCS6LyJH33rhhVhdPhZxbPegE4Qwfq'), provider, ); const crewProgram = new Program(CREW_IDL, CREW_PROGRAM_ID, provider); const cargoProgram = new Program( CARGO_IDL, new PublicKey('CArGoi989iv3VL3xArrJXmYYDNhjwCX5ey5sY5KKwMG'), provider, ); const pointsProgram = new Program( POINTS_IDL, new PublicKey('PointJfvuHi8DgGsPCy97EaZkQ6NvpghAAVkuquLf3w'), provider, ); const pointsStoreProgram = new Program( POINTS_STORE_IDL, new PublicKey('PsTRqmfqtLcXcGQoguTLAppxq1rnuTqqyUMRoY62XL1'), provider, ); const funder = keypairToAsyncSigner(Keypair.generate()); const authority = keypairToAsyncSigner(Keypair.generate()); const playerProfile = keypairToAsyncSigner(Keypair.generate()); const playerSigner = keypairToAsyncSigner(Keypair.generate()); const DO_CRAFTING_MANAGER = 1; // Sector stuff const _game = keypairToAsyncSigner(Keypair.generate()); let _gameState: PublicKey; const _sectorProfile = keypairToAsyncSigner(Keypair.generate()); const _sectorSigner = keypairToAsyncSigner(Keypair.generate()); let _sectorKey: PublicKey; const SECTOR_MANAGER = 1; const _updatedRiskZones = { usturSecurityZone: { center: [new BN(-5), new BN(-5)], radius: new BN(1), } as never, oniSecurityZone: { center: [new BN(5), new BN(5)], radius: new BN(1), } as never, mudSecurityZone: { center: [new BN(1), new BN(1)], radius: new BN(1), } as never, highRiskZone: { center: [new BN(-1), new BN(-1)], radius: new BN(1), } as never, mediumRiskZone: { center: [new BN(0), new BN(0)], radius: new BN(20), } as never, }; const _food = Keypair.generate(); const _atlas = Keypair.generate(); let _atlasVault: PublicKey; const _craftingFeeInAtlas = 337; // crafting stuff const _craftingProfile = keypairToAsyncSigner(Keypair.generate()); const _craftingNamespace = stringToByteArray('sector crafting 🎮', 32); const _craftingDomainSigner = keypairToAsyncSigner(Keypair.generate()); const _craftingRecipeCategorySigner = keypairToAsyncSigner( Keypair.generate(), ); const _craftingRecipeSigner = keypairToAsyncSigner(Keypair.generate()); const _craftingRecipeValue = 2.1; const _craftingFacilitySigner = keypairToAsyncSigner(Keypair.generate()); const _craftingSigner = keypairToAsyncSigner(Keypair.generate()); const _craftingOutputKeys = [...Array(1).keys()].map(() => Keypair.generate(), ); const _craftingIngredientKeys = [...Array(2).keys()].map(() => Keypair.generate(), ); const _craftingInputOutputKeys = _craftingIngredientKeys.concat(_craftingOutputKeys); const _craftingInputOutputPubKeys = _craftingInputOutputKeys.map( (it) => it.publicKey, ); let _craftingProcessKey: PublicKey; let _craftingInstanceKey: PublicKey; let _craftingProcessDuration: number; let _craftingProcessNumCrew: BN; const CRAFTING_MANAGER = 1; let _craftingSleepTime = 0; // cargo stuff const _cargoStatsDefinitionSigner = keypairToAsyncSigner(Keypair.generate()); const _cargoProfile = keypairToAsyncSigner(Keypair.generate()); const _definitionManagerKey = keypairToAsyncSigner(Keypair.generate()); const _cargoTypeManagerKey = keypairToAsyncSigner(Keypair.generate()); // Points let _craftingXpCategory: PublicKey; let _pilotXpCategory: PublicKey; let _councilRankXpCategory: PublicKey; let _lpCategory: PublicKey; let _userLp: PublicKey; let _lpModifier: PublicKey; let _userCraftingXp: PublicKey; let _craftingXpModifier: PublicKey; let _userCouncilRankXp: PublicKey; let _councilRankXpModifier: PublicKey; const redemptionConfig = keypairToAsyncSigner(Keypair.generate()); const userRedemption = keypairToAsyncSigner(Keypair.generate()); // `Ship` const ship = keypairToAsyncSigner(Keypair.generate()); const shipMint = keypairToAsyncSigner(Keypair.generate()); // fleets stuff const _fleetSigner = keypairToAsyncSigner(Keypair.generate()); let _sagePlayerProfileKey: PublicKey; let _starbaseKey: PublicKey; let _starbasePlayerKey: PublicKey; const _starbasePlayerPodSeeds = CraftingProcess.getCargoPodSeeds(); const _starbasePlayerPodKey = CargoPod.findAddress( cargoProgram, _starbasePlayerPodSeeds, )[0]; const _starbasePodBaseAmount = new BN(100); const FLEET_ADMIN = 2; const _starbaseSeqId = 0; const foodRecipe = keypairToAsyncSigner(Keypair.generate()); const foodRecipeValue = 1.2; const dummyExhaustionTime = 0; const dummyExhaustedCraftingRate = 1.0; const _createCargoStatsDefinition = async ( statsDefinitionSigner = _cargoStatsDefinitionSigner, ) => { await buildSendAndCheck( CargoStatsDefinition.initDefinition({ program: cargoProgram, profile: _cargoProfile.publicKey(), statsDefinition: statsDefinitionSigner, input: { cargoStats: 1, }, }), walletSigner, provider.connection, ); }; const _createCraftingDomain = async () => { await buildSendAndCheck( CraftingDomain.initializeDomain( craftingProgram, _craftingDomainSigner, _craftingSigner, _craftingProfile.publicKey(), _craftingNamespace, ), walletSigner, provider.connection, ); }; const _createCraftingFacility = async ( location: PublicKey, locationType = CraftingFacilityLocationType.Starbase, efficiency = 10, maxConcurrentProcesses = 500, keyIndex = CRAFTING_MANAGER, ) => { const input = { locationType, efficiency, maxConcurrentProcesses, keyIndex, }; await buildSendAndCheck( CraftingFacility.registerCraftingFacility( craftingProgram, _craftingFacilitySigner, _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), location, input, ), walletSigner, provider.connection, ); }; const _createGame = async () => { await buildSendAndCheck( Game.initGame(program, authority, _sectorProfile.publicKey(), _game), walletSigner, provider.connection, ); const game = await readFromRPCOrError( provider.connection, program, _game.publicKey(), Game, 'confirmed', ); const gameStateId = GameState.findAddress( program, _game.publicKey(), game.data.updateId.add(new BN(1)), ); _gameState = gameStateId[0]; const createGameStateIx = GameState.initGameState( program, _sectorSigner, _sectorProfile.publicKey(), _game.publicKey(), _gameState, { keyIndex: SECTOR_MANAGER, }, ); const updateGameIx = Game.updateGame( _game.publicKey(), program, _sectorSigner, _sectorProfile.publicKey(), SECTOR_MANAGER, undefined /** points */, { atlas: _atlas.publicKey, food: _food.publicKey } /** mints */, { atlas: _atlasVault } /** vaults */, { domain: _craftingDomainSigner.publicKey() } /** crafting */, { statsDefinition: _cargoStatsDefinitionSigner.publicKey() } /** cargo */, _updatedRiskZones /** risk zones*/, ); const updateGameIx2 = Game.updateGame( _game.publicKey(), program, _sectorSigner, _sectorProfile.publicKey(), SECTOR_MANAGER, { lpCategory: _lpCategory, craftingXpCategory: _craftingXpCategory, pilotXpCategory: _pilotXpCategory, councilRankXpCategory: _councilRankXpCategory, } /** points */, ); const updateGameStateIx = GameState.updateGameState( program, _sectorSigner, _sectorProfile.publicKey(), _game.publicKey(), _gameState, SECTOR_MANAGER, { starbaseLevels: [ { starbaseLevelIndex: 6, faction: Faction.MUD as number, hp: new BN(1337), level: 6, sectorRingAvailable: SectorRing.Inner, recipeCategoryForLevel: PublicKey.default, oldRecipeForUpgrade: PublicKey.default, newRecipeForUpgrade: PublicKey.default, sp: new BN(7331), warpLaneMovementFee: new BN(7337), repairFee: new BN(1000), repairEfficiency: 90, shieldRechargeRate: 90, shieldBreakDelay: 90, }, ], starbaseUpkeep: [ { level: 6, info: { ammoReserve: new BN(6_000_000), ammoDepletionRate: 60, foodReserve: new BN(6_000_000), foodDepletionRate: 60, toolkitReserve: new BN(6_000_000), toolkitDepletionRate: 60, }, }, ], maxFleetSize: 64, } /** fleet */, ); const activateGameIx = GameState.activateGameState( program, _sectorSigner, _sectorProfile.publicKey(), _game.publicKey(), _gameState, { keyIndex: SECTOR_MANAGER, }, ); const sectorIx = Sector.registerSector( program, _sectorSigner, _sectorProfile.publicKey(), PublicKey.default, _game.publicKey(), [new BN(1), new BN(1)], 'Super Sector', SECTOR_MANAGER, ); _sectorKey = sectorIx.sectorKey[0]; await sendManyToChain( [ createGameStateIx, updateGameStateIx, updateGameIx, updateGameIx2, activateGameIx, sectorIx.instructions, ].flat(), walletSigner, provider.connection, ); }; const _createStarbase = async (theSector?: Sector) => { const sectorAcc = theSector || (await readFromRPCOrError( provider.connection, program, _sectorKey, Sector, 'confirmed', )); await buildSendAndCheck( Starbase.registerStarbase( program, _fleetSigner, _sectorProfile.publicKey(), _sectorKey, _game.publicKey(), _gameState, { name: stringToByteArray('Starbase Alpha', 64), subCoordinates: [new BN(1), new BN(1)], starbaseLevelIndex: 6, faction: Faction.MUD, sectorCoordinates: sectorAcc.data.coordinates as [BN, BN], keyIndex: FLEET_ADMIN, }, ), walletSigner, provider.connection, ); _starbaseKey = Starbase.findAddress( program, _game.publicKey(), sectorAcc.data.coordinates as [BN, BN], )[0]; await buildSendAndCheck( Starbase.updateStarbase( program, _starbaseKey, _fleetSigner, _sectorProfile.publicKey(), _game.publicKey(), { name: stringToByteArray('Starbase Alpha', 64), subCoordinates: [new BN(1), new BN(1)], keyIndex: FLEET_ADMIN, }, _craftingDomainSigner.publicKey(), _craftingFacilitySigner.publicKey(), ), walletSigner, provider.connection, ); }; const _createSagePlayerProfile = async () => { await buildSendAndCheck( SagePlayerProfile.registerSagePlayerProfile( program, playerProfile.publicKey(), _game.publicKey(), _gameState, ), walletSigner, provider.connection, ); _sagePlayerProfileKey = SagePlayerProfile.findAddress( program, playerProfile.publicKey(), _game.publicKey(), )[0]; }; const _createStarbasePlayer = async () => { await buildSendAndCheck( StarbasePlayer.registerStarbasePlayer( program, ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, _starbaseKey, _game.publicKey(), _gameState, _starbaseSeqId, ), walletSigner, provider.connection, ); _starbasePlayerKey = StarbasePlayer.findAddress( program, _starbaseKey, _sagePlayerProfileKey, _starbaseSeqId, )[0]; }; const _createAndFundStarbaseCargoPod = async () => { const createIx = StarbasePlayer.createCargoPod( program, cargoProgram, _starbasePlayerKey, _fleetSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _cargoStatsDefinitionSigner.publicKey(), _game.publicKey(), _gameState, { keyIndex: FLEET_ADMIN, podSeeds: Array.from(_starbasePlayerPodSeeds), }, ); const tokensToDeposit = _craftingIngredientKeys; const depositIxs = tokensToDeposit.map(async (it) => { const tokenFrom = await getAssociatedTokenAddress( it.publicKey, _fleetSigner.publicKey(), true, ); const tokenTo = await getAssociatedTokenAddress( it.publicKey, _starbasePlayerPodKey, true, ); return StarbasePlayer.depositCargoToGame( program, cargoProgram, _starbasePlayerKey, _fleetSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), it.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _game.publicKey(), _gameState, { amount: _starbasePodBaseAmount, keyIndex: FLEET_ADMIN, }, ); }); await buildSendAndCheck( (await Promise.all([createIx, ...depositIxs])).flat(), walletSigner, provider.connection, ); }; const _createShipMint = async (mint = shipMint) => { // mint `Ship` to walletSigner token account const initMintIx = createMint(mint, 0, authority.publicKey(), null); // simulate transaction await buildSendAndCheck(initMintIx, walletSigner, provider.connection); }; const _createShip = async ( shipKey = ship, mint = shipMint.publicKey(), sizeClass = randomWithinRange(SizeClass.XxSmall, SizeClass.Large), activate = true, ) => { let warpTimes = [...Array(32).keys()]; warpTimes = warpTimes.map((w) => Math.floor(w / 2)); const defaultStats = shipStatsDefault(); const stats: ShipStats = { ...defaultStats, miscStats: { ...defaultStats.miscStats, requiredCrew: 10, }, }; await buildSendAndCheck( Ship.registerShip( program, _sectorSigner, _sectorProfile.publicKey(), shipKey, mint, _game.publicKey(), { name: stringToByteArray('💀❤️🤖', 64) as FixedSizeArray, sizeClass: getSizeClassAnchorEnum(sizeClass), stats, isActive: activate, keyIndex: SECTOR_MANAGER, }, ), walletSigner, provider.connection, ); }; // init Ship const _createShipTokens = async ( mint = shipMint, originAuthority = walletSigner.publicKey(), escrowAuthority = _sagePlayerProfileKey, amount = 1, ): Promise<{ shipOriginTokenAccount: PublicKey; shipEscrowTokenAccount: PublicKey; }> => { // walletSigner's token account that will own Ship const shipOriginTokenAccount = keypairToAsyncSigner(Keypair.generate()); const initTokenAccountIx = createTokenAccount( shipOriginTokenAccount, originAuthority, mint.publicKey(), ); const mintToIx = mintToTokenAccount( authority, mint.publicKey(), shipOriginTokenAccount.publicKey(), amount, ); const shipEscrowTokenAccount = keypairToAsyncSigner(Keypair.generate()); const initShipEscrowIx = createTokenAccount( shipEscrowTokenAccount, escrowAuthority, mint.publicKey(), ); await buildSendAndCheck( [initTokenAccountIx, mintToIx, initShipEscrowIx], walletSigner, provider.connection, ); return { shipOriginTokenAccount: shipOriginTokenAccount.publicKey(), shipEscrowTokenAccount: shipEscrowTokenAccount.publicKey(), }; }; // _createShipTokens const _addShipsToGame = async ( shipMintToUse = shipMint, shipToUse = ship, shipAmount = 1, ) => { const shipTokenResult = await _createShipTokens( shipMintToUse, walletSigner.publicKey(), _sagePlayerProfileKey, shipAmount, ); await buildSendAndCheck( SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority walletSigner, // origin token account owner shipTokenResult.shipOriginTokenAccount, shipToUse.publicKey(), shipTokenResult.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: new BN(shipAmount), index: null, }, ), walletSigner, provider.connection, ); }; const userRedemptionExists = async () => { return ( (await readFromRPCNullable( connection, pointsStoreProgram, userRedemption.publicKey(), UserRedemption, 'confirmed', )) === null ); }; const depositUpkeepFood = async (amountToDeposit = 30000) => { const lpBefore = ( await readFromRPCOrError(connection, pointsProgram, _userLp, UserPoints) ).data.earnedPoints.toNumber(); const foodAmount = new BN(amountToDeposit); const playerFoodToken = createAssociatedTokenAccountIdempotent( _food.publicKey, _fleetSigner.publicKey(), true, ); const starbaseFoodToken = createAssociatedTokenAccountIdempotent( _food.publicKey, _starbasePlayerPodKey, true, ); const initializeUserRedemption = await userRedemptionExists(); await sendManyToChain( [ playerFoodToken.instructions, starbaseFoodToken.instructions, mintToTokenAccount( authority, _food.publicKey, playerFoodToken.address, foodAmount.toNumber(), ), StarbasePlayer.depositCargoToGame( program, cargoProgram, _starbasePlayerKey, _fleetSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), playerFoodToken.address, starbaseFoodToken.address, _game.publicKey(), _gameState, { amount: foodAmount, keyIndex: FLEET_ADMIN, }, ), Starbase.depositStarbaseUpkeepResource( program, cargoProgram, pointsProgram, authority, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], 'funder', _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), foodRecipe.publicKey(), starbaseFoodToken.address, _food.publicKey, _userLp, _lpCategory, _lpModifier, initializeUserRedemption ? userRedemption : userRedemption.publicKey(), // userRedemptionAccount redemptionConfig.publicKey(), // redemptionConfig pointsStoreProgram, // pointsStoreProgram { pointsProgramPermissionsKeyIndex: 0, sagePermissionsKeyIndex: 0, amount: foodAmount, resourceType: UpkeepResourceType.Food, resourceIndex: 0, epochIndex: 0, }, ), ], walletSigner, provider.connection, 'confirmed', true, ); const expectedLp = lpBefore + Math.floor( foodRecipeValue * foodAmount.toNumber() * (ProgressionItemType.Upkeep + 1), ); await checkUserPoints(_userLp, expectedLp, pointsProgram, connection); }; const quickCreateRecipes = async ( items: Array<[AsyncSigner, Keypair, number]>, ) => { for (let index = 0; index < items.length; index++) { const item = items[index]; await sendManyToChain( [ CraftableItem.registerCraftableItem( craftingProgram, _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), item[1].publicKey, { namespace: stringToByteArray('Item xXx', 32), keyIndex: CRAFTING_MANAGER, }, ), Recipe.registerRecipe( craftingProgram, item[0], _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), _craftingRecipeCategorySigner.publicKey(), { namespace: stringToByteArray('Recipe oOo', 32), duration: new BN(2), minDuration: new BN(1), keyIndex: CRAFTING_MANAGER, }, ), Recipe.addOutputToRecipe( craftingProgram, item[0].publicKey(), _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), CraftableItem.findAddress( craftingProgram, _craftingDomainSigner.publicKey(), item[1].publicKey, )[0], { amount: new BN(1), mint: item[1].publicKey, keyIndex: CRAFTING_MANAGER, }, ), Recipe.updateRecipe( craftingProgram, item[0].publicKey(), _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), { value: new BN(item[2] * GLOBAL_SCALE_DECIMALS_4), status: RecipeStatus.Active, keyIndex: CRAFTING_MANAGER, }, ), ], walletSigner, provider.connection, 'confirmed', true, ); } }; const _setupTokenRedemption = async (mint = _atlas.publicKey) => { const result = await setupTokenRedemptionInstructions( mint, _lpCategory, authority, _sectorProfile.publicKey(), 0, pointsStoreProgram, connection, redemptionConfig, userRedemption, ); await buildSendAndCheck( result.instructions, walletSigner, provider.connection, ); }; beforeAll(async () => { const airdropTx = await connection.requestAirdrop( walletSigner.publicKey(), 500_000_000_000, ); await connection.confirmTransaction(airdropTx); const airdropTx2 = await connection.requestAirdrop( funder.publicKey(), 500_000_000_000, ); await connection.confirmTransaction(airdropTx2); // create mints await createManyMints( _craftingIngredientKeys.concat(_craftingOutputKeys), authority.publicKey(), walletSigner, provider.connection, ); await createManyMints( [_atlas, _food], authority.publicKey(), walletSigner, provider.connection, ); // create player profile await createProfile( playerProfileProgram, profileFactionProgram, playerProfile, funder, [ { key: playerSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.doCraftingPermissions(), }, { key: _fleetSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.all(), }, ], provider.connection, authority, walletSigner, ); // create crafting profile await createProfile( playerProfileProgram, profileFactionProgram, _craftingProfile, funder, [ { key: _craftingSigner.publicKey(), expireTime: null, scope: craftingProgram.programId, permissions: CraftingPermissions.all(), }, ], provider.connection, authority, walletSigner, ); // create sector profile await createProfile( playerProfileProgram, profileFactionProgram, _sectorProfile, funder, [ { key: _sectorSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.all(), }, { key: _fleetSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.all(), }, ], provider.connection, authority, walletSigner, ); // create cargo profile await createProfile( playerProfileProgram, profileFactionProgram, _cargoProfile, funder, [ { key: _definitionManagerKey.publicKey(), expireTime: null, scope: cargoProgram.programId, permissions: CargoPermissions.definitionPermissions(), }, { key: _cargoTypeManagerKey.publicKey(), expireTime: null, scope: cargoProgram.programId, permissions: CargoPermissions.cargoTypePermissions(), }, ], provider.connection, walletSigner, ); // create and fund ATLAS token accounts await createAndFundTokenAccounts( authority, [_atlas].map((it) => [it.publicKey, 0]), authority.publicKey(), funder, walletSigner, provider.connection, ); _atlasVault = await getAssociatedTokenAddress( _atlas.publicKey, authority.publicKey(), true, ); await createAndFundTokenAccounts( authority, [_atlas].map((it) => [it.publicKey, 133_337]), playerSigner.publicKey(), funder, walletSigner, provider.connection, ); await _createCargoStatsDefinition(); await _createCraftingDomain(); const pointsResult = await setupPointsAccounts( authority, walletSigner, _sectorProfile.publicKey(), playerProfile.publicKey(), pointsProgram, connection, ); _craftingXpCategory = pointsResult.craftingXpCategory; _pilotXpCategory = pointsResult.pilotXpCategory; _councilRankXpCategory = pointsResult.councilRankXpCategory; _lpCategory = pointsResult.lpCategory; await _createGame(); // setup crew const crewConfigResult = await setupCrewConfigInstructions( authority, _sectorProfile.publicKey(), 0, _game.publicKey(), program, crewProgram, await setupUmi(walletSigner, provider.connection), provider.connection, ); if (crewConfigResult.instructions.length > 0) { await sendManyToChain( crewConfigResult.instructions, walletSigner, provider.connection, 'confirmed', true, ); } const sectorAcc = await readFromRPCOrError( provider.connection, program, _sectorKey, Sector, 'confirmed', ); await _createCraftingFacility( Starbase.findAddress( program, _game.publicKey(), sectorAcc.data.coordinates as [BN, BN], )[0], ); await _createStarbase(sectorAcc); await _createSagePlayerProfile(); await _createStarbasePlayer(); await _createShipMint(shipMint); await _createShip(ship, shipMint.publicKey()); await _addShipsToGame(shipMint, ship, 1); await createCargoTypes( _craftingInputOutputPubKeys.concat([_food.publicKey]), _cargoStatsDefinitionSigner.publicKey(), _cargoTypeManagerKey, _cargoProfile.publicKey(), walletSigner, 0, { keyIndex: 2, values: [new BN(1)], }, provider.connection, cargoProgram, ); await createRecipeAndRecipeCategory( _craftingFacilitySigner.publicKey(), _craftingRecipeSigner, _craftingRecipeCategorySigner, walletSigner, new BN(1), new BN(1), _craftingDomainSigner.publicKey(), [...Array(3).keys()].map(() => _craftingSigner), _craftingProfile.publicKey(), [...Array(3).keys()].map(() => CRAFTING_MANAGER), provider.connection, craftingProgram, ); // create and fund ingredient token accounts for `_fleetSigner` await Promise.all([ createAndFundTokenAccounts( authority /** TODO fix this */, _craftingInputOutputPubKeys.map((it) => [it, 10000]), _fleetSigner.publicKey(), funder, walletSigner, provider.connection, ), ]); // create and fund ingredient token accounts for `_starbasePlayerPodKey` await Promise.all([ createAndFundTokenAccounts( authority /** TODO fix this */, _craftingInputOutputPubKeys.map((it) => [it, 0]), _starbasePlayerPodKey, funder, walletSigner, provider.connection, ), ]); // create and fund ingredient token accounts for `_craftingPodKey` const craftableItemKeys = await registerCraftableItems( _craftingInputOutputPubKeys.map((it) => { return { mint: it, mintAuthority: authority }; }), walletSigner, _craftingDomainSigner.publicKey(), _craftingSigner, _craftingProfile.publicKey(), CRAFTING_MANAGER, provider.connection, craftingProgram, ); // create and fund craftableItem banks for (let index = 0; index < craftableItemKeys.length; index++) { const craftableItemKey = craftableItemKeys[index]; if (!craftableItemKey) { throw 'craftable item should exist'; } await Promise.all([ createAndFundTokenAccounts( authority /** TODO fix this */, _craftingInputOutputPubKeys.map((it) => [it, 100_000_000]), craftableItemKey, funder, walletSigner, provider.connection, ), ]); } // register recipe inputs & outputs const nonConsumableIngredients: Array<[PublicKey, number]> = _craftingIngredientKeys.slice(1, 2).map((it) => [it.publicKey, 1]); await registerRecipeInputOutputs( _craftingRecipeSigner.publicKey(), _craftingIngredientKeys .slice(0, 1) .map((it) => [it.publicKey, 1]) /** consumable */, nonConsumableIngredients /** non-consumable */, _craftingOutputKeys.map((it) => [it.publicKey, 1]) /** output */, walletSigner, _craftingDomainSigner.publicKey(), _craftingSigner, _craftingProfile.publicKey(), CRAFTING_MANAGER, provider.connection, craftingProgram, ); // add the crafting fee await buildSendAndCheck( Recipe.updateRecipe( craftingProgram, _craftingRecipeSigner.publicKey(), _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), { feeAmount: new BN(_craftingFeeInAtlas), value: new BN(_craftingRecipeValue * GLOBAL_SCALE_DECIMALS_4), keyIndex: CRAFTING_MANAGER, }, _atlasVault, ), walletSigner, provider.connection, ); // activate the recipe await buildSendAndCheck( Recipe.updateRecipe( craftingProgram, _craftingRecipeSigner.publicKey(), _craftingSigner, _craftingProfile.publicKey(), _craftingDomainSigner.publicKey(), { status: RecipeStatus.Active, keyIndex: CRAFTING_MANAGER, }, ), walletSigner, provider.connection, ); // create recipes for progressions await quickCreateRecipes([[foodRecipe, _food, foodRecipeValue]]); await _createAndFundStarbaseCargoPod(); await setupPointModifierAccounts( { pilotXpCategory: _pilotXpCategory, councilRankXpCategory: _councilRankXpCategory, craftingXpCategory: _craftingXpCategory, lpCategory: _lpCategory, }, authority, walletSigner, _sectorProfile.publicKey(), _game.publicKey(), program, pointsProgram, connection, ); await setupProgression( authority, walletSigner, _sectorProfile.publicKey(), _game.publicKey(), program, connection, ); _userLp = UserPoints.findAddress( pointsProgram, _lpCategory, playerProfile.publicKey(), )[0]; _lpModifier = Game.findPointsModifierAddress( program, _game.publicKey(), _lpCategory, )[0]; _userCraftingXp = UserPoints.findAddress( pointsProgram, _craftingXpCategory, playerProfile.publicKey(), )[0]; _craftingXpModifier = Game.findPointsModifierAddress( program, _game.publicKey(), _craftingXpCategory, )[0]; _userCouncilRankXp = UserPoints.findAddress( pointsProgram, _councilRankXpCategory, playerProfile.publicKey(), )[0]; _councilRankXpModifier = Game.findPointsModifierAddress( program, _game.publicKey(), _councilRankXpCategory, )[0]; await _setupTokenRedemption(); // import crew to game await mintAndImportCrewToGame( crewConfigResult.merkleTree, authority, crewConfigResult.crewProgramConfig, await setupUmi(walletSigner, provider.connection), program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _game.publicKey(), funder, connection, [], // knownLeaves 5, // numCrew ); }); it('can calculate crafting duration', function () { expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(1), new BN(1), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(1)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(2), new BN(2), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(1)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(1), new BN(2), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(1)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(3), new BN(3), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(1)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(4), new BN(3), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(2)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(10), new BN(3), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(4)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(5), new BN(11), new BN(10), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(10)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(5), new BN(12), new BN(13), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(5)), ).toBe(true); expect( CraftingInstance.calculateCraftingDuration( new BN(1), new BN(1000), new BN(100), dummyExhaustionTime, dummyExhaustedCraftingRate, ).eq(new BN(10)), ).toBe(true); // bad result let success = true; try { CraftingInstance.calculateCraftingDuration( new BN(30), new BN(10), new BN(0), dummyExhaustionTime, dummyExhaustedCraftingRate, ); } catch { success = false; } expect(success).toBe(false); }); it('can initialize Starbase crafting', async function () { const craftingId = new BN(1); const numCrew = new BN(3); const quantity = new BN(1); const createCraftingProcessIx = CraftingInstance.createCraftingProcess( program, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _craftingDomainSigner.publicKey(), { keyIndex: DO_CRAFTING_MANAGER, craftingId, recipeCategoryIndex: 0, quantity: new BN(1), numCrew, }, ); await buildSendAndCheck( createCraftingProcessIx, walletSigner, provider.connection, ); _craftingProcessKey = CraftingProcess.findAddress( craftingProgram, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), craftingId, )[0]; const craftingInstanceAddressResult = CraftingInstance.findAddress( program, _starbasePlayerKey, _craftingProcessKey, ); _craftingInstanceKey = craftingInstanceAddressResult[0]; const craftingInstance = await readFromRPCOrError( provider.connection, program, _craftingInstanceKey, CraftingInstance, 'confirmed', ); expect( craftingInstanceDataEquals(craftingInstance.data, { version: 0, seqId: 0, numCrew, authority: _starbasePlayerKey, instanceType: CraftingInstanceType.StarbaseCrafting, craftingProcess: _craftingProcessKey, bump: craftingInstanceAddressResult[1], }), ).toBe(true); const craftingProcess = await readFromRPCOrError( provider.connection, craftingProgram, _craftingProcessKey, CraftingProcess, 'confirmed', ); expect(craftingProcess.data.denyPermissionlessClaiming).toBe(1); expect(craftingProcess.data.useLocalTime).toBe(1); expect(craftingProcess.data.quantity.eq(quantity)).toBe(true); expect(craftingProcess.data.authority.equals(_craftingInstanceKey)).toBe( true, ); expect( craftingProcess.data.craftingFacility.equals( _craftingFacilitySigner.publicKey(), ), ).toBe(true); const starbasePlayer = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(starbasePlayer.data.busyCrew.eq(numCrew)).toBe(true); }); it('can deposit ingredients to Starbase crafting', async function () { // create ingredient token accounts for `_craftingProcessKey` await Promise.all([ createTokenAccounts( _craftingInputOutputPubKeys, _craftingProcessKey, funder, walletSigner, provider.connection, ), ]); const recipe = await readFromRPCOrError( provider.connection, craftingProgram, _craftingRecipeSigner.publicKey(), Recipe, 'confirmed', ); const depositIxs = _craftingIngredientKeys.map(async (it) => { const tokenFrom = await getAssociatedTokenAddress( it.publicKey, _starbasePlayerPodKey, true, ); const tokenTo = await getAssociatedTokenAddress( it.publicKey, _craftingProcessKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex((item) => item.mint.equals(it.publicKey), ); const recipeIngredient = recipe.ingredientInputsOutputs[ingredientIndex]; if (!recipeIngredient) { throw new Error('recipe ingredient not found'); } return CraftingInstance.depositCraftingIngredient( program, cargoProgram, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), it.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _game.publicKey(), _gameState, { amount: recipeIngredient.amount, keyIndex: CRAFTING_MANAGER, ingredientIndex, }, ); }); await buildSendAndCheck( (await Promise.all(depositIxs)).flat(), walletSigner, provider.connection, ); // check token balance for (let index = 0; index < _craftingIngredientKeys.length; index++) { const element = _craftingIngredientKeys[index]; if (!element) { throw new Error('recipe ingredient not found'); } const craftingTokenAcc = await getAssociatedTokenAddress( element.publicKey, _craftingProcessKey, true, ); await checkTokenBalance( craftingTokenAcc, new BN(0).add(new BN(1)), provider.connection, ); } }); it('can withdraw ingredients from Starbase crafting', async function () { const mint = _craftingIngredientKeys[0]; if (!mint) { throw new Error('mint not found'); } const recipe = await readFromRPCOrError( provider.connection, craftingProgram, _craftingRecipeSigner.publicKey(), Recipe, 'confirmed', ); const starbaseTokenAcc = await getAssociatedTokenAddress( mint.publicKey, _starbasePlayerPodKey, true, ); const craftingTokenAcc = await getAssociatedTokenAddress( mint.publicKey, _craftingProcessKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex((item) => item.mint.equals(mint.publicKey), ); const recipeIngredient = recipe.ingredientInputsOutputs[ingredientIndex]; if (!recipeIngredient) { throw new Error('recipe ingredient not found'); } const withdrawIx = CraftingInstance.withdrawCraftingIngredient( program, cargoProgram, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), craftingTokenAcc, starbaseTokenAcc, mint.publicKey, _game.publicKey(), _gameState, { amount: recipeIngredient.amount, keyIndex: CRAFTING_MANAGER, ingredientIndex, }, ); await buildSendAndCheck(withdrawIx, walletSigner, provider.connection); let success = true; try { await checkTokenBalance(craftingTokenAcc, new BN(0), provider.connection); } catch (err) { expect((err as Error).message).toContain( 'failed to get token account balance: Invalid param: could not find account', ); success = false; } expect(success).toBe(false); // recreate the token account and deposit back into it const createTokenIx = createAssociatedTokenAccount( mint.publicKey, _craftingProcessKey, ); const depositIx = CraftingInstance.depositCraftingIngredient( program, cargoProgram, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), starbaseTokenAcc, craftingTokenAcc, _game.publicKey(), _gameState, { amount: recipeIngredient.amount, keyIndex: CRAFTING_MANAGER, ingredientIndex, }, ); await buildSendAndCheck( [createTokenIx.instructions, depositIx], walletSigner, provider.connection, ); await checkTokenBalance(craftingTokenAcc, new BN(1), provider.connection); }); it('can cancel a Starbase crafting process', async function () { // Creating a new crafting process to be canceled const initialSBP = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const craftingId = new BN(2); const process2Crew = new BN(2); const createCraftingProcessIx = CraftingInstance.createCraftingProcess( program, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _craftingDomainSigner.publicKey(), { keyIndex: DO_CRAFTING_MANAGER, craftingId, recipeCategoryIndex: 0, quantity: new BN(1), numCrew: process2Crew, }, ); await buildSendAndCheck( createCraftingProcessIx, walletSigner, provider.connection, ); const craftingProcess2Key = CraftingProcess.findAddress( craftingProgram, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), craftingId, )[0]; const craftingInstance2Key = CraftingInstance.findAddress( program, _starbasePlayerKey, craftingProcess2Key, )[0]; // canceling the `craftingProcess2Key` const previousSBP = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const previousCraftingInstance = await readFromRPCOrError( provider.connection, program, craftingInstance2Key, CraftingInstance, 'confirmed', ); const cancelCraftingProcessIx = CraftingInstance.cancelCraftingProcess( program, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], funder.publicKey(), _starbasePlayerKey, _starbaseKey, craftingInstance2Key, craftingProcess2Key, _craftingFacilitySigner.publicKey(), _game.publicKey(), _gameState, { keyIndex: DO_CRAFTING_MANAGER, }, ); await buildSendAndCheck( cancelCraftingProcessIx, walletSigner, provider.connection, ); // check closed crafting instance and crafting process const craftingProcess = await readFromRPCNullable( provider.connection, craftingProgram, craftingProcess2Key, CraftingProcess, 'confirmed', ); expect(craftingProcess).toBeNull; const craftingInstance = await readFromRPCNullable( provider.connection, program, craftingInstance2Key, CraftingInstance, 'confirmed', ); expect(craftingInstance).toBeNull; // check SB player busyCrew const newSBP = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const initialCrew = initialSBP.data.busyCrew; expect(previousSBP.data.busyCrew.eq(initialCrew.add(process2Crew))).toBe( true, ); expect(newSBP.data.busyCrew.toNumber()).toBe( previousSBP.data.busyCrew.toNumber() - previousCraftingInstance.data.numCrew.toNumber(), ); }); it('can start & stop a Starbase crafting process', async function () { const playerFeeToken = await getAssociatedTokenAddress( _atlas.publicKey, playerSigner.publicKey(), true, ); const feeEscrowTokenResult = createAssociatedTokenAccount( _atlas.publicKey, _craftingProcessKey, true, ); const playerFeeTokenAccount0 = await getAccount( program.provider.connection, playerFeeToken, 'processed', ); const startCraftingProcessIx = CraftingInstance.startCraftingProcess( program, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _game.publicKey(), _gameState, { keyIndex: DO_CRAFTING_MANAGER, }, _atlasVault, playerSigner, playerFeeToken, feeEscrowTokenResult.address, ); const stopCraftingProcessIx = CraftingInstance.stopCraftingProcess( program, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _game.publicKey(), _gameState, { keyIndex: DO_CRAFTING_MANAGER, }, feeEscrowTokenResult.address, playerFeeToken, ); await buildSendAndCheck( [ feeEscrowTokenResult.instructions, startCraftingProcessIx, stopCraftingProcessIx, ], walletSigner, provider.connection, ); // confirm it was actually stopped const playerFeeTokenAccount1 = await getAccount( program.provider.connection, playerFeeToken, 'processed', ); const craftingProcess = await readFromRPCOrError( provider.connection, craftingProgram, _craftingProcessKey, CraftingProcess, 'confirmed', ); const stoppedCraftingProcessStatus = 1; expect(craftingProcess.data.status).toBe(stoppedCraftingProcessStatus); expect(craftingProcess.data.startTime.toNumber()).toBe(0); expect(craftingProcess.data.endTime.toNumber()).toBe(0); expect(Number(playerFeeTokenAccount1.amount)).toEqual( Number(playerFeeTokenAccount0.amount), ); // start it again so that we can proceed with testing const feeEscrowTokenResult2 = createAssociatedTokenAccount( _atlas.publicKey, _craftingProcessKey, true, ); expect( feeEscrowTokenResult2.address.equals(feeEscrowTokenResult.address), ).toBe(true); await buildSendAndCheck( [ feeEscrowTokenResult2.instructions, CraftingInstance.startCraftingProcess( program, craftingProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _game.publicKey(), _gameState, { keyIndex: DO_CRAFTING_MANAGER, }, _atlasVault, playerSigner, playerFeeToken, feeEscrowTokenResult2.address, ), ], walletSigner, provider.connection, ); const feeEscrowAfter = await getAccount( program.provider.connection, feeEscrowTokenResult2.address, 'processed', ); const playerFeeTokenAccount2 = await getAccount( program.provider.connection, playerFeeToken, 'processed', ); const craftingProcess2 = await readFromRPCOrError( provider.connection, craftingProgram, _craftingProcessKey, CraftingProcess, 'confirmed', ); const recipe = await readFromRPCOrError( provider.connection, craftingProgram, _craftingRecipeSigner.publicKey(), Recipe, 'confirmed', ); const craftingInstance = await readFromRPCOrError( provider.connection, program, _craftingInstanceKey, CraftingInstance, 'confirmed', ); const startedCraftingProcessStatus = 2; expect(craftingProcess2.data.status).toBe(startedCraftingProcessStatus); const estimatedDuration = CraftingInstance.calculateCraftingDuration( recipe.data.duration, craftingProcess2.data.quantity, craftingInstance.data.numCrew, dummyExhaustionTime, dummyExhaustedCraftingRate, ).toNumber(); const effectiveDuration = craftingProcess2.data.endTime.toNumber() - craftingProcess2.data.startTime.toNumber(); expect(effectiveDuration).toBe(estimatedDuration); expect(Number(feeEscrowAfter.amount)).toEqual(_craftingFeeInAtlas); expect( Number(playerFeeTokenAccount1.amount) - Number(playerFeeTokenAccount2.amount), ).toEqual(_craftingFeeInAtlas); _craftingProcessNumCrew = craftingInstance.data.numCrew; _craftingProcessDuration = effectiveDuration; // sleep for crafting process duration to end _craftingSleepTime = (estimatedDuration + 1) * 1000; await new Promise((r) => setTimeout(r, _craftingSleepTime)); }); it('can close a Starbase crafting process', async function () { const craftingXpBefore = ( await readFromRPCOrError( connection, pointsProgram, _userCraftingXp, UserPoints, ) ).data.earnedPoints.toNumber(); const councilRankXpBefore = ( await readFromRPCOrError( connection, pointsProgram, _userCouncilRankXp, UserPoints, ) ).data.earnedPoints.toNumber(); // Burn consumables & Claim output from crafting(permission-less IXs) to be able to close crafting process const recipe = await readFromRPCOrError( provider.connection, craftingProgram, _craftingRecipeSigner.publicKey(), Recipe, 'confirmed', ); const consumableTokenAccountKeys = await Promise.all( _craftingIngredientKeys .slice(0, 1) .map((it) => getAssociatedTokenAddress(it.publicKey, _craftingProcessKey, true), ), ); // burn consumables // can't burn directly via Crafting Program const failingBurnConsumablesIxs = _craftingIngredientKeys .slice(0, 1) .map(async (it) => { const consumableTokenAccount = await getAssociatedTokenAddress( it.publicKey, _craftingProcessKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex( (item) => item.mint.equals(it.publicKey), ); return CraftingProcess.burnConsumableIngredient( craftingProgram, _craftingProcessKey, _craftingRecipeSigner.publicKey(), consumableTokenAccount, it.publicKey, { ingredientIndex, }, ); }); let success0 = true; try { await sendManyToChain( (await Promise.all(failingBurnConsumablesIxs)).flat(), walletSigner, provider.connection, ); } catch (error) { success0 = false; } expect(success0).toBe(false); // burning via CPI works const burnConsumablesIxs = _craftingIngredientKeys .slice(0, 1) .map(async (it) => { const consumableTokenAccount = await getAssociatedTokenAddress( it.publicKey, _craftingProcessKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex( (item) => item.mint.equals(it.publicKey), ); return CraftingInstance.burnCraftingConsumables( program, craftingProgram, _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _game.publicKey(), _gameState, consumableTokenAccount, it.publicKey, { ingredientIndex, }, ); }); await depositUpkeepFood(); // ensure crafting process ends await new Promise((r) => setTimeout(r, _craftingSleepTime)); await sendManyToChain( (await Promise.all(burnConsumablesIxs)).flat(), walletSigner, provider.connection, 'confirmed', true, ); // the token accounts were burned and closed const consumableTokenAccounts = await Promise.all( consumableTokenAccountKeys.map((it) => program.provider.connection.getAccountInfo(it, 'confirmed'), ), ); for (let index = 0; index < consumableTokenAccounts.length; index++) { const element = consumableTokenAccounts[index]; expect(element).toBeNull(); } // claim non-consumables // can't claim directly via Crafting Program const failingClaimNonConsumablesIxs = _craftingIngredientKeys .slice(1, 2) .map(async (it) => { const nonConsumableTokenAccount = await getAssociatedTokenAddress( it.publicKey, _craftingProcessKey, true, ); const nonConsumableOutputTokenAccount = await getAssociatedTokenAddress( it.publicKey, _craftingInstanceKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex( (item) => item.mint.equals(it.publicKey), ); return CraftingProcess.claimNonConsumableIngredient( craftingProgram, _craftingProcessKey, _craftingInstanceKey /** authority */, _craftingRecipeSigner.publicKey(), nonConsumableTokenAccount, nonConsumableOutputTokenAccount, it.publicKey, { ingredientIndex, }, ); }); let success1 = true; try { await buildSendAndCheck( (await Promise.all(failingClaimNonConsumablesIxs)).flat(), walletSigner, provider.connection, { suppressLogging: true, }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); // claiming via CPI works const nonConsumableTokenAccountKeys = await Promise.all( _craftingIngredientKeys .slice(1, 2) .map((it) => getAssociatedTokenAddress(it.publicKey, _starbasePlayerPodKey, true), ), ); const nonConsumableTokenAccountsBefore = await Promise.all( nonConsumableTokenAccountKeys.map((it) => getAccount(program.provider.connection, it, 'processed'), ), ); const claimNonConsumablesIxs = _craftingIngredientKeys .slice(1, 2) .map(async (it) => { const craftingProcessTokenAcc = await getAssociatedTokenAddress( it.publicKey, _craftingProcessKey, true, ); const starbaseTokenAcc = await getAssociatedTokenAddress( it.publicKey, _starbasePlayerPodKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex( (item) => item.mint.equals(it.publicKey), ); const recipeIngredient = recipe.ingredientInputsOutputs[ingredientIndex]; if (!recipeIngredient) { throw new Error('recipe ingredient not found'); } return CraftingInstance.claimCraftingNonConsumables( program, cargoProgram, craftingProgram, _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), it.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _game.publicKey(), _gameState, craftingProcessTokenAcc, starbaseTokenAcc, it.publicKey, { ingredientIndex }, ); }); await buildSendAndCheck( (await Promise.all(claimNonConsumablesIxs)).flat(), walletSigner, provider.connection, { sendOptions: { skipPreflight: true } }, ); const nonConsumableTokenAccountsAfter = await Promise.all( nonConsumableTokenAccountKeys.map((it) => getAccount(program.provider.connection, it, 'processed'), ), ); // the non-consumables were returned to user for ( let index = 0; index < nonConsumableTokenAccountsAfter.length; index++ ) { const elementBefore = nonConsumableTokenAccountsBefore[index]; const elementAfter = nonConsumableTokenAccountsAfter[index]; expect(Number(elementAfter.amount) - Number(elementBefore.amount)).toBe( recipe.ingredientInputsOutputs[1].amount.toNumber(), ); } // claim outputs // can't claim directly via Crafting Program const failingClaimIxs = _craftingOutputKeys.map(async (it) => { const craftableItemKey = CraftableItem.findAddress( craftingProgram, _craftingDomainSigner.publicKey(), it.publicKey, )[0]; const outputTokenFrom = await getAssociatedTokenAddress( it.publicKey, craftableItemKey, true, ); const outputTokenTo = await getAssociatedTokenAddress( it.publicKey, _craftingInstanceKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex((item) => item.mint.equals(it.publicKey), ); return CraftingProcess.claimRecipeOutput( craftingProgram, _craftingProcessKey, _craftingInstanceKey /** authority */, _craftingRecipeSigner.publicKey(), outputTokenFrom, outputTokenTo, craftableItemKey, { ingredientIndex }, ); }); let success2 = true; try { await buildSendAndCheck( (await Promise.all(failingClaimIxs)).flat(), walletSigner, provider.connection, { suppressLogging: true, }, ); } catch (error) { success2 = false; } expect(success2).toBe(false); // claiming via CPI works const outputTokenAccountKeys = await Promise.all( _craftingOutputKeys.map((it) => getAssociatedTokenAddress(it.publicKey, _starbasePlayerPodKey, true), ), ); const outputTokenAccountsBefore = await Promise.all( outputTokenAccountKeys.map((it) => getAccount(program.provider.connection, it, 'processed'), ), ); const claimIxs = _craftingOutputKeys.map(async (it) => { const craftableItemKey = CraftableItem.findAddress( craftingProgram, _craftingDomainSigner.publicKey(), it.publicKey, )[0]; const craftableItemTokenAcc = await getAssociatedTokenAddress( it.publicKey, craftableItemKey, true, ); const starbaseTokenAcc = await getAssociatedTokenAddress( it.publicKey, _starbasePlayerPodKey, true, ); const ingredientIndex = recipe.ingredientInputsOutputs.findIndex((item) => item.mint.equals(it.publicKey), ); const recipeIngredient = recipe.ingredientInputsOutputs[ingredientIndex]; if (!recipeIngredient) { throw new Error('recipe ingredient not found'); } return CraftingInstance.claimCraftingOutputs( program, cargoProgram, craftingProgram, _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), craftableItemKey, _starbasePlayerPodKey, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), it.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _game.publicKey(), _gameState, craftableItemTokenAcc, starbaseTokenAcc, { ingredientIndex, }, ); }); await sendManyToChain( (await Promise.all(claimIxs)).flat(), walletSigner, provider.connection, ); const outputTokenAccountsAfter = await Promise.all( outputTokenAccountKeys.map((it) => getAccount(program.provider.connection, it, 'processed'), ), ); // the user received the outputs for (let index = 0; index < outputTokenAccountsAfter.length; index++) { const elementBefore = outputTokenAccountsBefore[index]; const elementAfter = outputTokenAccountsAfter[index]; expect(Number(elementAfter.amount) - Number(elementBefore.amount)).toBe( recipe.ingredientInputsOutputs[index].amount.toNumber(), ); } // now close the crafting process for real const previousSBP = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const previousCraftingInstance = await readFromRPCOrError( provider.connection, program, _craftingInstanceKey, CraftingInstance, 'confirmed', ); const tokenFrom = await getAssociatedTokenAddress( _atlas.publicKey, _craftingProcessKey, true, ); const vaultBefore = await getAccount( program.provider.connection, _atlasVault, 'processed', ); const closeCraftingProcess = CraftingInstance.closeCraftingProcess( program, craftingProgram, pointsProgram, playerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], funder.publicKey(), _starbasePlayerKey, _starbaseKey, _craftingInstanceKey, _craftingProcessKey, _craftingFacilitySigner.publicKey(), _craftingRecipeSigner.publicKey(), _userCraftingXp, _craftingXpCategory, _craftingXpModifier, _userCouncilRankXp, _councilRankXpCategory, _councilRankXpModifier, _game.publicKey(), _gameState, { keyIndex: DO_CRAFTING_MANAGER, }, tokenFrom, _atlasVault, ); await sendManyToChain( closeCraftingProcess, walletSigner, provider.connection, 'confirmed', true, ); // check closed crafting instance and crafting process const craftingProcess = await readFromRPCNullable( provider.connection, craftingProgram, _craftingProcessKey, CraftingProcess, 'confirmed', ); expect(craftingProcess).toBeNull; const craftingInstance = await readFromRPCNullable( provider.connection, program, _craftingInstanceKey, CraftingInstance, 'confirmed', ); expect(craftingInstance).toBeNull; // check SB player busyCrew const newSBP = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(newSBP.data.busyCrew.toNumber()).toBe( previousSBP.data.busyCrew.toNumber() - previousCraftingInstance.data.numCrew.toNumber(), ); // check that recipe fees were received const vaultAfter = await getAccount( program.provider.connection, _atlasVault, 'processed', ); expect(Number(vaultAfter.amount)).toEqual( Number(vaultBefore.amount) + _craftingFeeInAtlas, ); // check that XP was awarded as expected const earnedXP = Math.floor( _craftingProcessNumCrew.toNumber() * _craftingProcessDuration * _craftingRecipeValue * (ProgressionItemType.Crafting + 1), ); await checkUserPoints( _userCraftingXp, craftingXpBefore + earnedXP, pointsProgram, connection, ); await checkUserPoints( _userCouncilRankXp, councilRankXpBefore + earnedXP, pointsProgram, connection, ); }); });