import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID, } from '@solana/spl-token'; import { KeyedAccountInfo, PublicKey, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, SYSVAR_SLOT_HASHES_PUBKEY, } from '@solana/web3.js'; import { BN } from '@staratlas/anchor'; import { CargoIDLProgram } from '@staratlas/cargo'; import { Account, AccountStatic, arrayDeepEquals, AsyncSigner, decodeAccount, DecodedAccountData, InstructionReturn, staticImplements, transferToTokenAccount, } from '@staratlas/data-source'; import { PointsIDLProgram } from '@staratlas/points'; import { DeregisterSurveyDataUnitTrackerInput, DrainSurveyDataUnitsBankInput, RegisterSurveyDataUnitTrackerInput, SageIDL, SageIDLProgram, ScanForSurveyDataUnitsInput, SurveyDataUnitTrackerAccount, UpdateSurveyDataUnitTrackerInput, } from './constants'; import { ProgressionConfig } from './progressionConfig'; export interface RegisterSurveyDataUnitTrackerParams extends RegisterSurveyDataUnitTrackerInput { cssCoordinates: BN[][]; } export interface UpdateSurveyDataUnitTrackerParams extends Partial { keyIndex: number; } /** * Check if two `cssCoordinates` instances are equal * @param first - first cssCoordinates * @param second - second cssCoordinates * @returns a boolean */ const cssCoordinatesDataEquals = (first: Array, second: Array) => { if (first.length !== second.length) { return false; } return first.reduce((prev, current, index) => { const other = second[index]; if (other == null) { return false; } return prev && arrayDeepEquals(current, other, (a, b) => a.eq(b)); }, true); }; /** * Check if two `SurveyDataUnitTrackerAccount` instances are equal * @param data1 - first SurveyDataUnitTrackerAccount * @param data2 - second SurveyDataUnitTrackerAccount * @returns a boolean */ export function surveyDataUnitTrackerDataEquals( data1: SurveyDataUnitTrackerAccount, data2: SurveyDataUnitTrackerAccount, ): boolean { return ( data1.version === data2.version && data1.gameId.equals(data2.gameId) && data1.sduMint.equals(data2.sduMint) && data1.resourceMint.equals(data2.resourceMint) && data1.signer.equals(data2.signer) && arrayDeepEquals(data1.coordinatesRange, data2.coordinatesRange, (a, b) => a.eq(b), ) && data1.sduMaxPerSector === data2.sduMaxPerSector && data1.scanChanceRegenPeriod === data2.scanChanceRegenPeriod && cssCoordinatesDataEquals( data1.cssCoordinates as BN[][], data2.cssCoordinates as BN[][], ) && arrayDeepEquals(data1.originCoordinates, data2.originCoordinates, (a, b) => a.eq(b), ) && data1.cssMaxDistance === data2.cssMaxDistance && data1.originMaxDistance === data2.originMaxDistance && data1.distanceWeighting === data2.distanceWeighting && data1.tMax.eq(data2.tMax) && data1.xMul === data2.xMul && data1.yMul === data2.yMul && data1.zMul === data2.zMul ); } @staticImplements>() export class SurveyDataUnitTracker implements Account { static readonly ACCOUNT_NAME: NonNullable< SageIDL['accounts'] >[number]['name'] = 'surveyDataUnitTracker'; static readonly MIN_DATA_SIZE = 8 + // discriminator 1 + // version 32 + // gameId 32 + // sduMint 32 + // resourceMint 32 + // signer 1 + // signer bump 8 * 2 + // coordinatesRange 8 * 2 * 3 + // cssCoordinates 8 * 2 + // originCoordinates 4 + // cssMaxDistance 4 + // originMaxDistance 4 + // originMaxDistance 8 + // tMax 4 + // xMul 4 + // yMul 4 + // zMul 4 + // sduMaxPerSector 2; // scanChanceRegenPeriod constructor( private _data: SurveyDataUnitTrackerAccount, private _key: PublicKey, ) {} get data(): Readonly { return this._data; } get key(): PublicKey { return this._key; } /** * Finds the address of a `SurveyDataUnitTracker` signer * @param program - the SAGE program * @param tracker - the survey data units tracker * @returns - The PDA and bump respectively */ static findSignerAddress( program: SageIDLProgram, tracker: PublicKey, ): [PublicKey, number] { return PublicKey.findProgramAddressSync( [Buffer.from('SurveyDataUnitTracker'), tracker.toBuffer()], program.programId, ); } /** * Finds the address of the SDU token account owned by the `SurveyDataUnitTracker` signer * @param trackerAccount - the survey data units tracker account * @returns - The associated token account address */ static findSignerTokenAccountAddress( trackerAccount: SurveyDataUnitTrackerAccount, ): PublicKey { return getAssociatedTokenAddressSync( trackerAccount.sduMint, trackerAccount.signer, true, ); } /** * Fund a `SurveyDataUnitTracker` `SDU` bank * The bank token account must already exist * @param trackerAccount - the survey data units tracker account * @param tokenFromOwner - the owner of the source token account * @param tokenFrom - the source token account * @param amount - the amount of tokens * @returns InstructionReturn */ static fundSurveyDataUnitTrackerBank( trackerAccount: SurveyDataUnitTrackerAccount, tokenFromOwner: AsyncSigner, tokenFrom: PublicKey, amount: number, ): InstructionReturn { const tokenTo = this.findSignerTokenAccountAddress(trackerAccount); return transferToTokenAccount(tokenFromOwner, tokenFrom, tokenTo, amount); } /** * Register a Survey Data Unit Tracker * Only use this if the empty surveyDataUnitTracker account has already been created on-chain * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param surveyDataUnitTracker - the new survey data unit tracker * @param sduMint - the mint for the survey data units * @param resourceMint - the mint for the resource that is consumed when scanning for survey data units * @param gameId - the SAGE game id * @param input - instruction input params * @returns InstructionReturn */ static registerSurveyDataUnitTracker( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, surveyDataUnitTracker: AsyncSigner, sduMint: PublicKey, resourceMint: PublicKey, gameId: PublicKey, input: RegisterSurveyDataUnitTrackerParams, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .registerSurveyDataUnitTracker(input) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, funder: funder.publicKey(), surveyDataUnitTracker: surveyDataUnitTracker.publicKey(), sduMint, resourceMint, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder, surveyDataUnitTracker], }, ]; } /** * Update a Survey Data Unit Tracker * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param surveyDataUnitTracker - the survey data unit tracker * @param gameId - the SAGE game id * @param input - instruction input params * @param mints - the mints for the survey data units and the resource consumed when scanning * @param mints.sduMint - the mint for the survey data units * @param mints.resourceMint - the mint for the resource consumed when scanning for survey data units * @returns InstructionReturn */ static updateSurveyDataUnitTracker( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, surveyDataUnitTracker: PublicKey, gameId: PublicKey, input: UpdateSurveyDataUnitTrackerParams, mints?: { sduMint: PublicKey; resourceMint: PublicKey; }, ): InstructionReturn { const remainingAccounts = mints ? [ { pubkey: mints.sduMint, isSigner: false, isWritable: false, }, { pubkey: mints.resourceMint, isSigner: false, isWritable: false, }, ] : []; const params: UpdateSurveyDataUnitTrackerInput = { ...input, coordinatesRange: input.coordinatesRange ?? null, cssCoordinates: input.cssCoordinates ?? null, originCoordinates: input.originCoordinates ?? null, cssMaxDistance: input.cssMaxDistance ?? null, originMaxDistance: input.originMaxDistance ?? null, distanceWeighting: input.distanceWeighting ?? null, tMax: input.tMax ?? null, xMul: input.xMul ?? null, yMul: input.yMul ?? null, zMul: input.zMul ?? null, scanChanceRegenPeriod: input.scanChanceRegenPeriod ?? null, sduMaxPerSector: input.sduMaxPerSector ?? null, }; return async () => [ { instruction: await program.methods .updateSurveyDataUnitTracker(params) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, surveyDataUnitTracker, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [key], }, ]; } /** * Drain a Survey Data Units token account * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param fundsTo - the entity that should receive rent refunds * @param surveyDataUnitTracker - the survey data unit tracker * @param gameId - the SAGE game id * @param tokenFrom - The mine item token bank to drain * @param tokenTo - Where to send tokens from the bank * @param input - instruction input params * @returns InstructionReturn */ static drainSurveyDataUnitsBank( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, fundsTo: PublicKey | 'funder', surveyDataUnitTracker: PublicKey, gameId: PublicKey, tokenFrom: PublicKey, tokenTo: PublicKey, input: DrainSurveyDataUnitsBankInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .drainSurveyDataUnitsBank(input) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, fundsTo: fundsTo === 'funder' ? funder.publicKey() : fundsTo, surveyDataUnitTracker, surveyDataUnitTrackerSigner: this.findSignerAddress( program, surveyDataUnitTracker, )[0], tokenFrom, tokenTo, tokenProgram: TOKEN_PROGRAM_ID, }) .instruction(), signers: [key], }, ]; } /** * Deregister a Survey Data Unit Tracker * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param surveyDataUnitTracker - the new survey data unit tracker * @param fundsTo - the entity that should receive rent refunds * @param gameId - the SAGE game id * @param input - instruction input params * @returns InstructionReturn */ static deregisterSurveyDataUnitTracker( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, surveyDataUnitTracker: PublicKey, fundsTo: PublicKey | 'funder', gameId: PublicKey, input: DeregisterSurveyDataUnitTrackerInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .deregisterSurveyDataUnitTracker(input) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, surveyDataUnitTracker, fundsTo: fundsTo === 'funder' ? funder.publicKey() : fundsTo, }) .instruction(), signers: [key], }, ]; } /** * Scan For Survey Data Units * @param program - SAGE program * @param cargoProgram - cargo program * @param pointsProgram - points program * @param key - the key authorized to run this instruction * @param playerProfile - the profile with the required permissions for the instruction * @param profileFaction - the profile's faction * @param fleet - the fleet * @param sector - the sector in which the fleet is located * @param surveyDataUnitTracker - the new survey data unit tracker * @param cargoHold - the fleet's cargo hold cargo pod * @param sduCargoType - the SDU cargo type * @param resourceCargoType - the cargo type of the resource consumed when scanning * @param cargoStatsDefinition - the cargo stats definition * @param sduTokenFrom - the source token SDU account, owned by the survey data unit tracker's signer (must match SDU mint in surveyDataUnitTracker) * @param sduTokenTo - the destination SDU token account, owned by `cargoHold` (must match SDU mint in surveyDataUnitTracker) * @param resourceTokenFrom - the token account, owned by `cargoHold`, of the resource consumed when scanning * @param resourceMint - the token mint of the resource consumed when scanning * @param dataRunningXpUserAccount - the user account for Data Running XP * @param dataRunningXpCategory - the Data Running XP Points Category Account * @param dataRunningXpModifier - the Data Running XP modifier * @param councilRankXpUserAccount - the user account for Council Rank XP * @param councilRankXpCategory - the Council Rank XP Points Category Account * @param councilRankXpModifier - the Council Rank XP modifier * @param gameId - the SAGE game id * @param gameState - the game state * @param input - instruction input params * @returns InstructionReturn */ static scanForSurveyDataUnits( program: SageIDLProgram, cargoProgram: CargoIDLProgram, pointsProgram: PointsIDLProgram, key: AsyncSigner, playerProfile: PublicKey, profileFaction: PublicKey, fleet: PublicKey, sector: PublicKey, surveyDataUnitTracker: PublicKey, cargoHold: PublicKey, sduCargoType: PublicKey, resourceCargoType: PublicKey, cargoStatsDefinition: PublicKey, sduTokenFrom: PublicKey, sduTokenTo: PublicKey, resourceTokenFrom: PublicKey, resourceMint: PublicKey, dataRunningXpUserAccount: PublicKey, dataRunningXpCategory: PublicKey, dataRunningXpModifier: PublicKey, councilRankXpUserAccount: PublicKey, councilRankXpCategory: PublicKey, councilRankXpModifier: PublicKey, gameId: PublicKey, gameState: PublicKey, input: ScanForSurveyDataUnitsInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .scanForSurveyDataUnits(input) .accountsStrict({ gameAccountsFleetAndOwner: { gameFleetAndOwner: { fleetAndOwner: { fleet, owningProfile: playerProfile, owningProfileFaction: profileFaction, key: key.publicKey(), }, gameId, }, gameState, }, sector, surveyDataUnitTracker, surveyDataUnitTrackerSigner: this.findSignerAddress( program, surveyDataUnitTracker, )[0], cargoHold, sduCargoType, resourceCargoType, cargoStatsDefinition, sduTokenFrom, sduTokenTo, resourceTokenFrom, resourceMint, dataRunningXpAccounts: { userPointsAccount: dataRunningXpUserAccount, pointsCategory: dataRunningXpCategory, pointsModifierAccount: dataRunningXpModifier, }, councilRankXpAccounts: { userPointsAccount: councilRankXpUserAccount, pointsCategory: councilRankXpCategory, pointsModifierAccount: councilRankXpModifier, }, progressionConfig: ProgressionConfig.findAddress( program, gameId, )[0], pointsProgram: pointsProgram.programId, cargoProgram: cargoProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, recentSlothashes: SYSVAR_SLOT_HASHES_PUBKEY, instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, }) .instruction(), signers: [key], }, ]; } static decodeData( account: KeyedAccountInfo, program: SageIDLProgram, ): DecodedAccountData { return decodeAccount(account, program, SurveyDataUnitTracker); } }