import { getAccount, getAssociatedTokenAddressSync } 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 { CREW_IDL } from '@staratlas/crew'; import { FixedSizeArray, InstructionReturn, arrayDeepEquals, buildSendAndCheck, byteArrayToString, createAssociatedTokenAccount, createAssociatedTokenAccountIdempotent, createMint, createTokenAccount, keypairToAsyncSigner, mintToTokenAccount, readFromRPCOrError, stringToByteArray, } from '@staratlas/data-source'; import { PLAYER_PROFILE_IDL } from '@staratlas/player-profile'; import { POINTS_IDL, UserPoints } from '@staratlas/points'; import { Faction, PROFILE_FACTION_IDL, ProfileFactionAccount, } from '@staratlas/profile-faction'; import { Fleet, Game, GameState, LocationType, MineItem, Planet, PlanetType, ProgressionItemType, RegisterShipInput, Resource, SAGE_IDL, SagePermissions, SagePlayerProfile, Sector, SectorRing, Ship, ShipStats, SizeClass, Starbase, StarbasePlayer, getSizeClassAnchorEnum, mineItemDataEquals, resourceDataEquals, } from '../src'; import { BALANCED_STAR_BASE_LEVELS, CREW_PROGRAM_ID, SAGE_PROGRAM_ID, SageGameTest, checkTokenBalance, checkUserPoints, createAndFundTokenAccounts, createCargoTypes, createManyMints, createMineItems, createProfile, mintAndImportCrewToGame, randomWithinRange, sendManyToChain, setupCrewConfigInstructions, setupPointModifierAccounts, setupPointsAccounts, setupProgression, setupUmi, } from './helpers'; import { chunk } from 'lodash'; describe('Asteroid Mining', () => { 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 cargoProgram = new Program( CARGO_IDL, new PublicKey('CArGoi989iv3VL3xArrJXmYYDNhjwCX5ey5sY5KKwMG'), provider, ); const crewProgram = new Program(CREW_IDL, CREW_PROGRAM_ID, provider); const pointsProgram = new Program( POINTS_IDL, new PublicKey('PointJfvuHi8DgGsPCy97EaZkQ6NvpghAAVkuquLf3w'), provider, ); const funder = keypairToAsyncSigner(Keypair.generate()); const authority = keypairToAsyncSigner(Keypair.generate()); const playerProfile = keypairToAsyncSigner(Keypair.generate()); // fleets stuff const _sageSigner = keypairToAsyncSigner(Keypair.generate()); const _fleetMinerSigner = keypairToAsyncSigner(Keypair.generate()); const _fleetRespawnSigner = keypairToAsyncSigner(Keypair.generate()); const _mineItemSigner = keypairToAsyncSigner(Keypair.generate()); const _resourceSigner = keypairToAsyncSigner(Keypair.generate()); const _movementSigner = keypairToAsyncSigner(Keypair.generate()); let _sagePlayerProfileKey: PublicKey; let _starbaseKey: PublicKey; let _starbasePlayerKey: PublicKey; const _starbasePodSeeds = Keypair.generate().publicKey.toBuffer(); const _starbaseCargoPod = CargoPod.findAddress( cargoProgram, _starbasePodSeeds, )[0]; const MINE_ITEM_MANAGER = 2; const RESOURCE_MANAGER = 3; const CAN_MINE_WITH_FLEET = 4; const CAN_RESPAWN_FLEET = 6; const _resourceMintKeys = [...Array(3).keys()].map(() => Keypair.generate()); // `Ship` const ship = keypairToAsyncSigner(Keypair.generate()); const ship2 = keypairToAsyncSigner(Keypair.generate()); const ship3 = keypairToAsyncSigner(Keypair.generate()); const ship4 = keypairToAsyncSigner(Keypair.generate()); const ship5 = keypairToAsyncSigner(Keypair.generate()); // ship mint const shipMint = keypairToAsyncSigner(Keypair.generate()); let fleet1: PublicKey; // Sector stuff const _game = keypairToAsyncSigner(Keypair.generate()); let _gameState: PublicKey; let _sectorKey: PublicKey; const _planet = keypairToAsyncSigner(Keypair.generate()); const _food = Keypair.generate(); const _ammo = Keypair.generate(); const _fuel = Keypair.generate(); const _fleetCargoMints = [_food, _ammo, _fuel]; const SAGE_MANAGER = 1; const _updatedRiskZones = { mudSecurityZone: { 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, usturSecurityZone: { 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 _atlas = Keypair.generate(); let _atlasVault: PublicKey; // cargo stuff const _cargoStatsDefinitionSigner = keypairToAsyncSigner(Keypair.generate()); const _cargoSigner = keypairToAsyncSigner(Keypair.generate()); const CARGO_MANAGER = 5; // Points let _miningXpCategory: PublicKey; let _pilotXpCategory: PublicKey; let _councilRankXpCategory: PublicKey; let _userMiningXp: PublicKey; let _miningXpModifier: PublicKey; let _userPilotXp: PublicKey; let _pilotXpModifier: PublicKey; let _userCouncilRankXp: PublicKey; let _councilRankXpModifier: PublicKey; const _createCargoStatsDefinition = async ( statsDefinitionSigner = _cargoStatsDefinitionSigner, ) => { await buildSendAndCheck( CargoStatsDefinition.initDefinition({ program: cargoProgram, profile: playerProfile.publicKey(), statsDefinition: statsDefinitionSigner, input: { cargoStats: 1, }, }), walletSigner, provider.connection, ); }; const _createGame = async () => { await buildSendAndCheck( Game.initGame(program, authority, playerProfile.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, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, { keyIndex: SAGE_MANAGER, }, ); const updateLevers = GameState.updateGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, SAGE_MANAGER, { maxFleetSize: 64, } /** fleet */, ); // update levers await buildSendAndCheck( [createGameStateIx, updateLevers].flat(), walletSigner, provider.connection, ); const updateGameIx = Game.updateGame( _game.publicKey(), program, _sageSigner, playerProfile.publicKey(), SAGE_MANAGER, undefined /** points */, { ammo: _ammo.publicKey, food: _food.publicKey, fuel: _fuel.publicKey, atlas: _atlas.publicKey, } /** mints */, { atlas: _atlasVault } /** vaults */, undefined /** crafting */, { statsDefinition: _cargoStatsDefinitionSigner.publicKey() } /** cargo */, _updatedRiskZones /** risk zones*/, ); const updateGameIx2 = Game.updateGame( _game.publicKey(), program, _sageSigner, playerProfile.publicKey(), SAGE_MANAGER, { miningXpCategory: _miningXpCategory, pilotXpCategory: _pilotXpCategory, councilRankXpCategory: _councilRankXpCategory, } /** points */, ); const updateGameStateIx = GameState.updateGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, SAGE_MANAGER, undefined /** fleet */, { warpLaneFuelCostReduction: 75, upkeepMiningEmissionsPenalty: 50, respawnFee: new BN(25), } /** misc */, ); const starbaseLevelsChunks = chunk(BALANCED_STAR_BASE_LEVELS, 7); const updateStarbaseLevelInfoIxs: InstructionReturn[] = starbaseLevelsChunks.map((thisChunk) => GameState.updateGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, SAGE_MANAGER, { starbaseLevels: thisChunk }, ), ); const activateGameIx = GameState.activateGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, { keyIndex: SAGE_MANAGER, }, ); const sectorIx = Sector.registerSector( program, _sageSigner, playerProfile.publicKey(), playerProfile.publicKey() /** discoverer */, _game.publicKey(), [new BN(1), new BN(1)], 'Super Sector', SAGE_MANAGER, ); _sectorKey = sectorIx.sectorKey[0]; await sendManyToChain( [ updateGameIx, updateGameIx2, updateGameStateIx, ...updateStarbaseLevelInfoIxs, activateGameIx, sectorIx.instructions, ], walletSigner, provider.connection, ); }; const _createStarbase = async () => { const sectorAcc = await readFromRPCOrError( provider.connection, program, _sectorKey, Sector, 'confirmed', ); await buildSendAndCheck( Starbase.registerStarbase( program, _sageSigner, playerProfile.publicKey(), _sectorKey, _game.publicKey(), _gameState, { name: stringToByteArray('Starbase Alpha', 64), subCoordinates: [new BN(1), new BN(1)], sectorCoordinates: sectorAcc.data.coordinates as [BN, BN], starbaseLevelIndex: 6, faction: Faction.Ustur, keyIndex: SAGE_MANAGER, }, ), walletSigner, provider.connection, ); _starbaseKey = Starbase.findAddress( program, _game.publicKey(), sectorAcc.data.coordinates as [BN, BN], )[0]; }; 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 _addCargoToStarbasePlayer = async ( starbasePlayerKey: PublicKey, amount: BN = new BN(100_000_000_000), ) => { // create token accounts for _starbaseCargoPod await Promise.all([ createAndFundTokenAccounts( authority, _fleetCargoMints.map((it) => [it.publicKey, 0]), _starbaseCargoPod, walletSigner, walletSigner, provider.connection, ), ]); // create starbasePlayer cargo pod await buildSendAndCheck( StarbasePlayer.createCargoPod( program, cargoProgram, starbasePlayerKey, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _cargoStatsDefinitionSigner.publicKey(), _game.publicKey(), _gameState, { keyIndex: SAGE_MANAGER, podSeeds: Array.from(_starbasePodSeeds), }, ), walletSigner, provider.connection, ); // add cargo to starbasePlayer for (let index = 0; index < _fleetCargoMints.length; index++) { const mint = _fleetCargoMints[index]; if (mint) { const tokenFrom = getAssociatedTokenAddressSync( mint.publicKey, _sageSigner.publicKey(), true, ); const tokenTo = getAssociatedTokenAddressSync( mint.publicKey, _starbaseCargoPod, true, ); const ix = StarbasePlayer.depositCargoToGame( program, cargoProgram, starbasePlayerKey, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _starbaseCargoPod, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _game.publicKey(), _gameState, { amount, keyIndex: SAGE_MANAGER, }, ); await buildSendAndCheck(ix, walletSigner, provider.connection); } } }; const _createStarbasePlayer = async () => { const starbaseSeqId = 0; 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]; await _addCargoToStarbasePlayer(_starbasePlayerKey); }; const _createPlanet = async ( name = '🤖 armoury', size = new BN(133_337), maxHp = new BN(1000_000_000), subCoordinates = [new BN(2), new BN(2)], planetType = PlanetType.AsteroidBelt, position = SectorRing.Inner, ) => { await buildSendAndCheck( Planet.registerPlanet( program, _sageSigner, playerProfile.publicKey(), _planet, _sectorKey, _game.publicKey(), { name, size, maxHp, subCoordinates: subCoordinates as FixedSizeArray, planetType, position, keyIndex: SAGE_MANAGER, }, ), 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 _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 _createShip = async ( shipKey = ship, mint = shipMint.publicKey(), sizeClass = randomWithinRange(SizeClass.XSmall, SizeClass.Medium), crew = 0, ammoConsumptionRate?: number, foodConsumptionRate?: number, ) => { const stats: ShipStats = SageGameTest.getDefaultShipStats( crew, undefined, ammoConsumptionRate, foodConsumptionRate, ); const input: RegisterShipInput = { name: stringToByteArray('💀❤️🤖', 64) as FixedSizeArray, sizeClass: getSizeClassAnchorEnum(sizeClass), stats: stats, isActive: true, keyIndex: SAGE_MANAGER, }; const shipInstructions = ( await Promise.all([ Ship.registerShip( program, _sageSigner, playerProfile.publicKey(), shipKey, mint, _game.publicKey(), input, ), ]) ).flat(); await buildSendAndCheck( shipInstructions, walletSigner, provider.connection, ); }; // init Ship const _depositCargoToFleet = ( fleetAddress: PublicKey, fleetCargoPod: PublicKey, cargoMint: PublicKey, amount: BN, ) => { const tokenToResult = createAssociatedTokenAccountIdempotent( cargoMint, fleetCargoPod, true, ); return Promise.all([ tokenToResult.instructions, Fleet.depositCargoToFleet( program, cargoProgram, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleetAddress, _starbaseCargoPod, fleetCargoPod, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), cargoMint, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), getAssociatedTokenAddressSync(cargoMint, _starbaseCargoPod, true), getAssociatedTokenAddressSync(cargoMint, fleetCargoPod, true), cargoMint, _game.publicKey(), _gameState, { amount, keyIndex: SAGE_MANAGER, }, ), ]); }; const _createFleet = async ( fleetName = 'mosh', shipsToUse = [ship], shipAmounts = [1], shipEscrowIndices = [0], fuelAmtToDeposit: number | undefined = undefined, foodAmtToDeposit: number | undefined = undefined, ammoAmtToDeposit: number | undefined = undefined, fillCargoHold = false, ): Promise<[PublicKey, number]> => { const fleetLabel = stringToByteArray(fleetName, 32); const firstShip = shipsToUse.shift(); const firstShipAmt = shipAmounts.shift(); const firstShipIdx = shipEscrowIndices.shift(); if (!firstShip || !firstShipAmt || firstShipIdx == null) { throw 'Cant find ship'; } const createFleetIxResult = Fleet.createFleet( program, cargoProgram, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], firstShip.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, _cargoStatsDefinitionSigner.publicKey(), { shipAmount: firstShipAmt, shipEscrowIndex: firstShipIdx, fleetLabel, keyIndex: SAGE_MANAGER, }, ); const ixs = await Promise.all( shipsToUse.map((ship, idx) => { const shipAmt = shipAmounts[idx]; const shipIdx = shipEscrowIndices[idx]; if (!shipAmt || shipIdx == null) { throw 'Cant find ship inner'; } return Fleet.addShipToFleet( program, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], createFleetIxResult.fleetKey[0], ship.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: shipAmt, shipEscrowIndex: shipIdx, fleetShipInfoIndex: null, keyIndex: SAGE_MANAGER, }, ); }), ); ixs.unshift(createFleetIxResult.instructions); await buildSendAndCheck(ixs.flat(), walletSigner, provider.connection); // deposit food and fuel const fleetAccount = await readFromRPCOrError( provider.connection, program, createFleetIxResult.fleetKey[0], Fleet, 'confirmed', ); const fuelTankIx = _depositCargoToFleet( createFleetIxResult.fleetKey[0], fleetAccount.data.fuelTank, _fuel.publicKey, fuelAmtToDeposit == null ? new BN((fleetAccount.data.stats.cargoStats as any).fuelCapacity) : new BN(fuelAmtToDeposit), ); const cargoHoldCapacity = new BN( Math.floor((fleetAccount.data.stats.cargoStats as any).cargoCapacity), ); const actualFoodAmount = foodAmtToDeposit == null ? cargoHoldCapacity.div(new BN(2)) : foodAmtToDeposit === -1 // set to FULL cargoCapacity if -1 ? cargoHoldCapacity : new BN(foodAmtToDeposit); const foodIx = _depositCargoToFleet( createFleetIxResult.fleetKey[0], fleetAccount.data.cargoHold, _food.publicKey, actualFoodAmount, ); const ammoBankIx = _depositCargoToFleet( createFleetIxResult.fleetKey[0], fleetAccount.data.ammoBank, _ammo.publicKey, ammoAmtToDeposit == null ? new BN((fleetAccount.data.stats.cargoStats as any).ammoCapacity) : new BN(ammoAmtToDeposit), ); await buildSendAndCheck( (await fuelTankIx).flat().concat((await foodIx).flat()), walletSigner, provider.connection, ); await buildSendAndCheck( (await ammoBankIx).flat(), walletSigner, provider.connection, ); if (fillCargoHold && !actualFoodAmount.eq(cargoHoldCapacity)) { const fuelInCargoHoldIx = await _depositCargoToFleet( createFleetIxResult.fleetKey[0], fleetAccount.data.cargoHold, _fuel.publicKey, cargoHoldCapacity.sub(actualFoodAmount), ); await buildSendAndCheck( fuelInCargoHoldIx.flat(), walletSigner, provider.connection, ); } await buildSendAndCheck( Fleet.loadingBayToIdle( program, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], createFleetIxResult.fleetKey[0], _starbaseKey, _starbasePlayerKey, _game.publicKey(), _gameState, SAGE_MANAGER, ), walletSigner, provider.connection, ); return createFleetIxResult.fleetKey; }; 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( _resourceMintKeys, authority.publicKey(), walletSigner, provider.connection, ); await createManyMints( _fleetCargoMints, authority.publicKey(), walletSigner, provider.connection, ); await createManyMints( [_atlas], authority.publicKey(), walletSigner, provider.connection, ); await createAndFundTokenAccounts( authority, [_atlas].map((it) => [it.publicKey, 0]), _sageSigner.publicKey(), funder, walletSigner, provider.connection, ); _atlasVault = getAssociatedTokenAddressSync( _atlas.publicKey, _sageSigner.publicKey(), true, ); await createAndFundTokenAccounts( authority, _resourceMintKeys.map((it) => [it.publicKey, 999_999_999_999]), authority.publicKey(), funder, walletSigner, provider.connection, ); await createAndFundTokenAccounts( authority, _fleetCargoMints.map((it) => [it.publicKey, 999_999_999_999]), _sageSigner.publicKey(), funder, walletSigner, provider.connection, ); // create and fund atlas token account await createAndFundTokenAccounts( authority, [_atlas].map((it) => [it.publicKey, 999_999]), _fleetMinerSigner.publicKey(), funder, walletSigner, provider.connection, ); // create player profile await createProfile( playerProfileProgram, profileFactionProgram, playerProfile, funder, [ { key: _sageSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.all(), }, { key: _mineItemSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageMineItemPermissions(), }, { key: _resourceSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageResourcePermissions(), }, { key: _fleetMinerSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.permissionsBuilder([ 'doMining', 'respawn', ]), }, { key: _cargoSigner.publicKey(), expireTime: null, scope: cargoProgram.programId, permissions: CargoPermissions.all(), }, { key: _fleetRespawnSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.respawnPermissions(), }, { key: _movementSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.moveFleetPermissions(), }, ], provider.connection, authority, walletSigner, Faction.Ustur, ); await _createCargoStatsDefinition(); await createCargoTypes( _resourceMintKeys.concat(_fleetCargoMints).map((it) => it.publicKey), _cargoStatsDefinitionSigner.publicKey(), _cargoSigner, playerProfile.publicKey(), walletSigner, 0, { keyIndex: CARGO_MANAGER, values: [new BN(1)], }, provider.connection, cargoProgram, ); const pointsResult = await setupPointsAccounts( authority, walletSigner, playerProfile.publicKey(), playerProfile.publicKey(), pointsProgram, connection, ); _miningXpCategory = pointsResult.miningXpCategory; _pilotXpCategory = pointsResult.pilotXpCategory; _councilRankXpCategory = pointsResult.councilRankXpCategory; await _createGame(); // setup crew const crewConfigResult = await setupCrewConfigInstructions( authority, playerProfile.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, ); } await _createStarbase(); await _createSagePlayerProfile(); await _createStarbasePlayer(); await _createShipMint(); await _createShip(ship, shipMint.publicKey(), SizeClass.XxSmall, 1); await _createShip(ship2, shipMint.publicKey(), undefined, 1); await _createShip(ship3, shipMint.publicKey(), SizeClass.XxSmall, 1); await _createShip( ship4, shipMint.publicKey(), SizeClass.XxSmall, 1, 0 /** no ammo consumption */, undefined, ); await _createShip( ship5, shipMint.publicKey(), SizeClass.XxSmall, 1, undefined, 0 /** no food consumption */, ); await setupPointModifierAccounts( { pilotXpCategory: _pilotXpCategory, councilRankXpCategory: _councilRankXpCategory, miningXpCategory: _miningXpCategory, }, authority, walletSigner, playerProfile.publicKey(), _game.publicKey(), program, pointsProgram, connection, ); await setupProgression( authority, walletSigner, playerProfile.publicKey(), _game.publicKey(), program, connection, ); _userMiningXp = UserPoints.findAddress( pointsProgram, _miningXpCategory, playerProfile.publicKey(), )[0]; _miningXpModifier = Game.findPointsModifierAddress( program, _game.publicKey(), _miningXpCategory, )[0]; _userPilotXp = UserPoints.findAddress( pointsProgram, _pilotXpCategory, playerProfile.publicKey(), )[0]; _pilotXpModifier = Game.findPointsModifierAddress( program, _game.publicKey(), _pilotXpCategory, )[0]; _userCouncilRankXp = UserPoints.findAddress( pointsProgram, _councilRankXpCategory, playerProfile.publicKey(), )[0]; _councilRankXpModifier = Game.findPointsModifierAddress( program, _game.publicKey(), _councilRankXpCategory, )[0]; // 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 25, // numCrew ); }); it('create planet', async function () { await _createPlanet(); }); it('create MineItems', async function () { const items = _resourceMintKeys.map( (it, index) => [it.publicKey, `mineItem-${index}`, 1] as [PublicKey, string, number], ); const mineItemKeys = await createMineItems( items, _mineItemSigner, playerProfile.publicKey(), _game.publicKey(), MINE_ITEM_MANAGER, walletSigner, provider.connection, program, ); for (let index = 0; index < mineItemKeys.length; index++) { const key = mineItemKeys[index]; const data = items[index]; if (key && data) { const mineItemAcc = await readFromRPCOrError( provider.connection, program, key[0], MineItem, 'confirmed', ); const expectedKey = MineItem.findAddress( program, _game.publicKey(), data[0], )[0]; expect(mineItemAcc.key.equals(expectedKey)).toBe(true); expect( mineItemDataEquals(mineItemAcc.data, { version: 0, gameId: _game.publicKey(), mint: data[0], name: stringToByteArray(data[1], 64), resourceHardness: data[2], numResourceAccounts: new BN(0), bump: key[1], }), ).toBe(true); } else { throw 'key and/or data do not exist'; } } }); it('deregister MineItem', async function () { const mint = _resourceMintKeys[0]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; await buildSendAndCheck( MineItem.deregisterMineItem( program, _mineItemSigner, playerProfile.publicKey(), walletSigner.publicKey() /** fundsTo */, mineItemKey, _game.publicKey(), { keyIndex: MINE_ITEM_MANAGER }, ), walletSigner, provider.connection, ); const account = await program.account.mineItem.fetchNullable(mineItemKey); expect(account).toBeNull; }); it('create Resources', async function () { for (let index = 1; index < _resourceMintKeys.length; index++) { const mint = _resourceMintKeys[index]; if (!mint) { throw 'mint should exist'; } const mineItem = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const planetAccBefore = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const mineItemAccBefore = await readFromRPCOrError( provider.connection, program, mineItem, MineItem, 'confirmed', ); const ixResult = Resource.registerResource( program, _resourceSigner, playerProfile.publicKey(), _planet.publicKey(), mineItem, _game.publicKey(), { systemRichness: 250, keyIndex: RESOURCE_MANAGER, locationType: LocationType.Planet, }, ); await buildSendAndCheck( ixResult.instructions, walletSigner, provider.connection, ); const resourceAcc = await readFromRPCOrError( provider.connection, program, ixResult.resourceKey[0], Resource, 'confirmed', ); const expectedKey = Resource.findAddress( program, mineItem, _planet.publicKey(), )[0]; expect(resourceAcc.key.equals(expectedKey)).toBe(true); expect( resourceDataEquals(resourceAcc.data, { version: 0, gameId: _game.publicKey(), location: _planet.publicKey(), mineItem, locationType: LocationType.Planet, numMiners: new BN(0), amountMined: new BN(0), systemRichness: 250, bump: ixResult.resourceKey[1], }), ).toBe(true); const planetAcc = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItem, MineItem, 'confirmed', ); expect(planetAccBefore.data.numResources + 1).toBe( planetAcc.data.numResources, ); expect( mineItemAccBefore.data.numResourceAccounts .add(new BN(1)) .eq(mineItemAcc.data.numResourceAccounts), ).toBe(true); } }); it('cannot deregister MineItem with existing Resource', async function () { const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; let success = true; try { await buildSendAndCheck( MineItem.deregisterMineItem( program, _mineItemSigner, playerProfile.publicKey(), walletSigner.publicKey() /** fundsTo */, mineItemKey, _game.publicKey(), { keyIndex: MINE_ITEM_MANAGER }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch { success = false; } expect(success).toBe(false); }); it('manage MineItem banks', async function () { for (let index = 1; index < _resourceMintKeys.length; index++) { const mint = _resourceMintKeys[index]; if (!mint) { throw 'mint should exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const createIx = createAssociatedTokenAccount( mint.publicKey, mineItemKey, true, ); const fundIx1 = MineItem.fundMineItemBank( mineItemKey, mint.publicKey, authority, getAssociatedTokenAddressSync( mint.publicKey, authority.publicKey(), true, ), 1337, ); const drainIx = MineItem.drainMineItemBank( program, _mineItemSigner, playerProfile.publicKey(), walletSigner.publicKey() /** fundsTo */, mineItemKey, _game.publicKey(), createIx.address, getAssociatedTokenAddressSync( mint.publicKey, authority.publicKey(), true, ), { keyIndex: MINE_ITEM_MANAGER }, ); await buildSendAndCheck( (await Promise.all([createIx.instructions, fundIx1, drainIx])).flat(), walletSigner, provider.connection, ); expect(await connection.getAccountInfo(createIx.address, 'processed')) .toBeNull; const createIx2 = createAssociatedTokenAccount( mint.publicKey, mineItemKey, true, ); const fundIx2 = MineItem.fundMineItemBank( mineItemKey, mint.publicKey, authority, getAssociatedTokenAddressSync( mint.publicKey, authority.publicKey(), true, ), 100_000, ); await buildSendAndCheck( (await Promise.all([createIx2.instructions, fundIx2])).flat(), walletSigner, provider.connection, ); await checkTokenBalance( getAssociatedTokenAddressSync(mint.publicKey, mineItemKey, true), new BN(100_000), provider.connection, ); } }); it('update MineItem', async function () { const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const name = 'IRON ORE'; const resourceHardness = 300; await buildSendAndCheck( MineItem.updateMineItem( program, _mineItemSigner, playerProfile.publicKey(), mineItemKey, _game.publicKey(), { keyIndex: MINE_ITEM_MANAGER, name: stringToByteArray(name, 64), resourceHardness, }, ), walletSigner, provider.connection, ); const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); expect(mineItemAcc.data.resourceHardness).toBe(resourceHardness); expect(name).toEqual(byteArrayToString(mineItemAcc.data.name)); }); it('update Resource', async function () { const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const systemRichness = 350; await buildSendAndCheck( Resource.updateResource( program, _resourceSigner, playerProfile.publicKey(), resourceKey, mineItemKey, _game.publicKey(), { keyIndex: RESOURCE_MANAGER, systemRichness, }, ), walletSigner, provider.connection, ); const resourceAcc = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); expect(resourceAcc.data.systemRichness).toBe(systemRichness); }); it('create fleet & start mining', async function () { // first add ships to escrow const { shipOriginTokenAccount, shipEscrowTokenAccount } = await _createShipTokens( shipMint, authority.publicKey(), _sagePlayerProfileKey, 150, ); const ixs = ( await Promise.all( [ship, ship2, ship3, ship4, ship5].map((it) => SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority authority, // origin token account owner shipOriginTokenAccount, it.publicKey(), shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: new BN(16), index: null, }, ), ), ) ).flat(); await buildSendAndCheck(ixs, walletSigner, provider.connection); // then create fleet const fleetAddress = await _createFleet( 'big dog', [ship, ship2, ship3], [2, 2, 2], [0, 1, 2], undefined /** fuel */, 270000 /** food */, ); fleet1 = fleetAddress[0]; // finally start mining const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceBefore = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const planetBefore = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const fleetAccount = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // unauthorized key cannot mine let success = true; try { await buildSendAndCheck( Fleet.startMiningAsteroid( program, _fleetRespawnSigner /** not authorized to mine */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('KeyMismatch'); success = false; } expect(success).toBe(false); // can start mining okay await buildSendAndCheck( Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), walletSigner, provider.connection, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const resourceAfter = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const planetAfter = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); expect( fleetAccountAfter.state.MineAsteroid?.asteroid.equals( _planet.publicKey(), ), ).toBe(true); expect( fleetAccountAfter.state.MineAsteroid?.resource.equals(resourceKey), ).toBe(true); expect( fleetAccountAfter.state.MineAsteroid?.start.eq( fleetAccountAfter.state.MineAsteroid.lastUpdate, ), ).toBe(true); expect(fleetAccountAfter.state.MineAsteroid?.end.eq(new BN(0))).toBe(true); expect( resourceAfter.data.numMiners.eq( resourceBefore.data.numMiners.add(new BN(1)), ), ).toBe(true); expect( planetAfter.data.numMiners.eq(planetBefore.data.numMiners.add(new BN(1))), ).toBe(true); }); it('call planetary mining handler', async function () { // wait for some mining to happen await new Promise((r) => setTimeout(r, 1000)); const fleetAccount = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); const resourceAcc = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const fleetAmmoTokenBefore = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.asteroidMiningHandler( program, cargoProgram, fleet1, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), ]) ).flat(), walletSigner, provider.connection, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); if (!fleetAccountAfter.state.MineAsteroid) { throw 'Fleet should be in MineAsteroid'; } // mining hasn't ended expect(fleetAccountAfter.state.MineAsteroid.end.eq(new BN(0))).toBe(true); const miningDuration = fleetAccountAfter.state.MineAsteroid.lastUpdate .sub(fleetAccountAfter.state.MineAsteroid.start) .toNumber(); const maxFoodDuration = Fleet.calculateAsteroidMiningFoodDuration( fleetAccount.data.stats, Number(fleetFoodTokenBefore.delegatedAmount), ); const maxAmmoDuration = Fleet.calculateAsteroidMiningAmmoDuration( fleetAccount.data.stats, Number(fleetAmmoTokenBefore.delegatedAmount), ); const foodConsumed = Fleet.calculateAsteroidMiningFoodToConsume( fleetAccount.data.stats, Number(fleetFoodTokenBefore.delegatedAmount), Math.min(miningDuration, Math.floor(maxFoodDuration)), ); const ammoConsumed = Fleet.calculateAsteroidMiningAmmoToConsume( fleetAccount.data.stats, Number(fleetAmmoTokenBefore.delegatedAmount), Math.min( miningDuration, Math.floor(maxFoodDuration), Math.floor(maxAmmoDuration), ), ); const resourceExtracted = Fleet.calculateAsteroidMiningResourceToExtract( fleetAccount.data.stats, mineItemAcc.data, resourceAcc.data, Math.min( miningDuration, Math.floor(maxFoodDuration), Math.floor(maxAmmoDuration), ), 1_000_000 /** arbitrary large number */, ); const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const fleetAmmoTokenAfter = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( Number( fleetFoodTokenBefore.delegatedAmount - fleetFoodTokenAfter.delegatedAmount, ), ).toBe(Math.floor(foodConsumed)); expect( Number( fleetAmmoTokenBefore.delegatedAmount - fleetAmmoTokenAfter.delegatedAmount, ), ).toBe(Math.floor(ammoConsumed)); expect(Number(resourceTokenAfter.delegatedAmount)).toBe( Math.floor(resourceExtracted), ); // call the handler again await new Promise((r) => setTimeout(r, 2200)); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleet1, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, ); const fleetAccountAfter2 = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); if (!fleetAccountAfter2.state.MineAsteroid) { throw 'Fleet should be in MineAsteroid'; } // mining still hasn't ended expect(fleetAccountAfter2.state.MineAsteroid.end.eq(new BN(0))).toBe(true); const resourceTokenAfter2 = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const fleetFoodTokenAfter2 = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const fleetAmmoTokenAfter2 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( Number(fleetFoodTokenAfter.delegatedAmount) > Number(fleetFoodTokenAfter2.delegatedAmount), ).toBe(true); expect( Number(fleetAmmoTokenAfter.delegatedAmount) > Number(fleetAmmoTokenAfter2.delegatedAmount), ).toBe(true); expect( Number(resourceTokenAfter2.delegatedAmount) > Number(resourceTokenAfter.delegatedAmount), ).toBe(true); }); it('stop mining', async function () { // wait for some mining to happen await new Promise((r) => setTimeout(r, 1000)); const fleetAccount = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); if (fleetAccount.state.MineAsteroid == null) { throw 'Fleet not MineAsteroid'; } const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); const resourceBefore = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const planetBefore = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const miningXpBefore = ( await readFromRPCOrError( connection, pointsProgram, _userMiningXp, UserPoints, ) ).data.earnedPoints.toNumber(); const pilotXpBefore = ( await readFromRPCOrError( connection, pointsProgram, _userPilotXp, UserPoints, ) ).data.earnedPoints.toNumber(); const councilRankXpBefore = ( await readFromRPCOrError( connection, pointsProgram, _userCouncilRankXp, UserPoints, ) ).data.earnedPoints.toNumber(); // unauthorized key cannot stop mining let success = true; try { await sendManyToChain( [ Fleet.asteroidMiningHandler( program, cargoProgram, fleet1, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), Fleet.stopMiningAsteroid( program, cargoProgram, pointsProgram, _fleetRespawnSigner /** not authorized */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.fuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _userMiningXp, _miningXpCategory, _miningXpModifier, _userPilotXp, _pilotXpCategory, _pilotXpModifier, _userCouncilRankXp, _councilRankXpCategory, _councilRankXpModifier, _gameState, _game.publicKey(), fleetFuelToken, _fuel.publicKey, { keyIndex: CAN_MINE_WITH_FLEET }, ), ], walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('KeyMismatch'); success = false; } expect(success).toBe(false); // can stop mining okay await sendManyToChain( [ Fleet.asteroidMiningHandler( program, cargoProgram, fleet1, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), ], walletSigner, provider.connection, 'confirmed', true, ); const fleetAccount0 = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); if (fleetAccount0.state.MineAsteroid == null) { throw 'Fleet not MineAsteroid'; } const amountMined = fleetAccount0.state.MineAsteroid.amountMined; await sendManyToChain( [ Fleet.stopMiningAsteroid( program, cargoProgram, pointsProgram, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.fuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _userMiningXp, _miningXpCategory, _miningXpModifier, _userPilotXp, _pilotXpCategory, _pilotXpModifier, _userCouncilRankXp, _councilRankXpCategory, _councilRankXpModifier, _gameState, _game.publicKey(), fleetFuelToken, _fuel.publicKey, { keyIndex: CAN_MINE_WITH_FLEET }, ), ], walletSigner, provider.connection, 'confirmed', true, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const resourceAfter = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const planetAfter = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const sectorAcc = await readFromRPCOrError( provider.connection, program, _sectorKey, Sector, 'confirmed', ); const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); if (fleetAccountAfter.state.Idle == null) { throw 'Fleet not idle'; } expect( arrayDeepEquals( fleetAccountAfter.state.Idle?.sector, sectorAcc.data.coordinates, (a, b) => a.eq(b), ), ).toBe(true); expect( resourceAfter.data.numMiners.eq( resourceBefore.data.numMiners.sub(new BN(1)), ), ).toBe(true); expect( planetAfter.data.numMiners.eq(planetBefore.data.numMiners.sub(new BN(1))), ).toBe(true); expect( resourceTokenAfter.delegatedAmount >= resourceTokenBefore.delegatedAmount, ).toBe(true); expect( fleetAccount0.state.MineAsteroid.amountMined.gt( fleetAccount.state.MineAsteroid.amountMined, ), ).toBe(true); const earnedPilotXP = Fleet.getFleetSize(fleetAccount0.data.shipCounts) * (ProgressionItemType.AsteroidExit + 1); const mineItemAccount = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); const earnedMiningXP = Math.floor( amountMined.toNumber() * (mineItemAccount.data.resourceHardness / MineItem.RESOURCE_HARDNESS_DECIMALS) * (ProgressionItemType.Mining + 1), ); await checkUserPoints( _userMiningXp, miningXpBefore + earnedMiningXP, pointsProgram, connection, ); await checkUserPoints( _userPilotXp, pilotXpBefore + earnedPilotXP, pointsProgram, connection, ); await checkUserPoints( _userCouncilRankXp, councilRankXpBefore + earnedPilotXP + earnedMiningXP, pointsProgram, connection, ); }); it('transition from mining to respawn', async function () { // create fleet, passing in amount of fuel to start with const fleetAddress = await _createFleet( 'big cat', [ship3], [1], [2], undefined /** fuel */, undefined /** food */, undefined /** ammo */, ); const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // elect to respawn const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const resourceBefore = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const planetBefore = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const atlasTokenFrom = getAssociatedTokenAddressSync( _atlas.publicKey, _fleetMinerSigner.publicKey(), true, ); // unauthorized key cannot mine let success = true; try { await buildSendAndCheck( [ Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), Fleet.mineAsteroidToRespawn( program, _fleetRespawnSigner /** not authorized */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), atlasTokenFrom, _atlasVault, { keyIndex: CAN_MINE_WITH_FLEET }, ), ], walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('KeyMismatch'); success = false; } expect(success).toBe(false); // Stop mining call moves fleet to respawn const atlasVaultBefore = await getAccount( program.provider.connection, _atlasVault, 'processed', ); await buildSendAndCheck( [ Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), Fleet.mineAsteroidToRespawn( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), atlasTokenFrom, _atlasVault, { keyIndex: CAN_MINE_WITH_FLEET }, ), ], walletSigner, provider.connection, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const resourceAfter = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const planetAfter = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); if (!fleetAccountAfter.state.Respawn) { throw 'Fleet should be in respawn state'; } expect( resourceAfter.data.numMiners.eq( resourceBefore.data.numMiners.sub(new BN(1)), ), ).toBe(true); expect( planetAfter.data.numMiners.eq(planetBefore.data.numMiners.sub(new BN(1))), ).toBe(true); expect( resourceTokenAfter.delegatedAmount > resourceTokenBefore.delegatedAmount, ).toBe(true); const atlasVaultAfter = await getAccount( program.provider.connection, _atlasVault, 'processed', ); const gameStateAcc = await readFromRPCOrError( provider.connection, program, _gameState, GameState, 'confirmed', ); expect( Number(atlasVaultAfter.amount) - Number(atlasVaultBefore.amount), ).toBe(gameStateAcc.data.misc.respawnFee.toNumber()); // empty fleet cargo const dropCargoInstructions = [ _fuel.publicKey, _food.publicKey, _ammo.publicKey, mint.publicKey, ].map((it) => { return Fleet.forceDropFleetCargo( program, cargoProgram, fleetKey, it.equals(_fuel.publicKey) ? fleetAccountAfter.data.fuelTank : it.equals(_ammo.publicKey) ? fleetAccountAfter.data.ammoBank : fleetAccountAfter.data.cargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), it, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _game.publicKey(), it.equals(_fuel.publicKey) ? fleetFuelToken : it.equals(_ammo.publicKey) ? fleetAmmoToken : it.equals(_food.publicKey) ? fleetFoodToken : resourceTokenTo, it, ); }); await buildSendAndCheck( (await Promise.all(dropCargoInstructions)).flat(), walletSigner, provider.connection, ); const fuelTankAfter: CargoPod = await readFromRPCOrError( provider.connection, cargoProgram, fleetAccountAfter.data.fuelTank, CargoPod, 'confirmed', ); expect(fuelTankAfter.data.openTokenAccounts).toBe(0); const cargoHoldAfter: CargoPod = await readFromRPCOrError( provider.connection, cargoProgram, fleetAccountAfter.data.cargoHold, CargoPod, 'confirmed', ); expect(cargoHoldAfter.data.openTokenAccounts).toBe(0); const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // transition from respawn to loading bay // can't respawn until respawn time has passed success = true; try { await buildSendAndCheck( Fleet.respawnToLoadingBay( program, _fleetRespawnSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, fleetAccount.data.cargoHold, fleetAccount.data.fuelTank, fleetAccount.data.ammoBank, _game.publicKey(), _gameState, { keyIndex: CAN_RESPAWN_FLEET }, ), walletSigner, provider.connection, ); } catch (e) { expect(JSON.stringify(e as Error)).toContain('RespawnTimeNotElapsed'); success = false; } expect(success).toBe(false); const respawnTime = (fleetAccount.data.stats).miscStats .respawnTime; // Sleep for 5 second to allow for the respawn time to pass await new Promise((f) => setTimeout(f, respawnTime * 1000)); await buildSendAndCheck( Fleet.respawnToLoadingBay( program, _fleetRespawnSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, fleetAccount.data.cargoHold, fleetAccount.data.fuelTank, fleetAccount.data.ammoBank, _game.publicKey(), _gameState, { keyIndex: CAN_RESPAWN_FLEET }, ), walletSigner, provider.connection, { sendOptions: { skipPreflight: true } }, ); const fleetAccountAfter1 = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); expect( fleetAccountAfter1.state.StarbaseLoadingBay?.starbase.equals( _starbaseKey, ), ).toBe(true); const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // no effect on totalCrew expect(sbp0.data.oldTotalCrew).toBe(sbp1.data.oldTotalCrew); expect(sbp0.data.newTotalCrew).toBe(sbp1.data.newTotalCrew); }); it('cannot start mining with no fuel', async function () { // create fleet, passing in amount of fuel to start with const fleetAddress = await _createFleet( 'caterpillar', [ship3], [1], [2], 0 /** no fuel */, undefined /** food */, undefined /** ammo */, ); const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); // Insufficient fuel cannot start mining let success0 = true; try { await sendManyToChain( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, 'confirmed', true, ); } catch (err) { success0 = false; } expect(success0).toBe(false); }); it('mining with a small amount of food works okay', async function () { // create fleet with a tiny amount of food food const fleetAddress = await _createFleet( 'tiny food', [ship], [1], [0], undefined /** fuel */, 1 /** food */, ); const fleet2 = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleet2, Fleet, 'confirmed', ); // finally start mining const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect(Number(fleetFoodTokenBefore.delegatedAmount)).toBe(1); const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); // start mining with tiny amount of food await buildSendAndCheck( [ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet2, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ], walletSigner, provider.connection, ); const resourceTokenToBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect(Number(resourceTokenToBefore.delegatedAmount)).toBe(0); // let some time pass await new Promise((f) => setTimeout(f, 1000)); // call mining handler await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleet2, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, ); const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const resourceTokenToAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleet2, Fleet, 'confirmed', ); expect(fleetAccountAfter.state.MineAsteroid?.end.gt(new BN(0))).toBe(true); expect(Number(fleetFoodTokenAfter.delegatedAmount)).toBe(0); // a tiny amount of resource was mined expect(Number(resourceTokenToAfter.delegatedAmount)).toBeLessThanOrEqual(5); }); it('mining stops when food is exhausted', async function () { // create fleet, passing in amount of food to start with const fleetAddress = await _createFleet( 'big bunny', [ship, ship2], [5, 5], [0, 1], undefined /** fuel */, 8 /** food */, ); /** food should run out in this test */ const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // call mining handler const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const ammoToken0 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, ); // food is exhausted const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect(Number(fleetFoodTokenBefore.delegatedAmount) === 0).toBe(true); // resource was extracted const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( resourceTokenAfter.delegatedAmount > resourceTokenBefore.delegatedAmount, ).toBe(true); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); expect(fleetAccountAfter.state.MineAsteroid?.end.gt(new BN(0))).toBe(true); // ammo was consumed const ammoToken1 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect(ammoToken0.delegatedAmount > ammoToken1.delegatedAmount).toBe(true); // call mining again await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, ); // food was not consumed && resource was not mined const resourceTokenAfter2 = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( resourceTokenAfter.delegatedAmount === resourceTokenAfter2.delegatedAmount, ).toBe(true); const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect( fleetFoodTokenAfter.delegatedAmount === fleetFoodTokenBefore.delegatedAmount, ).toBe(true); // ammo was not consumed const ammoToken2 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect(ammoToken2.delegatedAmount === ammoToken1.delegatedAmount).toBe( true, ); }); it('mining stops when ammo is exhausted', async function () { // create fleet, passing in amount of food to start with const fleetAddress = await _createFleet( 'a3', [ship2], [1], [1], undefined /** fuel */, 800 /** food */, 8 /** ammo */, ); /** ammo should run out in this test */ const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // call mining handler const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); const foodToken0 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, ); // ammo is exhausted const fleetAmmoTokenBefore = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect(Number(fleetAmmoTokenBefore.delegatedAmount) === 0).toBe(true); // resource was extracted const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( resourceTokenAfter.delegatedAmount > resourceTokenBefore.delegatedAmount, ).toBe(true); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); expect(fleetAccountAfter.state.MineAsteroid?.end.gt(new BN(0))).toBe(true); // food was consumed const foodToken1 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect(foodToken0.delegatedAmount > foodToken1.delegatedAmount).toBe(true); // call mining again await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, ); // food was not consumed && resource was not mined const resourceTokenAfter2 = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( resourceTokenAfter.delegatedAmount === resourceTokenAfter2.delegatedAmount, ).toBe(true); const fleetAmmoTokenAfter = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( fleetAmmoTokenAfter.delegatedAmount === fleetAmmoTokenBefore.delegatedAmount, ).toBe(true); // food was not consumed const foodToken2 = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( Number(foodToken2.delegatedAmount) === Number(foodToken1.delegatedAmount), ).toBe(true); }); it('mining uses cargo space freed by consumed resources and then stops', async function () { const fleetAddress = await _createFleet( 'a1', [ship], [1], [0], undefined /** fuel */, 40 /** food */, undefined /** ammo */, ); /** food should run out in this test */ const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // call mining handler const fleetAmmoTokenBefore = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, { sendOptions: { skipPreflight: true } }, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); if (!fleetAccountAfter.state.MineAsteroid) { throw 'Fleet should be in MineAsteroid'; } const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); const resourceAcc = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const miningDuration = Fleet.calculateAsteroidMiningFoodDuration( fleetAccount.data.stats, Number(fleetFoodTokenBefore.delegatedAmount), ); const foodConsumed = Fleet.calculateAsteroidMiningFoodToConsume( fleetAccount.data.stats, Number(fleetFoodTokenBefore.delegatedAmount), miningDuration, ); const ammoConsumed = Fleet.calculateAsteroidMiningAmmoToConsume( fleetAccount.data.stats, Number(fleetAmmoTokenBefore.delegatedAmount), miningDuration, ); const resourceExtracted = Fleet.calculateAsteroidMiningResourceToExtract( fleetAccount.data.stats, mineItemAcc.data, resourceAcc.data, miningDuration, 1_000_000 /** arbitrary large number */, ); // ammo is consumed const fleetAmmoTokenAfter = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( Number( fleetAmmoTokenBefore.delegatedAmount - fleetAmmoTokenAfter.delegatedAmount, ), ).toBe(Math.floor(ammoConsumed)); // food is consumed const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect( Number( fleetFoodTokenBefore.delegatedAmount - fleetFoodTokenAfter.delegatedAmount, ), ).toBe(Math.floor(foodConsumed)); // resource was extracted const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( Number(resourceTokenAfter.delegatedAmount) - Number(resourceTokenBefore.delegatedAmount), ).toBe(Math.floor(resourceExtracted)); // mining ended expect(fleetAccountAfter.state.MineAsteroid.end.gt(new BN(0))).toBe(true); }); it('mining with 0 ammo consumption rate works okay', async function () { const fleetAddress = await _createFleet( '0ammo', [ship4], [1], [3], undefined /** fuel */, undefined /** food */, 0 /** no ammo */, ); const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // call mining handler const fleetAmmoTokenBefore = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, { sendOptions: { skipPreflight: true } }, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); if (!fleetAccountAfter.state.MineAsteroid) { throw 'Fleet should be in MineAsteroid'; } const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); const resourceAcc = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const miningDuration = Math.min( fleetAccountAfter.state.MineAsteroid.lastUpdate .sub(fleetAccountAfter.state.MineAsteroid.start) .toNumber(), Math.floor( Fleet.calculateAsteroidMiningFoodDuration( fleetAccount.data.stats, Number(fleetFoodTokenBefore.delegatedAmount), ), ), ); const foodConsumed = Fleet.calculateAsteroidMiningFoodToConsume( fleetAccount.data.stats, Number(fleetFoodTokenBefore.delegatedAmount), miningDuration, ); const resourceExtracted = Fleet.calculateAsteroidMiningResourceToExtract( fleetAccount.data.stats, mineItemAcc.data, resourceAcc.data, miningDuration, 1_000_000 /** arbitrary large number */, ); // ammo is not consumed const fleetAmmoTokenAfter = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( Number(fleetAmmoTokenBefore.delegatedAmount) === Number(fleetAmmoTokenAfter.delegatedAmount), ).toBe(true); // food is consumed const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect( Number( fleetFoodTokenBefore.delegatedAmount - fleetFoodTokenAfter.delegatedAmount, ), ).toBe(Math.floor(foodConsumed)); // resource was extracted const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( Number(resourceTokenAfter.delegatedAmount) - Number(resourceTokenBefore.delegatedAmount), ).toBe(Math.floor(resourceExtracted)); // mining did not end expect(fleetAccountAfter.state.MineAsteroid.end.eq(new BN(0))).toBe(true); }); it('mining with 0 food consumption rate works okay', async function () { const fleetAddress = await _createFleet( '0food', [ship5], [1], [4], undefined /** fuel */, 0 /** no food */, undefined /** ammo */, ); const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // call mining handler const fleetAmmoTokenBefore = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, { sendOptions: { skipPreflight: true } }, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); if (!fleetAccountAfter.state.MineAsteroid) { throw 'Fleet should be in MineAsteroid'; } const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); const resourceAcc = await readFromRPCOrError( provider.connection, program, resourceKey, Resource, 'confirmed', ); const miningDuration = Math.min( fleetAccountAfter.state.MineAsteroid.lastUpdate .sub(fleetAccountAfter.state.MineAsteroid.start) .toNumber(), Math.floor( Fleet.calculateAsteroidMiningAmmoDuration( fleetAccount.data.stats, Number(fleetAmmoTokenBefore.delegatedAmount), ), ), ); const ammoConsumed = Fleet.calculateAsteroidMiningAmmoToConsume( fleetAccount.data.stats, Number(fleetAmmoTokenBefore.delegatedAmount), miningDuration, ); const resourceExtracted = Fleet.calculateAsteroidMiningResourceToExtract( fleetAccount.data.stats, mineItemAcc.data, resourceAcc.data, miningDuration, 1_000_000 /** arbitrary large number */, ); // food is not consumed const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect( Number(fleetFoodTokenBefore.delegatedAmount) === Number(fleetFoodTokenAfter.delegatedAmount), ).toBe(true); // ammo is consumed const fleetAmmoTokenAfter = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( Number(fleetAmmoTokenBefore.delegatedAmount) - Number(fleetAmmoTokenAfter.delegatedAmount), ).toBe(Math.floor(ammoConsumed)); // resource was extracted const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( Number(resourceTokenAfter.delegatedAmount) - Number(resourceTokenBefore.delegatedAmount), ).toBe(Math.floor(resourceExtracted)); // mining did not end expect(fleetAccountAfter.state.MineAsteroid.end.eq(new BN(0))).toBe(true); }); it('mining stops when cargo hold is full', async function () { const fleetAddress = await _createFleet( 'a2', [ship], [1], [0], undefined /** fuel */, 400 /** food */, undefined /** ammo */, true /** fill with fuel */, ); /** food should run out in this test */ const fleetKey = fleetAddress[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const resourceTokenFrom = getAssociatedTokenAddressSync( mint.publicKey, mineItemKey, true, ); const resourceTokenTo = getAssociatedTokenAddressSync( mint.publicKey, fleetAccount.data.cargoHold, true, ); const fleetFoodToken = getAssociatedTokenAddressSync( _food.publicKey, fleetAccount.data.cargoHold, true, ); const fleetAmmoToken = getAssociatedTokenAddressSync( _ammo.publicKey, fleetAccount.data.ammoBank, true, ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // start mining const createResourceTokenTo = createAssociatedTokenAccount( mint.publicKey, fleetAccount.data.cargoHold, ); await buildSendAndCheck( ( await Promise.all([ createResourceTokenTo.instructions, Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleetKey, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ]) ).flat(), walletSigner, provider.connection, ); // wait for some mining to happen await new Promise((r) => setTimeout(r, 2000)); // call mining handler const fleetFoodTokenBefore = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); const fleetAmmoTokenBefore = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); const resourceTokenBefore = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); await buildSendAndCheck( Fleet.asteroidMiningHandler( program, cargoProgram, fleetKey, _starbaseKey, mineItemKey, resourceKey, _planet.publicKey(), fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _food.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), _gameState, _game.publicKey(), fleetFoodToken, fleetAmmoToken, resourceTokenFrom, resourceTokenTo, _food.publicKey, _ammo.publicKey, ), walletSigner, provider.connection, { sendOptions: { skipPreflight: true } }, ); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, fleetKey, Fleet, 'confirmed', ); if (!fleetAccountAfter.state.MineAsteroid) { throw 'Fleet should be in MineAsteroid'; } // ammo is NOT consumed const fleetAmmoTokenAfter = await getAccount( program.provider.connection, fleetAmmoToken, 'processed', ); expect( fleetAmmoTokenBefore.delegatedAmount === fleetAmmoTokenAfter.delegatedAmount, ).toBe(true); // food is NOT consumed const fleetFoodTokenAfter = await getAccount( program.provider.connection, fleetFoodToken, 'processed', ); expect( fleetFoodTokenBefore.delegatedAmount === fleetFoodTokenAfter.delegatedAmount, ).toBe(true); // resource was NOT extracted const resourceTokenAfter = await getAccount( program.provider.connection, resourceTokenTo, 'processed', ); expect( Number(resourceTokenAfter.delegatedAmount) === Number(resourceTokenBefore.delegatedAmount), ).toBe(true); // mining ended expect(fleetAccountAfter.state.MineAsteroid.end.gt(new BN(0))).toBe(true); }); it('cannot mine on planets which are not asteroid', async () => { const nonAsteroidPlanet = keypairToAsyncSigner(Keypair.generate()); await buildSendAndCheck( Planet.registerPlanet( program, _sageSigner, playerProfile.publicKey(), nonAsteroidPlanet, _sectorKey, _game.publicKey(), { name: 'Non asteroid Planet', size: new BN(10000), maxHp: new BN(10000), subCoordinates: [new BN(1), new BN(2)], planetType: PlanetType.Terrestrial, position: SectorRing.Inner, keyIndex: SAGE_MANAGER, }, ), walletSigner, provider.connection, ); // finally start mining const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; const fleetAccount = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); // should fail let success = true; try { await buildSendAndCheck( Fleet.startMiningAsteroid( program, _fleetRespawnSigner /** not authorized to mine */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, nonAsteroidPlanet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('InvalidPlanetType'); success = false; } expect(success).toBe(false); }); it('cannot mine if not in the same sector as the asteroid', async () => { // create a another sector and move the fleet there const fleetAccount = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const fleetFuelToken = getAssociatedTokenAddressSync( _fuel.publicKey, fleetAccount.data.fuelTank, true, ); const fuelCargoType = CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0]; const coordinates: [BN, BN] = [new BN(1), new BN(0)]; const sectorIx = Sector.registerSector( program, _sageSigner, playerProfile.publicKey(), playerProfile.publicKey() /** discoverer */, _game.publicKey(), coordinates, 'Another Sector', SAGE_MANAGER, ); const moveIx = Fleet.warpToCoordinate( program, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleetAccount.data.fuelTank, fuelCargoType, _cargoStatsDefinitionSigner.publicKey(), fleetFuelToken, _fuel.publicKey, _gameState, _game.publicKey(), cargoProgram, { keyIndex: SAGE_MANAGER, toSector: coordinates, }, ); await buildSendAndCheck( [sectorIx.instructions, moveIx], walletSigner, provider.connection, ); // wait for movement to finish await new Promise((r) => setTimeout(r, 2000)); const mint = _resourceMintKeys[1]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; // should fail let success = true; try { await sendManyToChain( [ Fleet.moveWarpHandler( program, pointsProgram, playerProfile.publicKey(), fleet1, _userPilotXp, _pilotXpCategory, _pilotXpModifier, _userCouncilRankXp, _councilRankXpCategory, _councilRankXpModifier, _game.publicKey(), ), Fleet.startMiningAsteroid( program, _fleetMinerSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, _starbaseKey, _starbasePlayerKey, mineItemKey, resourceKey, _planet.publicKey(), _gameState, _game.publicKey(), fleetFuelToken, { keyIndex: CAN_MINE_WITH_FLEET, }, ), ], walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('InvalidSector'); success = false; } expect(success).toBe(false); }); it('deregister Resource', async function () { const mint = _resourceMintKeys[2]; if (!mint) { throw 'mint data does not exist'; } const mineItemKey = MineItem.findAddress( program, _game.publicKey(), mint.publicKey, )[0]; const planetAccBefore = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const mineItemAccBefore = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); const resourceKey = Resource.findAddress( program, mineItemKey, _planet.publicKey(), )[0]; await buildSendAndCheck( Resource.deregisterResource( program, _resourceSigner, playerProfile.publicKey(), _starbasePlayerKey, resourceKey, mineItemKey, _planet.publicKey(), _game.publicKey(), { keyIndex: RESOURCE_MANAGER }, ), walletSigner, provider.connection, ); const account = await program.account.resource.fetchNullable(resourceKey); expect(account).toBeNull; const planetAcc = await readFromRPCOrError( provider.connection, program, _planet.publicKey(), Planet, 'confirmed', ); const mineItemAcc = await readFromRPCOrError( provider.connection, program, mineItemKey, MineItem, 'confirmed', ); expect(planetAccBefore.data.numResources - 1).toBe( planetAcc.data.numResources, ); expect( mineItemAccBefore.data.numResourceAccounts .sub(new BN(1)) .eq(mineItemAcc.data.numResourceAccounts), ).toBe(true); }); });