import { getAccount, getAssociatedTokenAddress } from '@solana/spl-token'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { AnchorProvider, BN, Program, Wallet } from '@staratlas/anchor'; import { CARGO_IDL, CargoPermissions, CargoPod, CargoStatsDefinition, CargoType, } from '@staratlas/cargo'; import { CREW_IDL } from '@staratlas/crew'; import { FixedSizeArray, arrayDeepEquals, buildSendAndCheck, createMint, createTokenAccount, keypairToAsyncSigner, mintToTokenAccount, readFromRPCOrError, stringToByteArray, } from '@staratlas/data-source'; import { PLAYER_PROFILE_IDL } from '@staratlas/player-profile'; import { Faction, PROFILE_FACTION_IDL, ProfileFactionAccount, } from '@staratlas/profile-faction'; import { AddShipEscrowInput, CombatStats, DisbandedFleet, Fleet, FleetShips, Game, GameState, MiscStats, RegisterShipInput, RemoveShipEscrowInput, SAGE_IDL, SagePermissions, SagePlayerProfile, Sector, Ship, ShipStats, SizeClass, Starbase, StarbasePlayer, disbandedFleetDataEquals, getSizeClassAnchorEnum, shipStatsEquals, } from '../src'; import { BALANCED_STAR_BASE_LEVELS, CREW_PROGRAM_ID, SAGE_PROGRAM_ID, SageGameTest, checkTokenBalance, createAndFundTokenAccounts, createCargoTypes, createManyMints, createProfile, getRandomInt, mintAndImportCrewToGame, sendManyToChain, setupCrewConfigInstructions, setupUmi, } from './helpers'; import { chunk } from 'lodash'; describe('Fleet CRUD', () => { 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 funder = keypairToAsyncSigner(Keypair.generate()); const authority = keypairToAsyncSigner(Keypair.generate()); const playerProfile = keypairToAsyncSigner(Keypair.generate()); // fleets stuff const _sageSigner = 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 _fleetManagerKey = keypairToAsyncSigner(Keypair.generate()); const _removeShipEscrowKey = keypairToAsyncSigner(Keypair.generate()); const _shipManagerKey = keypairToAsyncSigner(Keypair.generate()); const _manageFleetCargoKey = keypairToAsyncSigner(Keypair.generate()); let fleet1: PublicKey; let fleet1FuelTank: PublicKey; let fleet1AmmoBank: PublicKey; let fleet1CargoHold: PublicKey; // `Ship` const ship = keypairToAsyncSigner(Keypair.generate()); const ship2 = keypairToAsyncSigner(Keypair.generate()); const ship3 = keypairToAsyncSigner(Keypair.generate()); // ship mint const shipMint = keypairToAsyncSigner(Keypair.generate()); let _shipOriginTokenAccount: PublicKey; let _shipEscrowTokenAccount: PublicKey; // Sector stuff const _game = keypairToAsyncSigner(Keypair.generate()); let _gameState: PublicKey; let _sectorKey: PublicKey; const _ammo = Keypair.generate(); const _food = Keypair.generate(); const _fuel = Keypair.generate(); const _repairKit = Keypair.generate(); const _fleetCargoMints = [_fuel, _ammo, _repairKit, _food]; 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, }; // cargo stuff const _cargoStatsDefinitionSigner = keypairToAsyncSigner(Keypair.generate()); const _cargoSigner = keypairToAsyncSigner(Keypair.generate()); // key indices const CARGO_MANAGER = 1; const SAGE_MANAGER = 2; const FLEET_MANAGER = 3; const REMOVE_ESCROW = 4; const MANAGE_SHIP = 5; const MANAGE_CARGO = 6; 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, repairKit: _repairKit.publicKey, } /** mints */, undefined /** vaults */, undefined /** crafting */, { statsDefinition: _cargoStatsDefinitionSigner.publicKey() } /** cargo */, _updatedRiskZones /** risk zones */, ); 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 buildSendAndCheck( [updateGameIx].flat(), walletSigner, provider.connection, ); const starbaseLevelsChunks = chunk(BALANCED_STAR_BASE_LEVELS, 7); for (let index = 0; index < starbaseLevelsChunks.length; index++) { const starbaseLevelsChunk = starbaseLevelsChunks[index]; await buildSendAndCheck( GameState.updateGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, SAGE_MANAGER, { starbaseLevels: starbaseLevelsChunk, } /** fleet */, ), walletSigner, provider.connection, ); } await buildSendAndCheck( [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 = await getAssociatedTokenAddress( mint.publicKey, _sageSigner.publicKey(), true, ); const tokenTo = await getAssociatedTokenAddress( 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 _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 = getRandomInt(SizeClass.Medium), crew = 0, activate = true, ) => { const stats: ShipStats = SageGameTest.getDefaultShipStats(crew); const input: RegisterShipInput = { name: stringToByteArray('💀❤️🤖', 64) as FixedSizeArray, sizeClass: getSizeClassAnchorEnum(sizeClass), stats: stats, isActive: activate, keyIndex: SAGE_MANAGER, }; await buildSendAndCheck( Ship.registerShip( program, _sageSigner, playerProfile.publicKey(), shipKey, mint, _game.publicKey(), input, ), walletSigner, provider.connection, ); }; // init Ship const _createFleet = async ( fleetName = 'mosh', shipToUse = ship, shipAmount = 1, shipEscrowIndex = 0, ) => { const fleetLabel = stringToByteArray(fleetName, 32); const fleetAddress = Fleet.findAddress( program, _game.publicKey(), playerProfile.publicKey(), fleetLabel, )[0]; const createFleetIxResult = Fleet.createFleet( program, cargoProgram, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], shipToUse.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, _cargoStatsDefinitionSigner.publicKey(), { shipAmount, shipEscrowIndex, fleetLabel, keyIndex: FLEET_MANAGER, }, ); await buildSendAndCheck( createFleetIxResult.instructions, walletSigner, provider.connection, ); const fleet = await readFromRPCOrError( provider.connection, program, fleetAddress, Fleet, 'confirmed', ); expect(fleet.key.equals(fleetAddress)).toBe(true); expect( fleet.data.cargoHold.equals(createFleetIxResult.cargoHoldKey[0]), ).toBe(true); expect(fleet.data.fuelTank.equals(createFleetIxResult.fuelTankKey[0])).toBe( true, ); expect(fleet.data.ammoBank.equals(createFleetIxResult.ammoBankKey[0])).toBe( true, ); expect(fleet.state.StarbaseLoadingBay?.starbase.equals(_starbaseKey)).toBe( true, ); expect(fleet.data.shipCounts.total).toEqual(fleet.data.shipCounts.updated); expect(fleet.data.faction).toEqual(Faction.Ustur); return { ammoBankKey: createFleetIxResult.ammoBankKey, cargoHoldKey: createFleetIxResult.cargoHoldKey, fuelTankKey: createFleetIxResult.fuelTankKey, fleetKey: createFleetIxResult.fleetKey, fleetData: fleet, }; }; const syncGameAccount = async () => { // Sync game account const gameAcc = await readFromRPCOrError( provider.connection, program, _game.publicKey(), Game, ); const newGameStateKey = GameState.findAddress( program, _game.publicKey(), gameAcc.data.updateId.add(new BN(1)), )[0]; const ixs = [ GameState.copyGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), _gameState, newGameStateKey, { keyIndex: SAGE_MANAGER }, ), GameState.activateGameState( program, _sageSigner, playerProfile.publicKey(), _game.publicKey(), newGameStateKey, { keyIndex: SAGE_MANAGER, }, ), ]; await buildSendAndCheck( (await Promise.all(ixs)).flat(), walletSigner, provider.connection, ); _gameState = newGameStateKey; return newGameStateKey; }; const updateFleetShip = async ( firstShip: PublicKey, secondShip: PublicKey, fleetAddress: PublicKey, ) => { // update ship in fleet await buildSendAndCheck( Ship.setNextShip( program, _shipManagerKey, playerProfile.publicKey(), firstShip, secondShip, _game.publicKey(), MANAGE_SHIP, ), walletSigner, provider.connection, ); // update the _gameState await syncGameAccount(); const fleetAccount = await readFromRPCOrError( provider.connection, program, fleetAddress, Fleet, 'confirmed', ); const gameAccount = await readFromRPCOrError( provider.connection, program, _game.publicKey(), Game, 'confirmed', ); expect( gameAccount.data.updateId.eq(fleetAccount.data.updateId.add(new BN(1))), ).toBe(true); return [fleetAccount, gameAccount] as [Fleet, Game]; }; const syncShipEscrow = async (theStarbasePlayer: PublicKey) => { const sbp = await readFromRPCOrError( provider.connection, program, theStarbasePlayer, StarbasePlayer, 'confirmed', ); if (sbp.wrappedShipEscrows.length == 0) { await buildSendAndCheck( StarbasePlayer.syncStarbasePlayer( program, theStarbasePlayer, sbp.data.starbase, _game.publicKey(), _gameState, ), walletSigner, provider.connection, ); } else { await buildSendAndCheck( ( await Promise.all( sbp.wrappedShipEscrows.map(async (it, index) => { const thisShip = await readFromRPCOrError( provider.connection, program, it.ship, Ship, 'confirmed', ); return SagePlayerProfile.updateShipEscrow( program, it.ship, thisShip.data.next.key, theStarbasePlayer, sbp.data.starbase, _game.publicKey(), _gameState, { shipEscrowIndex: index, }, ); }), ) ).flat(), walletSigner, provider.connection, ); } }; beforeAll(async () => { const airdropTx = await connection.requestAirdrop( walletSigner.publicKey(), 500_000_000_000, ); await connection.confirmTransaction(airdropTx); const airdropTx2 = await connection.requestAirdrop( funder.publicKey(), 500_000_000_000, ); await connection.confirmTransaction(airdropTx2); // create mints await createManyMints( _fleetCargoMints, authority.publicKey(), walletSigner, provider.connection, ); await createAndFundTokenAccounts( authority, _fleetCargoMints.map((it) => [it.publicKey, 999_999_999_999]), _sageSigner.publicKey(), funder, walletSigner, provider.connection, ); // create player profile await createProfile( playerProfileProgram, profileFactionProgram, playerProfile, funder, [ // Cargo Superuser // 1 { key: _cargoSigner.publicKey(), expireTime: null, scope: cargoProgram.programId, permissions: CargoPermissions.all(), }, // Fleet Superuser // 2 { key: _sageSigner.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.all(), }, // SagePermissions::MANAGE_FLEET // 3 { key: _fleetManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageFleetPermissions(), }, // SagePermissions::REMOVE_SHIP_ESCROW // 4 { key: _removeShipEscrowKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.removeShipEscrowPermissions(), }, // SagePermissions::MANAGE_SHIP // 5 { key: _shipManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageShipPermissions(), }, // SagePermissions::MANAGE_FLEET_CARGO // 6 { key: _manageFleetCargoKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageFleetCargoPermissions(), }, ], provider.connection, authority, walletSigner, Faction.Ustur, ); await _createCargoStatsDefinition(); await createCargoTypes( _fleetCargoMints.map((it) => it.publicKey), _cargoStatsDefinitionSigner.publicKey(), _cargoSigner, playerProfile.publicKey(), walletSigner, 0, { keyIndex: CARGO_MANAGER, values: [new BN(1)], }, provider.connection, cargoProgram, ); 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, 2); await _createShip(ship2, shipMint.publicKey(), SizeClass.Medium, 3); await _createShip(ship3, shipMint.publicKey(), SizeClass.Large, 4); // 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 30, // numCrew ); }); it('add Ship to escrow', async () => { const shipTokenResult = await _createShipTokens( shipMint, undefined, undefined, 100, ); _shipOriginTokenAccount = shipTokenResult.shipOriginTokenAccount; _shipEscrowTokenAccount = shipTokenResult.shipEscrowTokenAccount; const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // transfer `Ship` from walletSigner to `SagePlayerProfile` escrow account const input: AddShipEscrowInput = { shipAmount: new BN(1), index: null, // no index in `StarbasePlayer` remaining data implies this is a new Ship mint to insert }; const addShipIx = SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority walletSigner, // origin token account owner _shipOriginTokenAccount, ship.publicKey(), _shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, input, ); await buildSendAndCheck(addShipIx, walletSigner, provider.connection); // check `Ship` added to `StarbasePlayer` remaining data const sbp = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp.data.updatedShipEscrowCount).toEqual(sbp.data.shipEscrowCount); const list = sbp.wrappedShipEscrows; const escrow = list[0]; if (!escrow) { throw new Error('escrow not found'); } expect(escrow?.amount.eq(input.shipAmount)).toBe(true); expect(escrow?.ship.equals(ship.publicKey())).toBe(true); // check `Ship` added to escrow token account const account = await getAccount( provider.connection, _shipEscrowTokenAccount, 'confirmed', ); if (account == null) { throw new Error('escrow account not found'); } expect(new BN(account.amount.toString()).eq(input.shipAmount)).toBe(true); expect(account.mint.equals(shipMint.publicKey())).toBe(true); expect(sbp.totalCrew()).toBe(sbp0.totalCrew()); }); it('create a Fleet', async () => { const sbpBefore = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const result = await _createFleet(); fleet1 = result.fleetKey[0]; fleet1FuelTank = result.fuelTankKey[0]; fleet1AmmoBank = result.ammoBankKey[0]; fleet1CargoHold = result.cargoHoldKey[0]; // test fleet ships const fleetShips = await readFromRPCOrError( provider.connection, program, FleetShips.findAddress(program, fleet1)[0], FleetShips, 'confirmed', ); expect(fleetShips.data.fleetShipsInfoCount).toBe(1); const fleetShipInfo = fleetShips.fleetShips[0]; expect(fleetShipInfo?.amount.eq(new BN(1))).toBe(true); expect(fleetShipInfo?.ship.equals(ship.publicKey())).toBe(true); const gameAcc = await readFromRPCOrError( provider.connection, program, _game.publicKey(), Game, 'confirmed', ); expect(fleetShipInfo?.updateId.eq(gameAcc.data.updateId)).toBe(true); // test ship escrow const sbpAfter = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const fleetData = result.fleetData; const miscStats = fleetData.data.stats.miscStats as MiscStats; // the fleet's crew was deducted from Starbase crew expect(sbpAfter.totalCrew()).toBe( sbpBefore.totalCrew() - miscStats.crewCount, ); expect(sbpAfter.data.busyCrew.eq(sbpBefore.data.busyCrew)).toBe(true); expect(sbpAfter.wrappedShipEscrows.length).toBe(0); // TODO: add more ship data tests expect(miscStats.crewCount).toBe(miscStats.requiredCrew); expect(miscStats.rentedCrew).toBe(0); const shipAcc = await readFromRPCOrError( provider.connection, program, ship.publicKey(), Ship, 'confirmed', ); expect(miscStats.passengerCapacity).toBe( (shipAcc.data.stats).miscStats.passengerCapacity, ); expect(miscStats.requiredCrew).toBe( (shipAcc.data.stats).miscStats.requiredCrew, ); const combatStats = fleetData.data.stats.combatStats as CombatStats; expect(combatStats.ap).toBe((shipAcc.data.stats).combatStats.ap); expect(combatStats.sp).toBe((shipAcc.data.stats).combatStats.sp); expect(combatStats.hp).toBe((shipAcc.data.stats).combatStats.hp); }); it('add ship to a Fleet', async () => { const sbpBefore = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const previousFleet = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const previousShipStats = previousFleet.data.stats; const shipAmount = new BN(2); // transfer `Ship` from walletSigner to `SagePlayerProfile` escrow account const addShipEscrowInput: AddShipEscrowInput = { shipAmount, index: null, // no index in `StarbasePlayer` remaining data implies this is a new Ship mint to insert }; await buildSendAndCheck( SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority walletSigner, // origin token account owner _shipOriginTokenAccount, ship3.publicKey(), _shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, addShipEscrowInput, ), walletSigner, provider.connection, ); // ship3 was added to escrow which did not affect the crew count const ship3Account = await readFromRPCOrError( provider.connection, program, ship3.publicKey(), Ship, 'confirmed', ); const ship3Stats = ship3Account.data.stats; const sbpAfter0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbpAfter0.totalCrew()).toBe(sbpBefore.totalCrew()); const ship3RequiredCrew = new BN(ship3Stats.miscStats.requiredCrew) .mul(shipAmount) .toNumber(); await buildSendAndCheck( Fleet.addShipToFleet( program, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, ship3.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: shipAmount.toNumber(), shipEscrowIndex: 0, fleetShipInfoIndex: null, keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const fleet = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); expect(fleet.data.shipCounts.total).toBe(3); expect(fleet.data.shipCounts.large).toBe(shipAmount.toNumber()); expect(fleet.data.shipCounts.total).toEqual(fleet.data.shipCounts.updated); const newShipStats = fleet.data.stats; expect(newShipStats.miscStats.crewCount).toBe( previousShipStats.miscStats.crewCount + ship3Stats.miscStats.requiredCrew * shipAmount.toNumber(), ); expect(newShipStats.miscStats.rentedCrew).toBe( previousShipStats.miscStats.rentedCrew, ); expect(newShipStats.miscStats.rentedCrew).toBe(0); expect(newShipStats.miscStats.passengerCapacity).toBe( previousShipStats.miscStats.passengerCapacity + ship3Stats.miscStats.passengerCapacity * shipAmount.toNumber(), ); expect(newShipStats.miscStats.requiredCrew).toBe( previousShipStats.miscStats.requiredCrew + ship3Stats.miscStats.requiredCrew * shipAmount.toNumber(), ); // Test combined stats const expectedShipStats = Fleet.calculateCombinedStats( previousShipStats, ship3Stats, shipAmount.toNumber(), ); // add the crew counts from the fleet as these are NOT coming from ships expectedShipStats.miscStats.crewCount = newShipStats.miscStats.crewCount; expectedShipStats.miscStats.rentedCrew = newShipStats.miscStats.rentedCrew; expect(shipStatsEquals(newShipStats, expectedShipStats)).toBe(true); const combatStats = fleet.data.stats.combatStats as CombatStats; expect(combatStats.ap).toBe(expectedShipStats.combatStats.ap); expect(combatStats.sp).toBe(expectedShipStats.combatStats.sp); expect(combatStats.hp).toBe(expectedShipStats.combatStats.hp); // test fleet ships const fleetShips = await readFromRPCOrError( provider.connection, program, FleetShips.findAddress(program, fleet1)[0], FleetShips, 'confirmed', ); expect(fleetShips.data.fleetShipsInfoCount).toBe(2); const fleetShipInfo = fleetShips.fleetShips[1]; expect(fleetShipInfo?.amount.eq(new BN(2))).toBe(true); expect(fleetShipInfo?.ship.equals(ship3.publicKey())).toBe(true); // test wrapped ship escrow const sbpAfter1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // ship3 was added to the fleet which decreased the crew count expect(sbpAfter1.totalCrew()).toBe( sbpAfter0.totalCrew() - ship3RequiredCrew, ); expect(sbpAfter1.data.busyCrew.eq(sbpBefore.data.busyCrew)).toBe(true); expect(sbpAfter1.wrappedShipEscrows.length).toBe(0); }); it('deposit fuel to fleet', async () => { // create and fund pod token accounts await Promise.all([ createAndFundTokenAccounts( authority, [[_fuel.publicKey, 0]], fleet1FuelTank, walletSigner, walletSigner, provider.connection, ), ]); const fleet = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const fuelCapacity: BN = (fleet.data.stats.cargoStats as any).fuelCapacity; const amount = new BN(fuelCapacity); const otherTokenFrom = await getAssociatedTokenAddress( _food.publicKey, _starbaseCargoPod, true, ); const otherTokenTo = await getAssociatedTokenAddress( _food.publicKey, fleet1FuelTank, true, ); let success0 = true; try { await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1FuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), otherTokenFrom, otherTokenTo, _food.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success0 = false; } expect(success0).toBe(false); // can deposit okay const tokenFrom = await getAssociatedTokenAddress( _fuel.publicKey, _starbaseCargoPod, true, ); const tokenTo = await getAssociatedTokenAddress( _fuel.publicKey, fleet1FuelTank, true, ); await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1FuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, ); await checkTokenBalance(tokenTo, amount, provider.connection); // cant deposit over capacity let success1 = true; try { await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1FuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); await checkTokenBalance(tokenTo, amount, provider.connection); //I think it would be better to get used to always use the `delegated_amount` as a source of true, even if in this case would be the same }); it('deposit ammo to fleet', async () => { // create and fund pod token accounts await Promise.all([ createAndFundTokenAccounts( authority, [ [_ammo.publicKey, 0], [_food.publicKey, 0], ], fleet1AmmoBank, walletSigner, walletSigner, provider.connection, ), ]); const fleet = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const ammoCapacity: BN = (fleet.data.stats.cargoStats as any).ammoCapacity; const amount = new BN(ammoCapacity); /** don't fill it up */ // cant deposit something that isn't ammo const otherTokenFrom = await getAssociatedTokenAddress( _food.publicKey, _starbaseCargoPod, true, ); const otherTokenTo = await getAssociatedTokenAddress( _food.publicKey, fleet1AmmoBank, true, ); let success0 = true; try { await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1AmmoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), otherTokenFrom, otherTokenTo, _food.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success0 = false; } expect(success0).toBe(false); // can deposit okay const tokenFrom = await getAssociatedTokenAddress( _ammo.publicKey, _starbaseCargoPod, true, ); const tokenTo = await getAssociatedTokenAddress( _ammo.publicKey, fleet1AmmoBank, true, ); await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1AmmoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _ammo.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, ); await checkTokenBalance(tokenTo, amount, provider.connection); // cant deposit over capacity let success1 = true; try { await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1AmmoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _ammo.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); await checkTokenBalance(tokenTo, amount, provider.connection); }); it('deposit general cargo to fleet', async () => { // create and fund pod token accounts await Promise.all([ createAndFundTokenAccounts( authority, [[_repairKit.publicKey, 0]], fleet1CargoHold, walletSigner, walletSigner, provider.connection, ), ]); const fleet = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const cargoCapacity: BN = new BN( (fleet.data.stats.cargoStats as any).cargoCapacity, ); const amount = cargoCapacity.sub(new BN(100)); /** don't fill it up */ // can deposit okay const tokenFrom = await getAssociatedTokenAddress( _repairKit.publicKey, _starbaseCargoPod, true, ); const tokenTo = await getAssociatedTokenAddress( _repairKit.publicKey, fleet1CargoHold, true, ); await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _repairKit.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _repairKit.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, ); await checkTokenBalance(tokenTo, amount, provider.connection); // cant deposit over capacity let success1 = true; try { await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _repairKit.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _repairKit.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); await checkTokenBalance(tokenTo, amount, provider.connection); }); it('cannot deposit cargo to an auxiliary token account', async () => { // create auxiliary token account owned by the fleet const auxTokenTo = keypairToAsyncSigner(Keypair.generate()); await buildSendAndCheck( createTokenAccount(auxTokenTo, fleet1CargoHold, _repairKit.publicKey), walletSigner, provider.connection, ); // confirm token account exists const axuTokenAccount = await getAccount( connection, auxTokenTo.publicKey(), 'confirmed', ); expect(axuTokenAccount.mint.equals(_repairKit.publicKey)).toBe(true); expect(axuTokenAccount.owner.equals(fleet1CargoHold)).toBe(true); // confirm that deposit fails const tokenFrom = await getAssociatedTokenAddress( _repairKit.publicKey, _starbaseCargoPod, true, ); let success = true; try { await buildSendAndCheck( Fleet.depositCargoToFleet( program, cargoProgram, _manageFleetCargoKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], walletSigner.publicKey(), _starbaseKey, _starbasePlayerKey, fleet1, _starbaseCargoPod, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _repairKit.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, auxTokenTo.publicKey(), _repairKit.publicKey, _game.publicKey(), _gameState, { amount: new BN(1), keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('AddressMismatch'); success = false; } expect(success).toBe(false); }); it('transfer into fleet cargo hold', async () => { // create pod token account await Promise.all([ createAndFundTokenAccounts( authority, [ [_ammo.publicKey, 0], [_fuel.publicKey, 0], ], fleet1CargoHold, walletSigner, walletSigner, provider.connection, ), ]); const amount = new BN(10); // can transfer from fuel tank okay let tokenFrom = await getAssociatedTokenAddress( _fuel.publicKey, fleet1FuelTank, true, ); let tokenTo = await getAssociatedTokenAddress( _fuel.publicKey, fleet1CargoHold, true, ); await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1FuelTank, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, ); await checkTokenBalance(tokenTo, amount, provider.connection); // can transfer from ammo bank okay tokenFrom = await getAssociatedTokenAddress( _ammo.publicKey, fleet1AmmoBank, true, ); tokenTo = await getAssociatedTokenAddress( _ammo.publicKey, fleet1CargoHold, true, ); await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1AmmoBank, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _ammo.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, ); await checkTokenBalance(tokenTo, amount, provider.connection); // cant transfer over capacity const cargoPodInfo: CargoPod = await readFromRPCOrError( provider.connection, cargoProgram, fleet1FuelTank, CargoPod, 'confirmed', ); tokenFrom = await getAssociatedTokenAddress( _fuel.publicKey, fleet1FuelTank, true, ); tokenTo = await getAssociatedTokenAddress( _fuel.publicKey, fleet1CargoHold, true, ); let success1 = true; try { await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1FuelTank, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount: cargoPodInfo.stats[0] || new BN(100), keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); await checkTokenBalance(tokenTo, amount, provider.connection); }); it('transfer into fleet ammo bank', async () => { const amount = new BN(10); // can transfer okay let tokenFrom = await getAssociatedTokenAddress( _ammo.publicKey, fleet1CargoHold, true, ); let tokenTo = await getAssociatedTokenAddress( _ammo.publicKey, fleet1AmmoBank, true, ); const tokenAccount = await provider.connection.getTokenAccountBalance( tokenTo, ); await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1CargoHold, fleet1AmmoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _ammo.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, ); await checkTokenBalance( tokenTo, new BN(tokenAccount.value.amount).add(amount), provider.connection, ); // test token closure const tokenFromInfo = await connection.getAccountInfo( tokenFrom, 'processed', ); expect(tokenFromInfo).toBeNull; // cant deposit something that is not ammo // first, create pod token account await Promise.all([ createAndFundTokenAccounts( authority, [[_fuel.publicKey, 0]], fleet1AmmoBank, walletSigner, walletSigner, provider.connection, ), ]); tokenFrom = await getAssociatedTokenAddress( _fuel.publicKey, fleet1CargoHold, true, ); tokenTo = await getAssociatedTokenAddress( _fuel.publicKey, fleet1AmmoBank, true, ); let success1 = true; try { await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1CargoHold, fleet1AmmoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); await checkTokenBalance(tokenTo, new BN(0), provider.connection); }); it('transfer into fleet fuel tank', async () => { const amount = new BN(10); // can transfer okay let tokenFrom = await getAssociatedTokenAddress( _fuel.publicKey, fleet1CargoHold, true, ); let tokenTo = await getAssociatedTokenAddress( _fuel.publicKey, fleet1FuelTank, true, ); const tokenAccount = await provider.connection.getTokenAccountBalance( tokenTo, ); await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1CargoHold, fleet1FuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, ); await checkTokenBalance( tokenTo, new BN(tokenAccount.value.amount).add(amount), provider.connection, ); // test token closure const tokenFromInfo = await connection.getAccountInfo( tokenFrom, 'processed', ); expect(tokenFromInfo).toBeNull; // cant deposit something that is not ammo // first, create pod token account await Promise.all([ createAndFundTokenAccounts( authority, [[_ammo.publicKey, 0]], fleet1FuelTank, walletSigner, walletSigner, provider.connection, ), ]); tokenFrom = await getAssociatedTokenAddress( _ammo.publicKey, fleet1CargoHold, true, ); tokenTo = await getAssociatedTokenAddress( _ammo.publicKey, fleet1FuelTank, true, ); let success1 = true; try { await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1CargoHold, fleet1FuelTank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _fuel.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (error) { success1 = false; } expect(success1).toBe(false); await checkTokenBalance(tokenTo, new BN(0), provider.connection); }); it('cannot transfer cargo to an auxiliary token account', async () => { // create auxiliary token account owned by the fleet const auxTokenTo = keypairToAsyncSigner(Keypair.generate()); await buildSendAndCheck( createTokenAccount(auxTokenTo, fleet1CargoHold, _fuel.publicKey), walletSigner, provider.connection, ); // confirm token account exists const axuTokenAccount = await getAccount( connection, auxTokenTo.publicKey(), 'confirmed', ); expect(axuTokenAccount.mint.equals(_fuel.publicKey)).toBe(true); expect(axuTokenAccount.owner.equals(fleet1CargoHold)).toBe(true); // confirm that transfer fails const tokenFrom = await getAssociatedTokenAddress( _fuel.publicKey, fleet1FuelTank, true, ); let success = true; try { await buildSendAndCheck( Fleet.transferCargoWithinFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1FuelTank, fleet1CargoHold, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _fuel.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, auxTokenTo.publicKey(), _fuel.publicKey, _game.publicKey(), _gameState, { amount: new BN(1), keyIndex: MANAGE_CARGO, }, _starbaseKey, ), walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('AddressMismatch'); success = false; } expect(success).toBe(false); }); it('withdraw cargo from fleet', async () => { const amount = new BN(5); const items = [ { pod: fleet1CargoHold, mint: _repairKit }, { pod: fleet1AmmoBank, mint: _ammo }, { pod: fleet1FuelTank, mint: _fuel }, ]; for (let index = 0; index < items.length; index++) { const element = items[index]; if (element) { const tokenFrom = await getAssociatedTokenAddress( element.mint.publicKey, element.pod, true, ); const tokenTo = await getAssociatedTokenAddress( element.mint.publicKey, _starbaseCargoPod, true, ); const podTokenAccount = await provider.connection.getTokenAccountBalance(tokenFrom); const starbaseTokenAccount = await provider.connection.getTokenAccountBalance(tokenTo); await buildSendAndCheck( Fleet.withdrawCargoFromFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _starbasePlayerKey, fleet1, element.pod, _starbaseCargoPod, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), element.mint.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, element.mint.publicKey, _game.publicKey(), _gameState, { amount, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, ); await checkTokenBalance( tokenFrom, new BN(podTokenAccount.value.amount).sub(amount), provider.connection, ); await checkTokenBalance( tokenTo, new BN(starbaseTokenAccount.value.amount).add(amount), provider.connection, ); } } // test token closure const tokenFrom = await getAssociatedTokenAddress( _ammo.publicKey, fleet1AmmoBank, true, ); const tokenTo = await getAssociatedTokenAddress( _ammo.publicKey, _starbaseCargoPod, true, ); const podTokenAccount = await provider.connection.getTokenAccountBalance( tokenFrom, ); await buildSendAndCheck( Fleet.withdrawCargoFromFleet( program, cargoProgram, _manageFleetCargoKey, walletSigner.publicKey() /** fundsTo */, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _starbaseKey, _starbasePlayerKey, fleet1, fleet1AmmoBank, _starbaseCargoPod, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _ammo.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), tokenFrom, tokenTo, _ammo.publicKey, _game.publicKey(), _gameState, { amount: new BN( podTokenAccount.value.amount, ) /** withdraw everything */, keyIndex: MANAGE_CARGO, }, ), walletSigner, provider.connection, ); const tokenFromInfo = await connection.getAccountInfo( tokenFrom, 'processed', ); expect(tokenFromInfo).toBeNull; }); it('close token account owned by fleet cargo pod', async function () { const auxToken = keypairToAsyncSigner(Keypair.generate()); await buildSendAndCheck( [ createTokenAccount(auxToken, fleet1AmmoBank, _repairKit.publicKey), Fleet.closeFleetCargoPodTokenAccount( program, cargoProgram, _manageFleetCargoKey, funder.publicKey(), playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, fleet1AmmoBank, CargoType.findAddress( cargoProgram, _cargoStatsDefinitionSigner.publicKey(), _repairKit.publicKey, 0, )[0], _cargoStatsDefinitionSigner.publicKey(), auxToken.publicKey(), _repairKit.publicKey, _game.publicKey(), _gameState, { keyIndex: MANAGE_CARGO, }, ), ], walletSigner, provider.connection, ); const tokenInfo = await connection.getAccountInfo( auxToken.publicKey(), 'processed', ); expect(tokenInfo).toBeNull(); }); it('undock then dock fleet', async () => { const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const fleet0 = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); await buildSendAndCheck( Fleet.loadingBayToIdle( program, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, _starbaseKey, _starbasePlayerKey, _game.publicKey(), _gameState, SAGE_MANAGER, ), walletSigner, provider.connection, ); // crew count at Starbase did not change const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp1.totalCrew()).toBe(sbp0.totalCrew()); expect(sbp1.data.busyCrew.eq(sbp0.data.busyCrew)).toBe(true); await buildSendAndCheck( Fleet.idleToLoadingBay( program, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet1, _starbaseKey, _starbasePlayerKey, _game.publicKey(), _gameState, SAGE_MANAGER, ), walletSigner, provider.connection, ); // crew count at Starbase increased const sbp2 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp2.totalCrew()).toBe(sbp0.totalCrew()); expect(sbp2.data.busyCrew.eq(sbp0.data.busyCrew)).toBe(true); }); it('disband a Fleet', async () => { const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const fleetShipsAddress = FleetShips.findAddress(program, fleet1)[0]; const fleet = await readFromRPCOrError( provider.connection, program, fleet1, Fleet, 'confirmed', ); const fleetMiscStats = fleet.data.stats.miscStats as MiscStats; const disbandFleet = Fleet.disbandFleet( program, cargoProgram, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], fleet, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { keyIndex: FLEET_MANAGER, }, ); await buildSendAndCheck( disbandFleet.instructions, walletSigner, provider.connection, ); const disbandedFleet = await readFromRPCOrError( provider.connection, program, disbandFleet.disbandedFleetKey[0], DisbandedFleet, 'confirmed', ); disbandedFleetDataEquals(disbandedFleet.data, { version: 0, starbase: _starbaseKey, bump: disbandFleet.disbandedFleetKey[1], gameId: fleet.data.gameId, ownerProfile: fleet.data.ownerProfile, fleetShips: fleetShipsAddress, fleetLabel: fleet.data.fleetLabel, }); const fleetAfter = await program.account.fleet.fetchNullable(fleet1); expect(fleetAfter).toBeNull; // cargo pods were transferred to SBP const fleetCargoPods = [ fleet.data.ammoBank, fleet.data.cargoHold, fleet.data.fuelTank, ]; const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // crew increases expect(sbp1.totalCrew()).toBe(sbp0.totalCrew() + fleetMiscStats.crewCount); expect(sbp1.data.busyCrew.eq(sbp0.data.busyCrew)).toBe(true); for (let index = 0; index < fleetCargoPods.length; index++) { const podKey = fleetCargoPods[index]; if (!podKey) { throw new Error('Cargo pod should exist!'); } const cargoPod: CargoPod = await readFromRPCOrError( provider.connection, cargoProgram, podKey, CargoPod, 'confirmed', ); expect(cargoPod.data.authority.equals(_starbasePlayerKey)).toBe(true); } await buildSendAndCheck( DisbandedFleet.disbandedFleetToEscrow( program, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], disbandFleet.disbandedFleetKey[0], fleetShipsAddress, ship.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { fleetShipInfoIndex: 0, shipAmount: 1, shipEscrowIndex: null, keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const fleetShips = await readFromRPCOrError( provider.connection, program, fleetShipsAddress, FleetShips, 'confirmed', ); expect(fleetShips.data.fleetShipsInfoCount).toBe(1); const fleetShipInfo = fleetShips.fleetShips[0]; expect(fleetShipInfo?.amount.eq(new BN(2))).toBe(true); expect(fleetShipInfo?.ship.equals(ship3.publicKey())).toBe(true); const sbp2 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp1.totalCrew()).toBe(sbp2.totalCrew()); expect(sbp2.wrappedShipEscrows.length).toBe(1); let list = sbp2.wrappedShipEscrows; let ship1Escrow = list[0]; if (!ship1Escrow) { throw new Error('escrow not found'); } expect(ship1Escrow?.amount.eq(new BN(1))).toBe(true); await buildSendAndCheck( DisbandedFleet.disbandedFleetToEscrow( program, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], disbandFleet.disbandedFleetKey[0], fleetShipsAddress, ship3.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { fleetShipInfoIndex: 0, shipAmount: 2, shipEscrowIndex: null, keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const fleetShips2 = await readFromRPCOrError( provider.connection, program, fleetShipsAddress, FleetShips, 'confirmed', ); expect(fleetShips2.data.fleetShipsInfoCount).toBe(0); const sbp3 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp2.totalCrew()).toBe(sbp3.totalCrew()); expect(sbp3.data.busyCrew.eq(new BN(0))).toBe(true); expect(sbp3.wrappedShipEscrows.length).toBe(2); list = sbp3.wrappedShipEscrows; ship1Escrow = list[0]; const ship2Escrow = list[1]; if (!ship1Escrow || !ship2Escrow) { throw new Error('escrow not found'); } expect(ship1Escrow?.amount.eq(new BN(1))).toBe(true); expect(ship1Escrow?.ship.equals(ship.publicKey())).toBe(true); expect(ship2Escrow?.amount.eq(new BN(2))).toBe(true); expect(ship2Escrow?.ship.equals(ship3.publicKey())).toBe(true); await buildSendAndCheck( DisbandedFleet.closeDisbandedFleet( program, _fleetManagerKey, playerProfile.publicKey(), walletSigner.publicKey(), disbandFleet.disbandedFleetKey[0], fleetShipsAddress, { keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const account = await program.account.disbandedFleet.fetchNullable( disbandFleet.disbandedFleetKey[0], ); expect(account).toBeNull; const fleetShips3 = await program.account.fleetShips.fetchNullable( fleetShipsAddress, ); expect(fleetShips3).toBeNull; const sbp4 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp4.totalCrew()).toBe(sbp3.totalCrew()); expect(sbp4.data.busyCrew.eq(sbp3.data.busyCrew)).toBe(true); }); it('add Ship to escrow, remove Ship from escrow', async () => { const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // transfer `Ship` from walletSigner to `SagePlayerProfile` escrow account const shipAmount = new BN(7); const addShipEscrowInput: AddShipEscrowInput = { shipAmount, index: null, // no index in `StarbasePlayer` remaining data implies this is a new Ship mint to insert }; const addShipIx = SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, walletSigner, _shipOriginTokenAccount, ship2.publicKey(), _shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, addShipEscrowInput, ); await buildSendAndCheck(addShipIx, walletSigner, provider.connection); // check `Ship` added to `StarbasePlayer` remaining data const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp1.data.updatedShipEscrowCount).toEqual(sbp1.data.shipEscrowCount); const list = sbp1.wrappedShipEscrows; const escrow = list[2]; if (!escrow) { throw new Error('escrow not found'); } expect(escrow.amount.eq(addShipEscrowInput.shipAmount)).toBe(true); expect(escrow.ship.equals(ship2.publicKey())).toBe(true); const ship2Account = await readFromRPCOrError( provider.connection, program, ship2.publicKey(), Ship, 'confirmed', ); const ship2Stats = ship2Account.data.stats; // crew unaffected expect(sbp1.totalCrew()).toBe(sbp0.totalCrew()); const shipEscrowTokenAccountBefore = await getAccount( provider.connection, _shipEscrowTokenAccount, 'confirmed', ); const shipOriginTokenAccountBefore = await getAccount( provider.connection, _shipOriginTokenAccount, 'confirmed', ); if ( shipOriginTokenAccountBefore == null || shipEscrowTokenAccountBefore == null ) { throw new Error('escrow account not found'); } // remove Ship from escrow const removeShipEscrowInput: RemoveShipEscrowInput = { // amount to remove from escrow shipAmount, // index of `ProfileKey` in `Profile` permissionKeyIndex: REMOVE_ESCROW, // index of `WrappedShipEscrow` in `StarbasePlayer` remaining data shipEscrowIndex: 2, }; const removeShipIx = SagePlayerProfile.removeShipEscrow( program, _removeShipEscrowKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, _shipOriginTokenAccount, ship2.publicKey(), _shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, removeShipEscrowInput, ); await buildSendAndCheck(removeShipIx, walletSigner, provider.connection); // check `Ship` removed from `WrappedShipEscrow` amount const sbp2 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const listAfter = sbp2.wrappedShipEscrows; expect(listAfter.length).toBe(2); // check `Ship` added to destinationTokenAccount const shipEscrowTokenAccountAfter = await getAccount( provider.connection, _shipEscrowTokenAccount, 'confirmed', ); const shipOriginTokenAccountAfter = await getAccount( provider.connection, _shipOriginTokenAccount, 'confirmed', ); if ( shipOriginTokenAccountAfter == null || shipEscrowTokenAccountAfter == null ) { throw new Error('escrow account not found'); } expect( new BN(shipOriginTokenAccountAfter.amount.toString()) .sub(new BN(shipOriginTokenAccountBefore.amount.toString())) .eq(shipAmount), ).toBe(true); expect(shipOriginTokenAccountAfter.mint.equals(shipMint.publicKey())).toBe( true, ); // check `Ship` removed from escrow token account expect( new BN(shipEscrowTokenAccountBefore.amount.toString()) .sub(new BN(shipEscrowTokenAccountAfter.amount.toString())) .eq(shipAmount), ).toBe(true); expect(shipOriginTokenAccountAfter.mint.equals(shipMint.publicKey())).toBe( true, ); // crew still unaffected expect(sbp2.totalCrew()).toBe(sbp0.totalCrew()); expect(sbp2.data.updatedShipEscrowCount).toEqual(sbp2.data.shipEscrowCount); }); it('update a fleet', async () => { const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const newShip1 = keypairToAsyncSigner(Keypair.generate()); const newShip2 = keypairToAsyncSigner(Keypair.generate()); const newShip3 = keypairToAsyncSigner(Keypair.generate()); const newShip4 = keypairToAsyncSigner(Keypair.generate()); const newShips = [ newShip1.publicKey().toBase58(), newShip2.publicKey().toBase58(), newShip3.publicKey().toBase58(), newShip4.publicKey().toBase58(), ]; const mint = keypairToAsyncSigner(Keypair.generate()); // create mints await buildSendAndCheck( ( await Promise.all( [mint].map((it) => createMint(it, 0, authority.publicKey(), null)), ) ).flat(), walletSigner, provider.connection, ); // create ships await _createShip(newShip1, mint.publicKey(), SizeClass.XxSmall, 1); await _createShip(newShip2, mint.publicKey(), SizeClass.XxSmall, 1); const escrowResult1 = await _createShipTokens( mint, undefined, undefined, 3, ); // add ships to game await buildSendAndCheck( ( await Promise.all( [newShip1, newShip2].map((it) => SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority walletSigner, // origin token account owner escrowResult1.shipOriginTokenAccount, it.publicKey(), escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: new BN(1), index: null, }, ), ), ) ).flat(), walletSigner, provider.connection, ); const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // does not affect crew expect(sbp1.totalCrew()).toBe(sbp0.totalCrew()); // create fleet const shipEscrowIndex = sbp0.wrappedShipEscrows.length; const result = await _createFleet('A', newShip1, 1, shipEscrowIndex); await buildSendAndCheck( Fleet.addShipToFleet( program, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], result.fleetKey[0], newShip2.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: 1, shipEscrowIndex, fleetShipInfoIndex: null, keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const sbp2 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp2.totalCrew()).toBe(sbp1.totalCrew() - 2); // update ship in fleet await _createShip(newShip3, mint.publicKey(), SizeClass.XxSmall, 5, false); const updateFleetShipResult = await updateFleetShip( newShip1.publicKey(), newShip3.publicKey(), result.fleetKey[0], ); const fleetAccount = updateFleetShipResult[0]; const fleetShipsBefore = await readFromRPCOrError( provider.connection, program, FleetShips.findAddress(program, result.fleetKey[0])[0], FleetShips, 'confirmed', ); // sync the ships in escrow await syncShipEscrow(_starbasePlayerKey); const sbp3 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp3.totalCrew()).toBe(sbp2.totalCrew()); const fleetAcc0 = await readFromRPCOrError( provider.connection, program, result.fleetKey[0], Fleet, 'confirmed', ); expect(fleetAcc0.data.shipCounts.total).toEqual( fleetAcc0.data.shipCounts.updated, ); // update the fleet's program first ship await buildSendAndCheck( Fleet.updateShipInFleet( program, result.fleetKey[0], newShip1.publicKey(), newShip3.publicKey(), _game.publicKey(), _gameState, { shipAmount: 1, fleetShipInfoIndex: 0, }, ), walletSigner, provider.connection, ); const fleetAcc1 = await readFromRPCOrError( provider.connection, program, result.fleetKey[0], Fleet, 'confirmed', ); expect(fleetAcc1.data.shipCounts.updated).toEqual(1); expect((fleetAcc0.data.stats).miscStats.crewCount).toBe( (fleetAcc1.data.stats).miscStats.crewCount, ); expect((fleetAcc0.data.stats).miscStats.rentedCrew).toBe( (fleetAcc1.data.stats).miscStats.rentedCrew, ); const newShip3Acc = await readFromRPCOrError( provider.connection, program, newShip3.publicKey(), Ship, 'confirmed', ); expect((fleetAcc1.data.stats).miscStats.requiredCrew).toBe( (newShip3Acc.data.stats).miscStats.requiredCrew, ); expect((fleetAcc1.data.stats).miscStats.passengerCapacity).toBe( (newShip3Acc.data.stats).miscStats.passengerCapacity, ); // update the fleet's second/last ship const update2Ix = Fleet.updateShipInFleet( program, result.fleetKey[0], newShip2.publicKey(), PublicKey.default, _game.publicKey(), _gameState, { shipAmount: 1, fleetShipInfoIndex: 1, }, ); await buildSendAndCheck(update2Ix, walletSigner, provider.connection); const fleetAccountAfter = await readFromRPCOrError( provider.connection, program, result.fleetKey[0], Fleet, 'confirmed', ); expect(fleetAccountAfter.data.shipCounts.total).toEqual( fleetAccountAfter.data.shipCounts.updated, ); expect((fleetAcc0.data.stats).miscStats.crewCount).toBe( (fleetAccountAfter.data.stats).miscStats.crewCount, ); expect((fleetAcc0.data.stats).miscStats.rentedCrew).toBe( (fleetAccountAfter.data.stats).miscStats.rentedCrew, ); const newShip2Acc = await readFromRPCOrError( provider.connection, program, newShip2.publicKey(), Ship, 'confirmed', ); expect( (fleetAccountAfter.data.stats).miscStats.requiredCrew, ).toBe( (fleetAcc1.data.stats).miscStats.requiredCrew + (newShip2Acc.data.stats).miscStats.requiredCrew, ); expect( (fleetAccountAfter.data.stats).miscStats.passengerCapacity, ).toBe( (fleetAcc1.data.stats).miscStats.passengerCapacity + (newShip2Acc.data.stats).miscStats.passengerCapacity, ); const gameAccountAfter = await readFromRPCOrError( provider.connection, program, _game.publicKey(), Game, 'confirmed', ); const fleetShipsAfter = await readFromRPCOrError( provider.connection, program, FleetShips.findAddress(program, result.fleetKey[0])[0], FleetShips, 'confirmed', ); const sbp4 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // no change in Starbase crew due to `updateShipInFleet` expect(sbp4.totalCrew()).toBe(sbp3.totalCrew()); expect(gameAccountAfter.data.updateId.toNumber()).toBe( fleetAccountAfter.data.updateId.toNumber(), ); expect(fleetAccountAfter.data.shipCounts.total).toEqual( fleetAccountAfter.data.shipCounts.updated, ); expect(gameAccountAfter.data.updateId.toNumber()).toBe( fleetShipsAfter.fleetShips[0]?.updateId.toNumber(), ); expect(gameAccountAfter.data.updateId.toNumber()).toBe( fleetShipsAfter.fleetShips[1]?.updateId.toNumber(), ); expect(fleetShipsAfter.data.fleetShipsInfoCount).toBe( fleetShipsBefore.data.fleetShipsInfoCount, ); expect( fleetShipsBefore.fleetShips[0]?.ship.equals(newShip1.publicKey()), ).toBe(true); expect( fleetShipsAfter.fleetShips[0]?.ship.equals(newShip3.publicKey()), ).toBe(true); expect( fleetShipsBefore.fleetShips[1]?.ship.equals(newShip2.publicKey()), ).toBe(true); expect( fleetShipsAfter.fleetShips[1]?.ship.equals(newShip2.publicKey()), ).toBe(true); // can't update again let success = true; try { await buildSendAndCheck(update2Ix, walletSigner, provider.connection, { suppressLogging: true, }); } catch (err) { success = false; } expect(success).toBe(false); // test counts expect( arrayDeepEquals( Object.values(fleetAccount.data.shipCounts), Object.values(fleetAccountAfter.data.shipCounts), (a, b) => a === b, ), ).toBe(true); // test that crew numbers are adjusted when ship is updated await _createShip(newShip4, mint.publicKey(), SizeClass.XxSmall, 1, false); await updateFleetShip( newShip3.publicKey(), newShip4.publicKey(), result.fleetKey[0], ); await syncShipEscrow(_starbasePlayerKey); await buildSendAndCheck( [ Fleet.updateShipInFleet( program, result.fleetKey[0], newShip3.publicKey(), newShip4.publicKey(), _game.publicKey(), _gameState, { shipAmount: 1, fleetShipInfoIndex: 0, }, ), Fleet.updateShipInFleet( program, result.fleetKey[0], newShip2.publicKey(), PublicKey.default, _game.publicKey(), _gameState, { shipAmount: 1, fleetShipInfoIndex: 1, }, ), ], walletSigner, provider.connection, ); const sbp5 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp5.totalCrew()).toBe(sbp4.totalCrew()); // un-dock so that we can test forced disbanding await buildSendAndCheck( Fleet.loadingBayToIdle( program, _sageSigner, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], result.fleetKey[0], _starbaseKey, _starbasePlayerKey, _game.publicKey(), _gameState, SAGE_MANAGER, ), walletSigner, provider.connection, ); const sbp6 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // undocking has no effect on Starbase crew expect(sbp6.totalCrew()).toBe(sbp4.totalCrew()); // invalidate ship in fleet await buildSendAndCheck( Ship.invalidateShip( program, _shipManagerKey, playerProfile.publicKey(), newShip4.publicKey(), _game.publicKey(), MANAGE_SHIP, ), walletSigner, provider.connection, ); // update the _gameState await syncGameAccount(); // sync ship escrow await syncShipEscrow(_starbasePlayerKey); // updating the fleet's ship won't work let success9 = true; try { await buildSendAndCheck( Fleet.updateShipInFleet( program, result.fleetKey[0], newShip3.publicKey(), PublicKey.default, _game.publicKey(), _gameState, { shipAmount: 1, fleetShipInfoIndex: 0, }, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('InvalidNextShipAddress'); success9 = false; } expect(success9).toBe(false); // force disband fleet const fleetShipsAddress = FleetShips.findAddress( program, result.fleetKey[0], )[0]; const fleet = await readFromRPCOrError( provider.connection, program, result.fleetKey[0], Fleet, 'confirmed', ); const newShip4Acc = await readFromRPCOrError( provider.connection, program, newShip4.publicKey(), Ship, 'confirmed', ); expect((fleetAcc0.data.stats).miscStats.crewCount).toBe( (fleet.data.stats).miscStats.crewCount, ); expect((fleetAcc0.data.stats).miscStats.rentedCrew).toBe( (fleet.data.stats).miscStats.rentedCrew, ); expect((fleet.data.stats).miscStats.requiredCrew).toBe( (newShip4Acc.data.stats).miscStats.requiredCrew + (newShip2Acc.data.stats).miscStats.requiredCrew, ); expect((fleet.data.stats).miscStats.passengerCapacity).toBe( (newShip4Acc.data.stats).miscStats.passengerCapacity + (newShip2Acc.data.stats).miscStats.passengerCapacity, ); const disbandFleet = Fleet.forceDisbandFleet( program, cargoProgram, fleet, _starbasePlayerKey, _starbaseKey, newShip4.publicKey(), _game.publicKey(), _gameState, { fleetShipInfoIndex: 0, }, ); await buildSendAndCheck( disbandFleet.instructions, walletSigner, provider.connection, ); const sbp6b = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // crew was added back to Starbase expect(sbp6b.totalCrew()).toBe( sbp6.totalCrew() + (fleet.data.stats).miscStats.crewCount, ); const disbandedFleet = await readFromRPCOrError( provider.connection, program, disbandFleet.disbandedFleetKey[0], DisbandedFleet, 'confirmed', ); disbandedFleetDataEquals(disbandedFleet.data, { version: 0, starbase: _starbaseKey, bump: disbandFleet.disbandedFleetKey[1], gameId: fleet.data.gameId, ownerProfile: fleet.data.ownerProfile, fleetShips: fleetShipsAddress, fleetLabel: fleet.data.fleetLabel, }); const fleetAfter = await program.account.fleet.fetchNullable(fleet1); expect(fleetAfter).toBeNull; // process the valid ship await buildSendAndCheck( DisbandedFleet.disbandedFleetToEscrow( program, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], disbandFleet.disbandedFleetKey[0], fleetShipsAddress, newShip2.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { fleetShipInfoIndex: 1, shipAmount: 1, shipEscrowIndex: null, keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const sbp7 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // crew not affected expect(sbp7.totalCrew()).toBe(sbp6b.totalCrew()); // process the invalid ship await buildSendAndCheck( DisbandedFleet.disbandedFleetToEscrow( program, _fleetManagerKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], disbandFleet.disbandedFleetKey[0], fleetShipsAddress, newShip4.publicKey(), _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { fleetShipInfoIndex: 0, shipAmount: 1, shipEscrowIndex: null, keyIndex: FLEET_MANAGER, }, ), walletSigner, provider.connection, ); const sbp8 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp8.totalCrew()).toBe(sbp7.totalCrew()); // remove invalid ship from escrow await buildSendAndCheck( SagePlayerProfile.removeShipEscrow( program, _removeShipEscrowKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, escrowResult1.shipOriginTokenAccount, newShip4.publicKey(), escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: new BN(1), permissionKeyIndex: REMOVE_ESCROW, shipEscrowIndex: 3, }, ), walletSigner, provider.connection, ); const sbp9 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp9.wrappedShipEscrows.length).toBe( sbp8.wrappedShipEscrows.length - 1, ); // crew didn't change expect(sbp9.totalCrew()).toBe(sbp8.totalCrew()); // clean up ships for (let index = sbp9.wrappedShipEscrows.length - 1; index >= 0; index--) { const element = sbp9.wrappedShipEscrows[index]; if (newShips.includes(element.ship.toBase58())) { await buildSendAndCheck( SagePlayerProfile.removeShipEscrow( program, _removeShipEscrowKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, escrowResult1.shipOriginTokenAccount, element.ship, escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: element.amount, permissionKeyIndex: REMOVE_ESCROW, shipEscrowIndex: index, }, ), walletSigner, provider.connection, ); } } }); it('update ship in escrow', async () => { // clear out ship escrow const sbpBefore = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // loop backwards so that shipEscrowIndex is not affected for ( let index = sbpBefore.wrappedShipEscrows.length - 1; index >= 0; index-- ) { const element = sbpBefore.wrappedShipEscrows[index]; await buildSendAndCheck( SagePlayerProfile.removeShipEscrow( program, _removeShipEscrowKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, _shipOriginTokenAccount, element.ship, _shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: element.amount, permissionKeyIndex: REMOVE_ESCROW, shipEscrowIndex: index, }, ), walletSigner, provider.connection, ); } // create some new ships const newShip1 = keypairToAsyncSigner(Keypair.generate()); const newShip2 = keypairToAsyncSigner(Keypair.generate()); const newShip3 = keypairToAsyncSigner(Keypair.generate()); const newShip4 = keypairToAsyncSigner(Keypair.generate()); const newShips = [ newShip1.publicKey().toBase58(), newShip2.publicKey().toBase58(), newShip3.publicKey().toBase58(), newShip4.publicKey().toBase58(), ]; const mint = keypairToAsyncSigner(Keypair.generate()); // create mint await buildSendAndCheck( createMint(mint, 0, authority.publicKey(), null), walletSigner, provider.connection, ); // create ships await _createShip(newShip1, mint.publicKey(), SizeClass.XxSmall, 1); await _createShip(newShip2, mint.publicKey(), SizeClass.XxSmall, 2); await _createShip(newShip3, mint.publicKey(), SizeClass.XxSmall, 3); await _createShip(newShip4, mint.publicKey(), SizeClass.XxSmall, 4, false); const escrowResult1 = await _createShipTokens( mint, undefined, undefined, 3, ); // add ships to game await buildSendAndCheck( ( await Promise.all( [newShip1, newShip2, newShip3].map((it) => SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority walletSigner, // origin token account owner escrowResult1.shipOriginTokenAccount, it.publicKey(), escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: new BN(1), index: null, }, ), ), ) ).flat(), walletSigner, provider.connection, ); const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp0.data.updatedShipEscrowCount).toEqual(sbp0.data.shipEscrowCount); // replace one ship await buildSendAndCheck( Ship.setNextShip( program, _shipManagerKey, playerProfile.publicKey(), newShip1.publicKey(), newShip4.publicKey(), _game.publicKey(), MANAGE_SHIP, ), walletSigner, provider.connection, ); await syncGameAccount(); await syncShipEscrow(_starbasePlayerKey); const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp1.wrappedShipEscrows.length).toEqual( sbp0.wrappedShipEscrows.length, ); expect(sbp1.totalCrew()).toBe(sbp0.totalCrew()); expect(sbp1.data.updatedShipEscrowCount).toEqual(sbp1.data.shipEscrowCount); // invalidate ship await buildSendAndCheck( Ship.invalidateShip( program, _shipManagerKey, playerProfile.publicKey(), newShip2.publicKey(), _game.publicKey(), MANAGE_SHIP, ), walletSigner, provider.connection, ); await syncGameAccount(); await syncShipEscrow(_starbasePlayerKey); const sbp2 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp2.wrappedShipEscrows.length).toEqual( sbp1.wrappedShipEscrows.length, ); expect(sbp2.totalCrew()).toBe(sbp1.totalCrew()); expect(sbp2.data.updatedShipEscrowCount).toEqual(sbp2.data.shipEscrowCount); // clean up ships for (let index = sbp2.wrappedShipEscrows.length - 1; index >= 0; index--) { const element = sbp2.wrappedShipEscrows[index]; if (newShips.includes(element.ship.toBase58())) { await buildSendAndCheck( SagePlayerProfile.removeShipEscrow( program, _removeShipEscrowKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, escrowResult1.shipOriginTokenAccount, element.ship, escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: element.amount, permissionKeyIndex: REMOVE_ESCROW, shipEscrowIndex: index, }, ), walletSigner, provider.connection, ); } } const sbp3 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp3.data.updatedShipEscrowCount).toEqual(sbp3.data.shipEscrowCount); expect(sbp3.wrappedShipEscrows.length).toEqual(0); expect(sbp3.totalCrew()).toBe(sbp2.totalCrew()); }); it('normalize fleet crew', async () => { // clear out ship escrow const sbp0 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); // create some new ships const newShip1 = keypairToAsyncSigner(Keypair.generate()); const newShip2 = keypairToAsyncSigner(Keypair.generate()); const newShips = [ newShip1.publicKey().toBase58(), newShip2.publicKey().toBase58(), ]; const mint = keypairToAsyncSigner(Keypair.generate()); // create mint await buildSendAndCheck( createMint(mint, 0, authority.publicKey(), null), walletSigner, provider.connection, ); // create ships await _createShip(newShip1, mint.publicKey(), SizeClass.XxSmall, 1); await _createShip(newShip2, mint.publicKey(), SizeClass.XxSmall, 2, false); const escrowResult1 = await _createShipTokens( mint, undefined, undefined, 3, ); // add ships to game await buildSendAndCheck( ( await Promise.all( [newShip1].map((it) => SagePlayerProfile.addShipEscrow( program, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, // escrow authority walletSigner, // origin token account owner escrowResult1.shipOriginTokenAccount, it.publicKey(), escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: new BN(1), index: null, }, ), ), ) ).flat(), walletSigner, provider.connection, ); // create fleet const result = await _createFleet('Oo', newShip1, 1); // fleet should be normalized const fleet0 = await readFromRPCOrError( provider.connection, program, result.fleetKey[0], Fleet, ); expect(fleet0.normalizedCrew()).toBe(true); const sbp1 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); expect(sbp0.totalCrew()).toBe( sbp1.totalCrew() + (fleet0.data.stats).miscStats.crewCount, ); // replace fleet's ship with one that has a higher required crew await buildSendAndCheck( Ship.setNextShip( program, _shipManagerKey, playerProfile.publicKey(), newShip1.publicKey(), newShip2.publicKey(), _game.publicKey(), MANAGE_SHIP, ), walletSigner, provider.connection, ); await syncGameAccount(); await syncShipEscrow(_starbasePlayerKey); // update ship in fleet await buildSendAndCheck( Fleet.updateShipInFleet( program, result.fleetKey[0], newShip1.publicKey(), newShip2.publicKey(), _game.publicKey(), _gameState, { shipAmount: 1, fleetShipInfoIndex: 0, }, ), walletSigner, provider.connection, ); const fleet1 = await readFromRPCOrError( provider.connection, program, result.fleetKey[0], Fleet, ); expect(fleet1.normalizedCrew()).toBe(false); // crew on starbase should be deducted const sbp2 = await readFromRPCOrError( provider.connection, program, _starbasePlayerKey, StarbasePlayer, 'confirmed', ); const crewDiff = (fleet1.data.stats).miscStats.crewCount - (fleet0.data.stats).miscStats.crewCount; expect(sbp1.totalCrew()).toBe(sbp2.totalCrew() + crewDiff); // clean up ships for (let index = sbp2.wrappedShipEscrows.length - 1; index >= 0; index--) { const element = sbp2.wrappedShipEscrows[index]; if (newShips.includes(element.ship.toBase58())) { await buildSendAndCheck( SagePlayerProfile.removeShipEscrow( program, _removeShipEscrowKey, playerProfile.publicKey(), ProfileFactionAccount.findAddress( profileFactionProgram, playerProfile.publicKey(), )[0], _sagePlayerProfileKey, escrowResult1.shipOriginTokenAccount, element.ship, escrowResult1.shipEscrowTokenAccount, _starbasePlayerKey, _starbaseKey, _game.publicKey(), _gameState, { shipAmount: element.amount, permissionKeyIndex: REMOVE_ESCROW, shipEscrowIndex: index, }, ), walletSigner, provider.connection, ); } } }); });