From b402500cdf1e3cc8d4b4f929d37aeaf2a2fae418 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 9 Sep 2024 23:31:02 +0200 Subject: [PATCH] fix: optimize fleet creation --- src/lib/refill-player.ts | 34 +++-- src/lib/telegram/commands/withdraw.ts | 25 ++-- src/main/basedbot/basedbot.ts | 54 +++++++- .../fleet-strategies/atlasnet-fc-strategy.ts | 13 +- .../fleet-strategies/get-fleet-strategy.ts | 14 +- src/main/basedbot/lib/sage/act/load-cargo.ts | 1 - src/service/fleet/refill-fleet.ts | 4 +- src/service/gm/market.ts | 56 ++++---- src/service/sol/send-and-confirm-tx.ts | 131 +++++++++++++----- 9 files changed, 232 insertions(+), 100 deletions(-) diff --git a/src/lib/refill-player.ts b/src/lib/refill-player.ts index 6fb8297f..35f93d04 100644 --- a/src/lib/refill-player.ts +++ b/src/lib/refill-player.ts @@ -82,19 +82,25 @@ export const refillPlayer = async ( refill.amount, ) - return Refill.create({ - signature: tx, - walletPublicKey: wallet.publicKey, - fleet: shipName, - preBalance: playerBalance.toNumber(), - postBalance: playerBalance.sub(refill.price).toNumber(), - tip: wallet.tip, - price: refill.price.toNumber(), - food: refill.amount.food.toNumber(), - tool: refill.amount.tool.toNumber(), - fuel: refill.amount.fuel.toNumber(), - ammo: refill.amount.ammo.toNumber(), - }).save() + return Promise.all( + tx.map((signature) => { + return Refill.create({ + signature, + walletPublicKey: wallet.publicKey, + fleet: shipName, + preBalance: playerBalance.toNumber(), + postBalance: playerBalance + .sub(refill.price) + .toNumber(), + tip: wallet.tip, + price: refill.price.toNumber(), + food: refill.amount.food.toNumber(), + tool: refill.amount.tool.toNumber(), + fuel: refill.amount.fuel.toNumber(), + ammo: refill.amount.ammo.toNumber(), + }).save() + }), + ) } catch (e) { Sentry.captureException(e) logger.error( @@ -132,5 +138,5 @@ export const refillPlayer = async ( await Wallet.update({ publicKey: wallet.publicKey }, { nextRefill }) - return fleetRefills.filter((f): f is Refill => f !== null) + return fleetRefills.filter((f): f is Refill[] => f !== null).flat() } diff --git a/src/lib/telegram/commands/withdraw.ts b/src/lib/telegram/commands/withdraw.ts index 049ffe8a..8f7c5dd6 100644 --- a/src/lib/telegram/commands/withdraw.ts +++ b/src/lib/telegram/commands/withdraw.ts @@ -40,22 +40,27 @@ export const withdraw = (bot: Telegraf): void => { await ctx.reply( `Sending ${withdrawAmount} ATLAS to ${ctx.user.publicKey}`, ) - const signature = await sendAtlas( + const signatures = await sendAtlas( new PublicKey(ctx.user.publicKey), withdrawAmount.toNumber(), ) - await ctx.reply(`https://solscan.io/tx/${signature}`) const amount = -withdrawAmount - await Transaction.create({ - wallet, - amount, - signature, - time: dayjs().toDate(), - originalAmount: amount, - resource: 'ATLAS', - }).save() + await Promise.all( + signatures.map(async (signature) => { + await ctx.reply(`https://solscan.io/tx/${signature}`) + + return Transaction.create({ + wallet, + amount, + signature, + time: dayjs().toDate(), + originalAmount: amount, + resource: 'ATLAS', + }).save() + }), + ) }) }) } diff --git a/src/main/basedbot/basedbot.ts b/src/main/basedbot/basedbot.ts index 4da06994..482f66b7 100644 --- a/src/main/basedbot/basedbot.ts +++ b/src/main/basedbot/basedbot.ts @@ -3,8 +3,17 @@ import { TOKEN_PROGRAM_ID, } from '@solana/spl-token' import { PublicKey } from '@solana/web3.js' -import { getParsedTokenAccountsByOwner } from '@staratlas/data-source' -import { Fleet, Game } from '@staratlas/sage' +import { + getParsedTokenAccountsByOwner, + ixReturnsToIxs, +} from '@staratlas/data-source' +import { + Fleet, + Game, + getCleanPodsByStarbasePlayerAccounts, + getPodCleanupInstructions, + Starbase, +} from '@staratlas/sage' import BN from 'bn.js' import { Sentry } from '../../sentry' @@ -12,16 +21,20 @@ import { Sentry } from '../../sentry' import { logger } from '../../logger' import { sleep } from '../../service/sleep' import { connection } from '../../service/sol' +import { sendAndConfirmInstructions } from '../../service/sol/send-and-confirm-tx' import { keyPair } from '../../service/wallet' import { getFleetStrategy } from './fleet-strategies/get-fleet-strategy' import { StrategyConfig } from './fleet-strategies/strategy-config' import { createInfoStrategy } from './fsm/info' +import { programs } from './lib/programs' import { createFleet, FleetShip } from './lib/sage/act/create-fleet' import { depositCargo } from './lib/sage/act/deposit-cargo' import { ensureShips } from './lib/sage/act/deposit-ship' +import { getCargoStatsDefinition } from './lib/sage/state/cargo-stats-definition' import { sageGame } from './lib/sage/state/game' import { settleFleet } from './lib/sage/state/settle-fleet' +import { getStarbasePlayer } from './lib/sage/state/starbase-player' import { getPlayerContext, Player } from './lib/sage/state/user-account' import { FleetInfo, @@ -29,8 +42,8 @@ import { getUserFleets, } from './lib/sage/state/user-fleets' import { getMapContext, WorldMap } from './lib/sage/state/world-map' -import { getName } from './lib/sage/util' // eslint-disable-next-line import/max-dependencies +import { getName } from './lib/sage/util' // eslint-disable-next-line require-await export const create = async (): Promise => { @@ -170,6 +183,39 @@ const ensureFleets = async ( ) } +const cleanupPods = async (player: Player, game: Game, starbase: Starbase) => { + const starbasePlayer = await getStarbasePlayer(player, starbase, programs) + const podCleanup = await getCleanPodsByStarbasePlayerAccounts( + connection, + programs.cargo, + starbasePlayer.key, + ) + const [cargoStatsDefinition] = await getCargoStatsDefinition() + + if (!podCleanup) { + logger.info('Nothing to Clean up') + + return + } + + const ixs = getPodCleanupInstructions( + podCleanup, + programs.sage, + programs.cargo, + starbasePlayer.key, + starbase.key, + player.profile.key, + player.profileFaction.key, + cargoStatsDefinition.key, + game.key, + game.data.gameState, + player.signer, + 0, + ) + + await sendAndConfirmInstructions(await ixReturnsToIxs(ixs, player.signer)) +} + const basedbot = async (botConfig: BotConfig) => { logger.info( '-------------------------------------------------------------------------------------', @@ -183,6 +229,8 @@ const basedbot = async (botConfig: BotConfig) => { fleets.map((f) => getFleetInfo(f, player, map)), ) + await cleanupPods(player, game, player.homeStarbase) + await Promise.all([ importR4(player, game), ensureFleets(player, game, fleets, botConfig.fleetStrategies), diff --git a/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts b/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts index 7d4dafc8..65cd270d 100644 --- a/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts +++ b/src/main/basedbot/fleet-strategies/atlasnet-fc-strategy.ts @@ -53,21 +53,26 @@ const getRandomFleetForFaction = (faction: Faction): FleetShips => { }, ] default: - throw new Error('Unknwon Faction') + throw new Error('Unknown Faction') } } export const atlasnetFcStrategy = (count: number) => - (map: WorldMap, player: Player, game: Game): StrategyConfig => { + ( + map: WorldMap, + player: Player, + game: Game, + seed: string = 'basedbot', + ): StrategyConfig => { const strategyMap: StrategyMap = makeStrategyMap() - const chance = new Chance() + const chance = new Chance(seed) const sectors = galaxySectorsData() .filter((sector) => sector.closestFaction === player.faction) .sort((a, b) => a.name.localeCompare(b.name)) for (let i = 0; i < count; i++) { - strategyMap.set(`${chance.animal()} Fleet [${i}]`, { + strategyMap.set(`${chance.animal()} Fleet`, { fleet: getRandomFleetForFaction(player.faction), strategy: createMiningStrategy( mine( diff --git a/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts b/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts index 7589b148..99a3f668 100644 --- a/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts +++ b/src/main/basedbot/fleet-strategies/get-fleet-strategy.ts @@ -20,16 +20,16 @@ export const getFleetStrategy = ( case 'AePY3wEoUFcFuXeUU9X26YK6tNKQMZovBgvY54LK2B8N': return mainnetLuStrategy(map, player, game) case 'CgHvzwGbwWv3CwLTvEgeqSKeD8EwMdTfiiCG3dFrKVVC': - // return atlasnetFcStrategy(5)(map, player, game) - return disbandAllStrategy(map, player, game) + return atlasnetFcStrategy(10)(map, player, game, 'mud') + // return disbandAllStrategy(map, player, game) case '9KBrgWVjsmdZ3YEjcsa3wrbbJREgZgS7vDbgoz2aHaNm': - // return atlasnetFcStrategy(5)(map, player, game) - return disbandAllStrategy(map, player, game) + return atlasnetFcStrategy(10)(map, player, game, 'ustur') + // return disbandAllStrategy(map, player, game) case 'FUwHSqujzcPD44SDZYJXuk73NbkEyYQwcLMioHhpjbx2': - // return atlasnetFcStrategy(5)(map, player, game) - return disbandAllStrategy(map, player, game) + return atlasnetFcStrategy(10)(map, player, game, 'oni') + // return disbandAllStrategy(map, player, game) case '34ghznSJCYEMrS1aC55UYZZUuxfuurA9441aKnigmYyz': - // return atlasnetFcStrategy(5)(map, player, game) + // return atlasnetFcStrategy(10)(map, player, game, 'le.local') return disbandAllStrategy(map, player, game) default: throw new Error('Unknown strategy') diff --git a/src/main/basedbot/lib/sage/act/load-cargo.ts b/src/main/basedbot/lib/sage/act/load-cargo.ts index e2d2452f..dc2fbcd5 100644 --- a/src/main/basedbot/lib/sage/act/load-cargo.ts +++ b/src/main/basedbot/lib/sage/act/load-cargo.ts @@ -74,7 +74,6 @@ export const loadCargo = async ( cargoTokenAccount.delegatedAmount.toString(), ) - if (fuelAmountAtOrigin.lt(new BN(amount))) { throw new Error('Not enough cargo available at origin Starbase') } diff --git a/src/service/fleet/refill-fleet.ts b/src/service/fleet/refill-fleet.ts index ecd721a5..e44cf840 100644 --- a/src/service/fleet/refill-fleet.ts +++ b/src/service/fleet/refill-fleet.ts @@ -17,7 +17,7 @@ export const refillFleet = async ( player: PublicKey, fleetUnit: ShipStakingInfo, amounts: Amounts, -): Promise => { +): Promise => { const [foodAccount, fuelAccount, ammoAccount, toolAccount] = await Promise.all([ getAccount(keyPair.publicKey, resource.food), @@ -93,5 +93,5 @@ export const refillFleet = async ( ) } - return await sendAndConfirmInstructions(instructions) + return sendAndConfirmInstructions(instructions) } diff --git a/src/service/gm/market.ts b/src/service/gm/market.ts index c4fd708d..65cda71f 100644 --- a/src/service/gm/market.ts +++ b/src/service/gm/market.ts @@ -1,14 +1,10 @@ import { createTransferCheckedInstruction, + getAssociatedTokenAddressSync, getOrCreateAssociatedTokenAccount, } from '@solana/spl-token' import { Keypair, PublicKey } from '@solana/web3.js' -import { - GmClientService, - GmOrderbookService, - Order, - getAssociatedTokenAddress, -} from '@staratlas/factory' +import { GmClientService, GmOrderbookService, Order } from '@staratlas/factory' import Big from 'big.js' import { Sentry } from '../../sentry' @@ -62,15 +58,19 @@ export const getBalanceAtlas = async (pubKey: PublicKey): Promise => { } } -export const sendAtlas = async ( +export const sendAtlas = ( receiver: PublicKey, amount: number, -): Promise => { +): Promise => { const instructions = [ createTransferCheckedInstruction( - await getAssociatedTokenAddress(keyPair.publicKey, resource.atlas), + getAssociatedTokenAddressSync( + resource.atlas, + keyPair.publicKey, + true, + ), resource.atlas, - await getAssociatedTokenAddress(receiver, resource.atlas), + getAssociatedTokenAddressSync(resource.atlas, receiver, true), keyPair.publicKey, Big(amount).mul(100000000).toNumber(), 8, @@ -78,7 +78,7 @@ export const sendAtlas = async ( ), ] - return await sendAndConfirmInstructions(instructions) + return sendAndConfirmInstructions(instructions) } export const getBalanceMarket = async ( @@ -115,7 +115,7 @@ export const initOrderBook = async (): Promise => { export const buyResource = async ( res: PublicKey, amount: Big, -): Promise => { +): Promise => { const orders = gmOrderbookService .getSellOrdersByCurrencyAndItem( resource.atlas.toString(), @@ -135,23 +135,25 @@ export const buyResource = async ( logger.info(`Buying ${amount.toFixed(0)} ${res} for ${order.uiPrice} each`) - return await sendAndConfirmInstructions(exchangeTx.transaction.instructions) + return sendAndConfirmInstructions(exchangeTx.transaction.instructions) } export const buyResources = async (amount: Amounts): Promise => { - const res = await Promise.all([ - amount.food.gt(0) - ? buyResource(resource.food, amount.food) - : Promise.resolve(''), - amount.ammo.gt(0) - ? buyResource(resource.ammo, amount.ammo) - : Promise.resolve(''), - amount.fuel.gt(0) - ? buyResource(resource.fuel, amount.fuel) - : Promise.resolve(''), - amount.tool.gt(0) - ? buyResource(resource.tool, amount.tool) - : Promise.resolve(''), - ]) + const res = ( + await Promise.all([ + amount.food.gt(0) + ? buyResource(resource.food, amount.food) + : Promise.resolve(''), + amount.ammo.gt(0) + ? buyResource(resource.ammo, amount.ammo) + : Promise.resolve(''), + amount.fuel.gt(0) + ? buyResource(resource.fuel, amount.fuel) + : Promise.resolve(''), + amount.tool.gt(0) + ? buyResource(resource.tool, amount.tool) + : Promise.resolve(''), + ]) + ).flat() return res.filter((r) => r !== '') } diff --git a/src/service/sol/send-and-confirm-tx.ts b/src/service/sol/send-and-confirm-tx.ts index 66b680c8..aa859b56 100644 --- a/src/service/sol/send-and-confirm-tx.ts +++ b/src/service/sol/send-and-confirm-tx.ts @@ -1,4 +1,5 @@ import { + PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction, @@ -11,6 +12,11 @@ import { connection } from './const' import { createComputeUnitInstruction } from './priority-fee/compute-unit-instruction' import { createPriorityFeeInstruction } from './priority-fee/priority-fee-instruction' +// Constants for Solana transaction size limits +const MAX_TRANSACTION_SIZE = 1232 // Maximum size of a transaction in bytes +const TRANSACTION_HEADER_SIZE = 100 // Approximate size of transaction header, adjust if needed +const SIGNATURE_SIZE = 64 // Size of a signature in bytes + const sleep = (ms: number) => // eslint-disable-next-line promise/avoid-new new Promise((resolve) => { @@ -109,44 +115,105 @@ const createAndSignTransaction = ( return transaction } -export const sendAndConfirmInstructions = async ( +const getInstructionSize = (instructions: TransactionInstruction[]): number => { + const messageV0 = new TransactionMessage({ + payerKey: keyPair.publicKey, + recentBlockhash: PublicKey.default.toBase58(), + instructions, + }).compileToV0Message() + + // Serialize the message and return its length + return new VersionedTransaction(messageV0).serialize().byteLength + // return messageV0.serialize().length +} +const getOptimalInstructionChunk = ( instructions: TransactionInstruction[], -): Promise => { - const maxRetries = 10 + maxSize: number, +): TransactionInstruction[] => { + for (let i = 0; i < instructions.length; ++i) { + const instructionSize = getInstructionSize(instructions.slice(0, i + 1)) - /* eslint-disable no-await-in-loop */ - for (let i = 0; i < maxRetries; ++i) { - const [ - latestBlockHash, - priorityFeeInstruction, - computeUnitsInstruction, - ] = await Promise.all([ - connection.getLatestBlockhash(), - createPriorityFeeInstruction(), - createComputeUnitInstruction(instructions), - ]) - - const txInstructions = [ - computeUnitsInstruction, - priorityFeeInstruction, - ...instructions, - ] - - const transaction = createAndSignTransaction( - txInstructions, - latestBlockHash.blockhash, + logger.info( + `Transaction with ${i + 1} instructions has size ${instructionSize}`, ) - try { - return await sendAndConfirmTx(transaction, latestBlockHash) - } catch (e) { - const message = (e as any).message as string + if (instructionSize > maxSize) { + return instructions.slice(0, i) + } + } + + return instructions +} - logger.error( - `Transaction failed: ${message}, retrying... (${i + 1}/${maxRetries})`, +export const sendAndConfirmInstructions = async ( + instructionArray: TransactionInstruction[], +): Promise => { + const maxRetries = 10 + let instructions = instructionArray + const results: string[] = [] + + while (instructions.length > 0) { + const availableSize = + MAX_TRANSACTION_SIZE - TRANSACTION_HEADER_SIZE - SIGNATURE_SIZE + + const chunk = getOptimalInstructionChunk(instructions, availableSize) + + /* eslint-disable no-await-in-loop */ + for (let i = 0; i < maxRetries; ++i) { + const [ + latestBlockHash, + priorityFeeInstruction, + computeUnitsInstruction, + ] = await Promise.all([ + connection.getLatestBlockhash(), + createPriorityFeeInstruction(), + createComputeUnitInstruction(chunk), + ]) + + const txInstructions = [ + computeUnitsInstruction, + priorityFeeInstruction, + ...chunk, + ] + + const transaction = createAndSignTransaction( + txInstructions, + latestBlockHash.blockhash, ) + + const rawTransaction = transaction.serialize() + + if (rawTransaction.length > MAX_TRANSACTION_SIZE) { + throw new Error( + `Transaction too large: ${rawTransaction.length} bytes`, + ) + } + + try { + const result = await sendAndConfirmTx( + transaction, + latestBlockHash, + ) + + results.push(result) + instructions = instructions.slice(chunk.length) + break // Exit retry loop if successful + } catch (e) { + const message = (e as any).message as string + + logger.error( + `Transaction failed: ${message}, retrying... (${i + 1}/${maxRetries})`, + ) + + if (i === maxRetries - 1) { + throw new Error( + `Transaction failed after ${maxRetries} attempts`, + ) + } + } } + /* eslint-enable no-await-in-loop */ } - /* eslint-enable no-await-in-loop */ - throw new Error(`Transaction failed after ${maxRetries} attempts`) + + return results }