import { MPL_BUBBLEGUM_PROGRAM_ID, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, } from '@metaplex-foundation/mpl-bubblegum'; import { toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { AccountMeta, KeyedAccountInfo, PublicKey, SystemProgram, } from '@solana/web3.js'; import { Account, AccountStatic, AsyncSigner, DecodedAccountData, InstructionReturn, decodeAccount, staticImplements, } from '@staratlas/data-source'; import { AddShipEscrowInput, RemoveShipEscrowInput, SageIDL, SageIDLProgram, SagePlayerProfileAccount, UpdateShipEscrowInput, } from './constants'; import { SageCrewConfig } from './sageCrewConfig'; import { normalizePublicKey } from './utils'; /** * Check if two `SagePlayerProfileAccount` instances are equal * @param sppData1 - first SagePlayerProfileAccount * @param sppData2 - second SagePlayerProfileAccount * @returns boolean */ export function sppDataEquals( sppData1: SagePlayerProfileAccount, sppData2: SagePlayerProfileAccount, ): boolean { return ( sppData1.version === sppData2.version && sppData1.playerProfile.equals(sppData2.playerProfile) && sppData1.gameId.equals(sppData2.gameId) && sppData1.bump === sppData2.bump ); } export interface CrewTransferInput { merkleTree: PublicKey; creatorHash: number[] | PublicKey; root: number[] | PublicKey; dataHash: number[] | PublicKey; // The crew cNFT index (leaf index) leafIndex: number; // The proof keys for the leaf proof: PublicKey[]; } export interface AddCrewToGameInput { // inputs required to transfer crew cNFTs items: CrewTransferInput[]; } /** * Gets the transfer ix data from the CrewTransferInput object * @param input - CrewTransferInput * @returns Returns the data hash, leaf index, and proof count */ export function crewTransferIxData(input: CrewTransferInput): { dataHash: number[]; leafIndex: number; proofCount: number; } { const dataHash = normalizePublicKey(input.dataHash).toBytes(); return { dataHash: Array.from(dataHash), leafIndex: input.leafIndex, proofCount: input.proof.length, }; } /** * Gets the remaining accounts from the CrewTransferInput object * @param input - CrewTransferInput * @returns The list of remaining accounts */ export function crewTransferRemainingAccounts( input: CrewTransferInput, ): AccountMeta[] { const treeConfig = PublicKey.findProgramAddressSync( [input.merkleTree.toBuffer()], toWeb3JsPublicKey(MPL_BUBBLEGUM_PROGRAM_ID), )[0]; const remainingAccounts: AccountMeta[] = [ { pubkey: input.merkleTree, isSigner: false, isWritable: true, }, { pubkey: treeConfig, isSigner: false, isWritable: false, }, { pubkey: normalizePublicKey(input.root), isSigner: false, isWritable: false, }, { pubkey: normalizePublicKey(input.creatorHash), isSigner: false, isWritable: false, }, ]; for (const proofKey of input.proof) { remainingAccounts.push({ pubkey: proofKey, isSigner: false, isWritable: false, }); } return remainingAccounts; } export interface RemoveCrewFromGameInput extends AddCrewToGameInput { keyIndex: number; } @staticImplements>() export class SagePlayerProfile implements Account { static readonly ACCOUNT_NAME: NonNullable< SageIDL['accounts'] >[number]['name'] = 'sagePlayerProfile'; static readonly MIN_DATA_SIZE: number = 8 + // discriminator 1 + // version 32 + // playerProfile 32 + // gameId 1; // bump constructor( private _data: SagePlayerProfileAccount, private _key: PublicKey, ) {} get data(): Readonly { return this._data; } get key(): PublicKey { return this._key; } /** * Find the SagePlayerProfile account address * @param program - SAGE program * @param playerProfile - the player's profile * @param gameId - the SAGE game id * @returns The PDA and bump respectively */ static findAddress( program: SageIDLProgram, playerProfile: PublicKey, gameId: PublicKey, ): [PublicKey, number] { return PublicKey.findProgramAddressSync( [ Buffer.from('sage_player_profile'), playerProfile.toBuffer(), gameId.toBuffer(), ], program.programId, ); } /** * Register the SAGE player profile * @param program - SAGE program * @param profile - the profile with the required permissions for the instruction * @param gameId - the SAGE game id * @param gameState - the game state account * @returns InstructionReturn */ static registerSagePlayerProfile( program: SageIDLProgram, profile: PublicKey, gameId: PublicKey, gameState: PublicKey, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .registerSagePlayerProfile() .accountsStrict({ profile, funder: funder.publicKey(), sagePlayerProfile: this.findAddress(program, profile, gameId)[0], gameAccounts: { gameId, gameState, }, systemProgram: SystemProgram.programId, }) .instruction(), signers: [funder], }, ]; } /** * Add a `Ship` to the game by transferring it to escrow PDA * @param program - SAGE program * @param playerProfile - the profile with the required permissions for the instruction * @param profileFaction - the profile's faction * @param sagePlayerProfile - the SAGE player profile * @param signerShipOrigin - the owner of the ship token account * @param originTokenAccount - the source ship token account, owned by `signerShipOrigin` * @param ship - the ship * @param shipEscrowTokenAccount - the ship escrow token account (destination), owned by `sagePlayerProfile` * @param starbasePlayer - the Starbase player * @param starbase - the Starbase * @param gameId - the SAGE game id * @param gameState - the game state account * @param input - the instruction input params * @returns InstructionReturn */ static addShipEscrow( program: SageIDLProgram, playerProfile: PublicKey, profileFaction: PublicKey, sagePlayerProfile: PublicKey, signerShipOrigin: AsyncSigner, originTokenAccount: PublicKey, ship: PublicKey, shipEscrowTokenAccount: PublicKey, starbasePlayer: PublicKey, starbase: PublicKey, gameId: PublicKey, gameState: PublicKey, input: AddShipEscrowInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .addShipEscrow(input) .accountsStrict({ funder: funder.publicKey(), sagePlayerProfile, originTokenAccount, ship, shipEscrowTokenAccount, starbaseAndStarbasePlayer: { starbase, starbasePlayer, }, gameAccountsAndProfile: { gameAndProfileAndFaction: { key: signerShipOrigin.publicKey(), profile: playerProfile, profileFaction, gameId, }, gameState, }, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .instruction(), signers: [signerShipOrigin, funder], }, ]; } /** * Update `Ship` in escrow * @param program - SAGE program * @param oldShip - the ship that is being replaced * @param next - the value of the `next` field on `oldShip` * @param starbasePlayer - the Starbase player * @param starbase - the Starbase * @param gameId - the SAGE game id * @param gameState - the game state account * @param input - the instruction input params * @returns InstructionReturn */ static updateShipEscrow( program: SageIDLProgram, oldShip: PublicKey, next: PublicKey, starbasePlayer: PublicKey, starbase: PublicKey, gameId: PublicKey, gameState: PublicKey, input: UpdateShipEscrowInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .updateShipEscrow(input) .accountsStrict({ oldShip, next, starbaseAndStarbasePlayer: { starbase, starbasePlayer, }, gameAccounts: { gameId, gameState, }, }) .instruction(), signers: [], }, ]; } /** * Remove a ship from the game * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the player's profile with the required permissions for the instruction * @param profileFaction - the profile's faction * @param sagePlayerProfile - the SAGE player profile * @param destinationTokenAccount - the destination token account, any owner is valid * @param ship - the ship * @param shipEscrowTokenAccount - the ship escrow token account (source), owned by `sagePlayerProfile` * @param starbasePlayer - the Starbase player * @param starbase - the Starbase * @param gameId - the SAGE game id * @param gameState - the game state account * @param input - the instruction input params * @returns InstructionReturn */ static removeShipEscrow( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, profileFaction: PublicKey, sagePlayerProfile: PublicKey, destinationTokenAccount: PublicKey, ship: PublicKey, shipEscrowTokenAccount: PublicKey, starbasePlayer: PublicKey, starbase: PublicKey, gameId: PublicKey, gameState: PublicKey, input: RemoveShipEscrowInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .removeShipEscrow(input) .accountsStrict({ gameAccountsAndProfile: { gameAndProfileAndFaction: { key: key.publicKey(), profile, profileFaction, gameId, }, gameState, }, fundsTo: funder.publicKey(), sagePlayerProfile: sagePlayerProfile, destinationTokenAccount: destinationTokenAccount, ship: ship, shipEscrowTokenAccount: shipEscrowTokenAccount, starbaseAndStarbasePlayer: { starbase, starbasePlayer, }, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key], }, ]; } /** * Remove an invalid ship from the game * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the player's profile with the required permissions for the instruction * @param profileFaction - the profile's faction * @param sagePlayerProfile - the SAGE player profile * @param destinationTokenAccount - the destination token account, any owner is valid * @param ship - the ship * @param shipEscrowTokenAccount - the ship escrow token account (destination), owned by `sagePlayerProfile` * @param starbasePlayer - the Starbase player * @param starbase - the Starbase * @param gameId - the SAGE game id * @param gameState - the game state account * @param input - the instruction input params * @returns InstructionReturn */ static removeInvalidShipEscrow( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, profileFaction: PublicKey, sagePlayerProfile: PublicKey, destinationTokenAccount: PublicKey, ship: PublicKey, shipEscrowTokenAccount: PublicKey, starbasePlayer: PublicKey, starbase: PublicKey, gameId: PublicKey, gameState: PublicKey, input: RemoveShipEscrowInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .removeInvalidShipEscrow(input) .accountsStrict({ gameAccountsAndProfile: { gameAndProfileAndFaction: { key: key.publicKey(), profile, profileFaction, gameId, }, gameState, }, fundsTo: funder.publicKey(), sagePlayerProfile, destinationTokenAccount, ship, shipEscrowTokenAccount, starbaseAndStarbasePlayer: { starbase, starbasePlayer, }, tokenProgram: TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key], }, ]; } /** * Add a crew member to the game after the "pre add" period * @param program - SAGE program * @param playerProfile - the profile with the required permissions for the instruction * @param profileFaction - the profile's faction * @param crewOwner - the account that owns the crew cNFT * @param starbasePlayer - the Starbase player * @param starbase - the Starbase * @param crewProgramConfig - the crew program config account * @param gameId - the SAGE game id * @param input - the instruction input params * @param crewDelegate - the delegate as defined in the crew cNFT * @returns InstructionReturn */ static addCrewToGame( program: SageIDLProgram, playerProfile: PublicKey, profileFaction: PublicKey, crewOwner: AsyncSigner, starbasePlayer: PublicKey, starbase: PublicKey, crewProgramConfig: PublicKey, gameId: PublicKey, input: AddCrewToGameInput, crewDelegate?: PublicKey, ): InstructionReturn { const remainingAccounts: AccountMeta[] = input.items .map(crewTransferRemainingAccounts) .flat(); return async () => [ { instruction: await program.methods .addCrewToGame({ items: input.items.map(crewTransferIxData), }) .accountsStrict({ sagePlayerProfile: this.findAddress( program, playerProfile, gameId, )[0], starbaseAndStarbasePlayer: { starbase, starbasePlayer, }, sageCrewConfig: SageCrewConfig.findAddress(program, gameId)[0], gameAndProfileAndFaction: { key: crewOwner.publicKey(), profile: playerProfile, profileFaction, gameId, }, crewConfig: crewProgramConfig, crewOwner: crewOwner.publicKey(), crewDelegate: crewDelegate ?? crewOwner.publicKey(), compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, logWrapper: SPL_NOOP_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [crewOwner], }, ]; } /** * Remove a crew member from the game after the "pre add" period * @param program - SAGE 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 newCrewOwner - the account that should own the withdrawn crew cNFT * @param starbasePlayer - the Starbase player * @param starbase - the Starbase * @param crewProgramConfig - the crew program config account * @param gameId - the SAGE game id * @param input - the instruction input params * @param crewDelegate - the delegate as defined in the crew cNFT * @returns InstructionReturn */ static removeCrewFromGame( program: SageIDLProgram, key: AsyncSigner, playerProfile: PublicKey, profileFaction: PublicKey, newCrewOwner: PublicKey, starbasePlayer: PublicKey, starbase: PublicKey, crewProgramConfig: PublicKey, gameId: PublicKey, input: RemoveCrewFromGameInput, crewDelegate?: PublicKey, ): InstructionReturn { if (input.items.length === 0) { throw new Error('No crew members being removed!'); } const remainingAccounts: AccountMeta[] = input.items .map(crewTransferRemainingAccounts) .flat(); const sagePlayerProfile = this.findAddress( program, playerProfile, gameId, )[0]; return async () => [ { instruction: await program.methods .removeCrewFromGame({ keyIndex: input.keyIndex, items: input.items.map(crewTransferIxData), }) .accountsStrict({ sagePlayerProfile, starbaseAndStarbasePlayer: { starbase, starbasePlayer, }, sageCrewConfig: SageCrewConfig.findAddress(program, gameId)[0], gameAndProfileAndFaction: { key: key.publicKey(), profile: playerProfile, profileFaction, gameId, }, crewConfig: crewProgramConfig, newCrewOwner, crewDelegate: crewDelegate ?? sagePlayerProfile, compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, logWrapper: SPL_NOOP_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [key], }, ]; } static decodeData( account: KeyedAccountInfo, program: SageIDLProgram, ): DecodedAccountData { return decodeAccount(account, program, SagePlayerProfile); } }